Files
Adam 7e82efd7ec Phase 8: REPL, printing, source maps, and nREPL server
- 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>
2026-03-08 11:03:10 -04:00

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