erlang: exit-signal propagation + trap_exit (+11 tests)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled

This commit is contained in:
2026-04-25 02:51:32 +00:00
parent c363856df6
commit 1a5a2e8982
6 changed files with 193 additions and 8 deletions

View File

@@ -324,6 +324,29 @@
er-bif-is-reference
(fn (vs) (er-bool (er-ref? (er-bif-arg1 vs "is_reference")))))
(define
er-bif-process-flag
(fn
(vs)
(if
(not (= (len vs) 2))
(error "Erlang: process_flag/2: arity")
(let
((flag (nth vs 0))
(val (nth vs 1))
(me (er-sched-current-pid)))
(cond
(and (er-atom? flag) (= (get flag :name) "trap_exit"))
(let
((old (er-proc-field me :trap-exit)))
(er-proc-set! me :trap-exit (er-truthy? val))
(er-bool old))
:else (error
(str
"Erlang: process_flag: unsupported flag '"
(er-format-value flag)
"'")))))))
(define
er-bif-make-ref
(fn
@@ -551,6 +574,14 @@
(define
er-sched-step!
(fn
(pid)
(cond
(= (er-proc-field pid :state) "dead") nil
:else (er-sched-step-alive! pid))))
(define
er-sched-step-alive!
(fn
(pid)
(er-sched-set-current! pid)
@@ -578,10 +609,103 @@
(er-proc-set! pid :state "dead")
(er-proc-set! pid :exit-reason (get r :reason))
(er-proc-set! pid :exit-result nil)
(er-proc-set! pid :continuation nil))
(er-proc-set! pid :continuation nil)
(er-propagate-exit! pid (get r :reason)))
:else (do
(er-proc-set! pid :state "dead")
(er-proc-set! pid :exit-reason (er-mk-atom "normal"))
(er-proc-set! pid :exit-result r)
(er-proc-set! pid :continuation nil)))))
(er-proc-set! pid :continuation nil)
(er-propagate-exit! pid (er-mk-atom "normal"))))))
(er-sched-set-current! nil)))
;; ── exit-signal propagation ─────────────────────────────────────
;; Called when `pid` finishes (normally or via exit). Walks the
;; process's `:monitored-by` and `:links` lists to deliver `{'DOWN'}`
;; messages and exit signals respectively. Linked processes without
;; `trap_exit` cascade-die with the same reason; those with
;; `trap_exit` true receive an `{'EXIT', From, Reason}` message.
(define
er-propagate-exit!
(fn
(pid reason)
(er-fire-monitors! pid reason)
(er-fire-links! pid reason)))
(define
er-fire-monitors!
(fn
(pid reason)
(let
((mons (er-proc-field pid :monitored-by)))
(for-each
(fn
(i)
(let
((m (nth mons i)))
(let
((from (get m :from)) (ref (get m :ref)))
(when
(and (er-proc-exists? from)
(not (= (er-proc-field from :state) "dead")))
(let
((msg
(er-mk-tuple
(list
(er-mk-atom "DOWN")
ref
(er-mk-atom "process")
pid
reason))))
(er-proc-mailbox-push! from msg)
(when
(= (er-proc-field from :state) "waiting")
(er-proc-set! from :state "runnable")
(er-sched-enqueue! from)))))))
(range 0 (len mons))))))
(define
er-fire-links!
(fn
(pid reason)
(let
((links (er-proc-field pid :links))
(is-normal (er-is-atom-named? reason "normal")))
(for-each
(fn
(i)
(let
((target (nth links i)))
(when
(and (er-proc-exists? target)
(not (= (er-proc-field target :state) "dead")))
(let
((trap (er-proc-field target :trap-exit)))
(cond
trap (er-deliver-exit-msg! target pid reason)
is-normal nil
:else (er-cascade-exit! target reason))))))
(range 0 (len links))))))
(define
er-deliver-exit-msg!
(fn
(target from reason)
(let
((msg
(er-mk-tuple (list (er-mk-atom "EXIT") from reason))))
(er-proc-mailbox-push! target msg)
(when
(= (er-proc-field target :state) "waiting")
(er-proc-set! target :state "runnable")
(er-sched-enqueue! target)))))
(define
er-cascade-exit!
(fn
(target reason)
(er-proc-set! target :state "dead")
(er-proc-set! target :exit-reason reason)
(er-proc-set! target :exit-result nil)
(er-proc-set! target :continuation nil)
(er-propagate-exit! target reason)))

View File

@@ -1,11 +1,11 @@
{
"language": "erlang",
"total_pass": 375,
"total": 375,
"total_pass": 386,
"total": 386,
"suites": [
{"name":"tokenize","pass":62,"total":62,"status":"ok"},
{"name":"parse","pass":52,"total":52,"status":"ok"},
{"name":"eval","pass":191,"total":191,"status":"ok"},
{"name":"eval","pass":202,"total":202,"status":"ok"},
{"name":"runtime","pass":39,"total":39,"status":"ok"},
{"name":"ring","pass":4,"total":4,"status":"ok"},
{"name":"ping-pong","pass":4,"total":4,"status":"ok"},

View File

@@ -1,12 +1,12 @@
# Erlang-on-SX Scoreboard
**Total: 375 / 375 tests passing**
**Total: 386 / 386 tests passing**
| | Suite | Pass | Total |
|---|---|---|---|
| ✅ | tokenize | 62 | 62 |
| ✅ | parse | 52 | 52 |
| ✅ | eval | 191 | 191 |
| ✅ | eval | 202 | 202 |
| ✅ | runtime | 39 | 39 |
| ✅ | ring | 4 | 4 |
| ✅ | ping-pong | 4 | 4 |

View File

@@ -502,6 +502,65 @@
(= (len (er-proc-field (er-mk-pid 1) :monitored-by)) 0)))
true)
;; ── exit-signal propagation + trap_exit ────────────────────────
(er-eval-test "process_flag default false"
(nm (ev "process_flag(trap_exit, true)")) "false")
(er-eval-test "process_flag returns prev"
(nm (ev "process_flag(trap_exit, true), process_flag(trap_exit, false)"))
"true")
;; Monitor fires on normal exit.
(er-eval-test "monitor DOWN normal"
(nm (ev "P = spawn(fun () -> ok end), monitor(process, P), receive {'DOWN', _, process, _, R} -> R end"))
"normal")
;; Monitor fires on abnormal exit.
(er-eval-test "monitor DOWN abnormal"
(nm (ev "P = spawn(fun () -> exit(boom) end), monitor(process, P), receive {'DOWN', _, process, _, R} -> R end"))
"boom")
;; Monitor's ref appears in DOWN message.
(er-eval-test "monitor DOWN ref matches"
(nm (ev "P = spawn(fun () -> exit(bye) end), Ref = monitor(process, P), receive {'DOWN', Ref, process, _, _} -> ok_match end"))
"ok_match")
;; Two monitors -> both fire.
(er-eval-test "two monitors both fire"
(ev "P = spawn(fun () -> exit(crash) end), monitor(process, P), monitor(process, P), receive {'DOWN', _, _, _, _} -> ok end, receive {'DOWN', _, _, _, _} -> 2 end")
2)
;; trap_exit + link + abnormal exit -> {'EXIT', From, Reason} message.
(er-eval-test "trap_exit catches abnormal"
(nm (ev "process_flag(trap_exit, true), P = spawn(fun () -> exit(boom) end), link(P), receive {'EXIT', _, R} -> R end"))
"boom")
;; trap_exit + link + normal exit -> {'EXIT', From, normal}.
(er-eval-test "trap_exit catches normal"
(nm (ev "process_flag(trap_exit, true), P = spawn(fun () -> ok end), link(P), receive {'EXIT', _, R} -> R end"))
"normal")
;; Cascade exit: A links B, B dies abnormally, A dies with same reason.
(er-eval-test "cascade reason"
(do
(ev "A = spawn(fun () -> B = spawn(fun () -> exit(crash) end), link(B), receive forever -> ok end end), receive after 0 -> ok end")
(nm (er-proc-field (er-mk-pid 1) :exit-reason)))
"crash")
;; Normal exit doesn't cascade (without trap_exit) — A's body returns
;; "survived" via the `after` clause and A dies normally.
(er-eval-test "normal exit no cascade"
(do
(ev "A = spawn(fun () -> B = spawn(fun () -> ok end), link(B), receive {'EXIT', _, _} -> got_exit after 50 -> survived end end), receive after 0 -> ok end")
(list
(nm (er-proc-field (er-mk-pid 1) :exit-reason))
(nm (er-proc-field (er-mk-pid 1) :exit-result))))
(list "normal" "survived"))
;; Monitor without trap_exit: monitored proc abnormal doesn't kill the monitor.
(er-eval-test "monitor doesn't cascade"
(nm (ev "P = spawn(fun () -> exit(boom) end), monitor(process, P), receive {'DOWN', _, _, _, _} -> alive end"))
"alive")
(define
er-eval-test-summary
(str "eval " er-eval-test-pass "/" er-eval-test-count))

View File

@@ -572,6 +572,7 @@
(= name "unlink") (er-bif-unlink vs)
(= name "monitor") (er-bif-monitor vs)
(= name "demonitor") (er-bif-demonitor vs)
(= name "process_flag") (er-bif-process-flag vs)
:else (error
(str "Erlang: undefined function '" name "/" (len vs) "'")))))