rpc: add support for Yosys RPC protocol.
authorwhitequark <cz@m-labs.hk>
Fri, 27 Sep 2019 02:35:45 +0000 (02:35 +0000)
committerwhitequark <cz@m-labs.hk>
Mon, 30 Sep 2019 17:38:54 +0000 (17:38 +0000)
nmigen/rpc.py [new file with mode: 0644]
setup.py

diff --git a/nmigen/rpc.py b/nmigen/rpc.py
new file mode 100644 (file)
index 0000000..ac19395
--- /dev/null
@@ -0,0 +1,112 @@
+import sys
+import json
+import argparse
+import importlib
+
+from .hdl import Signal, Elaboratable
+from .back import rtlil
+
+
+__all__ = ["main"]
+
+
+def _collect_modules(names):
+    modules = {}
+    for name in names:
+        py_module_name, py_class_name = name.rsplit(".", 1)
+        py_module = importlib.import_module(py_module_name)
+        if py_class_name == "*":
+            for py_class_name in py_module.__all__:
+                py_class = py_module.__dict__[py_class_name]
+                if not issubclass(py_class, Elaboratable):
+                    continue
+                modules["{}.{}".format(py_module_name, py_class_name)] = py_class
+        else:
+            py_class = py_module.__dict__[py_class_name]
+            if not isinstance(py_class, type) or not issubclass(py_class, Elaboratable):
+                raise TypeError("{}.{} is not a class inheriting from Elaboratable"
+                                .format(py_module_name, py_class_name))
+            modules[name] = py_class
+    return modules
+
+
+def _serve_yosys(modules):
+    while True:
+        request_json = sys.stdin.readline()
+        if not request_json: break
+        request = json.loads(request_json)
+
+        if request["method"] == "modules":
+            response = {"modules": list(modules.keys())}
+
+        elif request["method"] == "derive":
+            module_name = request["module"]
+
+            args, kwargs = [], {}
+            for parameter_name, parameter in request["parameters"].items():
+                if parameter["type"] == "unsigned":
+                    parameter_value = int(parameter["value"], 2)
+                elif parameter["type"] == "signed":
+                    width = len(parameter["value"])
+                    parameter_value = int(parameter["value"], 2)
+                    if parameter_value & (1 << (width - 1)):
+                        parameter_value = -((1 << width) - value)
+                elif parameter["type"] == "string":
+                    parameter_value = parameter["value"]
+                elif parameter["type"] == "real":
+                    parameter_value = float(parameter["value"])
+                else:
+                    raise NotImplementedError("Unrecognized parameter type {}"
+                                              .format(parameter_name))
+                if parameter_name.startswith("$"):
+                    index = int(parameter_name[1:])
+                    while len(args) < index:
+                        args.append(None)
+                    args[index] = parameter_value
+                if parameter_name.startswith("\\"):
+                    kwargs[parameter_name[1:]] = parameter_value
+
+            try:
+                elaboratable = modules[module_name](*args, **kwargs)
+                def has_port(elaboratable, port_name):
+                    # By convention, any public attribute that is a Signal is considered a port.
+                    return (not port_name.startswith("_") and
+                            isinstance(getattr(elaboratable, port_name), Signal))
+                ports = [getattr(elaboratable, port_name)
+                         for port_name in dir(elaboratable)
+                         if has_port(elaboratable, port_name)]
+                rtlil_text = rtlil.convert(elaboratable, name=module_name, ports=ports)
+                response = {"frontend": "ilang", "source": rtlil_text}
+            except Exception as error:
+                response = {"error": "{}: {}".format(type(error).__name__, str(error))}
+
+        else:
+            return {"error": "Unrecognized method {!r}".format(request["method"])}
+
+        sys.stdout.write(json.dumps(response))
+        sys.stdout.write("\n")
+        sys.stdout.flush()
+
+
+def main():
+    parser = argparse.ArgumentParser(description=r"""
+    The nMigen RPC server allows a HDL synthesis program to request an nMigen module to
+    be elaborated on demand using the parameters it provides. For example, using Yosys together
+    with the nMigen RPC server allows instantiating parametric nMigen modules directly
+    from Verilog.
+    """)
+    def add_modules_arg(parser):
+        parser.add_argument("modules", metavar="MODULE", type=str, nargs="+",
+            help="import and provide MODULES")
+    protocols = parser.add_subparsers(metavar="PROTOCOL", dest="protocol", required=True)
+    protocol_yosys = protocols.add_parser("yosys", help="use Yosys JSON-based RPC protocol")
+    add_modules_arg(protocol_yosys)
+
+    args = parser.parse_args()
+    modules = _collect_modules(args.modules)
+    if args.protocol == "yosys":
+        _serve_yosys(modules)
+
+
+if __name__ == "__main__":
+    main()
index b3da5d26d21997ba651ef83c7cf1c2faf1674085..390e007dc76d3b91e0209db1918da37fdd31dae8 100644 (file)
--- a/setup.py
+++ b/setup.py
@@ -23,6 +23,11 @@ setup(
     setup_requires=["setuptools_scm"],
     install_requires=["setuptools", "pyvcd>=0.1.4", "bitarray", "Jinja2"],
     packages=find_packages(),
+    entry_points={
+        "console_scripts": [
+            "nmigen-rpc = nmigen.rpc:main",
+        ]
+    },
     project_urls={
         #"Documentation": "https://nmigen.readthedocs.io/",
         "Source Code": "https://github.com/m-labs/nmigen",