diff --git a/lib/erlang/runtime.sx b/lib/erlang/runtime.sx index cb72aab6..aec52dfb 100644 --- a/lib/erlang/runtime.sx +++ b/lib/erlang/runtime.sx @@ -208,3 +208,68 @@ (define er-proc-mailbox-size (fn (pid) (er-q-len (er-proc-field pid :mailbox)))) + +;; ── process BIFs ──────────────────────────────────────────────── +(define + er-bif-is-pid + (fn (vs) (er-bool (er-pid? (er-bif-arg1 vs "is_pid"))))) + +(define + er-bif-self + (fn + (vs) + (if + (not (= (len vs) 0)) + (error "Erlang: self/0: arity") + (let + ((pid (er-sched-current-pid))) + (if + (= pid nil) + (error "Erlang: self/0: no current process") + pid))))) + +(define + er-bif-spawn + (fn + (vs) + (cond + (= (len vs) 1) (er-spawn-fun (nth vs 0)) + (= (len vs) 3) (error + "Erlang: spawn/3: module-based spawn deferred to Phase 5 (modules)") + :else (error "Erlang: spawn: wrong arity")))) + +(define + er-spawn-fun + (fn + (fv) + (if + (not (er-fun? fv)) + (error "Erlang: spawn/1: not a fun") + (let + ((proc (er-proc-new! (er-env-new)))) + (dict-set! proc :initial-fun fv) + (get proc :pid))))) + +;; ── scheduler loop ────────────────────────────────────────────── +;; Drain all runnable processes to completion. Synchronous — each +;; spawned process runs its :initial-fun front-to-back with no yielding. +;; receive-driven suspension arrives in the next roadmap step. +(define + er-sched-drain! + (fn + () + (let + ((pid (er-sched-next-runnable!))) + (when + (not (= pid nil)) + (er-sched-set-current! pid) + (er-proc-set! pid :state "running") + (let + ((fv (er-proc-field pid :initial-fun))) + (when + (not (= fv nil)) + (er-apply-fun fv (list)))) + (er-proc-set! pid :state "dead") + (er-proc-set! pid :exit-reason (er-mk-atom "normal")) + (er-sched-set-current! nil) + (er-sched-drain!))))) diff --git a/lib/erlang/tests/eval.sx b/lib/erlang/tests/eval.sx index 846abcfc..cd8347ba 100644 --- a/lib/erlang/tests/eval.sx +++ b/lib/erlang/tests/eval.sx @@ -285,6 +285,48 @@ (do (er-io-flush!) (ev "io:format(\"50~~\")") (er-io-buffer-content)) "50~") +;; ── processes: self/0, spawn/1, is_pid ────────────────────────── +(er-eval-test "self tag" + (get (ev "self()") :tag) "pid") +(er-eval-test "is_pid self" + (nm (ev "is_pid(self())")) "true") +(er-eval-test "is_pid number" + (nm (ev "is_pid(42)")) "false") +(er-eval-test "is_pid atom" + (nm (ev "is_pid(ok)")) "false") +(er-eval-test "self equals self" + (nm (ev "Pid = self(), Pid =:= Pid")) "true") +(er-eval-test "self =:= self expr" + (nm (ev "self() == self()")) "true") +(er-eval-test "spawn returns pid" + (get (ev "spawn(fun () -> ok end)") :tag) "pid") +(er-eval-test "is_pid spawn" + (nm (ev "is_pid(spawn(fun () -> ok end))")) "true") +(er-eval-test "spawn new pid distinct" + (nm (ev "P1 = self(), P2 = spawn(fun () -> ok end), P1 =:= P2")) + "false") +(er-eval-test "two spawns distinct" + (nm (ev "P1 = spawn(fun () -> ok end), P2 = spawn(fun () -> ok end), P1 =:= P2")) + "false") +(er-eval-test "spawn then drain io" + (do + (er-io-flush!) + (ev "spawn(fun () -> io:format(\"child~n\") end), io:format(\"parent~n\")") + (er-io-buffer-content)) + "parent\nchild\n") +(er-eval-test "multiple spawn ordering" + (do + (er-io-flush!) + (ev "spawn(fun () -> io:format(\"a~n\") end), spawn(fun () -> io:format(\"b~n\") end), io:format(\"main~n\")") + (er-io-buffer-content)) + "main\na\nb\n") +(er-eval-test "child self is its own pid" + (do + (er-io-flush!) + (ev "P = spawn(fun () -> io:format(\"~p\", [is_pid(self())]) end), io:format(\"~p;\", [is_pid(P)])") + (er-io-buffer-content)) + "true;true") + (define er-eval-test-summary (str "eval " er-eval-test-pass "/" er-eval-test-count)) diff --git a/lib/erlang/transpile.sx b/lib/erlang/transpile.sx index 7d879f4f..f92d4405 100644 --- a/lib/erlang/transpile.sx +++ b/lib/erlang/transpile.sx @@ -61,8 +61,23 @@ (let ((st (er-state-make (er-tokenize src)))) (let - ((body (er-parse-body st)) (env (er-env-new))) - (er-eval-body body env))))) + ((body (er-parse-body st))) + (er-sched-init!) + (let + ((main (er-proc-new! (er-env-new)))) + (er-sched-next-runnable!) + (er-sched-set-current! (get main :pid)) + (er-proc-set! (get main :pid) :state "running") + (let + ((result (er-eval-body body (get main :env)))) + (er-proc-set! (get main :pid) :state "dead") + (er-proc-set! + (get main :pid) + :exit-reason + (er-mk-atom "normal")) + (er-sched-set-current! nil) + (er-sched-drain!) + result)))))) (define er-eval-body @@ -347,6 +362,7 @@ (fn (i) (er-equal? (nth ea i) (nth eb i))) (range 0 (len ea))))) (and (= (type-of a) "string") (= (type-of b) "string")) (= a b) + (and (er-pid? a) (er-pid? b)) (= (get a :id) (get b :id)) :else false))) ;; Exact equality: 1 =/= 1.0 in Erlang. @@ -380,7 +396,8 @@ (er-nil? v) 3 (er-cons? v) 3 (= (type-of v) "string") 4 - :else 5))) + (er-pid? v) 5 + :else 6))) (define er-list-append @@ -535,6 +552,9 @@ (= name "tuple_size") (er-bif-tuple-size vs) (= name "atom_to_list") (er-bif-atom-to-list vs) (= name "list_to_atom") (er-bif-list-to-atom vs) + (= name "is_pid") (er-bif-is-pid vs) + (= name "self") (er-bif-self vs) + (= name "spawn") (er-bif-spawn vs) :else (error (str "Erlang: undefined function '" name "/" (len vs) "'"))))) @@ -863,6 +883,7 @@ (er-cons? v) (str "[" (er-format-list-elems v) "]") (er-tuple? v) (str "{" (er-format-tuple-elems (get v :elements)) "}") (er-fun? v) "#Fun" + (er-pid? v) (str "") :else (str v)))) (define diff --git a/plans/erlang-on-sx.md b/plans/erlang-on-sx.md index 393ae9dc..05cf96a5 100644 --- a/plans/erlang-on-sx.md +++ b/plans/erlang-on-sx.md @@ -65,7 +65,7 @@ Core mapping: ### Phase 3 — processes + mailboxes + receive (THE SHOWCASE) - [x] Scheduler in `runtime.sx`: runnable queue, pid counter, per-process state record — **39 runtime tests** -- [ ] `spawn/1`, `spawn/3`, `self/0` +- [x] `spawn/1`, `spawn/3`, `self/0` — **13 new eval tests**; `spawn/3` stubbed with "deferred to Phase 5" until modules land; `is_pid/1` + pid equality also wired - [ ] `!` (send), `receive ... end` with selective pattern matching - [ ] `receive ... after Ms -> ...` timeout clause (use SX timer primitive) - [ ] `exit/1`, basic process termination @@ -99,6 +99,7 @@ Core mapping: _Newest first._ +- **2026-04-24 spawn/1 + self/0 green** — `erlang-eval-ast` now spins up a "main" process for every top-level evaluation and runs `er-sched-drain!` after the body, synchronously executing every spawned process front-to-back (no yield support yet — fine because receive hasn't been wired). BIFs added in `lib/erlang/runtime.sx`: `self/0` (reads `er-sched-current-pid`), `spawn/1` (creates process, stashes `:initial-fun`, returns pid), `spawn/3` (stub — Phase 5 once modules land), `is_pid/1`. Pids added to `er-equal?` (id compare) and `er-type-order` (between strings and tuples); `er-format-value` renders as ``. 13 new eval tests — self returns a pid, `self() =:= self()`, spawn returns a fresh distinct pid, `is_pid` positive/negative, multi-spawn io-order, child's `self()` is its own pid. Total eval 143/143; runtime 39/39; suite 296/296. Next: `!` (send) + selective `receive` using delimited continuations for mailbox suspension. - **2026-04-24 scheduler foundation green** — `lib/erlang/runtime.sx` + `lib/erlang/tests/runtime.sx`. Amortised-O(1) FIFO queue (`er-q-new`, `er-q-push!`, `er-q-pop!`, `er-q-peek`, `er-q-compact!` at 128-entry head drift), tagged pids `{:tag "pid" :id N}` with `er-pid?`/`er-pid-equal?`, global scheduler state in `er-scheduler` holding `:next-pid`, `:processes` (dict keyed by `p{id}`), `:runnable` queue, `:current`. Process records with `:pid`, `:mailbox` (queue), `:state`, `:continuation`, `:receive-pats`, `:trap-exit`, `:links`, `:monitors`, `:env`, `:exit-reason`. 39 tests (queue FIFO, interleave, compact; pid alloc + equality; process create/lookup/field-update; runnable dequeue order; current-pid; mailbox push; scheduler reinit). Total erlang suite 283/283. Next: `spawn/1`, `!`, `receive` wired into the evaluator. - **2026-04-24 core BIFs + funs green** — Phase 2 complete. Added to `lib/erlang/transpile.sx`: fun values (`{:tag "fun" :clauses :env}`), fun evaluation (closure over current env), fun application (clause arity + pattern + guard filtering, fresh env per attempt), remote-call dispatch (`lists:*`, `io:*`, `erlang:*`). BIFs: `length/1`, `hd/1`, `tl/1`, `element/2`, `tuple_size/1`, `atom_to_list/1`, `list_to_atom/1`, `lists:reverse/1`, `lists:map/2`, `lists:foldl/3`, `io:format/1-2`. `io:format` writes to a capture buffer (`er-io-buffer`, `er-io-flush!`, `er-io-buffer-content`) and returns `ok` — supports `~n`, `~p`/`~w`/`~s`, `~~`. 35 new eval tests. Total eval 130/130; erlang suite 244/244. **Phase 2 complete — Phase 3 (processes, scheduler, receive) is next.** - **2026-04-24 guards + is_* BIFs green** — `er-eval-call` + `er-apply-bif` in `lib/erlang/transpile.sx` wire local function calls to a BIF dispatcher. Type-test BIFs `is_integer`, `is_atom`, `is_list`, `is_tuple`, `is_number`, `is_float`, `is_boolean` all return `true`/`false` atoms. Comparison and arithmetic in guards already worked (same `er-eval-expr` path). 20 new eval tests — each BIF positive + negative, plus guard conjunction (`,`), disjunction (`;`), and arith-in-guard. Total eval 95/95; erlang suite 209/209.