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,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
|
||||
Reference in New Issue
Block a user