[fpvgen] FPV boilerplate generator script

This adds a boilerplate generator script for FPV testbenches. The
generator can be used for both comportable or noncomportable IPs.

Signed-off-by: Michael Schaffner <msf@opentitan.org>
diff --git a/util/fpvgen/sv_parse.py b/util/fpvgen/sv_parse.py
new file mode 100644
index 0000000..325e798
--- /dev/null
+++ b/util/fpvgen/sv_parse.py
@@ -0,0 +1,231 @@
+#!/usr/bin/env python3
+# Copyright lowRISC contributors.
+# Licensed under the Apache License, Version 2.0, see LICENSE for details.
+# SPDX-License-Identifier: Apache-2.0
+r"""Helper functions for parsing a systemverilog module header.
+"""
+
+from io import StringIO
+from pathlib import Path
+
+
+class Param():
+    name = ""
+    datatype = ""
+    style = ""
+    value = ""
+
+    def __init__(self, name_="", datatype_="", style_="", value_=""):
+        self.name = name_
+        self.datatype = datatype_
+        self.style = style_
+        self.value = value_
+
+
+class Port():
+    name = ""
+    datatype = ""
+    direction = ""
+
+    def __init__(self, name_="", datatype_="", direction_=""):
+        self.name = name_
+        self.datatype = datatype_
+        self.direction = direction_
+
+
+class Dut():
+    name = ""
+    pkgs = []
+    params = []
+    ports = []
+    deps = []
+    is_cip = False
+
+    def __init__(self, name_="", pkgs_=[], ports_=[], \
+        params_=[], deps_=[], is_cip_=False):
+        self.name = name_
+        self.pkgs = pkgs_
+        self.ports = ports_
+        self.params = params_
+        self.deps = deps_
+        self.is_cip = is_cip_
+
+
+# strip // comments
+def strip_comments(buf):
+    outbuf = ""
+    for line in buf.split('\n'):
+        for k in range(len(line) - 1):
+            if line[k:k + 2] == "//":
+                break
+            else:
+                outbuf += line[k]
+        else:
+            if line:
+                outbuf += line[-1]
+        outbuf += " "
+
+    return outbuf
+
+
+PARENTH_STYLES = {'(': ')', '[': ']', '{': '}'}
+
+
+# parse parenthesis and optionally handle the body using the handler function
+# if no handler is specified, it just calls itself recursively
+def parse_parenthesis(hdl_raw, dut, style='(', handler=None):
+    if style not in PARENTH_STYLES:
+        print("Unknown parenthesis style %s, aborting." % style)
+    else:
+        par_opened = False
+        while hdl_raw:
+            c = hdl_raw.pop(0)
+            if c == style:
+                par_opened = True
+                if handler:
+                    handler(hdl_raw, dut)
+                else:
+                    parse_parenthesis(hdl_raw, dut, style)
+            if c == PARENTH_STYLES[style]:
+                if not par_opened:
+                    hdl_raw.insert(0, c)
+                break
+    return
+
+
+# parse individual port declarations
+# may not be fully correct with unpacked dimensions,
+# but works for the common case
+def parse_port(buf, dut):
+    words = buf.split()
+    if words:
+        if words[0] not in ["input", "inout", "output"]:
+            print("Warning, expected input, inout or output keyword")
+        else:
+            if len(words) > 2:
+                dut.ports += [Port(words[-1], "".join(words[1:-1]), words[0])]
+            elif len(words) == 2:
+                dut.ports += [Port(words[-1], "", words[0])]
+            else:
+                print("Warning, port declaration incomplete")
+    else:
+        print("Warning, port declaration empty")
+    return
+
+
+# parse individual parameter declaration
+def parse_param(buf, dut):
+    words = buf.split('=')
+    value = '='.join(words[1:])
+    words = words[0].split()
+
+    if words:
+        if words[0] not in ["parameter", "localparam"]:
+            print("Warning, expected parameter or localparam keyword")
+        else:
+            if len(words) > 2:
+                dut.params += [
+                    Param(words[-1], " ".join(words[1:-1]), words[0], value)
+                ]
+            elif len(words) == 2:
+                dut.params += [Param(words[-1], "", words[0], value)]
+            else:
+                print("Warning, parameter declaration incomplete")
+    else:
+        print("Warning, port declaration empty")
+    return
+
+
+# extract individual declarations
+def parse_declaration(hdl_raw, dut, handler):
+    buf = ""
+    par_opened = 0
+    while hdl_raw:
+        c = hdl_raw.pop(0)
+        # end of this port
+        if c == ',':
+            handler(buf, dut)
+            buf = ""
+        elif c == '(':
+            par_opened = par_opened + 1
+            buf += c
+        elif c == ')':
+            if par_opened:
+                # part of the declaration
+                par_opened = par_opened - 1
+                buf += c
+            else:
+                # end of the declaration list
+                handler(buf, dut)
+                hdl_raw.insert(0, ')')
+                break
+        else:
+            buf += c
+    return
+
+
+def parse_ports(hdl_raw, dut):
+    parse_declaration(hdl_raw, dut, parse_port)
+
+
+def parse_params(hdl_raw, dut):
+    parse_declaration(hdl_raw, dut, parse_param)
+
+
+def parse_module(words, dut):
+    # check for imports first
+    while words:
+        w = words.pop(0)
+        if w == "import":
+            if words:
+                # get package names to import
+                pkg = words.pop(0).split(";")
+                dut.pkgs += [pkg[0]]
+            else:
+                print("Unexpected end")
+        # stop package scan and move on to body
+        elif '#' in w or '(' in w:
+            words.insert(0, w)
+            break
+
+    hdl_raw = list(' '.join(words))
+    while hdl_raw:
+        c = hdl_raw.pop(0)
+        if c == '#':
+            parse_parenthesis(hdl_raw, dut, '(', parse_params)
+        elif c == '(':
+            hdl_raw.insert(0, '(')
+            parse_parenthesis(hdl_raw, dut, '(', parse_ports)
+        elif c == ';':
+            break
+    return
+
+
+# simplistic module declaration parser.
+# this works in most cases, but there are exceptions.
+def parse_file(file):
+    dut = Dut()
+    hdl_raw = ""
+    with open(file, 'r') as fp:
+        hdl_raw = strip_comments(fp.read())
+    # extract first module declaration in file and extract port list
+    # also look for imported packages (either in the module declaration
+    # or before it)
+    words = hdl_raw.split()
+    while words:
+        w = words.pop(0)
+        if w == "import":
+            if words:
+                # get package names to import
+                pkg = words.pop(0).split(";")
+                dut.pkgs += [pkg[0]]
+            else:
+                print("Unexpected end")
+        elif w == "module":
+            if words:
+                # get module name
+                dut.name = words[0]
+                # parse the module params and port list and exit
+                parse_module(words, dut)
+            break
+    return dut