[otbn,dv] Alter "loopy" configuration to see maximal loop depth
This lets us hit the FullToEmpty_C cover assertion in the loop
interface, which wants to see a loop of maximal depth, followed by the
loop stack becoming completely empty again.
Signed-off-by: Rupert Swarbrick <rswarbrick@lowrisc.org>
diff --git a/hw/ip/otbn/dv/rig/rig/config.py b/hw/ip/otbn/dv/rig/rig/config.py
index 7847120..a543128 100644
--- a/hw/ip/otbn/dv/rig/rig/config.py
+++ b/hw/ip/otbn/dv/rig/rig/config.py
@@ -4,7 +4,7 @@
import os
import random
-from typing import Dict, List, Optional, Set
+from typing import Dict, List, Optional, Set, Tuple
from shared.yaml_parse_helpers import check_str, check_keys, load_yaml
@@ -45,6 +45,85 @@
self.values[key] = self.get(key) * other.get(key)
+class MinMaxes:
+ '''An object representing a dict of maximum int values, indexed by string
+
+ Each key starts with either "min-" or "max-" (and we need to know which, so
+ that we can merge dictionaries properly).
+
+ '''
+ def __init__(self, what: str, yml: object):
+ if not isinstance(yml, dict):
+ raise ValueError('{} is expected to be a dict, '
+ 'but was actually a {}.'
+ .format(what, type(yml).__name__))
+
+ self.min_values = {} # type: Dict[str, int]
+ self.max_values = {} # type: Dict[str, int]
+
+ for key, value in yml.items():
+ if not isinstance(key, str):
+ raise ValueError('{} had key {!r}, which is not a string.'
+ .format(what, key))
+ is_min = key.startswith('min-')
+ if not is_min:
+ if not key.startswith('max-'):
+ raise ValueError('{} had key {!r}, which does not start '
+ 'with "min-" or "max-".'
+ .format(what, key))
+
+ try:
+ ival = int(value)
+ except ValueError:
+ raise ValueError('{} at key {!r} had value {!r}, '
+ 'which is not an integer.'
+ .format(what, key, value)) from None
+
+ if is_min:
+ self.min_values[key[4:]] = ival
+ else:
+ self.max_values[key[4:]] = ival
+
+ def get_min(self, key: str, default: Optional[int] = None) -> Optional[int]:
+ '''Get a minimum from the dictionary'''
+ return self.min_values.get(key, default)
+
+ def get_max(self, key: str, default: Optional[int] = None) -> Optional[int]:
+ '''Get a maximum from the dictionary'''
+ return self.max_values.get(key, default)
+
+ def get_range(self,
+ key: str,
+ default_min: int,
+ default_max: int) -> Tuple[int, int]:
+ '''Get a (min, max) pair from the dictionary'''
+ return (self.min_values.get(key, default_min),
+ self.max_values.get(key, default_max))
+
+ def _merge_key(self, is_min: bool, key: str, other: 'MinMaxes') -> int:
+ '''Compute a merged value for key.
+
+ This should appear in either self or other.
+
+ '''
+ a = self.min_values.get(key) if is_min else self.max_values.get(key)
+ b = other.min_values.get(key) if is_min else other.max_values.get(key)
+ if a is None:
+ assert b is not None
+ return b
+ if b is None:
+ assert a is not None
+ return a
+
+ return max(a, b) if is_min else min(a, b)
+
+ def merge(self, other: 'MinMaxes') -> None:
+ for key in set(self.min_values.keys()) | set(other.min_values.keys()):
+ self.min_values[key] = self._merge_key(True, key, other)
+ for key in set(self.max_values.keys()) | set(other.max_values.keys()):
+ self.max_values[key] = self._merge_key(False, key, other)
+
+
class Inheritance:
'''One or more named parents, plus a weight'''
def __init__(self, item_num: int, yml: object):
@@ -101,7 +180,7 @@
yml: object):
yd = check_keys(yml, 'top-level',
[],
- ['gen-weights', 'insn-weights', 'inherit'])
+ ['gen-weights', 'insn-weights', 'inherit', 'ranges'])
# The most general form for the inherit field is a list of dictionaries
# that get parsed into Inheritance objects. As a shorthand, these
@@ -134,10 +213,9 @@
parents, known_names)
self.path = path
- self.gen_weights = Weights('gen-weights',
- yd.get('gen-weights', {}))
- self.insn_weights = Weights('insn-weights',
- yd.get('insn-weights', {}))
+ self.gen_weights = Weights('gen-weights', yd.get('gen-weights', {}))
+ self.insn_weights = Weights('insn-weights', yd.get('insn-weights', {}))
+ self.ranges = MinMaxes('ranges', yd.get('ranges', {}))
if merged_ancestors is not None:
self.merge(merged_ancestors)
@@ -175,3 +253,4 @@
def merge(self, other: 'Config') -> None:
self.gen_weights.merge(other.gen_weights)
self.insn_weights.merge(other.insn_weights)
+ self.ranges.merge(other.ranges)
diff --git a/hw/ip/otbn/dv/rig/rig/configs/README.md b/hw/ip/otbn/dv/rig/rig/configs/README.md
index a2ae88c..029e33f 100644
--- a/hw/ip/otbn/dv/rig/rig/configs/README.md
+++ b/hw/ip/otbn/dv/rig/rig/configs/README.md
@@ -71,3 +71,16 @@
As an even shorter shorthand, a one-item list whose only item is a
string can be replaced by just that string.
+
+- ranges:
+
+ A dictionary of maximum or minimum values, keyed with names like
+ `max-FOO` or `min-FOO`, respectively.
+
+ Supported names:
+
+ - `max-loop-iters`: Used by the Loop generator to constrain the
+ maximum number of loop iterations that it will pick.
+ - `max-loop-tail-insns`: Used by the Loop generator to constrain the
+ maximum number of straight-line instructions it will generate as
+ part of a loop's tail.
diff --git a/hw/ip/otbn/dv/rig/rig/configs/loopy.yml b/hw/ip/otbn/dv/rig/rig/configs/loopy.yml
index 7048f1a..2a6e04d 100644
--- a/hw/ip/otbn/dv/rig/rig/configs/loopy.yml
+++ b/hw/ip/otbn/dv/rig/rig/configs/loopy.yml
@@ -3,10 +3,17 @@
# SPDX-License-Identifier: Apache-2.0
# An example custom configuration that generates lots of loops (100
-# times as many as the default config)
+# times as many as the default config), but constrains them not to
+# take too long. We also force the generator not to make long loop
+# tails (the straight-line code that appears up to and including the
+# last instruction of the loop). The idea is that we'll be much more
+# likely to get deeply nested loops this way.
inherit: base
gen-weights:
Loop: 100
+ranges:
+ max-loop-iters: 2
+ max-loop-tail-insns: 4
diff --git a/hw/ip/otbn/dv/rig/rig/gens/loop.py b/hw/ip/otbn/dv/rig/rig/gens/loop.py
index dd55779..3736fb2 100644
--- a/hw/ip/otbn/dv/rig/rig/gens/loop.py
+++ b/hw/ip/otbn/dv/rig/rig/gens/loop.py
@@ -67,6 +67,18 @@
else:
self.loopi_prob = loopi_weight / sum_weights
+ self.cfg_max_iters = cfg.ranges.get_max('loop-iters')
+ if self.cfg_max_iters is not None and self.cfg_max_iters < 1:
+ raise RuntimeError(f'Invalid max-loop-iters value of '
+ f'{self.cfg_max_iters}: this must be '
+ f'at least 1.')
+
+ self.cfg_max_tail = cfg.ranges.get_max('loop-tail-insns')
+ if self.cfg_max_tail is not None and self.cfg_max_tail < 1:
+ raise RuntimeError(f'Invalid max-loop-tail-insns value of '
+ f'{self.cfg_max_tail}: this must be '
+ f'at least 1.')
+
def _pick_loop_iterations(self,
max_iters: int,
model: Model) -> Optional[Tuple[int, int]]:
@@ -90,6 +102,9 @@
# boring).
max_iters = min(max_iters, 10)
+ if self.cfg_max_iters is not None:
+ max_iters = min(max_iters, self.cfg_max_iters)
+
# Iterate over the known registers, trying to pick a weight
poss_pairs = [] # type: List[Tuple[int, int]]
weights = [] # type: List[float]
@@ -120,6 +135,12 @@
assert iters_range is not None
iters_lo, iters_hi = iters_range
+ # Constrain iters_hi if the max-loop-iters configuration value was set.
+ if self.cfg_max_iters is not None:
+ iters_hi = min(iters_hi, self.cfg_max_iters)
+ if iters_hi < iters_lo:
+ return None
+
# Very occasionally, generate iters_hi iterations (the maximum number
# representable) if we've got fuel for it. We don't do this often,
# because the instruction sequence will end up just testing loop
@@ -324,6 +345,9 @@
else:
max_tail_len = min(bodysize, model.fuel - 1)
+ if self.cfg_max_tail is not None:
+ max_tail_len = min(max_tail_len, self.cfg_max_tail)
+
# program.space gives another bound on the tail length. If the bodysize
# is large enough that we'll need to jump to the tail, the tail can't
# be more than program.space - 1 in length. If we don't need to