Files
CljElixir/lib/clj_elixir/printer.ex
Adam 7e82efd7ec Phase 8: REPL, printing, source maps, and nREPL server
- IPrintWithWriter protocol + CljElixir.Printer module with pr-str,
  print-str, pr, prn for all BEAM types (EDN-like output)
- Source-mapped error messages: line/col metadata from reader now
  propagated through transformer into Elixir AST for accurate error
  locations in .clje files
- Interactive REPL (mix clje.repl) with multi-line input detection,
  history, bindings persistence, and pr-str formatted output
- nREPL server (mix clje.nrepl) with TCP transport, Bencode wire
  protocol, session management, and core operations (clone, close,
  eval, describe, ls-sessions, load-file, interrupt, completions).
  Writes .nrepl-port for editor auto-discovery.

92 new tests (699 total, 0 failures).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 11:03:10 -04:00

146 lines
4.1 KiB
Elixir

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