- 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>
343 lines
9.3 KiB
Elixir
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
|