[otbn] Add support for bn.sid to RIG

This isn't massively different from bn.lid, but we need to check that
we pick a source WDR that actually has an architectural value. We also
slightly weaken the checks for bn.lid, which can use any GRD with an
architectural value: we don't actually need to know it.

Signed-off-by: Rupert Swarbrick <rswarbrick@lowrisc.org>
diff --git a/hw/ip/otbn/util/rig/gens/straight_line_insn.py b/hw/ip/otbn/util/rig/gens/straight_line_insn.py
index 65bfd82..af07ea0 100644
--- a/hw/ip/otbn/util/rig/gens/straight_line_insn.py
+++ b/hw/ip/otbn/util/rig/gens/straight_line_insn.py
@@ -29,9 +29,9 @@
             if not insn.straight_line:
                 continue
 
-            # Skip bn.sid and bn.movr (these have special "LSU behaviour" and
-            # aren't yet implemented)
-            if insn.mnemonic in ['bn.sid', 'bn.movr']:
+            # Skip bn.movr (this has special "LSU behaviour" and isn't yet
+            # implemented)
+            if insn.mnemonic in ['bn.movr']:
                 continue
 
             self.insns.append(insn)
@@ -102,8 +102,8 @@
         # Special-case BN load/store instructions by mnemonic. These use
         # complicated indirect addressing, so it's probably more sensible to
         # give them special code.
-        if insn.mnemonic == 'bn.lid':
-            return self._fill_bn_lid(insn, model)
+        if insn.mnemonic in ['bn.lid', 'bn.sid']:
+            return self._fill_bn_xid(insn, model)
 
         return self._fill_lsu_insn(insn, model)
 
@@ -125,42 +125,91 @@
         assert len(op_vals) == len(insn.operands)
         return ProgInsn(insn, op_vals, None)
 
-    def _fill_bn_lid(self, insn: Insn, model: Model) -> Optional[ProgInsn]:
-        '''Fill out a BN.LID instruction'''
-        assert insn.mnemonic == 'bn.lid'
-        # bn.lid expects the operands: grd, grs1, offset, grs1_inc, grd_inc
-        if len(insn.operands) != 5:
-            raise RuntimeError('Unexpected number of operands for bn.lid')
+    def _fill_bn_xid(self, insn: Insn, model: Model) -> Optional[ProgInsn]:
+        '''Fill out a BN.LID or BN.SID instruction'''
+        if insn.mnemonic == 'bn.lid':
+            is_load = True
+            # bn.lid expects the operands: grd, grs1, offset, grs1_inc, grd_inc
+            if len(insn.operands) != 5:
+                raise RuntimeError('Unexpected number of operands for bn.lid')
 
-        grd, grs1, offset, grs1_inc, grd_inc = insn.operands
-        exp_shape = (
-            # grd
-            isinstance(grd.op_type, RegOperandType) and
-            grd.op_type.reg_type == 'gpr' and
-            not grd.op_type.is_dest() and
-            # grs1
-            isinstance(grs1.op_type, RegOperandType) and
-            grs1.op_type.reg_type == 'gpr' and
-            not grs1.op_type.is_dest() and
-            # offset
-            isinstance(offset.op_type, ImmOperandType) and
-            offset.op_type.signed and
-            # grs1_inc
-            isinstance(grs1_inc.op_type, OptionOperandType) and
-            # grd_inc
-            isinstance(grd_inc.op_type, OptionOperandType)
-        )
+            grd, grs1, offset, grs1_inc, grd_inc = insn.operands
+            exp_shape = (
+                # grd
+                isinstance(grd.op_type, RegOperandType) and
+                grd.op_type.reg_type == 'gpr' and
+                not grd.op_type.is_dest() and
+                # grs1
+                isinstance(grs1.op_type, RegOperandType) and
+                grs1.op_type.reg_type == 'gpr' and
+                not grs1.op_type.is_dest() and
+                # offset
+                isinstance(offset.op_type, ImmOperandType) and
+                offset.op_type.signed and
+                # grs1_inc
+                isinstance(grs1_inc.op_type, OptionOperandType) and
+                # grd_inc
+                isinstance(grd_inc.op_type, OptionOperandType)
+            )
+        else:
+            assert insn.mnemonic == 'bn.sid'
+            is_load = False
+            # bn.sid expects the operands: grs1, grs2, offset, grs1_inc,
+            # grs2_inc
+            if len(insn.operands) != 5:
+                raise RuntimeError('Unexpected number of operands for bn.sid')
+
+            grs1, grs2, offset, grs1_inc, grs2_inc = insn.operands
+            exp_shape = (
+                # grs1
+                isinstance(grs1.op_type, RegOperandType) and
+                grs1.op_type.reg_type == 'gpr' and
+                not grs1.op_type.is_dest() and
+                # grs2
+                isinstance(grs2.op_type, RegOperandType) and
+                grs2.op_type.reg_type == 'gpr' and
+                not grs2.op_type.is_dest() and
+                # offset
+                isinstance(offset.op_type, ImmOperandType) and
+                offset.op_type.signed and
+                # grs1_inc
+                isinstance(grs1_inc.op_type, OptionOperandType) and
+                # grs2_inc
+                isinstance(grs2_inc.op_type, OptionOperandType)
+            )
+
         if not exp_shape:
