[otbn,dv] Add support for "giant loops" to RIG
These are loops that have an end address above the top of IMEM.
They'll never actually complete, so we generate a sequence of
instructions that *does* complete to go inside them.
Signed-off-by: Rupert Swarbrick <rswarbrick@lowrisc.org>
diff --git a/hw/ip/otbn/dv/rig/rig/configs/base.yml b/hw/ip/otbn/dv/rig/rig/configs/base.yml
index a42c545..533bc3c 100644
--- a/hw/ip/otbn/dv/rig/rig/configs/base.yml
+++ b/hw/ip/otbn/dv/rig/rig/configs/base.yml
@@ -13,4 +13,5 @@
# Generators that end the program
ECall: 1
BadInsn: 1
+ BadGiantLoop: 1
BadZeroLoop: 1
diff --git a/hw/ip/otbn/dv/rig/rig/gens/bad_giant_loop.py b/hw/ip/otbn/dv/rig/rig/gens/bad_giant_loop.py
new file mode 100644
index 0000000..c8cdf58
--- /dev/null
+++ b/hw/ip/otbn/dv/rig/rig/gens/bad_giant_loop.py
@@ -0,0 +1,103 @@
+# Copyright lowRISC contributors.
+# Licensed under the Apache License, Version 2.0, see LICENSE for details.
+# SPDX-License-Identifier: Apache-2.0
+
+import random
+from typing import Optional
+
+from shared.insn_yaml import InsnsFile
+
+from .loop import Loop
+from ..config import Config
+from ..model import Model
+from ..program import ProgInsn, Program
+from ..snippet import LoopSnippet
+from ..snippet_gen import GenCont, GenRet
+
+
+class BadGiantLoop(Loop):
+ '''A generator for loops with end addresses that don't lie in memory
+
+ This generator has "ends_program = True", but doesn't end the program in
+ itself. Instead, it sets up a loop with an endpoint that doesn't fit in
+ memory (so the loop body will never actually terminate) and then generates
+ a sequence for the rest of the program, to go inside the body. OTBN will
+ complete its operation without popping the loop off the stack.
+
+ '''
+
+ ends_program = True
+
+ def __init__(self, cfg: Config, insns_file: InsnsFile) -> None:
+ super().__init__(cfg, insns_file)
+
+ def gen(self,
+ cont: GenCont,
+ model: Model,
+ program: Program) -> Optional[GenRet]:
+
+ # We need space for at least 2 instructions here: one for the loop head
+ # instruction and one for the minimal body (which might contain an
+ # ECALL, for example)
+ space_here = program.get_insn_space_at(model.pc)
+ if space_here < 2:
+ return None
+
+ # Similarly, the smallest possible loop will take 2 instructions
+ if program.space < 2:
+ return None
+
+ # And we don't want to overflow the loop stack
+ if model.loop_depth == Model.max_loop_depth:
+ return None
+
+ insn = self._pick_loop_insn()
+ iters_op_type = insn.operands[0].op_type
+ bodysize_op_type = insn.operands[1].op_type
+
+ bodysize_range = bodysize_op_type.get_op_val_range(model.pc)
+ assert bodysize_range is not None
+
+ bodysize_min, bodysize_max = bodysize_range
+
+ # Adjust bodysize_min to make sure we pick an end address that's larger
+ # than IMEM (and check that we can represent the result).
+ bodysize_min = max(bodysize_min, (program.imem_size - model.pc) // 4)
+ if bodysize_max < bodysize_min:
+ return None
+
+ # Pick bodysize, preferring the endpoints
+ x = random.random()
+ if x < 0.4:
+ bodysize = bodysize_min
+ elif x < 0.8:
+ bodysize = bodysize_max
+ else:
+ bodysize = random.randint(bodysize_min, bodysize_max)
+
+ enc_bodysize = bodysize_op_type.op_val_to_enc_val(bodysize, model.pc)
+ assert enc_bodysize is not None
+
+ # Now pick the number of iterations (not that the number actually
+ # matters)
+ iters = self._pick_iterations(iters_op_type, bodysize, model)
+ if iters is None:
+ return None
+
+ iters_opval, num_iters = iters
+ hd_addr = model.pc
+ hd_insn = ProgInsn(insn, [iters_opval, enc_bodysize], None)
+
+ body_model = self._setup_body(hd_insn, model, program)
+
+ # At this point, all the "loop related work" is done: we've entered the
+ # loop body (from which we can never leave). Now call the continuation
+ # with a flag to say that we'd like the generation to complete.
+ body_snippet, end_model = cont(body_model, program, True)
+
+ # Because we passed end=True to cont, the snippet from the continuation
+ # is not None.
+ assert body_snippet is not None
+
+ loop_snippet = LoopSnippet(hd_addr, hd_insn, body_snippet)
+ return (loop_snippet, True, end_model)
diff --git a/hw/ip/otbn/dv/rig/rig/gens/loop.py b/hw/ip/otbn/dv/rig/rig/gens/loop.py
index c4d066d..4382069 100644
--- a/hw/ip/otbn/dv/rig/rig/gens/loop.py
+++ b/hw/ip/otbn/dv/rig/rig/gens/loop.py
@@ -415,6 +415,21 @@
is_loopi = random.random() < self.loopi_prob
return self.loopi if is_loopi else self.loop
+ def _setup_body(self,
+ hd_insn: ProgInsn,
+ model: Model,
+ program: Program) -> Model:
+ '''Set up a Model for use in body; insert hd_insn into program'''
+ body_model = model.copy()
+ body_model.update_for_insn(hd_insn)
+ body_model.pc += 4
+ body_model.loop_depth += 1
+ assert body_model.loop_depth <= Model.max_loop_depth
+
+ program.add_insns(model.pc, [hd_insn])
+
+ return body_model
+
def _gen_pieces(self,
cont: GenCont,
model: Model,
@@ -465,13 +480,7 @@
hd_insn = ProgInsn(insn, [iter_opval, enc_bodysize], None)
body_program = program.copy()
- body_program.add_insns(model.pc, [hd_insn])
-
- body_model = model.copy()
- body_model.update_for_insn(hd_insn)
- body_model.pc += 4
- body_model.loop_depth += 1
- assert body_model.loop_depth <= Model.max_loop_depth
+ body_model = self._setup_body(hd_insn, model, body_program)
# Constrain fuel in body_model: subtract one (for the first instruction
# after the loop) and then divide by the number of iterations. When we
diff --git a/hw/ip/otbn/dv/rig/rig/snippet_gen.py b/hw/ip/otbn/dv/rig/rig/snippet_gen.py
index 754e8a2..0089106 100644
--- a/hw/ip/otbn/dv/rig/rig/snippet_gen.py
+++ b/hw/ip/otbn/dv/rig/rig/snippet_gen.py
@@ -19,10 +19,10 @@
# The return type of a single generator. This is a tuple (snippet, done,
# model). snippet is a generated snippet. done is true if the processor has
-# just executed an ECALL instruction. model is a Model object representing the
-# state of the processor after executing the code in the snippet(s). The PC of
-# the model will be the next instruction to be executed unless done is true, in
-# which case it still points at the ECALL.
+# just executed an instruction that causes it to stop. model is a Model object
+# representing the state of the processor after executing the code in the
+# snippet(s). The PC of the model will be the next instruction to be executed
+# unless done is true, in which case it still points at the final instruction.
GenRet = Tuple[Snippet, bool, Model]
# An "internal" return type for generators that never cause termination.
@@ -35,7 +35,8 @@
# A continuation type that allows a generator to recursively generate some more
# stuff. If the boolean argument is true, the continuation will try to generate
-# a snippet that causes OTBN to stop.
+# a snippet that causes OTBN to stop. In this case, the Snippet term in the
+# GensRet will not be None.
GenCont = Callable[[Model, Program, bool], GensRet]
diff --git a/hw/ip/otbn/dv/rig/rig/snippet_gens.py b/hw/ip/otbn/dv/rig/rig/snippet_gens.py
index 8639ceb..fb04799 100644
--- a/hw/ip/otbn/dv/rig/rig/snippet_gens.py
+++ b/hw/ip/otbn/dv/rig/rig/snippet_gens.py
@@ -21,6 +21,7 @@
from .gens.straight_line_insn import StraightLineInsn
from .gens.bad_insn import BadInsn
+from .gens.bad_giant_loop import BadGiantLoop
from .gens.bad_zero_loop import BadZeroLoop
@@ -35,6 +36,7 @@
ECall,
BadInsn,
+ BadGiantLoop,
BadZeroLoop
]
@@ -216,6 +218,8 @@
'''
snippets, next_model = self._gens(model, program, end)
+ # If end was True, there must be at least one instruction
+ assert snippets or not end
snippet = Snippet.merge_list(snippets) if snippets else None
return (snippet, next_model)