Files
CljElixir/lib/clj_elixir/nrepl/bencode.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

97 lines
2.2 KiB
Elixir

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