From 6c5dbb1e34cd29f2de0c941022fcb6a3b677b59e Mon Sep 17 00:00:00 2001
From: Neil Webber <neil@webber.com>
Date: Thu, 28 Sep 2023 09:04:10 -0500
Subject: [PATCH] stacklimit red and yellow now both conform to SIMH behavior

---
 machine.py  | 38 +++++++++++++++---------
 pdptests.py | 85 +++++++++++++++++++++++++++++++++++++++++++----------
 2 files changed, 94 insertions(+), 29 deletions(-)

diff --git a/machine.py b/machine.py
index d52183f..a430c5d 100644
--- a/machine.py
+++ b/machine.py
@@ -628,8 +628,6 @@ class PDP11:
         if self.r[self.SP] < lim:
             if not (self.straps & self.STRAPBITS.YELLOW):
                 self.logger.info(f"YELLOW ZONE, {list(map(oct, self.r))}")
-                # definitely in at least a yellow condition
-                self.straps |= self.STRAPBITS.YELLOW
 
             # how about red?
             if self.r[self.SP] + 32 < lim:    # uh oh - below the yellow!
@@ -638,6 +636,8 @@ class PDP11:
                 # and this trap is executed
                 self.r[self.SP] = 4             # !! just enough room for...
                 raise PDPTraps.AddressError(cpuerr=self.CPUERR_BITS.REDZONE)
