- IPrintWithWriter protocol + CljElixir.Printer module with pr-str, print-str, pr, prn for all BEAM types (EDN-like output) - Source-mapped error messages: line/col metadata from reader now propagated through transformer into Elixir AST for accurate error locations in .clje files - Interactive REPL (mix clje.repl) with multi-line input detection, history, bindings persistence, and pr-str formatted output - nREPL server (mix clje.nrepl) with TCP transport, Bencode wire protocol, session management, and core operations (clone, close, eval, describe, ls-sessions, load-file, interrupt, completions). Writes .nrepl-port for editor auto-discovery. 92 new tests (699 total, 0 failures). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1401 lines
35 KiB
Elixir
1401 lines
35 KiB
Elixir
defmodule CljElixir.TransformerTest do
|
|
use ExUnit.Case, async: true
|
|
|
|
alias CljElixir.Transformer
|
|
alias CljElixir.Transformer.Context
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# Parse CljElixir source, transform, return Elixir AST
|
|
defp transform(source) do
|
|
{:ok, forms} = CljElixir.Reader.read_string(source)
|
|
Transformer.transform(forms)
|
|
end
|
|
|
|
# Parse, transform, eval, return result
|
|
# Uses vector_as_list: true until PersistentVector is implemented (Phase 3 WS-3)
|
|
defp eval(source) do
|
|
{:ok, result, _bindings} = CljElixir.Compiler.eval_string(source, vector_as_list: true)
|
|
result
|
|
end
|
|
|
|
# Generate a unique module name string and its Elixir module atom
|
|
defp unique_mod(prefix) do
|
|
n = System.unique_integer([:positive])
|
|
name_str = "#{prefix}#{n}"
|
|
mod_atom = String.to_atom("Elixir.#{name_str}")
|
|
{name_str, mod_atom}
|
|
end
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 1. defmodule
|
|
# ---------------------------------------------------------------------------
|
|
|
|
describe "defmodule" do
|
|
test "basic module definition" do
|
|
{name, mod} = unique_mod("TfTestMod")
|
|
source = "(defmodule #{name} (defn hello [] :world))"
|
|
eval(source)
|
|
assert apply(mod, :hello, []) == :world
|
|
end
|
|
|
|
test "module with docstring" do
|
|
ast = transform(~S|(defmodule MyMod "A test module" (def x 1))|)
|
|
ast_str = Macro.to_string(ast)
|
|
assert ast_str =~ "moduledoc"
|
|
end
|
|
|
|
test "module with multiple defs" do
|
|
{name, mod} = unique_mod("TfTestMultiDef")
|
|
|
|
source = """
|
|
(defmodule #{name}
|
|
(defn add [x y] (+ x y))
|
|
(defn sub [x y] (- x y)))
|
|
"""
|
|
|
|
eval(source)
|
|
assert apply(mod, :add, [3, 4]) == 7
|
|
assert apply(mod, :sub, [10, 3]) == 7
|
|
end
|
|
end
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 2. defn / defn-
|
|
# ---------------------------------------------------------------------------
|
|
|
|
describe "defn" do
|
|
test "single-arity function" do
|
|
{name, mod} = unique_mod("TfTestDefn1")
|
|
|
|
source = """
|
|
(defmodule #{name}
|
|
(defn double [x] (* x 2)))
|
|
"""
|
|
|
|
eval(source)
|
|
assert apply(mod, :double, [5]) == 10
|
|
end
|
|
|
|
test "multi-clause function" do
|
|
{name, mod} = unique_mod("TfTestDefn2")
|
|
|
|
source = """
|
|
(defmodule #{name}
|
|
(defn greet
|
|
([name] (str "hello " name))
|
|
([name greeting] (str greeting " " name))))
|
|
"""
|
|
|
|
eval(source)
|
|
assert apply(mod, :greet, ["Ada"]) == "hello Ada"
|
|
assert apply(mod, :greet, ["Ada", "hi"]) == "hi Ada"
|
|
end
|
|
|
|
test "defn with docstring" do
|
|
ast = transform(~S|(defn hello "Greets someone" [name] (str "hi " name))|)
|
|
ast_str = Macro.to_string(ast)
|
|
assert ast_str =~ "doc"
|
|
end
|
|
|
|
test "defn- produces private function" do
|
|
ast = transform("(defn- helper [x] (* x 2))")
|
|
ast_str = Macro.to_string(ast)
|
|
assert ast_str =~ "defp"
|
|
end
|
|
|
|
test "defn with multiple body expressions" do
|
|
{name, mod} = unique_mod("TfTestDefnBody")
|
|
|
|
source = """
|
|
(defmodule #{name}
|
|
(defn process [x]
|
|
(+ x 1)
|
|
(* x 2)))
|
|
"""
|
|
|
|
eval(source)
|
|
# Last expression is returned
|
|
assert apply(mod, :process, [5]) == 10
|
|
end
|
|
end
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 3. fn (anonymous functions)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
describe "fn" do
|
|
test "single-arity anonymous function" do
|
|
assert eval("((fn [x] (* x x)) 5)") == 25
|
|
end
|
|
|
|
test "multi-clause same-arity anonymous function" do
|
|
# Elixir anonymous functions support multiple clauses of same arity
|
|
result = eval("((fn ([0] :zero) ([x] x)) 42)")
|
|
assert result == 42
|
|
end
|
|
|
|
test "fn used as argument to HOF" do
|
|
result = eval("(Enum/map [1 2 3] (fn [x] (* x x)))")
|
|
assert result == [1, 4, 9]
|
|
end
|
|
|
|
test "fn with two params" do
|
|
result = eval("((fn [x y] (+ x y)) 3 4)")
|
|
assert result == 7
|
|
end
|
|
|
|
test "fn with no params" do
|
|
result = eval("((fn [] 42))")
|
|
assert result == 42
|
|
end
|
|
end
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 4. #() anonymous shorthand
|
|
# ---------------------------------------------------------------------------
|
|
|
|
describe "anonymous fn shorthand" do
|
|
test "single arg with %" do
|
|
result = eval("(Enum/map [1 2 3] #(* % 2))")
|
|
assert result == [2, 4, 6]
|
|
end
|
|
|
|
test "two args with %1 %2" do
|
|
result = eval("(Enum/reduce [1 2 3 4 5] 0 #(+ %1 %2))")
|
|
assert result == 15
|
|
end
|
|
|
|
test "produces correct arity" do
|
|
ast = transform("#(+ %1 %2)")
|
|
{:fn, _, [{:->, _, [params, _body]}]} = ast
|
|
assert length(params) == 2
|
|
end
|
|
end
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 5. let
|
|
# ---------------------------------------------------------------------------
|
|
|
|
describe "let" do
|
|
test "basic let binding" do
|
|
assert eval("(let [x 1 y 2] (+ x y))") == 3
|
|
end
|
|
|
|
test "let with multiple bindings" do
|
|
assert eval("(let [a 1 b 2 c 3] (+ a (+ b c)))") == 6
|
|
end
|
|
|
|
test "let bindings are sequential" do
|
|
assert eval("(let [x 1 y (+ x 1)] y)") == 2
|
|
end
|
|
|
|
test "let with expression in binding" do
|
|
assert eval("(let [x (* 3 4)] x)") == 12
|
|
end
|
|
end
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 6. if / when / cond / case / do
|
|
# ---------------------------------------------------------------------------
|
|
|
|
describe "if" do
|
|
test "if with true condition" do
|
|
assert eval("(if true :yes :no)") == :yes
|
|
end
|
|
|
|
test "if with false condition" do
|
|
assert eval("(if false :yes :no)") == :no
|
|
end
|
|
|
|
test "if without else returns nil on false" do
|
|
assert eval("(if false :yes)") == nil
|
|
end
|
|
|
|
test "if with complex condition" do
|
|
assert eval("(if (> 5 3) :greater :lesser)") == :greater
|
|
end
|
|
end
|
|
|
|
describe "when" do
|
|
test "when with true condition" do
|
|
assert eval("(when true :yes)") == :yes
|
|
end
|
|
|
|
test "when with false condition returns nil" do
|
|
assert eval("(when false :yes)") == nil
|
|
end
|
|
|
|
test "when with multiple body expressions" do
|
|
result = eval("(when true 1 2 3)")
|
|
assert result == 3
|
|
end
|
|
end
|
|
|
|
describe "cond" do
|
|
test "basic cond" do
|
|
assert eval("(cond false :a true :b)") == :b
|
|
end
|
|
|
|
test "cond with :else" do
|
|
assert eval("(cond (> 1 2) :a (< 1 2) :b :else :c)") == :b
|
|
end
|
|
|
|
test "cond falls through to :else" do
|
|
assert eval("(cond false :a false :b :else :c)") == :c
|
|
end
|
|
end
|
|
|
|
describe "case" do
|
|
test "basic case" do
|
|
assert eval("(case :ok :ok :yes :error :no)") == :yes
|
|
end
|
|
|
|
test "case with tuple patterns" do
|
|
assert eval("(case #el[:ok 42] [:ok val] val [:error _] nil)") == 42
|
|
end
|
|
|
|
test "case with wildcard" do
|
|
assert eval("(case :unknown :ok :yes _ :default)") == :default
|
|
end
|
|
end
|
|
|
|
describe "do" do
|
|
test "do block returns last expression" do
|
|
assert eval("(do 1 2 3)") == 3
|
|
end
|
|
|
|
test "do with side effects" do
|
|
result = eval("(do (+ 1 2) (+ 3 4))")
|
|
assert result == 7
|
|
end
|
|
end
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 7. loop / recur
|
|
# ---------------------------------------------------------------------------
|
|
|
|
describe "loop/recur" do
|
|
test "basic loop" do
|
|
result = eval("(loop [i 0 acc 0] (if (>= i 5) acc (recur (inc i) (+ acc i))))")
|
|
assert result == 10
|
|
end
|
|
|
|
test "factorial via loop/recur" do
|
|
result =
|
|
eval("""
|
|
(loop [n 5 acc 1]
|
|
(if (<= n 1)
|
|
acc
|
|
(recur (dec n) (* acc n))))
|
|
""")
|
|
|
|
assert result == 120
|
|
end
|
|
|
|
test "recur in defn" do
|
|
{name, mod} = unique_mod("TfTestRecur")
|
|
|
|
source = """
|
|
(defmodule #{name}
|
|
(defn countdown [n]
|
|
(if (<= n 0)
|
|
:done
|
|
(recur (dec n)))))
|
|
"""
|
|
|
|
eval(source)
|
|
assert apply(mod, :countdown, [5]) == :done
|
|
end
|
|
end
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 8. def (top-level binding)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
describe "def" do
|
|
test "def creates a zero-arity function" do
|
|
{name, mod} = unique_mod("TfTestDef")
|
|
source = "(defmodule #{name} (def answer 42))"
|
|
eval(source)
|
|
assert apply(mod, :answer, []) == 42
|
|
end
|
|
end
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 9. Module/function calls (FFI)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
describe "module calls" do
|
|
test "Elixir module call (uppercase)" do
|
|
assert eval("(Enum/map [1 2 3] (fn [x] (* x x)))") == [1, 4, 9]
|
|
end
|
|
|
|
test "Elixir module call with hyphens in function name" do
|
|
# String.split -> String.split (no hyphens, just testing the pattern)
|
|
result = eval(~S|(String/split "a-b-c" "-")|)
|
|
assert result == ["a", "b", "c"]
|
|
end
|
|
|
|
test "Erlang module call (lowercase)" do
|
|
result = eval("(erlang/system_time)")
|
|
assert is_integer(result)
|
|
end
|
|
|
|
test "Map module call" do
|
|
result = eval("(Map/put {:a 1} :b 2)")
|
|
assert result == %{a: 1, b: 2}
|
|
end
|
|
|
|
test "Enum/reduce" do
|
|
result = eval("(Enum/reduce [1 2 3] 0 (fn [acc x] (+ acc x)))")
|
|
assert result == 6
|
|
end
|
|
end
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 10. Unqualified function calls
|
|
# ---------------------------------------------------------------------------
|
|
|
|
describe "unqualified calls" do
|
|
test "hyphen to underscore conversion" do
|
|
ast = transform("(my-func arg1)")
|
|
ast_str = Macro.to_string(ast)
|
|
assert ast_str =~ "my_func"
|
|
end
|
|
|
|
test "calling Kernel functions" do
|
|
assert eval("(length [1 2 3])") == 3
|
|
end
|
|
|
|
test "calling rem" do
|
|
assert eval("(rem 10 3)") == 1
|
|
end
|
|
end
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 11. Data literals
|
|
# ---------------------------------------------------------------------------
|
|
|
|
describe "data literals" do
|
|
test "map literal" do
|
|
assert eval("{:a 1 :b 2}") == %{a: 1, b: 2}
|
|
end
|
|
|
|
test "empty map" do
|
|
assert eval("{}") == %{}
|
|
end
|
|
|
|
test "vector in value position becomes list" do
|
|
assert eval("[1 2 3]") == [1, 2, 3]
|
|
end
|
|
|
|
test "vector in pattern position becomes tuple match" do
|
|
assert eval("(case #el[:ok 42] [:ok x] x)") == 42
|
|
end
|
|
|
|
test "tuple literal" do
|
|
assert eval("#el[:ok 42]") == {:ok, 42}
|
|
end
|
|
|
|
test "tuple with three elements" do
|
|
assert eval("#el[:a :b :c]") == {:a, :b, :c}
|
|
end
|
|
|
|
test "set literal" do
|
|
result = eval(~S|#{:a :b :c}|)
|
|
assert result == MapSet.new([:a, :b, :c])
|
|
end
|
|
|
|
test "nested data structures" do
|
|
result = eval(~S|{:users [{:name "Ada"} {:name "Bob"}]}|)
|
|
assert result == %{users: [%{name: "Ada"}, %{name: "Bob"}]}
|
|
end
|
|
|
|
test "quoted list" do
|
|
result = eval("(quote (1 2 3))")
|
|
assert result == [1, 2, 3]
|
|
end
|
|
|
|
test "regex literal" do
|
|
ast = transform(~S|#"^\d+$"|)
|
|
ast_str = Macro.to_string(ast)
|
|
# Macro.to_string renders sigil_r as ~r/pattern/
|
|
assert ast_str =~ "~r" or ast_str =~ "sigil_r"
|
|
end
|
|
end
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 12. Keyword-as-function
|
|
# ---------------------------------------------------------------------------
|
|
|
|
describe "keyword-as-function" do
|
|
test "keyword access on map" do
|
|
assert eval(~S|(:name {:name "Ada" :age 30})|) == "Ada"
|
|
end
|
|
|
|
test "keyword access with default" do
|
|
assert eval(~S|(:missing {:name "Ada"} :default)|) == :default
|
|
end
|
|
|
|
test "keyword access returns nil on missing key" do
|
|
assert eval(~S|(:missing {:name "Ada"})|) == nil
|
|
end
|
|
end
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 13. defprotocol
|
|
# ---------------------------------------------------------------------------
|
|
|
|
describe "defprotocol" do
|
|
test "basic protocol definition" do
|
|
ast =
|
|
transform("""
|
|
(defprotocol Describable
|
|
(describe [value]))
|
|
""")
|
|
|
|
ast_str = Macro.to_string(ast)
|
|
assert ast_str =~ "defprotocol"
|
|
assert ast_str =~ "describe"
|
|
end
|
|
|
|
test "protocol with docstring" do
|
|
ast =
|
|
transform("""
|
|
(defprotocol Describable
|
|
"Protocol for descriptions"
|
|
(describe [value]))
|
|
""")
|
|
|
|
ast_str = Macro.to_string(ast)
|
|
assert ast_str =~ "defprotocol"
|
|
assert ast_str =~ "moduledoc"
|
|
end
|
|
end
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 14. defrecord
|
|
# ---------------------------------------------------------------------------
|
|
|
|
describe "defrecord" do
|
|
test "basic record definition" do
|
|
ast =
|
|
transform("""
|
|
(defrecord User [name age])
|
|
""")
|
|
|
|
ast_str = Macro.to_string(ast)
|
|
assert ast_str =~ "defstruct"
|
|
assert ast_str =~ "defmodule"
|
|
end
|
|
|
|
test "record with docstring" do
|
|
ast =
|
|
transform("""
|
|
(defrecord User "A user" [name age])
|
|
""")
|
|
|
|
ast_str = Macro.to_string(ast)
|
|
assert ast_str =~ "defstruct"
|
|
assert ast_str =~ "moduledoc"
|
|
end
|
|
end
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 15. extend-type / extend-protocol
|
|
# ---------------------------------------------------------------------------
|
|
|
|
describe "extend-type" do
|
|
test "produces defimpl" do
|
|
ast =
|
|
transform("""
|
|
(extend-type Map
|
|
MyProto
|
|
(-lookup [m k] (Map/get m k)))
|
|
""")
|
|
|
|
ast_str = Macro.to_string(ast)
|
|
assert ast_str =~ "defimpl"
|
|
assert ast_str =~ "MyProto"
|
|
assert ast_str =~ "Map"
|
|
end
|
|
end
|
|
|
|
describe "extend-protocol" do
|
|
test "produces defimpl" do
|
|
ast =
|
|
transform("""
|
|
(extend-protocol MyProto
|
|
Map
|
|
(-lookup [m k] (Map/get m k)))
|
|
""")
|
|
|
|
ast_str = Macro.to_string(ast)
|
|
assert ast_str =~ "defimpl"
|
|
end
|
|
end
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 16. reify
|
|
# ---------------------------------------------------------------------------
|
|
|
|
describe "reify" do
|
|
test "produces defmodule and struct instance" do
|
|
ast =
|
|
transform("""
|
|
(reify
|
|
MyProto
|
|
(-describe [_] "hello"))
|
|
""")
|
|
|
|
ast_str = Macro.to_string(ast)
|
|
assert ast_str =~ "defmodule"
|
|
assert ast_str =~ "defstruct"
|
|
end
|
|
end
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 17. with
|
|
# ---------------------------------------------------------------------------
|
|
|
|
describe "with" do
|
|
test "basic with" do
|
|
result =
|
|
eval("""
|
|
(with [[:ok x] #el[:ok 42]]
|
|
x)
|
|
""")
|
|
|
|
assert result == 42
|
|
end
|
|
|
|
test "with multiple bindings" do
|
|
result =
|
|
eval("""
|
|
(with [[:ok a] #el[:ok 1]
|
|
[:ok b] #el[:ok 2]]
|
|
(+ a b))
|
|
""")
|
|
|
|
assert result == 3
|
|
end
|
|
|
|
test "with short-circuit on mismatch" do
|
|
result =
|
|
eval("""
|
|
(with [[:ok x] #el[:error :oops]]
|
|
x)
|
|
""")
|
|
|
|
assert result == {:error, :oops}
|
|
end
|
|
end
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 18. receive
|
|
# ---------------------------------------------------------------------------
|
|
|
|
describe "receive" do
|
|
test "produces receive AST" do
|
|
ast =
|
|
transform("""
|
|
(receive
|
|
[:ok val] val
|
|
[:error _] nil)
|
|
""")
|
|
|
|
ast_str = Macro.to_string(ast)
|
|
assert ast_str =~ "receive"
|
|
end
|
|
|
|
test "receive with after" do
|
|
ast =
|
|
transform("""
|
|
(receive
|
|
[:ok val] val
|
|
:after 1000 :timeout)
|
|
""")
|
|
|
|
ast_str = Macro.to_string(ast)
|
|
assert ast_str =~ "receive"
|
|
assert ast_str =~ "after"
|
|
end
|
|
|
|
test "receive with timeout evaluates" do
|
|
# Send ourselves a message, then receive it
|
|
result =
|
|
eval("""
|
|
(do
|
|
(Kernel/send *self* #el[:ok 42])
|
|
(receive
|
|
[:ok val] val
|
|
:after 100 :timeout))
|
|
""")
|
|
|
|
assert result == 42
|
|
end
|
|
|
|
test "receive timeout fires when no message" do
|
|
result =
|
|
eval("""
|
|
(receive
|
|
[:ok val] val
|
|
:after 1 :timeout)
|
|
""")
|
|
|
|
assert result == :timeout
|
|
end
|
|
end
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 18b. send/spawn/spawn-link (bare unqualified calls)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
describe "send/spawn/spawn-link" do
|
|
test "send delivers message to self" do
|
|
result =
|
|
eval("""
|
|
(do
|
|
(send *self* :hello)
|
|
(receive
|
|
:hello :got-it
|
|
:after 100 :timeout))
|
|
""")
|
|
|
|
assert result == :"got-it"
|
|
end
|
|
|
|
test "send with tuple message" do
|
|
result =
|
|
eval("""
|
|
(do
|
|
(send *self* #el[:ok 42])
|
|
(receive
|
|
[:ok val] val
|
|
:after 100 :timeout))
|
|
""")
|
|
|
|
assert result == 42
|
|
end
|
|
|
|
test "spawn creates a process" do
|
|
result =
|
|
eval("""
|
|
(do
|
|
(let [pid (spawn (fn [] :done))]
|
|
(is-pid pid)))
|
|
""")
|
|
|
|
assert result == true
|
|
end
|
|
|
|
test "spawn and send between processes" do
|
|
result =
|
|
eval("""
|
|
(do
|
|
(let [parent *self*
|
|
child (spawn (fn []
|
|
(send parent #el[:from-child 99])
|
|
:done))]
|
|
(receive
|
|
[:from-child val] val
|
|
:after 1000 :timeout)))
|
|
""")
|
|
|
|
assert result == 99
|
|
end
|
|
|
|
test "spawn-link creates a linked process" do
|
|
result =
|
|
eval("""
|
|
(do
|
|
(let [pid (spawn-link (fn [] :done))]
|
|
(is-pid pid)))
|
|
""")
|
|
|
|
assert result == true
|
|
end
|
|
|
|
test "spawn with module/function/args" do
|
|
# spawn/3 with module, function, args
|
|
ast = transform("(spawn Kernel :is-integer '(42))")
|
|
ast_str = Macro.to_string(ast)
|
|
assert ast_str =~ "spawn"
|
|
end
|
|
end
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 18c. Process primitives: monitor, link, unlink, alive?
|
|
# ---------------------------------------------------------------------------
|
|
|
|
describe "process primitives" do
|
|
test "link/unlink" do
|
|
result =
|
|
eval("""
|
|
(do
|
|
(let [pid (spawn (fn [] (receive _ :ok :after 1000 :done)))]
|
|
(link pid)
|
|
(unlink pid)
|
|
(alive? pid)))
|
|
""")
|
|
|
|
assert result == true
|
|
end
|
|
|
|
test "monitor produces reference" do
|
|
result =
|
|
eval("""
|
|
(do
|
|
(let [pid (spawn (fn [] (receive _ :ok :after 1000 :done)))
|
|
ref (monitor pid)]
|
|
(is-reference ref)))
|
|
""")
|
|
|
|
assert result == true
|
|
end
|
|
|
|
test "monitor with type" do
|
|
result =
|
|
eval("""
|
|
(do
|
|
(let [pid (spawn (fn [] :done))
|
|
ref (monitor :process pid)]
|
|
(is-reference ref)))
|
|
""")
|
|
|
|
assert result == true
|
|
end
|
|
|
|
test "alive? returns boolean" do
|
|
result =
|
|
eval("""
|
|
(do
|
|
(let [pid (spawn (fn [] :done))]
|
|
(Process/sleep 50)
|
|
(alive? pid)))
|
|
""")
|
|
|
|
assert result == false
|
|
end
|
|
end
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 19. for / doseq
|
|
# ---------------------------------------------------------------------------
|
|
|
|
describe "for" do
|
|
test "basic for comprehension" do
|
|
result = eval("(for [x [1 2 3]] (* x x))")
|
|
assert result == [1, 4, 9]
|
|
end
|
|
|
|
test "for with :when filter" do
|
|
result = eval("(for [x [1 2 3 4 5] :when (> x 2)] x)")
|
|
assert result == [3, 4, 5]
|
|
end
|
|
|
|
test "for with multiple generators" do
|
|
result = eval("(for [x [1 2] y [3 4]] (+ x y))")
|
|
assert result == [4, 5, 5, 6]
|
|
end
|
|
end
|
|
|
|
describe "doseq" do
|
|
test "doseq produces for AST" do
|
|
ast = transform("(doseq [x [1 2 3]] (println x))")
|
|
ast_str = Macro.to_string(ast)
|
|
assert ast_str =~ "for"
|
|
end
|
|
end
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 20. if-let / when-let / if-some / when-some
|
|
# ---------------------------------------------------------------------------
|
|
|
|
describe "if-let" do
|
|
test "if-let with truthy value" do
|
|
result = eval("(if-let [x 42] x :nope)")
|
|
assert result == 42
|
|
end
|
|
|
|
test "if-let with nil" do
|
|
result = eval("(if-let [x nil] x :nope)")
|
|
assert result == :nope
|
|
end
|
|
|
|
test "if-let with false" do
|
|
result = eval("(if-let [x false] x :nope)")
|
|
assert result == :nope
|
|
end
|
|
end
|
|
|
|
describe "when-let" do
|
|
test "when-let with truthy value" do
|
|
result = eval("(when-let [x 42] (+ x 1))")
|
|
assert result == 43
|
|
end
|
|
|
|
test "when-let with nil returns nil" do
|
|
result = eval("(when-let [x nil] (+ x 1))")
|
|
assert result == nil
|
|
end
|
|
end
|
|
|
|
describe "if-some" do
|
|
test "if-some with non-nil value" do
|
|
result = eval("(if-some [x 42] x :nope)")
|
|
assert result == 42
|
|
end
|
|
|
|
test "if-some with false (not nil)" do
|
|
result = eval("(if-some [x false] x :nope)")
|
|
assert result == false
|
|
end
|
|
|
|
test "if-some with nil" do
|
|
result = eval("(if-some [x nil] x :nope)")
|
|
assert result == :nope
|
|
end
|
|
end
|
|
|
|
describe "when-some" do
|
|
test "when-some with non-nil value" do
|
|
result = eval("(when-some [x 42] (+ x 1))")
|
|
assert result == 43
|
|
end
|
|
|
|
test "when-some with nil returns nil" do
|
|
result = eval("(when-some [x nil] (+ x 1))")
|
|
assert result == nil
|
|
end
|
|
|
|
test "when-some with false (non-nil)" do
|
|
# false is not nil, so the body should execute
|
|
# Keywords preserve hyphens: :got-it stays as :got-it
|
|
result = eval("(when-some [x false] :done)")
|
|
assert result == :done
|
|
end
|
|
end
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 21. use / require / import / alias
|
|
# ---------------------------------------------------------------------------
|
|
|
|
describe "directives" do
|
|
test "use produces use AST" do
|
|
ast = transform("(use GenServer)")
|
|
ast_str = Macro.to_string(ast)
|
|
assert ast_str =~ "use"
|
|
assert ast_str =~ "GenServer"
|
|
end
|
|
|
|
test "require produces require AST" do
|
|
ast = transform("(require Logger)")
|
|
ast_str = Macro.to_string(ast)
|
|
assert ast_str =~ "require"
|
|
assert ast_str =~ "Logger"
|
|
end
|
|
|
|
test "import produces import AST" do
|
|
ast = transform("(import Enum)")
|
|
ast_str = Macro.to_string(ast)
|
|
assert ast_str =~ "import"
|
|
assert ast_str =~ "Enum"
|
|
end
|
|
|
|
test "alias produces alias AST" do
|
|
ast = transform("(alias MyApp)")
|
|
ast_str = Macro.to_string(ast)
|
|
assert ast_str =~ "alias"
|
|
assert ast_str =~ "MyApp"
|
|
end
|
|
end
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 22. Operators and builtins
|
|
# ---------------------------------------------------------------------------
|
|
|
|
describe "arithmetic operators" do
|
|
test "addition" do
|
|
assert eval("(+ 1 2)") == 3
|
|
end
|
|
|
|
test "addition variadic" do
|
|
assert eval("(+ 1 2 3 4)") == 10
|
|
end
|
|
|
|
test "subtraction" do
|
|
assert eval("(- 10 3)") == 7
|
|
end
|
|
|
|
test "unary minus" do
|
|
assert eval("(- 5)") == -5
|
|
end
|
|
|
|
test "multiplication" do
|
|
assert eval("(* 3 4)") == 12
|
|
end
|
|
|
|
test "multiplication variadic" do
|
|
assert eval("(* 2 3 4)") == 24
|
|
end
|
|
|
|
# Note: / is not a valid symbol start char in the reader,
|
|
# so division uses the Kernel module call instead
|
|
test "division via Kernel" do
|
|
result = eval("(Kernel/div 10 2)")
|
|
assert result == 5
|
|
end
|
|
end
|
|
|
|
describe "comparison operators" do
|
|
test "greater than" do
|
|
assert eval("(> 5 3)") == true
|
|
assert eval("(> 3 5)") == false
|
|
end
|
|
|
|
test "less than" do
|
|
assert eval("(< 3 5)") == true
|
|
assert eval("(< 5 3)") == false
|
|
end
|
|
|
|
test "greater than or equal" do
|
|
assert eval("(>= 5 5)") == true
|
|
assert eval("(>= 5 6)") == false
|
|
end
|
|
|
|
test "less than or equal" do
|
|
assert eval("(<= 5 5)") == true
|
|
assert eval("(<= 6 5)") == false
|
|
end
|
|
end
|
|
|
|
describe "equality" do
|
|
test "= for value equality" do
|
|
assert eval("(= 1 1)") == true
|
|
assert eval("(= 1 2)") == false
|
|
end
|
|
|
|
test "== for numeric equality" do
|
|
assert eval("(== 1 1)") == true
|
|
end
|
|
|
|
test "not=" do
|
|
assert eval("(not= 1 2)") == true
|
|
assert eval("(not= 1 1)") == false
|
|
end
|
|
|
|
test "!=" do
|
|
assert eval("(!= 1 2)") == true
|
|
assert eval("(!= 1 1)") == false
|
|
end
|
|
end
|
|
|
|
describe "boolean operators" do
|
|
test "not" do
|
|
assert eval("(not true)") == false
|
|
assert eval("(not false)") == true
|
|
end
|
|
|
|
test "and" do
|
|
assert eval("(and true true)") == true
|
|
assert eval("(and true false)") == false
|
|
end
|
|
|
|
test "or" do
|
|
assert eval("(or false true)") == true
|
|
assert eval("(or false false)") == false
|
|
end
|
|
|
|
test "and variadic" do
|
|
assert eval("(and true true true)") == true
|
|
assert eval("(and true false true)") == false
|
|
end
|
|
end
|
|
|
|
describe "builtins" do
|
|
test "inc" do
|
|
assert eval("(inc 5)") == 6
|
|
end
|
|
|
|
test "dec" do
|
|
assert eval("(dec 5)") == 4
|
|
end
|
|
|
|
test "str concatenation" do
|
|
assert eval(~S|(str "hello" " " "world")|) == "hello world"
|
|
end
|
|
|
|
test "str single arg" do
|
|
assert eval(~S|(str 42)|) == "42"
|
|
end
|
|
|
|
test "str empty" do
|
|
assert eval("(str)") == ""
|
|
end
|
|
|
|
test "nil?" do
|
|
assert eval("(nil? nil)") == true
|
|
assert eval("(nil? 42)") == false
|
|
end
|
|
|
|
test "count" do
|
|
assert eval("(count [1 2 3])") == 3
|
|
end
|
|
|
|
test "hd" do
|
|
assert eval("(hd [1 2 3])") == 1
|
|
end
|
|
|
|
test "tl" do
|
|
assert eval("(tl [1 2 3])") == [2, 3]
|
|
end
|
|
|
|
test "cons" do
|
|
assert eval("(cons 0 [1 2 3])") == [0, 1, 2, 3]
|
|
end
|
|
|
|
test "throw produces raise" do
|
|
ast = transform(~S|(throw "oops")|)
|
|
ast_str = Macro.to_string(ast)
|
|
assert ast_str =~ "raise"
|
|
end
|
|
|
|
test "println produces IO.puts" do
|
|
ast = transform(~S|(println "hello")|)
|
|
ast_str = Macro.to_string(ast)
|
|
assert ast_str =~ "IO"
|
|
assert ast_str =~ "puts"
|
|
end
|
|
end
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 23. Dynamic vars
|
|
# ---------------------------------------------------------------------------
|
|
|
|
describe "dynamic vars" do
|
|
test "*self* produces self() call" do
|
|
ast = transform("*self*")
|
|
assert match?({:self, _, []}, ast)
|
|
end
|
|
|
|
test "*node* produces node() call" do
|
|
ast = transform("*node*")
|
|
assert match?({:node, _, []}, ast)
|
|
end
|
|
|
|
test "*self* evaluates to current process" do
|
|
result = eval("*self*")
|
|
assert is_pid(result)
|
|
end
|
|
end
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 24. munge_name
|
|
# ---------------------------------------------------------------------------
|
|
|
|
describe "munge_name" do
|
|
test "hyphens to underscores" do
|
|
assert Transformer.munge_name("my-func") == "my_func"
|
|
end
|
|
|
|
test "question mark to _qmark" do
|
|
assert Transformer.munge_name("nil?") == "nil_qmark"
|
|
end
|
|
|
|
test "exclamation to _bang" do
|
|
assert Transformer.munge_name("reset!") == "reset_bang"
|
|
end
|
|
|
|
test "combined hyphen and bang" do
|
|
assert Transformer.munge_name("do-thing!") == "do_thing_bang"
|
|
end
|
|
|
|
test "no change for plain names" do
|
|
assert Transformer.munge_name("hello") == "hello"
|
|
end
|
|
|
|
test "arrow preserved" do
|
|
# -> contains a hyphen, but it's part of the arrow
|
|
assert Transformer.munge_name("map->set") == "map_>set"
|
|
end
|
|
end
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 25. Symbols as variables
|
|
# ---------------------------------------------------------------------------
|
|
|
|
describe "symbols" do
|
|
test "plain symbol becomes variable" do
|
|
ast = transform("x")
|
|
assert match?({:x, _, nil}, ast)
|
|
end
|
|
|
|
test "symbol with hyphens becomes munged variable" do
|
|
ast = transform("my-var")
|
|
assert match?({:my_var, _, nil}, ast)
|
|
end
|
|
|
|
test "true symbol becomes true literal" do
|
|
assert eval("true") == true
|
|
end
|
|
|
|
test "false symbol becomes false literal" do
|
|
assert eval("false") == false
|
|
end
|
|
|
|
test "nil symbol becomes nil literal" do
|
|
assert eval("nil") == nil
|
|
end
|
|
end
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 26. Pattern position vectors -> tuple matches
|
|
# ---------------------------------------------------------------------------
|
|
|
|
describe "pattern position vectors" do
|
|
test "vector in case pattern becomes tuple" do
|
|
result = eval("(case #el[:ok 42] [:ok x] x [:error _] nil)")
|
|
assert result == 42
|
|
end
|
|
|
|
test "vector in let LHS becomes tuple match" do
|
|
result = eval("(let [[:ok x] #el[:ok 99]] x)")
|
|
assert result == 99
|
|
end
|
|
|
|
test "nested vector patterns in case" do
|
|
result =
|
|
eval("""
|
|
(case #el[:ok #el[:inner 5]]
|
|
[:ok [:inner n]] n
|
|
_ nil)
|
|
""")
|
|
|
|
assert result == 5
|
|
end
|
|
|
|
test "defn params are not in pattern context (they are parameter lists)" do
|
|
# Params in defn are parameter lists, not patterns for tuple matching
|
|
# The params vector itself is parameter list, but inner vectors would be patterns
|
|
{name, mod} = unique_mod("TfTestPatParams")
|
|
|
|
source = """
|
|
(defmodule #{name}
|
|
(defn extract [msg]
|
|
(case msg
|
|
[:ok val] val
|
|
_ nil)))
|
|
"""
|
|
|
|
eval(source)
|
|
assert apply(mod, :extract, [{:ok, 42}]) == 42
|
|
end
|
|
end
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Integration: end-to-end tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
describe "integration" do
|
|
test "fibonacci with loop/recur" do
|
|
result =
|
|
eval("""
|
|
(loop [n 10 a 0 b 1]
|
|
(if (= n 0)
|
|
a
|
|
(recur (dec n) b (+ a b))))
|
|
""")
|
|
|
|
assert result == 55
|
|
end
|
|
|
|
test "map + filter pipeline" do
|
|
result =
|
|
eval("""
|
|
(Enum/filter
|
|
(Enum/map [1 2 3 4 5 6 7 8 9 10]
|
|
(fn [x] (* x x)))
|
|
(fn [x] (> x 25)))
|
|
""")
|
|
|
|
assert result == [36, 49, 64, 81, 100]
|
|
end
|
|
|
|
test "nested let with function calls" do
|
|
result =
|
|
eval("""
|
|
(let [nums [1 2 3 4 5]
|
|
doubled (Enum/map nums (fn [x] (* x 2)))
|
|
total (Enum/reduce doubled 0 (fn [acc x] (+ acc x)))]
|
|
total)
|
|
""")
|
|
|
|
assert result == 30
|
|
end
|
|
|
|
test "defmodule with multiple features" do
|
|
{name, mod} = unique_mod("TfTestIntegration")
|
|
|
|
source = """
|
|
(defmodule #{name}
|
|
(defn factorial [n]
|
|
(loop [i n acc 1]
|
|
(if (<= i 1)
|
|
acc
|
|
(recur (dec i) (* acc i)))))
|
|
|
|
(defn sum-squares [nums]
|
|
(Enum/reduce
|
|
(Enum/map nums (fn [x] (* x x)))
|
|
0
|
|
(fn [acc x] (+ acc x)))))
|
|
"""
|
|
|
|
eval(source)
|
|
assert apply(mod, :factorial, [5]) == 120
|
|
assert apply(mod, :sum_squares, [[1, 2, 3, 4, 5]]) == 55
|
|
end
|
|
|
|
test "keyword access chain" do
|
|
result =
|
|
eval("""
|
|
(let [person {:name "Ada" :age 30}]
|
|
(str (:name person) " is " (Kernel/to_string (:age person))))
|
|
""")
|
|
|
|
assert result == "Ada is 30"
|
|
end
|
|
|
|
test "for comprehension with filter" do
|
|
result =
|
|
eval("""
|
|
(for [x [1 2 3 4 5 6 7 8 9 10]
|
|
:when (= 0 (rem x 2))]
|
|
(* x x))
|
|
""")
|
|
|
|
assert result == [4, 16, 36, 64, 100]
|
|
end
|
|
|
|
test "case with multiple patterns in module" do
|
|
{name, mod} = unique_mod("TfTestCase")
|
|
|
|
source = """
|
|
(defmodule #{name}
|
|
(defn handle [msg]
|
|
(case msg
|
|
[:ok data] (str "ok: " (Kernel/to_string data))
|
|
[:error reason] (str "error: " (Kernel/to_string reason))
|
|
_ "unknown")))
|
|
"""
|
|
|
|
eval(source)
|
|
assert apply(mod, :handle, [{:ok, 42}]) == "ok: 42"
|
|
assert apply(mod, :handle, [{:error, :bad}]) == "error: bad"
|
|
assert apply(mod, :handle, [:other]) == "unknown"
|
|
end
|
|
|
|
test "with chain" do
|
|
result =
|
|
eval("""
|
|
(with [[:ok a] #el[:ok 1]
|
|
[:ok b] #el[:ok 2]
|
|
[:ok c] #el[:ok 3]]
|
|
(+ a (+ b c)))
|
|
""")
|
|
|
|
assert result == 6
|
|
end
|
|
|
|
test "with chain short-circuits" do
|
|
result =
|
|
eval("""
|
|
(with [[:ok a] #el[:ok 1]
|
|
[:ok b] #el[:error :fail]
|
|
[:ok c] #el[:ok 3]]
|
|
(+ a (+ b c)))
|
|
""")
|
|
|
|
assert result == {:error, :fail}
|
|
end
|
|
|
|
test "anonymous function as value" do
|
|
result =
|
|
eval("""
|
|
(let [f (fn [x] (* x x))]
|
|
(Enum/map [1 2 3] f))
|
|
""")
|
|
|
|
assert result == [1, 4, 9]
|
|
end
|
|
|
|
test "nested cond" do
|
|
result =
|
|
eval("""
|
|
(let [x 15]
|
|
(cond
|
|
(> x 20) :high
|
|
(> x 10) :medium
|
|
:else :low))
|
|
""")
|
|
|
|
assert result == :medium
|
|
end
|
|
end
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Literals passthrough
|
|
# ---------------------------------------------------------------------------
|
|
|
|
describe "literals" do
|
|
test "integers" do
|
|
assert eval("42") == 42
|
|
assert eval("-7") == -7
|
|
end
|
|
|
|
test "floats" do
|
|
assert eval("3.14") == 3.14
|
|
end
|
|
|
|
test "strings" do
|
|
assert eval(~S|"hello"|) == "hello"
|
|
end
|
|
|
|
test "keywords" do
|
|
assert eval(":ok") == :ok
|
|
assert eval(":error") == :error
|
|
end
|
|
|
|
test "booleans" do
|
|
assert eval("true") == true
|
|
assert eval("false") == false
|
|
end
|
|
|
|
test "nil" do
|
|
assert eval("nil") == nil
|
|
end
|
|
end
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Context struct
|
|
# ---------------------------------------------------------------------------
|
|
|
|
describe "Context" do
|
|
test "default context" do
|
|
ctx = %Context{}
|
|
assert ctx.module_name == nil
|
|
assert ctx.function_name == nil
|
|
assert ctx.function_arity == nil
|
|
assert ctx.loop_var == nil
|
|
assert ctx.loop_arity == nil
|
|
assert ctx.in_pattern == false
|
|
assert ctx.records == %{}
|
|
assert ctx.gensym_counter == 0
|
|
end
|
|
end
|
|
end
|