Files
CljElixir/test/clj_elixir/phase8_test.exs
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

343 lines
9.3 KiB
Elixir

defmodule CljElixir.Phase8Test do
use ExUnit.Case, async: false
defp eval!(source) do
case CljElixir.Compiler.eval_string(source) do
{:ok, result, _} -> result
{:error, errors} -> raise "Compilation failed: #{inspect(errors)}"
end
end
describe "CljElixir.Printer.pr_str" do
test "nil" do
assert CljElixir.Printer.pr_str(nil) == "nil"
end
test "booleans" do
assert CljElixir.Printer.pr_str(true) == "true"
assert CljElixir.Printer.pr_str(false) == "false"
end
test "integers" do
assert CljElixir.Printer.pr_str(42) == "42"
assert CljElixir.Printer.pr_str(-7) == "-7"
end
test "floats" do
assert CljElixir.Printer.pr_str(3.14) == "3.14"
end
test "strings are quoted" do
assert CljElixir.Printer.pr_str("hello") == "\"hello\""
end
test "strings with escapes" do
assert CljElixir.Printer.pr_str("hello\nworld") == "\"hello\\nworld\""
assert CljElixir.Printer.pr_str("say \"hi\"") == "\"say \\\"hi\\\"\""
end
test "atoms/keywords" do
assert CljElixir.Printer.pr_str(:hello) == ":hello"
assert CljElixir.Printer.pr_str(:ok) == ":ok"
end
test "lists" do
assert CljElixir.Printer.pr_str([1, 2, 3]) == "(1 2 3)"
assert CljElixir.Printer.pr_str([]) == "()"
end
test "maps" do
result = CljElixir.Printer.pr_str(%{name: "Ada"})
assert result =~ ":name"
assert result =~ "\"Ada\""
assert String.starts_with?(result, "{")
assert String.ends_with?(result, "}")
end
test "tuples" do
assert CljElixir.Printer.pr_str({:ok, 42}) == "#el[:ok 42]"
end
test "nested structures" do
result = CljElixir.Printer.pr_str(%{data: [1, 2, {:ok, "hi"}]})
assert result =~ ":data"
assert result =~ "(1 2 #el[:ok \"hi\"])"
end
test "MapSet" do
result = CljElixir.Printer.pr_str(MapSet.new([:a, :b]))
assert String.starts_with?(result, "\#{")
assert String.ends_with?(result, "}")
end
test "module names" do
assert CljElixir.Printer.pr_str(Enum) == "Enum"
assert CljElixir.Printer.pr_str(IO) == "IO"
end
test "pids" do
result = CljElixir.Printer.pr_str(self())
assert String.starts_with?(result, "#PID<")
end
end
describe "CljElixir.Printer.print_str" do
test "strings not quoted" do
assert CljElixir.Printer.print_str("hello") == "hello"
end
test "non-strings same as pr_str" do
assert CljElixir.Printer.print_str(42) == "42"
assert CljElixir.Printer.print_str(:ok) == ":ok"
end
end
describe "pr-str builtin" do
test "pr-str on integer" do
result = eval!("(pr-str 42)")
assert result == "42"
end
test "pr-str on string" do
result = eval!("(pr-str \"hello\")")
assert result == "\"hello\""
end
test "pr-str on keyword" do
result = eval!("(pr-str :foo)")
assert result == ":foo"
end
test "pr-str on list" do
result = eval!("(pr-str (list 1 2 3))")
assert result == "(1 2 3)"
end
test "pr-str on map" do
result = eval!("(pr-str {:a 1})")
assert result =~ ":a"
assert result =~ "1"
end
test "pr-str on tuple" do
result = eval!("(pr-str #el[:ok 42])")
assert result == "#el[:ok 42]"
end
test "pr-str on nil" do
result = eval!("(pr-str nil)")
assert result == "nil"
end
test "pr-str on boolean" do
result = eval!("(pr-str true)")
assert result == "true"
end
test "pr-str multiple args joined with space" do
result = eval!("(pr-str 1 2 3)")
assert result == "1 2 3"
end
end
describe "print-str builtin" do
test "print-str on string (no quotes)" do
result = eval!("(print-str \"hello\")")
assert result == "hello"
end
test "print-str on integer" do
result = eval!("(print-str 42)")
assert result == "42"
end
test "print-str multiple args joined with space" do
result = eval!("(print-str \"hello\" \"world\")")
assert result == "hello world"
end
end
describe "prn builtin" do
test "prn outputs to stdout with newline" do
output =
ExUnit.CaptureIO.capture_io(fn ->
eval!("(prn 42)")
end)
assert String.trim(output) == "42"
end
test "prn with string (quoted)" do
output =
ExUnit.CaptureIO.capture_io(fn ->
eval!("(prn \"hello\")")
end)
assert String.trim(output) == "\"hello\""
end
test "prn multiple args" do
output =
ExUnit.CaptureIO.capture_io(fn ->
eval!("(prn 1 2 3)")
end)
assert String.trim(output) == "1 2 3"
end
end
describe "pr builtin" do
test "pr outputs to stdout without newline" do
output =
ExUnit.CaptureIO.capture_io(fn ->
eval!("(pr 42)")
end)
assert output == "42"
end
test "pr with string (quoted)" do
output =
ExUnit.CaptureIO.capture_io(fn ->
eval!("(pr \"hello\")")
end)
assert output == "\"hello\""
end
test "pr multiple args separated by spaces" do
output =
ExUnit.CaptureIO.capture_io(fn ->
eval!("(pr 1 2 3)")
end)
assert output == "1 2 3"
end
end
# ---------------------------------------------------------------------------
# Source-mapped line/col metadata
# ---------------------------------------------------------------------------
describe "source-mapped metadata" do
test "symbol AST carries line/col from reader" do
{:ok, ast} = CljElixir.Compiler.compile_string("(+ x 1)")
# x should have line: 1 metadata in the Elixir AST
# Walk the AST to find the :x variable
{_, found} = Macro.prewalk(ast, false, fn
{:x, meta, nil} = node, _acc when is_list(meta) ->
{node, meta[:line] == 1}
node, acc ->
{node, acc}
end)
assert found, "variable :x should have line: 1 metadata"
end
test "defmodule AST carries line metadata" do
{:ok, ast} = CljElixir.Compiler.compile_string("(defmodule Foo (defn bar [] 1))")
# The defmodule node should have line: 1
{:defmodule, meta, _} = ast
assert meta[:line] == 1
end
test "defn AST carries line metadata" do
{:ok, ast} = CljElixir.Compiler.compile_string("""
(defmodule MetaTest1
(defn foo [x] x))
""")
# Find the :def node inside the module body
{_, found_line} = Macro.prewalk(ast, nil, fn
{:def, meta, _} = node, nil when is_list(meta) ->
{node, Keyword.get(meta, :line)}
node, acc ->
{node, acc}
end)
assert found_line == 2, "defn should have line: 2 metadata"
end
test "if AST carries line metadata" do
{:ok, ast} = CljElixir.Compiler.compile_string("(if true 1 2)")
{:if, meta, _} = ast
assert meta[:line] == 1
end
test "fn AST carries line metadata" do
{:ok, ast} = CljElixir.Compiler.compile_string("(fn [x] x)")
{:fn, meta, _} = ast
assert meta[:line] == 1
end
test "case AST carries line metadata" do
{:ok, ast} = CljElixir.Compiler.compile_string("(case 1 1 :one 2 :two)")
{:case, meta, _} = ast
assert meta[:line] == 1
end
test "cond AST carries line metadata" do
{:ok, ast} = CljElixir.Compiler.compile_string("(cond true :yes)")
{:cond, meta, _} = ast
assert meta[:line] == 1
end
test "module call AST carries line metadata" do
{:ok, ast} = CljElixir.Compiler.compile_string("(Enum/map [1 2] inc)")
# The outer call should be {{:., meta, [Enum, :map]}, meta, args}
{{:., dot_meta, _}, call_meta, _} = ast
assert dot_meta[:line] == 1
assert call_meta[:line] == 1
end
test "unqualified call AST carries line metadata" do
{:ok, ast} = CljElixir.Compiler.compile_string("""
(defmodule MetaTest2
(defn foo [] (bar 1)))
""")
# Find the :bar call inside the module
{_, found_line} = Macro.prewalk(ast, nil, fn
{:bar, meta, [1]} = node, nil when is_list(meta) ->
{node, meta[:line]}
node, acc ->
{node, acc}
end)
assert found_line == 2, "unqualified call should have line: 2 metadata"
end
test "runtime error has source location" do
# This tests that evaluated code preserves source info
result = CljElixir.Compiler.eval_string("""
(defmodule LineTest1
(defn hello [name]
(str "hello " name)))
(LineTest1/hello "world")
""", file: "line_test.clje")
assert {:ok, "hello world", _} = result
end
test "let block carries line metadata" do
{:ok, ast} = CljElixir.Compiler.compile_string("(let [x 1] x)")
# let produces either a block or a single = expression
case ast do
{:__block__, meta, _} -> assert meta[:line] == 1
{:=, meta, _} -> assert meta[:line] == 1
end
end
test "for comprehension carries line metadata" do
{:ok, ast} = CljElixir.Compiler.compile_string("(for [x [1 2 3]] x)")
{:for, meta, _} = ast
assert meta[:line] == 1
end
test "try carries line metadata" do
{:ok, ast} = CljElixir.Compiler.compile_string("""
(try
(throw "oops")
(catch e (str "caught: " e)))
""")
{:try, meta, _} = ast
assert meta[:line] == 1
end
end
end