Step 4: implement (std actor supervisor) — OTP supervision trees

ober

7c633f1e6290679512fffc95c79be8c80e07c91f

diff --git a/docs/actor-model.md b/docs/actor-model.md
index acb86c4..157c24d 100644
--- a/docs/actor-model.md
+++ b/docs/actor-model.md
@@ -3343,33 +3343,37 @@ Implementation checklist:
 - [x] Test: `reply` in non-ask context raises error
 - [x] Test: multiple concurrent asks to same actor
 
-### Step 4: Supervision Trees
+### Step 4: Supervision Trees ✓ COMPLETE
 
 **File**: `lib/std/actor/supervisor.sls`
 **Test**: `tests/test-actor-supervisor.ss`
-**Dependencies**: `core.sls`, `protocol.sls`, `(jerboa core)` (for `match`)
+**Dependencies**: `core.sls`, `protocol.sls`, `(only (jerboa core) match)`
 
 Implementation checklist:
-- [ ] `make-child-spec` with all fields (id, start-thunk, restart, shutdown, type)
-- [ ] `start-supervisor` starts children in order, monitors each
-- [ ] one-for-one: only restart the dead child
-- [ ] one-for-all: stop all in reverse, restart all in forward
-- [ ] rest-for-one: stop failed + later siblings in reverse, restart in forward
-- [ ] permanent: always restart
-- [ ] transient: restart only on abnormal exit (not 'normal, not 'killed)
-- [ ] temporary: never restart
-- [ ] Restart intensity tracking via timestamp log
-- [ ] Supervisor crashes (raises error) when intensity exceeded
-- [ ] Graceful shutdown: send '(shutdown), wait up to timeout, then kill
-- [ ] `supervisor-which-children` returns child status list
-- [ ] `supervisor-start-child!` adds child dynamically
-- [ ] `supervisor-delete-child!` removes child
-- [ ] `current-seconds` helper avoids SRFI-19 dependency
-- [ ] Test: worker crashes, one-for-one restarts only it
-- [ ] Test: worker crashes, one-for-all restarts all
-- [ ] Test: permanent vs transient vs temporary
-- [ ] Test: intensity exceeded causes supervisor crash
-- [ ] Test: nested supervisors (tree structure)
+- [x] `make-child-spec` with all fields (id, start-thunk, restart, shutdown, type)
+- [x] `start-supervisor` starts children in order, monitors each
+- [x] one-for-one: only restart the dead child
+- [x] one-for-all: stop all in reverse (brutal-kill), restart all in forward
+- [x] rest-for-one: stop failed + later siblings in reverse, restart in forward
+- [x] permanent: always restart
+- [x] transient: restart only on abnormal exit (not 'normal, not 'killed)
+- [x] temporary: never restart
+- [x] Restart intensity tracking via timestamp log
+- [x] Supervisor crashes (raises error) when intensity exceeded
+- [x] Graceful shutdown: send '(shutdown), wait up to timeout, then kill
+- [x] Monitor removed before forced stop to prevent spurious restart
+- [x] `supervisor-which-children` returns child status list
+- [x] `supervisor-start-child!` adds child dynamically
+- [x] `supervisor-delete-child!` removes child
+- [x] `current-seconds` helper avoids SRFI-19 dependency
+- [x] Test: worker crashes, one-for-one restarts only it
+- [x] Test: worker crashes, one-for-all restarts all
+- [x] Test: permanent vs transient vs temporary
+- [x] Test: intensity exceeded causes supervisor crash
+- [x] Test: dynamic start/terminate/delete
+
+**Note**: Use `(only (jerboa core) match)` not bare `(jerboa core)` to avoid
+identifier conflicts with `(chezscheme)` (`1+`, `iota`, `make-hash-table`).
 
 ### Step 5: Registry
 
diff --git a/lib/std/actor/supervisor.sls b/lib/std/actor/supervisor.sls
new file mode 100644
index 0000000..02dcfaa
--- /dev/null
+++ b/lib/std/actor/supervisor.sls
@@ -0,0 +1,305 @@
+#!chezscheme
+;;; (std actor supervisor) — OTP-style supervision trees
+;;;
+;;; Strategies: one-for-one, one-for-all, rest-for-one
+;;; Restart policies: permanent, transient, temporary
+;;; Monitors child actors; escalates if restart intensity exceeded.
+
+(library (std actor supervisor)
+  (export
+    make-child-spec
+    child-spec?
+    child-spec-id
+    child-spec-start-thunk
+    child-spec-restart
+    child-spec-shutdown
+    child-spec-type
+
+    start-supervisor
+
+    supervisor-which-children
+    supervisor-count-children
+    supervisor-terminate-child!
+    supervisor-restart-child!
+    supervisor-start-child!
+    supervisor-delete-child!
+  )
+  (import (chezscheme)
+          (only (jerboa core) match)
+          (std actor core)
+          (std actor protocol))
+
+  ;; -------- Child spec --------
+
+  (define-record-type child-spec
+    (fields
+      (immutable id)           ;; symbol
+      (immutable start-thunk)  ;; (lambda () → actor-ref)
+      (immutable restart)      ;; 'permanent | 'transient | 'temporary
+      (immutable shutdown)     ;; 'brutal-kill | number (seconds)
+      (immutable type))        ;; 'worker | 'supervisor
+    (sealed #t))
+
+  ;; -------- Runtime child entry --------
+
+  (define-record-type child-entry
+    (fields
+      (immutable spec)
+      (mutable actor-ref)   ;; current actor-ref or #f
+      (mutable status))     ;; 'running | 'stopped | 'dead
+    (sealed #t))
+
+  ;; -------- Supervisor state (captured in behavior closure) --------
+
+  (define-record-type supervisor-state
+    (fields
+      (immutable strategy)       ;; 'one-for-one | 'one-for-all | 'rest-for-one
+      (immutable max-restarts)
+      (immutable period-secs)
+      (mutable children)         ;; ordered list of child-entry
+      (mutable restart-log))     ;; list of timestamps (floats)
+    (sealed #t))
+
+  ;; -------- Time helper (no SRFI-19 needed) --------
+
+  (define (current-seconds)
+    (let ([t (current-time)])
+      (+ (time-second t) (/ (time-nanosecond t) 1e9))))
+
+  ;; -------- Supervisor startup --------
+
+  (define start-supervisor
+    (case-lambda
+      [(strategy child-specs)
+       (start-supervisor-impl strategy child-specs 10 5)]
+      [(strategy child-specs max-restarts)
+       (start-supervisor-impl strategy child-specs max-restarts 5)]
+      [(strategy child-specs max-restarts period-secs)
+       (start-supervisor-impl strategy child-specs max-restarts period-secs)]))
+
+  (define (start-supervisor-impl strategy child-specs max-restarts period-secs)
+    (let ([state (make-supervisor-state strategy max-restarts period-secs '() '())])
+      (let ([sup (spawn-actor
+                   (lambda (msg) (supervisor-behavior state msg))
+                   'supervisor)])
+        (for-each (lambda (spec) (start-child! state sup spec)) child-specs)
+        sup)))
+
+  ;; -------- Start a single child --------
+
+  (define (start-child! state sup spec)
+    (let* ([child ((child-spec-start-thunk spec))]
+           [entry (make-child-entry spec child 'running)])
+      ;; Monitor: supervisor gets 'DOWN when child dies (one-way)
+      (actor-ref-monitors-set! child
+        (cons (cons sup (child-spec-id spec))
+              (actor-ref-monitors child)))
+      (supervisor-state-children-set! state
+        (append (supervisor-state-children state) (list entry)))
+      entry))
+
+  ;; -------- Supervisor behavior --------
+
+  (define (supervisor-behavior state msg)
+    (with-ask-context msg
+      (lambda (actual)
+        (match actual
+          [('DOWN spec-id child-id reason)
+           (handle-child-exit! state spec-id child-id reason)]
+
+          [('which-children)
+           (reply (format-children state))]
+
+          [('terminate-child id)
+           (terminate-child-by-id! state id)
+           (reply 'ok)]
+
+          [('restart-child id)
+           (reply (restart-child-by-id! state id))]
+
+          [('start-child spec)
+           (let ([entry (start-child! state (self) spec)])
+             (reply (child-entry-actor-ref entry)))]
+
+          [('delete-child id)
+           (delete-child-by-id! state id)
+           (reply 'ok)]
+
+          [_ (void)]))))
+
+  ;; -------- Handle child exit --------
+
+  (define (handle-child-exit! state spec-id child-id reason)
+    (let ([entry (find-child-by-id state spec-id)])
+      (when entry
+        (let ([spec (child-entry-spec entry)])
+          (let ([should-restart?
+                 (case (child-spec-restart spec)
+                   [(permanent) #t]
+                   [(transient) (not (memq reason '(normal killed)))]
+                   [(temporary) #f]
+                   [else #f])])
+            (if should-restart?
+              (begin
+                (check-restart-intensity! state)
+                (case (supervisor-state-strategy state)
+                  [(one-for-one) (restart-one! state entry)]
+                  [(one-for-all) (restart-all! state)]
+                  [(rest-for-one) (restart-rest! state entry)]))
+              (child-entry-status-set! entry 'dead)))))))
+
+  ;; -------- Restart intensity --------
+
+  (define (check-restart-intensity! state)
+    (let* ([now    (current-seconds)]
+           [period (supervisor-state-period-secs state)]
+           [recent (filter (lambda (t) (> t (- now period)))
+                           (supervisor-state-restart-log state))])
+      (supervisor-state-restart-log-set! state (cons now recent))
+      (when (>= (length recent) (supervisor-state-max-restarts state))
+        (error 'supervisor "restart intensity exceeded"
+               (supervisor-state-max-restarts state)
+               (supervisor-state-period-secs state)))))
+
+  ;; -------- Restart strategies --------
+
+  ;; NOTE: restart-one!, restart-all!, restart-rest! are called only from
+  ;; handle-child-exit!, which runs inside the supervisor actor's behavior.
+  ;; (self) correctly returns the supervisor actor-ref in this context.
+
+  (define (restart-one! state entry)
+    (stop-child-entry! entry)
+    (let* ([spec      (child-entry-spec entry)]
+           [new-actor ((child-spec-start-thunk spec))])
+      (child-entry-actor-ref-set! entry new-actor)
+      (child-entry-status-set!    entry 'running)
+      (actor-ref-monitors-set! new-actor
+        (cons (cons (self) (child-spec-id spec))
+              (actor-ref-monitors new-actor)))))
+
+  (define (restart-all! state)
+    (let ([children (supervisor-state-children state)])
+      (for-each stop-child-entry! (reverse children))
+      (for-each
+        (lambda (entry)
+          (let* ([spec      (child-entry-spec entry)]
+                 [new-actor ((child-spec-start-thunk spec))])
+            (child-entry-actor-ref-set! entry new-actor)
+            (child-entry-status-set!    entry 'running)
+            (actor-ref-monitors-set! new-actor
+              (cons (cons (self) (child-spec-id spec))
+                    (actor-ref-monitors new-actor)))))
+        children)))
+
+  (define (restart-rest! state failed-entry)
+    (let* ([children (supervisor-state-children state)]
+           [pos (let loop ([cs children] [i 0])
+                  (cond [(null? cs) -1]
+                        [(eq? (car cs) failed-entry) i]
+                        [else (loop (cdr cs) (fx+ i 1))]))]
+           [rest (if (fx>= pos 0) (list-tail children pos) '())])
+      (for-each stop-child-entry! (reverse rest))
+      (for-each
+        (lambda (entry)
+          (let* ([spec      (child-entry-spec entry)]
+                 [new-actor ((child-spec-start-thunk spec))])
+            (child-entry-actor-ref-set! entry new-actor)
+            (child-entry-status-set!    entry 'running)
+            (actor-ref-monitors-set! new-actor
+              (cons (cons (self) (child-spec-id spec))
+                    (actor-ref-monitors new-actor)))))
+        rest)))
+
+  ;; -------- Stop a child --------
+
+  (define (stop-child-entry! entry)
+    (let ([a        (child-entry-actor-ref entry)]
+          [shutdown (child-spec-shutdown (child-entry-spec entry))])
+      (when (and a (actor-alive? a))
+        ;; Remove our monitor BEFORE killing so the forced stop does not
+        ;; deliver a DOWN message back to this supervisor and trigger a restart.
+        (actor-ref-monitors-set! a
+          (filter (lambda (mon) (not (eq? (car mon) (self))))
+                  (actor-ref-monitors a)))
+        (cond
+          [(eq? shutdown 'brutal-kill)
+           (actor-kill! a)]
+          [(number? shutdown)
+           ;; Graceful: send 'shutdown, wait, then force-kill
+           (guard (exn [#t (void)])
+             (send a '(shutdown)))
+           (let ([deadline (+ (current-seconds) shutdown)])
+             (let loop ()
+               (cond
+                 [(not (actor-alive? a))  (void)]
+                 [(>= (current-seconds) deadline) (actor-kill! a)]
+                 [else
+                  (sleep (make-time 'time-duration 20000000 0)) ;; 20ms
+                  (loop)])))]))
+      (child-entry-actor-ref-set! entry #f)
+      (child-entry-status-set!    entry 'stopped)))
+
+  ;; -------- Dynamic child management --------
+
+  (define (terminate-child-by-id! state id)
+    (let ([entry (find-child-by-id state id)])
+      (when entry (stop-child-entry! entry))))
+
+  (define (restart-child-by-id! state id)
+    (let ([entry (find-child-by-id state id)])
+      (if (and entry (eq? (child-entry-status entry) 'stopped))
+        (begin (restart-one! state entry) 'ok)
+        'not-found)))
+
+  (define (delete-child-by-id! state id)
+    (let ([entry (find-child-by-id state id)])
+      (when entry
+        (stop-child-entry! entry)
+        (supervisor-state-children-set! state
+          (filter (lambda (e) (not (eq? e entry)))
+                  (supervisor-state-children state))))))
+
+  ;; -------- Public management API --------
+  ;; Called from outside the supervisor actor via ask-sync.
+
+  (define (supervisor-which-children sup)
+    (ask-sync sup '(which-children)))
+
+  (define (supervisor-count-children sup)
+    (let ([children (supervisor-which-children sup)])
+      (let loop ([cs children] [total 0] [active 0])
+        (if (null? cs)
+          (values total active)
+          (loop (cdr cs)
+                (fx+ total 1)
+                (if (eq? (cadr (car cs)) 'running) (fx+ active 1) active))))))
+
+  (define (supervisor-terminate-child! sup id)
+    (ask-sync sup (list 'terminate-child id)))
+
+  (define (supervisor-restart-child! sup id)
+    (ask-sync sup (list 'restart-child id)))
+
+  (define (supervisor-start-child! sup spec)
+    (ask-sync sup (list 'start-child spec)))
+
+  (define (supervisor-delete-child! sup id)
+    (ask-sync sup (list 'delete-child id)))
+
+  ;; -------- Helpers --------
+
+  (define (find-child-by-id state id)
+    (let loop ([cs (supervisor-state-children state)])
+      (cond
+        [(null? cs) #f]
+        [(eq? (child-spec-id (child-entry-spec (car cs))) id) (car cs)]
+        [else (loop (cdr cs))])))
+
+  (define (format-children state)
+    (map (lambda (entry)
+           (list (child-spec-id   (child-entry-spec entry))
+                 (child-entry-status entry)
+                 (child-entry-actor-ref entry)))
+         (supervisor-state-children state)))
+
+  ) ;; end library
diff --git a/tests/test-actor-supervisor.ss b/tests/test-actor-supervisor.ss
new file mode 100644
index 0000000..04b34a5
--- /dev/null
+++ b/tests/test-actor-supervisor.ss
@@ -0,0 +1,188 @@
+#!chezscheme
+;;; Tests for (std actor supervisor) — OTP supervision trees
+
+(import (chezscheme) (jerboa core)
+        (std actor core) (std actor protocol) (std actor supervisor))
+
+(define pass 0)
+(define fail 0)
+
+(define-syntax test
+  (syntax-rules ()
+    [(_ name expr expected)
+     (guard (exn
+              [#t (set! fail (+ fail 1))
+                  (printf "FAIL ~a: exception ~a~%" name
+                    (if (message-condition? exn) (condition-message exn) exn))])
+       (let ([got expr])
+         (if (equal? got expected)
+           (begin (set! pass (+ pass 1)) (printf "  ok ~a~%" name))
+           (begin (set! fail (+ fail 1))
+                  (printf "FAIL ~a: got ~s, expected ~s~%" name got expected)))))]))
+
+(define (wait-ms n)
+  (sleep (make-time 'time-duration (* n 1000000) 0)))
+
+(printf "--- (std actor supervisor) tests ---~%")
+
+;; Test 1: start-supervisor starts all children
+(let ([sup (start-supervisor
+              'one-for-one
+              (list
+                (make-child-spec 'a (lambda () (spawn-actor (lambda (msg) (void)))) 'permanent 1.0 'worker)
+                (make-child-spec 'b (lambda () (spawn-actor (lambda (msg) (void)))) 'permanent 1.0 'worker))
+              10 5)])
+  (wait-ms 50)
+  (let ([children (supervisor-which-children sup)])
+    (test "start-count"   (length children) 2)
+    (test "start-status-a" (cadr (assq 'a children)) 'running)
+    (test "start-status-b" (cadr (assq 'b children)) 'running))
+  (actor-kill! sup))
+
+;; Helper: look up actor-ref for a named child
+(define (child-ref sup name)
+  (caddr (assq name (supervisor-which-children sup))))
+
+;; Test 2: one-for-one — only crashed child is restarted
+(let ([started-a 0] [started-b 0])
+  (let ([sup (start-supervisor
+                'one-for-one
+                (list
+                  (make-child-spec 'a
+                    (lambda ()
+                      (set! started-a (+ started-a 1))
+                      (spawn-actor (lambda (msg) (match msg ['crash (error 'a "crash")]))))
+                    'permanent 1.0 'worker)
+                  (make-child-spec 'b
+                    (lambda ()
+                      (set! started-b (+ started-b 1))
+                      (spawn-actor (lambda (msg) (void))))
+                    'permanent 1.0 'worker))
+                10 5)])
+    (wait-ms 50)
+    (test "one-for-one-initial-a" started-a 1)
+    (test "one-for-one-initial-b" started-b 1)
+    ;; Crash child a
+    (send (child-ref sup 'a) 'crash)
+    (wait-ms 150)  ;; wait for restart
+    (test "one-for-one-restarted-a"  started-a 2)
+    (test "one-for-one-b-untouched"  started-b 1)
+    (actor-kill! sup)))
+
+;; Test 3: one-for-all — all children restarted when one crashes
+;; Use brutal-kill shutdown so restart-all! doesn't wait on graceful timeout.
+(let ([started-a 0] [started-b 0])
+  (let ([sup (start-supervisor
+                'one-for-all
+                (list
+                  (make-child-spec 'a
+                    (lambda ()
+                      (set! started-a (+ started-a 1))
+                      (spawn-actor (lambda (msg) (match msg ['crash (error 'a "crash")]))))
+                    'permanent 'brutal-kill 'worker)
+                  (make-child-spec 'b
+                    (lambda ()
+                      (set! started-b (+ started-b 1))
+                      (spawn-actor (lambda (msg) (void))))
+                    'permanent 'brutal-kill 'worker))
+                10 5)])
+    (wait-ms 50)
+    (send (child-ref sup 'a) 'crash)
+    (wait-ms 300)
+    (test "one-for-all-restarted-a" started-a 2)
+    (test "one-for-all-restarted-b" started-b 2)
+    (actor-kill! sup)))
+
+;; Test 4: permanent — always restarts (even on kill)
+(let ([started 0])
+  (let ([sup (start-supervisor
+                'one-for-one
+                (list (make-child-spec 'w
+                        (lambda ()
+                          (set! started (+ started 1))
+                          (spawn-actor (lambda (msg) (void))))
+                        'permanent 'brutal-kill 'worker))
+                10 5)])
+    (wait-ms 50)
+    (actor-kill! (child-ref sup 'w))
+    (wait-ms 150)
+    (test "permanent-restart" started 2)
+    (actor-kill! sup)))
+
+;; Test 5: temporary — never restarts
+(let ([started 0])
+  (let ([sup (start-supervisor
+                'one-for-one
+                (list (make-child-spec 'w
+                        (lambda ()
+                          (set! started (+ started 1))
+                          (spawn-actor (lambda (msg) (error 'w "crash"))))
+                        'temporary 'brutal-kill 'worker))
+                10 5)])
+    (wait-ms 50)
+    (send (child-ref sup 'w) 'go)
+    (wait-ms 150)
+    (test "temporary-no-restart" started 1)
+    (actor-kill! sup)))
+
+;; Test 6: transient — restarts on crash but not on kill
+(let ([started 0])
+  (let ([sup (start-supervisor
+                'one-for-one
+                (list (make-child-spec 'w
+                        (lambda ()
+                          (set! started (+ started 1))
+                          (spawn-actor (lambda (msg) (error 'w "crash"))))
+                        'transient 'brutal-kill 'worker))
+                10 5)])
+    (wait-ms 50)
+    ;; Crash it (error = abnormal exit) — should restart
+    (send (child-ref sup 'w) 'go)
+    (wait-ms 150)
+    (test "transient-crash-restart" started 2)
+    ;; Kill it (killed = normal-ish) — should NOT restart
+    (actor-kill! (child-ref sup 'w))
+    (wait-ms 150)
+    (test "transient-kill-no-restart" started 2)
+    (actor-kill! sup)))
+
+;; Test 7: dynamic child management
+(let ([sup (start-supervisor 'one-for-one '() 10 5)])
+  (wait-ms 20)
+  (let ([new-ref (supervisor-start-child! sup
+                   (make-child-spec 'dyn
+                     (lambda () (spawn-actor (lambda (msg) (void))))
+                     'permanent 1.0 'worker))])
+    (test "dynamic-start" (actor-ref? new-ref) #t)
+    (supervisor-terminate-child! sup 'dyn)
+    (wait-ms 50)
+    (let ([ch (supervisor-which-children sup)])
+      (test "dynamic-terminate-status"
+            (cadr (assq 'dyn ch))
+            'stopped))
+    (supervisor-delete-child! sup 'dyn)
+    (wait-ms 20)
+    (test "dynamic-delete" (length (supervisor-which-children sup)) 0))
+  (actor-kill! sup))
+
+;; Test 8: restart intensity — supervisor dies after too many restarts
+;; max-restarts=2 within period=1s means the 3rd restart in 1s kills the supervisor.
+(let ([sup (start-supervisor
+              'one-for-one
+              (list (make-child-spec 'c
+                      (lambda () (spawn-actor (lambda (msg) (error 'c "crash"))))
+                      'permanent 'brutal-kill 'worker))
+              2   ;; max 2 restarts
+              1)]) ;; within 1 second
+  (wait-ms 50)
+  ;; Trigger 3 rapid crashes (3rd one exceeds intensity)
+  (send (child-ref sup 'c) 'go)
+  (wait-ms 50)
+  (send (child-ref sup 'c) 'go)
+  (wait-ms 50)
+  (send (child-ref sup 'c) 'go)
+  (wait-ms 300)
+  (test "intensity-sup-dead" (actor-alive? sup) #f))
+
+(printf "~%Results: ~a passed, ~a failed~%" pass fail)
+(when (> fail 0) (exit 1))