erlang: -module/M:F cross-module calls (+10 tests)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled

This commit is contained in:
2026-04-25 04:01:14 +00:00
parent 882205aa70
commit 424b5ca472
6 changed files with 152 additions and 7 deletions

View File

@@ -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)))))

View File

@@ -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"},

View File

@@ -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 |

View File

@@ -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))

View File

@@ -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)