defmodule Mix.Tasks.Ptc.Repl do @shortdoc "Interactive PTC-Lisp REPL with turn history (*1, *2, *3)" @moduledoc """ Starts an interactive REPL for testing PTC-Lisp expressions. ## Usage mix ptc.repl # Interactive REPL (default) mix ptc.repl -l prelude.clj # Load file, then interactive mix ptc.repl -e "(+ 1 2)" # Eval and print result mix ptc.repl -e "(def x 1)" -e "(* x 2)" # Chain evals (memory persists) mix ptc.repl script.clj # Run script file mix ptc.repl - # Run from stdin ## Options * `-e, --eval` - Evaluate expression and print result (can be repeated) * `-l, --load` - Load file before entering interactive mode * `-h, --help` - Print this help ## Features - Evaluate PTC-Lisp expressions interactively - Multi-line input: continues prompting until parens are balanced - Turn history: `*1`, `*2`, `*3` reference last 3 results - Memory persists between evaluations - Exit with Ctrl+D ## Example Session ptc> (+ 1 2) 3 ptc> (* *1 10) 30 ptc> {:sum *1, :product *2} %{sum: 30, product: 30} """ use Mix.Task alias PtcRunner.Lisp.Format alias PtcRunner.SubAgent.Loop.ResponseHandler @switches [eval: :keep, load: :string, help: :boolean] @aliases [e: :eval, l: :load, h: :help] @impl Mix.Task def run(args) do Mix.Task.run("app.start") {opts, rest, _invalid} = OptionParser.parse(args, switches: @switches, aliases: @aliases) cond do opts[:help] -> print_help() rest == ["-"] -> run_stdin() rest != [] -> run_file(hd(rest)) opts[:eval] -> run_evals(Keyword.get_values(opts, :eval)) opts[:load] -> load_and_repl(opts[:load]) true -> interactive_repl() end end defp print_help do IO.puts(@moduledoc) end defp run_stdin do case IO.read(:stdio, :eof) do {:error, reason} -> IO.puts(:stderr, "Error reading stdin: #{reason}") System.halt(1) source -> run_source(source) end end defp run_file(path) do case File.read(path) do {:ok, source} -> run_source(source) {:error, reason} -> IO.puts(:stderr, "Error reading #{path}: #{:file.format_error(reason)}") System.halt(1) end end defp run_evals(exprs) do # run_source halts on error, so we can use simple reduce Enum.reduce(exprs, %{}, fn expr, memory -> {:ok, new_memory} = run_source(expr, memory) new_memory end) end defp run_source(source, memory \\ %{}) do case PtcRunner.Lisp.run(source, memory: memory) do {:ok, step} -> {output, _truncated?} = Format.to_clojure(step.return) IO.puts(output) {:ok, step.memory} {:error, step} -> IO.puts(:stderr, format_error(step.fail)) System.halt(1) end end defp interactive_repl do IO.puts("PTC-Lisp REPL (Ctrl+D to exit)") IO.puts("Turn history: *1, *2, *3 reference last 3 results\n") loop([], %{}) end defp load_and_repl(path) do case File.read(path) do {:ok, source} -> case PtcRunner.Lisp.run(source) do {:ok, step} -> IO.puts("Loaded #{path}") IO.puts("PTC-Lisp REPL (Ctrl+D to exit)") IO.puts("Turn history: *1, *2, *3 reference last 3 results\n") loop([], step.memory) {:error, step} -> IO.puts(:stderr, format_error(step.fail)) System.halt(1) end {:error, reason} -> IO.puts(:stderr, "Error reading #{path}: #{:file.format_error(reason)}") System.halt(1) end end defp loop(history, memory) do case read_expression("ptc> ", "") do :eof -> IO.puts("\nGoodbye!") "" -> # Ignore empty lines, only Ctrl+D exits loop(history, memory) input -> {history, memory} = evaluate(input, history, memory) loop(history, memory) end end defp read_expression(prompt, buffer) do case IO.gets(prompt) do :eof -> :eof line -> combined = buffer <> line if balanced?(combined) do String.trim(combined) else read_expression("...> ", combined) end end end defp balanced?(str) do str |> String.graphemes() |> Enum.reduce_while(0, fn _, n when n < 0 -> {:halt, -1} "(", n -> {:cont, n + 1} ")", n -> {:cont, n - 1} _, n -> {:cont, n} end) |> Kernel.==(0) end defp evaluate(input, history, memory) do case PtcRunner.Lisp.run(input, turn_history: history, memory: memory) do {:ok, step} -> # Show truncated output exactly like LLM feedback sees {output, _truncated?} = ResponseHandler.format_execution_result(step.return) IO.puts(output) new_history = (history ++ [step.return]) |> Enum.take(-3) {new_history, step.memory} {:error, step} -> IO.puts(format_error(step.fail)) {history, memory} end end defp format_error(%{reason: reason, message: message}) do reason_str = reason |> to_string() |> String.replace("_", " ") "Error (#{reason_str}): #{message}" end end