[otbn] Spot invalid increments in BN.MOVR/BN.LID/BN.SID in ISS

Signed-off-by: Rupert Swarbrick <rswarbrick@lowrisc.org>
diff --git a/hw/ip/otbn/dv/otbnsim/sim/decode.py b/hw/ip/otbn/dv/otbnsim/sim/decode.py
index 5383246..60eabef 100644
--- a/hw/ip/otbn/dv/otbnsim/sim/decode.py
+++ b/hw/ip/otbn/dv/otbnsim/sim/decode.py
@@ -7,7 +7,8 @@
 import struct
 from typing import List, Optional, Tuple, Type
 
-from .isa import OTBNInsn
+from .alert import Alert, ERR_CODE_ILLEGAL_INSN
+from .isa import DecodeError, OTBNInsn
 from .insn import INSN_CLASSES
 from .state import OTBNState
 
@@ -17,6 +18,18 @@
 _MaskTuple = Tuple[int, int, Type[OTBNInsn]]
 
 
+class IllegalInsnError(Alert):
+    '''Raised on a bad instruction'''
+    err_code = ERR_CODE_ILLEGAL_INSN
+
+    def __init__(self, word: int, msg: str):
+        self.word = word
+        self.msg = msg
+
+    def __str__(self) -> str:
+        return ('Illegal instruction {:#010x}: {}'.format(self.word, self.msg))
+
+
 class IllegalInsn(OTBNInsn):
     '''A catch-all subclass of Instruction for bad data
 
@@ -29,12 +42,16 @@
     we know this doesn't match any real instruction.
 
     '''
-    def __init__(self, word: int) -> None:
-        self.word = word
+    def __init__(self, pc: int, raw: int, msg: str) -> None:
+        super().__init__(raw, {})
+        self.msg = msg
+
+        # Override the memoized disassembly for the instruction, avoiding us
+        # disassembling the underlying DummyInsn.
+        self._disasm = (pc, '?? 0x{:08x}'.format(raw))
 
     def execute(self, state: OTBNState) -> None:
-        raise RuntimeError('Illegal instruction at {:#x}: encoding {:#010x}.'
-                           .format(int(state.pc), self.word))
+        raise IllegalInsnError(self.raw, self.msg)
 
 
 MASK_TUPLES = None  # type: Optional[List[_MaskTuple]]
@@ -68,7 +85,7 @@
     return MASK_TUPLES
 
 
-def _decode_word(word_off: int, word: int) -> OTBNInsn:
+def _decode_word(pc: int, word: int) -> OTBNInsn:
     found_cls = None
     for m0, m1, cls in get_insn_masks():
         # If any bit is set that should be zero or if any bit is clear that
@@ -80,7 +97,7 @@
         break
 
     if found_cls is None:
-        return IllegalInsn(word)
+        return IllegalInsn(pc, word, 'No legal decoding')
 
     # Decode the instruction. We know that we have an encoding (we checked in
     # get_insn_masks).
@@ -89,18 +106,24 @@
 
     # Make sense of these encoded values as "operand values" (doing any
     # shifting, sign interpretation etc.)
-    op_vals = cls.insn.enc_vals_to_op_vals(4 * word_off, enc_vals)
+    op_vals = cls.insn.enc_vals_to_op_vals(pc, enc_vals)
 
-    return cls(word, op_vals)
+    # Catch any decode errors raised by the instruction constructor. This lets
+    # us generate errors if an instruction encoding has extra constraints that
+    # can't be captured by the logic in the Encoding class.
+    try:
+        return cls(word, op_vals)
+    except DecodeError as err:
+        return IllegalInsn(pc, word, str(err))
 
 
-def decode_bytes(data: bytes) -> List[OTBNInsn]:
+def decode_bytes(base_addr: int, data: bytes) -> List[OTBNInsn]:
     '''Decode instruction bytes as instructions'''
     assert len(data) & 3 == 0