-            raise RuntimeError('Unexpected shape for bn.lid')
+            raise RuntimeError('Unexpected shape for {}'.format(insn.mnemonic))
 
         # Assertions to guide mypy
         assert isinstance(offset.op_type, ImmOperandType)
 
-        # bn.lid reads the bottom 5 bits of grd to get the index of the
-        # destination WDR. So look at the registers with architectural values.
-        # Since this is a destination register, we can safely pick anything.
         known_regs = model.regs_with_known_vals('gpr')
-        grd_idx, grd_val = random.choices(known_regs)[0]
+        if is_load:
+            # bn.lid reads the bottom 5 bits of grd to get the index of the
+            # destination WDR. We can pick any GPR that has an architectural
+            # value.
+            arch_gprs = model.regs_with_architectural_vals('gpr')
+            assert arch_gprs
+            wdr_gpr_idx = random.choices(arch_gprs)[0]
+        else:
+            # bn.sid looks at the bottom 5 bits of grs2 to get a source WDR.
+            # Unlike bn.lid above, we need an architectural value in the
+            # corresponding WDR. Thus we have to pick a GPR with a known value
+            # which, in turn, points at a WDR with an architectural value.
+            arch_wdrs = set(model.regs_with_architectural_vals('wdr'))
+            valid_grs2_indices = []
+            for grs2_idx, grs2_val in known_regs:
+                if grs2_val is None:
+                    continue
+                wdr_idx = grs2_val & 31
+                if wdr_idx in arch_wdrs:
+                    valid_grs2_indices.append(grs2_idx)
+
+            if not valid_grs2_indices:
+                return None
+
+            wdr_gpr_idx = random.choices(valid_grs2_indices)[0]
 
         # Now pick the source register and offset. The range for offset
         # shouldn't be none (because we know the width of the underlying bit
