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>
This commit is contained in:
@@ -0,0 +1,96 @@
|
|||||||
|
defmodule CljElixir.NRepl.Bencode do
|
||||||
|
@moduledoc "Bencode encoder/decoder for nREPL wire protocol."
|
||||||
|
|
||||||
|
# --- Encoder ---
|
||||||
|
|
||||||
|
def encode(data) when is_binary(data) do
|
||||||
|
"#{byte_size(data)}:#{data}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def encode(data) when is_integer(data) do
|
||||||
|
"i#{data}e"
|
||||||
|
end
|
||||||
|
|
||||||
|
def encode(data) when is_list(data) do
|
||||||
|
"l" <> Enum.map_join(data, "", &encode/1) <> "e"
|
||||||
|
end
|
||||||
|
|
||||||
|
def encode(data) when is_map(data) do
|
||||||
|
sorted = data |> Enum.sort_by(fn {k, _} -> to_string(k) end)
|
||||||
|
|
||||||
|
"d" <>
|
||||||
|
Enum.map_join(sorted, "", fn {k, v} ->
|
||||||
|
encode(to_string(k)) <> encode(v)
|
||||||
|
end) <> "e"
|
||||||
|
end
|
||||||
|
|
||||||
|
def encode(data) when is_atom(data) do
|
||||||
|
encode(Atom.to_string(data))
|
||||||
|
end
|
||||||
|
|
||||||
|
# --- Decoder ---
|
||||||
|
|
||||||
|
def decode(data) when is_binary(data) do
|
||||||
|
{result, _rest} = decode_one(data)
|
||||||
|
result
|
||||||
|
end
|
||||||
|
|
||||||
|
def decode_all(data) do
|
||||||
|
decode_all(data, [])
|
||||||
|
end
|
||||||
|
|
||||||
|
defp decode_all("", acc), do: Enum.reverse(acc)
|
||||||
|
|
||||||
|
defp decode_all(data, acc) do
|
||||||
|
{result, rest} = decode_one(data)
|
||||||
|
decode_all(rest, [result | acc])
|
||||||
|
end
|
||||||
|
|
||||||
|
# Public so the TCP server can call it for streaming decode
|
||||||
|
def decode_one("i" <> rest) do
|
||||||
|
{num_str, "e" <> rest2} = split_at_char(rest, ?e)
|
||||||
|
{String.to_integer(num_str), rest2}
|
||||||
|
end
|
||||||
|
|
||||||
|
def decode_one("l" <> rest) do
|
||||||
|
decode_list(rest, [])
|
||||||
|
end
|
||||||
|
|
||||||
|
def decode_one("d" <> rest) do
|
||||||
|
decode_dict(rest, %{})
|
||||||
|
end
|
||||||
|
|
||||||
|
def decode_one(data) do
|
||||||
|
# String: <length>:<data>
|
||||||
|
{len_str, ":" <> rest} = split_at_char(data, ?:)
|
||||||
|
len = String.to_integer(len_str)
|
||||||
|
<<str::binary-size(len), rest2::binary>> = rest
|
||||||
|
{str, rest2}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp decode_list("e" <> rest, acc), do: {Enum.reverse(acc), rest}
|
||||||
|
|
||||||
|
defp decode_list(data, acc) do
|
||||||
|
{item, rest} = decode_one(data)
|
||||||
|
decode_list(rest, [item | acc])
|
||||||
|
end
|
||||||
|
|
||||||
|
defp decode_dict("e" <> rest, acc), do: {acc, rest}
|
||||||
|
|
||||||
|
defp decode_dict(data, acc) do
|
||||||
|
{key, rest} = decode_one(data)
|
||||||
|
{value, rest2} = decode_one(rest)
|
||||||
|
decode_dict(rest2, Map.put(acc, key, value))
|
||||||
|
end
|
||||||
|
|
||||||
|
defp split_at_char(data, char) do
|
||||||
|
case :binary.match(data, <<char>>) do
|
||||||
|
{pos, 1} ->
|
||||||
|
<<before::binary-size(pos), _::8, rest::binary>> = data
|
||||||
|
{before, <<char, rest::binary>>}
|
||||||
|
|
||||||
|
:nomatch ->
|
||||||
|
{data, ""}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
defmodule CljElixir.NRepl.Handler do
|
||||||
|
@moduledoc "Handles nREPL protocol messages."
|
||||||
|
|
||||||
|
alias CljElixir.NRepl.SessionManager
|
||||||
|
|
||||||
|
def handle(msg, session_manager) do
|
||||||
|
op = Map.get(msg, "op")
|
||||||
|
id = Map.get(msg, "id", "unknown")
|
||||||
|
session = Map.get(msg, "session")
|
||||||
|
|
||||||
|
case op do
|
||||||
|
"clone" -> handle_clone(id, session, session_manager)
|
||||||
|
"close" -> handle_close(id, session, session_manager)
|
||||||
|
"eval" -> handle_eval(msg, id, session, session_manager)
|
||||||
|
"describe" -> handle_describe(id, session)
|
||||||
|
"ls-sessions" -> handle_ls_sessions(id, session_manager)
|
||||||
|
"load-file" -> handle_load_file(msg, id, session, session_manager)
|
||||||
|
"interrupt" -> handle_interrupt(id, session)
|
||||||
|
"completions" -> handle_completions(msg, id, session)
|
||||||
|
_ -> [%{"id" => id, "session" => session || "", "status" => ["done", "error", "unknown-op"]}]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp handle_clone(id, _session, manager) do
|
||||||
|
new_id = SessionManager.create_session(manager)
|
||||||
|
[%{"id" => id, "new-session" => new_id, "status" => ["done"]}]
|
||||||
|
end
|
||||||
|
|
||||||
|
defp handle_close(id, session, manager) do
|
||||||
|
SessionManager.close_session(manager, session)
|
||||||
|
[%{"id" => id, "session" => session, "status" => ["done"]}]
|
||||||
|
end
|
||||||
|
|
||||||
|
defp handle_eval(msg, id, session, manager) do
|
||||||
|
code = Map.get(msg, "code", "")
|
||||||
|
|
||||||
|
# Capture stdout inside the Agent process where eval actually runs
|
||||||
|
{output, result} = SessionManager.eval_with_capture(manager, session, code)
|
||||||
|
|
||||||
|
responses = []
|
||||||
|
|
||||||
|
# Send stdout if any
|
||||||
|
responses =
|
||||||
|
if output != "" do
|
||||||
|
responses ++ [%{"id" => id, "session" => session, "out" => output}]
|
||||||
|
else
|
||||||
|
responses
|
||||||
|
end
|
||||||
|
|
||||||
|
# Send value or error
|
||||||
|
responses =
|
||||||
|
case result do
|
||||||
|
{:ok, value} ->
|
||||||
|
responses ++ [%{"id" => id, "session" => session, "value" => value, "ns" => "user"}]
|
||||||
|
|
||||||
|
{:error, error} ->
|
||||||
|
responses ++
|
||||||
|
[%{"id" => id, "session" => session, "err" => error, "status" => ["eval-error"]}]
|
||||||
|
end
|
||||||
|
|
||||||
|
# Send done
|
||||||
|
responses ++ [%{"id" => id, "session" => session, "status" => ["done"]}]
|
||||||
|
end
|
||||||
|
|
||||||
|
defp handle_describe(id, session) do
|
||||||
|
[
|
||||||
|
%{
|
||||||
|
"id" => id,
|
||||||
|
"session" => session || "",
|
||||||
|
"status" => ["done"],
|
||||||
|
"ops" => %{
|
||||||
|
"clone" => %{},
|
||||||
|
"close" => %{},
|
||||||
|
"eval" => %{},
|
||||||
|
"describe" => %{},
|
||||||
|
"ls-sessions" => %{},
|
||||||
|
"load-file" => %{},
|
||||||
|
"interrupt" => %{},
|
||||||
|
"completions" => %{}
|
||||||
|
},
|
||||||
|
"versions" => %{
|
||||||
|
"clj-elixir" => %{"major" => 0, "minor" => 1, "incremental" => 0},
|
||||||
|
"nrepl" => %{"major" => 1, "minor" => 0, "incremental" => 0}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
defp handle_ls_sessions(id, manager) do
|
||||||
|
sessions = SessionManager.list_sessions(manager)
|
||||||
|
[%{"id" => id, "sessions" => sessions, "status" => ["done"]}]
|
||||||
|
end
|
||||||
|
|
||||||
|
defp handle_load_file(msg, id, session, manager) do
|
||||||
|
code = Map.get(msg, "file", "")
|
||||||
|
handle_eval(Map.put(msg, "code", code), id, session, manager)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp handle_interrupt(id, session) do
|
||||||
|
# Not truly implemented -- just acknowledge
|
||||||
|
[%{"id" => id, "session" => session, "status" => ["done"]}]
|
||||||
|
end
|
||||||
|
|
||||||
|
defp handle_completions(msg, id, session) do
|
||||||
|
_prefix = Map.get(msg, "prefix", "")
|
||||||
|
# Basic completions -- return empty for now, can be extended later
|
||||||
|
[%{"id" => id, "session" => session || "", "completions" => [], "status" => ["done"]}]
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
defmodule CljElixir.NRepl.Server do
|
||||||
|
@moduledoc """
|
||||||
|
nREPL TCP server for CljElixir.
|
||||||
|
|
||||||
|
Listens on a configurable port and handles nREPL protocol messages
|
||||||
|
over TCP using Bencode encoding.
|
||||||
|
"""
|
||||||
|
|
||||||
|
use GenServer
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
alias CljElixir.NRepl.{Bencode, Handler, SessionManager}
|
||||||
|
|
||||||
|
defstruct [:listen_socket, :port, :session_manager]
|
||||||
|
|
||||||
|
def start_link(opts \\ []) do
|
||||||
|
port = Keyword.get(opts, :port, 0)
|
||||||
|
GenServer.start_link(__MODULE__, port, opts)
|
||||||
|
end
|
||||||
|
|
||||||
|
def port(server) do
|
||||||
|
GenServer.call(server, :port)
|
||||||
|
end
|
||||||
|
|
||||||
|
# --- Callbacks ---
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def init(port) do
|
||||||
|
{:ok, manager} = SessionManager.start_link()
|
||||||
|
|
||||||
|
# Open TCP listener
|
||||||
|
opts = [:binary, packet: :raw, active: false, reuseaddr: true]
|
||||||
|
|
||||||
|
case :gen_tcp.listen(port, opts) do
|
||||||
|
{:ok, listen_socket} ->
|
||||||
|
# Get actual port (important when port=0)
|
||||||
|
{:ok, actual_port} = :inet.port(listen_socket)
|
||||||
|
|
||||||
|
state = %__MODULE__{
|
||||||
|
listen_socket: listen_socket,
|
||||||
|
port: actual_port,
|
||||||
|
session_manager: manager
|
||||||
|
}
|
||||||
|
|
||||||
|
# Start accepting connections in a separate process
|
||||||
|
spawn_link(fn -> accept_loop(listen_socket, manager) end)
|
||||||
|
|
||||||
|
Logger.info("nREPL server started on port #{actual_port}")
|
||||||
|
{:ok, state}
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
{:stop, reason}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_call(:port, _from, state) do
|
||||||
|
{:reply, state.port, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def terminate(_reason, state) do
|
||||||
|
if state.listen_socket do
|
||||||
|
:gen_tcp.close(state.listen_socket)
|
||||||
|
end
|
||||||
|
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
|
||||||
|
# --- Connection handling ---
|
||||||
|
|
||||||
|
defp accept_loop(listen_socket, manager) do
|
||||||
|
case :gen_tcp.accept(listen_socket) do
|
||||||
|
{:ok, client_socket} ->
|
||||||
|
spawn(fn -> handle_connection(client_socket, manager) end)
|
||||||
|
accept_loop(listen_socket, manager)
|
||||||
|
|
||||||
|
{:error, :closed} ->
|
||||||
|
:ok
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
Logger.error("Accept error: #{inspect(reason)}")
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp handle_connection(socket, manager) do
|
||||||
|
case read_message(socket, "") do
|
||||||
|
{:ok, msg, rest} ->
|
||||||
|
responses = Handler.handle(msg, manager)
|
||||||
|
|
||||||
|
Enum.each(responses, fn response ->
|
||||||
|
encoded = Bencode.encode(response)
|
||||||
|
:gen_tcp.send(socket, encoded)
|
||||||
|
end)
|
||||||
|
|
||||||
|
handle_connection_with_buffer(socket, manager, rest)
|
||||||
|
|
||||||
|
{:error, _reason} ->
|
||||||
|
:gen_tcp.close(socket)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp handle_connection_with_buffer(socket, manager, buffer) do
|
||||||
|
case try_decode(buffer) do
|
||||||
|
{:ok, msg, rest} ->
|
||||||
|
responses = Handler.handle(msg, manager)
|
||||||
|
|
||||||
|
Enum.each(responses, fn response ->
|
||||||
|
encoded = Bencode.encode(response)
|
||||||
|
:gen_tcp.send(socket, encoded)
|
||||||
|
end)
|
||||||
|
|
||||||
|
handle_connection_with_buffer(socket, manager, rest)
|
||||||
|
|
||||||
|
:incomplete ->
|
||||||
|
case :gen_tcp.recv(socket, 0) do
|
||||||
|
{:ok, data} ->
|
||||||
|
handle_connection_with_buffer(socket, manager, buffer <> data)
|
||||||
|
|
||||||
|
{:error, _} ->
|
||||||
|
:gen_tcp.close(socket)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp read_message(socket, buffer) do
|
||||||
|
case try_decode(buffer) do
|
||||||
|
{:ok, msg, rest} ->
|
||||||
|
{:ok, msg, rest}
|
||||||
|
|
||||||
|
:incomplete ->
|
||||||
|
case :gen_tcp.recv(socket, 0) do
|
||||||
|
{:ok, data} -> read_message(socket, buffer <> data)
|
||||||
|
{:error, reason} -> {:error, reason}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp try_decode(data) when byte_size(data) == 0, do: :incomplete
|
||||||
|
|
||||||
|
defp try_decode(data) do
|
||||||
|
try do
|
||||||
|
{msg, rest} = Bencode.decode_one(data)
|
||||||
|
{:ok, msg, rest}
|
||||||
|
rescue
|
||||||
|
_ -> :incomplete
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
defmodule CljElixir.NRepl.SessionManager do
|
||||||
|
@moduledoc "Manages nREPL sessions with independent REPL state."
|
||||||
|
|
||||||
|
use GenServer
|
||||||
|
|
||||||
|
def start_link(opts \\ []) do
|
||||||
|
GenServer.start_link(__MODULE__, %{}, opts)
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def init(_) do
|
||||||
|
{:ok, %{sessions: %{}}}
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_session(manager) do
|
||||||
|
GenServer.call(manager, :create_session)
|
||||||
|
end
|
||||||
|
|
||||||
|
def close_session(manager, session_id) do
|
||||||
|
GenServer.call(manager, {:close_session, session_id})
|
||||||
|
end
|
||||||
|
|
||||||
|
def list_sessions(manager) do
|
||||||
|
GenServer.call(manager, :list_sessions)
|
||||||
|
end
|
||||||
|
|
||||||
|
def eval(manager, session_id, code) do
|
||||||
|
GenServer.call(manager, {:eval, session_id, code}, :infinity)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc "Eval with stdout capture. Returns {output, result} where result is {:ok, value} | {:error, error}."
|
||||||
|
def eval_with_capture(manager, session_id, code) do
|
||||||
|
GenServer.call(manager, {:eval_with_capture, session_id, code}, :infinity)
|
||||||
|
end
|
||||||
|
|
||||||
|
# --- Callbacks ---
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_call(:create_session, _from, state) do
|
||||||
|
id = generate_uuid()
|
||||||
|
{:ok, pid} = Agent.start_link(fn -> CljElixir.REPL.new() end)
|
||||||
|
new_sessions = Map.put(state.sessions, id, pid)
|
||||||
|
{:reply, id, %{state | sessions: new_sessions}}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_call({:close_session, id}, _from, state) do
|
||||||
|
case Map.pop(state.sessions, id) do
|
||||||
|
{nil, _} ->
|
||||||
|
{:reply, :not_found, state}
|
||||||
|
|
||||||
|
{pid, new_sessions} ->
|
||||||
|
Agent.stop(pid)
|
||||||
|
{:reply, :ok, %{state | sessions: new_sessions}}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_call(:list_sessions, _from, state) do
|
||||||
|
{:reply, Map.keys(state.sessions), state}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_call({:eval, id, code}, _from, state) do
|
||||||
|
case Map.get(state.sessions, id) do
|
||||||
|
nil ->
|
||||||
|
{:reply, {:error, "unknown session"}, state}
|
||||||
|
|
||||||
|
pid ->
|
||||||
|
result =
|
||||||
|
Agent.get_and_update(
|
||||||
|
pid,
|
||||||
|
fn repl_state ->
|
||||||
|
case CljElixir.REPL.eval(code, repl_state) do
|
||||||
|
{:ok, value, new_state} -> {{:ok, value}, new_state}
|
||||||
|
{:error, error, new_state} -> {{:error, error}, new_state}
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
:infinity
|
||||||
|
)
|
||||||
|
|
||||||
|
{:reply, result, state}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_call({:eval_with_capture, id, code}, _from, state) do
|
||||||
|
case Map.get(state.sessions, id) do
|
||||||
|
nil ->
|
||||||
|
{:reply, {"", {:error, "unknown session"}}, state}
|
||||||
|
|
||||||
|
pid ->
|
||||||
|
{output, result} =
|
||||||
|
Agent.get_and_update(
|
||||||
|
pid,
|
||||||
|
fn repl_state ->
|
||||||
|
{output, eval_result, new_state} = eval_capturing_output(code, repl_state)
|
||||||
|
|
||||||
|
{{output, eval_result}, new_state}
|
||||||
|
end,
|
||||||
|
:infinity
|
||||||
|
)
|
||||||
|
|
||||||
|
{:reply, {output, result}, state}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Evaluate code while capturing stdout within the current process
|
||||||
|
defp eval_capturing_output(code, repl_state) do
|
||||||
|
{:ok, string_io} = StringIO.open("")
|
||||||
|
old_gl = Process.group_leader()
|
||||||
|
Process.group_leader(self(), string_io)
|
||||||
|
|
||||||
|
try do
|
||||||
|
case CljElixir.REPL.eval(code, repl_state) do
|
||||||
|
{:ok, value, new_state} ->
|
||||||
|
Process.group_leader(self(), old_gl)
|
||||||
|
{_input, output} = StringIO.contents(string_io)
|
||||||
|
StringIO.close(string_io)
|
||||||
|
{output, {:ok, value}, new_state}
|
||||||
|
|
||||||
|
{:error, error, new_state} ->
|
||||||
|
Process.group_leader(self(), old_gl)
|
||||||
|
{_input, output} = StringIO.contents(string_io)
|
||||||
|
StringIO.close(string_io)
|
||||||
|
{output, {:error, error}, new_state}
|
||||||
|
end
|
||||||
|
rescue
|
||||||
|
e ->
|
||||||
|
Process.group_leader(self(), old_gl)
|
||||||
|
StringIO.close(string_io)
|
||||||
|
{"", {:error, Exception.message(e)}, repl_state}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Simple UUID v4 generation using :crypto (OTP built-in, no deps)
|
||||||
|
defp generate_uuid do
|
||||||
|
<<a::32, b::16, c::16, d::16, e::48>> = :crypto.strong_rand_bytes(16)
|
||||||
|
|
||||||
|
:io_lib.format("~8.16.0b-~4.16.0b-4~3.16.0b-~4.16.0b-~12.16.0b", [
|
||||||
|
a,
|
||||||
|
b,
|
||||||
|
Bitwise.band(c, 0x0FFF),
|
||||||
|
Bitwise.bor(Bitwise.band(d, 0x3FFF), 0x8000),
|
||||||
|
e
|
||||||
|
])
|
||||||
|
|> IO.iodata_to_binary()
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
defmodule CljElixir.Printer do
|
||||||
|
@moduledoc """
|
||||||
|
Machine-readable printing for CljElixir values.
|
||||||
|
|
||||||
|
Produces EDN-compatible string representations:
|
||||||
|
- Strings: `"\"hello\""`
|
||||||
|
- Keywords/atoms: `":name"`
|
||||||
|
- Integers: `"42"`
|
||||||
|
- Maps: `"{:name \"Ada\", :age 30}"`
|
||||||
|
- Lists: `"(1 2 3)"`
|
||||||
|
- Vectors (PersistentVector): `"[1 2 3]"`
|
||||||
|
- Tuples: `"#el[:ok \"data\"]"`
|
||||||
|
- Sets (MapSet): `"\#{:a :b :c}"`
|
||||||
|
- nil: `"nil"`
|
||||||
|
- Booleans: `"true"` / `"false"`
|
||||||
|
- Records (structs): `"#RecordName{:field val, ...}"`
|
||||||
|
"""
|
||||||
|
|
||||||
|
@doc "Convert value to machine-readable string (EDN-like)"
|
||||||
|
def pr_str(value) do
|
||||||
|
# Check if the value implements IPrintWithWriter protocol.
|
||||||
|
# If so, use it. Otherwise, use built-in printing.
|
||||||
|
if protocol_implemented?(value) do
|
||||||
|
try do
|
||||||
|
CljElixir.IPrintWithWriter.pr_writer(value, nil, nil)
|
||||||
|
rescue
|
||||||
|
_ -> do_pr_str(value)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
do_pr_str(value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc "Convert value to human-readable string"
|
||||||
|
def print_str(value) do
|
||||||
|
do_print_str(value)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Check if IPrintWithWriter is compiled and implemented for this value
|
||||||
|
defp protocol_implemented?(value) do
|
||||||
|
case Code.ensure_loaded(CljElixir.IPrintWithWriter) do
|
||||||
|
{:module, _} ->
|
||||||
|
CljElixir.IPrintWithWriter.impl_for(value) != nil
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# --- Machine-readable (EDN) ---
|
||||||
|
|
||||||
|
defp do_pr_str(nil), do: "nil"
|
||||||
|
defp do_pr_str(true), do: "true"
|
||||||
|
defp do_pr_str(false), do: "false"
|
||||||
|
|
||||||
|
defp do_pr_str(n) when is_integer(n), do: Integer.to_string(n)
|
||||||
|
defp do_pr_str(n) when is_float(n), do: Float.to_string(n)
|
||||||
|
|
||||||
|
defp do_pr_str(s) when is_binary(s) do
|
||||||
|
"\"" <> escape_string(s) <> "\""
|
||||||
|
end
|
||||||
|
|
||||||
|
defp do_pr_str(a) when is_atom(a) do
|
||||||
|
name = Atom.to_string(a)
|
||||||
|
|
||||||
|
if String.starts_with?(name, "Elixir.") do
|
||||||
|
# Module name - strip Elixir. prefix
|
||||||
|
String.replace_prefix(name, "Elixir.", "")
|
||||||
|
else
|
||||||
|
":" <> name
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp do_pr_str(list) when is_list(list) do
|
||||||
|
"(" <> Enum.map_join(list, " ", &pr_str/1) <> ")"
|
||||||
|
end
|
||||||
|
|
||||||
|
defp do_pr_str(%MapSet{} = set) do
|
||||||
|
"\#{" <> Enum.map_join(set, " ", &pr_str/1) <> "}"
|
||||||
|
end
|
||||||
|
|
||||||
|
# Struct handling - use runtime __struct__ check for PersistentVector/SubVector
|
||||||
|
# since they are .clje modules not available at compile time.
|
||||||
|
defp do_pr_str(%{__struct__: struct_mod} = struct) do
|
||||||
|
struct_name = Atom.to_string(struct_mod)
|
||||||
|
|
||||||
|
cond do
|
||||||
|
String.ends_with?(struct_name, "PersistentVector") ->
|
||||||
|
# PersistentVector - print as [...]
|
||||||
|
elements = apply(struct_mod, :to_list, [struct])
|
||||||
|
"[" <> Enum.map_join(elements, " ", &pr_str/1) <> "]"
|
||||||
|
|
||||||
|
String.ends_with?(struct_name, "SubVector") ->
|
||||||
|
# SubVector - print as [...]
|
||||||
|
elements = apply(struct_mod, :sv_to_list, [struct])
|
||||||
|
"[" <> Enum.map_join(elements, " ", &pr_str/1) <> "]"
|
||||||
|
|
||||||
|
true ->
|
||||||
|
# Generic struct/record
|
||||||
|
mod_name = struct_mod |> Module.split() |> List.last()
|
||||||
|
fields = struct |> Map.from_struct() |> Map.to_list()
|
||||||
|
|
||||||
|
"#" <>
|
||||||
|
mod_name <>
|
||||||
|
"{" <>
|
||||||
|
Enum.map_join(fields, ", ", fn {k, v} -> pr_str(k) <> " " <> pr_str(v) end) <>
|
||||||
|
"}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Plain map
|
||||||
|
defp do_pr_str(map) when is_map(map) do
|
||||||
|
"{" <>
|
||||||
|
Enum.map_join(map, ", ", fn {k, v} -> pr_str(k) <> " " <> pr_str(v) end) <>
|
||||||
|
"}"
|
||||||
|
end
|
||||||
|
|
||||||
|
defp do_pr_str(tuple) when is_tuple(tuple) do
|
||||||
|
elements = Tuple.to_list(tuple)
|
||||||
|
"#el[" <> Enum.map_join(elements, " ", &pr_str/1) <> "]"
|
||||||
|
end
|
||||||
|
|
||||||
|
defp do_pr_str(pid) when is_pid(pid), do: inspect(pid)
|
||||||
|
defp do_pr_str(ref) when is_reference(ref), do: inspect(ref)
|
||||||
|
defp do_pr_str(port) when is_port(port), do: inspect(port)
|
||||||
|
defp do_pr_str(fun) when is_function(fun), do: inspect(fun)
|
||||||
|
|
||||||
|
defp do_pr_str(other), do: inspect(other)
|
||||||
|
|
||||||
|
# --- Human-readable ---
|
||||||
|
|
||||||
|
defp do_print_str(s) when is_binary(s), do: s
|
||||||
|
defp do_print_str(other), do: do_pr_str(other)
|
||||||
|
|
||||||
|
# --- String escaping ---
|
||||||
|
|
||||||
|
defp escape_string(s) do
|
||||||
|
s
|
||||||
|
|> String.replace("\\", "\\\\")
|
||||||
|
|> String.replace("\"", "\\\"")
|
||||||
|
|> String.replace("\n", "\\n")
|
||||||
|
|> String.replace("\t", "\\t")
|
||||||
|
|> String.replace("\r", "\\r")
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
defmodule CljElixir.REPL do
|
||||||
|
@moduledoc """
|
||||||
|
CljElixir Read-Eval-Print Loop engine.
|
||||||
|
|
||||||
|
Maintains state across evaluations: bindings persist,
|
||||||
|
modules defined in one evaluation are available in the next.
|
||||||
|
"""
|
||||||
|
|
||||||
|
defstruct bindings: [],
|
||||||
|
history: [],
|
||||||
|
counter: 1,
|
||||||
|
env: nil
|
||||||
|
|
||||||
|
@doc "Create a new REPL state"
|
||||||
|
def new do
|
||||||
|
%__MODULE__{}
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Evaluate a CljElixir source string in the given REPL state.
|
||||||
|
Returns {:ok, result_string, new_state} or {:error, error_string, new_state}.
|
||||||
|
"""
|
||||||
|
def eval(source, state) do
|
||||||
|
opts = [
|
||||||
|
bindings: state.bindings,
|
||||||
|
file: "repl"
|
||||||
|
]
|
||||||
|
|
||||||
|
case CljElixir.Compiler.eval_string(source, opts) do
|
||||||
|
{:ok, result, new_bindings} ->
|
||||||
|
result_str = CljElixir.Printer.pr_str(result)
|
||||||
|
|
||||||
|
new_state = %{state |
|
||||||
|
bindings: new_bindings,
|
||||||
|
history: [source | state.history],
|
||||||
|
counter: state.counter + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
{:ok, result_str, new_state}
|
||||||
|
|
||||||
|
{:error, errors} ->
|
||||||
|
error_str = format_errors(errors)
|
||||||
|
new_state = %{state | counter: state.counter + 1}
|
||||||
|
{:error, error_str, new_state}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc "Check if input has balanced delimiters (parens, brackets, braces)"
|
||||||
|
def balanced?(input) do
|
||||||
|
input
|
||||||
|
|> String.graphemes()
|
||||||
|
|> count_delimiters(0, 0, 0, false, false)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Count open/close delimiters, respecting strings and comments
|
||||||
|
defp count_delimiters([], parens, brackets, braces, _in_string, _escape) do
|
||||||
|
parens == 0 and brackets == 0 and braces == 0
|
||||||
|
end
|
||||||
|
|
||||||
|
defp count_delimiters([char | rest], p, b, br, in_string, escape) do
|
||||||
|
cond do
|
||||||
|
escape ->
|
||||||
|
# Previous char was \, skip this one
|
||||||
|
count_delimiters(rest, p, b, br, in_string, false)
|
||||||
|
|
||||||
|
char == "\\" and in_string ->
|
||||||
|
count_delimiters(rest, p, b, br, in_string, true)
|
||||||
|
|
||||||
|
char == "\"" and not in_string ->
|
||||||
|
count_delimiters(rest, p, b, br, true, false)
|
||||||
|
|
||||||
|
char == "\"" and in_string ->
|
||||||
|
count_delimiters(rest, p, b, br, false, false)
|
||||||
|
|
||||||
|
in_string ->
|
||||||
|
count_delimiters(rest, p, b, br, true, false)
|
||||||
|
|
||||||
|
char == ";" ->
|
||||||
|
# Comment - skip rest of line
|
||||||
|
rest_after_newline = Enum.drop_while(rest, &(&1 != "\n"))
|
||||||
|
count_delimiters(rest_after_newline, p, b, br, false, false)
|
||||||
|
|
||||||
|
char == "(" -> count_delimiters(rest, p + 1, b, br, false, false)
|
||||||
|
char == ")" -> count_delimiters(rest, p - 1, b, br, false, false)
|
||||||
|
char == "[" -> count_delimiters(rest, p, b + 1, br, false, false)
|
||||||
|
char == "]" -> count_delimiters(rest, p, b - 1, br, false, false)
|
||||||
|
char == "{" -> count_delimiters(rest, p, b, br + 1, false, false)
|
||||||
|
char == "}" -> count_delimiters(rest, p, b, br - 1, false, false)
|
||||||
|
|
||||||
|
true -> count_delimiters(rest, p, b, br, false, false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp format_errors(errors) when is_list(errors) do
|
||||||
|
Enum.map_join(errors, "\n", fn
|
||||||
|
%{message: msg, line: line} when is_integer(line) and line > 0 ->
|
||||||
|
"Error on line #{line}: #{msg}"
|
||||||
|
|
||||||
|
%{message: msg} ->
|
||||||
|
"Error: #{msg}"
|
||||||
|
|
||||||
|
other ->
|
||||||
|
"Error: #{inspect(other)}"
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp format_errors(other) do
|
||||||
|
"Error: #{inspect(other)}"
|
||||||
|
end
|
||||||
|
end
|
||||||
+202
-69
@@ -199,12 +199,12 @@ defmodule CljElixir.Transformer do
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
# Dynamic vars
|
# Dynamic vars
|
||||||
defp transform_symbol("*self*", _meta, ctx) do
|
defp transform_symbol("*self*", meta, ctx) do
|
||||||
{{:self, [], []}, ctx}
|
{{:self, elixir_meta(meta), []}, ctx}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp transform_symbol("*node*", _meta, ctx) do
|
defp transform_symbol("*node*", meta, ctx) do
|
||||||
{{:node, [], []}, ctx}
|
{{:node, elixir_meta(meta), []}, ctx}
|
||||||
end
|
end
|
||||||
|
|
||||||
# true/false/nil are also possible as symbols
|
# true/false/nil are also possible as symbols
|
||||||
@@ -213,10 +213,10 @@ defmodule CljElixir.Transformer do
|
|||||||
defp transform_symbol("nil", _meta, ctx), do: {nil, ctx}
|
defp transform_symbol("nil", _meta, ctx), do: {nil, ctx}
|
||||||
|
|
||||||
# Module-qualified symbol like Enum/map or io/format
|
# Module-qualified symbol like Enum/map or io/format
|
||||||
defp transform_symbol(name, _meta, ctx) when is_binary(name) do
|
defp transform_symbol(name, meta, ctx) when is_binary(name) do
|
||||||
cond do
|
cond do
|
||||||
String.contains?(name, "/") ->
|
String.contains?(name, "/") ->
|
||||||
transform_module_call_symbol(name, ctx)
|
transform_module_call_symbol(name, meta, ctx)
|
||||||
|
|
||||||
module_reference?(name) ->
|
module_reference?(name) ->
|
||||||
# Bare module reference (e.g., CljElixir.SubVector as a value)
|
# Bare module reference (e.g., CljElixir.SubVector as a value)
|
||||||
@@ -226,7 +226,7 @@ defmodule CljElixir.Transformer do
|
|||||||
# Plain variable
|
# Plain variable
|
||||||
munged = munge_name(name)
|
munged = munge_name(name)
|
||||||
atom_name = String.to_atom(munged)
|
atom_name = String.to_atom(munged)
|
||||||
{{atom_name, [], nil}, ctx}
|
{{atom_name, elixir_meta(meta), nil}, ctx}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -239,10 +239,11 @@ defmodule CljElixir.Transformer do
|
|||||||
String.contains?(name, ".")
|
String.contains?(name, ".")
|
||||||
end
|
end
|
||||||
|
|
||||||
defp transform_module_call_symbol(name, ctx) do
|
defp transform_module_call_symbol(name, meta, ctx) do
|
||||||
{mod_ast, fun_atom} = parse_module_function(name)
|
{mod_ast, fun_atom} = parse_module_function(name)
|
||||||
|
em = elixir_meta(meta)
|
||||||
# Bare qualified symbol → zero-arg call (e.g., Enum/count as value)
|
# Bare qualified symbol → zero-arg call (e.g., Enum/count as value)
|
||||||
ast = {{:., [], [mod_ast, fun_atom]}, [], []}
|
ast = {{:., em, [mod_ast, fun_atom]}, em, []}
|
||||||
{ast, ctx}
|
{ast, ctx}
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -317,6 +318,10 @@ defmodule CljElixir.Transformer do
|
|||||||
{:symbol, _, "dec"} -> transform_dec(args, ctx)
|
{:symbol, _, "dec"} -> transform_dec(args, ctx)
|
||||||
{:symbol, _, "str"} -> transform_str(args, ctx)
|
{:symbol, _, "str"} -> transform_str(args, ctx)
|
||||||
{:symbol, _, "println"} -> transform_println(args, ctx)
|
{:symbol, _, "println"} -> transform_println(args, ctx)
|
||||||
|
{:symbol, _, "pr-str"} -> transform_pr_str(args, ctx)
|
||||||
|
{:symbol, _, "pr"} -> transform_pr(args, ctx)
|
||||||
|
{:symbol, _, "prn"} -> transform_prn(args, ctx)
|
||||||
|
{:symbol, _, "print-str"} -> transform_print_str(args, ctx)
|
||||||
{:symbol, _, "nil?"} -> transform_nil_check(args, ctx)
|
{:symbol, _, "nil?"} -> transform_nil_check(args, ctx)
|
||||||
{:symbol, _, "throw"} -> transform_throw(args, ctx)
|
{:symbol, _, "throw"} -> transform_throw(args, ctx)
|
||||||
{:symbol, _, "count"} -> transform_count(args, ctx)
|
{:symbol, _, "count"} -> transform_count(args, ctx)
|
||||||
@@ -382,7 +387,7 @@ defmodule CljElixir.Transformer do
|
|||||||
|
|
||||||
# --- Keyword-as-function: (:name user) ---
|
# --- Keyword-as-function: (:name user) ---
|
||||||
kw when is_atom(kw) ->
|
kw when is_atom(kw) ->
|
||||||
transform_keyword_call(kw, args, ctx)
|
transform_keyword_call(kw, args, meta, ctx)
|
||||||
|
|
||||||
# --- Module/function calls from symbol with / ---
|
# --- Module/function calls from symbol with / ---
|
||||||
{:symbol, _, name} when is_binary(name) ->
|
{:symbol, _, name} when is_binary(name) ->
|
||||||
@@ -390,18 +395,18 @@ defmodule CljElixir.Transformer do
|
|||||||
expand_macro(name, args, ctx)
|
expand_macro(name, args, ctx)
|
||||||
else
|
else
|
||||||
if String.contains?(name, "/") do
|
if String.contains?(name, "/") do
|
||||||
transform_module_call(name, args, ctx)
|
transform_module_call(name, args, meta, ctx)
|
||||||
else
|
else
|
||||||
# Check for ->Constructor and map->Constructor
|
# Check for ->Constructor and map->Constructor
|
||||||
cond do
|
cond do
|
||||||
String.starts_with?(name, "->") and not String.starts_with?(name, "->>") ->
|
String.starts_with?(name, "->") and not String.starts_with?(name, "->>") ->
|
||||||
transform_positional_constructor(name, args, ctx)
|
transform_positional_constructor(name, args, meta, ctx)
|
||||||
|
|
||||||
String.starts_with?(name, "map->") ->
|
String.starts_with?(name, "map->") ->
|
||||||
transform_map_constructor(name, args, ctx)
|
transform_map_constructor(name, args, meta, ctx)
|
||||||
|
|
||||||
true ->
|
true ->
|
||||||
transform_unqualified_call(name, args, ctx)
|
transform_unqualified_call(name, args, meta, ctx)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -418,7 +423,7 @@ defmodule CljElixir.Transformer do
|
|||||||
# 1. defmodule
|
# 1. defmodule
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
defp transform_defmodule(args, _meta, ctx) do
|
defp transform_defmodule(args, meta, ctx) do
|
||||||
{name_form, rest} = extract_name(args)
|
{name_form, rest} = extract_name(args)
|
||||||
mod_alias = module_name_ast(name_form)
|
mod_alias = module_name_ast(name_form)
|
||||||
|
|
||||||
@@ -459,7 +464,7 @@ defmodule CljElixir.Transformer do
|
|||||||
end
|
end
|
||||||
|
|
||||||
ast =
|
ast =
|
||||||
{:defmodule, [context: Elixir],
|
{:defmodule, [context: Elixir] ++ elixir_meta(meta),
|
||||||
[mod_alias, [do: block]]}
|
[mod_alias, [do: block]]}
|
||||||
|
|
||||||
{ast, ctx}
|
{ast, ctx}
|
||||||
@@ -469,7 +474,7 @@ defmodule CljElixir.Transformer do
|
|||||||
# 2. defn / defn-
|
# 2. defn / defn-
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
defp transform_defn(args, _meta, ctx, kind) do
|
defp transform_defn(args, meta, ctx, kind) do
|
||||||
{name_form, rest} = extract_name(args)
|
{name_form, rest} = extract_name(args)
|
||||||
fun_name = symbol_to_atom(name_form)
|
fun_name = symbol_to_atom(name_form)
|
||||||
|
|
||||||
@@ -485,6 +490,7 @@ defmodule CljElixir.Transformer do
|
|||||||
clauses = parse_defn_clauses(rest)
|
clauses = parse_defn_clauses(rest)
|
||||||
|
|
||||||
def_kind = if kind == :def, do: :def, else: :defp
|
def_kind = if kind == :def, do: :def, else: :defp
|
||||||
|
em = elixir_meta(meta)
|
||||||
|
|
||||||
doc_ast =
|
doc_ast =
|
||||||
if doc do
|
if doc do
|
||||||
@@ -519,12 +525,12 @@ defmodule CljElixir.Transformer do
|
|||||||
clause =
|
clause =
|
||||||
case guard do
|
case guard do
|
||||||
nil ->
|
nil ->
|
||||||
{def_kind, [], [call_with_args(fun_name, param_asts), [do: body_ast]]}
|
{def_kind, em, [call_with_args(fun_name, param_asts), [do: body_ast]]}
|
||||||
|
|
||||||
guard_form ->
|
guard_form ->
|
||||||
guard_ast = transform(guard_form, fn_ctx)
|
guard_ast = transform(guard_form, fn_ctx)
|
||||||
|
|
||||||
{def_kind, [],
|
{def_kind, em,
|
||||||
[
|
[
|
||||||
{:when, [],
|
{:when, [],
|
||||||
[call_with_args(fun_name, param_asts), guard_ast]},
|
[call_with_args(fun_name, param_asts), guard_ast]},
|
||||||
@@ -600,8 +606,9 @@ defmodule CljElixir.Transformer do
|
|||||||
# 3. fn
|
# 3. fn
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
defp transform_fn(args, _meta, ctx) do
|
defp transform_fn(args, meta, ctx) do
|
||||||
clauses = parse_fn_clauses(args)
|
clauses = parse_fn_clauses(args)
|
||||||
|
em = elixir_meta(meta)
|
||||||
|
|
||||||
fn_clauses =
|
fn_clauses =
|
||||||
Enum.flat_map(clauses, fn {params, rest_param, guard, body_forms} ->
|
Enum.flat_map(clauses, fn {params, rest_param, guard, body_forms} ->
|
||||||
@@ -632,7 +639,7 @@ defmodule CljElixir.Transformer do
|
|||||||
[clause]
|
[clause]
|
||||||
end)
|
end)
|
||||||
|
|
||||||
{{:fn, [], fn_clauses}, ctx}
|
{{:fn, em, fn_clauses}, ctx}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp parse_fn_clauses(args) do
|
defp parse_fn_clauses(args) do
|
||||||
@@ -739,15 +746,16 @@ defmodule CljElixir.Transformer do
|
|||||||
# 5. let
|
# 5. let
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
defp transform_let([{:vector, _, bindings} | body], _meta, ctx) do
|
defp transform_let([{:vector, _, bindings} | body], meta, ctx) do
|
||||||
binding_pairs = Enum.chunk_every(bindings, 2)
|
binding_pairs = Enum.chunk_every(bindings, 2)
|
||||||
|
em = elixir_meta(meta)
|
||||||
|
|
||||||
{binding_asts, final_ctx} =
|
{binding_asts, final_ctx} =
|
||||||
Enum.map_reduce(binding_pairs, ctx, fn [pattern, expr], acc ->
|
Enum.map_reduce(binding_pairs, ctx, fn [pattern, expr], acc ->
|
||||||
pattern_ctx = %{acc | in_pattern: true}
|
pattern_ctx = %{acc | in_pattern: true}
|
||||||
pat_ast = transform(pattern, pattern_ctx)
|
pat_ast = transform(pattern, pattern_ctx)
|
||||||
expr_ast = transform(expr, acc)
|
expr_ast = transform(expr, acc)
|
||||||
match_ast = {:=, [], [pat_ast, expr_ast]}
|
match_ast = {:=, em, [pat_ast, expr_ast]}
|
||||||
{match_ast, acc}
|
{match_ast, acc}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -757,7 +765,7 @@ defmodule CljElixir.Transformer do
|
|||||||
ast =
|
ast =
|
||||||
case all do
|
case all do
|
||||||
[single] -> single
|
[single] -> single
|
||||||
multiple -> {:__block__, [], multiple}
|
multiple -> {:__block__, em, multiple}
|
||||||
end
|
end
|
||||||
|
|
||||||
{ast, ctx}
|
{ast, ctx}
|
||||||
@@ -767,7 +775,7 @@ defmodule CljElixir.Transformer do
|
|||||||
# 6. if / when / cond / case / do
|
# 6. if / when / cond / case / do
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
defp transform_if([test, then_form | else_form], _meta, ctx) do
|
defp transform_if([test, then_form | else_form], meta, ctx) do
|
||||||
test_ast = transform(test, ctx)
|
test_ast = transform(test, ctx)
|
||||||
then_ast = transform(then_form, ctx)
|
then_ast = transform(then_form, ctx)
|
||||||
|
|
||||||
@@ -777,18 +785,18 @@ defmodule CljElixir.Transformer do
|
|||||||
[] -> nil
|
[] -> nil
|
||||||
end
|
end
|
||||||
|
|
||||||
ast = {:if, [], [test_ast, [do: then_ast, else: else_ast]]}
|
ast = {:if, elixir_meta(meta), [test_ast, [do: then_ast, else: else_ast]]}
|
||||||
{ast, ctx}
|
{ast, ctx}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp transform_when([test | body], _meta, ctx) do
|
defp transform_when([test | body], meta, ctx) do
|
||||||
test_ast = transform(test, ctx)
|
test_ast = transform(test, ctx)
|
||||||
body_ast = transform_body(body, ctx)
|
body_ast = transform_body(body, ctx)
|
||||||
ast = {:if, [], [test_ast, [do: body_ast]]}
|
ast = {:if, elixir_meta(meta), [test_ast, [do: body_ast]]}
|
||||||
{ast, ctx}
|
{ast, ctx}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp transform_cond(pairs, _meta, ctx) do
|
defp transform_cond(pairs, meta, ctx) do
|
||||||
clauses =
|
clauses =
|
||||||
pairs
|
pairs
|
||||||
|> Enum.chunk_every(2)
|
|> Enum.chunk_every(2)
|
||||||
@@ -803,11 +811,11 @@ defmodule CljElixir.Transformer do
|
|||||||
{:->, [], [[cond_ast], result_ast]}
|
{:->, [], [[cond_ast], result_ast]}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
ast = {:cond, [], [[do: clauses]]}
|
ast = {:cond, elixir_meta(meta), [[do: clauses]]}
|
||||||
{ast, ctx}
|
{ast, ctx}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp transform_case([val | clauses], _meta, ctx) do
|
defp transform_case([val | clauses], meta, ctx) do
|
||||||
val_ast = transform(val, ctx)
|
val_ast = transform(val, ctx)
|
||||||
pattern_ctx = %{ctx | in_pattern: true}
|
pattern_ctx = %{ctx | in_pattern: true}
|
||||||
|
|
||||||
@@ -828,11 +836,12 @@ defmodule CljElixir.Transformer do
|
|||||||
{:->, [], [[pat_ast], body_ast]}
|
{:->, [], [[pat_ast], body_ast]}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
ast = {:case, [], [val_ast, [do: case_clauses]]}
|
ast = {:case, elixir_meta(meta), [val_ast, [do: case_clauses]]}
|
||||||
{ast, ctx}
|
{ast, ctx}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp transform_do(exprs, _meta, ctx) do
|
defp transform_do(exprs, _meta, ctx) do
|
||||||
|
# transform_body handles its own block wrapping; meta not needed here
|
||||||
body_ast = transform_body(exprs, ctx)
|
body_ast = transform_body(exprs, ctx)
|
||||||
{body_ast, ctx}
|
{body_ast, ctx}
|
||||||
end
|
end
|
||||||
@@ -841,9 +850,10 @@ defmodule CljElixir.Transformer do
|
|||||||
# 7. loop / recur
|
# 7. loop / recur
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
defp transform_loop([{:vector, _, bindings} | body], _meta, ctx) do
|
defp transform_loop([{:vector, _, bindings} | body], meta, ctx) do
|
||||||
binding_pairs = Enum.chunk_every(bindings, 2)
|
binding_pairs = Enum.chunk_every(bindings, 2)
|
||||||
arity = length(binding_pairs)
|
arity = length(binding_pairs)
|
||||||
|
em = elixir_meta(meta)
|
||||||
|
|
||||||
loop_var = unique_var(:loop_fn, ctx)
|
loop_var = unique_var(:loop_fn, ctx)
|
||||||
loop_ctx = %{ctx | loop_var: loop_var, loop_arity: arity}
|
loop_ctx = %{ctx | loop_var: loop_var, loop_arity: arity}
|
||||||
@@ -859,16 +869,16 @@ defmodule CljElixir.Transformer do
|
|||||||
|
|
||||||
# Pattern: loop_fn = fn loop_fn, bindings... -> body end; loop_fn.(loop_fn, init_vals...)
|
# Pattern: loop_fn = fn loop_fn, bindings... -> body end; loop_fn.(loop_fn, init_vals...)
|
||||||
fn_params = [loop_var | param_names]
|
fn_params = [loop_var | param_names]
|
||||||
fn_ast = {:fn, [], [{:->, [], [fn_params, body_ast]}]}
|
fn_ast = {:fn, em, [{:->, [], [fn_params, body_ast]}]}
|
||||||
|
|
||||||
assign = {:=, [], [loop_var, fn_ast]}
|
assign = {:=, em, [loop_var, fn_ast]}
|
||||||
invoke = {{:., [], [loop_var]}, [], [loop_var | init_vals]}
|
invoke = {{:., em, [loop_var]}, em, [loop_var | init_vals]}
|
||||||
|
|
||||||
ast = {:__block__, [], [assign, invoke]}
|
ast = {:__block__, em, [assign, invoke]}
|
||||||
{ast, ctx}
|
{ast, ctx}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp transform_recur(args, _meta, ctx) do
|
defp transform_recur(args, _meta_unused, ctx) do
|
||||||
t_args = Enum.map(args, fn a -> transform(a, ctx) end)
|
t_args = Enum.map(args, fn a -> transform(a, ctx) end)
|
||||||
|
|
||||||
ast =
|
ast =
|
||||||
@@ -891,21 +901,22 @@ defmodule CljElixir.Transformer do
|
|||||||
# 8. def (top-level binding)
|
# 8. def (top-level binding)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
defp transform_def([name_form, value], _meta, ctx) do
|
defp transform_def([name_form, value], meta, ctx) do
|
||||||
fun_name = symbol_to_atom(name_form)
|
fun_name = symbol_to_atom(name_form)
|
||||||
|
em = elixir_meta(meta)
|
||||||
|
|
||||||
# Check if value looks like a schema definition
|
# Check if value looks like a schema definition
|
||||||
case detect_schema(value) do
|
case detect_schema(value) do
|
||||||
nil ->
|
nil ->
|
||||||
val_ast = transform(value, ctx)
|
val_ast = transform(value, ctx)
|
||||||
def_ast = {:def, [], [{fun_name, [], []}, [do: val_ast]]}
|
def_ast = {:def, em, [{fun_name, [], []}, [do: val_ast]]}
|
||||||
{def_ast, ctx}
|
{def_ast, ctx}
|
||||||
|
|
||||||
schema_data ->
|
schema_data ->
|
||||||
# For schema defs, use the plain data as the runtime value
|
# For schema defs, use the plain data as the runtime value
|
||||||
# (avoids trying to transform schema references like PositiveInt as variables)
|
# (avoids trying to transform schema references like PositiveInt as variables)
|
||||||
val_ast = Macro.escape(schema_data)
|
val_ast = Macro.escape(schema_data)
|
||||||
def_ast = {:def, [], [{fun_name, [], []}, [do: val_ast]]}
|
def_ast = {:def, em, [{fun_name, [], []}, [do: val_ast]]}
|
||||||
|
|
||||||
# Generate type name: "User" -> :user, "PositiveInt" -> :positive_int
|
# Generate type name: "User" -> :user, "PositiveInt" -> :positive_int
|
||||||
schema_name = symbol_name(name_form)
|
schema_name = symbol_name(name_form)
|
||||||
@@ -931,9 +942,10 @@ defmodule CljElixir.Transformer do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp transform_def([name_form], _meta, ctx) do
|
defp transform_def([name_form], meta, ctx) do
|
||||||
fun_name = symbol_to_atom(name_form)
|
fun_name = symbol_to_atom(name_form)
|
||||||
ast = {:def, [], [{fun_name, [], []}, [do: nil]]}
|
em = elixir_meta(meta)
|
||||||
|
ast = {:def, em, [{fun_name, [], []}, [do: nil]]}
|
||||||
{ast, ctx}
|
{ast, ctx}
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -941,10 +953,11 @@ defmodule CljElixir.Transformer do
|
|||||||
# 9. Module/function calls (FFI)
|
# 9. Module/function calls (FFI)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
defp transform_module_call(name, args, ctx) do
|
defp transform_module_call(name, args, meta, ctx) do
|
||||||
{mod_ast, fun_atom} = parse_module_function(name)
|
{mod_ast, fun_atom} = parse_module_function(name)
|
||||||
t_args = Enum.map(args, fn a -> transform(a, ctx) end)
|
t_args = Enum.map(args, fn a -> transform(a, ctx) end)
|
||||||
ast = {{:., [], [mod_ast, fun_atom]}, [], t_args}
|
em = elixir_meta(meta)
|
||||||
|
ast = {{:., em, [mod_ast, fun_atom]}, em, t_args}
|
||||||
{ast, ctx}
|
{ast, ctx}
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -982,11 +995,11 @@ defmodule CljElixir.Transformer do
|
|||||||
# 10. Unqualified function calls
|
# 10. Unqualified function calls
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
defp transform_unqualified_call(name, args, ctx) do
|
defp transform_unqualified_call(name, args, meta, ctx) do
|
||||||
munged = munge_name(name)
|
munged = munge_name(name)
|
||||||
fun_atom = String.to_atom(munged)
|
fun_atom = String.to_atom(munged)
|
||||||
t_args = Enum.map(args, fn a -> transform(a, ctx) end)
|
t_args = Enum.map(args, fn a -> transform(a, ctx) end)
|
||||||
ast = {fun_atom, [], t_args}
|
ast = {fun_atom, elixir_meta(meta), t_args}
|
||||||
{ast, ctx}
|
{ast, ctx}
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -1192,7 +1205,7 @@ defmodule CljElixir.Transformer do
|
|||||||
# 12. Keyword-as-function
|
# 12. Keyword-as-function
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
defp transform_keyword_call(kw, args, ctx) do
|
defp transform_keyword_call(kw, args, _meta, ctx) do
|
||||||
case args do
|
case args do
|
||||||
[map_form] ->
|
[map_form] ->
|
||||||
map_ast = transform(map_form, ctx)
|
map_ast = transform(map_form, ctx)
|
||||||
@@ -1215,7 +1228,7 @@ defmodule CljElixir.Transformer do
|
|||||||
# 13. defprotocol
|
# 13. defprotocol
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
defp transform_defprotocol(args, _meta, ctx) do
|
defp transform_defprotocol(args, meta, ctx) do
|
||||||
{name_form, rest} = extract_name(args)
|
{name_form, rest} = extract_name(args)
|
||||||
proto_alias = module_name_ast(name_form)
|
proto_alias = module_name_ast(name_form)
|
||||||
|
|
||||||
@@ -1265,7 +1278,7 @@ defmodule CljElixir.Transformer do
|
|||||||
multiple -> {:__block__, [], multiple}
|
multiple -> {:__block__, [], multiple}
|
||||||
end
|
end
|
||||||
|
|
||||||
ast = {:defprotocol, [context: Elixir], [proto_alias, [do: block]]}
|
ast = {:defprotocol, [context: Elixir] ++ elixir_meta(meta), [proto_alias, [do: block]]}
|
||||||
{ast, ctx}
|
{ast, ctx}
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -1273,7 +1286,7 @@ defmodule CljElixir.Transformer do
|
|||||||
# 14. defrecord
|
# 14. defrecord
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
defp transform_defrecord(args, _meta, ctx) do
|
defp transform_defrecord(args, meta, ctx) do
|
||||||
{name_form, rest} = extract_name(args)
|
{name_form, rest} = extract_name(args)
|
||||||
record_alias = module_name_ast(name_form)
|
record_alias = module_name_ast(name_form)
|
||||||
record_name = symbol_name(name_form)
|
record_name = symbol_name(name_form)
|
||||||
@@ -1348,7 +1361,7 @@ defmodule CljElixir.Transformer do
|
|||||||
multiple -> {:__block__, [], multiple}
|
multiple -> {:__block__, [], multiple}
|
||||||
end
|
end
|
||||||
|
|
||||||
ast = {:defmodule, [context: Elixir], [record_alias, [do: block]]}
|
ast = {:defmodule, [context: Elixir] ++ elixir_meta(meta), [record_alias, [do: block]]}
|
||||||
{ast, new_ctx}
|
{ast, new_ctx}
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -1374,7 +1387,7 @@ defmodule CljElixir.Transformer do
|
|||||||
# 15. extend-type / extend-protocol
|
# 15. extend-type / extend-protocol
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
defp transform_extend_type([type_form | rest], _meta, ctx) do
|
defp transform_extend_type([type_form | rest], _meta_unused, ctx) do
|
||||||
type_alias = resolve_type_name(type_form)
|
type_alias = resolve_type_name(type_form)
|
||||||
|
|
||||||
groups = parse_protocol_groups(rest)
|
groups = parse_protocol_groups(rest)
|
||||||
@@ -1402,7 +1415,7 @@ defmodule CljElixir.Transformer do
|
|||||||
{ast, ctx}
|
{ast, ctx}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp transform_extend_protocol([proto_form | rest], _meta, ctx) do
|
defp transform_extend_protocol([proto_form | rest], _meta_unused, ctx) do
|
||||||
proto_alias = resolve_type_name(proto_form)
|
proto_alias = resolve_type_name(proto_form)
|
||||||
|
|
||||||
# Parse: TypeName (fn ...) TypeName (fn ...)
|
# Parse: TypeName (fn ...) TypeName (fn ...)
|
||||||
@@ -1435,7 +1448,7 @@ defmodule CljElixir.Transformer do
|
|||||||
# 16. reify
|
# 16. reify
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
defp transform_reify(args, _meta, ctx) do
|
defp transform_reify(args, _meta_unused, ctx) do
|
||||||
{counter, new_ctx} = bump_gensym(ctx)
|
{counter, new_ctx} = bump_gensym(ctx)
|
||||||
mod_name = String.to_atom("CljElixir.Reify_#{counter}")
|
mod_name = String.to_atom("CljElixir.Reify_#{counter}")
|
||||||
mod_alias = {:__aliases__, [alias: false], [mod_name]}
|
mod_alias = {:__aliases__, [alias: false], [mod_name]}
|
||||||
@@ -1475,7 +1488,7 @@ defmodule CljElixir.Transformer do
|
|||||||
# 17. with
|
# 17. with
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
defp transform_with([{:vector, _, bindings} | body_and_else], _meta, ctx) do
|
defp transform_with([{:vector, _, bindings} | body_and_else], meta, ctx) do
|
||||||
binding_pairs = Enum.chunk_every(bindings, 2)
|
binding_pairs = Enum.chunk_every(bindings, 2)
|
||||||
pattern_ctx = %{ctx | in_pattern: true}
|
pattern_ctx = %{ctx | in_pattern: true}
|
||||||
|
|
||||||
@@ -1507,7 +1520,7 @@ defmodule CljElixir.Transformer do
|
|||||||
[do: body_ast, else: else_pairs]
|
[do: body_ast, else: else_pairs]
|
||||||
end
|
end
|
||||||
|
|
||||||
ast = {:with, [], with_clauses ++ [opts]}
|
ast = {:with, elixir_meta(meta), with_clauses ++ [opts]}
|
||||||
{ast, ctx}
|
{ast, ctx}
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -1522,7 +1535,7 @@ defmodule CljElixir.Transformer do
|
|||||||
# 18. receive
|
# 18. receive
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
defp transform_receive(clauses, _meta, ctx) do
|
defp transform_receive(clauses, meta, ctx) do
|
||||||
pattern_ctx = %{ctx | in_pattern: true}
|
pattern_ctx = %{ctx | in_pattern: true}
|
||||||
|
|
||||||
{case_clauses, after_clause} = parse_receive_clauses(clauses)
|
{case_clauses, after_clause} = parse_receive_clauses(clauses)
|
||||||
@@ -1556,7 +1569,7 @@ defmodule CljElixir.Transformer do
|
|||||||
opts ++ [after: [{:->, [], [[timeout_ast], body_ast]}]]
|
opts ++ [after: [{:->, [], [[timeout_ast], body_ast]}]]
|
||||||
end
|
end
|
||||||
|
|
||||||
ast = {:receive, [], [opts]}
|
ast = {:receive, elixir_meta(meta), [opts]}
|
||||||
{ast, ctx}
|
{ast, ctx}
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -1628,21 +1641,21 @@ defmodule CljElixir.Transformer do
|
|||||||
# 19. for / doseq
|
# 19. for / doseq
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
defp transform_for([{:vector, _, bindings} | body], _meta, ctx) do
|
defp transform_for([{:vector, _, bindings} | body], meta, ctx) do
|
||||||
{generators, filters} = parse_comprehension_bindings(bindings, ctx)
|
{generators, filters} = parse_comprehension_bindings(bindings, ctx)
|
||||||
body_ast = transform_body(body, ctx)
|
body_ast = transform_body(body, ctx)
|
||||||
|
|
||||||
args = generators ++ filters ++ [[do: body_ast]]
|
args = generators ++ filters ++ [[do: body_ast]]
|
||||||
ast = {:for, [], args}
|
ast = {:for, elixir_meta(meta), args}
|
||||||
{ast, ctx}
|
{ast, ctx}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp transform_doseq([{:vector, _, bindings} | body], _meta, ctx) do
|
defp transform_doseq([{:vector, _, bindings} | body], meta, ctx) do
|
||||||
{generators, filters} = parse_comprehension_bindings(bindings, ctx)
|
{generators, filters} = parse_comprehension_bindings(bindings, ctx)
|
||||||
body_ast = transform_body(body, ctx)
|
body_ast = transform_body(body, ctx)
|
||||||
|
|
||||||
args = generators ++ filters ++ [[do: body_ast]]
|
args = generators ++ filters ++ [[do: body_ast]]
|
||||||
ast = {:for, [], args}
|
ast = {:for, elixir_meta(meta), args}
|
||||||
{ast, ctx}
|
{ast, ctx}
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -1991,6 +2004,111 @@ defmodule CljElixir.Transformer do
|
|||||||
{ast, ctx}
|
{ast, ctx}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# (pr-str val) -> CljElixir.Printer.pr_str(val)
|
||||||
|
# (pr-str a b c) -> join pr_str of each with space
|
||||||
|
defp transform_pr_str(args, ctx) do
|
||||||
|
t_args = Enum.map(args, fn a -> transform(a, ctx) end)
|
||||||
|
printer_mod = {:__aliases__, [alias: false], [:CljElixir, :Printer]}
|
||||||
|
|
||||||
|
case t_args do
|
||||||
|
[single] ->
|
||||||
|
ast = {{:., [], [printer_mod, :pr_str]}, [], [single]}
|
||||||
|
{ast, ctx}
|
||||||
|
|
||||||
|
multiple ->
|
||||||
|
strs =
|
||||||
|
Enum.map(multiple, fn a ->
|
||||||
|
{{:., [], [printer_mod, :pr_str]}, [], [a]}
|
||||||
|
end)
|
||||||
|
|
||||||
|
joined =
|
||||||
|
Enum.reduce(tl(strs), hd(strs), fn s, acc ->
|
||||||
|
{:<>, [], [acc, {:<>, [], [" ", s]}]}
|
||||||
|
end)
|
||||||
|
|
||||||
|
{joined, ctx}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# (pr val) -> IO.write(CljElixir.Printer.pr_str(val))
|
||||||
|
# Multiple args separated by spaces
|
||||||
|
defp transform_pr(args, ctx) do
|
||||||
|
t_args = Enum.map(args, fn a -> transform(a, ctx) end)
|
||||||
|
printer_mod = {:__aliases__, [alias: false], [:CljElixir, :Printer]}
|
||||||
|
io_mod = {:__aliases__, [alias: false], [:IO]}
|
||||||
|
|
||||||
|
writes =
|
||||||
|
t_args
|
||||||
|
|> Enum.with_index()
|
||||||
|
|> Enum.flat_map(fn {a, i} ->
|
||||||
|
pr_call = {{:., [], [printer_mod, :pr_str]}, [], [a]}
|
||||||
|
write_call = {{:., [], [io_mod, :write]}, [], [pr_call]}
|
||||||
|
|
||||||
|
if i > 0 do
|
||||||
|
space_call = {{:., [], [io_mod, :write]}, [], [" "]}
|
||||||
|
[space_call, write_call]
|
||||||
|
else
|
||||||
|
[write_call]
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
ast = {:__block__, [], writes}
|
||||||
|
{ast, ctx}
|
||||||
|
end
|
||||||
|
|
||||||
|
# (prn val) -> IO.puts(CljElixir.Printer.pr_str(val))
|
||||||
|
# Multiple args joined with spaces, then newline
|
||||||
|
defp transform_prn(args, ctx) do
|
||||||
|
t_args = Enum.map(args, fn a -> transform(a, ctx) end)
|
||||||
|
printer_mod = {:__aliases__, [alias: false], [:CljElixir, :Printer]}
|
||||||
|
io_mod = {:__aliases__, [alias: false], [:IO]}
|
||||||
|
|
||||||
|
strs =
|
||||||
|
Enum.map(t_args, fn a ->
|
||||||
|
{{:., [], [printer_mod, :pr_str]}, [], [a]}
|
||||||
|
end)
|
||||||
|
|
||||||
|
joined =
|
||||||
|
case strs do
|
||||||
|
[single] ->
|
||||||
|
single
|
||||||
|
|
||||||
|
multiple ->
|
||||||
|
Enum.reduce(tl(multiple), hd(multiple), fn s, acc ->
|
||||||
|
{:<>, [], [acc, {:<>, [], [" ", s]}]}
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
ast = {{:., [], [io_mod, :puts]}, [], [joined]}
|
||||||
|
{ast, ctx}
|
||||||
|
end
|
||||||
|
|
||||||
|
# (print-str val) -> CljElixir.Printer.print_str(val)
|
||||||
|
# (print-str a b c) -> join print_str of each with space
|
||||||
|
defp transform_print_str(args, ctx) do
|
||||||
|
t_args = Enum.map(args, fn a -> transform(a, ctx) end)
|
||||||
|
printer_mod = {:__aliases__, [alias: false], [:CljElixir, :Printer]}
|
||||||
|
|
||||||
|
case t_args do
|
||||||
|
[single] ->
|
||||||
|
ast = {{:., [], [printer_mod, :print_str]}, [], [single]}
|
||||||
|
{ast, ctx}
|
||||||
|
|
||||||
|
multiple ->
|
||||||
|
strs =
|
||||||
|
Enum.map(multiple, fn a ->
|
||||||
|
{{:., [], [printer_mod, :print_str]}, [], [a]}
|
||||||
|
end)
|
||||||
|
|
||||||
|
joined =
|
||||||
|
Enum.reduce(tl(strs), hd(strs), fn s, acc ->
|
||||||
|
{:<>, [], [acc, {:<>, [], [" ", s]}]}
|
||||||
|
end)
|
||||||
|
|
||||||
|
{joined, ctx}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# nil?
|
# nil?
|
||||||
defp transform_nil_check([a], ctx) do
|
defp transform_nil_check([a], ctx) do
|
||||||
a_ast = transform(a, ctx)
|
a_ast = transform(a, ctx)
|
||||||
@@ -2034,25 +2152,27 @@ defmodule CljElixir.Transformer do
|
|||||||
# Positional constructor: (->Name arg1 arg2 ...)
|
# Positional constructor: (->Name arg1 arg2 ...)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
defp transform_positional_constructor(name, args, ctx) do
|
defp transform_positional_constructor(name, args, meta, ctx) do
|
||||||
# "->Name" → strip ->
|
# "->Name" → strip ->
|
||||||
record_name = String.slice(name, 2..-1//1)
|
record_name = String.slice(name, 2..-1//1)
|
||||||
mod_alias = parse_module_name(record_name)
|
mod_alias = parse_module_name(record_name)
|
||||||
t_args = Enum.map(args, fn a -> transform(a, ctx) end)
|
t_args = Enum.map(args, fn a -> transform(a, ctx) end)
|
||||||
|
em = elixir_meta(meta)
|
||||||
|
|
||||||
# Call Module.new(args...)
|
# Call Module.new(args...)
|
||||||
ast = {{:., [], [mod_alias, :new]}, [], t_args}
|
ast = {{:., em, [mod_alias, :new]}, em, t_args}
|
||||||
{ast, ctx}
|
{ast, ctx}
|
||||||
end
|
end
|
||||||
|
|
||||||
# Map constructor: (map->Name {:field val ...})
|
# Map constructor: (map->Name {:field val ...})
|
||||||
defp transform_map_constructor(name, args, ctx) do
|
defp transform_map_constructor(name, args, meta, ctx) do
|
||||||
record_name = String.slice(name, 5..-1//1)
|
record_name = String.slice(name, 5..-1//1)
|
||||||
mod_alias = parse_module_name(record_name)
|
mod_alias = parse_module_name(record_name)
|
||||||
t_args = Enum.map(args, fn a -> transform(a, ctx) end)
|
t_args = Enum.map(args, fn a -> transform(a, ctx) end)
|
||||||
|
em = elixir_meta(meta)
|
||||||
|
|
||||||
# Use Kernel.struct!/2
|
# Use Kernel.struct!/2
|
||||||
ast = {{:., [], [{:__aliases__, [alias: false], [:Kernel]}, :struct!]}, [], [mod_alias | t_args]}
|
ast = {{:., em, [{:__aliases__, [alias: false], [:Kernel]}, :struct!]}, em, [mod_alias | t_args]}
|
||||||
{ast, ctx}
|
{ast, ctx}
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -2822,7 +2942,7 @@ defmodule CljElixir.Transformer do
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
# (try body... (catch ...) ... (finally ...))
|
# (try body... (catch ...) ... (finally ...))
|
||||||
defp transform_try(args, _meta, ctx) do
|
defp transform_try(args, meta, ctx) do
|
||||||
{body_forms, catch_clauses, finally_clause} = partition_try_args(args)
|
{body_forms, catch_clauses, finally_clause} = partition_try_args(args)
|
||||||
|
|
||||||
# Transform body
|
# Transform body
|
||||||
@@ -2858,7 +2978,7 @@ defmodule CljElixir.Transformer do
|
|||||||
try_opts ++ [after: after_ast]
|
try_opts ++ [after: after_ast]
|
||||||
end
|
end
|
||||||
|
|
||||||
ast = {:try, [], [try_opts]}
|
ast = {:try, elixir_meta(meta), [try_opts]}
|
||||||
{ast, ctx}
|
{ast, ctx}
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -3274,4 +3394,17 @@ defmodule CljElixir.Transformer do
|
|||||||
|> String.replace("-", "_")
|
|> String.replace("-", "_")
|
||||||
|> String.to_atom()
|
|> String.to_atom()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Source-mapping: CljElixir meta → Elixir AST metadata
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Convert CljElixir meta map to Elixir AST keyword list metadata.
|
||||||
|
# Line must be >= 1 for the Erlang compiler annotation layer;
|
||||||
|
# a zero line/col is treated as absent.
|
||||||
|
defp elixir_meta(%{line: line, col: col}) when line > 0 and col > 0,
|
||||||
|
do: [line: line, column: col]
|
||||||
|
defp elixir_meta(%{line: line}) when line > 0,
|
||||||
|
do: [line: line]
|
||||||
|
defp elixir_meta(_), do: []
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
defmodule Mix.Tasks.Clje.Nrepl do
|
||||||
|
@moduledoc """
|
||||||
|
Starts an nREPL server for CljElixir.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
mix clje.nrepl # random port
|
||||||
|
mix clje.nrepl --port 7888 # specific port
|
||||||
|
|
||||||
|
Writes port to `.nrepl-port` file for editor auto-discovery.
|
||||||
|
"""
|
||||||
|
|
||||||
|
use Mix.Task
|
||||||
|
|
||||||
|
@shortdoc "Start a CljElixir nREPL server"
|
||||||
|
|
||||||
|
@impl Mix.Task
|
||||||
|
def run(args) do
|
||||||
|
{opts, _, _} = OptionParser.parse(args, strict: [port: :integer])
|
||||||
|
port = Keyword.get(opts, :port, 0)
|
||||||
|
|
||||||
|
Mix.Task.run("compile")
|
||||||
|
Mix.Task.run("app.start")
|
||||||
|
|
||||||
|
{:ok, server} = CljElixir.NRepl.Server.start_link(port: port)
|
||||||
|
actual_port = CljElixir.NRepl.Server.port(server)
|
||||||
|
|
||||||
|
File.write!(".nrepl-port", Integer.to_string(actual_port))
|
||||||
|
|
||||||
|
IO.puts("nREPL server started on port #{actual_port} on host 127.0.0.1")
|
||||||
|
IO.puts("Port written to .nrepl-port")
|
||||||
|
IO.puts("Press Ctrl+C to stop\n")
|
||||||
|
|
||||||
|
Process.sleep(:infinity)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
defmodule Mix.Tasks.Clje.Repl do
|
||||||
|
@moduledoc """
|
||||||
|
Starts an interactive CljElixir REPL.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
mix clje.repl
|
||||||
|
|
||||||
|
Features:
|
||||||
|
- Full line editing (arrow keys, home/end, backspace)
|
||||||
|
- Multi-line input (auto-detects unbalanced parens)
|
||||||
|
- `pr-str` output formatting
|
||||||
|
- Bindings persist across evaluations
|
||||||
|
- Special commands: :quit, :history, :help
|
||||||
|
|
||||||
|
For readline-style history (up/down arrow recall), run with rlwrap:
|
||||||
|
|
||||||
|
rlwrap mix clje.repl
|
||||||
|
"""
|
||||||
|
|
||||||
|
use Mix.Task
|
||||||
|
|
||||||
|
@shortdoc "Start an interactive CljElixir REPL"
|
||||||
|
|
||||||
|
@impl Mix.Task
|
||||||
|
def run(_args) do
|
||||||
|
# Ensure the project is compiled first
|
||||||
|
Mix.Task.run("compile")
|
||||||
|
|
||||||
|
# Start the application
|
||||||
|
Mix.Task.run("app.start")
|
||||||
|
|
||||||
|
IO.puts("CljElixir REPL v0.1.0")
|
||||||
|
IO.puts("Type :help for help, :quit to exit\n")
|
||||||
|
|
||||||
|
state = CljElixir.REPL.new()
|
||||||
|
loop(state)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp loop(state) do
|
||||||
|
prompt = "clje:#{state.counter}> "
|
||||||
|
|
||||||
|
case read_input(prompt) do
|
||||||
|
:eof ->
|
||||||
|
IO.puts("\nBye!")
|
||||||
|
|
||||||
|
input ->
|
||||||
|
input = String.trim(input)
|
||||||
|
|
||||||
|
case input do
|
||||||
|
":quit" ->
|
||||||
|
IO.puts("Bye!")
|
||||||
|
|
||||||
|
":history" ->
|
||||||
|
print_history(state)
|
||||||
|
loop(state)
|
||||||
|
|
||||||
|
":help" ->
|
||||||
|
print_help()
|
||||||
|
loop(state)
|
||||||
|
|
||||||
|
"" ->
|
||||||
|
loop(state)
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
# Check for multi-line - read more if unbalanced
|
||||||
|
full_input = maybe_read_more(input)
|
||||||
|
|
||||||
|
case CljElixir.REPL.eval(full_input, state) do
|
||||||
|
{:ok, result_str, new_state} ->
|
||||||
|
IO.puts(result_str)
|
||||||
|
loop(new_state)
|
||||||
|
|
||||||
|
{:error, error_str, new_state} ->
|
||||||
|
IO.puts("\e[31m#{error_str}\e[0m")
|
||||||
|
loop(new_state)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp read_input(prompt) do
|
||||||
|
case IO.gets(prompt) do
|
||||||
|
:eof -> :eof
|
||||||
|
{:error, _} -> :eof
|
||||||
|
data -> data
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp maybe_read_more(input) do
|
||||||
|
if CljElixir.REPL.balanced?(input) do
|
||||||
|
input
|
||||||
|
else
|
||||||
|
read_continuation(input)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp read_continuation(acc) do
|
||||||
|
case IO.gets(" ") do
|
||||||
|
:eof -> acc
|
||||||
|
{:error, _} -> acc
|
||||||
|
line ->
|
||||||
|
new_acc = acc <> "\n" <> String.trim_trailing(line, "\n")
|
||||||
|
|
||||||
|
if CljElixir.REPL.balanced?(new_acc) do
|
||||||
|
new_acc
|
||||||
|
else
|
||||||
|
read_continuation(new_acc)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp print_history(state) do
|
||||||
|
state.history
|
||||||
|
|> Enum.reverse()
|
||||||
|
|> Enum.with_index(1)
|
||||||
|
|> Enum.each(fn {expr, i} ->
|
||||||
|
IO.puts(" #{i}: #{String.replace(expr, "\n", "\n ")}")
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp print_help do
|
||||||
|
IO.puts("""
|
||||||
|
CljElixir REPL Commands:
|
||||||
|
:help - show this help
|
||||||
|
:history - show expression history
|
||||||
|
:quit - exit the REPL
|
||||||
|
|
||||||
|
Tips:
|
||||||
|
- Multi-line input: unbalanced parens trigger continuation
|
||||||
|
- Bindings persist: (def x 42) makes x available in later expressions
|
||||||
|
- All CljElixir syntax is supported
|
||||||
|
- For up/down arrow history, run: rlwrap mix clje.repl
|
||||||
|
""")
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -55,6 +55,9 @@
|
|||||||
(defprotocol CljElixir.IEquiv
|
(defprotocol CljElixir.IEquiv
|
||||||
(-equiv [o other]))
|
(-equiv [o other]))
|
||||||
|
|
||||||
|
(defprotocol CljElixir.IPrintWithWriter
|
||||||
|
(-pr-writer [o writer opts]))
|
||||||
|
|
||||||
(defprotocol CljElixir.IClojurify
|
(defprotocol CljElixir.IClojurify
|
||||||
(-clojurify [o]))
|
(-clojurify [o]))
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,342 @@
|
|||||||
|
defmodule CljElixir.NReplTest do
|
||||||
|
use ExUnit.Case, async: false
|
||||||
|
|
||||||
|
alias CljElixir.NRepl.{Bencode, Server, SessionManager, Handler}
|
||||||
|
|
||||||
|
# --- Bencode Tests ---
|
||||||
|
|
||||||
|
describe "Bencode encoding" do
|
||||||
|
test "encode string" do
|
||||||
|
assert Bencode.encode("hello") == "5:hello"
|
||||||
|
assert Bencode.encode("") == "0:"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "encode integer" do
|
||||||
|
assert Bencode.encode(42) == "i42e"
|
||||||
|
assert Bencode.encode(0) == "i0e"
|
||||||
|
assert Bencode.encode(-7) == "i-7e"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "encode list" do
|
||||||
|
assert Bencode.encode([1, 2, 3]) == "li1ei2ei3ee"
|
||||||
|
assert Bencode.encode([]) == "le"
|
||||||
|
assert Bencode.encode(["hello", 42]) == "l5:helloi42ee"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "encode dict" do
|
||||||
|
assert Bencode.encode(%{"op" => "eval"}) == "d2:op4:evale"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "encode nested" do
|
||||||
|
msg = %{"op" => "eval", "code" => "(+ 1 2)", "id" => "1"}
|
||||||
|
encoded = Bencode.encode(msg)
|
||||||
|
assert is_binary(encoded)
|
||||||
|
assert String.starts_with?(encoded, "d")
|
||||||
|
assert String.ends_with?(encoded, "e")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "encode atom keys" do
|
||||||
|
assert Bencode.encode(%{op: "eval"}) == "d2:op4:evale"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "Bencode decoding" do
|
||||||
|
test "decode string" do
|
||||||
|
assert Bencode.decode("5:hello") == "hello"
|
||||||
|
assert Bencode.decode("0:") == ""
|
||||||
|
end
|
||||||
|
|
||||||
|
test "decode integer" do
|
||||||
|
assert Bencode.decode("i42e") == 42
|
||||||
|
assert Bencode.decode("i-7e") == -7
|
||||||
|
end
|
||||||
|
|
||||||
|
test "decode list" do
|
||||||
|
assert Bencode.decode("li1ei2ei3ee") == [1, 2, 3]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "decode dict" do
|
||||||
|
result = Bencode.decode("d2:op4:evale")
|
||||||
|
assert result == %{"op" => "eval"}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "roundtrip" do
|
||||||
|
original = %{"op" => "eval", "code" => "(+ 1 2)", "id" => "msg-1"}
|
||||||
|
assert Bencode.decode(Bencode.encode(original)) == original
|
||||||
|
end
|
||||||
|
|
||||||
|
test "roundtrip nested" do
|
||||||
|
original = %{"ops" => %{"eval" => %{}, "clone" => %{}}, "status" => ["done"]}
|
||||||
|
assert Bencode.decode(Bencode.encode(original)) == original
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# --- Session Manager Tests ---
|
||||||
|
|
||||||
|
describe "SessionManager" do
|
||||||
|
setup do
|
||||||
|
{:ok, manager} = SessionManager.start_link()
|
||||||
|
%{manager: manager}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "create session", %{manager: manager} do
|
||||||
|
id = SessionManager.create_session(manager)
|
||||||
|
assert is_binary(id)
|
||||||
|
assert String.length(id) > 0
|
||||||
|
end
|
||||||
|
|
||||||
|
test "list sessions", %{manager: manager} do
|
||||||
|
assert SessionManager.list_sessions(manager) == []
|
||||||
|
id = SessionManager.create_session(manager)
|
||||||
|
assert SessionManager.list_sessions(manager) == [id]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "eval in session", %{manager: manager} do
|
||||||
|
id = SessionManager.create_session(manager)
|
||||||
|
assert {:ok, "3"} = SessionManager.eval(manager, id, "(+ 1 2)")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "session state persists", %{manager: manager} do
|
||||||
|
id = SessionManager.create_session(manager)
|
||||||
|
|
||||||
|
{:ok, _} =
|
||||||
|
SessionManager.eval(manager, id, """
|
||||||
|
(defmodule NReplSessionTest
|
||||||
|
(defn hello [] :hi))
|
||||||
|
""")
|
||||||
|
|
||||||
|
{:ok, result} = SessionManager.eval(manager, id, "(NReplSessionTest/hello)")
|
||||||
|
assert result == ":hi"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "close session", %{manager: manager} do
|
||||||
|
id = SessionManager.create_session(manager)
|
||||||
|
assert :ok = SessionManager.close_session(manager, id)
|
||||||
|
assert SessionManager.list_sessions(manager) == []
|
||||||
|
end
|
||||||
|
|
||||||
|
test "independent sessions", %{manager: manager} do
|
||||||
|
id1 = SessionManager.create_session(manager)
|
||||||
|
id2 = SessionManager.create_session(manager)
|
||||||
|
assert id1 != id2
|
||||||
|
{:ok, "3"} = SessionManager.eval(manager, id1, "(+ 1 2)")
|
||||||
|
{:ok, "7"} = SessionManager.eval(manager, id2, "(+ 3 4)")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# --- Handler Tests ---
|
||||||
|
|
||||||
|
describe "Handler" do
|
||||||
|
setup do
|
||||||
|
{:ok, manager} = SessionManager.start_link()
|
||||||
|
%{manager: manager}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "clone creates session", %{manager: manager} do
|
||||||
|
[response] = Handler.handle(%{"op" => "clone", "id" => "1"}, manager)
|
||||||
|
assert response["status"] == ["done"]
|
||||||
|
assert is_binary(response["new-session"])
|
||||||
|
end
|
||||||
|
|
||||||
|
test "describe returns ops", %{manager: manager} do
|
||||||
|
[response] = Handler.handle(%{"op" => "describe", "id" => "1"}, manager)
|
||||||
|
assert response["status"] == ["done"]
|
||||||
|
assert is_map(response["ops"])
|
||||||
|
assert Map.has_key?(response["ops"], "eval")
|
||||||
|
assert Map.has_key?(response["ops"], "clone")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "eval returns value", %{manager: manager} do
|
||||||
|
[clone_resp] = Handler.handle(%{"op" => "clone", "id" => "1"}, manager)
|
||||||
|
session = clone_resp["new-session"]
|
||||||
|
|
||||||
|
responses =
|
||||||
|
Handler.handle(
|
||||||
|
%{
|
||||||
|
"op" => "eval",
|
||||||
|
"code" => "(+ 1 2)",
|
||||||
|
"session" => session,
|
||||||
|
"id" => "2"
|
||||||
|
},
|
||||||
|
manager
|
||||||
|
)
|
||||||
|
|
||||||
|
# Should have value response and done response
|
||||||
|
values = Enum.filter(responses, &Map.has_key?(&1, "value"))
|
||||||
|
dones = Enum.filter(responses, fn r -> r["status"] == ["done"] end)
|
||||||
|
|
||||||
|
assert length(values) == 1
|
||||||
|
assert hd(values)["value"] == "3"
|
||||||
|
assert length(dones) == 1
|
||||||
|
end
|
||||||
|
|
||||||
|
test "eval captures stdout", %{manager: manager} do
|
||||||
|
[clone_resp] = Handler.handle(%{"op" => "clone", "id" => "1"}, manager)
|
||||||
|
session = clone_resp["new-session"]
|
||||||
|
|
||||||
|
responses =
|
||||||
|
Handler.handle(
|
||||||
|
%{
|
||||||
|
"op" => "eval",
|
||||||
|
"code" => "(println \"hello from nrepl\")",
|
||||||
|
"session" => session,
|
||||||
|
"id" => "2"
|
||||||
|
},
|
||||||
|
manager
|
||||||
|
)
|
||||||
|
|
||||||
|
outs = Enum.filter(responses, &Map.has_key?(&1, "out"))
|
||||||
|
assert length(outs) >= 1
|
||||||
|
assert hd(outs)["out"] =~ "hello from nrepl"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "eval error", %{manager: manager} do
|
||||||
|
[clone_resp] = Handler.handle(%{"op" => "clone", "id" => "1"}, manager)
|
||||||
|
session = clone_resp["new-session"]
|
||||||
|
|
||||||
|
responses =
|
||||||
|
Handler.handle(
|
||||||
|
%{
|
||||||
|
"op" => "eval",
|
||||||
|
"code" => "(defmodule HandlerErrTest (bad-syntax",
|
||||||
|
"session" => session,
|
||||||
|
"id" => "2"
|
||||||
|
},
|
||||||
|
manager
|
||||||
|
)
|
||||||
|
|
||||||
|
errs = Enum.filter(responses, &Map.has_key?(&1, "err"))
|
||||||
|
assert length(errs) >= 1
|
||||||
|
end
|
||||||
|
|
||||||
|
test "ls-sessions", %{manager: manager} do
|
||||||
|
[clone1] = Handler.handle(%{"op" => "clone", "id" => "1"}, manager)
|
||||||
|
[clone2] = Handler.handle(%{"op" => "clone", "id" => "2"}, manager)
|
||||||
|
|
||||||
|
[response] = Handler.handle(%{"op" => "ls-sessions", "id" => "3"}, manager)
|
||||||
|
sessions = response["sessions"]
|
||||||
|
|
||||||
|
assert length(sessions) == 2
|
||||||
|
assert clone1["new-session"] in sessions
|
||||||
|
assert clone2["new-session"] in sessions
|
||||||
|
end
|
||||||
|
|
||||||
|
test "close session", %{manager: manager} do
|
||||||
|
[clone_resp] = Handler.handle(%{"op" => "clone", "id" => "1"}, manager)
|
||||||
|
session = clone_resp["new-session"]
|
||||||
|
|
||||||
|
[close_resp] =
|
||||||
|
Handler.handle(
|
||||||
|
%{
|
||||||
|
"op" => "close",
|
||||||
|
"session" => session,
|
||||||
|
"id" => "2"
|
||||||
|
},
|
||||||
|
manager
|
||||||
|
)
|
||||||
|
|
||||||
|
assert close_resp["status"] == ["done"]
|
||||||
|
|
||||||
|
[ls_resp] = Handler.handle(%{"op" => "ls-sessions", "id" => "3"}, manager)
|
||||||
|
assert ls_resp["sessions"] == []
|
||||||
|
end
|
||||||
|
|
||||||
|
test "unknown op", %{manager: manager} do
|
||||||
|
[response] = Handler.handle(%{"op" => "bogus", "id" => "1"}, manager)
|
||||||
|
assert "unknown-op" in response["status"]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# --- TCP Integration Test ---
|
||||||
|
|
||||||
|
describe "TCP server" do
|
||||||
|
test "server starts and accepts connections" do
|
||||||
|
{:ok, server} = Server.start_link(port: 0)
|
||||||
|
port = Server.port(server)
|
||||||
|
assert port > 0
|
||||||
|
|
||||||
|
# Connect as a client
|
||||||
|
{:ok, socket} = :gen_tcp.connect(~c"127.0.0.1", port, [:binary, active: false])
|
||||||
|
|
||||||
|
# Send clone request
|
||||||
|
clone_msg = Bencode.encode(%{"op" => "clone", "id" => "1"})
|
||||||
|
:ok = :gen_tcp.send(socket, clone_msg)
|
||||||
|
|
||||||
|
# Read response
|
||||||
|
{:ok, data} = :gen_tcp.recv(socket, 0, 5000)
|
||||||
|
response = Bencode.decode(data)
|
||||||
|
|
||||||
|
assert response["status"] == ["done"]
|
||||||
|
assert is_binary(response["new-session"])
|
||||||
|
|
||||||
|
:gen_tcp.close(socket)
|
||||||
|
GenServer.stop(server)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "full eval over TCP" do
|
||||||
|
{:ok, server} = Server.start_link(port: 0)
|
||||||
|
port = Server.port(server)
|
||||||
|
|
||||||
|
{:ok, socket} = :gen_tcp.connect(~c"127.0.0.1", port, [:binary, active: false])
|
||||||
|
|
||||||
|
# Clone
|
||||||
|
:ok = :gen_tcp.send(socket, Bencode.encode(%{"op" => "clone", "id" => "1"}))
|
||||||
|
{:ok, clone_data} = :gen_tcp.recv(socket, 0, 5000)
|
||||||
|
clone_resp = Bencode.decode(clone_data)
|
||||||
|
session = clone_resp["new-session"]
|
||||||
|
|
||||||
|
# Eval
|
||||||
|
eval_msg =
|
||||||
|
Bencode.encode(%{
|
||||||
|
"op" => "eval",
|
||||||
|
"code" => "(+ 21 21)",
|
||||||
|
"session" => session,
|
||||||
|
"id" => "2"
|
||||||
|
})
|
||||||
|
|
||||||
|
:ok = :gen_tcp.send(socket, eval_msg)
|
||||||
|
|
||||||
|
# Read all responses (value + done)
|
||||||
|
responses = read_all_responses(socket)
|
||||||
|
|
||||||
|
values = Enum.filter(responses, &Map.has_key?(&1, "value"))
|
||||||
|
assert length(values) >= 1
|
||||||
|
assert hd(values)["value"] == "42"
|
||||||
|
|
||||||
|
:gen_tcp.close(socket)
|
||||||
|
GenServer.stop(server)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Helper to read multiple bencode responses
|
||||||
|
defp read_all_responses(socket, acc \\ [], buffer \\ "") do
|
||||||
|
case :gen_tcp.recv(socket, 0, 2000) do
|
||||||
|
{:ok, data} ->
|
||||||
|
new_buffer = buffer <> data
|
||||||
|
{msgs, rest} = decode_available(new_buffer)
|
||||||
|
new_acc = acc ++ msgs
|
||||||
|
|
||||||
|
if Enum.any?(new_acc, fn r -> r["status"] == ["done"] end) do
|
||||||
|
new_acc
|
||||||
|
else
|
||||||
|
read_all_responses(socket, new_acc, rest)
|
||||||
|
end
|
||||||
|
|
||||||
|
{:error, :timeout} ->
|
||||||
|
{msgs, _rest} = decode_available(buffer)
|
||||||
|
acc ++ msgs
|
||||||
|
|
||||||
|
{:error, _} ->
|
||||||
|
acc
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp decode_available(data, acc \\ []) do
|
||||||
|
try do
|
||||||
|
{msg, rest} = Bencode.decode_one(data)
|
||||||
|
decode_available(rest, acc ++ [msg])
|
||||||
|
rescue
|
||||||
|
_ -> {acc, data}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,342 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
defmodule CljElixir.REPLTest do
|
||||||
|
use ExUnit.Case, async: true
|
||||||
|
|
||||||
|
alias CljElixir.REPL
|
||||||
|
|
||||||
|
describe "REPL.new" do
|
||||||
|
test "creates initial state" do
|
||||||
|
state = REPL.new()
|
||||||
|
assert state.bindings == []
|
||||||
|
assert state.history == []
|
||||||
|
assert state.counter == 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "REPL.eval" do
|
||||||
|
test "evaluates simple expression" do
|
||||||
|
state = REPL.new()
|
||||||
|
assert {:ok, "3", _} = REPL.eval("(+ 1 2)", state)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "pr-str formats output" do
|
||||||
|
state = REPL.new()
|
||||||
|
{:ok, result, _} = REPL.eval("\"hello\"", state)
|
||||||
|
assert result == "\"hello\""
|
||||||
|
end
|
||||||
|
|
||||||
|
test "nil result" do
|
||||||
|
state = REPL.new()
|
||||||
|
{:ok, result, _} = REPL.eval("nil", state)
|
||||||
|
assert result == "nil"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "map result" do
|
||||||
|
state = REPL.new()
|
||||||
|
{:ok, result, _} = REPL.eval("{:a 1 :b 2}", state)
|
||||||
|
assert result =~ ":a"
|
||||||
|
assert result =~ "1"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "increments counter" do
|
||||||
|
state = REPL.new()
|
||||||
|
{:ok, _, state2} = REPL.eval("1", state)
|
||||||
|
assert state2.counter == 2
|
||||||
|
{:ok, _, state3} = REPL.eval("2", state2)
|
||||||
|
assert state3.counter == 3
|
||||||
|
end
|
||||||
|
|
||||||
|
test "stores history" do
|
||||||
|
state = REPL.new()
|
||||||
|
{:ok, _, state2} = REPL.eval("(+ 1 2)", state)
|
||||||
|
assert state2.history == ["(+ 1 2)"]
|
||||||
|
{:ok, _, state3} = REPL.eval("(+ 3 4)", state2)
|
||||||
|
assert state3.history == ["(+ 3 4)", "(+ 1 2)"]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "error returns error tuple" do
|
||||||
|
state = REPL.new()
|
||||||
|
{:error, msg, _} = REPL.eval("(defmodule REPLErrTest (invalid-syntax", state)
|
||||||
|
assert is_binary(msg)
|
||||||
|
assert msg =~ "Error" or msg =~ "error"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "defmodule persists across evals" do
|
||||||
|
state = REPL.new()
|
||||||
|
|
||||||
|
{:ok, _, state2} =
|
||||||
|
REPL.eval("""
|
||||||
|
(defmodule REPLTestMod
|
||||||
|
(defn hello [] :hi))
|
||||||
|
""", state)
|
||||||
|
|
||||||
|
{:ok, result, _} = REPL.eval("(REPLTestMod/hello)", state2)
|
||||||
|
assert result == ":hi"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "tuple result" do
|
||||||
|
state = REPL.new()
|
||||||
|
{:ok, result, _} = REPL.eval("#el[:ok 42]", state)
|
||||||
|
assert result == "#el[:ok 42]"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "list result" do
|
||||||
|
state = REPL.new()
|
||||||
|
{:ok, result, _} = REPL.eval("(list 1 2 3)", state)
|
||||||
|
assert result == "(1 2 3)"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "REPL.balanced?" do
|
||||||
|
test "balanced parens" do
|
||||||
|
assert REPL.balanced?("(+ 1 2)")
|
||||||
|
assert REPL.balanced?("(defn foo [x] (+ x 1))")
|
||||||
|
assert REPL.balanced?("42")
|
||||||
|
assert REPL.balanced?("")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "unbalanced parens" do
|
||||||
|
refute REPL.balanced?("(+ 1 2")
|
||||||
|
refute REPL.balanced?("(defn foo [x]")
|
||||||
|
refute REPL.balanced?("(let [x 1")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "balanced with nested" do
|
||||||
|
assert REPL.balanced?("(let [x (+ 1 2)] (* x x))")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "string contents not counted" do
|
||||||
|
assert REPL.balanced?("(str \"(hello)\")")
|
||||||
|
assert REPL.balanced?("(str \"[not real]\")")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "comment contents not counted" do
|
||||||
|
assert REPL.balanced?("(+ 1 2) ; this has unbalanced (")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "mixed delimiters" do
|
||||||
|
assert REPL.balanced?("(let [{:keys [a b]} {:a 1 :b 2}] (+ a b))")
|
||||||
|
refute REPL.balanced?("(let [{:keys [a b]} {:a 1 :b 2}] (+ a b)")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1079,12 +1079,12 @@ defmodule CljElixir.TransformerTest do
|
|||||||
describe "dynamic vars" do
|
describe "dynamic vars" do
|
||||||
test "*self* produces self() call" do
|
test "*self* produces self() call" do
|
||||||
ast = transform("*self*")
|
ast = transform("*self*")
|
||||||
assert ast == {:self, [], []}
|
assert match?({:self, _, []}, ast)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "*node* produces node() call" do
|
test "*node* produces node() call" do
|
||||||
ast = transform("*node*")
|
ast = transform("*node*")
|
||||||
assert ast == {:node, [], []}
|
assert match?({:node, _, []}, ast)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "*self* evaluates to current process" do
|
test "*self* evaluates to current process" do
|
||||||
@@ -1131,12 +1131,12 @@ defmodule CljElixir.TransformerTest do
|
|||||||
describe "symbols" do
|
describe "symbols" do
|
||||||
test "plain symbol becomes variable" do
|
test "plain symbol becomes variable" do
|
||||||
ast = transform("x")
|
ast = transform("x")
|
||||||
assert ast == {:x, [], nil}
|
assert match?({:x, _, nil}, ast)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "symbol with hyphens becomes munged variable" do
|
test "symbol with hyphens becomes munged variable" do
|
||||||
ast = transform("my-var")
|
ast = transform("my-var")
|
||||||
assert ast == {:my_var, [], nil}
|
assert match?({:my_var, _, nil}, ast)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "true symbol becomes true literal" do
|
test "true symbol becomes true literal" do
|
||||||
|
|||||||
Reference in New Issue
Block a user