diff --git a/lib/erlang/runtime.sx b/lib/erlang/runtime.sx new file mode 100644 index 00000000..a7d81938 --- /dev/null +++ b/lib/erlang/runtime.sx @@ -0,0 +1,230 @@ +;; lib/erlang/runtime.sx — Erlang BIFs and stdlib wrappers on SX primitives +;; +;; Provides Erlang-idiomatic wrappers. Thin where spec primitives match; +;; inline where Erlang semantics differ (e.g. rem sign, integer division). +;; +;; Primitives used from spec: +;; integer?/float? (Phase 2) +;; remainder/quotient (Phase 2 / Phase 15) +;; bitwise-and/or/xor/not (Phase 7) +;; arithmetic-shift (Phase 7) +;; make-set/set-add!/etc (Phase 18) +;; make-regexp/regexp-match/etc (Phase 20) +;; gcd (Phase 15) + +;; --------------------------------------------------------------------------- +;; 1. Numeric tower — type predicates + conversions +;; --------------------------------------------------------------------------- + +(define er-is-integer? integer?) +(define er-is-float? float?) +(define (er-is-number? x) (or (integer? x) (float? x))) +(define (er-is-atom? x) (= (type-of x) "symbol")) +(define er-is-list? list?) +(define er-is-binary? bytevector?) + +;; Erlang float/1 coerces an integer to float +(define (er-float x) (* 1 x)) + +;; Erlang trunc/1 — truncate toward zero +(define er-trunc truncate) + +;; Erlang round/1 — round to nearest integer +(define er-round round) + +;; Erlang abs/1 +(define er-abs abs) + +;; Erlang max/min (BIFs in OTP 26) +(define (er-max a b) (if (>= a b) a b)) +(define (er-min a b) (if (<= a b) a b)) + +;; --------------------------------------------------------------------------- +;; 2. Integer arithmetic — div + rem (Erlang semantics) +;; --------------------------------------------------------------------------- + +;; Erlang div: integer division truncating toward zero +(define er-div quotient) + +;; Erlang rem: remainder with sign of dividend (matches remainder primitive) +(define er-rem remainder) + +;; Erlang gcd (non-standard BIF but useful) +(define er-gcd gcd) + +;; --------------------------------------------------------------------------- +;; 3. Bitwise ops — band / bor / bxor / bnot / bsl / bsr +;; --------------------------------------------------------------------------- + +(define er-band bitwise-and) +(define er-bor bitwise-or) +(define er-bxor bitwise-xor) +(define er-bnot bitwise-not) + +;; bsl: bit shift left by N positions +(define (er-bsl x n) (arithmetic-shift x n)) + +;; bsr: bit shift right by N positions +(define (er-bsr x n) (arithmetic-shift x (- 0 n))) + +;; --------------------------------------------------------------------------- +;; 4. Sets module — thin wrappers matching Erlang sets API +;; --------------------------------------------------------------------------- + +(define er-sets-new make-set) +(define er-sets-add-element set-add!) +(define er-sets-is-element set-member?) +(define er-sets-del-element set-remove!) +(define er-sets-union set-union) +(define er-sets-intersection set-intersection) +(define er-sets-subtract set-difference) +(define er-sets-to-list set->list) +(define er-sets-from-list list->set) +(define (er-sets-size s) (len (set->list s))) +(define (er-sets-is-set? x) (set? x)) + +;; --------------------------------------------------------------------------- +;; 5. Regexp — re module wrappers +;; --------------------------------------------------------------------------- + +;; er-re-run: returns match dict or nil (no match) +(define + (er-re-run subject pattern) + (regexp-match (make-regexp pattern) subject)) + +;; er-re-replace: replace first match +(define + (er-re-replace subject pattern replacement) + (regexp-replace (make-regexp pattern) subject replacement)) + +;; er-re-replace-all: global replace +(define + (er-re-replace-all subject pattern replacement) + (regexp-replace-all (make-regexp pattern) subject replacement)) + +;; er-re-match-groups: extract capture groups from a match result +(define (er-re-match-groups m) (if (= m nil) nil (get m :groups))) + +;; er-re-split: split string on regexp delimiter +(define + (er-re-split subject pattern) + (let + ((re (make-regexp pattern)) + (ms (regexp-match-all (make-regexp pattern) subject))) + (if + (= (len ms) 0) + (list subject) + (letrec + ((go (fn (matches pos acc) (if (= (len matches) 0) (append acc (list (substring subject pos (len subject)))) (let ((m (first matches)) (start (get (first matches) :start)) (end (get (first matches) :end))) (go (rest matches) end (append acc (list (substring subject pos start))))))))) + (go ms 0 (list)))))) + +;; --------------------------------------------------------------------------- +;; 6. List BIFs — hd/tl/length + lists module +;; --------------------------------------------------------------------------- + +(define (er-hd lst) (first lst)) +(define (er-tl lst) (rest lst)) +(define (er-length lst) (len lst)) + +;; lists:member/2 +(define + (er-lists-member elem lst) + (cond + ((= (len lst) 0) false) + ((= elem (first lst)) true) + (else (er-lists-member elem (rest lst))))) + +;; lists:reverse/1 +(define er-lists-reverse reverse) + +;; lists:append/2 +(define er-lists-append append) + +;; lists:flatten/1 +(define + (er-lists-flatten lst) + (cond + ((= (len lst) 0) (list)) + ((list? (first lst)) + (append (er-lists-flatten (first lst)) (er-lists-flatten (rest lst)))) + (else (cons (first lst) (er-lists-flatten (rest lst)))))) + +;; lists:nth/2 — 1-indexed +(define (er-lists-nth n lst) (nth lst (- n 1))) + +;; lists:map/2 +(define er-lists-map map) + +;; lists:filter/2 +(define er-lists-filter filter) + +;; lists:foldl/3 — (Fun, Acc0, List) +(define + (er-lists-foldl f acc lst) + (if + (= (len lst) 0) + acc + (er-lists-foldl f (f (first lst) acc) (rest lst)))) + +;; lists:foldr/3 +(define + (er-lists-foldr f acc lst) + (if + (= (len lst) 0) + acc + (f (first lst) (er-lists-foldr f acc (rest lst))))) + +;; lists:zip/2 +(define + (er-lists-zip a b) + (if + (or (= (len a) 0) (= (len b) 0)) + (list) + (cons (list (first a) (first b)) (er-lists-zip (rest a) (rest b))))) + +;; lists:seq/2 — generate integer range (1-indexed like Erlang) +(define + (er-lists-seq from to) + (if + (> from to) + (list) + (cons from (er-lists-seq (+ from 1) to)))) + +;; --------------------------------------------------------------------------- +;; 7. Type conversion BIFs +;; --------------------------------------------------------------------------- + +;; atom_to_list/1 — convert atom (symbol) to its name string +(define (er-atom-to-list a) (symbol->string a)) + +;; list_to_atom/1 — convert string to atom (symbol) +(define (er-list-to-atom s) (make-symbol s)) + +;; integer_to_list/1 +(define (er-integer-to-list n) (str n)) + +;; list_to_integer/1 +(define (er-list-to-integer s) (truncate (parse-number s))) + +;; float_to_list/1 +(define (er-float-to-list f) (str f)) + +;; list_to_float/1 +(define (er-list-to-float s) (* 1 (parse-number s))) + +;; integer_to_list/2 — with radix (e.g. 16 for hex) +(define (er-integer-to-list-radix n radix) (number->string n radix)) + +;; --------------------------------------------------------------------------- +;; 8. ok/error tuple helpers — Erlang idiom {ok, Val} / {error, Reason} +;; --------------------------------------------------------------------------- + +(define (er-ok val) (list "ok" val)) +(define (er-error reason) (list "error" reason)) +(define + (er-is-ok? t) + (and (list? t) (= (len t) 2) (= (first t) "ok"))) +(define + (er-is-error? t) + (and (list? t) (= (len t) 2) (= (first t) "error"))) +(define (er-unwrap t) (nth t 1)) diff --git a/lib/erlang/test.sh b/lib/erlang/test.sh new file mode 100755 index 00000000..3149cbd0 --- /dev/null +++ b/lib/erlang/test.sh @@ -0,0 +1,260 @@ +#!/usr/bin/env bash +# lib/erlang/test.sh — smoke-test the Erlang runtime layer. +# Uses sx_server.exe epoch protocol. +# +# Usage: +# bash lib/erlang/test.sh +# bash lib/erlang/test.sh -v + +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. Run: cd hosts/ocaml && dune build" + exit 1 +fi + +VERBOSE="${1:-}" +PASS=0; FAIL=0; ERRORS="" +TMPFILE=$(mktemp); trap "rm -f $TMPFILE" EXIT + +cat > "$TMPFILE" << 'EPOCHS' +(epoch 1) +(load "lib/erlang/runtime.sx") + +;; --- Numeric tower --- +(epoch 10) +(eval "(er-is-integer? 42)") +(epoch 11) +(eval "(er-is-integer? 3.14)") +(epoch 12) +(eval "(er-is-float? 3.14)") +(epoch 13) +(eval "(er-is-float? 42)") +(epoch 14) +(eval "(er-is-number? 42)") +(epoch 15) +(eval "(er-is-number? 3.14)") +(epoch 16) +(eval "(er-float 5)") +(epoch 17) +(eval "(er-trunc 3.9)") +(epoch 18) +(eval "(er-round 3.5)") +(epoch 19) +(eval "(er-abs -7)") +(epoch 20) +(eval "(er-max 3 7)") +(epoch 21) +(eval "(er-min 3 7)") + +;; --- div + rem --- +(epoch 30) +(eval "(er-div 10 3)") +(epoch 31) +(eval "(er-div -10 3)") +(epoch 32) +(eval "(er-rem 10 3)") +(epoch 33) +(eval "(er-rem -10 3)") +(epoch 34) +(eval "(er-gcd 12 8)") + +;; --- Bitwise --- +(epoch 40) +(eval "(er-band 12 10)") +(epoch 41) +(eval "(er-bor 12 10)") +(epoch 42) +(eval "(er-bxor 12 10)") +(epoch 43) +(eval "(er-bnot 0)") +(epoch 44) +(eval "(er-bsl 1 4)") +(epoch 45) +(eval "(er-bsr 16 2)") + +;; --- Sets --- +(epoch 50) +(eval "(er-sets-is-set? (er-sets-new))") +(epoch 51) +(eval "(let ((s (er-sets-new))) (do (er-sets-add-element s 1) (er-sets-is-element s 1)))") +(epoch 52) +(eval "(er-sets-is-element (er-sets-new) 42)") +(epoch 53) +(eval "(er-sets-is-element (er-sets-from-list (list 1 2 3)) 2)") +(epoch 54) +(eval "(er-sets-size (er-sets-from-list (list 1 2 3)))") +(epoch 55) +(eval "(len (er-sets-to-list (er-sets-from-list (list 1 2 3))))") + +;; --- Regexp --- +(epoch 60) +(eval "(not (= (er-re-run \"hello\" \"ll\") nil))") +(epoch 61) +(eval "(= (er-re-run \"hello\" \"xyz\") nil)") +(epoch 62) +(eval "(get (er-re-run \"hello\" \"ll\") :match)") +(epoch 63) +(eval "(er-re-replace \"hello\" \"l\" \"r\")") +(epoch 64) +(eval "(er-re-replace-all \"hello\" \"l\" \"r\")") +(epoch 65) +(eval "(er-re-match-groups (er-re-run \"hello world\" \"(\\w+)\\s+(\\w+)\"))") +(epoch 66) +(eval "(len (er-re-split \"a,b,c\" \",\"))") + +;; --- List BIFs --- +(epoch 70) +(eval "(er-hd (list 1 2 3))") +(epoch 71) +(eval "(er-tl (list 1 2 3))") +(epoch 72) +(eval "(er-length (list 1 2 3))") +(epoch 73) +(eval "(er-lists-member 2 (list 1 2 3))") +(epoch 74) +(eval "(er-lists-member 9 (list 1 2 3))") +(epoch 75) +(eval "(er-lists-reverse (list 1 2 3))") +(epoch 76) +(eval "(er-lists-nth 2 (list 10 20 30))") +(epoch 77) +(eval "(er-lists-foldl + 0 (list 1 2 3 4 5))") +(epoch 78) +(eval "(er-lists-seq 1 5)") +(epoch 79) +(eval "(er-lists-flatten (list 1 (list 2 3) (list 4 (list 5))))") + +;; --- Type conversions --- +(epoch 80) +(eval "(er-integer-to-list 42)") +(epoch 81) +(eval "(er-list-to-integer \"42\")") +(epoch 82) +(eval "(er-integer-to-list-radix 255 16)") +(epoch 83) +(eval "(er-atom-to-list (make-symbol \"hello\"))") +(epoch 84) +(eval "(= (type-of (er-list-to-atom \"foo\")) \"symbol\")") + +;; --- ok/error tuples --- +(epoch 90) +(eval "(er-is-ok? (er-ok 42))") +(epoch 91) +(eval "(er-is-error? (er-error \"reason\"))") +(epoch 92) +(eval "(er-unwrap (er-ok 42))") +(epoch 93) +(eval "(er-is-ok? (er-error \"bad\"))") + +EPOCHS + +OUTPUT=$(timeout 30 "$SX_SERVER" < "$TMPFILE" 2>/dev/null) + +check() { + local epoch="$1" desc="$2" expected="$3" + local actual + actual=$(echo "$OUTPUT" | grep -A1 "^(ok-len $epoch " | tail -1 || true) + if echo "$actual" | grep -q "^(ok-len"; then actual=""; fi + if [ -z "$actual" ]; then + actual=$(echo "$OUTPUT" | grep "^(ok $epoch " | head -1 || true) + fi + if [ -z "$actual" ]; then + actual=$(echo "$OUTPUT" | grep "^(error $epoch " | head -1 || true) + fi + [ -z "$actual" ] && actual="" + + if echo "$actual" | grep -qF -- "$expected"; then + PASS=$((PASS+1)) + [ "$VERBOSE" = "-v" ] && echo " ok $desc" + else + FAIL=$((FAIL+1)) + ERRORS+=" FAIL [$desc] (epoch $epoch) expected: $expected | actual: $actual +" + fi +} + +# Numeric tower +check 10 "is-integer? 42" "true" +check 11 "is-integer? float" "false" +check 12 "is-float? 3.14" "true" +check 13 "is-float? int" "false" +check 14 "is-number? int" "true" +check 15 "is-number? float" "true" +check 16 "float 5" "5" +check 17 "trunc 3.9" "3" +check 18 "round 3.5" "4" +check 19 "abs -7" "7" +check 20 "max 3 7" "7" +check 21 "min 3 7" "3" + +# div + rem +check 30 "div 10 3" "3" +check 31 "div -10 3" "-3" +check 32 "rem 10 3" "1" +check 33 "rem -10 3" "-1" +check 34 "gcd 12 8" "4" + +# Bitwise +check 40 "band 12 10" "8" +check 41 "bor 12 10" "14" +check 42 "bxor 12 10" "6" +check 43 "bnot 0" "-1" +check 44 "bsl 1 4" "16" +check 45 "bsr 16 2" "4" + +# Sets +check 50 "sets-new is-set?" "true" +check 51 "sets add+member" "true" +check 52 "member empty" "false" +check 53 "from-list member" "true" +check 54 "sets-size" "3" +check 55 "sets-to-list len" "3" + +# Regexp +check 60 "re-run match" "true" +check 61 "re-run no match" "true" +check 62 "re-run match text" '"ll"' +check 63 "re-replace first" '"herlo"' +check 64 "re-replace-all" '"herro"' +check 65 "re-match-groups" '"hello"' +check 66 "re-split count" "3" + +# List BIFs +check 70 "hd" "1" +check 71 "tl" "(2 3)" +check 72 "length" "3" +check 73 "member hit" "true" +check 74 "member miss" "false" +check 75 "reverse" "(3 2 1)" +check 76 "nth 2" "20" +check 77 "foldl sum" "15" +check 78 "seq 1..5" "(1 2 3 4 5)" +check 79 "flatten" "(1 2 3 4 5)" + +# Type conversions +check 80 "integer-to-list" '"42"' +check 81 "list-to-integer" "42" +check 82 "integer-to-list hex" '"ff"' +check 83 "atom-to-list" '"hello"' +check 84 "list-to-atom" "true" + +# ok/error +check 90 "ok? ok-tuple" "true" +check 91 "error? error-tuple" "true" +check 92 "unwrap ok" "42" +check 93 "ok? error-tuple" "false" + +TOTAL=$((PASS+FAIL)) +if [ $FAIL -eq 0 ]; then + echo "ok $PASS/$TOTAL lib/erlang tests passed" +else + echo "FAIL $PASS/$TOTAL passed, $FAIL failed:" + echo "$ERRORS" +fi +[ $FAIL -eq 0 ]