- 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>
146 lines
4.1 KiB
Elixir
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
|