+            else:
+                self.straps |= self.STRAPBITS.YELLOW
 
     def get_synchronous_trap(self, abort_trap):
         """Return a synchronous trap, or possibly None.
@@ -724,18 +724,7 @@ class PDP11:
         self.psw = newps
         self.psw_prevmode = saved_curmode   # i.e., override newps<13:12>
 
-        prepushSP = self.r[6]
-        try:
-            self.stackpush(saved_psw)
-            self.stackpush(self.r[self.PC])
-        except PDPTrap as e:
-            # again this is a pretty egregious error it means the kernel
-            # stack is not mapped, or the stack pointer is odd, or similar
-            # very bad mistakes by the kernel code. It is a fatal halt
-            # NOTE: The stack register is restored
-            self.logger.info(f"Trap pushing trap onto stack")
-            self.r[6] = prepushSP
-            self.halted = self.HALTED_STACK
+        self._trappush(self.r[self.PC], saved_psw)
 
         # The error register records (accumulates) reasons (if given)
         self.error_register |= trap.cpuerr
@@ -805,6 +794,27 @@ class PDP11:
         self.r[6] = self.u16add(self.r[6], 2)
         return w
 
+    # this is special because stack limit checking is disabled
+    # during pushes of trap/interrupt frames. Any other types of
+    # traps during a stack trap push are a fatal CPU halt and represent
+    # a serious kernel programming error (invalid kernel stack)
+    def _trappush(self, pc, psw):
+        try:
+            # NOTE: The stack pointer is only modified if
+            #       both of these succeed with no mmu/addressing traps
+            self.mmu.wordRW_KD(self.u16add(self.r[self.SP], -2), psw)
+            self.mmu.wordRW_KD(self.u16add(self.r[self.SP], -4), pc)
+        except PDPTrap as e:
+            # again this is a pretty egregious error it means the kernel
+            # stack is not mapped, or the stack pointer is odd, or similar
+            # very bad mistakes by the kernel code. It is a fatal halt
+            self.logger.info(f"Trap ({e}) pushing trap {trap} onto stack")
+            self.logger.info(f"Machine state: {self.machinestate()}")
+            self.logger.info("HALTING")
+            self.halted = self.HALTED_STACK
+        else:
+            self.r[self.SP] = self.u16add(self.r[self.SP], -4)
+
 
 class PDP1170(PDP11):
 
diff --git a/pdptests.py b/pdptests.py
index d02ff22..f093a21 100644
--- a/pdptests.py
+++ b/pdptests.py
@@ -1309,13 +1309,7 @@ class TestMethods(unittest.TestCase):
         p.run(pc=aa)
         self.assertEqual(p.r[0], 3)    # confirm made it all the way through
 
-    def test_stacklim1(self):
-        # Set the stack at the top of the yellow zone and then use it.
-        # This test should cause loopcount (3) YELLOW synchronous traps.
-        # Behavior and expected results verified by running identical
-        # machine code in SIMH
-        p = self.make_pdp()
-
+    def _stacklimcode(self, go_red=False):
         # memory usage:
         # 0o4000.. is the test code
         # 0o6000.. is the trap handler
@@ -1324,17 +1318,27 @@ class TestMethods(unittest.TestCase):
         # r5 is used to walk through the 0o7000+ storage, it is initialized
         # in the test code and used in the trap handler
 
+        p = self.make_pdp()
+
         with ASM() as tr:
             # record...
-            tr.mov('r2', '(r5)+')                # ...separator/entry number
-            tr.mov('sp', '(r5)+')                # ...the sp
-            tr.mov('(sp)', '(r5)+')              # ...the trap-saved pc
-            tr.mov(tr.ptr(0o177766), '(r5)+')    # ...cpu error register
-            tr.mov('r2', '(r5)+')                # ...separator/entry number
+            tr.mov('r2', '(r5)+')              # ...separator/entry number
+            tr.mov('sp', '(r5)+')              # ...the sp
+            tr.mov('(sp)', '(r5)+')            # ...the trap-saved pc
+            tr.mov(tr.ptr(0o177766), 'r1')     # (will be used later)
+            tr.mov('r1', '(r5)+')              # ...cpu error register
+            tr.mov('r2', '(r5)+')              # ...separator/entry number
 
             # indicate successfully completed the above, bump entry number
             tr.inc('r2')
+
+            # but if RED trap, stop here.
+            tr.bit(p.CPUERR_BITS.REDZONE, 'r1')
+            tr.beq('rtt')
+            tr.halt()
+            tr.label('rtt')
             tr.rtt()
+
         tra = 0o6000
         self.loadphysmem(p, tr.instructions(), tra)
 
@@ -1355,7 +1359,7 @@ class TestMethods(unittest.TestCase):
             a.mov(tra, a.ptr(0o4))
             a.mov(0o340, a.ptr(0o6))
 
-            loopcount = 3
+            loopcount = 3 if not go_red else 30   # will never get to 30
             a.mov(loopcount, 'r0')
             a.label('push')
             a.clr('-(sp)')
@@ -1364,10 +1368,22 @@ class TestMethods(unittest.TestCase):
 
         aa = 0o4000
         self.loadphysmem(p, a.instructions(), aa)
+        p.r[p.PC] = aa
+        return p
 
-        p.run(pc=aa)
+    def test_stacklim1(self):
+        # Set the stack at the top of the yellow zone and then use it.
+        # This test should cause loopcount (3) YELLOW synchronous traps.
+        # Behavior and expected results verified by running identical
+        # machine code in SIMH
 
-        # obtained by running above in SIMH
+        # r5 is used to walk through the 0o7000+ storage, it is initialized
+        # in the test code and used in the trap handler
+
+        p = self._stacklimcode()
+        p.run()
+
+        # obtained by running machine code in SIMH
         expected_7000 = [
             # MARKER       SP       PC     CPUERR    MARKER
             0o066000, 0o000372, 0o004052, 0o000010, 0o066000,
@@ -1380,6 +1396,45 @@ class TestMethods(unittest.TestCase):
             with self.subTest(i=i, val=val):
                 self.assertEqual(val, p.physmem[recbase + i])
 
+    def test_stacklim_red(self):
+        p = self._stacklimcode(go_red=True)
+        p.run()
+
+        # Behavior/results verified by running machine code on SIMH;
+        # however, SIMH halts the simulation on the red stack trap and
+        # requires intervention to continue into the actual trap handler.
+        # Doing that (i.e., "CONTINUE") leads to these same results.
+        self.assertEqual(p.r[1], 0o14)      # RED|YELLOW
+        self.assertEqual(p.r[2], 0o66021)   # known magic iteration marker
+        self.assertEqual(p.r[6], 0)         # stack should be at zero
+
+        # obtained by running machine code in SIMH
+        expected_7000 = [
+            # MARKER       SP       PC     CPUERR    MARKER
+            0o066000, 0o000372, 0o004052, 0o000010, 0o066000,
+            0o066001, 0o000370, 0o004052, 0o000010, 0o066001,
+            0o066002, 0o000366, 0o004052, 0o000010, 0o066002,
+            0o066003, 0o000364, 0o004052, 0o000010, 0o066003,
+            0o066004, 0o000362, 0o004052, 0o000010, 0o066004,
+            0o066005, 0o000360, 0o004052, 0o000010, 0o066005,
+            0o066006, 0o000356, 0o004052, 0o000010, 0o066006,
+            0o066007, 0o000354, 0o004052, 0o000010, 0o066007,
+            0o066010, 0o000352, 0o004052, 0o000010, 0o066010,
+            0o066011, 0o000350, 0o004052, 0o000010, 0o066011,
+            0o066012, 0o000346, 0o004052, 0o000010, 0o066012,
+            0o066013, 0o000344, 0o004052, 0o000010, 0o066013,
+            0o066014, 0o000342, 0o004052, 0o000010, 0o066014,
+            0o066015, 0o000340, 0o004052, 0o000010, 0o066015,
+            0o066016, 0o000336, 0o004052, 0o000010, 0o066016,
+            0o066017, 0o000334, 0o004052, 0o000010, 0o066017,
+            0o066020, 0o000000, 0o004052, 0o000014, 0o066020,
+            0]
+
+        recbase = 0o7000//2       # word address in phys mem
+        for i, val in enumerate(expected_7000):
+            with self.subTest(i=i, val=val):
+                self.assertEqual(val, p.physmem[recbase + i])
+
     def test_ubmap(self):
         p = self.make_pdp()