borodust / cl-bodge

Feature-rich game framework for Common Lisp
http://borodust.org/projects/cl-bodge/
MIT License
174 stars 14 forks source link

[SBCL] Hiding irrelevant/dangerous restarts debugger #106

Open AkashaP opened 2 years ago

AkashaP commented 2 years ago

loving this project so far. when the debugger prompt is started from the update or draw threads they are flooded with restart-binds from many other dependencies

take this scenario for example:

(defvar *dummy-output-stream* (make-string-output-stream))
(defmethod gamekit:act ((app my-game))
  (break))

when i start this i get the following restarts:

break
   [Condition of type SIMPLE-CONDITION]

Restarts:
 0: [CONTINUE] Return from BREAK.
 1: [RERUN-FLOW-BLOCK] Rerun current flow block
 2: [SKIP-FLOW-BLOCK] Skip flow block returning nil
 3: [USE-FLOW-BLOCK-VALUE] Skip flow block returning provided value
 4: [INJECT-FLOW] Inject flow to run instead of current block
 5: [CONTINUE] Ignore error by skipping current flow block

Backtrace:
 6: [ABORT] Shutdown cl-bodge engine
 7: [CONTINUE] Ignore error by skipping current flow block
 8: [ABORT] Shutdown cl-bodge engine
 9: [ABORT] abort thread (#<THREAD "single-threaded-executor" RUNNING {1005EDC803}>)

All of these buttons except the first seem pretty dangerous to press, they can potentially bury the useful restarts. I've accidentally hit the wrong button once and it messed up my active system forcing me to restart my computer.

I had a look into where these restarts were coming from in SBCL, and found they are bound to a let symbol sb-kernel::*restart-clusters*. Then I had an idea: i can just shadow this variable in another let, filtering out the dangerous restarts:

;; It's a macro only to get the local value of sb-kernel::*restart-clusters*
(defmacro shadow-restarts-sbcl (dummy-output-stream)
  `(mapcar (lambda (restart-list)
             (remove-if (lambda (restart)
                          (or 
                           ;; Hide cl-flow condition handlers
                           (member (restart-name restart)
                                   '(cl-flow::rerun-flow-block
                                     cl-flow::skip-flow-block
                                     cl-flow::use-flow-block-value
                                     cl-flow::inject-flow))

                           ;; Hide dangerous thread restart operations 
                           (funcall (sb-kernel::restart-report-function restart) ,dummy-output-stream) 
                           (let ((report-string (get-output-stream-string ,dummy-output-stream)))
                             (or
                              ;; you can just close the window to do this.
                              (member report-string
                                      '("Shutdown cl-bodge engine"
                                        "Ignore error by skipping current flow block"
                                        "Skip condition")
                                      :test #'equalp)
                              ;; Hide sbcl's abort thread restart - dangerous to use, stuff wont clean up properly
                              (str:starts-with? "abort thread" report-string :ignore-case t)))))
                        restart-list)) sb-kernel::*restart-clusters*))

(defvar *dummy-output-stream* (make-string-output-stream))
(defmethod gamekit:act ((app my-game))
  (with-simple-restart (skip "Skip rest of this game loop iteration") ; see SLY user manual on game REPLs
    (let ((sb-kernel::*restart-clusters* (shadow-restarts-sbcl *dummy-output-stream*)))
      (declare (dynamic-extent *restart-clusters*))  ; avoid consing all those mapcars and remove-ifs on the heap

      (let ((restarts sb-kernel::*restart-clusters*))
        (break "Update loop. Restarts: ~A" restarts)
        (print restarts)) 
      ;; YOUR GAME UPDATE LOGIC HERE
)))

(defvar *dummy-output-stream-2* (make-string-output-stream))
(defmethod gamekit:draw ((app my-game))
  (with-simple-restart (skip "Skip this drawing cycle")
    (let ((sb-kernel::*restart-clusters* (shadow-restarts-sbcl *dummy-output-stream-2*)))
      (declare (dynamic-extent *restart-clusters*))

      (let ((restarts sb-kernel::*restart-clusters*))
        (break "Draw loop. Restarts: ~A" restarts)
        (print restarts))))
    ;; YOUR DRAW LOGIC HERE
)

This offers the following restart cases in SLY's debugger, only affecting the update and draw methods and anything called from inside them:

gamekit:update

Update loop. Restarts: ((Skip rest of this game loop iteration)
                        NIL NIL NIL NIL)
   [Condition of type SIMPLE-CONDITION]

Restarts:
 0: [CONTINUE] Return from BREAK.
 1: [SKIP] Skip rest of this game loop iteration

gamekit:draw

Draw loop. Restarts: ((Skip this drawing cycle) NIL NIL NIL)
   [Condition of type SIMPLE-CONDITION]

Restarts:
 0: [CONTINUE] Return from BREAK.
 1: [SKIP] Skip this drawing cycle
borodust commented 2 years ago

Ho, that's a very interesting workaround for SBCL! I'll certainly think about adding it to gamekit.

Unfortunately though, these dangerously looking restarts (something-something-flow) are actually belong to the project. cl-bodge uses very weird way for an engine to handle concurrency - reactive programming paradigm provided by cl-flow project. So yes, those are actually correct restarts for cl-bodge. gamekit user manual mentions them here.

But for trivial-gamekit, which is single threaded, those indeed make no sense and it's better to have something more obvious and not scary sounding.