[reggen] Define a class wrapping the top-level IP block

The bulk of this patch is in ip_block.py, which defines the IpBlock class.
This object replaces the top-level dictionary that we were parsing.
Client code then replaces something like this:

    obj = hjson.load(hjson_file.open('r'),
                     use_decimal=True,
                     object_pairs_hook=OrderedDict)
    if validate.validate(obj, params=[]) != 0:
        log.info("Parsing %s configuration failed." % hjson_file)
        sys.exit(1)

with

    obj = IpBlock.from_path(str(hjson_file), [])

where obj is now an IpBlock object instead of a dict.

Other than some pesky rewrites in the various gen_FOO scripts and
template files, the other big change on the reggen side was to replace
the hierarchical "Block" class that was defined in data.py. Now, we
have a Top class (created by topgen code) and a Top can contain
multiple blocks. We've also now got some validation logic to make sure
that the sub-blocks and memories don't overlap: I'm not sure that was
there before.

As well as changing how we load files (as described above), topgen
also needed a bit of work. We now have to convert various objects to
dicts in the merge stage. (Before, we cloned the dictionaries and
added some keys; now we construct the new dictionary explicitly).

The idea is that in time we'll start to generate objects instead of
dicts in topgen as well. As a bonus, we should be able to get rid of
some of the spurious "dump & load" logic found there.

