[otbn,dv] Misaligned load/store snippet

Misaligned load-store generates valid but misaligned address which
results with an error.

Signed-off-by: Canberk Topal <ctopal@lowrisc.org>
Co-authored-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 9c590b8..3695435 100644
--- a/hw/ip/otbn/dv/rig/rig/configs/base.yml
+++ b/hw/ip/otbn/dv/rig/rig/configs/base.yml
@@ -25,3 +25,4 @@
   BadInsn: 1
   BadGiantLoop: 1
   BadZeroLoop: 1
+  MisalignedLoadStore: 1
diff --git a/hw/ip/otbn/dv/rig/rig/gens/misaligned_load_store.py b/hw/ip/otbn/dv/rig/rig/gens/misaligned_load_store.py
new file mode 100644
index 0000000..8ce69ee
--- /dev/null
+++ b/hw/ip/otbn/dv/rig/rig/gens/misaligned_load_store.py
@@ -0,0 +1,284 @@
+# 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.operand import ImmOperandType, RegOperandType, OptionOperandType
+from shared.insn_yaml import Insn, InsnsFile
+
+from ..config import Config
+from ..model import Model
+from ..program import ProgInsn, Program
+
+from ..snippet import ProgSnippet
+from ..snippet_gen import GenCont, GenRet, SnippetGen
+
+
+class MisalignedLoadStore(SnippetGen):
+    '''A snippet generator that generates random load/store instructions
+
+    Generated instructions are misaligned, therefore they end the
+    program.
+
+    '''
+
+    ends_program = True
+
+    def __init__(self, cfg: Config, insns_file: InsnsFile) -> None:
+        super().__init__()
+
+        self.insns = []
+        self.weights = []
+
+        self.sw = self._get_named_insn(insns_file, 'sw')
+        # SW Instruction Checks
+        # SW has three operands: grs1, offset, grs2
+        if not (len(self.sw.operands) == 3 and
+                isinstance(self.sw.operands[0].op_type, RegOperandType) and
+                self.sw.operands[0].op_type.reg_type == 'gpr' and
+                not self.sw.operands[0].op_type.is_dest() and
+                isinstance(self.sw.operands[1].op_type, ImmOperandType) and
+                isinstance(self.sw.operands[2].op_type, RegOperandType) and
+                self.sw.operands[2].op_type.reg_type == 'gpr' and
+                not self.sw.operands[2].op_type.is_dest()):
+            raise RuntimeError('SW instruction from instructions file is '
+                               'not the shape expected by the BadLoadStore'
+                               'generator.')
+
+        self.lw = self._get_named_insn(insns_file, 'lw')
+
+        # LW has three operands: grd, offset, grs
+        if not (len(self.lw.operands) == 3 and
+                isinstance(self.lw.operands[0].op_type, RegOperandType) and
+                self.lw.operands[0].op_type.reg_type == 'gpr' and
+                self.lw.operands[0].op_type.is_dest() and
+                isinstance(self.lw.operands[1].op_type, ImmOperandType) and
+                isinstance(self.lw.operands[2].op_type, RegOperandType) and
+                self.lw.operands[2].op_type.reg_type == 'gpr' and
+                not self.lw.operands[2].op_type.is_dest()):
+            raise RuntimeError('LW instruction from instructions file is '
+                               'not the shape expected by the BadLoadStore'
+                               'generator.')
+
+        # BN.LID Instruction Checks
+        self.bnlid = self._get_named_insn(insns_file, 'bn.lid')
+
+        # bn.lid expects the operands: grd, grs1, offset, grs1_inc, grd_inc
+        if len(self.bnlid.operands) != 5:
+            raise RuntimeError('Unexpected number of operands for bn.lid')
+
+        if not (isinstance(self.bnlid.operands[0].op_type, RegOperandType) and
+                (self.bnlid.operands[0].op_type.reg_type == 'gpr' and
+                not self.bnlid.operands[0].op_type.is_dest() and
+                isinstance(self.bnlid.operands[1].op_type, RegOperandType) and
+                self.bnlid.operands[1].op_type.reg_type == 'gpr' and
+                not self.bnlid.operands[1].op_type.is_dest() and
+                isinstance(self.bnlid.operands[2].op_type, ImmOperandType) and
+                self.bnlid.operands[2].op_type.signed and
+                isinstance(self.bnlid.operands[3].op_type, OptionOperandType) and
+                isinstance(self.bnlid.operands[4].op_type, OptionOperandType))):
+            raise RuntimeError('BN.LID instruction from instructions file is '
+                               'not the shape expected by the BadLoadStore'
+                               'generator.')
+
+        # BN.SID Instruction Checks
+        self.bnsid = self._get_named_insn(insns_file, 'bn.sid')
+
+        # bn.sid expects the operands: grs1, grs2, offset, grs1_inc,
+        # grs2_inc
+        if len(self.bnsid.operands) != 5:
+            raise RuntimeError('Unexpected number of operands for bn.sid')
+
+        if not (isinstance(self.bnsid.operands[0].op_type, RegOperandType) and
+                self.bnsid.operands[0].op_type.reg_type == 'gpr' and
+                not self.bnsid.operands[0].op_type.is_dest() and
+                isinstance(self.bnsid.operands[1].op_type, RegOperandType) and
+                self.bnsid.operands[1].op_type.reg_type == 'gpr' and
+                not self.bnsid.operands[1].op_type.is_dest() and
+                isinstance(self.bnsid.operands[2].op_type, ImmOperandType) and
+                self.bnsid.operands[2].op_type.signed and
+                isinstance(self.bnsid.operands[3].op_type, OptionOperandType) and
+                isinstance(self.bnsid.operands[4].op_type, OptionOperandType)):
+            raise RuntimeError('BN.LID instruction from instructions file is '
+                               'not the shape expected by the BadLoadStore'
+                               'generator.')
+
+        for insn in [self.sw, self.bnsid, self.lw, self.bnlid]:
+            weight = cfg.insn_weights.get(insn.mnemonic)
+            if weight > 0:
+                self.weights.append(weight)
+                self.insns.append(insn)
+
+        # Check that at least one instruction has a positive weight
+        assert len(self.insns) == len(self.weights)
+        if not self.weights:
+            self.disabled = True
+
+    def gen(self,
+            cont: GenCont,
+            model: Model,
+            program: Program) -> Optional[GenRet]:
+
+        weights = self.weights
+        prog_insn = None
+        while prog_insn is None:
+            idx = random.choices(range(len(self.insns)), weights=weights)[0]
+
+            # If all 4 instructions are not successful, give up.
+            if not weights[idx]:
+                return None
+
+            # Try to fill out the instruction. On failure, clear the weight for
+            # this index and go around again.
+            prog_insn = self.fill_insn(self.insns[idx], model)
+            if prog_insn is None:
+                weights[idx] = 0
+                continue
+
+        weights = self.weights.copy()
+        snippet = ProgSnippet(model.pc, [prog_insn])
+        snippet.insert_into_program(program)
+
+        return (snippet, True, model)
+
+    def fill_insn(self, insn: Insn, model: Model) -> Optional[ProgInsn]:
+        '''Try to pick one of BN.XID or XW instructions
+
+        '''
+
+        # 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 in ['bn.lid', 'bn.sid']:
+            return self._fill_bn_xid(insn, model)
+        if insn.mnemonic in ['lw', 'sw']:
+            return self._fill_xw(insn, model)
+
+        return None
+
+    def _fill_xw(self, insn: Insn, model: Model) -> Optional[ProgInsn]:
+
+        grd_op_type = self.lw.operands[0].op_type
+        imm_op_type = self.lw.operands[1].op_type
+
+        # Determine the offset range for XW
+        offset_rng = imm_op_type.get_op_val_range(model.pc)
+        assert offset_rng is not None
+
+        # Max/Min Offsets for XW (-2048, 2047)
+        min_offset, max_offset = offset_rng
+
+        # Get known registers
+        known_regs = model.regs_with_known_vals('gpr')
+
+        base = []
+        for reg_idx, reg_val in known_regs:
+            val = reg_val - (1 << 32) if reg_val >> 31 else reg_val
+            if 1 - max_offset <= val < model.dmem_size - min_offset:
+                base.append((reg_idx, val))
+
+        # We always have x0 as an eligible register for misaligned load/stores
+        assert base
+
+        # Pick a random register among known registers that is constrained above
+        idx, value = random.choice(base)
+
+        op_val_grs1 = idx
+        assert op_val_grs1 is not None
+
+        imm_val = None
+        for _ in range(50):
+            # Pick a random offset value. We will change it later for
+            # guaranteeing misaligned addresses.
+            imm_val = random.randrange(min_offset, max_offset)
+            addr = imm_val + value
+            if (addr % 4) and (0 <= addr < model.dmem_size):
+                break
+        # The loop body above should succeed 75% of the time, so we should only
+        # get here without an imm_val one time in 2^200.
+        assert imm_val is not None
+
+        offset_val = imm_op_type.op_val_to_enc_val(imm_val, model.pc)
+        assert offset_val is not None
+
+        if insn.mnemonic == 'lw':
+            # Pick grd randomly. Since we can write to any register (including x0)
+            # this should always succeed.
+            op_val_grd = model.pick_operand_value(grd_op_type)
+            assert op_val_grd is not None
+
+            op_val = [op_val_grd, offset_val, op_val_grs1]
+
+        elif insn.mnemonic == 'sw':
+            # Any known register is okay for grs2 operand since it will be
+            # guaranteed to have a misaligned address to store the value from.
+            op_val_grs2 = random.choice(known_regs)[0]
+
+            op_val = [op_val_grs2, offset_val, op_val_grs1]
+
+        return ProgInsn(insn, op_val, ('dmem', 4096))
+
+    def _fill_bn_xid(self, insn: Insn, model: Model) -> Optional[ProgInsn]:
+        '''Fill out a BN.LID or BN.SID instruction'''
+
+        bn_imm_op_type = self.bnlid.operands[2].op_type
+
+        # Determine the offset range for BN.XID
+        bn_offset_rng = bn_imm_op_type.get_op_val_range(model.pc)
+        assert bn_offset_rng is not None
+
+        # Max/Min Offsets for BN.XID (-512 * 32, 511 * 32)
+        min_offset, max_offset = bn_offset_rng
+
+        # Get known registers
+        known_regs = model.regs_with_known_vals('gpr')
+
+        base = []
+
+        # Get misaligned registers which can easily be in valid address range
+        for reg_idx, reg_val in known_regs:
+            val = reg_val - (1 << 32) if reg_val >> 31 else reg_val
+            if (((1 - max_offset <= val < model.dmem_size - min_offset) and
+                 val % 32)):
+                base.append((reg_idx, val))
+
+        if not base:
+            return None
+
+        # Pick a random register among known registers for initialization
+        idx, value = random.choice(base)
+
+        for _ in range(50):
+            # Pick a random offset value. We will change it later for
+            # guaranteeing valid misaligned addresses.
+            bn_imm_val = random.randrange(min_offset, max_offset, 32)
+            addr = bn_imm_val + value
+            if (addr % 32) and (0 <= addr < model.dmem_size):
+                break
+
+        bn_offset_val = bn_imm_op_type.op_val_to_enc_val(bn_imm_val, model.pc)
+        assert bn_offset_val is not None
+
+        # Get the chosen base register index as grs1 operand.
+        op_val_grs1 = idx
+        assert op_val_grs1 is not None
+
+        if insn.mnemonic == 'bn.lid':
+            # Pick grd randomly. Since we can write to any register (including x0)
+            # this should always succeed.
+            op_val_grd = model.pick_operand_value(self.bnlid.operands[0].op_type)
+            assert op_val_grd is not None
+
+            op_val = [op_val_grd, op_val_grs1, bn_offset_val, 0, 0]
+
+        elif insn.mnemonic == 'bn.sid':
+            # Any known register is okay for grs2 operand since it will be
+            # guaranteed to have an out of bounds address to store the value from.
+            op_val_grs2 = random.choice(known_regs)[0]
+            assert op_val_grs2 is not None
+
+            op_val = [op_val_grs1, op_val_grs2, bn_offset_val, 0, 0]
+
+        return ProgInsn(insn, op_val, ('dmem', 4096))
diff --git a/hw/ip/otbn/dv/rig/rig/snippet_gens.py b/hw/ip/otbn/dv/rig/rig/snippet_gens.py
index 1647370..838bc1a 100644
--- a/hw/ip/otbn/dv/rig/rig/snippet_gens.py
+++ b/hw/ip/otbn/dv/rig/rig/snippet_gens.py
@@ -32,6 +32,7 @@
 from .gens.bad_giant_loop import BadGiantLoop
 from .gens.bad_load_store import BadLoadStore
 from .gens.bad_zero_loop import BadZeroLoop
+from .gens.misaligned_load_store import MisalignedLoadStore
 
 
 class SnippetGens:
@@ -56,7 +57,8 @@
         BadInsn,
         BadGiantLoop,
         BadLoadStore,
-        BadZeroLoop
+        BadZeroLoop,
+        MisalignedLoadStore
     ]
 
     def __init__(self, cfg: Config, insns_file: InsnsFile) -> None: