erlang: -module/M:F cross-module calls (+10 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:
@@ -742,3 +742,75 @@
|
||||
(er-proc-set! target :exit-result nil)
|
||||
(er-proc-set! target :continuation nil)
|
||||
(er-propagate-exit! target reason)))
|
||||
|
||||
;; ── module registry ─────────────────────────────────────────────
|
||||
;; Global mutable dict from module name -> module env (which itself
|
||||
;; binds each function name to a fun value capturing the same env, so
|
||||
;; sibling functions can call each other recursively).
|
||||
(define er-modules (list {}))
|
||||
(define er-modules-get (fn () (nth er-modules 0)))
|
||||
(define er-modules-reset! (fn () (set-nth! er-modules 0 {})))
|
||||
|
||||
;; Load an Erlang module declaration. Source must start with
|
||||
;; `-module(Name).` and contain function definitions. Functions
|
||||
;; sharing a name (different arities) get their clauses concatenated
|
||||
;; into a single fun value — `er-apply-fun-clauses` already filters
|
||||
;; by arity, so multi-arity dispatch falls out for free.
|
||||
(define
|
||||
erlang-load-module
|
||||
(fn
|
||||
(src)
|
||||
(let
|
||||
((module-ast (er-parse-module src)))
|
||||
(let
|
||||
((mod-name (get module-ast :name))
|
||||
(functions (get module-ast :functions))
|
||||
(mod-env (er-env-new))
|
||||
(by-name {}))
|
||||
(for-each
|
||||
(fn
|
||||
(i)
|
||||
(let
|
||||
((f (nth functions i)))
|
||||
(let
|
||||
((name (get f :name)) (clauses (get f :clauses)))
|
||||
(if
|
||||
(dict-has? by-name name)
|
||||
(let
|
||||
((existing (get by-name name)))
|
||||
(for-each
|
||||
(fn (j) (append! existing (nth clauses j)))
|
||||
(range 0 (len clauses))))
|
||||
(let
|
||||
((init (list)))
|
||||
(for-each
|
||||
(fn (j) (append! init (nth clauses j)))
|
||||
(range 0 (len clauses)))
|
||||
(dict-set! by-name name init))))))
|
||||
(range 0 (len functions)))
|
||||
(for-each
|
||||
(fn
|
||||
(k)
|
||||
(let
|
||||
((all-clauses (get by-name k)))
|
||||
(er-env-bind! mod-env k (er-mk-fun all-clauses mod-env))))
|
||||
(keys by-name))
|
||||
(dict-set! (er-modules-get) mod-name mod-env)
|
||||
(er-mk-atom mod-name)))))
|
||||
|
||||
(define
|
||||
er-apply-user-module
|
||||
(fn
|
||||
(mod name vs)
|
||||
(let
|
||||
((mod-env (get (er-modules-get) mod)))
|
||||
(if
|
||||
(not (dict-has? mod-env name))
|
||||
(raise
|
||||
(er-mk-error-marker
|
||||
(er-mk-tuple
|
||||
(list
|
||||
(er-mk-atom "undef")
|
||||
(er-mk-atom mod)
|
||||
(er-mk-atom name)))))
|
||||
(er-apply-fun (get mod-env name) vs)))))
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"language": "erlang",
|
||||
"total_pass": 405,
|
||||
"total": 405,
|
||||
"total_pass": 415,
|
||||
"total": 415,
|
||||
"suites": [
|
||||
{"name":"tokenize","pass":62,"total":62,"status":"ok"},
|
||||
{"name":"parse","pass":52,"total":52,"status":"ok"},
|
||||
{"name":"eval","pass":221,"total":221,"status":"ok"},
|
||||
{"name":"eval","pass":231,"total":231,"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: 405 / 405 tests passing**
|
||||
**Total: 415 / 415 tests passing**
|
||||
|
||||
| | Suite | Pass | Total |
|
||||
|---|---|---|---|
|
||||
| ✅ | tokenize | 62 | 62 |
|
||||
| ✅ | parse | 52 | 52 |
|
||||
| ✅ | eval | 221 | 221 |
|
||||
| ✅ | eval | 231 | 231 |
|
||||
| ✅ | runtime | 39 | 39 |
|
||||
| ✅ | ring | 4 | 4 |
|
||||
| ✅ | ping-pong | 4 | 4 |
|
||||
|
||||
@@ -634,6 +634,71 @@
|
||||
(nm (ev "try exit(a) catch error:X -> e; throw:X -> t; exit:X -> x end"))
|
||||
"x")
|
||||
|
||||
;; ── modules: -module(M)., M:F/N cross-module calls ─────────────
|
||||
(er-eval-test "load module returns name"
|
||||
(nm (erlang-load-module "-module(m1). foo() -> 42."))
|
||||
"m1")
|
||||
|
||||
(er-eval-test "cross-module zero-arity"
|
||||
(do
|
||||
(erlang-load-module "-module(m2). val() -> 7.")
|
||||
(ev "m2:val()"))
|
||||
7)
|
||||
|
||||
(er-eval-test "cross-module n-ary"
|
||||
(do
|
||||
(erlang-load-module "-module(m3). add(X, Y) -> X + Y.")
|
||||
(ev "m3:add(3, 4)"))
|
||||
7)
|
||||
|
||||
(er-eval-test "module recursive fn"
|
||||
(do
|
||||
(erlang-load-module "-module(m4). fact(0) -> 1; fact(N) -> N * fact(N-1).")
|
||||
(ev "m4:fact(6)"))
|
||||
720)
|
||||
|
||||
(er-eval-test "module sibling calls"
|
||||
(do
|
||||
(erlang-load-module "-module(m5). a(X) -> b(X) + 1. b(X) -> X * 10.")
|
||||
(ev "m5:a(5)"))
|
||||
51)
|
||||
|
||||
(er-eval-test "module multi-arity"
|
||||
(do
|
||||
(erlang-load-module
|
||||
"-module(m6). f(X) -> X. f(X, Y) -> X + Y. f(X, Y, Z) -> X * Y + Z.")
|
||||
(ev "{m6:f(1), m6:f(2, 3), m6:f(2, 3, 4)}"))
|
||||
(er-mk-tuple (list 1 5 10)))
|
||||
|
||||
(er-eval-test "module pattern match clauses"
|
||||
(do
|
||||
(erlang-load-module
|
||||
"-module(m7). check(0) -> zero; check(N) when N > 0 -> pos; check(_) -> neg.")
|
||||
(nm (ev "m7:check(-3)")))
|
||||
"neg")
|
||||
|
||||
(er-eval-test "cross-module call within module"
|
||||
(do
|
||||
(erlang-load-module "-module(util1). dbl(X) -> X * 2.")
|
||||
(erlang-load-module "-module(util2). quad(X) -> util1:dbl(X) * 2.")
|
||||
(ev "util2:quad(5)"))
|
||||
20)
|
||||
|
||||
(er-eval-test "module undefined fn raises"
|
||||
(do
|
||||
(erlang-load-module "-module(m8). foo() -> 1.")
|
||||
(er-io-flush!)
|
||||
(ev "P = spawn(fun () -> m8:bar() 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)))))
|
||||
"undef")
|
||||
|
||||
(er-eval-test "module function used in spawn"
|
||||
(do
|
||||
(erlang-load-module "-module(m9). work(P) -> P ! done.")
|
||||
(ev "Me = self(), spawn(fun () -> m9:work(Me) end), receive done -> ok end"))
|
||||
(er-mk-atom "ok"))
|
||||
|
||||
(define
|
||||
er-eval-test-summary
|
||||
(str "eval " er-eval-test-pass "/" er-eval-test-count))
|
||||
|
||||
@@ -479,7 +479,12 @@
|
||||
((fun-node (get node :fun)) (args (get node :args)))
|
||||
(cond
|
||||
(= (get fun-node :type) "atom")
|
||||
(er-apply-bif (get fun-node :value) (er-eval-args args env))
|
||||
(let
|
||||
((name (get fun-node :value)) (vs (er-eval-args args env)))
|
||||
(cond
|
||||
(and (dict-has? env name) (er-fun? (get env name)))
|
||||
(er-apply-fun (get env name) vs)
|
||||
:else (er-apply-bif name vs)))
|
||||
(= (get fun-node :type) "remote")
|
||||
(er-apply-remote-bif
|
||||
(get (get fun-node :mod) :value)
|
||||
@@ -584,6 +589,8 @@
|
||||
(fn
|
||||
(mod name vs)
|
||||
(cond
|
||||
(dict-has? (er-modules-get) mod)
|
||||
(er-apply-user-module mod name vs)
|
||||
(= mod "lists") (er-apply-lists-bif name vs)
|
||||
(= mod "io") (er-apply-io-bif name vs)
|
||||
(= mod "erlang") (er-apply-bif name vs)
|
||||
|
||||
@@ -84,7 +84,7 @@ Core mapping:
|
||||
- [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
|
||||
- [x] `-module(M).` loading, `M:F(...)` calls across modules — **10 new eval tests**; multi-arity, sibling calls, cross-module dispatch via `er-modules` registry
|
||||
- [ ] `gen_server` behaviour (the big OTP win)
|
||||
- [ ] `supervisor` (simple one-for-one)
|
||||
- [ ] Registered processes: `register/2`, `whereis/1`
|
||||
@@ -99,6 +99,7 @@ Core mapping:
|
||||
|
||||
_Newest first._
|
||||
|
||||
- **2026-04-25 modules + cross-module calls green** — `er-modules` global registry (`{module-name -> mod-env}`) in `lib/erlang/runtime.sx`. `erlang-load-module SRC` parses a module declaration, groups functions by name (concatenating clauses across arities so multi-arity falls out of `er-apply-fun-clauses`'s arity filter), creates fun-values capturing the same `mod-env` so siblings see each other recursively, registers under `:name`. `er-apply-remote-bif` checks user modules first, then built-ins (`lists`, `io`, `erlang`). `er-eval-call` for atom-typed call targets now consults the current env first — local calls inside a module body resolve sibling functions via `mod-env`. Undefined cross-module call raises `error({undef, Mod, Fun})`. 10 new eval tests: load returns module name, zero-/n-ary cross-module call, recursive fact/6 = 720, sibling-call `c:a/1` ↦ `c:b/1`, multi-arity dispatch (`/1`, `/2`, `/3`), pattern + guard clauses, cross-module call from within another module, undefined fn raises `undef`, module fn used in spawn. Total suite 415/415.
|
||||
- **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.
|
||||
|
||||
Reference in New Issue
Block a user