erlang: sequential eval (+54 tests)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
This commit is contained in:
128
lib/erlang/tests/eval.sx
Normal file
128
lib/erlang/tests/eval.sx
Normal file
@@ -0,0 +1,128 @@
|
||||
;; Erlang evaluator tests — sequential expressions.
|
||||
|
||||
(define er-eval-test-count 0)
|
||||
(define er-eval-test-pass 0)
|
||||
(define er-eval-test-fails (list))
|
||||
|
||||
(define
|
||||
eev-deep=
|
||||
(fn
|
||||
(a b)
|
||||
(cond
|
||||
(and (= (type-of a) "dict") (= (type-of b) "dict"))
|
||||
(let
|
||||
((ka (sort (keys a))) (kb (sort (keys b))))
|
||||
(and (= ka kb) (every? (fn (k) (eev-deep= (get a k) (get b k))) ka)))
|
||||
(and (= (type-of a) "list") (= (type-of b) "list"))
|
||||
(and
|
||||
(= (len a) (len b))
|
||||
(every? (fn (i) (eev-deep= (nth a i) (nth b i))) (range 0 (len a))))
|
||||
:else (= a b))))
|
||||
|
||||
(define
|
||||
er-eval-test
|
||||
(fn
|
||||
(name actual expected)
|
||||
(set! er-eval-test-count (+ er-eval-test-count 1))
|
||||
(if
|
||||
(eev-deep= actual expected)
|
||||
(set! er-eval-test-pass (+ er-eval-test-pass 1))
|
||||
(append! er-eval-test-fails {:actual actual :expected expected :name name}))))
|
||||
|
||||
(define ev erlang-eval-ast)
|
||||
(define nm (fn (v) (get v :name)))
|
||||
|
||||
;; ── literals ──────────────────────────────────────────────────────
|
||||
(er-eval-test "int" (ev "42") 42)
|
||||
(er-eval-test "zero" (ev "0") 0)
|
||||
(er-eval-test "float" (ev "3.14") 3.14)
|
||||
(er-eval-test "string" (ev "\"hi\"") "hi")
|
||||
(er-eval-test "atom" (nm (ev "ok")) "ok")
|
||||
(er-eval-test "atom true" (nm (ev "true")) "true")
|
||||
(er-eval-test "atom false" (nm (ev "false")) "false")
|
||||
|
||||
;; ── arithmetic ────────────────────────────────────────────────────
|
||||
(er-eval-test "add" (ev "1 + 2") 3)
|
||||
(er-eval-test "sub" (ev "5 - 3") 2)
|
||||
(er-eval-test "mul" (ev "4 * 3") 12)
|
||||
(er-eval-test "div-real" (ev "10 / 4") 2.5)
|
||||
(er-eval-test "div-int" (ev "10 div 3") 3)
|
||||
(er-eval-test "rem" (ev "10 rem 3") 1)
|
||||
(er-eval-test "div-neg" (ev "-10 div 3") -3)
|
||||
(er-eval-test "precedence" (ev "1 + 2 * 3") 7)
|
||||
(er-eval-test "parens" (ev "(1 + 2) * 3") 9)
|
||||
(er-eval-test "unary-neg" (ev "-(1 + 2)") -3)
|
||||
(er-eval-test "unary-neg int" (ev "-7") -7)
|
||||
|
||||
;; ── comparison ────────────────────────────────────────────────────
|
||||
(er-eval-test "lt true" (nm (ev "1 < 2")) "true")
|
||||
(er-eval-test "gt false" (nm (ev "1 > 2")) "false")
|
||||
(er-eval-test "le equal" (nm (ev "2 =< 2")) "true")
|
||||
(er-eval-test "ge equal" (nm (ev "2 >= 2")) "true")
|
||||
(er-eval-test "eq" (nm (ev "2 == 2")) "true")
|
||||
(er-eval-test "neq" (nm (ev "1 /= 2")) "true")
|
||||
(er-eval-test "exact-eq same" (nm (ev "1 =:= 1")) "true")
|
||||
(er-eval-test "exact-neq int" (nm (ev "1 =:= 2")) "false")
|
||||
(er-eval-test "=/= true" (nm (ev "1 =/= 2")) "true")
|
||||
(er-eval-test "atom-eq" (nm (ev "ok == ok")) "true")
|
||||
(er-eval-test "atom-neq" (nm (ev "ok == error")) "false")
|
||||
|
||||
;; ── logical ───────────────────────────────────────────────────────
|
||||
(er-eval-test "and tt" (nm (ev "true and true")) "true")
|
||||
(er-eval-test "and tf" (nm (ev "true and false")) "false")
|
||||
(er-eval-test "or tf" (nm (ev "true or false")) "true")
|
||||
(er-eval-test
|
||||
"andalso short"
|
||||
(nm (ev "false andalso Neverref"))
|
||||
"false")
|
||||
(er-eval-test
|
||||
"orelse short"
|
||||
(nm (ev "true orelse Neverref"))
|
||||
"true")
|
||||
(er-eval-test "not true" (nm (ev "not true")) "false")
|
||||
(er-eval-test "not false" (nm (ev "not false")) "true")
|
||||
|
||||
;; ── tuples & lists ────────────────────────────────────────────────
|
||||
(er-eval-test "tuple tag" (get (ev "{1, 2, 3}") :tag) "tuple")
|
||||
(er-eval-test "tuple len" (len (get (ev "{1, 2, 3}") :elements)) 3)
|
||||
(er-eval-test "tuple elem" (nth (get (ev "{10, 20}") :elements) 1) 20)
|
||||
(er-eval-test "empty tuple" (len (get (ev "{}") :elements)) 0)
|
||||
(er-eval-test "nested tuple"
|
||||
(nm (nth (get (ev "{ok, error}") :elements) 0)) "ok")
|
||||
(er-eval-test "nil list" (get (ev "[]") :tag) "nil")
|
||||
(er-eval-test "list head" (get (ev "[1, 2, 3]") :head) 1)
|
||||
(er-eval-test
|
||||
"list tail tail head"
|
||||
(get (get (get (ev "[1, 2, 3]") :tail) :tail) :head)
|
||||
3)
|
||||
|
||||
;; ── list ops ──────────────────────────────────────────────────────
|
||||
(er-eval-test "++ head" (get (ev "[1, 2] ++ [3]") :head) 1)
|
||||
(er-eval-test "++ last"
|
||||
(get (get (get (ev "[1, 2] ++ [3]") :tail) :tail) :head) 3)
|
||||
|
||||
;; ── block ─────────────────────────────────────────────────────────
|
||||
(er-eval-test "block last wins" (ev "begin 1, 2, 3 end") 3)
|
||||
(er-eval-test "bare body" (ev "1, 2, 99") 99)
|
||||
|
||||
;; ── match + var ───────────────────────────────────────────────────
|
||||
(er-eval-test "match bind-and-use" (ev "X = 5, X + 1") 6)
|
||||
(er-eval-test "match sequential" (ev "X = 1, Y = 2, X + Y") 3)
|
||||
(er-eval-test
|
||||
"rebind equal ok"
|
||||
(ev "X = 5, X = 5, X") 5)
|
||||
|
||||
;; ── if ────────────────────────────────────────────────────────────
|
||||
(er-eval-test "if picks first" (ev "if true -> 1; true -> 2 end") 1)
|
||||
(er-eval-test
|
||||
"if picks second"
|
||||
(nm (ev "if 1 > 2 -> bad; true -> good end"))
|
||||
"good")
|
||||
(er-eval-test
|
||||
"if with guard"
|
||||
(ev "X = 5, if X > 0 -> 1; true -> 0 end")
|
||||
1)
|
||||
|
||||
(define
|
||||
er-eval-test-summary
|
||||
(str "eval " er-eval-test-pass "/" er-eval-test-count))
|
||||
338
lib/erlang/transpile.sx
Normal file
338
lib/erlang/transpile.sx
Normal file
@@ -0,0 +1,338 @@
|
||||
;; Erlang sequential evaluator — tree-walking interpreter over the
|
||||
;; parser AST. Phase 2 of plans/erlang-on-sx.md.
|
||||
;;
|
||||
;; Entry points:
|
||||
;; (erlang-eval-ast SRC) -- parse body, eval, return last value
|
||||
;; (er-eval-expr NODE ENV) -- evaluate one AST node
|
||||
;; (er-eval-body NODES ENV) -- evaluate a comma-sequence, return last
|
||||
;;
|
||||
;; Runtime values:
|
||||
;; integers / floats -> SX number
|
||||
;; atoms -> {:tag "atom" :name <string>}
|
||||
;; booleans -> atoms 'true' / 'false'
|
||||
;; strings -> SX string (char-list semantics deferred)
|
||||
;; empty list -> {:tag "nil"}
|
||||
;; cons cell -> {:tag "cons" :head V :tail V}
|
||||
;; tuple -> {:tag "tuple" :elements (list V ...)}
|
||||
;;
|
||||
;; Environment: mutable dict from variable name (string) to value.
|
||||
|
||||
;; ── value constructors / predicates ────────────────────────────────
|
||||
(define er-mk-atom (fn (name) {:name name :tag "atom"}))
|
||||
(define er-atom-true (er-mk-atom "true"))
|
||||
(define er-atom-false (er-mk-atom "false"))
|
||||
(define er-mk-nil (fn () {:tag "nil"}))
|
||||
(define er-mk-cons (fn (h t) {:tag "cons" :head h :tail t}))
|
||||
(define er-mk-tuple (fn (elems) {:tag "tuple" :elements elems}))
|
||||
(define er-bool (fn (b) (if b er-atom-true er-atom-false)))
|
||||
|
||||
(define
|
||||
er-is-tagged?
|
||||
(fn (v tag) (and (= (type-of v) "dict") (= (get v :tag) tag))))
|
||||
(define er-atom? (fn (v) (er-is-tagged? v "atom")))
|
||||
(define er-nil? (fn (v) (er-is-tagged? v "nil")))
|
||||
(define er-cons? (fn (v) (er-is-tagged? v "cons")))
|
||||
(define er-tuple? (fn (v) (er-is-tagged? v "tuple")))
|
||||
|
||||
(define
|
||||
er-is-atom-named?
|
||||
(fn (v name) (and (er-atom? v) (= (get v :name) name))))
|
||||
(define er-truthy? (fn (v) (er-is-atom-named? v "true")))
|
||||
|
||||
;; ── environment ───────────────────────────────────────────────────
|
||||
(define er-env-new (fn () {}))
|
||||
|
||||
(define
|
||||
er-env-lookup
|
||||
(fn
|
||||
(env name)
|
||||
(if
|
||||
(dict-has? env name)
|
||||
(get env name)
|
||||
(error (str "Erlang: unbound variable '" name "'")))))
|
||||
|
||||
(define er-env-bind! (fn (env name val) (dict-set! env name val)))
|
||||
|
||||
;; ── entry ─────────────────────────────────────────────────────────
|
||||
(define
|
||||
erlang-eval-ast
|
||||
(fn
|
||||
(src)
|
||||
(let
|
||||
((st (er-state-make (er-tokenize src))))
|
||||
(let
|
||||
((body (er-parse-body st)) (env (er-env-new)))
|
||||
(er-eval-body body env)))))
|
||||
|
||||
(define
|
||||
er-eval-body
|
||||
(fn
|
||||
(exprs env)
|
||||
(let
|
||||
((last (list nil)))
|
||||
(for-each
|
||||
(fn (i) (set-nth! last 0 (er-eval-expr (nth exprs i) env)))
|
||||
(range 0 (len exprs)))
|
||||
(nth last 0))))
|
||||
|
||||
;; ── dispatch ──────────────────────────────────────────────────────
|
||||
(define
|
||||
er-eval-expr
|
||||
(fn
|
||||
(node env)
|
||||
(let
|
||||
((ty (get node :type)))
|
||||
(cond
|
||||
(= 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)
|
||||
(= ty "nil") (er-mk-nil)
|
||||
(= ty "var") (er-eval-var node env)
|
||||
(= ty "tuple") (er-eval-tuple node env)
|
||||
(= ty "cons") (er-eval-cons node env)
|
||||
(= ty "op") (er-eval-op node env)
|
||||
(= ty "unop") (er-eval-unop node env)
|
||||
(= ty "block") (er-eval-body (get node :exprs) env)
|
||||
(= ty "if") (er-eval-if node env)
|
||||
(= ty "match") (er-eval-match node env)
|
||||
:else (error (str "Erlang eval: unsupported node type '" ty "'"))))))
|
||||
|
||||
(define
|
||||
er-eval-var
|
||||
(fn
|
||||
(node env)
|
||||
(let
|
||||
((name (get node :name)))
|
||||
(if
|
||||
(= name "_")
|
||||
(error "Erlang: '_' cannot be used as a value")
|
||||
(er-env-lookup env name)))))
|
||||
|
||||
(define
|
||||
er-eval-tuple
|
||||
(fn
|
||||
(node env)
|
||||
(let
|
||||
((out (list)))
|
||||
(for-each
|
||||
(fn
|
||||
(i)
|
||||
(append! out (er-eval-expr (nth (get node :elements) i) env)))
|
||||
(range 0 (len (get node :elements))))
|
||||
(er-mk-tuple out))))
|
||||
|
||||
(define
|
||||
er-eval-cons
|
||||
(fn
|
||||
(node env)
|
||||
(er-mk-cons
|
||||
(er-eval-expr (get node :head) env)
|
||||
(er-eval-expr (get node :tail) env))))
|
||||
|
||||
;; ── match (bare-var LHS only; full pattern matching comes next) ────
|
||||
(define
|
||||
er-eval-match
|
||||
(fn
|
||||
(node env)
|
||||
(let
|
||||
((lhs (get node :lhs))
|
||||
(rhs-val (er-eval-expr (get node :rhs) env)))
|
||||
(cond
|
||||
(= (get lhs :type) "var")
|
||||
(let
|
||||
((name (get lhs :name)))
|
||||
(cond
|
||||
(= name "_") rhs-val
|
||||
(dict-has? env name)
|
||||
(if
|
||||
(er-equal? (get env name) rhs-val)
|
||||
rhs-val
|
||||
(error "Erlang: badmatch (rebind mismatch)"))
|
||||
:else (do (er-env-bind! env name rhs-val) rhs-val)))
|
||||
:else (error
|
||||
"Erlang: pattern matching not yet supported (next Phase 2 step)")))))
|
||||
|
||||
;; ── operators ─────────────────────────────────────────────────────
|
||||
(define
|
||||
er-eval-op
|
||||
(fn
|
||||
(node env)
|
||||
(let
|
||||
((op (get node :op)) (args (get node :args)))
|
||||
(cond
|
||||
(= op "andalso") (er-eval-andalso args env)
|
||||
(= op "orelse") (er-eval-orelse args env)
|
||||
:else (er-apply-binop
|
||||
op
|
||||
(er-eval-expr (nth args 0) env)
|
||||
(er-eval-expr (nth args 1) env))))))
|
||||
|
||||
(define
|
||||
er-eval-andalso
|
||||
(fn
|
||||
(args env)
|
||||
(let
|
||||
((a (er-eval-expr (nth args 0) env)))
|
||||
(if (er-truthy? a) (er-eval-expr (nth args 1) env) a))))
|
||||
|
||||
(define
|
||||
er-eval-orelse
|
||||
(fn
|
||||
(args env)
|
||||
(let
|
||||
((a (er-eval-expr (nth args 0) env)))
|
||||
(if (er-truthy? a) a (er-eval-expr (nth args 1) env)))))
|
||||
|
||||
(define
|
||||
er-apply-binop
|
||||
(fn
|
||||
(op a b)
|
||||
(cond
|
||||
(= op "+") (+ a b)
|
||||
(= op "-") (- a b)
|
||||
(= op "*") (* a b)
|
||||
(= op "/") (/ a b)
|
||||
(= op "div") (truncate (/ a b))
|
||||
(= op "rem") (remainder a b)
|
||||
(= op "==") (er-bool (er-equal? a b))
|
||||
(= op "/=") (er-bool (not (er-equal? a b)))
|
||||
(= op "=:=") (er-bool (er-exact-equal? a b))
|
||||
(= op "=/=") (er-bool (not (er-exact-equal? a b)))
|
||||
(= op "<") (er-bool (er-lt? a b))
|
||||
(= op ">") (er-bool (er-lt? b a))
|
||||
(= op "=<") (er-bool (not (er-lt? b a)))
|
||||
(= op ">=") (er-bool (not (er-lt? a b)))
|
||||
(= op "++") (er-list-append a b)
|
||||
(= op "and") (er-bool (and (er-truthy? a) (er-truthy? b)))
|
||||
(= op "or") (er-bool (or (er-truthy? a) (er-truthy? b)))
|
||||
:else (error (str "Erlang eval: unsupported operator '" op "'")))))
|
||||
|
||||
(define
|
||||
er-eval-unop
|
||||
(fn
|
||||
(node env)
|
||||
(let
|
||||
((op (get node :op)) (a (er-eval-expr (get node :arg) env)))
|
||||
(cond
|
||||
(= op "-") (- 0 a)
|
||||
(= op "+") a
|
||||
(= op "not") (er-bool (not (er-truthy? a)))
|
||||
:else (error (str "Erlang eval: unsupported unary '" op "'"))))))
|
||||
|
||||
;; ── equality / comparison ─────────────────────────────────────────
|
||||
(define
|
||||
er-equal?
|
||||
(fn
|
||||
(a b)
|
||||
(cond
|
||||
(and (= (type-of a) "number") (= (type-of b) "number")) (= a b)
|
||||
(and (er-atom? a) (er-atom? b)) (= (get a :name) (get b :name))
|
||||
(and (er-nil? a) (er-nil? b)) true
|
||||
(and (er-cons? a) (er-cons? b))
|
||||
(and
|
||||
(er-equal? (get a :head) (get b :head))
|
||||
(er-equal? (get a :tail) (get b :tail)))
|
||||
(and (er-tuple? a) (er-tuple? b))
|
||||
(let
|
||||
((ea (get a :elements)) (eb (get b :elements)))
|
||||
(and
|
||||
(= (len ea) (len eb))
|
||||
(every?
|
||||
(fn (i) (er-equal? (nth ea i) (nth eb i)))
|
||||
(range 0 (len ea)))))
|
||||
(and (= (type-of a) "string") (= (type-of b) "string")) (= a b)
|
||||
:else false)))
|
||||
|
||||
;; Exact equality: 1 =/= 1.0 in Erlang.
|
||||
(define
|
||||
er-exact-equal?
|
||||
(fn
|
||||
(a b)
|
||||
(if
|
||||
(and (= (type-of a) "number") (= (type-of b) "number"))
|
||||
(and (= (integer? a) (integer? b)) (= a b))
|
||||
(er-equal? a b))))
|
||||
|
||||
(define
|
||||
er-lt?
|
||||
(fn
|
||||
(a b)
|
||||
(cond
|
||||
(and (= (type-of a) "number") (= (type-of b) "number")) (< a b)
|
||||
(and (er-atom? a) (er-atom? b)) (< (get a :name) (get b :name))
|
||||
(and (= (type-of a) "string") (= (type-of b) "string")) (< a b)
|
||||
:else (< (er-type-order a) (er-type-order b)))))
|
||||
|
||||
(define
|
||||
er-type-order
|
||||
(fn
|
||||
(v)
|
||||
(cond
|
||||
(= (type-of v) "number") 0
|
||||
(er-atom? v) 1
|
||||
(er-tuple? v) 2
|
||||
(er-nil? v) 3
|
||||
(er-cons? v) 3
|
||||
(= (type-of v) "string") 4
|
||||
:else 5)))
|
||||
|
||||
(define
|
||||
er-list-append
|
||||
(fn
|
||||
(a b)
|
||||
(cond
|
||||
(er-nil? a) b
|
||||
(er-cons? a)
|
||||
(er-mk-cons (get a :head) (er-list-append (get a :tail) b))
|
||||
:else (error "Erlang: ++ left argument is not a proper list"))))
|
||||
|
||||
;; ── if ────────────────────────────────────────────────────────────
|
||||
(define er-eval-if (fn (node env) (er-eval-if-clauses (get node :clauses) 0 env)))
|
||||
|
||||
(define
|
||||
er-eval-if-clauses
|
||||
(fn
|
||||
(clauses i env)
|
||||
(if
|
||||
(>= i (len clauses))
|
||||
(error "Erlang: if: no clause matched")
|
||||
(let
|
||||
((c (nth clauses i)))
|
||||
(if
|
||||
(er-eval-guards (get c :guards) env)
|
||||
(er-eval-body (get c :body) env)
|
||||
(er-eval-if-clauses clauses (+ i 1) env))))))
|
||||
|
||||
;; Guards: outer list = OR, inner list = AND. Empty outer = always pass.
|
||||
(define
|
||||
er-eval-guards
|
||||
(fn
|
||||
(alts env)
|
||||
(if (= (len alts) 0) true (er-eval-guards-any alts 0 env))))
|
||||
|
||||
(define
|
||||
er-eval-guards-any
|
||||
(fn
|
||||
(alts i env)
|
||||
(if
|
||||
(>= i (len alts))
|
||||
false
|
||||
(if
|
||||
(er-eval-guard-conj (nth alts i) env)
|
||||
true
|
||||
(er-eval-guards-any alts (+ i 1) env)))))
|
||||
|
||||
(define er-eval-guard-conj (fn (conj env) (er-eval-guard-conj-iter conj 0 env)))
|
||||
|
||||
(define
|
||||
er-eval-guard-conj-iter
|
||||
(fn
|
||||
(conj i env)
|
||||
(if
|
||||
(>= i (len conj))
|
||||
true
|
||||
(if
|
||||
(er-truthy? (er-eval-expr (nth conj i) env))
|
||||
(er-eval-guard-conj-iter conj (+ i 1) env)
|
||||
false))))
|
||||
Reference in New Issue
Block a user