[otbn] Simplify error tracking in the ISS

Rather than tracking proper errors with messages (which we didn't
actually have any way to look at anyway), set the ERR_BITS bits
explicitly. One advantage of doing this is that it lets us write
something more understandable in the ISA docs.

Signed-off-by: Rupert Swarbrick <rswarbrick@lowrisc.org>
diff --git a/hw/ip/otbn/dv/otbnsim/sim/alert.py b/hw/ip/otbn/dv/otbnsim/sim/alert.py
deleted file mode 100644
index d7b5287..0000000
--- a/hw/ip/otbn/dv/otbnsim/sim/alert.py
+++ /dev/null
@@ -1,88 +0,0 @@
-# Copyright lowRISC contributors.
-# Licensed under the Apache License, Version 2.0, see LICENSE for details.
-# SPDX-License-Identifier: Apache-2.0
-
-from typing import Optional
-
-# A copy of the list of bits in the ERR_BITS register. This also appears in the
-# documentation and otbn_pkg.sv: we should probably be generating them from the
-# hjson every time.
-BAD_DATA_ADDR = 1 << 0
-BAD_INSN_ADDR = 1 << 1
-CALL_STACK = 1 << 2
-ILLEGAL_INSN = 1 << 3
-LOOP = 1 << 4
-FATAL_IMEM = 1 << 5
-FATAL_DMEM = 1 << 6
-FATAL_REG = 1 << 7
-
-
-class Alert:
-    '''An object describing something the program did wrong
-
-    This maps onto alerts in the implementation. The err_bit value is the
-    value that should be OR'd into the ERR_BITS external register.
-
-    '''
-    # Subclasses should override this class field or the error_bit method
-    err_bit = None  # type: Optional[int]
-
-    def error_bit(self) -> int:
-        assert self.err_bit is not None
-        return self.err_bit
-
-
-class BadAddrError(Alert):
-    '''Generated when loading or storing or setting PC with a bad address'''
-
-    def __init__(self, operation: str, addr: int, what: str):
-        assert operation in ['pc',
-                             'narrow load', 'narrow store',
-                             'wide load', 'wide store']
-        self.operation = operation
-        self.addr = addr
-        self.what = what
-
-    def error_bit(self) -> int:
-        return BAD_INSN_ADDR if self.operation == 'fetch' else BAD_DATA_ADDR
-
-    def __str__(self) -> str:
-        return ('Bad {} address of {:#08x}: {}.'
-                .format(self.operation, self.addr, self.what))
-
-
-class LoopError(Alert):
-    '''Generated when doing something wrong with a LOOP/LOOPI'''
-
-    err_bit = LOOP
-
-    def __init__(self, what: str):
-        self.what = what
-
-    def __str__(self) -> str:
-        return 'Loop error: {}'.format(self.what)
-
-
-class IllegalInsnError(Alert):
-    '''Generated on a bad instruction'''
-    err_bit = 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 CallStackError(Alert):
-    '''Raised when under- or over-flowing the call stack'''
-
-    err_bit = CALL_STACK
-
-    def __init__(self, is_overflow: bool):
-        self.is_overflow = is_overflow
-
-    def __str__(self) -> str:
-        xflow = 'overflow' if self.is_overflow else 'underflow'
-        return 'Instruction caused {} of x1 call stack.'.format(xflow)
diff --git a/hw/ip/otbn/dv/otbnsim/sim/decode.py b/hw/ip/otbn/dv/otbnsim/sim/decode.py
index 146ec46..04cadfc 100644
--- a/hw/ip/otbn/dv/otbnsim/sim/decode.py
+++ b/hw/ip/otbn/dv/otbnsim/sim/decode.py
@@ -7,7 +7,7 @@
 import struct
 from typing import List, Optional, Tuple, Type
 
-from .alert import IllegalInsnError
+from .err_bits import ILLEGAL_INSN
 from .isa import DecodeError, OTBNInsn
 from .insn import INSN_CLASSES
 from .state import OTBNState
@@ -39,7 +39,7 @@
         self._disasm = (pc, '?? 0x{:08x}'.format(raw))
 
     def execute(self, state: OTBNState) -> None:
-        state.on_error(IllegalInsnError(self.raw, self.msg))
+        state.stop_at_end_of_cycle(ILLEGAL_INSN)
 
 
 MASK_TUPLES = None  # type: Optional[List[_MaskTuple]]
diff --git a/hw/ip/otbn/dv/otbnsim/sim/dmem.py b/hw/ip/otbn/dv/otbnsim/sim/dmem.py
index f2becd2..e6157b8 100644
--- a/hw/ip/otbn/dv/otbnsim/sim/dmem.py
+++ b/hw/ip/otbn/dv/otbnsim/sim/dmem.py
@@ -7,7 +7,7 @@
 
 from shared.mem_layout import get_memory_layout
 
-from .alert import BadAddrError
+from .err_bits import BAD_DATA_ADDR
 from .trace import Trace
 
 
@@ -61,7 +61,7 @@
         self.data = [uninit] * num_words
         self.trace = []  # type: List[TraceDmemStore]
 
-        self._errs = []  # type: List[BadAddrError]
+        self.err_flag = False
 
     def _get_u32s(self, idx: int) -> List[int]:
         '''Return the value at idx as 8 uint32's
@@ -145,15 +145,13 @@
         assert addr >= 0
 
         if addr & 31:
-            self._errs.append(BadAddrError('wide load', addr,
-                                           'address is not 32-byte aligned'))
+            self.err_flag = True
             return 0
 
         word_addr = addr // 32
 
         if word_addr >= len(self.data):
-            self._errs.append(BadAddrError('wide load', addr,
-                                           'address is above the top of dmem'))
+            self.err_flag = True
             return 0
 
         return self.data[word_addr]
@@ -164,14 +162,12 @@
         assert 0 <= value < (1 << 256)
 
         if addr & 31:
-            self._errs.append(BadAddrError('wide store', addr,
-                                           'address is not 32-byte aligned'))
+            self.err_flag = True
             return
 
         word_addr = addr // 32
         if word_addr >= len(self.data):
-            self._errs.append(BadAddrError('wide store', addr,
-                                           'address is above the top of dmem'))
+            self.err_flag = True
             return
 
         self.trace.append(TraceDmemStore(addr, value, True))
@@ -185,12 +181,11 @@
         '''
         assert addr >= 0
         if addr & 3:
-            self._errs.append(BadAddrError('narrow load', addr,
-                                           'address is not 4-byte aligned'))
+            self.err_flag = True
             return 0
+
         if (addr + 3) // 32 >= len(self.data):
-            self._errs.append(BadAddrError('narrow load', addr,
-                                           'address is above the top of dmem'))
+            self.err_flag = True
             return 0
 
         idx32 = addr // 4
@@ -209,18 +204,17 @@
         assert 0 <= value <= (1 << 32) - 1
 
         if addr & 3:
-            self._errs.append(BadAddrError('narrow load', addr,
-                                           'address is not 4-byte aligned'))
+            self.err_flag = True
             return
+
         if (addr + 3) // 32 >= len(self.data):
-            self._errs.append(BadAddrError('narrow load', addr,
-                                           'address is above the top of dmem'))
+            self.err_flag = True
             return
 
         self.trace.append(TraceDmemStore(addr, value, False))
 
-    def errors(self) -> List[BadAddrError]:
-        return self._errs
+    def err_bits(self) -> int:
+        return BAD_DATA_ADDR if self.err_flag else 0
 
     def changes(self) -> Sequence[Trace]:
         return self.trace
@@ -247,11 +241,11 @@
         self._set_u32s(idxW, u32s)
 
     def commit(self) -> None:
-        assert not self._errs
+        assert not self.err_flag
         for item in self.trace:
             self._commit_store(item)
         self.trace = []
 
     def abort(self) -> None:
         self.trace = []
