[ipgen] Strengthen template parameter type checks

The `IpConfig` class holds typed template parameters, which can be
either a string or an integer. Before, the parameter type was only
checked when data was loaded from text form (usually reading a Hjson
file), but not when constructing an `IpConfig` object programmatically.

Strengthen the checks to ensure that all template parameters which are
passed into `IpConfig` can ultimately be serialized to Hjson and survive
a round-trip.

This checking requires passing the list of expected template parameters,
encapsulated in `TemplateParams` around a bit more.

Signed-off-by: Philipp Wagner <phw@lowrisc.org>
diff --git a/util/ipgen/lib.py b/util/ipgen/lib.py
index c90198d..579e628 100644
--- a/util/ipgen/lib.py
+++ b/util/ipgen/lib.py
@@ -3,7 +3,7 @@
 # SPDX-License-Identifier: Apache-2.0
 
 from pathlib import Path
-from typing import Dict, Optional, Union, cast
+from typing import Any, Dict, Optional, Union
 
 import hjson  # type: ignore
 from reggen.lib import check_int, check_keys, check_list, check_name, check_str
@@ -16,8 +16,15 @@
 
 class TemplateParameter(BaseParam):
     """ A template parameter. """
+    VALID_PARAM_TYPES = (
+        'int',
+        'string',
+    )
+
     def __init__(self, name: str, desc: Optional[str], param_type: str,
                  default: str):
+        assert param_type in self.VALID_PARAM_TYPES
+
         super().__init__(name, desc, param_type)
         self.default = default
         self.value = None
@@ -41,16 +48,21 @@
 
     r_type = rd.get('type')
     param_type = check_str(r_type, 'type field of ' + where)
-    if not param_type in ('string', 'int'):
+    if param_type not in TemplateParameter.VALID_PARAM_TYPES:
         raise ValueError('At {}, the {} param has an invalid type field {!r}. '
-                         'Allowed values are: string, int.'.format(
-                             where, name, param_type))
+                         'Allowed values are: {}.'.format(
+                             where, name, param_type,
+                             ', '.join(TemplateParameter.VALID_PARAM_TYPES)))
 
     r_default = rd.get('default')
-    default = check_str(r_default, 'default field of ' + where)
-    if param_type[:3] == 'int':
-        check_int(default,
-                  'default field of {}, (an integer parameter)'.format(name))
+    if param_type == 'int':
+        default = check_int(
+            r_default,
+            'default field of {}, (an integer parameter)'.format(name))
+    elif param_type == 'string':
+        default = check_str(r_default, 'default field of ' + where)
+    else:
+        assert False, f"Unknown parameter type found: {param_type!r}"
 
     return TemplateParameter(name, desc, param_type, default)
 
@@ -82,10 +94,10 @@
     """
 
     name: str
-    params: Params
+    params: TemplateParams
     template_path: Path
 
-    def __init__(self, name: str, params: Params, template_path: Path):
+    def __init__(self, name: str, params: TemplateParams, template_path: Path):
         self.name = name
         self.params = params
         self.template_path = template_path
@@ -145,35 +157,52 @@
         return cls(template_name, params, template_path)
 
 
-def check_param_dict(obj: object, what: str) -> Dict[str, Union[int, str]]:
-    """ Check that obj is a dict with str keys and int or str values. """
-    if not isinstance(obj, dict):
-        raise ValueError(
-            "{} is expected to be a dict, but was actually a {}.".format(
-                what,
-                type(obj).__name__))
-
-    for key, value in obj.items():
-        if not isinstance(key, str):
-            raise ValueError(
-                '{} has a key {!r}, which is not a string.'.format(what, key))
-
-        if not (isinstance(value, str) or isinstance(value, int)):
-            raise ValueError(f"key {key} of {what} has a value {value!r}, "
-                             "which is neither a str, nor an int.")
-
-    return cast(Dict[str, Union[int, str]], obj)
-
-
 class IpConfig:
     def __init__(self,
+                 template_params: TemplateParams,
                  instance_name: str,
                  param_values: Dict[str, Union[str, int]] = {}):
+        self.template_params = template_params
         self.instance_name = instance_name
-        self.param_values = param_values
+        self.param_values = IpConfig._check_param_values(
+            template_params, param_values)
+
+    @staticmethod
+    def _check_param_values(template_params: TemplateParams,
+                            param_values: Any) -> Dict[str, Union[str, int]]:
+        """Check if parameter values are valid.
+
+        Returns the parameter values in typed form if successful, and throws
+        a ValueError otherwise.
+        """
+        param_values_typed = {}
+        for key, value in param_values.items():
+            if not isinstance(key, str):
+                raise ValueError(
+                    f"The IP configuration has a key {key!r} which is not a "
+                    "string.")
+
+            if key not in template_params:
+                raise ValueError(
+                    f"The IP configuration has a key {key!r} which is a "
+                    "valid parameter.")
+
+            if template_params[key].param_type == 'string':
+                param_value_typed = check_str(
+                    value, f"the key {key} of the IP configuration")
+            elif template_params[key].param_type == 'int':
+                param_value_typed = check_int(
+                    value, f"the key {key} of the IP configuration")
+            else:
+                assert True, "Unexpeced parameter type found, expand this check"
+
+            param_values_typed[key] = param_value_typed
+
+        return param_values_typed
 
     @classmethod
-    def from_raw(cls, raw: object, where: str) -> 'IpConfig':
+    def from_raw(cls, template_params: TemplateParams, raw: object,
+                 where: str) -> 'IpConfig':
         """ Load an IpConfig from a raw object """
 
         rd = check_keys(raw, 'configuration file ' + where, ['instance_name'],
@@ -181,10 +210,15 @@
         instance_name = check_name(rd.get('instance_name'),
                                    "the key 'instance_name' of " + where)
 
-        param_values = check_param_dict(rd.get('param_values', []),
-                                        f"the key 'param_values' of {where}")
+        if not isinstance(raw, dict):
+            raise ValueError(
+                "The IP configuration is expected to be a dict, but was "
+                "actually a " + type(raw).__name__)
 
-        return cls(instance_name, param_values)
+        param_values = IpConfig._check_param_values(template_params,
+                                                    rd['param_values'])
+
+        return cls(template_params, instance_name, param_values)
 
     @classmethod
     def from_text(cls, txt: str, where: str) -> 'IpConfig':
diff --git a/util/ipgen/renderer.py b/util/ipgen/renderer.py
index f2f5f4f..7607e25 100644
--- a/util/ipgen/renderer.py
+++ b/util/ipgen/renderer.py
@@ -14,7 +14,7 @@
 from mako.lookup import TemplateLookup as MakoTemplateLookup  # type: ignore
 from reggen.ip_block import IpBlock
 
-from .lib import IpConfig, IpTemplate
+from .lib import IpConfig, IpTemplate, TemplateParameter
 
 _HJSON_LICENSE_HEADER = ("""// Copyright lowRISC contributors.
 // Licensed under the Apache License, Version 2.0, see LICENSE for details.
@@ -72,7 +72,7 @@
                 log.info(f"Using default value for template parameter {name}")
                 val = template_param.default
 
-            assert template_param.param_type in ('string', 'int')
+            assert template_param.param_type in TemplateParameter.VALID_PARAM_TYPES
             try:
                 if template_param.param_type == 'string':
                     val_typed = str(val)  # type: Union[int, str]