Compare commits
3 Commits
loops/erla
...
4fc73a97f4
| Author | SHA1 | Date | |
|---|---|---|---|
| 4fc73a97f4 | |||
| 0f7444e0d5 | |||
| abde5fbac1 |
@@ -38,8 +38,6 @@ SUITES=(
|
||||
"fib|er-fib-test-pass|er-fib-test-count"
|
||||
"ffi|er-ffi-test-pass|er-ffi-test-count"
|
||||
"vm|er-vm-test-pass|er-vm-test-count"
|
||||
"send_after|er-sa-test-pass|er-sa-test-count"
|
||||
"lists_ext|er-lx-test-pass|er-lx-test-count"
|
||||
)
|
||||
|
||||
cat > "$TMPFILE" << 'EPOCHS'
|
||||
@@ -63,8 +61,6 @@ cat > "$TMPFILE" << 'EPOCHS'
|
||||
(load "lib/erlang/vm/dispatcher.sx")
|
||||
(load "lib/erlang/tests/ffi.sx")
|
||||
(load "lib/erlang/tests/vm.sx")
|
||||
(load "lib/erlang/tests/send_after.sx")
|
||||
(load "lib/erlang/tests/lists_ext.sx")
|
||||
(epoch 100)
|
||||
(eval "(list er-test-pass er-test-count)")
|
||||
(epoch 101)
|
||||
@@ -87,10 +83,6 @@ cat > "$TMPFILE" << 'EPOCHS'
|
||||
(eval "(list er-ffi-test-pass er-ffi-test-count)")
|
||||
(epoch 110)
|
||||
(eval "(list er-vm-test-pass er-vm-test-count)")
|
||||
(epoch 111)
|
||||
(eval "(list er-sa-test-pass er-sa-test-count)")
|
||||
(epoch 112)
|
||||
(eval "(list er-lx-test-pass er-lx-test-count)")
|
||||
EPOCHS
|
||||
|
||||
timeout 600 "$SX_SERVER" < "$TMPFILE" > "$OUTFILE" 2>&1
|
||||
|
||||
@@ -135,56 +135,6 @@
|
||||
(dict-set! s :next-ref (+ n 1))
|
||||
(er-mk-ref n)))))
|
||||
|
||||
;; ── logical clock + timer wheel ──────────────────────────────────
|
||||
;; The scheduler runs a synchronous model: logical time advances only
|
||||
;; when the runnable queue drains (see `er-sched-advance-time!`). The
|
||||
;; clock is in milliseconds, monotonic, never derived from wall time
|
||||
;; — deterministic and time-travel-safe. `send_after` schedules a
|
||||
;; message-delivery event at an absolute deadline; `receive after Ms`
|
||||
;; schedules a timeout event the same way. When no process is runnable
|
||||
;; the scheduler jumps the clock to the earliest pending deadline and
|
||||
;; fires that single event, then re-runs.
|
||||
(define er-clock (fn () (get (er-sched) :clock)))
|
||||
|
||||
;; Advance the clock to `ms`, but never backwards (monotonicity).
|
||||
(define
|
||||
er-clock-set!
|
||||
(fn (ms) (dict-set! (er-sched) :clock (max (er-clock) ms))))
|
||||
|
||||
(define er-sched-timers (fn () (get (er-sched) :timers)))
|
||||
|
||||
;; Register a timer event. `dest` is a pid or registered-atom value,
|
||||
;; resolved to a live process at fire time. Returns the timer ref.
|
||||
(define
|
||||
er-timer-add!
|
||||
(fn
|
||||
(deadline dest msg ref)
|
||||
(append!
|
||||
(er-sched-timers)
|
||||
{:ref ref :deadline deadline :dest dest :msg msg :alive true})
|
||||
ref))
|
||||
|
||||
;; Find the live timer with the given ref, or nil.
|
||||
(define
|
||||
er-timer-find-alive
|
||||
(fn
|
||||
(ref)
|
||||
(let
|
||||
((ts (er-sched-timers)) (found (list nil)))
|
||||
(for-each
|
||||
(fn
|
||||
(i)
|
||||
(let
|
||||
((t (nth ts i)))
|
||||
(when
|
||||
(and
|
||||
(= (nth found 0) nil)
|
||||
(get t :alive)
|
||||
(er-ref-equal? (get t :ref) ref))
|
||||
(set-nth! found 0 t))))
|
||||
(range 0 (len ts)))
|
||||
(nth found 0))))
|
||||
|
||||
;; ── scheduler state ──────────────────────────────────────────────
|
||||
(define er-scheduler (list nil))
|
||||
|
||||
@@ -201,8 +151,6 @@
|
||||
:processes {}
|
||||
:registered {}
|
||||
:ets {}
|
||||
:clock 0
|
||||
:timers (list)
|
||||
:runnable (er-q-new)})))
|
||||
|
||||
(define er-sched (fn () (nth er-scheduler 0)))
|
||||
@@ -269,7 +217,6 @@
|
||||
:trap-exit false
|
||||
:has-timeout false
|
||||
:timed-out false
|
||||
:timeout-deadline nil
|
||||
:exit-reason nil}))
|
||||
(dict-set! (er-sched-processes) (er-pid-key pid) proc)
|
||||
(er-sched-enqueue! pid)
|
||||
@@ -509,69 +456,6 @@
|
||||
(error "Erlang: make_ref/0: arity")
|
||||
(er-ref-new!))))
|
||||
|
||||
;; ── timer BIFs ───────────────────────────────────────────────────
|
||||
;; erlang:send_after(Time, Dest, Msg) -> Ref
|
||||
;; Schedules Msg to be delivered to Dest after Time ms (logical).
|
||||
;; Time must be a non-negative integer; Dest a pid or registered
|
||||
;; atom name. Returns a fresh timer reference.
|
||||
(define
|
||||
er-bif-send-after
|
||||
(fn
|
||||
(vs)
|
||||
(let
|
||||
((time (nth vs 0)) (dest (nth vs 1)) (msg (nth vs 2)))
|
||||
(cond
|
||||
(not (and (= (type-of time) "number") (>= time 0)))
|
||||
(raise (er-mk-error-marker (er-mk-atom "badarg")))
|
||||
(not (or (er-pid? dest) (er-atom? dest)))
|
||||
(raise (er-mk-error-marker (er-mk-atom "badarg")))
|
||||
:else
|
||||
(er-timer-add!
|
||||
(+ (er-clock) (truncate time))
|
||||
dest
|
||||
msg
|
||||
(er-ref-new!))))))
|
||||
|
||||
;; erlang:cancel_timer(Ref) -> RemainingMs | false
|
||||
;; For a live (not-yet-fired) timer, marks it cancelled and returns
|
||||
;; the milliseconds left until its deadline. For an already-fired,
|
||||
;; already-cancelled, or unknown ref, returns the atom `false`.
|
||||
(define
|
||||
er-bif-cancel-timer
|
||||
(fn
|
||||
(vs)
|
||||
(let
|
||||
((ref (nth vs 0)))
|
||||
(cond
|
||||
(not (er-ref? ref))
|
||||
(raise (er-mk-error-marker (er-mk-atom "badarg")))
|
||||
:else
|
||||
(let
|
||||
((t (er-timer-find-alive ref)))
|
||||
(cond
|
||||
(= t nil) (er-mk-atom "false")
|
||||
:else (do
|
||||
(dict-set! t :alive false)
|
||||
(max 0 (- (get t :deadline) (er-clock))))))))))
|
||||
|
||||
;; erlang:monotonic_time() | erlang:monotonic_time(Unit) -> Integer
|
||||
;; Returns the scheduler's logical monotonic clock in milliseconds.
|
||||
;; Unit (millisecond / second / native) is accepted for API
|
||||
;; compatibility; all units report from the same ms-resolution clock.
|
||||
(define
|
||||
er-bif-monotonic-time
|
||||
(fn
|
||||
(vs)
|
||||
(cond
|
||||
(= (len vs) 0) (er-clock)
|
||||
(and (= (len vs) 1) (er-atom? (nth vs 0)))
|
||||
(let
|
||||
((unit (get (nth vs 0) :name)))
|
||||
(cond
|
||||
(= unit "second") (truncate (/ (er-clock) 1000))
|
||||
:else (er-clock)))
|
||||
:else (raise (er-mk-error-marker (er-mk-atom "badarg"))))))
|
||||
|
||||
;; Add `target` to `pid`'s :links list if not already there.
|
||||
(define
|
||||
er-link-add-one!
|
||||
@@ -780,122 +664,37 @@
|
||||
(cond
|
||||
(not (= pid nil))
|
||||
(do (er-sched-step! pid) (er-sched-run-all!))
|
||||
;; Queue empty — advance logical time to the next pending
|
||||
;; deadline (timer delivery or receive-timeout) and go again.
|
||||
(er-sched-advance-time!) (er-sched-run-all!)
|
||||
;; Queue empty — fire one pending receive-with-timeout and go again.
|
||||
(er-sched-fire-one-timeout!) (er-sched-run-all!)
|
||||
:else nil))))
|
||||
|
||||
;; ── time advance ─────────────────────────────────────────────────
|
||||
;; Called when the runnable queue is empty. Two kinds of pending event
|
||||
;; carry a deadline: live `send_after` timers and waiting processes in
|
||||
;; a `receive ... after Ms` block. Find the single earliest deadline
|
||||
;; across both, jump the clock to it, and fire just that one event
|
||||
;; (timer wins ties — a message delivered exactly at the timeout
|
||||
;; arrives "first"). Returns true if an event fired, false when there
|
||||
;; is nothing left to wake (genuine idle / termination).
|
||||
;; Wake one waiting process whose receive had an `after Ms` clause.
|
||||
;; Returns true if one fired. In our synchronous model "time passes"
|
||||
;; once the runnable queue drains — timeouts only fire then.
|
||||
(define
|
||||
er-sched-advance-time!
|
||||
er-sched-fire-one-timeout!
|
||||
(fn
|
||||
()
|
||||
(let
|
||||
((best (er-sched-next-event)))
|
||||
(cond
|
||||
(= best nil) false
|
||||
:else (do
|
||||
(er-clock-set! (get best :deadline))
|
||||
(cond
|
||||
(= (get best :kind) "timer")
|
||||
(er-timer-fire! (get best :timer))
|
||||
:else (er-recv-timeout-fire! (get best :proc)))
|
||||
true)))))
|
||||
|
||||
;; Scan timers and waiting-with-timeout processes for the earliest
|
||||
;; deadline. Returns {:kind "timer"|"recv" :deadline D ...} or nil.
|
||||
(define
|
||||
er-sched-next-event
|
||||
(fn
|
||||
()
|
||||
(let
|
||||
((best (list nil)))
|
||||
(for-each
|
||||
(fn
|
||||
(i)
|
||||
(let
|
||||
((t (nth (er-sched-timers) i)))
|
||||
(when
|
||||
(get t :alive)
|
||||
(er-event-consider!
|
||||
best
|
||||
{:kind "timer" :deadline (get t :deadline) :timer t}))))
|
||||
(range 0 (len (er-sched-timers))))
|
||||
((ks (keys (er-sched-processes))) (fired (list false)))
|
||||
(for-each
|
||||
(fn
|
||||
(k)
|
||||
(let
|
||||
((p (get (er-sched-processes) k)))
|
||||
(when
|
||||
(and (= (get p :state) "waiting") (get p :has-timeout))
|
||||
(er-event-consider!
|
||||
best
|
||||
{:kind "recv"
|
||||
:deadline (get p :timeout-deadline)
|
||||
:proc p}))))
|
||||
(keys (er-sched-processes)))
|
||||
(nth best 0))))
|
||||
|
||||
;; Keep the earlier-deadline candidate in the single-cell `best`.
|
||||
;; Strictly-earlier replaces; equal deadlines keep the incumbent so a
|
||||
;; timer registered first (and timers over recv-timeouts) win ties.
|
||||
(define
|
||||
er-event-consider!
|
||||
(fn
|
||||
(best cand)
|
||||
(when
|
||||
(or
|
||||
(= (nth best 0) nil)
|
||||
(< (get cand :deadline) (get (nth best 0) :deadline)))
|
||||
(set-nth! best 0 cand))))
|
||||
|
||||
;; Deliver a fired timer's message to its destination and retire it.
|
||||
;; Destination is resolved at fire time; a dead/missing target (or an
|
||||
;; unregistered name) silently drops the message, as in real Erlang.
|
||||
(define
|
||||
er-timer-fire!
|
||||
(fn
|
||||
(t)
|
||||
(dict-set! t :alive false)
|
||||
(let
|
||||
((pid (er-timer-resolve-dest (get t :dest))))
|
||||
(when
|
||||
(and (not (= pid nil)) (er-proc-exists? pid))
|
||||
(er-proc-mailbox-push! pid (get t :msg))
|
||||
(when
|
||||
(= (er-proc-field pid :state) "waiting")
|
||||
(er-proc-set! pid :state "runnable")
|
||||
(er-sched-enqueue! pid))))))
|
||||
|
||||
;; Non-raising destination resolver for timer delivery.
|
||||
(define
|
||||
er-timer-resolve-dest
|
||||
(fn
|
||||
(v)
|
||||
(cond
|
||||
(er-pid? v) v
|
||||
(er-atom? v)
|
||||
(let
|
||||
((name (get v :name)))
|
||||
(if (dict-has? (er-registered) name) (get (er-registered) name) nil))
|
||||
:else nil)))
|
||||
|
||||
;; Wake a process whose `receive ... after Ms` deadline elapsed.
|
||||
(define
|
||||
er-recv-timeout-fire!
|
||||
(fn
|
||||
(p)
|
||||
(dict-set! p :timed-out true)
|
||||
(dict-set! p :has-timeout false)
|
||||
(dict-set! p :state "runnable")
|
||||
(er-sched-enqueue! (get p :pid))))
|
||||
(when
|
||||
(not (nth fired 0))
|
||||
(let
|
||||
((p (get (er-sched-processes) k)))
|
||||
(when
|
||||
(and
|
||||
(= (get p :state) "waiting")
|
||||
(get p :has-timeout))
|
||||
(dict-set! p :timed-out true)
|
||||
(dict-set! p :has-timeout false)
|
||||
(dict-set! p :state "runnable")
|
||||
(er-sched-enqueue! (get p :pid))
|
||||
(set-nth! fired 0 true)))))
|
||||
ks)
|
||||
(nth fired 0))))
|
||||
|
||||
(define
|
||||
er-sched-step!
|
||||
@@ -1265,15 +1064,8 @@
|
||||
{reply, Reply, NewState} ->
|
||||
From ! {Ref, Reply},
|
||||
gen_server:loop(Mod, NewState);
|
||||
{reply, Reply, NewState, Timeout} ->
|
||||
From ! {Ref, Reply},
|
||||
erlang:send_after(Timeout, self(), {timeout}),
|
||||
gen_server:loop(Mod, NewState);
|
||||
{noreply, NewState} ->
|
||||
gen_server:loop(Mod, NewState);
|
||||
{noreply, NewState, Timeout} ->
|
||||
erlang:send_after(Timeout, self(), {timeout}),
|
||||
gen_server:loop(Mod, NewState);
|
||||
{stop, Reason, Reply, NewState} ->
|
||||
From ! {Ref, Reply},
|
||||
exit(Reason)
|
||||
@@ -1281,17 +1073,11 @@
|
||||
{'$gen_cast', Msg} ->
|
||||
case Mod:handle_cast(Msg, State) of
|
||||
{noreply, NewState} -> gen_server:loop(Mod, NewState);
|
||||
{noreply, NewState, Timeout} ->
|
||||
erlang:send_after(Timeout, self(), {timeout}),
|
||||
gen_server:loop(Mod, NewState);
|
||||
{stop, Reason, NewState} -> exit(Reason)
|
||||
end;
|
||||
Other ->
|
||||
case Mod:handle_info(Other, State) of
|
||||
{noreply, NewState} -> gen_server:loop(Mod, NewState);
|
||||
{noreply, NewState, Timeout} ->
|
||||
erlang:send_after(Timeout, self(), {timeout}),
|
||||
gen_server:loop(Mod, NewState);
|
||||
{stop, Reason, NewState} -> exit(Reason)
|
||||
end
|
||||
end.")
|
||||
@@ -1720,10 +1506,6 @@
|
||||
(er-register-bif! "erlang" "exit" 1 er-bif-exit)
|
||||
(er-register-bif! "erlang" "exit" 2 er-bif-exit)
|
||||
(er-register-bif! "erlang" "make_ref" 0 er-bif-make-ref)
|
||||
(er-register-bif! "erlang" "send_after" 3 er-bif-send-after)
|
||||
(er-register-bif! "erlang" "cancel_timer" 1 er-bif-cancel-timer)
|
||||
(er-register-bif! "erlang" "monotonic_time" 0 er-bif-monotonic-time)
|
||||
(er-register-bif! "erlang" "monotonic_time" 1 er-bif-monotonic-time)
|
||||
(er-register-bif! "erlang" "link" 1 er-bif-link)
|
||||
(er-register-bif! "erlang" "unlink" 1 er-bif-unlink)
|
||||
(er-register-bif! "erlang" "monitor" 2 er-bif-monitor)
|
||||
@@ -1753,42 +1535,6 @@
|
||||
(er-register-pure-bif! "lists" "any" 2 er-bif-lists-any)
|
||||
(er-register-pure-bif! "lists" "all" 2 er-bif-lists-all)
|
||||
(er-register-pure-bif! "lists" "duplicate" 2 er-bif-lists-duplicate)
|
||||
(er-register-pure-bif! "lists" "sort" 1 er-bif-lists-sort)
|
||||
(er-register-pure-bif! "lists" "sort" 2 er-bif-lists-sort)
|
||||
(er-register-pure-bif! "lists" "usort" 1 er-bif-lists-usort)
|
||||
(er-register-pure-bif! "lists" "keyfind" 3 er-bif-lists-keyfind)
|
||||
(er-register-pure-bif! "lists" "keymember" 3 er-bif-lists-keymember)
|
||||
(er-register-pure-bif! "lists" "keydelete" 3 er-bif-lists-keydelete)
|
||||
(er-register-pure-bif! "lists" "keyreplace" 4 er-bif-lists-keyreplace)
|
||||
(er-register-pure-bif! "lists" "keystore" 4 er-bif-lists-keystore)
|
||||
(er-register-pure-bif! "lists" "keytake" 3 er-bif-lists-keytake)
|
||||
(er-register-pure-bif! "lists" "keysort" 2 er-bif-lists-keysort)
|
||||
(er-register-pure-bif! "lists" "foldr" 3 er-bif-lists-foldr)
|
||||
(er-register-pure-bif! "lists" "partition" 2 er-bif-lists-partition)
|
||||
(er-register-pure-bif! "lists" "takewhile" 2 er-bif-lists-takewhile)
|
||||
(er-register-pure-bif! "lists" "dropwhile" 2 er-bif-lists-dropwhile)
|
||||
(er-register-pure-bif! "lists" "splitwith" 2 er-bif-lists-splitwith)
|
||||
(er-register-pure-bif! "lists" "flatten" 1 er-bif-lists-flatten)
|
||||
(er-register-pure-bif! "lists" "max" 1 er-bif-lists-max)
|
||||
(er-register-pure-bif! "lists" "min" 1 er-bif-lists-min)
|
||||
(er-register-pure-bif! "lists" "zip" 2 er-bif-lists-zip)
|
||||
(er-register-pure-bif! "lists" "zipwith" 3 er-bif-lists-zipwith)
|
||||
(er-register-pure-bif! "lists" "unzip" 1 er-bif-lists-unzip)
|
||||
(er-register-pure-bif! "lists" "sublist" 2 er-bif-lists-sublist)
|
||||
(er-register-pure-bif! "lists" "sublist" 3 er-bif-lists-sublist)
|
||||
(er-register-pure-bif! "lists" "nthtail" 2 er-bif-lists-nthtail)
|
||||
(er-register-pure-bif! "lists" "split" 2 er-bif-lists-split)
|
||||
(er-register-pure-bif! "lists" "droplast" 1 er-bif-lists-droplast)
|
||||
(er-register-pure-bif! "lists" "flatmap" 2 er-bif-lists-flatmap)
|
||||
(er-register-pure-bif! "lists" "filtermap" 2 er-bif-lists-filtermap)
|
||||
(er-register-pure-bif! "lists" "mapfoldl" 3 er-bif-lists-mapfoldl)
|
||||
(er-register-pure-bif! "lists" "search" 2 er-bif-lists-search)
|
||||
(er-register-pure-bif! "proplists" "get_value" 2 er-bif-pl-get-value)
|
||||
(er-register-pure-bif! "proplists" "get_value" 3 er-bif-pl-get-value)
|
||||
(er-register-pure-bif! "proplists" "get_all_values" 2 er-bif-pl-get-all-values)
|
||||
(er-register-pure-bif! "proplists" "is_defined" 2 er-bif-pl-is-defined)
|
||||
(er-register-pure-bif! "proplists" "lookup" 2 er-bif-pl-lookup)
|
||||
(er-register-pure-bif! "proplists" "delete" 2 er-bif-pl-delete)
|
||||
;; io module — side-effecting (writes to io buffer)
|
||||
(er-register-bif! "io" "format" 1 er-bif-io-format)
|
||||
(er-register-bif! "io" "format" 2 er-bif-io-format)
|
||||
@@ -1815,66 +1561,7 @@
|
||||
(er-register-pure-bif! "crypto" "hash" 2 er-bif-crypto-hash)
|
||||
(er-register-pure-bif! "cid" "from_bytes" 1 er-bif-cid-from-bytes)
|
||||
(er-register-pure-bif! "cid" "to_string" 1 er-bif-cid-to-string)
|
||||
|
||||
;; ── binary_to_list / list_to_binary (Step 3b — term codec) ──────
|
||||
;; Standard Erlang semantics:
|
||||
;; binary_to_list(<<B1,B2,...>>) -> [B1, B2, ...] (Erlang cons of ints)
|
||||
;; list_to_binary(IoList) -> <<...>> (flattens nested
|
||||
;; iolists; elements are byte ints 0-255 or binaries)
|
||||
;; Bad arg / out-of-range byte / non-iolist element -> error:badarg.
|
||||
|
||||
(define er-bif-binary-to-list
|
||||
(fn (vs)
|
||||
(let ((v (nth vs 0)))
|
||||
(cond
|
||||
(not (er-binary? v))
|
||||
(raise (er-mk-error-marker (er-mk-atom "badarg")))
|
||||
:else
|
||||
(let ((bs (get v :bytes)) (out (er-mk-nil)))
|
||||
(for-each
|
||||
(fn (i)
|
||||
(set! out (er-mk-cons (nth bs (- (- (len bs) 1) i)) out)))
|
||||
(range 0 (len bs)))
|
||||
out)))))
|
||||
|
||||
;; Walk an Erlang iolist, appending bytes to `acc` (a mutable SX list).
|
||||
;; Accepts: nil, cons-of-X, binary, integer in 0..255. Anything else
|
||||
;; signals failure by setting (nth fail 0) to true.
|
||||
(define er-iolist-walk!
|
||||
(fn (v acc fail)
|
||||
(cond
|
||||
(nth fail 0) nil
|
||||
(er-nil? v) nil
|
||||
(er-cons? v)
|
||||
(do (er-iolist-walk! (get v :head) acc fail)
|
||||
(er-iolist-walk! (get v :tail) acc fail))
|
||||
(er-binary? v)
|
||||
(for-each
|
||||
(fn (i) (append! acc (nth (get v :bytes) i)))
|
||||
(range 0 (len (get v :bytes))))
|
||||
(= (type-of v) "number")
|
||||
(cond
|
||||
(and (>= v 0) (<= v 255)) (append! acc v)
|
||||
:else (set-nth! fail 0 true))
|
||||
:else (set-nth! fail 0 true))))
|
||||
|
||||
(define er-bif-list-to-binary
|
||||
(fn (vs)
|
||||
(let ((v (nth vs 0)) (acc (list)) (fail (list false)))
|
||||
(cond
|
||||
(not (or (er-nil? v) (er-cons? v) (er-binary? v)))
|
||||
(raise (er-mk-error-marker (er-mk-atom "badarg")))
|
||||
:else
|
||||
(do
|
||||
(er-iolist-walk! v acc fail)
|
||||
(cond
|
||||
(nth fail 0)
|
||||
(raise (er-mk-error-marker (er-mk-atom "badarg")))
|
||||
:else (er-mk-binary acc)))))))
|
||||
|
||||
(er-register-bif! "file" "list_dir" 1 er-bif-file-list-dir)
|
||||
(er-register-pure-bif! "erlang" "binary_to_list" 1 er-bif-binary-to-list)
|
||||
(er-register-pure-bif! "erlang" "list_to_binary" 1 er-bif-list-to-binary)
|
||||
(er-mk-atom "ok")))
|
||||
|
||||
;; Register everything at load time.
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
{
|
||||
"language": "erlang",
|
||||
"total_pass": 874,
|
||||
"total": 874,
|
||||
"total_pass": 729,
|
||||
"total": 729,
|
||||
"suites": [
|
||||
{"name":"tokenize","pass":62,"total":62,"status":"ok"},
|
||||
{"name":"parse","pass":52,"total":52,"status":"ok"},
|
||||
{"name":"eval","pass":408,"total":408,"status":"ok"},
|
||||
{"name":"eval","pass":385,"total":385,"status":"ok"},
|
||||
{"name":"runtime","pass":93,"total":93,"status":"ok"},
|
||||
{"name":"ring","pass":4,"total":4,"status":"ok"},
|
||||
{"name":"ping-pong","pass":4,"total":4,"status":"ok"},
|
||||
{"name":"bank","pass":8,"total":8,"status":"ok"},
|
||||
{"name":"echo","pass":7,"total":7,"status":"ok"},
|
||||
{"name":"fib","pass":8,"total":8,"status":"ok"},
|
||||
{"name":"ffi","pass":37,"total":37,"status":"ok"},
|
||||
{"name":"vm","pass":78,"total":78,"status":"ok"},
|
||||
{"name":"send_after","pass":10,"total":10,"status":"ok"},
|
||||
{"name":"lists_ext","pass":103,"total":103,"status":"ok"}
|
||||
{"name":"ffi","pass":28,"total":28,"status":"ok"},
|
||||
{"name":"vm","pass":78,"total":78,"status":"ok"}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,22 +1,20 @@
|
||||
# Erlang-on-SX Scoreboard
|
||||
|
||||
**Total: 874 / 874 tests passing**
|
||||
**Total: 729 / 729 tests passing**
|
||||
|
||||
| | Suite | Pass | Total |
|
||||
|---|---|---|---|
|
||||
| ✅ | tokenize | 62 | 62 |
|
||||
| ✅ | parse | 52 | 52 |
|
||||
| ✅ | eval | 408 | 408 |
|
||||
| ✅ | eval | 385 | 385 |
|
||||
| ✅ | runtime | 93 | 93 |
|
||||
| ✅ | ring | 4 | 4 |
|
||||
| ✅ | ping-pong | 4 | 4 |
|
||||
| ✅ | bank | 8 | 8 |
|
||||
| ✅ | echo | 7 | 7 |
|
||||
| ✅ | fib | 8 | 8 |
|
||||
| ✅ | ffi | 37 | 37 |
|
||||
| ✅ | ffi | 28 | 28 |
|
||||
| ✅ | vm | 78 | 78 |
|
||||
| ✅ | send_after | 10 | 10 |
|
||||
| ✅ | lists_ext | 103 | 103 |
|
||||
|
||||
|
||||
Generated by `lib/erlang/conformance.sh`.
|
||||
|
||||
@@ -228,10 +228,9 @@
|
||||
(er-eval-test "tuple_size 0" (ev "tuple_size({})") 0)
|
||||
|
||||
;; ── BIFs: atom / list conversions ───────────────────────────────
|
||||
(er-eval-test "atom_to_list -> charlist length" (ev "length(atom_to_list(hello))") 5)
|
||||
(er-eval-test "atom_to_list -> head $h" (ev "hd(atom_to_list(hello))") 104)
|
||||
(er-eval-test "atom_to_list" (ev "atom_to_list(hello)") "hello")
|
||||
(er-eval-test "list_to_atom roundtrip"
|
||||
(nm (ev "list_to_atom(atom_to_list(foo))")) "foo") ;; round-trip via charlist
|
||||
(nm (ev "list_to_atom(atom_to_list(foo))")) "foo")
|
||||
(er-eval-test "list_to_atom fresh"
|
||||
(nm (ev "list_to_atom(\"bar\")")) "bar")
|
||||
|
||||
@@ -1061,13 +1060,11 @@
|
||||
(er-eval-test "list_to_tuple roundtrip"
|
||||
(ev "tuple_size(list_to_tuple([10, 20, 30]))") 3)
|
||||
|
||||
(er-eval-test "integer_to_list -> charlist length" (ev "length(integer_to_list(42))") 2)
|
||||
(er-eval-test "integer_to_list 42 head $4" (ev "hd(integer_to_list(42))") 52)
|
||||
(er-eval-test "integer_to_list neg -> charlist length" (ev "length(integer_to_list(-99))") 3)
|
||||
(er-eval-test "integer_to_list -99 head $-" (ev "hd(integer_to_list(-99))") 45)
|
||||
(er-eval-test "integer_to_list" (ev "integer_to_list(42)") "42")
|
||||
(er-eval-test "integer_to_list neg" (ev "integer_to_list(-99)") "-99")
|
||||
(er-eval-test "list_to_integer" (ev "list_to_integer(\"123\")") 123)
|
||||
(er-eval-test "list_to_integer roundtrip"
|
||||
(ev "list_to_integer(integer_to_list(7))") 7) ;; round-trip via charlist
|
||||
(ev "list_to_integer(integer_to_list(7))") 7)
|
||||
|
||||
(er-eval-test "is_function fun"
|
||||
(nm (ev "F = fun (X) -> X end, is_function(F)")) "true")
|
||||
@@ -1344,42 +1341,6 @@
|
||||
(get (nth (get er-rt-cap-result :elements) 4) :name) "true")
|
||||
|
||||
|
||||
|
||||
;; ── $X char literals (Step 3b substrate fix 2026-06-04) ──────────
|
||||
(er-eval-test "char $A" (ev "$A") 65)
|
||||
(er-eval-test "char $a" (ev "$a") 97)
|
||||
(er-eval-test "char $0 is digit, not escape-NUL" (ev "$0") 48)
|
||||
(er-eval-test "char $\\n is newline (10)" (ev "$\\n") 10)
|
||||
(er-eval-test "char $\\t is tab (9)" (ev "$\\t") 9)
|
||||
(er-eval-test "char $\\r is CR (13)" (ev "$\\r") 13)
|
||||
(er-eval-test "char $\\s is space (32)" (ev "$\\s") 32)
|
||||
(er-eval-test "char $\\0 is NUL (0)" (ev "$\\0") 0)
|
||||
(er-eval-test "char $\\\\ is backslash (92)" (ev "$\\\\") 92)
|
||||
(er-eval-test "[$h,$i] head is 104" (ev "hd([$h, $i])") 104)
|
||||
(er-eval-test "list_to_binary char-list -> bytes"
|
||||
(ev "byte_size(list_to_binary([$f, $e, $d]))") 3)
|
||||
(er-eval-test "list_to_binary char-list round-trip"
|
||||
(nm (ev "list_to_binary([$h, $i]) =:= <<104, 105>>")) "true")
|
||||
|
||||
|
||||
;; ── atom_to_list / integer_to_list charlist semantics (Step 3b substrate fix #3) ──
|
||||
(er-eval-test "atom_to_list hd is char code"
|
||||
(ev "hd(atom_to_list(hi))") 104)
|
||||
(er-eval-test "atom_to_list maps to bytes via list_to_binary"
|
||||
(ev "byte_size(list_to_binary(atom_to_list(hello)))") 5)
|
||||
(er-eval-test "atom_to_list -> list_to_binary -> bytes content"
|
||||
(nm (ev "list_to_binary(atom_to_list(ok)) =:= <<111, 107>>")) "true")
|
||||
(er-eval-test "integer_to_list 12345 -> 5 chars"
|
||||
(ev "length(integer_to_list(12345))") 5)
|
||||
(er-eval-test "integer_to_list -> bytes -> back"
|
||||
(ev "list_to_integer(integer_to_list(99999))") 99999)
|
||||
(er-eval-test "list_to_atom from charlist"
|
||||
(nm (ev "list_to_atom([$f, $o, $o])")) "foo")
|
||||
(er-eval-test "list_to_atom from SX-string back-compat"
|
||||
(nm (ev "list_to_atom(\"bar\")")) "bar")
|
||||
(er-eval-test "list_to_integer from charlist"
|
||||
(ev "list_to_integer([$1, $0, $0])") 100)
|
||||
|
||||
(define
|
||||
er-eval-test-summary
|
||||
(str "eval " er-eval-test-pass "/" er-eval-test-count))
|
||||
|
||||
@@ -160,51 +160,6 @@
|
||||
(ffi-nm (ffi-ev "element(2, file:list_dir(\"/no/such/dir/xyz\"))"))
|
||||
"enoent")
|
||||
|
||||
(er-ffi-test
|
||||
"binary_to_list <<1,2,3>> length"
|
||||
(ffi-ev "length(binary_to_list(<<1,2,3,4,5>>))")
|
||||
5)
|
||||
|
||||
(er-ffi-test
|
||||
"binary_to_list hd byte"
|
||||
(ffi-ev "hd(binary_to_list(<<7,8,9>>))")
|
||||
7)
|
||||
|
||||
(er-ffi-test
|
||||
"binary_to_list empty -> []"
|
||||
(ffi-nm (ffi-ev "case binary_to_list(<<>>) of [] -> empty end"))
|
||||
"empty")
|
||||
|
||||
(er-ffi-test
|
||||
"list_to_binary flat list bytes"
|
||||
(ffi-ev "byte_size(list_to_binary([1,2,3]))")
|
||||
3)
|
||||
|
||||
(er-ffi-test
|
||||
"list_to_binary nested iolist"
|
||||
(ffi-ev "byte_size(list_to_binary([1, <<2,3>>, [4, [5]]]))")
|
||||
5)
|
||||
|
||||
(er-ffi-test
|
||||
"list_to_binary round-trip via binary_to_list"
|
||||
(ffi-nm (ffi-ev "list_to_binary(binary_to_list(<<10,20,30>>)) =:= <<10,20,30>>"))
|
||||
"true")
|
||||
|
||||
(er-ffi-test
|
||||
"binary_to_list non-binary -> error:badarg"
|
||||
(ffi-nm (ffi-ev "try binary_to_list(42) catch error:badarg -> ok end"))
|
||||
"ok")
|
||||
|
||||
(er-ffi-test
|
||||
"list_to_binary out-of-range byte -> error:badarg"
|
||||
(ffi-nm (ffi-ev "try list_to_binary([300]) catch error:badarg -> ok end"))
|
||||
"ok")
|
||||
|
||||
(er-ffi-test
|
||||
"list_to_binary non-iolist -> error:badarg"
|
||||
(ffi-nm (ffi-ev "try list_to_binary(42) catch error:badarg -> ok end"))
|
||||
"ok")
|
||||
|
||||
;; ── Still deferred (no host primitive): httpc (HTTP client, v2),
|
||||
;; sqlite-* (v2 indexes). Assert NOT registered so a future iteration
|
||||
;; that wires them without updating this suite fails fast.
|
||||
|
||||
@@ -1,385 +0,0 @@
|
||||
;; lists-ext tests — lists:sort/1, lists:sort/2, lists:usort/1.
|
||||
;; Each case evaluates an Erlang expression that reduces to the bool
|
||||
;; atom `true` (via =:= on the sorted result) and checks its name.
|
||||
|
||||
(define er-lx-test-count 0)
|
||||
(define er-lx-test-pass 0)
|
||||
(define er-lx-test-fails (list))
|
||||
|
||||
(define
|
||||
er-lx-test
|
||||
(fn
|
||||
(name actual expected)
|
||||
(set! er-lx-test-count (+ er-lx-test-count 1))
|
||||
(if
|
||||
(= actual expected)
|
||||
(set! er-lx-test-pass (+ er-lx-test-pass 1))
|
||||
(append! er-lx-test-fails {:name name :expected expected :actual actual}))))
|
||||
|
||||
;; eval an Erlang source string and return the result atom's name
|
||||
(define er-lx-nm (fn (src) (get (erlang-eval-ast src) :name)))
|
||||
|
||||
;; ── lists:sort/1 ──────────────────────────────────────────────────
|
||||
(er-lx-test "sort/1 ascending"
|
||||
(er-lx-nm "lists:sort([3,1,2]) =:= [1,2,3]") "true")
|
||||
|
||||
(er-lx-test "sort/1 already sorted"
|
||||
(er-lx-nm "lists:sort([1,2,3]) =:= [1,2,3]") "true")
|
||||
|
||||
(er-lx-test "sort/1 empty"
|
||||
(er-lx-nm "lists:sort([]) =:= []") "true")
|
||||
|
||||
(er-lx-test "sort/1 singleton"
|
||||
(er-lx-nm "lists:sort([7]) =:= [7]") "true")
|
||||
|
||||
(er-lx-test "sort/1 keeps duplicates"
|
||||
(er-lx-nm "lists:sort([3,1,2,1]) =:= [1,1,2,3]") "true")
|
||||
|
||||
(er-lx-test "sort/1 length preserved"
|
||||
(erlang-eval-ast "length(lists:sort([5,4,3,2,1]))") 5)
|
||||
|
||||
(er-lx-test "sort/1 term order: number < atom"
|
||||
(er-lx-nm "lists:sort([b,a,1]) =:= [1,a,b]") "true")
|
||||
|
||||
(er-lx-test "sort/1 tuples elementwise"
|
||||
(er-lx-nm "lists:sort([{2,a},{1,b},{1,a}]) =:= [{1,a},{1,b},{2,a}]") "true")
|
||||
|
||||
;; ── lists:sort/2 ──────────────────────────────────────────────────
|
||||
(er-lx-test "sort/2 ascending =<"
|
||||
(er-lx-nm "lists:sort(fun(A,B) -> A =< B end, [3,1,2]) =:= [1,2,3]") "true")
|
||||
|
||||
(er-lx-test "sort/2 descending >="
|
||||
(er-lx-nm "lists:sort(fun(A,B) -> A >= B end, [1,3,2]) =:= [3,2,1]") "true")
|
||||
|
||||
(er-lx-test "sort/2 stable on equal keys"
|
||||
(er-lx-nm
|
||||
"lists:sort(fun({A,_},{B,_}) -> A =< B end, [{1,x},{1,y},{0,z}]) =:= [{0,z},{1,x},{1,y}]")
|
||||
"true")
|
||||
|
||||
(er-lx-test "sort/2 empty"
|
||||
(er-lx-nm "lists:sort(fun(A,B) -> A =< B end, []) =:= []") "true")
|
||||
|
||||
;; ── lists:usort/1 ─────────────────────────────────────────────────
|
||||
(er-lx-test "usort/1 removes duplicates"
|
||||
(er-lx-nm "lists:usort([3,1,2,1,3]) =:= [1,2,3]") "true")
|
||||
|
||||
(er-lx-test "usort/1 empty"
|
||||
(er-lx-nm "lists:usort([]) =:= []") "true")
|
||||
|
||||
(er-lx-test "usort/1 all equal collapses to one"
|
||||
(er-lx-nm "lists:usort([5,5,5]) =:= [5]") "true")
|
||||
|
||||
(er-lx-test "usort/1 already unique"
|
||||
(er-lx-nm "lists:usort([1,2,3]) =:= [1,2,3]") "true")
|
||||
|
||||
(er-lx-test "usort/1 length after dedup"
|
||||
(erlang-eval-ast "length(lists:usort([4,4,2,2,1,1,4]))") 3)
|
||||
|
||||
;; ── lists:keyfind/3 ───────────────────────────────────────────────
|
||||
(er-lx-test "keyfind hit"
|
||||
(erlang-eval-ast "element(2, lists:keyfind(b, 1, [{a,1},{b,2},{c,3}]))") 2)
|
||||
|
||||
(er-lx-test "keyfind first match only"
|
||||
(erlang-eval-ast "element(2, lists:keyfind(a, 1, [{a,1},{a,9}]))") 1)
|
||||
|
||||
(er-lx-test "keyfind miss returns false"
|
||||
(er-lx-nm "lists:keyfind(z, 1, [{a,1},{b,2}])") "false")
|
||||
|
||||
(er-lx-test "keyfind on second element"
|
||||
(er-lx-nm "element(1, lists:keyfind(2, 2, [{a,1},{b,2}]))") "b")
|
||||
|
||||
(er-lx-test "keyfind skips short tuples"
|
||||
(er-lx-nm "lists:keyfind(x, 2, [{x},{y,x}]) =:= {y,x}") "true")
|
||||
|
||||
;; ── lists:keymember/3 ─────────────────────────────────────────────
|
||||
(er-lx-test "keymember true"
|
||||
(er-lx-nm "lists:keymember(b, 1, [{a,1},{b,2}])") "true")
|
||||
|
||||
(er-lx-test "keymember false"
|
||||
(er-lx-nm "lists:keymember(z, 1, [{a,1},{b,2}])") "false")
|
||||
|
||||
;; ── lists:keydelete/3 ─────────────────────────────────────────────
|
||||
(er-lx-test "keydelete removes first match"
|
||||
(er-lx-nm "lists:keydelete(b, 1, [{a,1},{b,2},{c,3}]) =:= [{a,1},{c,3}]") "true")
|
||||
|
||||
(er-lx-test "keydelete only first"
|
||||
(er-lx-nm "lists:keydelete(a, 1, [{a,1},{a,2},{b,3}]) =:= [{a,2},{b,3}]") "true")
|
||||
|
||||
(er-lx-test "keydelete miss unchanged"
|
||||
(er-lx-nm "lists:keydelete(z, 1, [{a,1},{b,2}]) =:= [{a,1},{b,2}]") "true")
|
||||
|
||||
;; ── lists:keyreplace/4 ────────────────────────────────────────────
|
||||
(er-lx-test "keyreplace hit"
|
||||
(er-lx-nm
|
||||
"lists:keyreplace(b, 1, [{a,1},{b,2},{c,3}], {b,99}) =:= [{a,1},{b,99},{c,3}]")
|
||||
"true")
|
||||
|
||||
(er-lx-test "keyreplace miss unchanged"
|
||||
(er-lx-nm
|
||||
"lists:keyreplace(z, 1, [{a,1}], {z,0}) =:= [{a,1}]") "true")
|
||||
|
||||
;; ── lists:keystore/4 ──────────────────────────────────────────────
|
||||
(er-lx-test "keystore replaces existing"
|
||||
(er-lx-nm
|
||||
"lists:keystore(b, 1, [{a,1},{b,2}], {b,99}) =:= [{a,1},{b,99}]") "true")
|
||||
|
||||
(er-lx-test "keystore appends when absent"
|
||||
(er-lx-nm
|
||||
"lists:keystore(z, 1, [{a,1},{b,2}], {z,0}) =:= [{a,1},{b,2},{z,0}]") "true")
|
||||
|
||||
;; ── lists:keytake/3 ───────────────────────────────────────────────
|
||||
(er-lx-test "keytake hit value tag"
|
||||
(er-lx-nm "element(1, lists:keytake(b, 1, [{a,1},{b,2},{c,3}]))") "value")
|
||||
|
||||
(er-lx-test "keytake hit tuple"
|
||||
(er-lx-nm
|
||||
"element(2, lists:keytake(b, 1, [{a,1},{b,2},{c,3}])) =:= {b,2}") "true")
|
||||
|
||||
(er-lx-test "keytake hit rest"
|
||||
(er-lx-nm
|
||||
"element(3, lists:keytake(b, 1, [{a,1},{b,2},{c,3}])) =:= [{a,1},{c,3}]") "true")
|
||||
|
||||
(er-lx-test "keytake miss false"
|
||||
(er-lx-nm "lists:keytake(z, 1, [{a,1}])") "false")
|
||||
|
||||
;; ── lists:keysort/2 ───────────────────────────────────────────────
|
||||
(er-lx-test "keysort by element 1"
|
||||
(er-lx-nm
|
||||
"lists:keysort(1, [{c,3},{a,1},{b,2}]) =:= [{a,1},{b,2},{c,3}]") "true")
|
||||
|
||||
(er-lx-test "keysort by element 2"
|
||||
(er-lx-nm
|
||||
"lists:keysort(2, [{a,3},{b,1},{c,2}]) =:= [{b,1},{c,2},{a,3}]") "true")
|
||||
|
||||
(er-lx-test "keysort stable on equal keys"
|
||||
(er-lx-nm
|
||||
"lists:keysort(1, [{a,1},{a,2},{a,3}]) =:= [{a,1},{a,2},{a,3}]") "true")
|
||||
|
||||
;; ── lists:foldr/3 ─────────────────────────────────────────────────
|
||||
(er-lx-test "foldr preserves order"
|
||||
(er-lx-nm
|
||||
"lists:foldr(fun(X,Acc) -> [X|Acc] end, [], [1,2,3]) =:= [1,2,3]") "true")
|
||||
|
||||
(er-lx-test "foldr sum"
|
||||
(erlang-eval-ast "lists:foldr(fun(X,A) -> X+A end, 0, [1,2,3,4])") 10)
|
||||
|
||||
(er-lx-test "foldr empty returns acc"
|
||||
(erlang-eval-ast "lists:foldr(fun(X,A) -> X+A end, 42, [])") 42)
|
||||
|
||||
;; ── lists:partition/2 ─────────────────────────────────────────────
|
||||
(er-lx-test "partition evens/odds"
|
||||
(er-lx-nm
|
||||
"lists:partition(fun(X) -> X rem 2 =:= 0 end, [1,2,3,4,5]) =:= {[2,4],[1,3,5]}")
|
||||
"true")
|
||||
|
||||
(er-lx-test "partition all satisfy"
|
||||
(er-lx-nm "lists:partition(fun(_) -> true end, [1,2]) =:= {[1,2],[]}") "true")
|
||||
|
||||
(er-lx-test "partition empty"
|
||||
(er-lx-nm "lists:partition(fun(_) -> true end, []) =:= {[],[]}") "true")
|
||||
|
||||
;; ── lists:takewhile/2 ─────────────────────────────────────────────
|
||||
(er-lx-test "takewhile prefix"
|
||||
(er-lx-nm "lists:takewhile(fun(X) -> X < 3 end, [1,2,3,4,1]) =:= [1,2]") "true")
|
||||
|
||||
(er-lx-test "takewhile none"
|
||||
(er-lx-nm "lists:takewhile(fun(X) -> X < 0 end, [1,2]) =:= []") "true")
|
||||
|
||||
(er-lx-test "takewhile all"
|
||||
(er-lx-nm "lists:takewhile(fun(X) -> X < 9 end, [1,2,3]) =:= [1,2,3]") "true")
|
||||
|
||||
;; ── lists:dropwhile/2 ─────────────────────────────────────────────
|
||||
(er-lx-test "dropwhile prefix"
|
||||
(er-lx-nm "lists:dropwhile(fun(X) -> X < 3 end, [1,2,3,4,1]) =:= [3,4,1]") "true")
|
||||
|
||||
(er-lx-test "dropwhile all"
|
||||
(er-lx-nm "lists:dropwhile(fun(X) -> X < 9 end, [1,2,3]) =:= []") "true")
|
||||
|
||||
(er-lx-test "dropwhile none"
|
||||
(er-lx-nm "lists:dropwhile(fun(X) -> X < 0 end, [1,2]) =:= [1,2]") "true")
|
||||
|
||||
;; ── lists:splitwith/2 ─────────────────────────────────────────────
|
||||
(er-lx-test "splitwith"
|
||||
(er-lx-nm
|
||||
"lists:splitwith(fun(X) -> X < 3 end, [1,2,3,4,1]) =:= {[1,2],[3,4,1]}") "true")
|
||||
|
||||
(er-lx-test "splitwith empty"
|
||||
(er-lx-nm "lists:splitwith(fun(_) -> true end, []) =:= {[],[]}") "true")
|
||||
|
||||
;; ── lists:flatten/1 ───────────────────────────────────────────────
|
||||
(er-lx-test "flatten nested"
|
||||
(er-lx-nm "lists:flatten([1,[2,[3,4]],5]) =:= [1,2,3,4,5]") "true")
|
||||
|
||||
(er-lx-test "flatten already flat"
|
||||
(er-lx-nm "lists:flatten([1,2,3]) =:= [1,2,3]") "true")
|
||||
|
||||
(er-lx-test "flatten empty"
|
||||
(er-lx-nm "lists:flatten([]) =:= []") "true")
|
||||
|
||||
(er-lx-test "flatten deep empties"
|
||||
(er-lx-nm "lists:flatten([[],[1],[[]]]) =:= [1]") "true")
|
||||
|
||||
(er-lx-test "flatten length"
|
||||
(erlang-eval-ast "length(lists:flatten([[1,2],[3],[4,5,6]]))") 6)
|
||||
|
||||
;; ── lists:max/1 ───────────────────────────────────────────────────
|
||||
(er-lx-test "max ints"
|
||||
(erlang-eval-ast "lists:max([3,1,4,1,5,9,2,6])") 9)
|
||||
|
||||
(er-lx-test "max single"
|
||||
(erlang-eval-ast "lists:max([7])") 7)
|
||||
|
||||
(er-lx-test "max atoms term order"
|
||||
(er-lx-nm "lists:max([a,c,b]) =:= c") "true")
|
||||
|
||||
;; ── lists:min/1 ───────────────────────────────────────────────────
|
||||
(er-lx-test "min ints"
|
||||
(erlang-eval-ast "lists:min([3,1,4,1,5])") 1)
|
||||
|
||||
(er-lx-test "min mixed term order"
|
||||
(er-lx-nm "lists:min([a,1,b]) =:= 1") "true")
|
||||
|
||||
;; ── lists:zip/2 ───────────────────────────────────────────────────
|
||||
(er-lx-test "zip pairs"
|
||||
(er-lx-nm "lists:zip([a,b,c],[1,2,3]) =:= [{a,1},{b,2},{c,3}]") "true")
|
||||
|
||||
(er-lx-test "zip empty"
|
||||
(er-lx-nm "lists:zip([],[]) =:= []") "true")
|
||||
|
||||
(er-lx-test "zip length"
|
||||
(erlang-eval-ast "length(lists:zip([1,2],[3,4]))") 2)
|
||||
|
||||
;; ── lists:zipwith/3 ───────────────────────────────────────────────
|
||||
(er-lx-test "zipwith sum"
|
||||
(er-lx-nm
|
||||
"lists:zipwith(fun(X,Y) -> X+Y end, [1,2,3], [10,20,30]) =:= [11,22,33]")
|
||||
"true")
|
||||
|
||||
(er-lx-test "zipwith tuple"
|
||||
(er-lx-nm "lists:zipwith(fun(X,Y) -> {X,Y} end, [a], [1]) =:= [{a,1}]") "true")
|
||||
|
||||
;; ── lists:unzip/1 ─────────────────────────────────────────────────
|
||||
(er-lx-test "unzip"
|
||||
(er-lx-nm "lists:unzip([{a,1},{b,2},{c,3}]) =:= {[a,b,c],[1,2,3]}") "true")
|
||||
|
||||
(er-lx-test "unzip empty"
|
||||
(er-lx-nm "lists:unzip([]) =:= {[],[]}") "true")
|
||||
|
||||
(er-lx-test "zip/unzip roundtrip"
|
||||
(er-lx-nm "lists:unzip(lists:zip([1,2],[3,4])) =:= {[1,2],[3,4]}") "true")
|
||||
|
||||
;; ── lists:sublist/2,3 ─────────────────────────────────────────────
|
||||
(er-lx-test "sublist/2 first n"
|
||||
(er-lx-nm "lists:sublist([1,2,3,4,5],3) =:= [1,2,3]") "true")
|
||||
|
||||
(er-lx-test "sublist/2 over length"
|
||||
(er-lx-nm "lists:sublist([1,2],5) =:= [1,2]") "true")
|
||||
|
||||
(er-lx-test "sublist/2 zero"
|
||||
(er-lx-nm "lists:sublist([1,2,3],0) =:= []") "true")
|
||||
|
||||
(er-lx-test "sublist/3 mid"
|
||||
(er-lx-nm "lists:sublist([1,2,3,4,5],2,3) =:= [2,3,4]") "true")
|
||||
|
||||
(er-lx-test "sublist/3 to end"
|
||||
(er-lx-nm "lists:sublist([1,2,3],2,10) =:= [2,3]") "true")
|
||||
|
||||
;; ── lists:nthtail/2 ───────────────────────────────────────────────
|
||||
(er-lx-test "nthtail mid"
|
||||
(er-lx-nm "lists:nthtail(2,[1,2,3,4]) =:= [3,4]") "true")
|
||||
|
||||
(er-lx-test "nthtail zero"
|
||||
(er-lx-nm "lists:nthtail(0,[1,2]) =:= [1,2]") "true")
|
||||
|
||||
(er-lx-test "nthtail full"
|
||||
(er-lx-nm "lists:nthtail(3,[1,2,3]) =:= []") "true")
|
||||
|
||||
;; ── lists:split/2 ─────────────────────────────────────────────────
|
||||
(er-lx-test "split mid"
|
||||
(er-lx-nm "lists:split(2,[1,2,3,4,5]) =:= {[1,2],[3,4,5]}") "true")
|
||||
|
||||
(er-lx-test "split zero"
|
||||
(er-lx-nm "lists:split(0,[1,2]) =:= {[],[1,2]}") "true")
|
||||
|
||||
(er-lx-test "split full"
|
||||
(er-lx-nm "lists:split(3,[1,2,3]) =:= {[1,2,3],[]}") "true")
|
||||
|
||||
;; ── lists:droplast/1 ──────────────────────────────────────────────
|
||||
(er-lx-test "droplast"
|
||||
(er-lx-nm "lists:droplast([1,2,3]) =:= [1,2]") "true")
|
||||
|
||||
(er-lx-test "droplast single"
|
||||
(er-lx-nm "lists:droplast([9]) =:= []") "true")
|
||||
|
||||
;; ── lists:flatmap/2 ───────────────────────────────────────────────
|
||||
(er-lx-test "flatmap duplicates"
|
||||
(er-lx-nm "lists:flatmap(fun(X) -> [X,X] end, [1,2]) =:= [1,1,2,2]") "true")
|
||||
|
||||
(er-lx-test "flatmap empty"
|
||||
(er-lx-nm "lists:flatmap(fun(X) -> [X] end, []) =:= []") "true")
|
||||
|
||||
;; ── lists:filtermap/2 ─────────────────────────────────────────────
|
||||
(er-lx-test "filtermap transform"
|
||||
(er-lx-nm
|
||||
"lists:filtermap(fun(X) -> case X rem 2 of 0 -> {true, X*10}; _ -> false end end, [1,2,3,4]) =:= [20,40]")
|
||||
"true")
|
||||
|
||||
(er-lx-test "filtermap bool keep"
|
||||
(er-lx-nm "lists:filtermap(fun(X) -> X > 2 end, [1,2,3,4]) =:= [3,4]") "true")
|
||||
|
||||
;; ── lists:mapfoldl/3 ──────────────────────────────────────────────
|
||||
(er-lx-test "mapfoldl map+acc"
|
||||
(er-lx-nm
|
||||
"lists:mapfoldl(fun(X,A) -> {X*2, A+X} end, 0, [1,2,3]) =:= {[2,4,6],6}") "true")
|
||||
|
||||
(er-lx-test "mapfoldl empty"
|
||||
(er-lx-nm "lists:mapfoldl(fun(X,A) -> {X,A} end, 5, []) =:= {[],5}") "true")
|
||||
|
||||
;; ── lists:search/2 ────────────────────────────────────────────────
|
||||
(er-lx-test "search hit"
|
||||
(er-lx-nm "lists:search(fun(X) -> X > 2 end, [1,2,3,4]) =:= {value,3}") "true")
|
||||
|
||||
(er-lx-test "search miss"
|
||||
(er-lx-nm "lists:search(fun(X) -> X > 9 end, [1,2,3])") "false")
|
||||
|
||||
;; ── proplists:get_value/2,3 ───────────────────────────────────────
|
||||
(er-lx-test "pl get_value hit"
|
||||
(erlang-eval-ast "proplists:get_value(b, [{a,1},{b,2}])") 2)
|
||||
|
||||
(er-lx-test "pl get_value miss undefined"
|
||||
(er-lx-nm "proplists:get_value(z, [{a,1}])") "undefined")
|
||||
|
||||
(er-lx-test "pl get_value default"
|
||||
(erlang-eval-ast "proplists:get_value(z, [{a,1}], 99)") 99)
|
||||
|
||||
(er-lx-test "pl get_value bare atom is true"
|
||||
(er-lx-nm "proplists:get_value(flag, [flag, {a,1}])") "true")
|
||||
|
||||
(er-lx-test "pl get_value first occurrence"
|
||||
(erlang-eval-ast "proplists:get_value(a, [{a,1},{a,2}])") 1)
|
||||
|
||||
;; ── proplists:get_all_values/2 ────────────────────────────────────
|
||||
(er-lx-test "pl get_all_values"
|
||||
(er-lx-nm
|
||||
"proplists:get_all_values(a, [{a,1},{b,2},{a,3}]) =:= [1,3]") "true")
|
||||
|
||||
;; ── proplists:is_defined/2 ────────────────────────────────────────
|
||||
(er-lx-test "pl is_defined true"
|
||||
(er-lx-nm "proplists:is_defined(b, [{a,1},{b,2}])") "true")
|
||||
|
||||
(er-lx-test "pl is_defined false"
|
||||
(er-lx-nm "proplists:is_defined(z, [{a,1}])") "false")
|
||||
|
||||
;; ── proplists:lookup/2 ────────────────────────────────────────────
|
||||
(er-lx-test "pl lookup hit"
|
||||
(er-lx-nm "proplists:lookup(b, [{a,1},{b,2}]) =:= {b,2}") "true")
|
||||
|
||||
(er-lx-test "pl lookup bare atom"
|
||||
(er-lx-nm "proplists:lookup(flag, [flag]) =:= {flag,true}") "true")
|
||||
|
||||
(er-lx-test "pl lookup miss"
|
||||
(er-lx-nm "proplists:lookup(z, [{a,1}])") "none")
|
||||
|
||||
;; ── proplists:delete/2 ────────────────────────────────────────────
|
||||
(er-lx-test "pl delete removes all"
|
||||
(er-lx-nm "proplists:delete(a, [{a,1},{b,2},{a,3}]) =:= [{b,2}]") "true")
|
||||
@@ -1,163 +0,0 @@
|
||||
;; erlang:send_after / cancel_timer — timer primitives.
|
||||
;;
|
||||
;; A process schedules a message to itself (or another pid / registered
|
||||
;; name) after N logical milliseconds. `cancel_timer` removes a pending
|
||||
;; timer and reports the time left. These are the same primitives the
|
||||
;; gen_server library uses to implement `{noreply, State, Timeout}`.
|
||||
;;
|
||||
;; The scheduler runs a synchronous logical clock (see runtime.sx
|
||||
;; `er-sched-advance-time!`): time advances only when the runnable
|
||||
;; queue drains, jumping to the earliest pending deadline. That makes
|
||||
;; delivery deterministic and time-travel-safe — no wall clock.
|
||||
|
||||
(define er-sa-test-count 0)
|
||||
(define er-sa-test-pass 0)
|
||||
(define er-sa-test-fails (list))
|
||||
|
||||
(define
|
||||
er-sa-test
|
||||
(fn
|
||||
(name actual expected)
|
||||
(set! er-sa-test-count (+ er-sa-test-count 1))
|
||||
(if
|
||||
(= actual expected)
|
||||
(set! er-sa-test-pass (+ er-sa-test-pass 1))
|
||||
(append!
|
||||
er-sa-test-fails
|
||||
{:actual actual :expected expected :name name}))))
|
||||
|
||||
(define er-sa-pred
|
||||
(fn (name actual) (er-sa-test name (if actual true false) true)))
|
||||
|
||||
(define sa-ev erlang-eval-ast)
|
||||
|
||||
;; ── T1 — schedule a self-message, receive it after the deadline ──
|
||||
;; send_after returns a reference handle.
|
||||
(er-sa-pred
|
||||
"T1 send_after returns a ref"
|
||||
(er-ref?
|
||||
(sa-ev "erlang:send_after(50, self(), hello)")))
|
||||
|
||||
;; The scheduled message lands and a plain receive picks it up.
|
||||
(er-sa-test
|
||||
"T1 delivered message received"
|
||||
(get
|
||||
(sa-ev
|
||||
"erlang:send_after(50, self(), hello),
|
||||
receive M -> M end")
|
||||
:name)
|
||||
"hello")
|
||||
|
||||
;; Logical time advances exactly to the timer deadline (50ms) by the
|
||||
;; time the message is received — round-trip latency well under 100ms.
|
||||
(er-sa-test
|
||||
"T1 clock at deadline on receipt"
|
||||
(sa-ev
|
||||
"erlang:send_after(50, self(), hello),
|
||||
receive hello -> erlang:monotonic_time() end")
|
||||
50)
|
||||
|
||||
;; ── T2 — cancel_timer returns remaining ms; message never arrives ──
|
||||
;; Cancel immediately after scheduling: clock has not advanced, so the
|
||||
;; full duration (~1000ms) is reported as remaining.
|
||||
(er-sa-test
|
||||
"T2 cancel returns remaining ms"
|
||||
(sa-ev
|
||||
"Ref = erlang:send_after(1000, self(), late),
|
||||
erlang:cancel_timer(Ref)")
|
||||
1000)
|
||||
|
||||
;; The cancelled timer never delivers — the receive falls through to
|
||||
;; its `after` clause and returns `none`.
|
||||
(er-sa-test
|
||||
"T2 cancelled message never arrives"
|
||||
(get
|
||||
(sa-ev
|
||||
"Ref = erlang:send_after(1000, self(), late),
|
||||
erlang:cancel_timer(Ref),
|
||||
receive late -> got after 50 -> none end")
|
||||
:name)
|
||||
"none")
|
||||
|
||||
;; ── T3 — multiple timers fire in deadline order, not schedule order ──
|
||||
;; `b` is scheduled first (deadline 80) but `a` second (deadline 20).
|
||||
;; Two plain receives drain the mailbox in arrival order — and arrival
|
||||
;; is governed by deadline, so the first message out is `a`.
|
||||
(er-sa-test
|
||||
"T3 timers fire in deadline order"
|
||||
(er-format-value
|
||||
(sa-ev
|
||||
"erlang:send_after(80, self(), b),
|
||||
erlang:send_after(20, self(), a),
|
||||
X = receive M1 -> M1 end,
|
||||
Y = receive M2 -> M2 end,
|
||||
{X, Y}"))
|
||||
"{a,b}")
|
||||
|
||||
;; A selective receive on `a` matches the earlier-deadline timer even
|
||||
;; though `b` was scheduled first.
|
||||
(er-sa-test
|
||||
"T3 selective receive picks earliest deadline"
|
||||
(get
|
||||
(sa-ev
|
||||
"erlang:send_after(80, self(), b),
|
||||
erlang:send_after(20, self(), a),
|
||||
receive a -> first end")
|
||||
:name)
|
||||
"first")
|
||||
|
||||
;; ── T4 — cancel_timer on an already-fired timer returns false ──────
|
||||
;; Once `x` has been received the timer has fired; cancelling its ref
|
||||
;; now yields the atom `false`.
|
||||
(er-sa-test
|
||||
"T4 cancel of fired timer is false"
|
||||
(get
|
||||
(sa-ev
|
||||
"Ref = erlang:send_after(20, self(), x),
|
||||
receive x -> ok end,
|
||||
erlang:cancel_timer(Ref)")
|
||||
:name)
|
||||
"false")
|
||||
|
||||
;; ── T5 — send_after to a registered atom name ──────────────────────
|
||||
;; A second process registers itself as `srv`; the timer addresses it
|
||||
;; by name, and the delayed message lands in that process's mailbox.
|
||||
;; The server forwards what it got back to the parent for inspection.
|
||||
(er-sa-test
|
||||
"T5 timer delivers to registered name"
|
||||
(get
|
||||
(sa-ev
|
||||
"Me = self(),
|
||||
Pid = spawn(fun () -> receive M -> Me ! {got, M} end end),
|
||||
register(srv, Pid),
|
||||
erlang:send_after(20, srv, ping),
|
||||
receive {got, X} -> X end")
|
||||
:name)
|
||||
"ping")
|
||||
|
||||
;; ── T6 — gen_server {noreply, State, Timeout} hookup ───────────────
|
||||
;; A gen_server that, on the `arm` cast, returns {noreply, S, 100}.
|
||||
;; The library schedules {timeout} to itself via send_after; when no
|
||||
;; other message arrives first, handle_info({timeout}, S) fires. The
|
||||
;; handler signals the parent so we can confirm the timeout landed.
|
||||
(do
|
||||
(er-load-gen-server!)
|
||||
(erlang-load-module
|
||||
"-module(sa_tmo).
|
||||
init(Me) -> {ok, Me}.
|
||||
handle_call(_R, _F, S) -> {reply, ok, S}.
|
||||
handle_cast(arm, Me) -> {noreply, Me, 100}.
|
||||
handle_info({timeout}, Me) -> Me ! fired, {noreply, Me};
|
||||
handle_info(_M, S) -> {noreply, S}.")
|
||||
nil)
|
||||
|
||||
(er-sa-test
|
||||
"T6 gen_server timeout fires handle_info"
|
||||
(get
|
||||
(sa-ev
|
||||
"Me = self(),
|
||||
P = gen_server:start_link(sa_tmo, Me),
|
||||
gen_server:cast(P, arm),
|
||||
receive fired -> ok after 5000 -> timeout end")
|
||||
:name)
|
||||
"ok")
|
||||
@@ -229,37 +229,13 @@
|
||||
(= ch "$")
|
||||
(do
|
||||
(er-advance! 1)
|
||||
;; Emit the char's decimal code as the integer token value
|
||||
;; (was: raw "$X" text — parse-number then returned nil).
|
||||
(let
|
||||
((code (cond
|
||||
(>= pos src-len) 0
|
||||
(= (er-cur) "\\")
|
||||
(do
|
||||
(er-advance! 1)
|
||||
(let ((esc (if (< pos src-len) (er-cur) "")))
|
||||
(when (< pos src-len) (er-advance! 1))
|
||||
(cond
|
||||
(= esc "n") 10
|
||||
(= esc "t") 9
|
||||
(= esc "r") 13
|
||||
(= esc "s") 32
|
||||
(= esc "b") 8
|
||||
(= esc "e") 27
|
||||
(= esc "f") 12
|
||||
(= esc "v") 11
|
||||
(= esc "d") 127
|
||||
(= esc "0") 0
|
||||
(= esc "\\") 92
|
||||
(= esc "\"") 34
|
||||
(= esc "'") 39
|
||||
(= esc "") 0
|
||||
:else (char->integer (nth (string->list esc) 0)))))
|
||||
:else
|
||||
(let ((c (er-cur)))
|
||||
(er-advance! 1)
|
||||
(char->integer (nth (string->list c) 0))))))
|
||||
(er-emit! "integer" (str code) start))
|
||||
(if
|
||||
(and (< pos src-len) (= (er-cur) "\\"))
|
||||
(do
|
||||
(er-advance! 1)
|
||||
(when (< pos src-len) (er-advance! 1)))
|
||||
(when (< pos src-len) (er-advance! 1)))
|
||||
(er-emit! "integer" (slice src start pos) start)
|
||||
(scan!))
|
||||
(er-lower? ch)
|
||||
(do
|
||||
|
||||
@@ -107,12 +107,7 @@
|
||||
(let
|
||||
((ty (get node :type)))
|
||||
(cond
|
||||
(= ty "integer")
|
||||
(let ((n (parse-number (get node :value))))
|
||||
(cond
|
||||
(= n nil) (error (str "Erlang: invalid integer literal: "
|
||||
(get node :value)))
|
||||
:else (truncate n)))
|
||||
(= ty "integer") (parse-number (get node :value))
|
||||
(= ty "float") (parse-number (get node :value))
|
||||
(= ty "atom") (er-mk-atom (get node :value))
|
||||
(= ty "string") (get node :value)
|
||||
@@ -826,30 +821,16 @@
|
||||
(len (get v :elements))
|
||||
(error "Erlang: tuple_size: not a tuple")))))
|
||||
|
||||
(define er-string->charlist
|
||||
(fn (s)
|
||||
(let ((cs (string->list s)) (out (er-mk-nil)))
|
||||
(for-each
|
||||
(fn (i)
|
||||
(set! out (er-mk-cons
|
||||
(char->integer (nth cs (- (- (len cs) 1) i)))
|
||||
out)))
|
||||
(range 0 (len cs)))
|
||||
out)))
|
||||
|
||||
(define
|
||||
er-bif-atom-to-list
|
||||
(fn
|
||||
(vs)
|
||||
(let
|
||||
((v (er-bif-arg1 vs "atom_to_list")))
|
||||
;; Standard Erlang: atom_to_list/1 returns an Erlang charlist
|
||||
;; (list of integer char codes). Was: SX string of :name —
|
||||
;; unusable from Erlang-land for [Char|T] / ++ / binary segments.
|
||||
(if
|
||||
(er-atom? v)
|
||||
(er-string->charlist (get v :name))
|
||||
(raise (er-mk-error-marker (er-mk-atom "badarg")))))))
|
||||
(get v :name)
|
||||
(error "Erlang: atom_to_list: not an atom")))))
|
||||
|
||||
(define
|
||||
er-bif-list-to-atom
|
||||
@@ -857,11 +838,10 @@
|
||||
(vs)
|
||||
(let
|
||||
((v (er-bif-arg1 vs "list_to_atom")))
|
||||
;; Accept Erlang charlist (cons of ints) or SX string.
|
||||
(let ((s (er-source-to-string v)))
|
||||
(cond
|
||||
(= s nil) (raise (er-mk-error-marker (er-mk-atom "badarg")))
|
||||
:else (er-mk-atom s))))))
|
||||
(if
|
||||
(= (type-of v) "string")
|
||||
(er-mk-atom v)
|
||||
(error "Erlang: list_to_atom: not a string")))))
|
||||
|
||||
;; ── lists module ─────────────────────────────────────────────────
|
||||
(define
|
||||
@@ -1147,7 +1127,7 @@
|
||||
(and (er-atom? ms) (= (get ms :name) "infinity"))
|
||||
(er-eval-receive-loop node pid env)
|
||||
(= ms 0) (er-eval-receive-poll node pid env)
|
||||
:else (er-eval-receive-timed node pid env (+ (er-clock) ms))))))
|
||||
:else (er-eval-receive-timed node pid env)))))
|
||||
|
||||
;; after 0 — poll once; on no match, run the after-body immediately.
|
||||
(define
|
||||
@@ -1161,15 +1141,12 @@
|
||||
(get r :value)
|
||||
(er-eval-body (get node :after-body) env)))))
|
||||
|
||||
;; after Ms — suspend with an absolute `deadline` (logical ms). On
|
||||
;; resume check :timed-out: the scheduler fires the earliest pending
|
||||
;; deadline once the runnable queue drains. A non-matching message can
|
||||
;; wake the process early; it re-suspends on the SAME deadline so the
|
||||
;; timeout window is not extended.
|
||||
;; after Ms — suspend; on resume check :timed-out. When the scheduler
|
||||
;; runs out of other work it fires one pending timeout per round.
|
||||
(define
|
||||
er-eval-receive-timed
|
||||
(fn
|
||||
(node pid env deadline)
|
||||
(node pid env)
|
||||
(let
|
||||
((r (er-try-receive (get node :clauses) pid env)))
|
||||
(if
|
||||
@@ -1177,7 +1154,6 @@
|
||||
(get r :value)
|
||||
(do
|
||||
(er-proc-set! pid :has-timeout true)
|
||||
(er-proc-set! pid :timeout-deadline deadline)
|
||||
(call/cc
|
||||
(fn
|
||||
(k)
|
||||
@@ -1190,7 +1166,7 @@
|
||||
(er-proc-set! pid :timed-out false)
|
||||
(er-proc-set! pid :has-timeout false)
|
||||
(er-eval-body (get node :after-body) env))
|
||||
(er-eval-receive-timed node pid env deadline)))))))
|
||||
(er-eval-receive-timed node pid env)))))))
|
||||
|
||||
;; Scan mailbox in arrival order. For each msg, try every clause.
|
||||
;; On first match: remove that msg from mailbox and return body value.
|
||||
@@ -1621,12 +1597,10 @@
|
||||
(vs)
|
||||
(let
|
||||
((v (er-bif-arg1 vs "integer_to_list")))
|
||||
;; Standard Erlang: integer_to_list/1 returns an Erlang charlist
|
||||
;; (e.g. integer_to_list(42) -> [$4, $2] -> [52, 50]).
|
||||
(cond
|
||||
(not (= (type-of v) "number"))
|
||||
(raise (er-mk-error-marker (er-mk-atom "badarg")))
|
||||
:else (er-string->charlist (str v))))))
|
||||
:else (str v)))))
|
||||
|
||||
(define
|
||||
er-bif-list-to-integer
|
||||
@@ -1634,14 +1608,15 @@
|
||||
(vs)
|
||||
(let
|
||||
((v (er-bif-arg1 vs "list_to_integer")))
|
||||
;; Accept Erlang charlist (cons of ints) or SX string.
|
||||
(let ((s (er-source-to-string v)))
|
||||
(cond
|
||||
(= s nil) (raise (er-mk-error-marker (er-mk-atom "badarg")))
|
||||
:else (let ((n (parse-number s)))
|
||||
(cond
|
||||
(= n nil) (raise (er-mk-error-marker (er-mk-atom "badarg")))
|
||||
:else n)))))))
|
||||
(cond
|
||||
(not (= (type-of v) "string"))
|
||||
(raise (er-mk-error-marker (er-mk-atom "badarg")))
|
||||
:else (let
|
||||
((n (parse-number v)))
|
||||
(cond
|
||||
(= n nil)
|
||||
(raise (er-mk-error-marker (er-mk-atom "badarg")))
|
||||
:else n))))))
|
||||
|
||||
(define
|
||||
er-bif-is-function
|
||||
@@ -2039,657 +2014,4 @@
|
||||
(range 0 (len ks)))
|
||||
out)))
|
||||
|
||||
;; ── extra lists + proplists BIFs (folded from lists-ext.sx) ──
|
||||
;; ── cons <-> SX-list bridges ──────────────────────────────────────
|
||||
(define
|
||||
er-cons->sxlist
|
||||
(fn (lst)
|
||||
(cond
|
||||
(er-nil? lst) (list)
|
||||
(er-cons? lst) (cons (get lst :head) (er-cons->sxlist (get lst :tail)))
|
||||
:else (raise (er-mk-error-marker (er-mk-atom "badarg"))))))
|
||||
|
||||
(define
|
||||
er-sxlist->cons
|
||||
(fn (xs)
|
||||
(if (= (len xs) 0)
|
||||
(er-mk-nil)
|
||||
(er-mk-cons (first xs) (er-sxlist->cons (rest xs))))))
|
||||
|
||||
;; ── merge sort over SX lists (stable) ─────────────────────────────
|
||||
(define
|
||||
er-ext-take
|
||||
(fn (xs n)
|
||||
(if (or (= n 0) (= (len xs) 0))
|
||||
(list)
|
||||
(cons (first xs) (er-ext-take (rest xs) (- n 1))))))
|
||||
|
||||
(define
|
||||
er-ext-drop
|
||||
(fn (xs n)
|
||||
(if (or (= n 0) (= (len xs) 0))
|
||||
xs
|
||||
(er-ext-drop (rest xs) (- n 1)))))
|
||||
|
||||
;; le? returns a truthy value (Erlang bool atom or SX bool) iff a
|
||||
;; should sort at-or-before b. Taking from the left half first on a
|
||||
;; true result keeps the sort stable.
|
||||
(define
|
||||
er-ext-merge
|
||||
(fn (a b le?)
|
||||
(cond
|
||||
(= (len a) 0) b
|
||||
(= (len b) 0) a
|
||||
(er-truthy? (le? (first a) (first b)))
|
||||
(cons (first a) (er-ext-merge (rest a) b le?))
|
||||
:else (cons (first b) (er-ext-merge a (rest b) le?)))))
|
||||
|
||||
(define
|
||||
er-ext-msort
|
||||
(fn (xs le?)
|
||||
(if (<= (len xs) 1)
|
||||
xs
|
||||
(let ((mid (quotient (len xs) 2)))
|
||||
(er-ext-merge
|
||||
(er-ext-msort (er-ext-take xs mid) le?)
|
||||
(er-ext-msort (er-ext-drop xs mid) le?)
|
||||
le?)))))
|
||||
|
||||
;; Full Erlang term order. The shared er-lt? (transpile.sx) only
|
||||
;; deep-compares numbers/atoms/strings and otherwise falls back to a
|
||||
;; coarse type rank — so any two tuples (or two lists) compare as
|
||||
;; order-equal there. er-ext-lt? adds the missing structural cases:
|
||||
;; tuples by arity then elementwise, lists elementwise with a shorter
|
||||
;; proper prefix sorting first. Cross-type cases delegate to er-lt?.
|
||||
(define
|
||||
er-ext-lt-seq
|
||||
(fn (ea eb i)
|
||||
(cond
|
||||
(>= i (len ea)) false
|
||||
(er-ext-lt? (nth ea i) (nth eb i)) true
|
||||
(er-ext-lt? (nth eb i) (nth ea i)) false
|
||||
:else (er-ext-lt-seq ea eb (+ i 1)))))
|
||||
|
||||
(define
|
||||
er-ext-lt?
|
||||
(fn (a b)
|
||||
(cond
|
||||
(and (er-tuple? a) (er-tuple? b))
|
||||
(let ((ea (get a :elements)) (eb (get b :elements)))
|
||||
(cond
|
||||
(< (len ea) (len eb)) true
|
||||
(> (len ea) (len eb)) false
|
||||
:else (er-ext-lt-seq ea eb 0)))
|
||||
(and (er-cons? a) (er-cons? b))
|
||||
(cond
|
||||
(er-ext-lt? (get a :head) (get b :head)) true
|
||||
(er-ext-lt? (get b :head) (get a :head)) false
|
||||
:else (er-ext-lt? (get a :tail) (get b :tail)))
|
||||
(and (er-nil? a) (er-cons? b)) true
|
||||
(and (er-cons? a) (er-nil? b)) false
|
||||
(and (er-nil? a) (er-nil? b)) false
|
||||
:else (er-lt? a b))))
|
||||
|
||||
;; Default Erlang term order: a =< b == not (b < a).
|
||||
(define
|
||||
er-ext-term-le
|
||||
(fn (a b) (er-bool (not (er-ext-lt? b a)))))
|
||||
|
||||
;; ── lists:sort/1, lists:sort/2 ────────────────────────────────────
|
||||
(define
|
||||
er-bif-lists-sort
|
||||
(fn (vs)
|
||||
(cond
|
||||
(= (len vs) 1)
|
||||
(er-sxlist->cons
|
||||
(er-ext-msort (er-cons->sxlist (nth vs 0)) er-ext-term-le))
|
||||
(= (len vs) 2)
|
||||
(let ((f (nth vs 0)) (lst (nth vs 1)))
|
||||
(er-sxlist->cons
|
||||
(er-ext-msort
|
||||
(er-cons->sxlist lst)
|
||||
(fn (a b) (er-apply-fun f (list a b))))))
|
||||
:else (error "Erlang: lists:sort: wrong arity"))))
|
||||
|
||||
;; ── lists:usort/1 (sort then drop adjacent term-equal dups) ───────
|
||||
(define
|
||||
er-ext-dedup
|
||||
(fn (xs)
|
||||
(cond
|
||||
(= (len xs) 0) (list)
|
||||
(= (len xs) 1) xs
|
||||
(er-equal? (first xs) (nth xs 1)) (er-ext-dedup (rest xs))
|
||||
:else (cons (first xs) (er-ext-dedup (rest xs))))))
|
||||
|
||||
(define
|
||||
er-bif-lists-usort
|
||||
(fn (vs)
|
||||
(let ((lst (er-bif-arg1 vs "lists:usort")))
|
||||
(er-sxlist->cons
|
||||
(er-ext-dedup
|
||||
(er-ext-msort (er-cons->sxlist lst) er-ext-term-le))))))
|
||||
|
||||
;; ── keylists (lists of tuples keyed on element N, 1-indexed) ──────
|
||||
;; keyfind/keymember/keydelete/keyreplace/keystore/keytake/keysort.
|
||||
;; Key comparison is == (er-equal?), matching the standard lib. Only
|
||||
;; the FIRST matching tuple is acted on. Non-tuples / tuples shorter
|
||||
;; than N never match and are passed through unchanged.
|
||||
(define
|
||||
er-ext-tup-elem
|
||||
(fn (tup n)
|
||||
(if (er-tuple? tup)
|
||||
(let ((es (get tup :elements)))
|
||||
(if (and (>= n 1) (<= n (len es))) (nth es (- n 1)) nil))
|
||||
nil)))
|
||||
|
||||
(define
|
||||
er-ext-key-match?
|
||||
(fn (key n tup)
|
||||
(and
|
||||
(er-tuple? tup)
|
||||
(>= n 1)
|
||||
(<= n (len (get tup :elements)))
|
||||
(er-equal? key (nth (get tup :elements) (- n 1))))))
|
||||
|
||||
(define
|
||||
er-ext-keyfind
|
||||
(fn (key n lst)
|
||||
(cond
|
||||
(er-nil? lst) (er-mk-atom "false")
|
||||
(er-cons? lst)
|
||||
(if (er-ext-key-match? key n (get lst :head))
|
||||
(get lst :head)
|
||||
(er-ext-keyfind key n (get lst :tail)))
|
||||
:else (er-mk-atom "false"))))
|
||||
|
||||
(define
|
||||
er-ext-keydelete
|
||||
(fn (key n lst)
|
||||
(cond
|
||||
(er-nil? lst) (er-mk-nil)
|
||||
(er-cons? lst)
|
||||
(if (er-ext-key-match? key n (get lst :head))
|
||||
(get lst :tail)
|
||||
(er-mk-cons (get lst :head) (er-ext-keydelete key n (get lst :tail))))
|
||||
:else lst)))
|
||||
|
||||
(define
|
||||
er-ext-keyreplace
|
||||
(fn (key n lst new)
|
||||
(cond
|
||||
(er-nil? lst) (er-mk-nil)
|
||||
(er-cons? lst)
|
||||
(if (er-ext-key-match? key n (get lst :head))
|
||||
(er-mk-cons new (get lst :tail))
|
||||
(er-mk-cons (get lst :head) (er-ext-keyreplace key n (get lst :tail) new)))
|
||||
:else lst)))
|
||||
|
||||
(define
|
||||
er-ext-keystore
|
||||
(fn (key n lst new)
|
||||
(cond
|
||||
(er-nil? lst) (er-mk-cons new (er-mk-nil))
|
||||
(er-cons? lst)
|
||||
(if (er-ext-key-match? key n (get lst :head))
|
||||
(er-mk-cons new (get lst :tail))
|
||||
(er-mk-cons (get lst :head) (er-ext-keystore key n (get lst :tail) new)))
|
||||
:else lst)))
|
||||
|
||||
(define
|
||||
er-bif-lists-keyfind
|
||||
(fn (vs) (er-ext-keyfind (nth vs 0) (nth vs 1) (nth vs 2))))
|
||||
|
||||
(define
|
||||
er-bif-lists-keymember
|
||||
(fn (vs)
|
||||
(er-bool (not (er-atom? (er-ext-keyfind (nth vs 0) (nth vs 1) (nth vs 2)))))))
|
||||
|
||||
(define
|
||||
er-bif-lists-keydelete
|
||||
(fn (vs) (er-ext-keydelete (nth vs 0) (nth vs 1) (nth vs 2))))
|
||||
|
||||
(define
|
||||
er-bif-lists-keyreplace
|
||||
(fn (vs) (er-ext-keyreplace (nth vs 0) (nth vs 1) (nth vs 2) (nth vs 3))))
|
||||
|
||||
(define
|
||||
er-bif-lists-keystore
|
||||
(fn (vs) (er-ext-keystore (nth vs 0) (nth vs 1) (nth vs 2) (nth vs 3))))
|
||||
|
||||
(define
|
||||
er-bif-lists-keytake
|
||||
(fn (vs)
|
||||
(let ((key (nth vs 0)) (n (nth vs 1)) (lst (nth vs 2)))
|
||||
(let ((hit (er-ext-keyfind key n lst)))
|
||||
(if (er-atom? hit)
|
||||
(er-mk-atom "false")
|
||||
(er-mk-tuple
|
||||
(list (er-mk-atom "value") hit (er-ext-keydelete key n lst))))))))
|
||||
|
||||
(define
|
||||
er-bif-lists-keysort
|
||||
(fn (vs)
|
||||
(let ((n (nth vs 0)) (lst (nth vs 1)))
|
||||
(er-sxlist->cons
|
||||
(er-ext-msort
|
||||
(er-cons->sxlist lst)
|
||||
(fn (a b)
|
||||
(er-bool
|
||||
(not (er-ext-lt? (er-ext-tup-elem b n) (er-ext-tup-elem a n))))))))))
|
||||
|
||||
;; ── higher-order traversal (foldr / partition / *while) ───────────
|
||||
(define
|
||||
er-ext-foldr
|
||||
(fn (f acc lst)
|
||||
(cond
|
||||
(er-nil? lst) acc
|
||||
(er-cons? lst)
|
||||
(er-apply-fun f (list (get lst :head) (er-ext-foldr f acc (get lst :tail))))
|
||||
:else (raise (er-mk-error-marker (er-mk-atom "badarg"))))))
|
||||
|
||||
(define
|
||||
er-bif-lists-foldr
|
||||
(fn (vs) (er-ext-foldr (nth vs 0) (nth vs 1) (nth vs 2))))
|
||||
|
||||
(define
|
||||
er-ext-partition
|
||||
(fn (pred lst yes no)
|
||||
(cond
|
||||
(er-nil? lst)
|
||||
(er-mk-tuple
|
||||
(list
|
||||
(er-list-reverse-iter yes (er-mk-nil))
|
||||
(er-list-reverse-iter no (er-mk-nil))))
|
||||
(er-cons? lst)
|
||||
(if (er-truthy? (er-apply-fun pred (list (get lst :head))))
|
||||
(er-ext-partition pred (get lst :tail) (er-mk-cons (get lst :head) yes) no)
|
||||
(er-ext-partition pred (get lst :tail) yes (er-mk-cons (get lst :head) no)))
|
||||
:else (raise (er-mk-error-marker (er-mk-atom "badarg"))))))
|
||||
|
||||
(define
|
||||
er-bif-lists-partition
|
||||
(fn (vs) (er-ext-partition (nth vs 0) (nth vs 1) (er-mk-nil) (er-mk-nil))))
|
||||
|
||||
(define
|
||||
er-ext-takewhile
|
||||
(fn (pred lst)
|
||||
(cond
|
||||
(er-nil? lst) (er-mk-nil)
|
||||
(er-cons? lst)
|
||||
(if (er-truthy? (er-apply-fun pred (list (get lst :head))))
|
||||
(er-mk-cons (get lst :head) (er-ext-takewhile pred (get lst :tail)))
|
||||
(er-mk-nil))
|
||||
:else (er-mk-nil))))
|
||||
|
||||
(define
|
||||
er-bif-lists-takewhile
|
||||
(fn (vs) (er-ext-takewhile (nth vs 0) (nth vs 1))))
|
||||
|
||||
(define
|
||||
er-ext-dropwhile
|
||||
(fn (pred lst)
|
||||
(cond
|
||||
(er-nil? lst) (er-mk-nil)
|
||||
(er-cons? lst)
|
||||
(if (er-truthy? (er-apply-fun pred (list (get lst :head))))
|
||||
(er-ext-dropwhile pred (get lst :tail))
|
||||
lst)
|
||||
:else lst)))
|
||||
|
||||
(define
|
||||
er-bif-lists-dropwhile
|
||||
(fn (vs) (er-ext-dropwhile (nth vs 0) (nth vs 1))))
|
||||
|
||||
(define
|
||||
er-bif-lists-splitwith
|
||||
(fn (vs)
|
||||
(let ((pred (nth vs 0)) (lst (nth vs 1)))
|
||||
(er-mk-tuple
|
||||
(list (er-ext-takewhile pred lst) (er-ext-dropwhile pred lst))))))
|
||||
|
||||
;; ── structural / aggregate (flatten / max / min) ──────────────────
|
||||
(define
|
||||
er-ext-flatten
|
||||
(fn (lst)
|
||||
(cond
|
||||
(er-nil? lst) (er-mk-nil)
|
||||
(er-cons? lst)
|
||||
(let ((h (get lst :head)))
|
||||
(if (or (er-nil? h) (er-cons? h))
|
||||
(er-list-append (er-ext-flatten h) (er-ext-flatten (get lst :tail)))
|
||||
(er-mk-cons h (er-ext-flatten (get lst :tail)))))
|
||||
:else (raise (er-mk-error-marker (er-mk-atom "badarg"))))))
|
||||
|
||||
(define
|
||||
er-bif-lists-flatten
|
||||
(fn (vs) (er-ext-flatten (er-bif-arg1 vs "lists:flatten"))))
|
||||
|
||||
(define
|
||||
er-ext-extreme
|
||||
(fn (lst best lt?)
|
||||
(cond
|
||||
(er-nil? lst) best
|
||||
(er-cons? lst)
|
||||
(er-ext-extreme
|
||||
(get lst :tail)
|
||||
(if (lt? best (get lst :head)) (get lst :head) best)
|
||||
lt?)
|
||||
:else best)))
|
||||
|
||||
(define
|
||||
er-bif-lists-max
|
||||
(fn (vs)
|
||||
(let ((lst (er-bif-arg1 vs "lists:max")))
|
||||
(if (er-cons? lst)
|
||||
(er-ext-extreme (get lst :tail) (get lst :head)
|
||||
(fn (a b) (er-ext-lt? a b)))
|
||||
(raise (er-mk-error-marker (er-mk-atom "badarg")))))))
|
||||
|
||||
(define
|
||||
er-bif-lists-min
|
||||
(fn (vs)
|
||||
(let ((lst (er-bif-arg1 vs "lists:min")))
|
||||
(if (er-cons? lst)
|
||||
(er-ext-extreme (get lst :tail) (get lst :head)
|
||||
(fn (a b) (er-ext-lt? b a)))
|
||||
(raise (er-mk-error-marker (er-mk-atom "badarg")))))))
|
||||
|
||||
;; ── zip family (zip / zipwith / unzip) ────────────────────────────
|
||||
;; Length mismatch raises badarg (real Erlang raises function_clause;
|
||||
;; badarg is the closest in-port equivalent).
|
||||
(define
|
||||
er-ext-zip
|
||||
(fn (a b)
|
||||
(cond
|
||||
(and (er-nil? a) (er-nil? b)) (er-mk-nil)
|
||||
(and (er-cons? a) (er-cons? b))
|
||||
(er-mk-cons
|
||||
(er-mk-tuple (list (get a :head) (get b :head)))
|
||||
(er-ext-zip (get a :tail) (get b :tail)))
|
||||
:else (raise (er-mk-error-marker (er-mk-atom "badarg"))))))
|
||||
|
||||
(define
|
||||
er-bif-lists-zip
|
||||
(fn (vs) (er-ext-zip (nth vs 0) (nth vs 1))))
|
||||
|
||||
(define
|
||||
er-ext-zipwith
|
||||
(fn (f a b)
|
||||
(cond
|
||||
(and (er-nil? a) (er-nil? b)) (er-mk-nil)
|
||||
(and (er-cons? a) (er-cons? b))
|
||||
(er-mk-cons
|
||||
(er-apply-fun f (list (get a :head) (get b :head)))
|
||||
(er-ext-zipwith f (get a :tail) (get b :tail)))
|
||||
:else (raise (er-mk-error-marker (er-mk-atom "badarg"))))))
|
||||
|
||||
(define
|
||||
er-bif-lists-zipwith
|
||||
(fn (vs) (er-ext-zipwith (nth vs 0) (nth vs 1) (nth vs 2))))
|
||||
|
||||
(define
|
||||
er-ext-unzip
|
||||
(fn (lst as bs)
|
||||
(cond
|
||||
(er-nil? lst)
|
||||
(er-mk-tuple
|
||||
(list
|
||||
(er-list-reverse-iter as (er-mk-nil))
|
||||
(er-list-reverse-iter bs (er-mk-nil))))
|
||||
(and (er-cons? lst) (er-tuple? (get lst :head)))
|
||||
(let ((es (get (get lst :head) :elements)))
|
||||
(if (= (len es) 2)
|
||||
(er-ext-unzip (get lst :tail)
|
||||
(er-mk-cons (nth es 0) as)
|
||||
(er-mk-cons (nth es 1) bs))
|
||||
(raise (er-mk-error-marker (er-mk-atom "badarg")))))
|
||||
:else (raise (er-mk-error-marker (er-mk-atom "badarg"))))))
|
||||
|
||||
(define
|
||||
er-bif-lists-unzip
|
||||
(fn (vs)
|
||||
(er-ext-unzip (er-bif-arg1 vs "lists:unzip") (er-mk-nil) (er-mk-nil))))
|
||||
|
||||
;; ── slicing (sublist / nthtail / split / droplast) ────────────────
|
||||
(define
|
||||
er-ext-sublist2
|
||||
(fn (lst n)
|
||||
(cond
|
||||
(or (<= n 0) (er-nil? lst)) (er-mk-nil)
|
||||
(er-cons? lst)
|
||||
(er-mk-cons (get lst :head) (er-ext-sublist2 (get lst :tail) (- n 1)))
|
||||
:else (er-mk-nil))))
|
||||
|
||||
;; lenient drop (used by sublist/3); never raises
|
||||
(define
|
||||
er-ext-drop-cons
|
||||
(fn (lst n)
|
||||
(cond
|
||||
(or (<= n 0) (er-nil? lst)) lst
|
||||
(er-cons? lst) (er-ext-drop-cons (get lst :tail) (- n 1))
|
||||
:else lst)))
|
||||
|
||||
;; strict drop (used by nthtail/2 + split/2); raises if list too short
|
||||
(define
|
||||
er-ext-nthtail
|
||||
(fn (n lst)
|
||||
(cond
|
||||
(<= n 0) lst
|
||||
(er-cons? lst) (er-ext-nthtail (- n 1) (get lst :tail))
|
||||
:else (raise (er-mk-error-marker (er-mk-atom "badarg"))))))
|
||||
|
||||
(define
|
||||
er-bif-lists-sublist
|
||||
(fn (vs)
|
||||
(cond
|
||||
(= (len vs) 2) (er-ext-sublist2 (nth vs 0) (nth vs 1))
|
||||
(= (len vs) 3)
|
||||
(er-ext-sublist2
|
||||
(er-ext-drop-cons (nth vs 0) (- (nth vs 1) 1))
|
||||
(nth vs 2))
|
||||
:else (error "Erlang: lists:sublist: wrong arity"))))
|
||||
|
||||
(define
|
||||
er-bif-lists-nthtail
|
||||
(fn (vs) (er-ext-nthtail (nth vs 0) (nth vs 1))))
|
||||
|
||||
(define
|
||||
er-bif-lists-split
|
||||
(fn (vs)
|
||||
(let ((n (nth vs 0)) (lst (nth vs 1)))
|
||||
(er-mk-tuple
|
||||
(list (er-ext-sublist2 lst n) (er-ext-nthtail n lst))))))
|
||||
|
||||
(define
|
||||
er-ext-droplast
|
||||
(fn (lst)
|
||||
(cond
|
||||
(and (er-cons? lst) (er-nil? (get lst :tail))) (er-mk-nil)
|
||||
(er-cons? lst) (er-mk-cons (get lst :head) (er-ext-droplast (get lst :tail)))
|
||||
:else (raise (er-mk-error-marker (er-mk-atom "badarg"))))))
|
||||
|
||||
(define
|
||||
er-bif-lists-droplast
|
||||
(fn (vs) (er-ext-droplast (er-bif-arg1 vs "lists:droplast"))))
|
||||
|
||||
;; ── more higher-order (flatmap / filtermap / mapfoldl / search) ───
|
||||
(define
|
||||
er-ext-flatmap
|
||||
(fn (f lst)
|
||||
(cond
|
||||
(er-nil? lst) (er-mk-nil)
|
||||
(er-cons? lst)
|
||||
(er-list-append
|
||||
(er-apply-fun f (list (get lst :head)))
|
||||
(er-ext-flatmap f (get lst :tail)))
|
||||
:else (raise (er-mk-error-marker (er-mk-atom "badarg"))))))
|
||||
|
||||
(define
|
||||
er-bif-lists-flatmap
|
||||
(fn (vs) (er-ext-flatmap (nth vs 0) (nth vs 1))))
|
||||
|
||||
(define
|
||||
er-ext-atom-true?
|
||||
(fn (v) (and (er-atom? v) (= (get v :name) "true"))))
|
||||
|
||||
(define
|
||||
er-ext-filtermap
|
||||
(fn (f lst)
|
||||
(cond
|
||||
(er-nil? lst) (er-mk-nil)
|
||||
(er-cons? lst)
|
||||
(let ((r (er-apply-fun f (list (get lst :head)))))
|
||||
(cond
|
||||
(er-ext-atom-true? r)
|
||||
(er-mk-cons (get lst :head) (er-ext-filtermap f (get lst :tail)))
|
||||
(and
|
||||
(er-tuple? r)
|
||||
(= (len (get r :elements)) 2)
|
||||
(er-ext-atom-true? (nth (get r :elements) 0)))
|
||||
(er-mk-cons (nth (get r :elements) 1) (er-ext-filtermap f (get lst :tail)))
|
||||
:else (er-ext-filtermap f (get lst :tail))))
|
||||
:else (raise (er-mk-error-marker (er-mk-atom "badarg"))))))
|
||||
|
||||
(define
|
||||
er-bif-lists-filtermap
|
||||
(fn (vs) (er-ext-filtermap (nth vs 0) (nth vs 1))))
|
||||
|
||||
(define
|
||||
er-ext-mapfoldl
|
||||
(fn (f acc lst mapped)
|
||||
(cond
|
||||
(er-nil? lst)
|
||||
(er-mk-tuple (list (er-list-reverse-iter mapped (er-mk-nil)) acc))
|
||||
(er-cons? lst)
|
||||
(let ((r (er-apply-fun f (list (get lst :head) acc))))
|
||||
(let ((es (get r :elements)))
|
||||
(er-ext-mapfoldl f (nth es 1) (get lst :tail)
|
||||
(er-mk-cons (nth es 0) mapped))))
|
||||
:else (raise (er-mk-error-marker (er-mk-atom "badarg"))))))
|
||||
|
||||
(define
|
||||
er-bif-lists-mapfoldl
|
||||
(fn (vs) (er-ext-mapfoldl (nth vs 0) (nth vs 1) (nth vs 2) (er-mk-nil))))
|
||||
|
||||
(define
|
||||
er-ext-search
|
||||
(fn (pred lst)
|
||||
(cond
|
||||
(er-nil? lst) (er-mk-atom "false")
|
||||
(er-cons? lst)
|
||||
(if (er-truthy? (er-apply-fun pred (list (get lst :head))))
|
||||
(er-mk-tuple (list (er-mk-atom "value") (get lst :head)))
|
||||
(er-ext-search pred (get lst :tail)))
|
||||
:else (er-mk-atom "false"))))
|
||||
|
||||
(define
|
||||
er-bif-lists-search
|
||||
(fn (vs) (er-ext-search (nth vs 0) (nth vs 1))))
|
||||
|
||||
;; ── proplists module ──────────────────────────────────────────────
|
||||
;; A property list element is either a bare atom A (shorthand for
|
||||
;; {A, true}) or a tuple whose first element is the key (value = its
|
||||
;; second element, or true for a 1-tuple). Lookups use the FIRST match.
|
||||
(define
|
||||
er-ext-pl-key-of
|
||||
(fn (e)
|
||||
(cond
|
||||
(er-atom? e) e
|
||||
(and (er-tuple? e) (>= (len (get e :elements)) 1)) (nth (get e :elements) 0)
|
||||
:else nil)))
|
||||
|
||||
(define
|
||||
er-ext-pl-val-of
|
||||
(fn (e)
|
||||
(cond
|
||||
(and (er-tuple? e) (>= (len (get e :elements)) 2)) (nth (get e :elements) 1)
|
||||
:else (er-mk-atom "true"))))
|
||||
|
||||
(define
|
||||
er-ext-pl-match?
|
||||
(fn (key e)
|
||||
(let ((k (er-ext-pl-key-of e)))
|
||||
(and (not (= k nil)) (er-equal? key k)))))
|
||||
|
||||
(define
|
||||
er-ext-pl-get-value
|
||||
(fn (key lst default)
|
||||
(cond
|
||||
(er-nil? lst) default
|
||||
(er-cons? lst)
|
||||
(if (er-ext-pl-match? key (get lst :head))
|
||||
(er-ext-pl-val-of (get lst :head))
|
||||
(er-ext-pl-get-value key (get lst :tail) default))
|
||||
:else default)))
|
||||
|
||||
(define
|
||||
er-bif-pl-get-value
|
||||
(fn (vs)
|
||||
(cond
|
||||
(= (len vs) 2)
|
||||
(er-ext-pl-get-value (nth vs 0) (nth vs 1) (er-mk-atom "undefined"))
|
||||
(= (len vs) 3)
|
||||
(er-ext-pl-get-value (nth vs 0) (nth vs 1) (nth vs 2))
|
||||
:else (error "Erlang: proplists:get_value: wrong arity"))))
|
||||
|
||||
(define
|
||||
er-ext-pl-all
|
||||
(fn (key lst acc)
|
||||
(cond
|
||||
(er-nil? lst) (er-list-reverse-iter acc (er-mk-nil))
|
||||
(er-cons? lst)
|
||||
(er-ext-pl-all key (get lst :tail)
|
||||
(if (er-ext-pl-match? key (get lst :head))
|
||||
(er-mk-cons (er-ext-pl-val-of (get lst :head)) acc)
|
||||
acc))
|
||||
:else (er-list-reverse-iter acc (er-mk-nil)))))
|
||||
|
||||
(define
|
||||
er-bif-pl-get-all-values
|
||||
(fn (vs) (er-ext-pl-all (nth vs 0) (nth vs 1) (er-mk-nil))))
|
||||
|
||||
(define
|
||||
er-ext-pl-defined?
|
||||
(fn (key lst)
|
||||
(cond
|
||||
(er-nil? lst) false
|
||||
(er-cons? lst)
|
||||
(if (er-ext-pl-match? key (get lst :head))
|
||||
true
|
||||
(er-ext-pl-defined? key (get lst :tail)))
|
||||
:else false)))
|
||||
|
||||
(define
|
||||
er-bif-pl-is-defined
|
||||
(fn (vs) (er-bool (er-ext-pl-defined? (nth vs 0) (nth vs 1)))))
|
||||
|
||||
(define
|
||||
er-ext-pl-lookup
|
||||
(fn (key lst)
|
||||
(cond
|
||||
(er-nil? lst) (er-mk-atom "none")
|
||||
(er-cons? lst)
|
||||
(if (er-ext-pl-match? key (get lst :head))
|
||||
(let ((e (get lst :head)))
|
||||
(if (er-tuple? e) e (er-mk-tuple (list e (er-mk-atom "true")))))
|
||||
(er-ext-pl-lookup key (get lst :tail)))
|
||||
:else (er-mk-atom "none"))))
|
||||
|
||||
(define
|
||||
er-bif-pl-lookup
|
||||
(fn (vs) (er-ext-pl-lookup (nth vs 0) (nth vs 1))))
|
||||
|
||||
(define
|
||||
er-ext-pl-delete
|
||||
(fn (key lst)
|
||||
(cond
|
||||
(er-nil? lst) (er-mk-nil)
|
||||
(er-cons? lst)
|
||||
(if (er-ext-pl-match? key (get lst :head))
|
||||
(er-ext-pl-delete key (get lst :tail))
|
||||
(er-mk-cons (get lst :head) (er-ext-pl-delete key (get lst :tail))))
|
||||
:else lst)))
|
||||
|
||||
(define
|
||||
er-bif-pl-delete
|
||||
(fn (vs) (er-ext-pl-delete (nth vs 0) (nth vs 1))))
|
||||
|
||||
133
lib/go/conformance.sh
Executable file
133
lib/go/conformance.sh
Executable file
@@ -0,0 +1,133 @@
|
||||
#!/usr/bin/env bash
|
||||
# Go-on-SX conformance runner.
|
||||
#
|
||||
# Loads every Go-on-SX test suite via the epoch protocol, collects
|
||||
# pass/fail counts, and writes lib/go/scoreboard.json + .md.
|
||||
#
|
||||
# Usage:
|
||||
# bash lib/go/conformance.sh # run all suites
|
||||
# bash lib/go/conformance.sh -v # verbose per-suite
|
||||
|
||||
set -uo pipefail
|
||||
cd "$(git rev-parse --show-toplevel)"
|
||||
|
||||
SX_SERVER="${SX_SERVER:-hosts/ocaml/_build/default/bin/sx_server.exe}"
|
||||
if [ ! -x "$SX_SERVER" ]; then
|
||||
SX_SERVER="/root/rose-ash/hosts/ocaml/_build/default/bin/sx_server.exe"
|
||||
fi
|
||||
if [ ! -x "$SX_SERVER" ]; then
|
||||
echo "ERROR: sx_server.exe not found." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
VERBOSE="${1:-}"
|
||||
TMPFILE=$(mktemp)
|
||||
OUTFILE=$(mktemp)
|
||||
trap "rm -f $TMPFILE $OUTFILE" EXIT
|
||||
|
||||
# Each suite: name | pass-counter | total-counter
|
||||
SUITES=(
|
||||
"lex|go-test-pass|go-test-count"
|
||||
)
|
||||
|
||||
cat > "$TMPFILE" <<'EPOCHS'
|
||||
(epoch 1)
|
||||
(load "lib/guest/lex.sx")
|
||||
(load "lib/go/lex.sx")
|
||||
(load "lib/go/tests/lex.sx")
|
||||
EPOCHS
|
||||
|
||||
idx=0
|
||||
for entry in "${SUITES[@]}"; do
|
||||
name="${entry%%|*}"
|
||||
pass_var=$(echo "$entry" | awk -F'|' '{print $2}')
|
||||
total_var=$(echo "$entry" | awk -F'|' '{print $3}')
|
||||
epoch=$((100 + idx))
|
||||
echo "(epoch $epoch)" >> "$TMPFILE"
|
||||
echo "(eval \"(list $pass_var $total_var)\")" >> "$TMPFILE"
|
||||
idx=$((idx + 1))
|
||||
done
|
||||
|
||||
"$SX_SERVER" < "$TMPFILE" > "$OUTFILE" 2>&1
|
||||
|
||||
parse_pair() {
|
||||
local epoch="$1"
|
||||
local line
|
||||
line=$(grep -A1 "^(ok-len $epoch " "$OUTFILE" | tail -1)
|
||||
echo "$line" | sed -E 's/[()]//g'
|
||||
}
|
||||
|
||||
TOTAL_PASS=0
|
||||
TOTAL_COUNT=0
|
||||
JSON_SUITES=""
|
||||
MD_ROWS=""
|
||||
|
||||
idx=0
|
||||
for entry in "${SUITES[@]}"; do
|
||||
name="${entry%%|*}"
|
||||
epoch=$((100 + idx))
|
||||
pair=$(parse_pair "$epoch")
|
||||
pass=$(echo "$pair" | awk '{print $1}')
|
||||
count=$(echo "$pair" | awk '{print $2}')
|
||||
if [ -z "$pass" ] || [ -z "$count" ]; then
|
||||
pass=0
|
||||
count=0
|
||||
fi
|
||||
TOTAL_PASS=$((TOTAL_PASS + pass))
|
||||
TOTAL_COUNT=$((TOTAL_COUNT + count))
|
||||
status="ok"
|
||||
marker="✅"
|
||||
if [ "$pass" != "$count" ]; then
|
||||
status="fail"
|
||||
marker="❌"
|
||||
fi
|
||||
if [ "$VERBOSE" = "-v" ]; then
|
||||
printf " %-12s %s/%s\n" "$name" "$pass" "$count"
|
||||
fi
|
||||
if [ -n "$JSON_SUITES" ]; then JSON_SUITES+=","; fi
|
||||
JSON_SUITES+=$'\n '
|
||||
JSON_SUITES+="{\"name\":\"$name\",\"pass\":$pass,\"total\":$count,\"status\":\"$status\"}"
|
||||
MD_ROWS+="| $marker | $name | $pass | $count |"$'\n'
|
||||
idx=$((idx + 1))
|
||||
done
|
||||
|
||||
printf '\nGo-on-SX conformance: %d / %d\n' "$TOTAL_PASS" "$TOTAL_COUNT"
|
||||
|
||||
cat > lib/go/scoreboard.json <<JSON
|
||||
{
|
||||
"language": "go",
|
||||
"total_pass": $TOTAL_PASS,
|
||||
"total": $TOTAL_COUNT,
|
||||
"suites": [$JSON_SUITES,
|
||||
{"name":"parse","pass":0,"total":0,"status":"pending"},
|
||||
{"name":"types","pass":0,"total":0,"status":"pending"},
|
||||
{"name":"eval","pass":0,"total":0,"status":"pending"},
|
||||
{"name":"runtime","pass":0,"total":0,"status":"pending"},
|
||||
{"name":"stdlib","pass":0,"total":0,"status":"pending"},
|
||||
{"name":"e2e","pass":0,"total":0,"status":"pending"}
|
||||
]
|
||||
}
|
||||
JSON
|
||||
|
||||
cat > lib/go/scoreboard.md <<MD
|
||||
# Go-on-SX Scoreboard
|
||||
|
||||
**Total: ${TOTAL_PASS} / ${TOTAL_COUNT} tests passing**
|
||||
|
||||
| | Suite | Pass | Total |
|
||||
|---|---|---|---|
|
||||
$MD_ROWS| ⬜ | parse | 0 | 0 |
|
||||
| ⬜ | types | 0 | 0 |
|
||||
| ⬜ | eval | 0 | 0 |
|
||||
| ⬜ | runtime | 0 | 0 |
|
||||
| ⬜ | stdlib | 0 | 0 |
|
||||
| ⬜ | e2e | 0 | 0 |
|
||||
|
||||
Generated by \`lib/go/conformance.sh\`.
|
||||
MD
|
||||
|
||||
if [ "$TOTAL_PASS" -eq "$TOTAL_COUNT" ]; then
|
||||
exit 0
|
||||
else
|
||||
exit 1
|
||||
fi
|
||||
371
lib/go/lex.sx
Normal file
371
lib/go/lex.sx
Normal file
@@ -0,0 +1,371 @@
|
||||
;; lib/go/lex.sx — Go tokenizer with automatic semicolon insertion.
|
||||
;;
|
||||
;; Consumes lib/guest/lex.sx character-class predicates.
|
||||
;;
|
||||
;; Tokens: {:type T :value V :pos P}
|
||||
;; Types:
|
||||
;; "ident" — identifiers (foo, _bar, mixedCase)
|
||||
;; "keyword" — one of the 25 Go keywords
|
||||
;; "int" — integer literals (decimal only this iteration)
|
||||
;; "string" — interpreted string literals "..."
|
||||
;; "rune" — rune literals 'x' (single char + simple escapes)
|
||||
;; "op" — operators & punctuation; :value is the literal text
|
||||
;; "semi" — explicit ';' or auto-inserted (Go spec § Semicolons)
|
||||
;; "eof" — end-of-input sentinel
|
||||
;;
|
||||
;; ASI (Go spec § Semicolons): a newline (or EOF, or a block comment
|
||||
;; containing a newline) emits a ";semi" if the previous emitted token's
|
||||
;; type is ident/int/string/rune, or its value is one of
|
||||
;; {break, continue, fallthrough, return, ++, --, ), ], }}.
|
||||
;;
|
||||
;; All scanner locals are gl- prefixed: SX host primitives (peek/emit/etc.)
|
||||
;; silently shadow guest-language defines. See feedback_sx_bind_clash.
|
||||
|
||||
(define
|
||||
go-keywords
|
||||
(list
|
||||
"break"
|
||||
"case"
|
||||
"chan"
|
||||
"const"
|
||||
"continue"
|
||||
"default"
|
||||
"defer"
|
||||
"else"
|
||||
"fallthrough"
|
||||
"for"
|
||||
"func"
|
||||
"go"
|
||||
"goto"
|
||||
"if"
|
||||
"import"
|
||||
"interface"
|
||||
"map"
|
||||
"package"
|
||||
"range"
|
||||
"return"
|
||||
"select"
|
||||
"struct"
|
||||
"switch"
|
||||
"type"
|
||||
"var"))
|
||||
|
||||
(define go-keyword? (fn (s) (some (fn (k) (= k s)) go-keywords)))
|
||||
|
||||
(define go-asi-keywords (list "break" "continue" "fallthrough" "return"))
|
||||
|
||||
(define go-asi-ops (list "++" "--" ")" "]" "}"))
|
||||
|
||||
(define
|
||||
go-asi-trigger?
|
||||
(fn
|
||||
(tok)
|
||||
(if
|
||||
(= tok nil)
|
||||
false
|
||||
(let
|
||||
((ty (get tok :type)) (v (get tok :value)))
|
||||
(or
|
||||
(= ty "ident")
|
||||
(= ty "int")
|
||||
(= ty "string")
|
||||
(= ty "rune")
|
||||
(and (= ty "keyword") (some (fn (k) (= k v)) go-asi-keywords))
|
||||
(and (= ty "op") (some (fn (o) (= o v)) go-asi-ops)))))))
|
||||
|
||||
(define
|
||||
go-tokenize
|
||||
(fn
|
||||
(src)
|
||||
(let
|
||||
((tokens (list)) (pos 0) (src-len (len src)))
|
||||
(define
|
||||
gl-peek
|
||||
(fn
|
||||
(offset)
|
||||
(if (< (+ pos offset) src-len) (nth src (+ pos offset)) nil)))
|
||||
(define gl-cur (fn () (gl-peek 0)))
|
||||
(define gl-advance! (fn (n) (set! pos (+ pos n))))
|
||||
(define
|
||||
gl-last
|
||||
(fn
|
||||
()
|
||||
(if
|
||||
(= (len tokens) 0)
|
||||
nil
|
||||
(nth tokens (- (len tokens) 1)))))
|
||||
(define gl-emit! (fn (type value start) (append! tokens {:type type :value value :pos start})))
|
||||
(define
|
||||
gl-maybe-asi!
|
||||
(fn
|
||||
(at)
|
||||
(when (go-asi-trigger? (gl-last)) (gl-emit! "semi" "\n" at))))
|
||||
(define
|
||||
gl-skip-line!
|
||||
(fn
|
||||
()
|
||||
(when
|
||||
(and (< pos src-len) (not (= (gl-cur) "\n")))
|
||||
(gl-advance! 1)
|
||||
(gl-skip-line!))))
|
||||
(define
|
||||
gl-skip-block!
|
||||
(fn
|
||||
(saw-nl)
|
||||
(cond
|
||||
(>= pos src-len)
|
||||
saw-nl
|
||||
(and (= (gl-cur) "*") (= (gl-peek 1) "/"))
|
||||
(do (gl-advance! 2) saw-nl)
|
||||
:else (let
|
||||
((is-nl (= (gl-cur) "\n")))
|
||||
(gl-advance! 1)
|
||||
(gl-skip-block! (or saw-nl is-nl))))))
|
||||
(define
|
||||
gl-read-ident!
|
||||
(fn
|
||||
(start)
|
||||
(when
|
||||
(and (< pos src-len) (lex-ident-char? (gl-cur)))
|
||||
(gl-advance! 1)
|
||||
(gl-read-ident! start))
|
||||
(slice src start pos)))
|
||||
(define
|
||||
gl-read-digits!
|
||||
(fn
|
||||
()
|
||||
(when
|
||||
(and (< pos src-len) (lex-digit? (gl-cur)))
|
||||
(gl-advance! 1)
|
||||
(gl-read-digits!))))
|
||||
(define
|
||||
gl-read-string!
|
||||
(fn
|
||||
()
|
||||
(gl-advance! 1)
|
||||
(let
|
||||
((chars (list)))
|
||||
(define
|
||||
gl-string-loop
|
||||
(fn
|
||||
()
|
||||
(cond
|
||||
(>= pos src-len)
|
||||
nil
|
||||
(= (gl-cur) "\"")
|
||||
(gl-advance! 1)
|
||||
(= (gl-cur) "\\")
|
||||
(do
|
||||
(gl-advance! 1)
|
||||
(when
|
||||
(< pos src-len)
|
||||
(let
|
||||
((ch (gl-cur)))
|
||||
(cond
|
||||
(= ch "n")
|
||||
(append! chars "\n")
|
||||
(= ch "t")
|
||||
(append! chars "\t")
|
||||
(= ch "r")
|
||||
(append! chars "\r")
|
||||
(= ch "\\")
|
||||
(append! chars "\\")
|
||||
(= ch "\"")
|
||||
(append! chars "\"")
|
||||
(= ch "'")
|
||||
(append! chars "'")
|
||||
:else (append! chars ch))
|
||||
(gl-advance! 1)))
|
||||
(gl-string-loop))
|
||||
:else (do
|
||||
(append! chars (gl-cur))
|
||||
(gl-advance! 1)
|
||||
(gl-string-loop)))))
|
||||
(gl-string-loop)
|
||||
(join "" chars))))
|
||||
(define
|
||||
gl-read-rune!
|
||||
(fn
|
||||
()
|
||||
(gl-advance! 1)
|
||||
(let
|
||||
((chars (list)))
|
||||
(cond
|
||||
(and (< pos src-len) (= (gl-cur) "\\"))
|
||||
(do
|
||||
(gl-advance! 1)
|
||||
(when
|
||||
(< pos src-len)
|
||||
(let
|
||||
((ch (gl-cur)))
|
||||
(cond
|
||||
(= ch "n")
|
||||
(append! chars "\n")
|
||||
(= ch "t")
|
||||
(append! chars "\t")
|
||||
(= ch "r")
|
||||
(append! chars "\r")
|
||||
(= ch "\\")
|
||||
(append! chars "\\")
|
||||
(= ch "'")
|
||||
(append! chars "'")
|
||||
(= ch "\"")
|
||||
(append! chars "\"")
|
||||
:else (append! chars ch))
|
||||
(gl-advance! 1))))
|
||||
(< pos src-len)
|
||||
(do (append! chars (gl-cur)) (gl-advance! 1)))
|
||||
(when
|
||||
(and (< pos src-len) (= (gl-cur) "'"))
|
||||
(gl-advance! 1))
|
||||
(join "" chars))))
|
||||
(define
|
||||
gl-match-op
|
||||
(fn
|
||||
()
|
||||
(let
|
||||
((c0 (gl-cur))
|
||||
(c1 (gl-peek 1))
|
||||
(c2 (gl-peek 2)))
|
||||
(cond
|
||||
(and (= c0 "<") (= c1 "<") (= c2 "="))
|
||||
"<<="
|
||||
(and (= c0 ">") (= c1 ">") (= c2 "="))
|
||||
">>="
|
||||
(and (= c0 "&") (= c1 "^") (= c2 "="))
|
||||
"&^="
|
||||
(and (= c0 ".") (= c1 ".") (= c2 "."))
|
||||
"..."
|
||||
(and (= c0 "=") (= c1 "="))
|
||||
"=="
|
||||
(and (= c0 "!") (= c1 "="))
|
||||
"!="
|
||||
(and (= c0 "<") (= c1 "="))
|
||||
"<="
|
||||
(and (= c0 ">") (= c1 "="))
|
||||
">="
|
||||
(and (= c0 "&") (= c1 "&"))
|
||||
"&&"
|
||||
(and (= c0 "|") (= c1 "|"))
|
||||
"||"
|
||||
(and (= c0 "+") (= c1 "+"))
|
||||
"++"
|
||||
(and (= c0 "-") (= c1 "-"))
|
||||
"--"
|
||||
(and (= c0 "<") (= c1 "<"))
|
||||
"<<"
|
||||
(and (= c0 ">") (= c1 ">"))
|
||||
">>"
|
||||
(and (= c0 "+") (= c1 "="))
|
||||
"+="
|
||||
(and (= c0 "-") (= c1 "="))
|
||||
"-="
|
||||
(and (= c0 "*") (= c1 "="))
|
||||
"*="
|
||||
(and (= c0 "/") (= c1 "="))
|
||||
"/="
|
||||
(and (= c0 "%") (= c1 "="))
|
||||
"%="
|
||||
(and (= c0 "&") (= c1 "="))
|
||||
"&="
|
||||
(and (= c0 "|") (= c1 "="))
|
||||
"|="
|
||||
(and (= c0 "^") (= c1 "="))
|
||||
"^="
|
||||
(and (= c0 ":") (= c1 "="))
|
||||
":="
|
||||
(and (= c0 "<") (= c1 "-"))
|
||||
"<-"
|
||||
(and (= c0 "&") (= c1 "^"))
|
||||
"&^"
|
||||
(or
|
||||
(= c0 "+")
|
||||
(= c0 "-")
|
||||
(= c0 "*")
|
||||
(= c0 "/")
|
||||
(= c0 "%")
|
||||
(= c0 "&")
|
||||
(= c0 "|")
|
||||
(= c0 "^")
|
||||
(= c0 "<")
|
||||
(= c0 ">")
|
||||
(= c0 "=")
|
||||
(= c0 "!")
|
||||
(= c0 "(")
|
||||
(= c0 ")")
|
||||
(= c0 "{")
|
||||
(= c0 "}")
|
||||
(= c0 "[")
|
||||
(= c0 "]")
|
||||
(= c0 ",")
|
||||
(= c0 ".")
|
||||
(= c0 ":"))
|
||||
c0
|
||||
:else nil))))
|
||||
(define
|
||||
gl-scan!
|
||||
(fn
|
||||
()
|
||||
(cond
|
||||
(>= pos src-len)
|
||||
nil
|
||||
(= (gl-cur) "\n")
|
||||
(do (gl-maybe-asi! pos) (gl-advance! 1) (gl-scan!))
|
||||
(lex-space? (gl-cur))
|
||||
(do (gl-advance! 1) (gl-scan!))
|
||||
(and (= (gl-cur) "/") (= (gl-peek 1) "/"))
|
||||
(do (gl-advance! 2) (gl-skip-line!) (gl-scan!))
|
||||
(and (= (gl-cur) "/") (= (gl-peek 1) "*"))
|
||||
(do
|
||||
(gl-advance! 2)
|
||||
(let
|
||||
((saw-nl (gl-skip-block! false)))
|
||||
(when saw-nl (gl-maybe-asi! pos)))
|
||||
(gl-scan!))
|
||||
(= (gl-cur) ";")
|
||||
(do
|
||||
(gl-emit! "semi" ";" pos)
|
||||
(gl-advance! 1)
|
||||
(gl-scan!))
|
||||
(lex-ident-start? (gl-cur))
|
||||
(do
|
||||
(let
|
||||
((start pos))
|
||||
(gl-read-ident! start)
|
||||
(let
|
||||
((word (slice src start pos)))
|
||||
(gl-emit!
|
||||
(if (go-keyword? word) "keyword" "ident")
|
||||
word
|
||||
start)))
|
||||
(gl-scan!))
|
||||
(lex-digit? (gl-cur))
|
||||
(do
|
||||
(let
|
||||
((start pos))
|
||||
(gl-read-digits!)
|
||||
(gl-emit! "int" (slice src start pos) start))
|
||||
(gl-scan!))
|
||||
(= (gl-cur) "\"")
|
||||
(let
|
||||
((start pos) (v (gl-read-string!)))
|
||||
(gl-emit! "string" v start)
|
||||
(gl-scan!))
|
||||
(= (gl-cur) "'")
|
||||
(let
|
||||
((start pos) (v (gl-read-rune!)))
|
||||
(gl-emit! "rune" v start)
|
||||
(gl-scan!))
|
||||
:else (let
|
||||
((op (gl-match-op)))
|
||||
(cond
|
||||
op
|
||||
(do
|
||||
(gl-emit! "op" op pos)
|
||||
(gl-advance! (len op))
|
||||
(gl-scan!))
|
||||
:else (do (gl-advance! 1) (gl-scan!)))))))
|
||||
(gl-scan!)
|
||||
(gl-maybe-asi! pos)
|
||||
(gl-emit! "eof" nil pos)
|
||||
tokens)))
|
||||
14
lib/go/scoreboard.json
Normal file
14
lib/go/scoreboard.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"language": "go",
|
||||
"total_pass": 78,
|
||||
"total": 78,
|
||||
"suites": [
|
||||
{"name":"lex","pass":78,"total":78,"status":"ok"},
|
||||
{"name":"parse","pass":0,"total":0,"status":"pending"},
|
||||
{"name":"types","pass":0,"total":0,"status":"pending"},
|
||||
{"name":"eval","pass":0,"total":0,"status":"pending"},
|
||||
{"name":"runtime","pass":0,"total":0,"status":"pending"},
|
||||
{"name":"stdlib","pass":0,"total":0,"status":"pending"},
|
||||
{"name":"e2e","pass":0,"total":0,"status":"pending"}
|
||||
]
|
||||
}
|
||||
15
lib/go/scoreboard.md
Normal file
15
lib/go/scoreboard.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# Go-on-SX Scoreboard
|
||||
|
||||
**Total: 78 / 78 tests passing**
|
||||
|
||||
| | Suite | Pass | Total |
|
||||
|---|---|---|---|
|
||||
| ✅ | lex | 78 | 78 |
|
||||
| ⬜ | parse | 0 | 0 |
|
||||
| ⬜ | types | 0 | 0 |
|
||||
| ⬜ | eval | 0 | 0 |
|
||||
| ⬜ | runtime | 0 | 0 |
|
||||
| ⬜ | stdlib | 0 | 0 |
|
||||
| ⬜ | e2e | 0 | 0 |
|
||||
|
||||
Generated by `lib/go/conformance.sh`.
|
||||
204
lib/go/tests/lex.sx
Normal file
204
lib/go/tests/lex.sx
Normal file
@@ -0,0 +1,204 @@
|
||||
;; Go tokenizer tests.
|
||||
|
||||
(define go-test-count 0)
|
||||
(define go-test-pass 0)
|
||||
(define go-test-fails (list))
|
||||
|
||||
(define gtok-type (fn (t) (get t :type)))
|
||||
(define gtok-value (fn (t) (get t :value)))
|
||||
(define tok-types (fn (src) (map gtok-type (go-tokenize src))))
|
||||
(define tok-values (fn (src) (map gtok-value (go-tokenize src))))
|
||||
|
||||
(define
|
||||
go-test
|
||||
(fn
|
||||
(name actual expected)
|
||||
(set! go-test-count (+ go-test-count 1))
|
||||
(if
|
||||
(= actual expected)
|
||||
(set! go-test-pass (+ go-test-pass 1))
|
||||
(append! go-test-fails {:name name :expected expected :actual actual}))))
|
||||
|
||||
;; ── empty / whitespace ────────────────────────────────────────────
|
||||
(go-test "empty source" (tok-types "") (list "eof"))
|
||||
(go-test "spaces only" (tok-types " ") (list "eof"))
|
||||
(go-test "tabs only" (tok-types "\t\t") (list "eof"))
|
||||
(go-test
|
||||
"newline only — no prior token, no ASI"
|
||||
(tok-types "\n")
|
||||
(list "eof"))
|
||||
|
||||
;; ── identifiers ───────────────────────────────────────────────────
|
||||
(go-test "ident: simple" (tok-values "foo") (list "foo" "\n" nil))
|
||||
(go-test
|
||||
"ident: underscore prefix"
|
||||
(tok-values "_bar")
|
||||
(list "_bar" "\n" nil))
|
||||
(go-test "ident: mixed case" (tok-values "fooBar") (list "fooBar" "\n" nil))
|
||||
(go-test "ident: with digits" (tok-values "x123") (list "x123" "\n" nil))
|
||||
(go-test "ident: type tag" (tok-types "foo") (list "ident" "semi" "eof"))
|
||||
|
||||
;; ── keywords (all 25) ─────────────────────────────────────────────
|
||||
(go-test "kw: break" (tok-types "break") (list "keyword" "semi" "eof"))
|
||||
(go-test "kw: case" (tok-types "case") (list "keyword" "eof"))
|
||||
(go-test "kw: chan" (tok-types "chan") (list "keyword" "eof"))
|
||||
(go-test "kw: const" (tok-types "const") (list "keyword" "eof"))
|
||||
(go-test "kw: continue" (tok-types "continue") (list "keyword" "semi" "eof"))
|
||||
(go-test "kw: default" (tok-types "default") (list "keyword" "eof"))
|
||||
(go-test "kw: defer" (tok-types "defer") (list "keyword" "eof"))
|
||||
(go-test "kw: else" (tok-types "else") (list "keyword" "eof"))
|
||||
(go-test
|
||||
"kw: fallthrough"
|
||||
(tok-types "fallthrough")
|
||||
(list "keyword" "semi" "eof"))
|
||||
(go-test "kw: for" (tok-types "for") (list "keyword" "eof"))
|
||||
(go-test "kw: func" (tok-types "func") (list "keyword" "eof"))
|
||||
(go-test "kw: go" (tok-types "go") (list "keyword" "eof"))
|
||||
(go-test "kw: goto" (tok-types "goto") (list "keyword" "eof"))
|
||||
(go-test "kw: if" (tok-types "if") (list "keyword" "eof"))
|
||||
(go-test "kw: import" (tok-types "import") (list "keyword" "eof"))
|
||||
(go-test "kw: interface" (tok-types "interface") (list "keyword" "eof"))
|
||||
(go-test "kw: map" (tok-types "map") (list "keyword" "eof"))
|
||||
(go-test "kw: package" (tok-types "package") (list "keyword" "eof"))
|
||||
(go-test "kw: range" (tok-types "range") (list "keyword" "eof"))
|
||||
(go-test "kw: return" (tok-types "return") (list "keyword" "semi" "eof"))
|
||||
(go-test "kw: select" (tok-types "select") (list "keyword" "eof"))
|
||||
(go-test "kw: struct" (tok-types "struct") (list "keyword" "eof"))
|
||||
(go-test "kw: switch" (tok-types "switch") (list "keyword" "eof"))
|
||||
(go-test "kw: type" (tok-types "type") (list "keyword" "eof"))
|
||||
(go-test "kw: var" (tok-types "var") (list "keyword" "eof"))
|
||||
|
||||
;; ── integer literals ──────────────────────────────────────────────
|
||||
(go-test "int: zero" (tok-values "0") (list "0" "\n" nil))
|
||||
(go-test "int: small" (tok-values "42") (list "42" "\n" nil))
|
||||
(go-test "int: bigger" (tok-values "123456") (list "123456" "\n" nil))
|
||||
(go-test "int: type" (tok-types "42") (list "int" "semi" "eof"))
|
||||
|
||||
;; ── string literals ───────────────────────────────────────────────
|
||||
(go-test "string: empty" (tok-values "\"\"") (list "" "\n" nil))
|
||||
(go-test "string: hello" (tok-values "\"hello\"") (list "hello" "\n" nil))
|
||||
(go-test
|
||||
"string: with space"
|
||||
(tok-values "\"hi there\"")
|
||||
(list "hi there" "\n" nil))
|
||||
(go-test "string: escape n" (tok-values "\"a\\nb\"") (list "a\nb" "\n" nil))
|
||||
(go-test "string: escape quote" (tok-values "\"a\\\"b\"") (list "a\"b" "\n" nil))
|
||||
(go-test
|
||||
"string: escape backslash"
|
||||
(tok-values "\"a\\\\b\"")
|
||||
(list "a\\b" "\n" nil))
|
||||
(go-test "string: type" (tok-types "\"x\"") (list "string" "semi" "eof"))
|
||||
|
||||
;; ── rune literals ─────────────────────────────────────────────────
|
||||
(go-test "rune: simple" (tok-values "'a'") (list "a" "\n" nil))
|
||||
(go-test "rune: escape" (tok-values "'\\n'") (list "\n" "\n" nil))
|
||||
(go-test "rune: type" (tok-types "'a'") (list "rune" "semi" "eof"))
|
||||
|
||||
;; ── comments ──────────────────────────────────────────────────────
|
||||
(go-test "line comment" (tok-types "// ignored") (list "eof"))
|
||||
(go-test "line comment then code" (tok-values "// hi\nx") (list "x" "\n" nil))
|
||||
(go-test "block comment" (tok-types "/* a b c */") (list "eof"))
|
||||
(go-test
|
||||
"block comment inline"
|
||||
(tok-values "x /* mid */ y")
|
||||
(list "x" "y" "\n" nil))
|
||||
(go-test
|
||||
"block comment with newline — ASI"
|
||||
(tok-types "x /* multi\nline */ y")
|
||||
(list "ident" "semi" "ident" "semi" "eof"))
|
||||
|
||||
;; ── operators & punctuation ───────────────────────────────────────
|
||||
(go-test
|
||||
"ops: arithmetic"
|
||||
(tok-values "+ - * / %")
|
||||
(list "+" "-" "*" "/" "%" nil))
|
||||
(go-test
|
||||
"ops: comparison"
|
||||
(tok-values "== != < > <= >=")
|
||||
(list "==" "!=" "<" ">" "<=" ">=" nil))
|
||||
(go-test "ops: logical" (tok-values "&& || !") (list "&&" "||" "!" nil))
|
||||
(go-test
|
||||
"ops: assign forms"
|
||||
(tok-values "= := += -=")
|
||||
(list "=" ":=" "+=" "-=" nil))
|
||||
(go-test "ops: channel arrow" (tok-values "<- chan") (list "<-" "chan" nil))
|
||||
(go-test "ops: incdec ASI" (tok-types "++ --") (list "op" "op" "semi" "eof"))
|
||||
(go-test "ops: ellipsis" (tok-values "...") (list "..." nil))
|
||||
(go-test
|
||||
"punct: all brackets"
|
||||
(tok-values "( ) { } [ ]")
|
||||
(list "(" ")" "{" "}" "[" "]" "\n" nil))
|
||||
(go-test
|
||||
"punct: comma colon dot"
|
||||
(tok-values ", : .")
|
||||
(list "," ":" "." nil))
|
||||
|
||||
;; ── automatic semicolon insertion (Go spec § Semicolons) ──────────
|
||||
(go-test
|
||||
"ASI: after ident at newline"
|
||||
(tok-types "x\ny")
|
||||
(list "ident" "semi" "ident" "semi" "eof"))
|
||||
(go-test "ASI: after int" (tok-types "42\n") (list "int" "semi" "eof"))
|
||||
(go-test
|
||||
"ASI: after string"
|
||||
(tok-types "\"hi\"\n")
|
||||
(list "string" "semi" "eof"))
|
||||
(go-test "ASI: after rune" (tok-types "'a'\n") (list "rune" "semi" "eof"))
|
||||
(go-test
|
||||
"ASI: after )"
|
||||
(tok-types "f()\n")
|
||||
(list "ident" "op" "op" "semi" "eof"))
|
||||
(go-test
|
||||
"ASI: after ]"
|
||||
(tok-types "x[0]\n")
|
||||
(list "ident" "op" "int" "op" "semi" "eof"))
|
||||
(go-test "ASI: after }" (tok-types "{}\n") (list "op" "op" "semi" "eof"))
|
||||
(go-test "ASI: after ++" (tok-types "i++\n") (list "ident" "op" "semi" "eof"))
|
||||
(go-test
|
||||
"ASI: NOT after +"
|
||||
(tok-types "x +\ny")
|
||||
(list "ident" "op" "ident" "semi" "eof"))
|
||||
(go-test
|
||||
"ASI: NOT after ("
|
||||
(tok-types "f(\nx)")
|
||||
(list "ident" "op" "ident" "op" "semi" "eof"))
|
||||
(go-test
|
||||
"ASI: blank lines collapse — single semi only"
|
||||
(tok-types "x\n\n\ny")
|
||||
(list "ident" "semi" "ident" "semi" "eof"))
|
||||
(go-test
|
||||
"ASI: at EOF after ident"
|
||||
(tok-types "x")
|
||||
(list "ident" "semi" "eof"))
|
||||
(go-test
|
||||
"ASI: explicit semi"
|
||||
(tok-types "x;y")
|
||||
(list "ident" "semi" "ident" "semi" "eof"))
|
||||
|
||||
;; ── short program ─────────────────────────────────────────────────
|
||||
(go-test
|
||||
"short-decl: x := 42 (types)"
|
||||
(tok-types "x := 42")
|
||||
(list "ident" "op" "int" "semi" "eof"))
|
||||
(go-test
|
||||
"short-decl: x := 42 (values)"
|
||||
(tok-values "x := 42")
|
||||
(list "x" ":=" "42" "\n" nil))
|
||||
(go-test
|
||||
"func decl shape"
|
||||
(tok-types "func foo() int { return 0 }")
|
||||
(list
|
||||
"keyword"
|
||||
"ident"
|
||||
"op"
|
||||
"op"
|
||||
"ident"
|
||||
"op"
|
||||
"keyword"
|
||||
"int"
|
||||
"op"
|
||||
"semi"
|
||||
"eof"))
|
||||
|
||||
;; ── report ────────────────────────────────────────────────────────
|
||||
(define go-lex-test-summary (str "lex " go-test-pass "/" go-test-count))
|
||||
@@ -159,24 +159,6 @@ The Phase 9 opcodes are registered, tested, and bridged SX↔OCaml, but inert: n
|
||||
|
||||
_Newest first._
|
||||
|
||||
- **2026-06-30 retire separate `lists-ext.sx` — fold stdlib BIFs into canonical files** — The 8 stdlib commits had lived in a standalone `lib/erlang/lists-ext.sx` (a workaround for this worktree's broken sx-tree write tools). Folded the function bodies into `transpile.sx` (appended, alongside the existing `er-bif-lists-*`) and moved the registrations **directly inside `er-register-builtin-bifs!`** in `runtime.sx` — so they now reach every erlang consumer (fed-sx, identity, …) that loads the runtime, not just the conformance harness. The `er-register-builtin-bifs!` **wrapper trick is gone** (no longer needed once registrations live inside the function the registry-reset re-runs). Deleted `lists-ext.sx` and its `(load …)` from `conformance.sh`; kept the `lists_ext` test suite + wiring. Byte-exact splice (no transcription), `sx_validate` clean, conformance **874/874** unchanged. Mirrors the same fold-in landed on the `architecture` branch (commit `39dbb00c`, equivalence verified: identical test suite + function bodies). loops/erlang only.
|
||||
|
||||
- **2026-06-30 stdlib hardening — `proplists` module** — Added `proplists:get_value/2,3`, `get_all_values/2`, `is_defined/2`, `lookup/2`, `delete/2` to `lib/erlang/lists-ext.sx` (header widened to "lists + proplists"). Property-list semantics: a bare atom `A` is shorthand for `{A, true}`, a tuple's first element is the key, lookups use the first match. `lookup` returns the tuple (or `{Key,true}`) or `none`; `get_value` defaults to `undefined`. The `lists_ext` suite (counter trio `er-lx-*`, now spanning both modules) 91→**103** (+12). Conformance **862 → 874/874**. loops/erlang only.
|
||||
|
||||
- **2026-06-30 stdlib hardening — `lists` flatmap/filtermap/mapfoldl/search** — Added `flatmap/2`, `filtermap/2` (`true` keep / `false` drop / `{true, V}` transform), `mapfoldl/3` (returns `{MappedList, AccFinal}`), `search/2` (`{value, E}` | `false`) to `lib/erlang/lists-ext.sx`. `lists_ext` suite 83→**91** (+8). Conformance **854 → 862/862**. The `lists` module is now broadly covered (sort/usort/keylists/fold/partition/while/flatten/min/max/zip/slicing/flatmap/filtermap/mapfoldl/search on top of the originals). loops/erlang only.
|
||||
|
||||
- **2026-06-30 stdlib hardening — `lists` slicing** — Added `sublist/2`, `sublist/3`, `nthtail/2`, `split/2`, `droplast/1` to `lib/erlang/lists-ext.sx`. `sublist` is lenient (clamps to list length); `nthtail/2` and `split/2` are strict (`badarg` when the list is shorter than N, matching the stdlib); `droplast/1` raises on `[]`. `lists_ext` suite 70→**83** (+13). Conformance **841 → 854/854**. loops/erlang only.
|
||||
|
||||
- **2026-06-30 stdlib hardening — `lists` zip family** — Added `zip/2`, `zipwith/3`, `unzip/1` to `lib/erlang/lists-ext.sx`. Length mismatch (zip/zipwith) and malformed/non-pair elements (unzip) raise `badarg` (port equivalent of Erlang's `function_clause`). `lists_ext` suite 62→**70** (+8, incl. a zip/unzip roundtrip). Conformance **833 → 841/841**. loops/erlang only.
|
||||
|
||||
- **2026-06-30 stdlib hardening — `lists` flatten/max/min** — Added `flatten/1` (deep recursive flatten via `er-list-append`), `max/1`, `min/1` (full term order via `er-ext-lt?`, `badarg` on empty) to `lib/erlang/lists-ext.sx`. Gotcha caught: `er-ext-lt?` returns a raw SX boolean, so the extreme-finder uses it directly in `if` rather than wrapping in `er-truthy?` (which only recognises Erlang bool atoms, not SX booleans — the first cut wrapped it and silently never updated the running best). `lists_ext` suite 52→**62** (+10). Conformance **823 → 833/833**. loops/erlang only.
|
||||
|
||||
- **2026-06-30 stdlib hardening — `lists` higher-order traversal** — Added `foldr/3`, `partition/2`, `takewhile/2`, `dropwhile/2`, `splitwith/2` to `lib/erlang/lists-ext.sx`, registered pure through the `er-register-builtin-bifs!` wrapper (consistent with the existing pure `map`/`filter`/`foldl`). `foldr` right-folds (order-preserving when consing); `partition` returns `{Satisfying, NotSatisfying}` order-preserved via `er-list-reverse-iter`; `splitwith` = `{takewhile, dropwhile}`. `lists_ext` suite 38→**52** (+14). Conformance **809 → 823/823**. loops/erlang only.
|
||||
|
||||
- **2026-06-30 stdlib hardening — `lists` keylists** — Added the keylist family to `lib/erlang/lists-ext.sx`: `keyfind/3`, `keymember/3`, `keydelete/3`, `keyreplace/4`, `keystore/4`, `keytake/3`, `keysort/2`. All operate on lists of tuples keyed on element N (1-indexed), act on the first match only, and pass through non-tuples / tuples shorter than N. Key comparison is `==` (`er-equal?`) per the stdlib; `keysort/2` reuses the stable `er-ext-msort` + `er-ext-lt?` from the sort commit, comparing extracted keys. `keytake/3` returns `{value, Tuple, Rest}` / `false`. Registered through the same `er-register-builtin-bifs!` wrapper so they survive registry resets. `lists_ext` suite 17→**38** (+21: hit/miss/first-match-only/short-tuple-skip across all seven, keysort by elem 1 and 2 + stability). Conformance **788 → 809/809**. Test-harness note: `element(2, T)` returns an integer (no `:name`), so those two cases compare the raw number via `erlang-eval-ast` rather than `er-lx-nm`. loops/erlang only.
|
||||
|
||||
- **2026-06-30 stdlib hardening — `lists:sort/1,2` + `lists:usort/1`** — Roadmap is saturated within this loop's scope (every remaining `[ ]` is blocked: `httpc`/`sqlite` on absent host primitives, 10a/10c on out-of-scope `lib/compiler.sx`). Continued as forever-loop hardening by filling idiomatic-Erlang stdlib gaps. Added the `lists` sort family in a **new file `lib/erlang/lists-ext.sx`** (loaded after `runtime.sx`): stable merge sort over an SX-list bridge, registered via `er-register-pure-bif!`. `lists:sort/1` and `usort/1` use full Erlang term order; `sort/2` takes a `fun(A,B)->bool` comparator. **Two notable findings:** (1) the shared `er-lt?` (transpile.sx) only deep-compares numbers/atoms/strings and treats *any two tuples (or lists) as order-equal* — so `lists:sort` (and, latently, `min/2`/`max/2`) would not order compound terms. Fixed locally with a self-contained `er-ext-lt?` that compares tuples by arity-then-elementwise and lists elementwise (shorter proper prefix first), delegating cross-type cases to `er-lt?`. `er-lt?` itself left untouched (shared by the `<` operator; can't edit transpile.sx — see Blockers). (2) `tests/runtime.sx` resets the BIF registry mid-run via `er-register-builtin-bifs!`, which would wipe a one-shot registration; so `lists-ext.sx` **wraps** `er-register-builtin-bifs!` to re-add its BIFs on every rebuild. New `lists_ext` suite (17 tests: term order, dup-keeping, stability, descending comparator, usort dedup). Conformance **771 → 788/788** (12→13 suites). New-file workaround forced because every sx-tree write tool (incl. `sx_write_file`) raises yojson "Expected string, got null" in this worktree — authored via the `Write` fallback + `sx_validate`, the same pattern other loops use. loops/erlang only.
|
||||
|
||||
- **2026-05-18 Phase 8 host-primitive BIFs wired (crypto / cid / file:list_dir)** — `loops/fed-prims` (merged at architecture `380bc69f`) delivered the platform primitives; wired the 3 previously-BLOCKED Phase 8 BIF groups in `lib/erlang/runtime.sx` as `er-register-pure-bif!`/`er-register-bif!` entries with term marshalling at the boundary. **`crypto:hash/2`** → `crypto-sha256`/`crypto-sha512`/`crypto-sha3-256`; atom `Type` dispatch, `er-source-to-string` for `Data`, host hex result → raw bytes via new `er-hexval`/`er-hex->bytes`, returns Erlang binary; bad type/arg → `error:badarg`. **`cid:from_bytes/1`** → `cid-from-bytes` with raw codec `0x55` + sha2-256 multihash assembled in SX (`[0x12,0x20]++digest`); **`cid:to_string/1`** → `cid-from-sx` of `er-format-value` (cbor-encode rejects `er-to-sx`-marshalled symbols; the canonical string form is total + deterministic). **`file:list_dir/1`** → `file-list-dir`, `{ok,[Binary]}` via `er-of-sx` / `{error,Reason}` reusing `er-classify-file-error`. Test gotcha caught + fixed: this Erlang port's binary parser only supports integer/var segments — `<<"abc">>` string-binary literals silently produce **empty** binaries, so the first-cut distinct-input tests compared two empty inputs and failed; rewrote ffi inputs to integer-segment binaries (`<<97,98,99>>`). ffi suite 14→**28** (3 BLOCKED negative-asserts flipped to positive+negative functional tests; `httpc`/`sqlite` kept as deferred unregistered-asserts per fed-prims handoff). Built `sx_server.exe` (dune, opam 5.2.0) at `380bc69f`; full conformance **729/729** (eval 385/385, vm 78/78, **ffi 28/28**, all process suites green). loops/erlang only — not merged, not pushed to architecture.
|
||||
|
||||
- **2026-05-18 FIXED merge-blocking regression: cyclic-env hang in `er-env-derived-from?`** — A trial merge of loops/erlang → architecture regressed Erlang **715/715 → 0/0** on the architecture binary. Bisected: not loader semantics, not a uniform slowdown — pinpointed to the *single* Phase 7 capstone test (eval.sx lines 1314-1346; prefix-1313 was byte-identical speed on both binaries, 27s, prefix-1346 was 28s on loops vs >5min/hung on architecture). Isolated further: spawn+reload alone 0.6s, reload+purge alone 0.3s, but spawn+reload+**purge over forever-blocked procs** hung. Root cause: `er-env-derived-from?` (transpile.sx, used by `code:purge`/`soft_purge` via `er-procs-on-env`) compared closure envs with `(= env target-env)`. loops/erlang's evaluator implements dict `=` as **object identity**; architecture's 131-commit-newer evaluator changed it to **structural deep equality**. Erlang closure envs are large and **cyclic** (a module fun's `:env` transitively references the fun), so structural `=` over them never terminates. Fix: use `identical?` (pointer-identity predicate, present + consistent `(true false)` on *both* binaries) — the actually-intended semantics and host-independent. Verified: full eval.sx on the architecture binary >200s/hung → **59s**; full 10-suite conformance on the architecture binary now **715/715** (eval 385/385, vm 78/78, ffi 14/14, all process suites green). loops/erlang behaviour unchanged (`identical?` ≡ its old `=`-identity). One-file change (`lib/erlang/transpile.sx`, +7/-2). The merge can now be re-attempted; this was the sole blocker.
|
||||
@@ -269,8 +251,6 @@ _Newest first._
|
||||
|
||||
## Blockers
|
||||
|
||||
- **sx-tree WRITE tools broken in this worktree** (2026-06-30). Every sx-tree edit/write tool (`sx_replace_node`, `sx_insert_child`, `sx_insert_near`, …) **and even `sx_write_file`** raise `Yojson.Util.Type_error("Expected string, got null")` against the `mcp_tree.exe` bound in `.mcp.json` (the `/root/rose-ash/...` main-worktree binary). Read/comprehension tools (`sx_validate`, `sx_find_all`, `sx_eval`, `sx_read_*`) work fine. **Workaround:** author/edit `.sx` files with the plain `Write` tool, then `sx_validate` — the same fallback other loops document (see `project_host_on_sx.md`, `project_content_on_sx.md` memory). This is why new `lib/erlang` BIFs land as fresh files (e.g. `lists-ext.sx`) rather than in-place edits to the large `transpile.sx`/`runtime.sx`. Real fix: rebuild `mcp_tree.exe` from current `hosts/ocaml` (out of this loop's binary-build scope) or repoint `.mcp.json` at a fixed binary.
|
||||
|
||||
- **Phase 10a — opcode emission requires `lib/compiler.sx` (out of scope)** (2026-05-15). Architecture fully traced this iteration: the OCaml JIT (`sx_vm.ml` `jit_compile_lambda`, ref-set at line 1206) invokes the SX-level `compile` from **`lib/compiler.sx`** via the CEK machine; that is the sole SX→bytecode producer. Erlang's hot helpers (`er-match-tuple`, `er-bif-*`, …) are SX functions in `transpile.sx` that get JIT-compiled through this path. To emit `erlang.OP_*` they must be recognized as intrinsics inside `compiler.sx`'s `compile-call` (the file's own docstring already anticipates this: "Compilers call `extension-opcode-id` to emit extension opcodes" — designed, not yet implemented). `lib/compiler.sx` is **lib-root**, excluded by the ground rules ("Don't edit lib/ root") and absent from the widened `lib/erlang/** + hosts/ocaml/** (extension only)` scope — editing it changes every guest language's JIT, so it must be owned by a shared-compiler session, not this loop. **Fix path:** that session implements 10a.1 (intrinsic registry in `compiler.sx`) + 10a.2 (`compile-call` emits the opcode when registered & `extension-opcode-id` non-nil, else generic CALL). Erlang's BIF handlers (10b, ids 230-239, all real) light up the instant emission exists — zero further work here. The control opcodes (222-229) additionally need 10a.3 (operand contract) + OCaml↔SX runtime-state bridging (Erlang scheduler/mailbox live in `lib/erlang/runtime.sx`, not OCaml).
|
||||
|
||||
- **Phase 9g — Perf bench gated on 9a** (2026-05-14). The conformance half of 9g (709/709 with stub VM loaded) is satisfied; the perf-bench half requires 9a's bytecode compiler to actually emit the new opcodes at hot call sites. Until then a benchmark would measure today's `er-bif-*` / `er-match-*` numbers unchanged (since the stub handlers wrap them 1-to-1). Re-fire 9g after 9a lands.
|
||||
|
||||
@@ -1,24 +1,64 @@
|
||||
# Go-on-SX: Go on the CEK/VM
|
||||
# Go-on-SX — Go as an SX guest language
|
||||
|
||||
Compile Go source to SX AST; the existing CEK evaluator runs it. The unique angle: Go's
|
||||
goroutines and channels map cleanly onto SX's IO suspension machinery (`perform`/`cek-resume`)
|
||||
— a goroutine is a `cek-step-loop` running in a cooperative scheduler, a channel send/receive
|
||||
is a `perform` that suspends until the other end is ready.
|
||||
Port Go to SX as the **first static-typed, bidirectional-checked guest** in
|
||||
the rose-ash language family. Goal isn't a production Go compiler; it's to
|
||||
prove the substrate from a paradigm angle the existing eleven guests don't
|
||||
cover, and to chisel out the lib/guest kits that statically-typed guests N+1
|
||||
and N+2 will need.
|
||||
|
||||
End-state goal: **core Go programs running**, including goroutines, channels, defer/panic/recover,
|
||||
interfaces, and structs. Not a full Go compiler — no generics, no CGo, no full stdlib — but
|
||||
a faithful runtime for idiomatic Go concurrent programs.
|
||||
Reference:
|
||||
- `plans/lib-guest.md` — parent, chiselling discipline, two-language rule.
|
||||
- `plans/lib-guest-scheduler.md` — sister kit; Go's scheduler pairs with
|
||||
Erlang's. Extraction gated on this loop reaching Phase 5.
|
||||
- `plans/lib-guest-static-types-bidirectional.md` — sister kit; Go's
|
||||
checker pairs with a TBD second consumer. Extraction gated on this loop
|
||||
reaching Phase 3.
|
||||
- `plans/erlang-on-sx.md` — reference implementation for paradigm-port:
|
||||
process model, BIF registry, hot reload, VM bytecode opcodes.
|
||||
|
||||
## Ground rules
|
||||
**Branch:** `loops/go` (loop-style workstream once kicked off). SX files via
|
||||
`sx-tree` MCP only.
|
||||
|
||||
- **Scope:** only touch `lib/go/**` and `plans/go-on-sx.md`. Do **not** edit `spec/`,
|
||||
`hosts/`, `shared/`, or other `lib/<lang>/`.
|
||||
- **Shared-file issues** go under "Blockers" below with a minimal repro; do not fix here.
|
||||
- **SX files:** use `sx-tree` MCP tools only.
|
||||
- **Architecture:** Go source → Go AST → SX AST. No standalone Go evaluator.
|
||||
- **Concurrency model:** cooperative, not preemptive. Goroutines yield at channel ops and
|
||||
`time.Sleep`. A round-robin scheduler in SX drives them.
|
||||
- **Commits:** one feature per commit. Keep `## Progress log` updated and tick boxes.
|
||||
## Thesis — why Go
|
||||
|
||||
Eleven guests already live in `lib/`: apl, common-lisp, datalog, erlang,
|
||||
forth, haskell, hyperscript, js, kernel, lua, minikanren, ocaml, prolog,
|
||||
ruby, scheme, smalltalk, tcl. Every one is either **dynamically typed**
|
||||
(most) or **HM-inferred** (haskell, ocaml). None exercise:
|
||||
|
||||
1. **Bidirectional static type checking** — annotation-driven, locally-
|
||||
inferred, the dominant paradigm of modern statically-typed languages.
|
||||
2. **Anonymous-channel concurrency** — Go's `chan` and `select`. Erlang has
|
||||
addressed processes + mailboxes; Go has anonymous values + structural
|
||||
pairing. Two different vocabularies for the same underlying scheduler
|
||||
machinery.
|
||||
3. **Structural interfaces** — `io.Reader` is "anything with this method
|
||||
signature", not a declared subtype relationship. Different from Haskell
|
||||
typeclasses (nominal), different from Lua duck typing (no declaration).
|
||||
|
||||
These three together make Go an unusually high-value port for proving SX.
|
||||
If SX can host Go cleanly, it can host the next decade of mainstream
|
||||
statically-typed languages (Rust, TS, Swift, Kotlin, Scala 3, Hack) because
|
||||
they share these three properties.
|
||||
|
||||
Like Erlang-on-SX validated the actor model on the substrate, Go-on-SX
|
||||
validates the goroutine model + bidirectional types.
|
||||
|
||||
## Non-goals (deliberate)
|
||||
|
||||
Out of scope. Reject feature requests for these without further consideration:
|
||||
|
||||
- **`unsafe` package.** Memory mucking. Skip entirely.
|
||||
- **CGo.** C interop. Out of scope at every level.
|
||||
- **Full `reflect`.** Provide enough for `fmt.Println` to render values;
|
||||
reject the rest.
|
||||
- **Build tags, modules, vendoring.** Treat source as monolithic. One
|
||||
package per file, no real import resolution.
|
||||
- **Production performance.** Conformance tests pass; benchmarks don't.
|
||||
- **Garbage collection tuning.** SX's GC is what you get.
|
||||
- **Race detector, escape analysis, inlining.** Out of scope.
|
||||
- **`os`, `net/http`, full stdlib.** Provide a deliberately small slice
|
||||
(Phase 8 below).
|
||||
|
||||
## Architecture sketch
|
||||
|
||||
@@ -26,113 +66,335 @@ a faithful runtime for idiomatic Go concurrent programs.
|
||||
Go source text
|
||||
│
|
||||
▼
|
||||
lib/go/tokenizer.sx — Go tokens: keywords, idents, string/rune/number literals,
|
||||
│ operators, semicolon insertion rules
|
||||
lib/go/lex.sx — tokens; ASI; literals; operators
|
||||
│ (consumes lib/guest/core/lex.sx)
|
||||
▼
|
||||
lib/go/parser.sx — Go AST: package, import, var, const, type, func, struct,
|
||||
│ interface, goroutine, channel ops, defer, select, for range
|
||||
lib/go/parse.sx — AST: package/import/var/const/type/func/struct/
|
||||
│ interface; expressions; statements
|
||||
│ (consumes lib/guest/core/pratt.sx + ast.sx)
|
||||
▼
|
||||
lib/go/transpile.sx — Go AST → SX AST
|
||||
│
|
||||
lib/go/types.sx — bidirectional type checker. Synth + check judgments;
|
||||
│ structural interface satisfaction; pluggable subtype
|
||||
│ (INDEPENDENT — no lib/guest/static-types-bidirectional
|
||||
│ yet; this loop builds the first consumer)
|
||||
▼
|
||||
lib/go/runtime.sx — goroutine scheduler, channel primitives, defer stack,
|
||||
│ panic/recover, interface dispatch, slice/map ops
|
||||
lib/go/eval.sx — tree-walk evaluator on CEK. Variables as mutable cells;
|
||||
│ slices = (length, capacity, backing-vector); maps =
|
||||
│ SX dict; defer stack per frame.
|
||||
▼
|
||||
CEK / VM
|
||||
lib/go/sched.sx — goroutine scheduler + channels + select
|
||||
│ (INDEPENDENT — no lib/guest/scheduler yet; this loop
|
||||
│ builds the first consumer)
|
||||
▼
|
||||
lib/go/std/ — minimal stdlib slice (fmt, strings, strconv, sync,
|
||||
time, errors)
|
||||
```
|
||||
|
||||
Key semantic mappings:
|
||||
- `go fn()` → spawn new coroutine (SX coroutine primitive, Phase 4 of primitives)
|
||||
- `ch <- v` (send) → `perform` that suspends until receiver ready; scheduler picks next goroutine
|
||||
- `v := <-ch` (receive) → `perform` that suspends until sender ready
|
||||
- `select { case ... }` → scheduler checks all channel readiness, picks first ready
|
||||
- `defer fn()` → push onto a per-goroutine defer stack; run on return/panic
|
||||
- `panic(v)` → `raise` the value; `recover()` catches it in deferred function
|
||||
- `interface{}` → any SX value (duck typed)
|
||||
- `struct { ... }` → SX hash table with field names as keys
|
||||
- `slice` → SX vector with length + capacity metadata
|
||||
- `map[K]V` → SX mutable hash table (Phase 10 of primitives)
|
||||
Semantic mappings (operational):
|
||||
- `go fn(args)` → `task-spawn` on the local scheduler.
|
||||
- `ch <- v` → `task-block` with predicate "receiver waiting on ch".
|
||||
- `v := <-ch` → `task-block` with predicate "sender waiting on ch".
|
||||
- `select { case ... }` → `task-block` with predicate "any case ready".
|
||||
- `defer fn()` → push thunk onto per-frame defer stack; runs LIFO on
|
||||
return or panic.
|
||||
- `panic(v)` → raise SX exception; deferred fns run while unwinding.
|
||||
- `recover()` → CEK exception capture inside a deferred fn.
|
||||
- `interface{T}` → type-check matches structurally against T's method
|
||||
set; at runtime, the value carries its concrete-type metadata.
|
||||
- `struct{...}` → SX dict + type tag; methods are functions in the type's
|
||||
method table.
|
||||
- `*T` (pointer) → mutable cell (Common Lisp port did the same).
|
||||
- `[]T` (slice) → triple (length, capacity, backing-vector).
|
||||
- `map[K]V` → SX dict; iteration order spec-undefined (v1 = sorted for
|
||||
determinism — programs that depend on indeterminism fail loudly, which
|
||||
is a feature not a bug).
|
||||
|
||||
## Roadmap
|
||||
## Conformance scoreboard
|
||||
|
||||
### Phase 1 — tokenizer + parser
|
||||
- [ ] Tokenizer: keywords (`package`, `import`, `func`, `var`, `const`, `type`, `struct`,
|
||||
`interface`, `go`, `chan`, `select`, `defer`, `return`, `if`, `else`, `for`, `range`,
|
||||
`switch`, `case`, `default`, `break`, `continue`, `goto`, `fallthrough`, `map`,
|
||||
`make`, `new`, `nil`, `true`, `false`), automatic semicolon insertion, string literals
|
||||
(interpreted + raw `` `...` ``), rune literals `'a'`, number literals (int, float, hex,
|
||||
octal, binary, complex), operators, slices `[:]`
|
||||
- [ ] Parser: package clause, imports, top-level `func`/`var`/`const`/`type`; function
|
||||
bodies: short variable decl `:=`, assignments, `if`/`else`, `for`/`range`, `switch`,
|
||||
`return`, struct literals, slice literals, map literals, composite literals, type
|
||||
assertions `v.(T)`, method calls `v.Method(args)`, goroutine `go`, channel ops
|
||||
`<-ch`, `ch <- v`, `defer`, `select`
|
||||
- [ ] Tests in `lib/go/tests/parse.sx`
|
||||
Following `lib/erlang/scoreboard.json` precedent. Add
|
||||
`lib/go/scoreboard.json` on first iteration; populate as suites land.
|
||||
Suites planned:
|
||||
|
||||
### Phase 2 — transpile: basic Go (no goroutines)
|
||||
- [ ] `go-eval-ast` entry
|
||||
- [ ] Arithmetic, string ops, comparison, boolean
|
||||
- [ ] Variables, short decl, assignment, multiple assignment
|
||||
- [ ] `if`/`else if`/`else`
|
||||
- [ ] `for` (C-style), `for range` over slice/map/string
|
||||
- [ ] Functions: named + anonymous, multiple return values (SX multiple values, Phase 8)
|
||||
- [ ] Structs → SX hash tables; field access `.field`; struct literals `T{f: v}`
|
||||
- [ ] Slices → SX vectors; `len`, `cap`, `append`, `copy`, slice expressions `s[a:b]`
|
||||
- [ ] Maps → SX hash tables; `make(map[K]V)`, `m[k]`, `m[k] = v`, `delete(m, k)`,
|
||||
comma-ok `v, ok := m[k]`
|
||||
- [ ] Pointers — modelled as single-element mutable vectors; `&x` creates wrapper, `*p` dereferences
|
||||
- [ ] `fmt.Println`/`fmt.Printf`/`fmt.Sprintf` → SX IO perform (print)
|
||||
- [ ] 40+ eval tests in `lib/go/tests/eval.sx`
|
||||
| Suite | Tests target | What it covers |
|
||||
|---|---|---|
|
||||
| `lex` | 50+ | Keywords, operators, literals, ASI |
|
||||
| `parse` | 80+ | All statement & expression shapes |
|
||||
| `types` | 90+ | Synth, check, interface satisfaction, generics |
|
||||
| `eval` | 100+ | Tree-walk over typed AST |
|
||||
| `runtime` | 60+ | Goroutines, channels, select, close |
|
||||
| `stdlib` | 40+ | fmt, strings, strconv, sync, time, errors |
|
||||
| `e2e` | 10+ | Complete representative programs |
|
||||
|
||||
### Phase 3 — defer / panic / recover
|
||||
- [ ] Defer stack per function frame — SX list of thunks, run LIFO on return
|
||||
- [ ] `defer` statement pushes thunk; transpiler wraps function body in try/finally equivalent
|
||||
- [ ] `panic(v)` → `raise` with Go panic wrapper
|
||||
- [ ] `recover()` → catches panic value inside a deferred function; returns nil otherwise
|
||||
- [ ] Panic propagation across call stack until recovered or fatal
|
||||
- [ ] Tests: defer ordering, panic/recover, panic in goroutine without recover
|
||||
## Phasing — one feature per commit
|
||||
|
||||
### Phase 4 — goroutines + channels
|
||||
- [ ] Coroutine-based goroutine type using SX coroutine primitive (Phase 4 of primitives)
|
||||
- [ ] Round-robin scheduler in `lib/go/runtime.sx`: maintains run queue, steps each
|
||||
goroutine one turn at a time, suspends at channel ops
|
||||
- [ ] Unbuffered channels: `make(chan T)` → rendezvous point; send suspends until receive
|
||||
and vice versa. Implemented as a pair of waiting queues + `cek-resume`.
|
||||
- [ ] Buffered channels: `make(chan T, n)` → circular buffer; send only blocks when full,
|
||||
receive only blocks when empty
|
||||
- [ ] `close(ch)` — mark channel closed; receivers drain then get zero value + `false`
|
||||
- [ ] `select` — scheduler inspects all cases, picks a ready one (random if multiple),
|
||||
blocks if none ready until at least one becomes ready
|
||||
- [ ] `go fn(args)` — spawns new goroutine on run queue
|
||||
- [ ] `time.Sleep(d)` — yields current goroutine, re-queues after d milliseconds
|
||||
(simulated with IO perform timer)
|
||||
- [ ] Tests: ping-pong, fan-out, fan-in, select with default, range over channel
|
||||
Loop-style. Each phase: implement → test → commit → tick `[ ]` → append
|
||||
Progress-log line → push `origin/loops/go`.
|
||||
|
||||
### Phase 5 — interfaces
|
||||
- [ ] Interface type → SX dict `{:type "T" :methods {...}}` dispatch table
|
||||
- [ ] `interface{}` / `any` → any SX value (already implicit)
|
||||
- [ ] Type assertion `v.(T)` → check `:type` field, panic if mismatch
|
||||
- [ ] Type switch `switch v.(type) { case T: ... }` → dispatches on `:type`
|
||||
- [ ] Method sets — structs implement interfaces implicitly if they have the right methods
|
||||
- [ ] Value vs pointer receivers — pointer receiver gets the mutable vector wrapper
|
||||
- [ ] Built-in interfaces: `error` (`Error() string`), `Stringer` (`String() string`)
|
||||
- [ ] Tests: interface satisfaction, type assertion, type switch, error interface
|
||||
### Phase 1 — Tokenizer (`lib/go/lex.sx`) ⬜
|
||||
- [x] Scaffold + scoreboard + conformance runner (consumes lib/guest/lex.sx)
|
||||
- [x] Identifiers + 25 keywords
|
||||
- [x] Decimal integer literals
|
||||
- [x] Interpreted string literals `"..."` with `\n \t \r \\ \" \'` escapes
|
||||
- [x] Rune literals `'x'` (single char + simple escapes)
|
||||
- [x] Line + block comments (block w/ newline triggers ASI)
|
||||
- [x] Common operator/punct set incl. `:= <- ++ -- == != <= >= && || ...`
|
||||
- [x] **Automatic semicolon insertion** (Go spec § Semicolons) — newline,
|
||||
EOF, and block-comment-with-newline trigger `;` after
|
||||
ident/int/string/rune/{break,continue,fallthrough,return}/{++,--,),],}}.
|
||||
- [ ] Float / imaginary literals
|
||||
- [ ] Raw string literals `` `...` ``
|
||||
- [ ] Hex/octal/binary integer literals (0x… 0o… 0b…) + underscores
|
||||
- [ ] Full operator set audit (47 distinct per Go spec)
|
||||
- **Acceptance:** lex/ suite at 50+ tests. Current: 78/78.
|
||||
|
||||
### Phase 6 — standard library subset
|
||||
- [ ] `fmt` — `Println`, `Printf`, `Sprintf`, `Fprintf`, `Errorf`, `Stringer` dispatch
|
||||
- [ ] `strings` — `Contains`, `HasPrefix`, `HasSuffix`, `Split`, `Join`, `TrimSpace`,
|
||||
`ToUpper`, `ToLower`, `Replace`, `Index`, `Count`, `Repeat`
|
||||
- [ ] `strconv` — `Itoa`, `Atoi`, `FormatFloat`, `ParseFloat`, `ParseInt`, `FormatInt`
|
||||
- [ ] `math` — full surface via SX math primitives (Phase 15)
|
||||
- [ ] `sort` — `sort.Slice`, `sort.Ints`, `sort.Strings`
|
||||
- [ ] `errors` — `errors.New`, `errors.Is`, `errors.As`
|
||||
- [ ] `sync` — `sync.Mutex` (cooperative — just a boolean flag + goroutine queue),
|
||||
`sync.WaitGroup`, `sync.Once`
|
||||
- [ ] `io` — `io.Reader`/`io.Writer` interfaces; `io.ReadAll`; `strings.NewReader`
|
||||
### Phase 2 — Parser (`lib/go/parse.sx`) ⬜
|
||||
- Consume `lib/guest/core/pratt.sx` + `lib/guest/core/ast.sx`. Chisel notes
|
||||
`consumes-pratt consumes-ast`.
|
||||
- Grammar coverage:
|
||||
- Declarations: `package`, `import`, `var`, `const`, `type`, `func`
|
||||
- Types: basic, slice `[]T`, array `[N]T`, map `map[K]V`, chan `chan T`,
|
||||
func `func(...)...`, struct, interface, pointer `*T`
|
||||
- Expressions: literals, identifier, call, index `[]`, slice `[a:b]`,
|
||||
type assertion `v.(T)`, operators
|
||||
- Statements: `if`/`else`, `for` (C-style + range), `switch`, `select`,
|
||||
`return`, `defer`, `go`, `break`/`continue`, assign, short-decl `:=`,
|
||||
send `ch <- v`, recv `<-ch`
|
||||
- Output: SX-shaped AST per `lib/guest/core/ast.sx` conventions.
|
||||
- Tests: round-trip parse of hello world, fibonacci, FizzBuzz, goroutine
|
||||
ping-pong, struct + method.
|
||||
- **Acceptance:** parse/ suite at 80+ tests.
|
||||
|
||||
### Phase 7 — full conformance target
|
||||
- [ ] Vendor a Go test suite or hand-build 100+ program tests in `lib/go/tests/programs/`
|
||||
- [ ] Drive scoreboard
|
||||
### Phase 3 — Bidirectional type checker, MVP (`lib/go/types.sx`) ⬜
|
||||
- **Independent implementation.** Do NOT use lib/guest/static-types-
|
||||
bidirectional/ — that kit doesn't exist yet and depends on this work
|
||||
for its design. See `plans/lib-guest-static-types-bidirectional.md`.
|
||||
- Synth + check judgments. Context as a value (per-block scope).
|
||||
- Coverage MVP: declared-type variables, function signatures (params +
|
||||
returns), call type-checking, simple composite types (slice, map, chan
|
||||
element), interface satisfaction (structural match against method sets),
|
||||
short variable declaration `:=` (synth from RHS).
|
||||
- **Untyped constants.** `42` has type `untyped int` until contextualised;
|
||||
this is the canonical pitfall (see Gotchas below).
|
||||
- Defer: generics (Phase 7), full conversion rules.
|
||||
- Tests: positive (type-correct programs check) + negative (mismatched
|
||||
types fail with informative errors carrying AST paths).
|
||||
- **Acceptance:** types/ suite at 60+ tests. Chisel note `shapes-static-
|
||||
types-bidirectional` — append a paragraph to the sister plan's design
|
||||
diary describing what synth/check shape emerged.
|
||||
|
||||
### Phase 4 — Tree-walk evaluator (`lib/go/eval.sx`) ⬜
|
||||
- AST-walking interpreter over CEK. Each Go statement maps to one step
|
||||
function (precedent: `step-sf-if` etc. in spec/evaluator.sx).
|
||||
- Variables: mutable cells. Pointer semantics: `&x` returns the cell,
|
||||
`*p` dereferences.
|
||||
- Slices: triple (length, capacity, backing-vector). `append` honours
|
||||
capacity-grow per spec.
|
||||
- Maps: SX dict + key-type metadata.
|
||||
- Structs: SX dict + type tag. Methods looked up via type's method table.
|
||||
- Functions: closures over enclosing scope; multiple return values.
|
||||
- Channels: stub (Phase 5 wires them).
|
||||
- Tests: arithmetic, control flow, recursion, closures, slices, maps,
|
||||
structs, methods, pointer semantics, multiple-return.
|
||||
- **Acceptance:** eval/ suite at 80+ tests. No concurrency yet.
|
||||
|
||||
### Phase 5 — Goroutines + channels + select (`lib/go/sched.sx`) ⬜
|
||||
- **Independent implementation.** Do NOT use lib/guest/scheduler/ — that
|
||||
kit doesn't exist yet and depends on this work for its design. See
|
||||
`plans/lib-guest-scheduler.md`.
|
||||
- `go expr` — spawn a goroutine; returns nothing.
|
||||
- `chan T` — `make(chan T)` creates an unbuffered channel; `make(chan T,n)`
|
||||
creates a buffered channel (Phase 5b — defer buffer to a sub-phase).
|
||||
- `<-ch` — receive (blocks until sender ready).
|
||||
- `ch <- v` — send (blocks until receiver ready for unbuffered, or buffer
|
||||
has room for buffered).
|
||||
- `select { case ... }` — non-deterministic multiplexing; `default` makes
|
||||
it non-blocking.
|
||||
- `close(ch)` — closes channel. Receive on closed → zero value + ok=false.
|
||||
- Tests: ping-pong, fan-out/fan-in, work queue, select with default,
|
||||
select with timeout (via a `time.After`-like stub), close semantics,
|
||||
range over channel.
|
||||
- **Acceptance:** runtime/ suite at 40+ tests. Chisel note `shapes-
|
||||
scheduler` — append a paragraph to the sister plan's design diary
|
||||
describing what task-spawn/block/wake/yield shape emerged.
|
||||
|
||||
### Phase 5b — Buffered channels + select fairness ⬜
|
||||
- Buffered: send blocks only when buffer full; recv only when empty.
|
||||
- `select` random case ordering (spec mandates pseudo-random; v1 uses a
|
||||
fixed seed for determinism with a `runtime`-package knob to randomise).
|
||||
- Tests: buffer-full blocking, buffer-empty blocking, select fairness
|
||||
over many iterations.
|
||||
- **Acceptance:** runtime/ +20 tests.
|
||||
|
||||
### Phase 6 — `defer` + panic/recover ⬜
|
||||
- Defer stack per function frame; runs LIFO on return (normal or panic).
|
||||
- `panic(v)` unwinds frames running deferreds; `recover()` inside a
|
||||
deferred fn captures the panic value and stops unwinding.
|
||||
- Goroutine panic propagation: a panicking goroutine that doesn't recover
|
||||
crashes the whole program (honour Go spec, or document divergence).
|
||||
- Tests: defer order (LIFO), defer + named-return mutation, panic/recover,
|
||||
panic across goroutines, defer in a loop (push per iter, run on fn
|
||||
return — common bug).
|
||||
- **Acceptance:** eval/ +20 tests.
|
||||
|
||||
### Phase 7 — Generics (Go 1.18+) ⬜
|
||||
- Type parameters with constraints (type sets: `interface{ int | float64
|
||||
}`, `comparable`, `any`).
|
||||
- Type inference at call sites — basic; the full Go inference algorithm
|
||||
is notoriously complex. Implement enough for common cases; document
|
||||
limitations in a Blockers section below.
|
||||
- Tests: generic function (`func Map[T, U any](xs []T, f func(T) U) []U`),
|
||||
generic data structure (linked list), constrained type param.
|
||||
- **Acceptance:** types/ +30 tests.
|
||||
|
||||
### Phase 8 — Minimal stdlib (`lib/go/std/`) ⬜
|
||||
- Implement just what's needed for representative programs:
|
||||
- `fmt` — `Println`, `Printf`, `Sprintf`, `Fprintf`, `Errorf`,
|
||||
`Stringer` dispatch. Verbs: `%d %s %v %t %f %T %+v`.
|
||||
- `strings` — `Contains`, `HasPrefix`, `HasSuffix`, `Split`, `Join`,
|
||||
`TrimSpace`, `ToUpper`, `ToLower`, `Replace`, `Index`, `Count`,
|
||||
`Repeat`, `NewReader`.
|
||||
- `strconv` — `Itoa`, `Atoi`, `FormatFloat`, `ParseFloat`, `ParseInt`,
|
||||
`FormatInt`.
|
||||
- `errors` — `New`, `Is`, `As`, `Unwrap`.
|
||||
- `sync` — `Mutex` (cooperative — flag + waiter queue), `WaitGroup`,
|
||||
`Once`, `RWMutex`.
|
||||
- `time` — `Now`, `Since`, `After` (channel-returning timer), `Sleep`,
|
||||
`Duration`, `Time`.
|
||||
- `io` — `Reader`/`Writer` interfaces; `ReadAll`; `Copy`.
|
||||
- `sort` — `Slice`, `Ints`, `Strings`.
|
||||
- Tests: round-trip Itoa/Atoi, fmt verb coverage, sync.WaitGroup with
|
||||
goroutines, time.After in a select, sort.Slice with custom less fn.
|
||||
- **Acceptance:** stdlib/ suite at 40+ tests.
|
||||
|
||||
### Phase 9 — End-to-end programs ⬜
|
||||
- Complete programs from canonical sources (gopl.io, "concurrency
|
||||
patterns" talk examples) running end-to-end:
|
||||
- Concurrent prime sieve
|
||||
- HTTP-ish ping-pong over stubbed transport
|
||||
- Word frequency counter
|
||||
- Pipeline (channel chain)
|
||||
- Producer/consumer with sync.WaitGroup
|
||||
- "Bounded parallelism" pattern (worker pool over a job channel)
|
||||
- **Acceptance:** e2e/ suite at 10+ tests, all passing.
|
||||
|
||||
### Phase 10 — lib/guest extraction enabler ⬜
|
||||
- Now that Go has lex+parse+types+eval+sched, sister plans are unblocked
|
||||
on the Go side. This phase is **doc-only** in `loops/go`:
|
||||
- Cross-reference `plans/lib-guest-scheduler.md` — mark its Phase 1
|
||||
(Go scheduler independent) as complete from Go's side.
|
||||
- Cross-reference `plans/lib-guest-static-types-bidirectional.md` —
|
||||
mark its Phase 1 as complete from Go's side.
|
||||
- Update the chiselling diary in each sister plan with the actual
|
||||
Go-side surface that emerged.
|
||||
- **Acceptance:** sister plans cross-referenced + diaries updated. No
|
||||
new Go code.
|
||||
|
||||
### Phase 11 — VM bytecode opcodes (deferred, optional) ⬜
|
||||
- Following Erlang-on-SX Phase 10 precedent: identify hot paths in the
|
||||
tree-walk evaluator, define Go-specific bytecode opcodes, compile hot
|
||||
fns through them. Substantial work; only justified if Go programs
|
||||
exercise enough volume that performance starts mattering.
|
||||
- **Acceptance:** TBD on demand.
|
||||
|
||||
## Ground rules (loop-style)
|
||||
|
||||
- **Scope:** only `lib/go/**` and this plan. Do not touch `spec/`,
|
||||
`hosts/`, `shared/`, `lib/guest/**` (read-only consumer at this phase),
|
||||
or other `lib/<lang>/`.
|
||||
- **Consume `lib/guest/core/`** for lex/parse/ast/match/layout. Hand-
|
||||
rolling defeats the chiselling goal.
|
||||
- **Do NOT extract into `lib/guest/scheduler/` or `lib/guest/static-
|
||||
types-bidirectional/` from this loop.** Those extractions are gated on
|
||||
two consumers AND the discipline of writing each consumer
|
||||
independently. Extraction is its own workstream after Go and the
|
||||
second consumer both exist.
|
||||
- **Substrate gaps** → Blockers entry with minimal repro. Don't fix the
|
||||
substrate from this loop. Belongs to `sx-improvements.md`.
|
||||
- **NEVER call `sx_build` without timeout awareness** — 600s watchdog.
|
||||
- **SX files:** `sx-tree` MCP tools ONLY. `sx_validate` after every edit.
|
||||
- **Worktree:** branch `loops/go`, push `origin/loops/go`. Never `main`,
|
||||
never `architecture`.
|
||||
- **Commit granularity:** one feature per commit. Short factual messages:
|
||||
`go: parse short-decl + 6 tests [consumes-pratt]`. Chisel note at end
|
||||
in brackets.
|
||||
- **Plan file:** update Progress log + tick boxes every commit.
|
||||
- **If blocked** for two iterations on the same issue, add to Blockers
|
||||
and move on. Phases 1-4 are sequential; Phases 5-8 are largely
|
||||
independent once 4 lands.
|
||||
|
||||
## Chisel discipline (per parent lib-guest plan)
|
||||
|
||||
Every commit ends its message with a chisel note in brackets:
|
||||
|
||||
- `[consumes-X]` — used `lib/guest/X` kit.
|
||||
- `[shapes-scheduler]` / `[shapes-static-types-bidirectional]` — revealed
|
||||
something about what the sister lib-guest kits should look like. Add a
|
||||
paragraph to the relevant sister plan's design diary.
|
||||
- `[proposes-Y]` — revealed a gap in another existing kit. Blockers entry
|
||||
in the kit's plan.
|
||||
- `[nothing]` — pure Go work that didn't touch substrate or lib/guest
|
||||
story. Acceptable; if it shows up twice in a row, stop and reflect.
|
||||
|
||||
## Go-specific gotchas
|
||||
|
||||
- **ASI (automatic semicolon insertion).** Newline becomes `;` after
|
||||
identifier/literal/`)`/`]`/`}`. Build into the tokenizer; the Go spec's
|
||||
"Semicolons" section is unusually precise — follow it literally.
|
||||
- **Untyped constants.** `42` has type `untyped int` until used in a
|
||||
context that forces a type. The canonical example: `var x float64 = 42
|
||||
/ 7` — must compute as `untyped int / untyped int = 6` then convert to
|
||||
`float64 = 6.0`. Wrong: float-coercing eagerly gives 6.0 prematurely.
|
||||
Wrong: integer-truncating after coercion gives `5.something`. Test it.
|
||||
- **Methods vs functions.** `func (r Receiver) Method()` is a method
|
||||
bound to a type; `func Function(r Receiver)` is just a function.
|
||||
Methods on pointer-receivers vs value-receivers have asymmetric
|
||||
satisfaction in interfaces — pointer-receiver methods are NOT in the
|
||||
value's method set for interface satisfaction.
|
||||
- **Interface satisfaction is structural and silent.** Type satisfies an
|
||||
interface if its method set contains all the interface's methods.
|
||||
Lazy check: at every point a value flows into an interface-typed slot.
|
||||
- **Channels are first-class values.** Pass them, store them, send them
|
||||
through other channels. Each channel has identity.
|
||||
- **`select` with `default`** = non-blocking. Without `default`, blocks
|
||||
until a case is ready.
|
||||
- **`nil` is typed.** `var x *int` makes x a `(*int)(nil)`. Comparison
|
||||
`x == nil` works on typed nil; but `var i interface{} = (*int)(nil); i
|
||||
== nil` is `false` — i holds a typed-nil-of-type-`*int`, not untyped
|
||||
nil. The classic Go footgun. Test it.
|
||||
- **Goroutine panic propagation.** A panicking goroutine that doesn't
|
||||
recover crashes the whole program. Implement faithfully or document
|
||||
divergence.
|
||||
- **`defer` in a loop.** Each iteration pushes; they all run on function
|
||||
return. Common bug; tests should cover.
|
||||
- **Iteration order of maps.** Spec: unspecified. v1 = sorted by SX-
|
||||
canonical key order for determinism; document that programs depending
|
||||
on iteration order are not Go-conformant. Add a `runtime`-package knob
|
||||
to enable randomisation later.
|
||||
|
||||
## Style
|
||||
|
||||
- No comments in `.sx` unless non-obvious. Cite Go spec sections inline
|
||||
for non-obvious decisions (Go's spec is rigorous; citations work).
|
||||
- No new planning docs — update this plan inline.
|
||||
- One feature per iteration. Commit. Log. Push. Next.
|
||||
|
||||
## Open questions
|
||||
|
||||
1. **Module/import model.** Go has packages and import paths. Probably
|
||||
model "package" as one or more `.sx` files in a directory, no real
|
||||
import resolution against a remote module graph. Decide in Phase 2.
|
||||
2. **Goroutine identity.** Spec says goroutines have no identity; the
|
||||
scheduler does internally. Expose to user code? No (not Go). Expose
|
||||
for debugging? Yes via a `runtime`-package stub.
|
||||
3. **Error handling: panic-as-exception vs explicit error returns.** Go
|
||||
strongly prefers explicit errors. Stdlib stubs follow that: `strconv.
|
||||
Atoi("x")` returns `(0, err)`, not panic.
|
||||
4. **Memory model.** Go has a happens-before model for atomics + channel
|
||||
ops. SX runtime is single-threaded under the scheduler — every channel
|
||||
op is a synchronization point automatically. Don't model relaxed
|
||||
memory; document the simplification.
|
||||
5. **Iteration order of maps.** Already addressed in Gotchas; flagged
|
||||
here as a known divergence from spec.
|
||||
|
||||
## Blockers
|
||||
|
||||
@@ -140,6 +402,16 @@ _(none yet)_
|
||||
|
||||
## Progress log
|
||||
|
||||
_Newest first._
|
||||
_Newest first. Append one dated entry per commit._
|
||||
|
||||
_(awaiting phase 1)_
|
||||
- 2026-05-26 — Phase 1 first slice: `lib/go/lex.sx` tokenizer consuming
|
||||
`lib/guest/lex.sx` predicates. 25 keywords, ident/int/string/rune lits,
|
||||
line+block comments, common operators, automatic semicolon insertion per
|
||||
Go spec § Semicolons (newline / EOF / block-comment-with-newline triggers).
|
||||
Scoreboard + conformance.sh wired. 78/78 tests. `[consumes-lex]`.
|
||||
- 2026-05-26 — Plan rewritten to integrate the lib/guest framework
|
||||
(chiselling discipline, sister plans for scheduler + bidirectional
|
||||
types, type-checker phase added, conformance scoreboard model adopted).
|
||||
Original 2026-04-26 draft preserved in git history. Loop not yet
|
||||
kicked off; Phase 1 (tokenizer) is the first iteration when this loop
|
||||
spins up.
|
||||
|
||||
235
plans/lib-guest-scheduler.md
Normal file
235
plans/lib-guest-scheduler.md
Normal file
@@ -0,0 +1,235 @@
|
||||
# lib/guest/scheduler — extraction plan
|
||||
|
||||
Two distinct concurrency models — Erlang's addressed processes + mailboxes, and
|
||||
Go's anonymous channels + goroutines — sit on the same underlying machinery:
|
||||
a fork/yield/block/resume scheduler over CEK io-suspended continuations. This
|
||||
plan captures that machinery as `lib/guest/scheduler/` so language N+1 with a
|
||||
new concurrency model costs ~200 lines of model-specific code instead of
|
||||
re-inventing the scheduler.
|
||||
|
||||
Reference: `plans/lib-guest.md` (parent — two-language rule, stratification),
|
||||
`plans/erlang-on-sx.md` (first consumer, in production), Go-on-SX (second
|
||||
consumer, see `plans/go-on-sx.md` once that lands).
|
||||
|
||||
**Branch:** `architecture`. SX files via `sx-tree` MCP only.
|
||||
|
||||
## Thesis
|
||||
|
||||
The substrate already provides what a scheduler needs: CEK io-suspension
|
||||
(`make-cek-suspended`, `cek-resume`) gives suspendable execution; first-class
|
||||
environments give each unit of execution its own scope; the trampolined
|
||||
evaluator means we never blow the host stack. What every guest with concurrency
|
||||
*re-implements* on top of this is the **fork/yield/block/resume protocol** —
|
||||
the bookkeeping that decides which suspended computation runs next.
|
||||
|
||||
Two concrete consumers, two different concurrency vocabularies, sharing one
|
||||
underlying scheduler, is the proof. If only Erlang lives on it, "scheduler kit"
|
||||
is a euphemism for "Erlang scheduler with a Go skin." The two-language rule
|
||||
is the gate.
|
||||
|
||||
## Current state (2026-05-26)
|
||||
|
||||
- **Erlang-on-SX** has the full pattern in production: 729/729 conformance,
|
||||
spawn/send/receive, selective receive, monitor/link, hot reload. The
|
||||
scheduler logic is currently coupled to Erlang-shaped concepts (PIDs,
|
||||
mailboxes, links) — extraction-blocking but not extraction-defeating.
|
||||
- **Go-on-SX** does not exist yet. `plans/go-on-sx.md` is the umbrella plan
|
||||
(TBD); this scheduler plan is a sibling/dependency.
|
||||
- **lib/guest/scheduler/** does not exist. The two-language rule blocks
|
||||
extraction until Go-on-SX independently implements its scheduler.
|
||||
|
||||
**Status: Phase 0 (Erlang shape capture).** No code change in this plan yet.
|
||||
|
||||
## Why the two models actually share a kit
|
||||
|
||||
The non-obvious claim is that Erlang processes and Go goroutines really do
|
||||
share machinery beneath their different vocabularies. The mapping:
|
||||
|
||||
| Concept | Erlang | Go | Common kit name |
|
||||
|---|---|---|---|
|
||||
| Unit of execution | process (PID-addressed) | goroutine (anonymous) | **task** |
|
||||
| Spawn | `spawn(Fun)` → PID | `go expr` → nothing | `task-spawn` |
|
||||
| Block target | mailbox match | channel send/recv | `task-block` |
|
||||
| Wake condition | message arrives | counterpart ready | `task-resume` predicate |
|
||||
| Yield | `receive` with no match | channel blocked | scheduler hands off |
|
||||
| Termination | exit reason → linked tasks | panic / return | task lifecycle |
|
||||
| Selection | selective `receive` | `select` statement | both = "wait for any of N predicates" |
|
||||
|
||||
What the kit owns:
|
||||
- The **task table** (token → suspended CEK continuation + status).
|
||||
- The **runnable queue** + scheduling policy (round-robin v1; pluggable).
|
||||
- The **block→resume protocol**: a blocked task registers a predicate; when
|
||||
any task changes state, blocked tasks are re-polled; first whose predicate
|
||||
fires becomes runnable.
|
||||
- The **fairness/preemption budget** — gas per step before forced yield.
|
||||
|
||||
What each language owns:
|
||||
- The semantics layer on top: Erlang's PID→task map + mailbox per task +
|
||||
selective-receive predicates; Go's channel value → blocked-task list per
|
||||
channel + send/recv pairing + select multiplexing.
|
||||
- The language-visible API (`spawn`/`!`/`receive` vs `go`/`<-`/`select`).
|
||||
|
||||
This is exactly the lib/guest pattern: extract the dispatch skeleton, keep
|
||||
the rules in the language layer.
|
||||
|
||||
## API surface (proposed — design only, not yet implemented)
|
||||
|
||||
```
|
||||
(make-scheduler &key gas-per-step ;; default 1000
|
||||
policy) ;; :round-robin | :fifo
|
||||
-> scheduler-handle
|
||||
|
||||
(task-spawn sched body-thunk) -> task-token
|
||||
;; body-thunk is a 0-arg fn whose body runs as the task.
|
||||
;; Returns immediately; task is enqueued runnable.
|
||||
|
||||
(task-current sched) -> task-token
|
||||
;; Inside a task, the token of the running task. Useful for self-reference.
|
||||
|
||||
(task-yield sched) -> nil
|
||||
;; Voluntary yield. Caller is re-enqueued at the tail of runnable.
|
||||
|
||||
(task-block sched resume-predicate) -> any
|
||||
;; Caller suspends. Predicate is (fn () -> resume-value-or-#f).
|
||||
;; When predicate returns non-#f, caller resumes with that value.
|
||||
;; Predicate is polled on every scheduler tick when there's nothing
|
||||
;; obviously runnable. (Optimisation: language layer can wake explicitly —
|
||||
;; see task-wake.)
|
||||
|
||||
(task-wake sched task) -> nil
|
||||
;; Hint to the scheduler: re-poll this task's resume-predicate now.
|
||||
;; Used by sender-side when a receiver might unblock.
|
||||
|
||||
(task-status sched task) -> :runnable | :blocked | :finished | :crashed
|
||||
|
||||
(task-result sched task) -> value | {:error reason}
|
||||
;; After :finished or :crashed.
|
||||
|
||||
(scheduler-step sched) -> :ran | :idle | :all-done
|
||||
;; Run at most gas-per-step instructions of one task. Caller drives the
|
||||
;; loop.
|
||||
|
||||
(scheduler-run sched) -> nil
|
||||
;; Run until :all-done. Equivalent to (until (= :all-done (scheduler-step
|
||||
;; sched))).
|
||||
```
|
||||
|
||||
Notes on the design:
|
||||
- `task-block` with a resume-predicate is the universal blocking primitive.
|
||||
Erlang's `receive` is `(task-block sched (fn () (mailbox-match self pat)))`.
|
||||
Go's `<-ch` is `(task-block sched (fn () (channel-recv-ready ch)))`.
|
||||
- `task-wake` is the optimisation: instead of polling every blocked task
|
||||
every step, the language layer wakes the specific task whose predicate
|
||||
is now likely true. v1 can omit it; performance work later.
|
||||
- `gas-per-step` gives fairness without true preemption. Tasks that don't
|
||||
yield within their gas budget are force-yielded by the CEK loop. (CEK
|
||||
io-suspension already does this for IO; gas budget extends to plain
|
||||
instructions.)
|
||||
- No priority/affinity in v1. Both Erlang and Go default to non-priority
|
||||
scheduling; specialised cases (Erlang's high-priority processes) are
|
||||
language-layer concerns.
|
||||
|
||||
## Build order — phases
|
||||
|
||||
This is a long-running plan paced against Go-on-SX. Phases are not loop-style
|
||||
"one commit per phase" — they're milestone gates.
|
||||
|
||||
### Phase 0 — Erlang shape capture (doc-only) ⬜
|
||||
- Read `lib/erlang/runtime.sx` scheduler code (currently coupled to Erlang
|
||||
vocabulary).
|
||||
- Write a 1-page summary of what's actually a scheduler and what's actually
|
||||
Erlang. Identify the boundary.
|
||||
- **Acceptance:** summary committed to this plan as a new section "Erlang
|
||||
scheduler shape (captured 2026-MM-DD)". No code change.
|
||||
- **Output:** clear-eyed mental model. Without this, we'll merge Erlang's
|
||||
scheduler shape into the kit and pretend it generalises.
|
||||
|
||||
### Phase 1 — Go scheduler independent implementation ⬜
|
||||
- During Go-on-SX, implement `lib/go/sched.sx` from scratch. Do NOT look at
|
||||
Erlang's scheduler while doing this. (Or read it once, then close it.)
|
||||
- Pass Go's channel + goroutine + select conformance tests.
|
||||
- **Acceptance:** Go scheduler green, lib/go/scoreboard.json includes scheduler
|
||||
tests, two-consumer rule now passable.
|
||||
- **Output:** two independent, working implementations of the same idea.
|
||||
|
||||
### Phase 2 — Diff and proposed kit ⬜
|
||||
- Side-by-side diff: Erlang's scheduler vs Go's scheduler. Where do they
|
||||
agree? Where does each have language-specific bookkeeping?
|
||||
- The diff is the kit. Things in *both* go in `lib/guest/scheduler/`; things
|
||||
in only one stay in `lib/erlang/` or `lib/go/`.
|
||||
- Draft `lib/guest/scheduler/api.sx` (signatures only, no body) reflecting the
|
||||
proposed surface.
|
||||
- **Acceptance:** API draft circulated for review; agreement that the surface
|
||||
covers both consumers; no merge yet.
|
||||
|
||||
### Phase 3 — Implement `lib/guest/scheduler/` ⬜
|
||||
- Implement the kit per the agreed API. New file(s) in `lib/guest/scheduler/`.
|
||||
- The kit has its own tests in `lib/guest/scheduler/tests/` — agnostic of any
|
||||
particular language vocabulary.
|
||||
- **Acceptance:** kit tests pass. Erlang and Go conformance scoreboards
|
||||
unchanged (the language implementations still use their own scheduler —
|
||||
we haven't refactored yet).
|
||||
|
||||
### Phase 4 — Refactor Erlang to use the kit ⬜
|
||||
- `lib/erlang/runtime.sx` scheduler logic deleted; replaced with calls into
|
||||
`lib/guest/scheduler/`. Erlang's PID table, mailbox-per-PID, selective
|
||||
receive stay in `lib/erlang/`.
|
||||
- **No-regression gate:** Erlang conformance holds at current pass count
|
||||
(currently 729/729). Hard requirement.
|
||||
- **Acceptance:** Erlang scoreboard unchanged; `lib/erlang/runtime.sx`
|
||||
meaningfully smaller (the scheduler code is gone).
|
||||
|
||||
### Phase 5 — Refactor Go to use the kit ⬜
|
||||
- Same exercise for Go. `lib/go/sched.sx` shrinks to channel/goroutine
|
||||
bookkeeping + delegation.
|
||||
- **No-regression gate:** Go conformance scoreboard at its current pass
|
||||
count.
|
||||
- **Acceptance:** Go scoreboard unchanged; `lib/go/sched.sx` meaningfully
|
||||
smaller.
|
||||
|
||||
### Phase 6 — Documentation + design-diary close ⬜
|
||||
- Document `lib/guest/scheduler/` API in `lib/guest/README.md` (or wherever
|
||||
the lib/guest API index lives).
|
||||
- Capture the chiselling diary: what *almost* went in the kit but ended up
|
||||
language-specific, and why. This is the load-bearing knowledge for the
|
||||
third consumer when it arrives.
|
||||
- **Acceptance:** API documented; diary section added to this plan.
|
||||
|
||||
## Two-language rule — gating
|
||||
|
||||
**The rule is hard.** No code in `lib/guest/scheduler/` lands until BOTH
|
||||
Phase 1 (Go independent) AND Phase 0 (Erlang capture) are complete AND a
|
||||
review confirms the two implementations actually share machinery in a way
|
||||
the kit captures.
|
||||
|
||||
If, during Phase 2 diff, we discover that the agreement is shallow (e.g.,
|
||||
both have a runnable queue but the policies are fundamentally incompatible),
|
||||
the **right outcome is to NOT extract**. Add a "rejected extraction" note to
|
||||
this plan documenting what we learned and close it. That outcome is fine —
|
||||
it tells us the two concurrency models aren't actually sister, which is a
|
||||
real result.
|
||||
|
||||
## Open questions
|
||||
|
||||
- **Preemption.** v1 is cooperative; gas-per-step gives fairness but not
|
||||
hard preemption. Erlang BEAM does true preemption (reduction counting).
|
||||
Go uses async preemption (signal-driven since 1.14). Neither extreme fits
|
||||
cooperatively over CEK. Is gas-per-step + voluntary yield enough? Probably
|
||||
for v1; revisit if a guest needs hard real-time.
|
||||
- **Priority/affinity.** Both Erlang and Go can run without it. Defer.
|
||||
- **Distribution.** Erlang nodes, Go's distributed channels — both are
|
||||
language-specific layers on top of the local scheduler. Out of scope.
|
||||
- **Cancellation.** Go has `context.Context`; Erlang has `exit/2`. Both
|
||||
bottom out at "deliver an exception to a task." Worth modelling? Probably
|
||||
as a kit primitive `(task-cancel sched task reason)` that delivers an
|
||||
exception via CEK exception machinery, language layer wraps it.
|
||||
- **Third consumer.** If/when JS-on-SX gets a proper async/await + Promise
|
||||
scheduler, that'd be a great third consumer to validate the kit didn't
|
||||
over-fit to Erlang+Go.
|
||||
|
||||
## Progress log
|
||||
|
||||
_Newest first. Append one dated entry per milestone landed._
|
||||
|
||||
- 2026-05-26 — Plan drafted. Phase 0 unstarted. Awaiting Go-on-SX to begin
|
||||
Phase 1.
|
||||
287
plans/lib-guest-static-types-bidirectional.md
Normal file
287
plans/lib-guest-static-types-bidirectional.md
Normal file
@@ -0,0 +1,287 @@
|
||||
# lib/guest/static-types-bidirectional — design-diary plan
|
||||
|
||||
Capture the dispatch skeleton of bidirectional type checking
|
||||
(synthesis/checking judgments, context as a value, pluggable subtyping and
|
||||
unification) as `lib/guest/static-types-bidirectional/`, so static-typed
|
||||
guest languages that aren't Hindley-Milner-inferred cost ~300 lines of
|
||||
language-specific rules instead of re-inventing the checker plumbing.
|
||||
|
||||
Reference: `plans/lib-guest.md` (parent — two-language rule, stratification),
|
||||
`lib/guest/hm.sx` (sister module — full Hindley-Milner for inference-heavy
|
||||
languages like Haskell-on-SX), Go-on-SX (planned first consumer), TBD second
|
||||
consumer.
|
||||
|
||||
**Branch:** `architecture`. SX files via `sx-tree` MCP only.
|
||||
|
||||
## Thesis
|
||||
|
||||
`lib/guest/hm.sx` covers languages where the user writes few type annotations
|
||||
and the checker infers the rest globally (Haskell-on-SX, an eventual ML port,
|
||||
a typed-Scheme-with-Damas-Milner). But most modern statically-typed languages
|
||||
in actual production — Go, Rust, Swift, TypeScript, Kotlin, Scala 3, Hack —
|
||||
do **bidirectional checking instead**: declarations carry annotations, locals
|
||||
are inferred from immediate context, return types thread inwards from call
|
||||
sites. This isn't a weaker form of HM; it's a different design that scales
|
||||
better to mutation, subtyping, ad-hoc polymorphism, and gradual typing —
|
||||
none of which HM handles cleanly.
|
||||
|
||||
If `lib/guest/` is going to credibly host the next decade of statically-typed
|
||||
languages, it needs a bidirectional kit alongside `hm.sx`. They're sisters,
|
||||
not rivals.
|
||||
|
||||
**This plan is a design diary, not an implementation queue.** The two-language
|
||||
rule blocks extraction until two consumers exist. Go-on-SX is the first; the
|
||||
second is TBD. Until then, this plan documents what the API surface *should*
|
||||
be based on a single consumer, openly acknowledging that the second consumer
|
||||
will revise it.
|
||||
|
||||
## Current state (2026-05-26)
|
||||
|
||||
- `lib/guest/hm.sx` exists, used by Haskell-on-SX. 180 lines. The HM kit is
|
||||
the sister extraction this plan complements.
|
||||
- No bidirectional kit anywhere in `lib/guest/`.
|
||||
- Go-on-SX does not exist yet. When it does, `lib/go/types.sx` will be the
|
||||
first consumer.
|
||||
- Second consumer is unidentified. Most likely candidates, in order:
|
||||
1. **TypeScript-on-SX** — purely structural, gradual typing, the most-
|
||||
popular bidirectional language alive. Natural pair.
|
||||
2. **Rust-on-SX** — bidirectional with substantial extras (lifetimes,
|
||||
traits, borrow checking). Heavyweight; lifetimes don't go in this kit.
|
||||
3. **Typed Racket subset** — if anyone ports it. Bidirectional + gradual.
|
||||
4. **Hack / Flow / Python-with-types** — same shape.
|
||||
|
||||
**Status: Phase 0 (literature survey).** No code in this plan yet.
|
||||
|
||||
## Why bidirectional, not HM (for the languages that need it)
|
||||
|
||||
Five reasons HM doesn't fit these languages:
|
||||
|
||||
1. **Subtyping.** HM unification requires equality of types; subtyping
|
||||
requires a different judgment (`t ⊑ u`). Go's `interface{}` accepts any
|
||||
concrete type that satisfies it — subtyping, not unification.
|
||||
2. **Mutation.** HM's let-polymorphism interacts pathologically with
|
||||
mutable references (the value restriction). Go, Rust, TS all have
|
||||
first-class mutation and need rules that handle it directly.
|
||||
3. **Annotations as ground truth.** Bidirectional treats declared types as
|
||||
*given*, then propagates them. HM treats every type as a variable to be
|
||||
solved. For languages where annotations are expected, bidirectional is
|
||||
the natural shape.
|
||||
4. **Generics with constraints.** Go's type parameters carry constraints
|
||||
(`type T comparable`); Rust has trait bounds. HM has typeclasses but
|
||||
they're orthogonal to its constraint solver. Bidirectional weaves
|
||||
constraints into the checking rules naturally.
|
||||
5. **Gradual typing.** TS's `any`, Hack's pessimistic mode, Python's
|
||||
`Any` — gradual checking is built on bidirectional's "check or skip"
|
||||
distinction. HM either checks or it doesn't.
|
||||
|
||||
These languages collectively are the majority of new statically-typed code.
|
||||
Hosting them on lib/guest at all requires the bidirectional shape.
|
||||
|
||||
## API surface (proposed — design only, will revise with second consumer)
|
||||
|
||||
```
|
||||
;; --- judgments ---
|
||||
|
||||
(synth ctx expr) -> {:type T} | {:error msg}
|
||||
;; "expr synthesises type T in context ctx."
|
||||
;; Used at function calls (arg types known), let bindings, literals.
|
||||
|
||||
(check ctx expr expected-type) -> :ok | {:error msg}
|
||||
;; "expr checks against expected-type in context ctx."
|
||||
;; Used in function bodies (return type known), arguments (param type known),
|
||||
;; assignments (LHS type known).
|
||||
|
||||
;; --- context ---
|
||||
|
||||
(make-ctx) -> ctx
|
||||
(ctx-extend ctx name type) -> ctx ;; functional update
|
||||
(ctx-lookup ctx name) -> type | nil
|
||||
|
||||
;; --- pluggable rules ---
|
||||
|
||||
(register-synth-rule! kit ast-tag synth-fn) -> nil
|
||||
;; ast-tag: a keyword identifying the AST node shape (eg. :call :let :lit-int)
|
||||
;; synth-fn: (ctx node) -> {:type T} | {:error msg}
|
||||
|
||||
(register-check-rule! kit ast-tag check-fn) -> nil
|
||||
;; check-fn: (ctx node expected-type) -> :ok | {:error msg}
|
||||
|
||||
(register-type-equiv! kit pred) -> nil
|
||||
;; pred: (t1 t2) -> bool. The "are these types compatible" predicate.
|
||||
;; For Go: structural-interface-match-or-equal.
|
||||
;; For TS: structural-equality-with-any-bidirectional-bottom.
|
||||
;; For Rust: nominal equality + trait obligations.
|
||||
|
||||
(register-subtype! kit pred) -> nil
|
||||
;; pred: (sub super) -> bool. Optional; defaults to type-equiv.
|
||||
;; Go has no subtyping between concrete types but interface satisfaction
|
||||
;; is morally subtyping. TS has structural subtyping properly.
|
||||
|
||||
(register-unify! kit unifier) -> nil
|
||||
;; Optional; for type-variable resolution (generics).
|
||||
;; unifier: (t1 t2 subst) -> {:subst s'} | {:error msg}
|
||||
|
||||
;; --- driver ---
|
||||
|
||||
(make-kit) -> kit
|
||||
(check-program kit ctx program) -> {:ok ctx'} | {:error msg path-to-error}
|
||||
```
|
||||
|
||||
Design notes:
|
||||
- **The kit dispatches on AST tags**, which is what makes it pluggable. Each
|
||||
language registers rules for its node types. There's no hardcoded set of
|
||||
expression shapes in the kit.
|
||||
- **Synth and check are mutually recursive.** Inside a synth-rule for `call`,
|
||||
the rule synthesises the function's type, then `check`s each argument
|
||||
against the corresponding parameter type. Inside a check-rule for `lambda`,
|
||||
the rule pulls argument types from the expected function type and
|
||||
`synth`s the body. This pingponging is the bidirectional core.
|
||||
- **Pluggable type-equiv + subtype + unify** is the three-knob shape. Pierce
|
||||
& Turner ("Local Type Inference") and Dunfield & Krishnaswami ("Sound and
|
||||
Complete Bidirectional Typechecking") both factor it this way.
|
||||
- **No type variables in the core API.** Generics handling is a kit
|
||||
*extension*: when a language registers a `unify` predicate, the kit
|
||||
threads a substitution through synth/check. Languages without generics
|
||||
(early Go) leave it null.
|
||||
- **Errors carry a path.** `{:error msg path}` where path is a list of AST
|
||||
tags leading to the failure. Good error messages are why bidirectional is
|
||||
practical; the kit must support them.
|
||||
|
||||
## What's NOT in the kit (language-layer concerns)
|
||||
|
||||
Per the chiselling discipline, the kit is the dispatch skeleton; rules stay
|
||||
in the language. Specifically:
|
||||
|
||||
- **The literal type table.** Go's `42` is `untyped int` until contextualised;
|
||||
TS's `42` is the literal type `42`. Each language ships its own.
|
||||
- **Specific subtyping rules.** Go's interface satisfaction is recursive
|
||||
structural matching against method sets. TS's depends on object property
|
||||
satisfaction. Each language ships its own predicate.
|
||||
- **Generics constraint solving.** Go's type-set-based constraints, Rust's
|
||||
trait bounds, TS's conditional types — each is non-trivial and language-
|
||||
specific. The kit threads a substitution; the language defines what's in
|
||||
it.
|
||||
- **Effects, lifetimes, ownership.** Rust's borrow checker is not a type
|
||||
checker in the bidirectional-kit sense — it's a separate dataflow pass.
|
||||
Out of scope.
|
||||
- **Gradual fallback.** TS's `any` lets unchecked code coexist with checked
|
||||
code. The kit supports this via "check returns :ok on a sentinel any-type"
|
||||
but the sentinel is registered by the language.
|
||||
|
||||
## Build order — phases
|
||||
|
||||
### Phase 0 — Literature survey + Go's type system specifics ⬜
|
||||
- Read: Pierce & Turner "Local Type Inference" (2000); Dunfield & Krishnaswami
|
||||
"Sound and Complete Bidirectional Typechecking for Higher-Rank Polymorphism"
|
||||
(2013, 2019 revision); the Go language spec § "Types" + "Expressions".
|
||||
- Survey how Rust / TS / Kotlin / Scala 3 implement bidirectional in practice
|
||||
(their compilers are open source). Note where they diverge.
|
||||
- Output: a short summary section "Bidirectional design space (captured
|
||||
2026-MM-DD)" appended to this plan. Specifically: list every place
|
||||
language implementations diverge, so we can predict which divergences will
|
||||
show up between Go and the second consumer.
|
||||
- **Acceptance:** survey committed to this plan. No code.
|
||||
|
||||
### Phase 1 — Go independent implementation ⬜
|
||||
- During Go-on-SX, implement `lib/go/types.sx` from scratch. Do not write
|
||||
with extraction in mind — write the simplest Go-specific bidirectional
|
||||
checker.
|
||||
- Hit Go's distinctive type-system features: untyped constants, interface
|
||||
satisfaction (structural), generics (Go 1.18 type parameters with type-set
|
||||
constraints — defer this if scope explodes).
|
||||
- Pass Go's type-checker conformance tests.
|
||||
- **Acceptance:** Go conformance scoreboard includes type-checker tests, all
|
||||
passing.
|
||||
- **Output:** one consumer. Two-language rule still not met; no extraction.
|
||||
|
||||
### Phase 2 — Pick + start the second consumer ⬜
|
||||
- Decide between TS, Rust-subset, or typed-Scheme-subset. Recommendation:
|
||||
**TypeScript** — most-different from Go (gradual, structural everywhere),
|
||||
testing the kit's range maximally. Rust's lifetime/borrow machinery isn't
|
||||
part of this kit, so a Rust port wouldn't actually exercise the kit very
|
||||
hard.
|
||||
- Implement just enough of the second language to type-check a non-trivial
|
||||
function. Don't port the whole language; port the type checker.
|
||||
- **Acceptance:** second consumer's type checker green on its small slice.
|
||||
|
||||
### Phase 3 — Diff and proposed kit ⬜
|
||||
- Side-by-side: Go's checker vs the second consumer's checker. Where do they
|
||||
agree (the kit). Where does each diverge (the language).
|
||||
- Draft `lib/guest/static-types-bidirectional/api.sx` (signatures only).
|
||||
- Compare against the API sketch in this plan. The API WILL change at this
|
||||
step; that's the whole point of having two consumers.
|
||||
- **Acceptance:** revised API committed to this plan; agreement that both
|
||||
consumers can adopt it.
|
||||
|
||||
### Phase 4 — Implement the kit ⬜
|
||||
- `lib/guest/static-types-bidirectional/` with the agreed API. Kit tests in
|
||||
`lib/guest/static-types-bidirectional/tests/` — using a minimal "toy"
|
||||
language (synth-rule for `:int`, check-rule for `:lambda`) to verify the
|
||||
dispatch skeleton works.
|
||||
- **Acceptance:** kit tests pass; both consumers' scoreboards still green
|
||||
with their own implementations.
|
||||
|
||||
### Phase 5 — Refactor both consumers to use the kit ⬜
|
||||
- Go: `lib/go/types.sx` becomes a thin layer over the kit — registers Go's
|
||||
synth/check/equiv rules, calls `check-program`. Lifecycle code shrinks.
|
||||
- Second consumer: same exercise.
|
||||
- **No-regression gate:** both consumers' conformance scoreboards unchanged.
|
||||
- **Acceptance:** both `lib/<lang>/types.sx` files meaningfully smaller; kit
|
||||
is doing real work.
|
||||
|
||||
### Phase 6 — Documentation + chiselling diary ⬜
|
||||
- Document the API in lib/guest's README index.
|
||||
- Diary section in this plan: what we considered putting in the kit but
|
||||
ended up keeping language-specific, and why.
|
||||
- **Acceptance:** documentation present; diary captured.
|
||||
|
||||
## Two-language rule — gating
|
||||
|
||||
Same as `lib-guest-scheduler.md`. The kit does not exist until both consumers
|
||||
independently work AND we've reviewed the diff AND we believe the shared
|
||||
skeleton is real. Rejected-extraction is a valid outcome.
|
||||
|
||||
## Relationship to `lib/guest/hm.sx`
|
||||
|
||||
Sister modules, not rivals. Some languages will use HM (full inference,
|
||||
let-polymorphism); some will use bidirectional (annotation-driven, subtyping-
|
||||
friendly). Some might use both — Scala-on-SX, hypothetically, has local-type-
|
||||
inference in expressions and global-HM-style constraint solving in implicit
|
||||
resolution. The kit boundaries are:
|
||||
|
||||
- `hm.sx` — unification-based, whole-expression inference. Damas-Milner core.
|
||||
Best for: ML family, Haskell, OCaml subset, Standard ML.
|
||||
- `static-types-bidirectional/` — synth/check judgments, pluggable equiv +
|
||||
subtype. Best for: Go, Rust, TS, Kotlin, Swift, Scala 3, Hack.
|
||||
|
||||
A language can call into both: bidirectional for the surface, HM-style
|
||||
unification inside generics resolution. That's actually how Scala 3 works.
|
||||
The kits compose; design accordingly.
|
||||
|
||||
## Open questions
|
||||
|
||||
- **Variance.** Go has none; TS has covariant/contravariant/bivariant; Rust
|
||||
has variance markers per type parameter. Does the kit need a variance
|
||||
predicate as a fourth pluggable knob? Probably yes, but defer until the
|
||||
second consumer forces the question.
|
||||
- **Effect tracking.** Some bidirectional checkers (Koka, Eff, certain
|
||||
capability-effect TS variants) track effects in types. Out of scope for
|
||||
v1; the kit must not actively prevent it though.
|
||||
- **Refinement types.** TS has narrowing (`typeof x === "string"` refines
|
||||
`x` to `string`); Hack and Flow are similar. These layer above the kit
|
||||
(the kit's `check` returns a refined context as part of `:ok`). Sketch
|
||||
this in Phase 3 if TS is the second consumer.
|
||||
- **Error recovery.** Real-world type checkers don't halt on first error;
|
||||
they recover and continue to surface as many errors as possible. The kit
|
||||
needs an error-accumulation mode. Design it in Phase 4.
|
||||
- **Performance.** For toy languages, naive synth/check is fine. For Go-
|
||||
sized programs, the checker has to be memoised on synthesised types of
|
||||
subexpressions. Not a v1 concern; flag if it bites.
|
||||
|
||||
## Progress log
|
||||
|
||||
_Newest first. Append one dated entry per milestone landed._
|
||||
|
||||
- 2026-05-26 — Plan drafted as design diary. Phase 0 unstarted. Gated on
|
||||
Go-on-SX (first consumer) and a TBD second consumer (recommendation:
|
||||
TypeScript). No code yet — kit cannot exist before two consumers do.
|
||||
Reference in New Issue
Block a user