@@ -170,7 +219,7 @@
 
         op_to_known_regs = {'grs1': known_regs}
         tgt = model.pick_lsu_target('dmem',
-                                    True,
+                                    is_load,
                                     op_to_known_regs,
                                     offset_rng,
                                     offset.op_type.shift,
@@ -187,19 +236,29 @@
         offset_val = offset.op_type.op_val_to_enc_val(imm_val, model.pc)
         assert offset_val is not None
 
-        # Do we increment grs1 / grd? We can increment up to one of them.
+        # Do we increment the GPRs? We can increment up to one of them.
         inc_idx = random.randint(0, 2)
         if inc_idx == 0:
             grs1_inc_val = 0
-            grd_inc_val = 0
+            wdr_gpr_inc_val = 0
         elif inc_idx == 1:
             grs1_inc_val = 1
-            grd_inc_val = 0
+            wdr_gpr_inc_val = 0
         else:
             grs1_inc_val = 0
-            grd_inc_val = 1
+            wdr_gpr_inc_val = 1
 
-        enc_vals = [grd_idx, grs1_val, offset_val, grs1_inc_val, grd_inc_val]
+        # Finally, package up the operands properly for the instruction we're
+        # building.
+        if is_load:
+            # bn.lid: grd, grs1, offset, grs1_inc, grd_inc
+            enc_vals = [wdr_gpr_idx, grs1_val, offset_val,
+                        grs1_inc_val, wdr_gpr_inc_val]
+        else:
+            # bn.sid: grs1, grs2, offset, grs1_inc, grs2_inc
+            enc_vals = [grs1_val, wdr_gpr_idx, offset_val,
+                        grs1_inc_val, wdr_gpr_inc_val]
+
         return ProgInsn(insn, enc_vals, ('dmem', addr))
 
     def _fill_lsu_insn(self, insn: Insn, model: Model) -> Optional[ProgInsn]:
diff --git a/hw/ip/otbn/util/rig/model.py b/hw/ip/otbn/util/rig/model.py
index 08943dd..44060ab 100644
--- a/hw/ip/otbn/util/rig/model.py
+++ b/hw/ip/otbn/util/rig/model.py
@@ -503,6 +503,20 @@
 
         return ret
 
+    def regs_with_architectural_vals(self, reg_type: str) -> List[int]:
+        '''List registers that have an architectural value'''
+        known_regs = self._known_regs.setdefault(reg_type, {})
+        arch_regs = list(known_regs.keys())
+
+        # Handle x1, which has an architectural (and known) value iff the call
+        # stack is not empty.
+        if reg_type == 'gpr':
+            assert 1 not in arch_regs
+            if self._call_stack:
+                arch_regs.append(1)
+
+        return arch_regs
+
     def pick_lsu_target(self,
                         mem_type: str,
                         loads_value: bool,
@@ -702,6 +716,43 @@
         elif grd_inc:
             self._inc_gpr(grd, 1, 31)
 
+    def update_for_bnsid(self, prog_insn: ProgInsn) -> None:
+        '''Update model state after an BN.SID'''
+        insn = prog_insn.insn
+        op_vals = prog_insn.operands
+        assert insn.mnemonic == 'bn.sid'
+        assert len(insn.operands) == len(op_vals)
+
+        grs1_op, grs2_op, offset_op, grs1_inc_op, grs2_inc_op = insn.operands
+        exp_shape = (
+            # grs1
+            isinstance(grs1_op.op_type, RegOperandType) and
+            grs1_op.op_type.reg_type == 'gpr' and
+            not grs1_op.op_type.is_dest() and
+            # grs2
+            isinstance(grs2_op.op_type, RegOperandType) and
+            grs2_op.op_type.reg_type == 'gpr' and
+            not grs2_op.op_type.is_dest() and
+            # offset
+            isinstance(offset_op.op_type, ImmOperandType) and
+            offset_op.op_type.signed and
+            # grs1_inc
+            isinstance(grs1_inc_op.op_type, OptionOperandType) and
+            # grs2_inc
+            isinstance(grs2_inc_op.op_type, OptionOperandType)
+        )
+        if not exp_shape:
+            raise RuntimeError('Unexpected shape for bn.sid')
+
+        grs1, grs2, offset, grs1_inc, grs2_inc = op_vals
+
+        self._generic_update_for_insn(prog_insn)
+
+        if grs1_inc:
+            self._inc_gpr(grs1, 32, (1 << 32) - 1)
+        elif grs2_inc:
+            self._inc_gpr(grs2, 1, 31)
+
     def _generic_update_for_insn(self, prog_insn: ProgInsn) -> None:
         '''Update registers and memory for prog_insn
 
@@ -744,7 +795,8 @@
         updaters = {
             'lui': self.update_for_lui,
             'addi': self.update_for_addi,
-            'bn.lid': self.update_for_bnlid
+            'bn.lid': self.update_for_bnlid,
+            'bn.sid': self.update_for_bnsid
         }
         updater = updaters.get(prog_insn.insn.mnemonic)
         if updater is not None: