- 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>
97 lines
2.2 KiB
Elixir
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
|