erlang: try/catch/of/after, Phase 4 complete (+19 tests)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
This commit is contained in:
@@ -531,6 +531,30 @@
|
||||
er-mk-exit-marker
|
||||
(fn (reason) {:tag "er-exit-marker" :reason reason}))
|
||||
|
||||
(define
|
||||
er-mk-throw-marker
|
||||
(fn (reason) {:tag "er-throw-marker" :reason reason}))
|
||||
|
||||
(define
|
||||
er-mk-error-marker
|
||||
(fn (reason) {:tag "er-error-marker" :reason reason}))
|
||||
|
||||
(define
|
||||
er-thrown?
|
||||
(fn
|
||||
(v)
|
||||
(and
|
||||
(= (type-of v) "dict")
|
||||
(= (get v :tag) "er-throw-marker"))))
|
||||
|
||||
(define
|
||||
er-errored?
|
||||
(fn
|
||||
(v)
|
||||
(and
|
||||
(= (type-of v) "dict")
|
||||
(= (get v :tag) "er-error-marker"))))
|
||||
|
||||
(define
|
||||
er-sched-run-all!
|
||||
(fn
|
||||
@@ -592,7 +616,16 @@
|
||||
(guard
|
||||
(c
|
||||
((er-suspended? c) (set-nth! result-ref 0 c))
|
||||
((er-exited? c) (set-nth! result-ref 0 c)))
|
||||
((er-exited? c) (set-nth! result-ref 0 c))
|
||||
((er-thrown? c)
|
||||
(set-nth!
|
||||
result-ref
|
||||
0
|
||||
(er-mk-exit-marker
|
||||
(er-mk-tuple
|
||||
(list (er-mk-atom "nocatch") (get c :reason))))))
|
||||
((er-errored? c)
|
||||
(set-nth! result-ref 0 (er-mk-exit-marker (get c :reason)))))
|
||||
(set-nth!
|
||||
result-ref
|
||||
0
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"language": "erlang",
|
||||
"total_pass": 386,
|
||||
"total": 386,
|
||||
"total_pass": 405,
|
||||
"total": 405,
|
||||
"suites": [
|
||||
{"name":"tokenize","pass":62,"total":62,"status":"ok"},
|
||||
{"name":"parse","pass":52,"total":52,"status":"ok"},
|
||||
{"name":"eval","pass":202,"total":202,"status":"ok"},
|
||||
{"name":"eval","pass":221,"total":221,"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"},
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# Erlang-on-SX Scoreboard
|
||||
|
||||
**Total: 386 / 386 tests passing**
|
||||
**Total: 405 / 405 tests passing**
|
||||
|
||||
| | Suite | Pass | Total |
|
||||
|---|---|---|---|
|
||||
| ✅ | tokenize | 62 | 62 |
|
||||
| ✅ | parse | 52 | 52 |
|
||||
| ✅ | eval | 202 | 202 |
|
||||
| ✅ | eval | 221 | 221 |
|
||||
| ✅ | runtime | 39 | 39 |
|
||||
| ✅ | ring | 4 | 4 |
|
||||
| ✅ | ping-pong | 4 | 4 |
|
||||
|
||||
@@ -561,6 +561,79 @@
|
||||
(nm (ev "P = spawn(fun () -> exit(boom) end), monitor(process, P), receive {'DOWN', _, _, _, _} -> alive end"))
|
||||
"alive")
|
||||
|
||||
;; ── try / catch / of / after ─────────────────────────────────
|
||||
(er-eval-test "try plain"
|
||||
(ev "try 1 + 2 catch _ -> oops end") 3)
|
||||
|
||||
(er-eval-test "try throw caught"
|
||||
(nm (ev "try throw(boom) catch throw:X -> X end")) "boom")
|
||||
(er-eval-test "try error caught"
|
||||
(nm (ev "try error(crash) catch error:X -> X end")) "crash")
|
||||
(er-eval-test "try exit caught"
|
||||
(nm (ev "try exit(quit) catch exit:X -> X end")) "quit")
|
||||
|
||||
(er-eval-test "default class is throw"
|
||||
(nm (ev "try throw(bye) catch X -> X end")) "bye")
|
||||
(er-eval-test "default class doesn't catch error"
|
||||
(do
|
||||
(ev "P = spawn(fun () -> try error(crash) catch X -> X end end), receive after 0 -> ok end")
|
||||
(nm (er-proc-field (er-mk-pid 1) :exit-reason)))
|
||||
"crash")
|
||||
|
||||
;; of clauses
|
||||
(er-eval-test "try of single"
|
||||
(ev "try 42 of N -> N * 2 catch _ -> 0 end") 84)
|
||||
(er-eval-test "try of multi"
|
||||
(nm (ev "try ok of ok -> matched; _ -> nope catch _ -> oops end"))
|
||||
"matched")
|
||||
(er-eval-test "try of fallthrough"
|
||||
(nm (ev "try x of ok -> a; error -> b; _ -> default catch _ -> oops end"))
|
||||
"default")
|
||||
(er-eval-test "try of with guard"
|
||||
(nm (ev "try 5 of N when N > 0 -> pos; _ -> nonneg catch _ -> oops end"))
|
||||
"pos")
|
||||
|
||||
;; after clause
|
||||
(er-eval-test "after on success"
|
||||
(do (er-io-flush!)
|
||||
(ev "try 7 after io:format(\"a\") end")
|
||||
(er-io-buffer-content))
|
||||
"a")
|
||||
(er-eval-test "after on caught"
|
||||
(do (er-io-flush!)
|
||||
(ev "try throw(b) catch throw:_ -> caught after io:format(\"x\") end")
|
||||
(er-io-buffer-content))
|
||||
"x")
|
||||
(er-eval-test "after returns body value"
|
||||
(ev "try 99 after 0 end") 99)
|
||||
(er-eval-test "try preserves catch result"
|
||||
(nm (ev "try throw(x) catch throw:_ -> recovered after 0 end"))
|
||||
"recovered")
|
||||
|
||||
;; nested try
|
||||
(er-eval-test "try nested catch outer"
|
||||
(nm (ev "try (try throw(inner) catch error:_ -> bad end) catch throw:X -> X end"))
|
||||
"inner")
|
||||
(er-eval-test "try nested catch inner"
|
||||
(nm (ev "try (try throw(inner) catch throw:X -> X end) catch _ -> outer end"))
|
||||
"inner")
|
||||
|
||||
;; class re-raise on no-match
|
||||
(er-eval-test "throw without catch-throw escapes"
|
||||
(do
|
||||
(ev "P = spawn(fun () -> try throw(bye) catch error:_ -> nope end end), receive after 0 -> ok end")
|
||||
(let ((reason (er-proc-field (er-mk-pid 1) :exit-reason)))
|
||||
(and (er-tuple? reason) (nm (nth (get reason :elements) 0)))))
|
||||
"nocatch")
|
||||
|
||||
;; multi-clause catch
|
||||
(er-eval-test "multi-clause catch picks throw"
|
||||
(nm (ev "try throw(a) catch error:X -> e; throw:X -> t; exit:X -> x end"))
|
||||
"t")
|
||||
(er-eval-test "multi-clause catch picks exit"
|
||||
(nm (ev "try exit(a) catch error:X -> e; throw:X -> t; exit:X -> x end"))
|
||||
"x")
|
||||
|
||||
(define
|
||||
er-eval-test-summary
|
||||
(str "eval " er-eval-test-pass "/" er-eval-test-count))
|
||||
|
||||
@@ -122,6 +122,7 @@
|
||||
(= ty "fun") (er-eval-fun node env)
|
||||
(= ty "send") (er-eval-send node env)
|
||||
(= ty "receive") (er-eval-receive node env)
|
||||
(= ty "try") (er-eval-try node env)
|
||||
(= ty "match") (er-eval-match node env)
|
||||
:else (error (str "Erlang eval: unsupported node type '" ty "'"))))))
|
||||
|
||||
@@ -573,6 +574,8 @@
|
||||
(= name "monitor") (er-bif-monitor vs)
|
||||
(= name "demonitor") (er-bif-demonitor vs)
|
||||
(= name "process_flag") (er-bif-process-flag vs)
|
||||
(= name "throw") (raise (er-mk-throw-marker (er-bif-arg1 vs "throw")))
|
||||
(= name "error") (raise (er-mk-error-marker (er-bif-arg1 vs "error")))
|
||||
:else (error
|
||||
(str "Erlang: undefined function '" name "/" (len vs) "'")))))
|
||||
|
||||
@@ -1089,3 +1092,143 @@
|
||||
(do
|
||||
(er-env-restore! env snap)
|
||||
(er-try-receive-clauses clauses msg env (+ i 1))))))))
|
||||
|
||||
;; ── try/of/catch/after ────────────────────────────────────────────
|
||||
;; The outer guard captures any exception so the `after` body is
|
||||
;; guaranteed to run, then re-raises. The inner guard runs the
|
||||
;; expression body, optional `of` clauses on success, and `catch`
|
||||
;; clauses on a thrown/erred/exited condition. If no catch clause
|
||||
;; matches the raised class+pattern, the inner guard's clause
|
||||
;; re-raises by returning nothing (handled via re-raise marker).
|
||||
(define
|
||||
er-eval-try
|
||||
(fn
|
||||
(node env)
|
||||
(let
|
||||
((after-body (get node :after))
|
||||
(saved-exc (list nil))
|
||||
(result-ref (list nil)))
|
||||
(guard
|
||||
(c (:else (do (set-nth! saved-exc 0 c) nil)))
|
||||
(set-nth! result-ref 0 (er-eval-try-inner node env)))
|
||||
(when
|
||||
(> (len after-body) 0)
|
||||
(er-eval-body after-body env))
|
||||
(if
|
||||
(= (nth saved-exc 0) nil)
|
||||
(nth result-ref 0)
|
||||
(raise (nth saved-exc 0))))))
|
||||
|
||||
(define
|
||||
er-eval-try-inner
|
||||
(fn
|
||||
(node env)
|
||||
(let
|
||||
((catch-clauses (get node :catch-clauses))
|
||||
(of-clauses (get node :of-clauses))
|
||||
(caught-ref (list false))
|
||||
(result-ref (list nil))
|
||||
(re-raise-ref (list nil)))
|
||||
(guard
|
||||
(c
|
||||
((er-thrown? c)
|
||||
(er-eval-try-catch
|
||||
catch-clauses "throw" (get c :reason) env
|
||||
caught-ref result-ref re-raise-ref))
|
||||
((er-errored? c)
|
||||
(er-eval-try-catch
|
||||
catch-clauses "error" (get c :reason) env
|
||||
caught-ref result-ref re-raise-ref))
|
||||
((er-exited? c)
|
||||
(er-eval-try-catch
|
||||
catch-clauses "exit" (get c :reason) env
|
||||
caught-ref result-ref re-raise-ref)))
|
||||
(let
|
||||
((r (er-eval-body (get node :exprs) env)))
|
||||
(if
|
||||
(= (len of-clauses) 0)
|
||||
(set-nth! result-ref 0 r)
|
||||
(set-nth!
|
||||
result-ref
|
||||
0
|
||||
(er-eval-of-clauses of-clauses r env 0)))))
|
||||
(when (not (= (nth re-raise-ref 0) nil))
|
||||
(raise (nth re-raise-ref 0)))
|
||||
(nth result-ref 0))))
|
||||
|
||||
;; Try catch-clauses against (Class, Reason). If a clause matches,
|
||||
;; runs its body and writes to result-ref. If none match, queues a
|
||||
;; re-raise marker.
|
||||
(define
|
||||
er-eval-try-catch
|
||||
(fn
|
||||
(clauses class-name reason env caught-ref result-ref re-raise-ref)
|
||||
(er-eval-try-catch-iter
|
||||
clauses class-name reason env 0 caught-ref result-ref re-raise-ref)))
|
||||
|
||||
(define
|
||||
er-eval-try-catch-iter
|
||||
(fn
|
||||
(clauses class-name reason env i caught-ref result-ref re-raise-ref)
|
||||
(if
|
||||
(>= i (len clauses))
|
||||
(set-nth!
|
||||
re-raise-ref
|
||||
0
|
||||
(er-mk-class-marker class-name reason))
|
||||
(let
|
||||
((c (nth clauses i))
|
||||
(snap (er-env-copy env))
|
||||
(clause-class (get (get c :class) :value)))
|
||||
(cond
|
||||
(not (= clause-class class-name))
|
||||
(er-eval-try-catch-iter
|
||||
clauses class-name reason env (+ i 1)
|
||||
caught-ref result-ref re-raise-ref)
|
||||
:else
|
||||
(if
|
||||
(and
|
||||
(er-match! (get c :pattern) reason env)
|
||||
(er-eval-guards (get c :guards) env))
|
||||
(do
|
||||
(set-nth! caught-ref 0 true)
|
||||
(set-nth!
|
||||
result-ref
|
||||
0
|
||||
(er-eval-body (get c :body) env)))
|
||||
(do
|
||||
(er-env-restore! env snap)
|
||||
(er-eval-try-catch-iter
|
||||
clauses class-name reason env (+ i 1)
|
||||
caught-ref result-ref re-raise-ref))))))))
|
||||
|
||||
(define
|
||||
er-mk-class-marker
|
||||
(fn
|
||||
(class-name reason)
|
||||
(cond
|
||||
(= class-name "throw") (er-mk-throw-marker reason)
|
||||
(= class-name "error") (er-mk-error-marker reason)
|
||||
(= class-name "exit") (er-mk-exit-marker reason)
|
||||
:else (er-mk-error-marker reason))))
|
||||
|
||||
(define
|
||||
er-eval-of-clauses
|
||||
(fn
|
||||
(clauses subject env i)
|
||||
(if
|
||||
(>= i (len clauses))
|
||||
(raise
|
||||
(er-mk-error-marker
|
||||
(er-mk-tuple
|
||||
(list (er-mk-atom "try_clause") subject))))
|
||||
(let
|
||||
((c (nth clauses i)) (snap (er-env-copy env)))
|
||||
(if
|
||||
(and
|
||||
(er-match! (get c :pattern) subject env)
|
||||
(er-eval-guards (get c :guards) env))
|
||||
(er-eval-body (get c :body) env)
|
||||
(do
|
||||
(er-env-restore! env snap)
|
||||
(er-eval-of-clauses clauses subject env (+ i 1))))))))
|
||||
|
||||
@@ -81,7 +81,7 @@ Core mapping:
|
||||
### Phase 4 — links, monitors, exit signals
|
||||
- [x] `link/1`, `unlink/1`, `monitor/2`, `demonitor/1` — **17 new eval tests**; `make_ref/0`, `is_reference/1`, refs in `=:=`/format wired
|
||||
- [x] Exit-signal propagation; trap_exit flag — **11 new eval tests**; `process_flag/2`, monitor `{'DOWN', ...}`, `{'EXIT', From, Reason}` for trap-exit links, cascade death without trap_exit
|
||||
- [ ] `try/catch/of/end`
|
||||
- [x] `try/catch/of/end` — **19 new eval tests**; `throw/1`, `error/1` BIFs; `nocatch` re-raise wrapping for uncaught throws
|
||||
|
||||
### Phase 5 — modules + OTP-lite
|
||||
- [ ] `-module(M).` loading, `M:F(...)` calls across modules
|
||||
@@ -99,6 +99,7 @@ Core mapping:
|
||||
|
||||
_Newest first._
|
||||
|
||||
- **2026-04-25 try/catch/of/after green — Phase 4 complete** — Three new exception markers in runtime: `er-mk-throw-marker`, `er-mk-error-marker` alongside the existing `er-mk-exit-marker`; `er-thrown?`, `er-errored?` predicates. `throw/1` and `error/1` BIFs raise their respective markers. Scheduler step's guard now also catches throw/error: an uncaught throw becomes `exit({nocatch, X})`, an uncaught error becomes `exit(X)`. `er-eval-try` uses two-layer guard: outer captures any exception so the `after` body runs (then re-raises); inner catches throw/error/exit and dispatches to `catch` clauses by class name + pattern + guard. No matching catch clause re-raises with the same class via `er-mk-class-marker`. `of` clauses run on success; no-match raises `error({try_clause, V})`. 19 new eval tests: plain success, all three classes caught, default-class behaviour (throw), of-clause matching incl. fallthrough + guard, after on success/error/value-preservation, nested try, class re-raise wrapping, multi-clause catch dispatch. Total suite 405/405. **Phase 4 complete — Phase 5 (modules + OTP-lite) is next.** Gotcha: SX's `dynamic-wind` doesn't interact with `guard` — exceptions inside dynamic-wind body propagate past the surrounding guard untouched, so the `after`-runs-on-exception semantics had to be wired with two manual nested guards instead.
|
||||
- **2026-04-25 exit-signal propagation + trap_exit green** — `process_flag(trap_exit, Bool)` BIF returns the prior value. After every scheduler step that ends with a process dead, `er-propagate-exit!` walks `:monitored-by` (delivers `{'DOWN', Ref, process, From, Reason}` to each monitor + re-enqueues if waiting) and `:links` (with `trap_exit=true` -> deliver `{'EXIT', From, Reason}` and re-enqueue; `trap_exit=false` + abnormal reason -> recursive `er-cascade-exit!`; normal reason without trap_exit -> no signal). `er-sched-step!` short-circuits if the popped pid is already dead (could be cascade-killed mid-drain). 11 new eval tests: process_flag default + persistence, monitor DOWN on normal/abnormal/ref-bound, two monitors both fire, trap_exit catches abnormal/normal, cascade reason recorded on linked proc, normal-link no cascade (proc returns via `after` clause), monitor without trap_exit doesn't kill the monitor. Total suite 386/386. `kill`-as-special-reason and `exit/2` (signal to another) deferred.
|
||||
- **2026-04-25 link/unlink/monitor/demonitor + refs green** — Refs added to scheduler (`:next-ref`, `er-ref-new!`); `er-mk-ref`, `er-ref?`, `er-ref-equal?` in runtime. Process record gains `:monitored-by`. New BIFs in `lib/erlang/runtime.sx`: `make_ref/0`, `is_reference/1`, `link/1` (bidirectional, no-op for self, raises `noproc` for missing target), `unlink/1` (removes both sides; tolerates missing target), `monitor(process, Pid)` (returns fresh ref, adds entries to monitor's `:monitors` and target's `:monitored-by`), `demonitor(Ref)` (purges both sides). Refs participate in `er-equal?` (id compare) and render as `#Ref<N>`. 17 new eval tests covering `make_ref` distinctness, link return values, bidirectional link recording, unlink clearing both sides, monitor recording both sides, demonitor purging. Total suite 375/375. Signal propagation (the next checkbox) will hook into these data structures.
|
||||
- **2026-04-25 ring benchmark recorded — Phase 3 closed** — `lib/erlang/bench_ring.sh` runs the ring at N ∈ {10, 50, 100, 500, 1000} and times each end-to-end via wall clock. `lib/erlang/bench_ring_results.md` captures the table. Throughput plateaus at ~30-34 hops/s. 1M-process target IS NOT MET in this architecture — extrapolation = ~9h. The sub-task is ticked as complete with that fact recorded inline because the perf gap is architectural (env-copy per call, call/cc per receive, mailbox rebuild on delete-at) and out of scope for this loop's iterations. Phase 3 done; Phase 4 (links, monitors, exit signals, try/catch) is next.
|
||||
|
||||
Reference in New Issue
Block a user