-        self._errs = []
+        self.err_flag = False
diff --git a/hw/ip/otbn/dv/otbnsim/sim/err_bits.py b/hw/ip/otbn/dv/otbnsim/sim/err_bits.py
new file mode 100644
index 0000000..44b9020
--- /dev/null
+++ b/hw/ip/otbn/dv/otbnsim/sim/err_bits.py
@@ -0,0 +1,15 @@
+# Copyright lowRISC contributors.
+# Licensed under the Apache License, Version 2.0, see LICENSE for details.
+# SPDX-License-Identifier: Apache-2.0
+
+# A copy of the list of bits in the ERR_BITS register. This also appears in the
+# documentation and otbn_pkg.sv: we should probably be generating them from the
+# hjson every time.
+BAD_DATA_ADDR = 1 << 0
+BAD_INSN_ADDR = 1 << 1
+CALL_STACK = 1 << 2
+ILLEGAL_INSN = 1 << 3
+LOOP = 1 << 4
+FATAL_IMEM = 1 << 5
+FATAL_DMEM = 1 << 6
+FATAL_REG = 1 << 7
diff --git a/hw/ip/otbn/dv/otbnsim/sim/gpr.py b/hw/ip/otbn/dv/otbnsim/sim/gpr.py
index ea64eb8..9d7898d 100644
--- a/hw/ip/otbn/dv/otbnsim/sim/gpr.py
+++ b/hw/ip/otbn/dv/otbnsim/sim/gpr.py
@@ -4,7 +4,7 @@
 
 from typing import List
 
-from .alert import CallStackError
+from .err_bits import CALL_STACK
 from .reg import Reg, RegFile
 
 
@@ -29,7 +29,7 @@
             return self.stack[-1] if self.stack else 0xcafef00d
 
         if not self.stack:
-            self.gpr_parent.errs.append(CallStackError(False))
+            self.gpr_parent.err_flag = True
             return 0
 
         # Mark that we've read something (so that we pop from the stack as part
@@ -40,7 +40,7 @@
     def post_insn(self) -> None:
         if self._next_uval is not None:
             if not self.saw_read and len(self.stack) == 8:
-                self.gpr_parent.errs.append(CallStackError(True))
+                self.gpr_parent.err_flag = True
 
     def commit(self) -> None:
         if self.saw_read:
@@ -67,7 +67,7 @@
     def __init__(self) -> None:
         super().__init__('x', 32, 32)
         self._x1 = CallStackReg(self)
-        self.errs = []  # type: List[CallStackError]
+        self.err_flag = False
 
     def get_reg(self, idx: int) -> Reg:
         if idx == 0:
@@ -88,15 +88,15 @@
     def post_insn(self) -> None:
         return self._x1.post_insn()
 
-    def errors(self) -> List[CallStackError]:
-        return self.errs
+    def err_bits(self) -> int:
+        return CALL_STACK if self.err_flag else 0
 
     def commit(self) -> None:
         super().commit()
-        assert not self.errs
+        assert not self.err_flag
         self._x1.commit()
 
     def abort(self) -> None:
         super().abort()
         self._x1.abort()
-        self.errs = []
+        self.err_flag = False
diff --git a/hw/ip/otbn/dv/otbnsim/sim/insn.py b/hw/ip/otbn/dv/otbnsim/sim/insn.py
index 3828a7a..c7b1566 100644
--- a/hw/ip/otbn/dv/otbnsim/sim/insn.py
+++ b/hw/ip/otbn/dv/otbnsim/sim/insn.py
@@ -4,7 +4,7 @@
 
 from typing import Dict
 
-from .alert import LoopError
+from sim import err_bits
 from .flags import FlagReg
 from .isa import (DecodeError, OTBNInsn, RV32RegReg, RV32RegImm, RV32ImmShift,
                   insn_for_mnemonic, logical_byte_shift)
@@ -309,13 +309,9 @@
 class ECALL(OTBNInsn):
     insn = insn_for_mnemonic('ecall', 0)
 
-    def __init__(self, raw: int, op_vals: Dict[str, int]):
-        super().__init__(raw, op_vals)
-        pass
-
     def execute(self, state: OTBNState) -> None:
         # Set INTR_STATE.done and STATUS, reflecting the fact we've stopped.
-        state._stop(0)
+        state.stop_at_end_of_cycle(err_bits=0)
 
 
 class LOOP(OTBNInsn):
@@ -330,8 +326,7 @@
     def execute(self, state: OTBNState) -> None:
         num_iters = state.gprs.get_reg(self.grs).read_unsigned()
         if num_iters == 0:
-            state.on_error(LoopError('loop count in x{} was zero'
-                                     .format(self.grs)))
+            state.stop_at_end_of_cycle(err_bits.LOOP)
             return
 
         state.loop_start(num_iters, self.bodysize)