Signed-off-by: Rupert Swarbrick <rswarbrick@lowrisc.org>
diff --git a/util/reggen/ip_block.py b/util/reggen/ip_block.py
new file mode 100644
index 0000000..5d7fe3d
--- /dev/null
+++ b/util/reggen/ip_block.py
@@ -0,0 +1,319 @@
+# Copyright lowRISC contributors.
+# Licensed under the Apache License, Version 2.0, see LICENSE for details.
+# SPDX-License-Identifier: Apache-2.0
+
+'''Code representing an IP block for reggen'''
+
+from typing import Dict, List, Optional, Sequence, Tuple
+
+import hjson  # type: ignore
+
+from .alert import Alert
+from .block import Block
+from .inter_signal import InterSignal
+from .lib import (check_keys, check_name, check_int, check_bool,
+                  check_list, check_optional_str, check_name_list)
+from .params import Params, LocalParam
+from .reg_block import RegBlock
+from .signal import Signal
+
+
+REQUIRED_FIELDS = {
+    'name': ['s', "name of the component"],
+    'clock_primary': ['s', "name of the primary clock"],
+    'bus_device': ['s', "name of the bus interface for the device"],
+    'registers': [
+        'l',
+        "list of register definition groups and "
+        "offset control groups"
+    ]
+}
+
+OPTIONAL_FIELDS = {
+    'alert_list': ['lnw', "list of peripheral alerts"],
+    'available_inout_list': ['lnw', "list of available peripheral inouts"],
+    'available_input_list': ['lnw', "list of available peripheral inputs"],
+    'available_output_list': ['lnw', "list of available peripheral outputs"],
+    'bus_host': ['s', "name of the bus interface as host"],
+    'hier_path': [
+        None,
+        'additional hierarchy path before the reg block instance'
+    ],
+    'interrupt_list': ['lnw', "list of peripheral interrupts"],
+    'inter_signal_list': ['l', "list of inter-module signals"],
+    'no_auto_alert_regs': [
+        's', "Set to true to suppress automatic "
+        "generation of alert test registers. "
+        "Defaults to true if no alert_list is present. "
+        "Otherwise this defaults to false. "
+    ],
+    'no_auto_intr_regs': [
+        's', "Set to true to suppress automatic "
+        "generation of interrupt registers. "
+        "Defaults to true if no interrupt_list is present. "
+        "Otherwise this defaults to false. "
+    ],
+    'other_clock_list': ['l', "list of other chip clocks needed"],
+    'other_reset_list': ['l', "list of other resets"],
+    'param_list': ['lp', "list of parameters of the IP"],
+    'regwidth': ['d', "width of registers in bits (default 32)"],
+    'reset_primary': ['s', "primary reset used by the module"],
+    'reset_request_list': ['l', 'list of signals requesting reset'],
+    'scan': ['pb', 'Indicates the module have `scanmode_i`'],
+    'scan_reset': ['pb', 'Indicates the module have `test_rst_ni`'],
+    'SPDX-License-Identifier': [
+        's', "License ientifier (if using pure json) "
+        "Only use this if unable to put this "
+        "information in a comment at the top of the "
+        "file."
+    ],
+    'wakeup_list': ['lnw', "list of peripheral wakeups"]
+}
+
+
+class IpBlock(Block):
+    def __init__(self,
+                 name: str,
+                 regwidth: int,
+                 params: Params,
+                 regs: RegBlock,
+                 interrupts: Sequence[Signal],
+                 no_auto_intr: bool,
+                 alerts: List[Alert],
+                 no_auto_alert: bool,
+                 scan: bool,
+                 inter_signals: List[InterSignal],
+                 bus_device: Optional[str],
+                 bus_host: Optional[str],
+                 hier_path: Optional[str],
+                 clock_signals: List[str],
+                 reset_signals: List[str],
+                 xputs: Tuple[Sequence[Signal],
+                              Sequence[Signal],
+                              Sequence[Signal]],
+                 wakeups: Sequence[Signal],
+                 reset_requests: Sequence[Signal],
+                 scan_reset: bool):
+        assert clock_signals
+        assert reset_signals
+
+        super().__init__(name, regwidth, regs)
+
+        self.params = params
+        self.interrupts = interrupts
+        self.no_auto_intr = no_auto_intr
+        self.alerts = alerts
+        self.no_auto_alert = no_auto_alert
+        self.scan = scan
+        self.inter_signals = inter_signals
+        self.bus_device = bus_device
+        self.bus_host = bus_host
+        self.hier_path = hier_path
+        self.clock_signals = clock_signals
+        self.reset_signals = reset_signals
+        self.xputs = xputs
+        self.wakeups = wakeups
+        self.reset_requests = reset_requests
+        self.scan_reset = scan_reset
+
+    @staticmethod
+    def from_raw(param_defaults: List[Tuple[str, str]],
+                 raw: object,
+                 where: str) -> 'IpBlock':
+
+        rd = check_keys(raw, 'block at ' + where,
+                        list(REQUIRED_FIELDS.keys()),
+                        list(OPTIONAL_FIELDS.keys()))
+
+        name = check_name(rd['name'], 'name of block at ' + where)
+
+        what = '{} block at {}'.format(name, where)
+
+        r_regwidth = rd.get('regwidth')
+        if r_regwidth is None:
+            regwidth = 32
+        else:
+            regwidth = check_int(r_regwidth, 'regwidth field of ' + what)
+            if regwidth <= 0:
+                raise ValueError('Invalid regwidth field for {}: '
+                                 '{} is not positive.'
+                                 .format(what, regwidth))
+
+        params = Params.from_raw('parameter list for ' + what,
+                                 rd.get('param_list', []))
+        try:
+            params.apply_defaults(param_defaults)
+        except (ValueError, KeyError) as err:
+            raise ValueError('Failed to apply defaults to params: {}'
+                             .format(err)) from None
+
+        regs = RegBlock(regwidth, params)
+
+        interrupts = Signal.from_raw_list('interrupt_list for block {}'
+                                          .format(name),
+                                          rd.get('interrupt_list', []))
+        alerts = Alert.from_raw_list('alert_list for block {}'
+                                     .format(name),
+                                     rd.get('alert_list', []))
+
+        no_auto_intr = check_bool(rd.get('no_auto_intr_regs', not interrupts),
+                                  'no_auto_intr_regs field of ' + what)
+
+        no_auto_alert = check_bool(rd.get('no_auto_alert_regs', not alerts),
+                                   'no_auto_alert_regs field of ' + what)
+
+        if interrupts and not no_auto_intr:
+            if interrupts[-1].bits.msb >= regwidth:
+                raise ValueError("Interrupt list for {} is too wide: "
+                                 "msb is {}, which doesn't fit with a "
+                                 "regwidth of {}."
+                                 .format(what,
+                                         interrupts[-1].bits.msb, regwidth))
+            regs.make_intr_regs(interrupts)
+
+        if alerts:
+            if not no_auto_alert:
+                if len(alerts) > regwidth:
+                    raise ValueError("Interrupt list for {} is too wide: "
+                                     "{} alerts don't fit with a regwidth of {}."
+                                     .format(what, len(alerts), regwidth))
+                regs.make_alert_regs(alerts)
+
+            # Generate a NumAlerts parameter
+            existing_param = params.get('NumAlerts')
+            if existing_param is not None:
+                if ((not isinstance(existing_param, LocalParam) or
+                     existing_param.param_type != 'int' or
+                     existing_param.value != str(len(alerts)))):
+                    raise ValueError('Conflicting definition of NumAlerts '
+                                     'parameter.')
+            else:
+                params.add(LocalParam(name='NumAlerts',
+                                      desc='Number of alerts',
+                                      param_type='int',
+                                      value=str(len(alerts))))
+
+        scan = check_bool(rd.get('scan', False), 'scan field of ' + what)
+
+        regs.add_raw_registers(rd['registers'])
+        regs.validate()
+
+        r_inter_signals = check_list(rd.get('inter_signal_list', []),
+                                     'inter_signal_list field')
+        inter_signals = [
+            InterSignal.from_raw('entry {} of the inter_signal_list field'
+                                 .format(idx + 1),
+                                 entry)
+            for idx, entry in enumerate(r_inter_signals)
+        ]
+
+        bus_device = check_optional_str(rd.get('bus_device', None),
+                                        'bus_device field of ' + what)
+        bus_host = check_optional_str(rd.get('bus_host', None),
+                                      'bus_host field of ' + what)
+
+        if bus_device == "tlul":
+            # Add to inter_module_signal
+            port_name = "tl" if bus_host in ["none", "", None] else "tl_d"
+            inter_signals.append(InterSignal(port_name, None, 'tl', 'tlul_pkg',
+                                             'req_rsp', 'rsp', 1, None))
+
+        if bus_host == "tlul":
+            inter_signals.append(InterSignal('tl_h', None, 'tl', 'tlul_pkg',
+                                             'req_rsp', 'rsp', 1, None))
+
+        hier_path = check_optional_str(rd.get('hier_path', None),
+                                       'hier_path field of ' + what)
+
+        clock_primary = check_name(rd['clock_primary'],
+                                   'clock_primary field of ' + what)
+        other_clock_list = check_name_list(rd.get('other_clock_list', []),
+                                           'other_clock_list field of ' + what)
+        clock_signals = [clock_primary] + other_clock_list
+
+        reset_primary = check_name(rd.get('reset_primary', 'rst_ni'),
+                                   'reset_primary field of ' + what)
+        other_reset_list = check_name_list(rd.get('other_reset_list', []),
+                                           'other_reset_list field of ' + what)
+        reset_signals = [reset_primary] + other_reset_list
+
+        xputs = (
+            Signal.from_raw_list('available_inout_list for block ' + name,
+                                 rd.get('available_inout_list', [])),
+            Signal.from_raw_list('available_input_list for block ' + name,
+                                 rd.get('available_input_list', [])),
+            Signal.from_raw_list('available_output_list for block ' + name,
+                                 rd.get('available_output_list', []))
+        )
+        wakeups = Signal.from_raw_list('wakeup_list for block ' + name,
+                                       rd.get('wakeup_list', []))
+        rst_reqs = Signal.from_raw_list('reset_request_list for block ' + name,
+                                        rd.get('reset_request_list', []))
+
+        scan_reset = check_bool(rd.get('scan_reset', False),
+                                'scan_reset field of ' + what)
+
+        return IpBlock(name, regwidth, params, regs,
+                       interrupts, no_auto_intr, alerts, no_auto_alert,
+                       scan, inter_signals, bus_device, bus_host,
+                       hier_path, clock_signals, reset_signals,
+                       xputs, wakeups, rst_reqs, scan_reset)
+
+    @staticmethod
+    def from_text(txt: str,
+                  param_defaults: List[Tuple[str, str]],
+                  where: str) -> 'IpBlock':
+        '''Load an IpBlock from an hjson description in txt'''
+        return IpBlock.from_raw(param_defaults,
+                                hjson.loads(txt, use_decimal=True),
+                                where)
+
+    @staticmethod
+    def from_path(path: str,
+                  param_defaults: List[Tuple[str, str]]) -> 'IpBlock':
+        '''Load an IpBlock from an hjson description in a file at path'''
+        with open(path, 'r') as handle:
+            return IpBlock.from_text(handle.read(), param_defaults,
+                                     'file at {!r}'.format(path))
+
+    def _asdict(self) -> Dict[str, object]:
+        ret = super()._asdict()
+        ret['param_list'] = self.params.as_dicts()
+        ret['interrupt_list'] = self.interrupts
+        ret['no_auto_intr_regs'] = self.no_auto_intr
+        ret['alert_list'] = self.alerts
+        ret['no_auto_alert_regs'] = self.no_auto_alert
+        ret['scan'] = self.scan
+        ret['inter_signal_list'] = self.inter_signals
+
+        if self.bus_device is not None:
+            ret['bus_device'] = self.bus_device
+        if self.bus_host is not None:
+            ret['bus_host'] = self.bus_host
+        if self.hier_path is not None:
+            ret['hier_path'] = self.hier_path
+
+        ret['clock_primary'] = self.clock_signals[0]
+        if len(self.clock_signals) > 1:
+            ret['other_clock_list'] = self.clock_signals[1:]
+
+        ret['reset_primary'] = self.reset_signals[0]
+        if len(self.reset_signals) > 1:
+            ret['other_reset_list'] = self.reset_signals[1:]
+
+        inouts, inputs, outputs = self.xputs
+        if inouts:
+            ret['available_inout_list'] = inouts
+        if inputs:
+            ret['available_input_list'] = inputs
+        if outputs:
+            ret['available_output_list'] = outputs
+
+        if self.wakeups:
+            ret['wakeup_list'] = self.wakeups
+        if self.reset_requests:
+            ret['reset_request_list'] = self.reset_requests
+
+        ret['scan_reset'] = self.scan_reset
+
+        return ret