defmodule CljElixir.REPL do @moduledoc """ CljElixir Read-Eval-Print Loop engine. Maintains state across evaluations: bindings persist, modules defined in one evaluation are available in the next. """ defstruct bindings: [], history: [], counter: 1, env: nil @doc "Create a new REPL state" def new do %__MODULE__{} end @doc """ Evaluate a CljElixir source string in the given REPL state. Returns {:ok, result_string, new_state} or {:error, error_string, new_state}. """ def eval(source, state) do opts = [ bindings: state.bindings, file: "repl" ] case CljElixir.Compiler.eval_string(source, opts) do {:ok, result, new_bindings} -> result_str = CljElixir.Printer.pr_str(result) new_state = %{state | bindings: new_bindings, history: [source | state.history], counter: state.counter + 1 } {:ok, result_str, new_state} {:error, errors} -> error_str = format_errors(errors) new_state = %{state | counter: state.counter + 1} {:error, error_str, new_state} end end @doc "Check if input has balanced delimiters (parens, brackets, braces)" def balanced?(input) do input |> String.graphemes() |> count_delimiters(0, 0, 0, false, false) end # Count open/close delimiters, respecting strings and comments defp count_delimiters([], parens, brackets, braces, _in_string, _escape) do parens == 0 and brackets == 0 and braces == 0 end defp count_delimiters([char | rest], p, b, br, in_string, escape) do cond do escape -> # Previous char was \, skip this one count_delimiters(rest, p, b, br, in_string, false) char == "\\" and in_string -> count_delimiters(rest, p, b, br, in_string, true) char == "\"" and not in_string -> count_delimiters(rest, p, b, br, true, false) char == "\"" and in_string -> count_delimiters(rest, p, b, br, false, false) in_string -> count_delimiters(rest, p, b, br, true, false) char == ";" -> # Comment - skip rest of line rest_after_newline = Enum.drop_while(rest, &(&1 != "\n")) count_delimiters(rest_after_newline, p, b, br, false, false) char == "(" -> count_delimiters(rest, p + 1, b, br, false, false) char == ")" -> count_delimiters(rest, p - 1, b, br, false, false) char == "[" -> count_delimiters(rest, p, b + 1, br, false, false) char == "]" -> count_delimiters(rest, p, b - 1, br, false, false) char == "{" -> count_delimiters(rest, p, b, br + 1, false, false) char == "}" -> count_delimiters(rest, p, b, br - 1, false, false) true -> count_delimiters(rest, p, b, br, false, false) end end defp format_errors(errors) when is_list(errors) do Enum.map_join(errors, "\n", fn %{message: msg, line: line} when is_integer(line) and line > 0 -> "Error on line #{line}: #{msg}" %{message: msg} -> "Error: #{msg}" other -> "Error: #{inspect(other)}" end) end defp format_errors(other) do "Error: #{inspect(other)}" end end