diff --git a/hw/ip/otbn/dv/otbnsim/sim/loop.py b/hw/ip/otbn/dv/otbnsim/sim/loop.py
index 4e8e564..d36d367 100644
--- a/hw/ip/otbn/dv/otbnsim/sim/loop.py
+++ b/hw/ip/otbn/dv/otbnsim/sim/loop.py
@@ -4,7 +4,7 @@
 
 from typing import List, Optional
 
-from .alert import LoopError
+from .err_bits import LOOP
 from .trace import Trace
 
 
@@ -59,7 +59,7 @@
     def __init__(self) -> None:
         self.stack = []  # type: List[LoopLevel]
         self.trace = []  # type: List[Trace]
-        self.errs = []  # type: List[LoopError]
+        self.err_flag = False
 
     def start_loop(self,
                    next_addr: int,
@@ -78,7 +78,7 @@
         assert 0 < loop_count
 
         if len(self.stack) == LoopStack.stack_depth:
-            self.errs.append(LoopError('loop stack overflow'))
+            self.err_flag = True
 
         self.trace.append(TraceLoopStart(loop_count, insn_count))
         self.stack.append(LoopLevel(next_addr, insn_count, loop_count - 1))
@@ -92,8 +92,7 @@
                 # Make sure that it isn't a jump, branch or another loop
                 # instruction.
                 if insn_affects_control:
-                    self.errs.append(LoopError('control instruction '
-                                               'at end of loop'))
+                    self.err_flag = True
 
     def step(self, next_pc: int) -> Optional[int]:
         '''Update loop stack. If we should loop, return new PC'''
@@ -118,16 +117,16 @@
 
         return None
 
-    def errors(self) -> List[LoopError]:
-        return self.errs
+    def err_bits(self) -> int:
+        return LOOP if self.err_flag else 0
 
     def changes(self) -> List[Trace]:
         return self.trace
 
     def commit(self) -> None:
-        assert not self.errs
+        assert not self.err_flag
         self.trace = []
 
     def abort(self) -> None:
         self.trace = []
-        self.errs = []
+        self.err_flag = False
diff --git a/hw/ip/otbn/dv/otbnsim/sim/sim.py b/hw/ip/otbn/dv/otbnsim/sim/sim.py
index 05df7f1..9153741 100644
--- a/hw/ip/otbn/dv/otbnsim/sim/sim.py
+++ b/hw/ip/otbn/dv/otbnsim/sim/sim.py
@@ -4,7 +4,6 @@
 
 from typing import List, Optional, Tuple
 
-from .alert import Alert
 from .isa import OTBNInsn
 from .state import OTBNState
 from .trace import Trace
@@ -72,13 +71,14 @@
             insn.execute(self.state)
             self.state.post_insn()
 
-            errors = self.state.errors()
-            if errors:
+            if self.state.pending_halt:
                 # Roll back any pending state changes, ensuring that a faulting
-                # instruction doesn't actually do anything. Also generate a
-                # change that sets an appropriate error bits in the external
-                # ERR_CODE register and clears the busy flag.
-                self.state.die(errors)
+                # instruction doesn't actually do anything (this also aborts an
+                # ECALL instruction, but that doesn't really matter because it
+                # doesn't have any side-effects). Also generate a change that
+                # sets an appropriate error bits in the external ERR_CODE
+                # register and clears the busy flag.
+                self.state.stop()
                 changes = self.state.changes()
             else:
                 changes = self.state.changes()
diff --git a/hw/ip/otbn/dv/otbnsim/sim/state.py b/hw/ip/otbn/dv/otbnsim/sim/state.py
index 7d78084..d180a05 100644
--- a/hw/ip/otbn/dv/otbnsim/sim/state.py
+++ b/hw/ip/otbn/dv/otbnsim/sim/state.py
@@ -6,9 +6,9 @@
 
 from shared.mem_layout import get_memory_layout
 
-from .alert import Alert, BadAddrError
 from .csr import CSRFile
 from .dmem import Dmem
+from .err_bits import BAD_INSN_ADDR
 from .ext_regs import OTBNExtRegs
 from .flags import FlagReg
 from .gpr import GPRs
@@ -51,7 +51,8 @@
         self.ext_regs = OTBNExtRegs()
         self.running = False
 
-        self.errs = []  # type: List[Alert]
+        self._err_bits = 0
+        self.pending_halt = False
 
     def add_stall_cycles(self, num_cycles: int) -> None:
         '''Add stall cycles before the next insn completes'''
@@ -67,14 +68,6 @@
         if back_pc is not None:
             self.pc_next = back_pc
 
-    def errors(self) -> List[Alert]:
-        c = []  # type: List[Alert]
-        c += self.errs
-        c += self.gprs.errors()
-        c += self.dmem.errors()
-        c += self.loop_stack.errors()
-        return c
-
     def changes(self) -> List[Trace]:
         c = []  # type: List[Trace]
         c += self.gprs.changes()
@@ -89,6 +82,11 @@
         return c
 
     def commit(self) -> None:
+        # If the pending_halt flag is set or there are error bits (which should
+        # imply pending_halt is set), we shouldn't get as far as commit.
+        assert not self.pending_halt
+        assert self._err_bits == 0
+
         # Update self.stalled. If the instruction we just ran stalled us then
         # self._stalls will be positive but self.stalled will be false.
         assert self._stalls >= 0
@@ -145,30 +143,22 @@
         self.running = True
         self._start_stall = True
         self.stalled = True
+        self.pending_halt = False
+        self._err_bits = 0
 
-    def _stop(self, err_bits: int) -> None:
-        '''Set flags to stop the processor.
+    def stop(self) -> None:
+        '''Set flags to stop the processor and abort the instruction'''
+        self._abort()
 
-        err_bits is the value written to the ERR_BITS register.
-
-        '''
         # INTR_STATE is the interrupt state register. Bit 0 (which is being
         # set) is the 'done' flag.
         self.ext_regs.set_bits('INTR_STATE', 1 << 0)
         # STATUS is a status register. Bit 0 (being cleared) is the 'busy' flag
         self.ext_regs.clear_bits('STATUS', 1 << 0)
 
-        self.ext_regs.write('ERR_BITS', err_bits, True)
+        self.ext_regs.write('ERR_BITS', self._err_bits, True)
         self.running = False
 
-    def die(self, alerts: List[Alert]) -> None:
-        err_bits = 0
-        for alert in alerts:
-            err_bits |= alert.error_bit()
-
-        self._abort()
-        self._stop(err_bits)
-
     def get_quarter_word_unsigned(self, idx: int, qwsel: int) -> int:
         '''Select a 64-bit quarter of a wide register.
 
@@ -266,13 +256,11 @@
 
         # Check the new PC is word-aligned
         if self.pc_next & 3:
-            self.errs.append(BadAddrError('pc', self.pc_next,
-                                          'address is not 4-byte aligned'))
+            self._err_bits |= BAD_INSN_ADDR
 
         # Check the new PC lies in instruction memory
         if self.pc_next >= self.imem_size:
-            self.errs.append(BadAddrError('pc', self.pc_next,
-                                          'address lies above the top of imem'))
+            self._err_bits |= BAD_INSN_ADDR
 
     def post_insn(self) -> None:
         '''Update state after running an instruction but before commit'''
@@ -280,6 +268,12 @@
         self.loop_step()
         self.gprs.post_insn()
 
+        self._err_bits |= (self.gprs.err_bits() |
+                           self.dmem.err_bits() |
+                           self.loop_stack.err_bits())
+        if self._err_bits:
+            self.pending_halt = True
+
     def read_csr(self, idx: int) -> int:
         '''Read the CSR with index idx as an unsigned 32-bit number'''
         return self.csrs.read_unsigned(self.wsrs, idx)
@@ -292,6 +286,12 @@
         '''Return the current call stack, bottom-first'''
         return self.gprs.peek_call_stack()
 
-    def on_error(self, error: Alert) -> None:
-        '''Add a pending error that will be reported at the end of the cycle'''
-        self.errs.append(error)
+    def stop_at_end_of_cycle(self, err_bits: int) -> None:
+        '''Tell the simulation to stop at the end of the cycle
+
+        Any bits set in err_bits will be set in the ERR_BITS register when
+        we're done.
+
+        '''
+        self._err_bits |= err_bits
+        self.pending_halt = True