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 @doc "Clojure-compatible str: nil→\"\", strings pass through, else print representation" def str_value(nil), do: "" def str_value(value), do: print_str(value) # 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