-    return [_decode_word(offset, int_val[0])
+    return [_decode_word(base_addr + 4 * offset, int_val[0])
             for offset, int_val in enumerate(struct.iter_unpack('<I', data))]
 
 
-def decode_file(path: str) -> List[OTBNInsn]:
+def decode_file(base_addr: int, path: str) -> List[OTBNInsn]:
     with open(path, 'rb') as handle:
-        return decode_bytes(handle.read())
+        return decode_bytes(base_addr, handle.read())
diff --git a/hw/ip/otbn/dv/otbnsim/sim/elf.py b/hw/ip/otbn/dv/otbnsim/sim/elf.py
index f293f73..32eec53 100644
--- a/hw/ip/otbn/dv/otbnsim/sim/elf.py
+++ b/hw/ip/otbn/dv/otbnsim/sim/elf.py
@@ -133,7 +133,7 @@
                            'not a multiple of 4.'
                            .format(path, len(imem_bytes)))
 
-    imem_insns = decode_bytes(imem_bytes)
+    imem_insns = decode_bytes(0, imem_bytes)
 
     sim.load_program(imem_insns)
     sim.load_data(dmem_bytes)
diff --git a/hw/ip/otbn/dv/otbnsim/sim/insn.py b/hw/ip/otbn/dv/otbnsim/sim/insn.py
index a125c7a..594abee 100644
--- a/hw/ip/otbn/dv/otbnsim/sim/insn.py
+++ b/hw/ip/otbn/dv/otbnsim/sim/insn.py
@@ -6,7 +6,7 @@
 
 from .alert import LoopError
 from .flags import FlagReg
-from .isa import (OTBNInsn, RV32RegReg, RV32RegImm, RV32ImmShift,
+from .isa import (DecodeError, OTBNInsn, RV32RegReg, RV32RegImm, RV32ImmShift,
                   insn_for_mnemonic, logical_byte_shift)
 from .state import OTBNState
 
@@ -804,6 +804,9 @@
         self.grs1 = op_vals['grs1']
         self.grs1_inc = op_vals['grs1_inc']
 
+        if self.grd_inc and self.grs1_inc:
+            raise DecodeError('grd_inc and grs1_inc both set')
+
     def execute(self, state: OTBNState) -> None:
         grs1_val = state.gprs.get_reg(self.grs1).read_unsigned()
         addr = (grs1_val + self.offset) & ((1 << 32) - 1)
@@ -833,6 +836,9 @@
         self.grs1 = op_vals['grs1']
         self.grs1_inc = op_vals['grs1_inc']
 
+        if self.grs1_inc and self.grs2_inc:
+            raise DecodeError('grs1_inc and grs2_inc both set')
+
     def execute(self, state: OTBNState) -> None:
         grs1_val = state.gprs.get_reg(self.grs1).read_unsigned()
         addr = (grs1_val + self.offset) & ((1 << 32) - 1)
@@ -875,6 +881,9 @@
         self.grs = op_vals['grs']
         self.grs_inc = op_vals['grs_inc']
 
+        if self.grd_inc and self.grs_inc:
+            raise DecodeError('grd_inc and grs_inc both set')
+
     def execute(self, state: OTBNState) -> None:
         grd_val = state.gprs.get_reg(self.grd).read_unsigned()
         grs_val = state.gprs.get_reg(self.grs).read_unsigned()
diff --git a/hw/ip/otbn/dv/otbnsim/sim/isa.py b/hw/ip/otbn/dv/otbnsim/sim/isa.py
index cc9772e..46a9f7c 100644
--- a/hw/ip/otbn/dv/otbnsim/sim/isa.py
+++ b/hw/ip/otbn/dv/otbnsim/sim/isa.py
@@ -21,6 +21,15 @@
     sys.exit(1)
 
 
+class DecodeError(Exception):
+    '''An error raised when trying to decode malformed instruction bits'''
+    def __init__(self, msg: str):
+        self.msg = msg
+
+    def __str__(self) -> str:
+        return 'DecodeError: {}'.format(self.msg)
+
+
 class DummyInsn(Insn):
     '''A dummy instruction that will never be decoded. Used for the insn class
     variable in the OTBNInsn base class.
diff --git a/hw/ip/otbn/dv/otbnsim/stepped.py b/hw/ip/otbn/dv/otbnsim/stepped.py
index 8fb63df..bda5be1 100755
--- a/hw/ip/otbn/dv/otbnsim/stepped.py
+++ b/hw/ip/otbn/dv/otbnsim/stepped.py
@@ -144,7 +144,7 @@
     path = args[0]
 
     print('LOAD_I {!r}'.format(path))
-    sim.load_program(decode_file(path))
+    sim.load_program(decode_file(0, path))
 
 
 def on_dump_d(sim: OTBNSim, args: List[str]) -> None: