init commit

This commit is contained in:
2026-03-09 23:09:46 -04:00
parent 5cbc493cc5
commit 5da77e3360
73 changed files with 9935 additions and 103 deletions
+25
View File
@@ -100,6 +100,10 @@ defmodule CljElixir.Analyzer do
validate_loop(args, meta, ctx)
end
defp validate_form({:list, _meta, [{:symbol, _, "receive"} | args]}, ctx) do
validate_receive(args, ctx)
end
defp validate_form({:list, meta, [{:symbol, _, "recur"} | _args]}, ctx) do
validate_recur(meta, ctx)
end
@@ -529,6 +533,27 @@ defmodule CljElixir.Analyzer do
end
end
# receive propagates tail position into clause bodies
defp validate_receive(clauses, ctx) do
validate_receive_clauses(clauses, ctx)
end
defp validate_receive_clauses([], _ctx), do: []
defp validate_receive_clauses([:after, _timeout, body | rest], ctx) do
validate_form(body, ctx) ++ validate_receive_clauses(rest, ctx)
end
defp validate_receive_clauses([_pattern, :guard, _guard, body | rest], ctx) do
validate_form(body, ctx) ++ validate_receive_clauses(rest, ctx)
end
defp validate_receive_clauses([_pattern, body | rest], ctx) do
validate_form(body, ctx) ++ validate_receive_clauses(rest, ctx)
end
defp validate_receive_clauses([_], _ctx), do: []
defp validate_recur(meta, ctx) do
line = meta_line(meta)
col = meta_col(meta)
+39 -19
View File
@@ -83,26 +83,46 @@ defmodule CljElixir.Compiler do
@spec eval_string(String.t(), keyword()) :: {:ok, term(), keyword()} | {:error, list()}
def eval_string(source, opts \\ []) do
with {:ok, ast} <- compile_string(source, opts) do
try do
bindings = opts[:bindings] || []
env_opts = build_eval_opts(opts)
{result, new_bindings} = Code.eval_quoted(ast, bindings, env_opts)
{:ok, result, new_bindings}
rescue
e ->
file = opts[:file] || "nofile"
eval_ast(ast, opts)
end
end
{:error,
[
%{
severity: :error,
message: format_eval_error(e),
file: file,
line: extract_line(e),
col: 0
}
]}
end
@doc """
Evaluate pre-parsed CljElixir AST forms.
Runs analyze → transform → eval, skipping the read step.
Used by the REPL for incremental re-evaluation of accumulated definitions.
Returns `{:ok, result, bindings}` on success, or `{:error, diagnostics}` on failure.
"""
@spec eval_forms(list(), keyword()) :: {:ok, term(), keyword()} | {:error, list()}
def eval_forms(forms, opts \\ []) do
with {:ok, forms} <- analyze(forms, opts),
{:ok, ast} <- transform(forms, opts) do
eval_ast(ast, opts)
end
end
defp eval_ast(ast, opts) do
try do
bindings = opts[:bindings] || []
env_opts = build_eval_opts(opts)
{result, new_bindings} = Code.eval_quoted(ast, bindings, env_opts)
{:ok, result, new_bindings}
rescue
e ->
file = opts[:file] || "nofile"
{:error,
[
%{
severity: :error,
message: format_eval_error(e),
file: file,
line: extract_line(e),
col: 0
}
]}
end
end
+2 -2
View File
@@ -35,7 +35,7 @@ defmodule CljElixir.NRepl.Handler do
code = Map.get(msg, "code", "")
# Capture stdout inside the Agent process where eval actually runs
{output, result} = SessionManager.eval_with_capture(manager, session, code)
{output, result, ns} = SessionManager.eval_with_capture(manager, session, code)
responses = []
@@ -51,7 +51,7 @@ defmodule CljElixir.NRepl.Handler do
responses =
case result do
{:ok, value} ->
responses ++ [%{"id" => id, "session" => session, "value" => value, "ns" => "user"}]
responses ++ [%{"id" => id, "session" => session, "value" => value, "ns" => ns}]
{:error, error} ->
responses ++
+5 -4
View File
@@ -87,21 +87,22 @@ defmodule CljElixir.NRepl.SessionManager do
def handle_call({:eval_with_capture, id, code}, _from, state) do
case Map.get(state.sessions, id) do
nil ->
{:reply, {"", {:error, "unknown session"}}, state}
{:reply, {"", {:error, "unknown session"}, "user"}, state}
pid ->
{output, result} =
{output, result, ns} =
Agent.get_and_update(
pid,
fn repl_state ->
{output, eval_result, new_state} = eval_capturing_output(code, repl_state)
ns = CljElixir.REPL.current_ns(new_state)
{{output, eval_result}, new_state}
{{output, eval_result, ns}, new_state}
end,
:infinity
)
{:reply, {output, result}, state}
{:reply, {output, result, ns}, state}
end
end
+4
View File
@@ -36,6 +36,10 @@ defmodule CljElixir.Printer 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
+168 -22
View File
@@ -4,44 +4,52 @@ defmodule CljElixir.REPL do
Maintains state across evaluations: bindings persist,
modules defined in one evaluation are available in the next.
Tracks the current namespace (`ns`) so that bare `defn`/`def` forms
are merged into the active module and the module is recompiled
incrementally.
"""
defstruct bindings: [],
history: [],
counter: 1,
env: nil
env: nil,
current_ns: nil,
module_defs: %{}
@doc "Create a new REPL state"
def new do
Code.compiler_options(ignore_module_conflict: true)
%__MODULE__{}
end
@doc "Return the current namespace name (defaults to \"user\")"
def current_ns(%__MODULE__{current_ns: ns}), do: ns || "user"
@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.Reader.read_string(source) do
{:ok, forms} ->
has_ns = Enum.any?(forms, &ns_form?/1)
has_defs = Enum.any?(forms, &def_form?/1)
case CljElixir.Compiler.eval_string(source, opts) do
{:ok, result, new_bindings} ->
result_str = CljElixir.Printer.pr_str(result)
cond do
has_ns ->
eval_with_ns(forms, source, state)
new_state = %{state |
bindings: new_bindings,
history: [source | state.history],
counter: state.counter + 1
}
has_defs and state.current_ns != nil ->
eval_in_ns(forms, source, state)
{:ok, result_str, new_state}
true ->
eval_plain(source, state)
end
{:error, errors} ->
error_str = format_errors(errors)
new_state = %{state | counter: state.counter + 1}
{:error, error_str, new_state}
{:error, reason} ->
error_msg = if is_binary(reason), do: reason, else: inspect(reason)
{:error, "Read error: #{error_msg}", %{state | counter: state.counter + 1}}
end
end
@@ -52,15 +60,150 @@ defmodule CljElixir.REPL do
|> count_delimiters(0, 0, 0, false, false)
end
# Count open/close delimiters, respecting strings and comments
# ---------------------------------------------------------------------------
# Eval strategies
# ---------------------------------------------------------------------------
# Full ns block: set namespace, capture defs, compile normally
defp eval_with_ns(forms, source, state) do
ns_name = extract_ns_name(forms)
new_defs = collect_defs(forms)
opts = [bindings: state.bindings, file: "repl"]
case CljElixir.Compiler.eval_string(source, opts) do
{:ok, result, new_bindings} ->
new_state = %{state |
bindings: new_bindings,
current_ns: ns_name,
module_defs: new_defs,
history: [source | state.history],
counter: state.counter + 1
}
{:ok, CljElixir.Printer.pr_str(result), new_state}
{:error, errors} ->
{:error, format_errors(errors), %{state | counter: state.counter + 1}}
end
end
# Bare defs in active namespace: merge into module_defs and recompile module
defp eval_in_ns(forms, source, state) do
{new_def_forms, exprs} = Enum.split_with(forms, &def_form?/1)
# Merge new defs into accumulated module_defs (keyed by name)
merged_defs =
Enum.reduce(new_def_forms, state.module_defs, fn form, acc ->
name = extract_def_name(form)
Map.put(acc, name, form)
end)
# Reconstruct: ns + all accumulated defs + current expressions
ns_form = make_ns_form(state.current_ns)
all_forms = [ns_form | Map.values(merged_defs)] ++ exprs
opts = [bindings: state.bindings, file: "repl"]
case CljElixir.Compiler.eval_forms(all_forms, opts) do
{:ok, result, new_bindings} ->
result_str =
if exprs == [] do
# Def-only: show var-like representation
new_def_forms
|> Enum.map(&extract_def_name/1)
|> Enum.map_join(" ", &"#'#{state.current_ns}/#{&1}")
else
CljElixir.Printer.pr_str(result)
end
new_state = %{state |
bindings: new_bindings,
module_defs: merged_defs,
history: [source | state.history],
counter: state.counter + 1
}
{:ok, result_str, new_state}
{:error, errors} ->
{:error, format_errors(errors), %{state | counter: state.counter + 1}}
end
end
# No ns context: eval as-is (legacy / ad-hoc expressions)
defp eval_plain(source, state) do
opts = [bindings: state.bindings, file: "repl"]
case CljElixir.Compiler.eval_string(source, opts) do
{:ok, result, new_bindings} ->
new_state = %{state |
bindings: new_bindings,
history: [source | state.history],
counter: state.counter + 1
}
{:ok, CljElixir.Printer.pr_str(result), new_state}
{:error, errors} ->
{:error, format_errors(errors), %{state | counter: state.counter + 1}}
end
end
# ---------------------------------------------------------------------------
# Form classification helpers
# ---------------------------------------------------------------------------
defp ns_form?({:list, _, [{:symbol, _, "ns"} | _]}), do: true
defp ns_form?(_), do: false
defp def_form?({:list, _, [{:symbol, _, name} | _]})
when name in ~w(defn defn- def defprotocol defrecord extend-type
extend-protocol reify defmacro use),
do: true
defp def_form?({:list, _, [{:symbol, _, "m/=>"} | _]}), do: true
defp def_form?(_), do: false
defp extract_ns_name(forms) do
Enum.find_value(forms, fn
{:list, _, [{:symbol, _, "ns"}, {:symbol, _, name} | _]} -> name
_ -> nil
end)
end
defp collect_defs(forms) do
forms
|> Enum.filter(&def_form?/1)
|> Enum.reduce(%{}, fn form, acc ->
name = extract_def_name(form)
Map.put(acc, name, form)
end)
end
defp extract_def_name({:list, _, [{:symbol, _, _}, {:symbol, _, name} | _]}), do: name
defp extract_def_name(form), do: "anon_#{:erlang.phash2(form)}"
defp make_ns_form(ns_name) do
{:list, %{line: 0, col: 0}, [
{:symbol, %{line: 0, col: 0}, "ns"},
{:symbol, %{line: 0, col: 0}, ns_name}
]}
end
# ---------------------------------------------------------------------------
# Delimiter balancing
# ---------------------------------------------------------------------------
defp count_delimiters([], parens, brackets, braces, _in_string, _escape) do
parens == 0 and brackets == 0 and braces == 0
# Negative counts mean excess closing delimiters — let the reader report the error
parens < 0 or brackets < 0 or braces < 0 or
(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 ->
@@ -76,7 +219,6 @@ defmodule CljElixir.REPL do
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)
@@ -91,6 +233,10 @@ defmodule CljElixir.REPL do
end
end
# ---------------------------------------------------------------------------
# Error formatting
# ---------------------------------------------------------------------------
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 ->
+119 -33
View File
@@ -53,14 +53,51 @@ defmodule CljElixir.Transformer do
def transform(forms, ctx \\ %Context{})
def transform(forms, ctx) when is_list(forms) do
{elixir_forms, _ctx} =
Enum.map_reduce(forms, ctx, fn form, acc ->
{ast, new_ctx} = transform_form(form, acc)
{ast, new_ctx}
# Check if file has explicit defmodule forms (ns won't auto-wrap if so)
has_defmodule =
Enum.any?(forms, fn
{:list, _, [{:symbol, _, "defmodule"} | _]} -> true
_ -> false
end)
# Filter out nil (from defmacro which produces no runtime code)
elixir_forms = Enum.filter(elixir_forms, &(&1 != nil))
{elixir_forms, final_ctx} =
Enum.map_reduce(forms, ctx, fn form, acc ->
{ast, new_ctx} = transform_form(form, acc)
# Tag each transformed form with whether the source was a def-like form
{{ast, def_form?(form)}, new_ctx}
end)
# Filter out nil (from ns, defmacro which produce no runtime code)
elixir_forms = Enum.filter(elixir_forms, fn {ast, _} -> ast != nil end)
# If ns declared a module and there are no explicit defmodule forms,
# separate def-forms (inside module) from expressions (after module)
elixir_forms =
if final_ctx.module_name != nil and ctx.module_name == nil and not has_defmodule do
{defs, exprs} =
Enum.split_with(elixir_forms, fn {_ast, is_def} -> is_def end)
def_asts = Enum.map(defs, fn {ast, _} -> ast end)
expr_asts = Enum.map(exprs, fn {ast, _} -> ast end)
block =
case def_asts do
[] -> nil
[single] -> single
multiple -> {:__block__, [], multiple}
end
module_ast =
if block do
[{:defmodule, [context: Elixir], [final_ctx.module_name, [do: block]]}]
else
[]
end
module_ast ++ expr_asts
else
Enum.map(elixir_forms, fn {ast, _} -> ast end)
end
case elixir_forms do
[] -> nil
@@ -74,6 +111,30 @@ defmodule CljElixir.Transformer do
ast
end
# Transform a list of guard forms into a single ANDed Elixir guard AST.
# [:guard [(> x 0) (< x 10)]] → {:and, [], [guard1, guard2]}
defp transform_guards(guard_forms, ctx) do
guard_asts = Enum.map(guard_forms, &transform(&1, ctx))
case guard_asts do
[single] -> single
[first | rest] -> Enum.reduce(rest, first, fn g, acc ->
{:and, [context: Elixir], [acc, g]}
end)
end
end
# Is this CljElixir AST form a definition (goes inside defmodule)?
defp def_form?({:list, _, [{:symbol, _, name} | _]})
when name in ~w(defn defn- def defprotocol defrecord extend-type
extend-protocol reify defmacro use),
do: true
# m/=> schema annotations
defp def_form?({:list, _, [{:symbol, _, "m/=>"} | _]}), do: true
defp def_form?(_), do: false
# ---------------------------------------------------------------------------
# Main dispatch
# ---------------------------------------------------------------------------
@@ -258,6 +319,7 @@ defmodule CljElixir.Transformer do
defp transform_list([head | args], meta, ctx) do
case head do
# --- Special forms (symbols) ---
{:symbol, _, "ns"} -> transform_ns(args, meta, ctx)
{:symbol, _, "defmodule"} -> transform_defmodule(args, meta, ctx)
{:symbol, _, "defn"} -> transform_defn(args, meta, ctx, :def)
{:symbol, _, "defn-"} -> transform_defn(args, meta, ctx, :defp)
@@ -419,6 +481,17 @@ defmodule CljElixir.Transformer do
end
end
# ---------------------------------------------------------------------------
# 0. ns — module declaration (sets ctx.module_name for auto-wrapping)
# ---------------------------------------------------------------------------
defp transform_ns([name_form | _rest], _meta, ctx) do
mod_alias = module_name_ast(name_form)
{nil, %{ctx | module_name: mod_alias}}
end
defp transform_ns([], _meta, ctx), do: {nil, ctx}
# ---------------------------------------------------------------------------
# 1. defmodule
# ---------------------------------------------------------------------------
@@ -527,8 +600,8 @@ defmodule CljElixir.Transformer do
nil ->
{def_kind, em, [call_with_args(fun_name, param_asts), [do: body_ast]]}
guard_form ->
guard_ast = transform(guard_form, fn_ctx)
guard_forms ->
guard_ast = transform_guards(guard_forms, fn_ctx)
{def_kind, em,
[
@@ -568,18 +641,18 @@ defmodule CljElixir.Transformer do
{required, rest_param, nil, body}
{:list, _, clause_elements} ->
# Might have guard: ([params] :when guard body)
parse_clause_with_guard(clause_elements)
# Might have guard: ([params] :guard guard body)
parse_clause_with_guards(clause_elements)
end)
end
end
defp parse_clause_with_guard([{:vector, _, params}, :when, guard | body]) do
defp parse_clause_with_guards([{:vector, _, params}, :guard, {:vector, _, guards} | body]) do
{required, rest_param} = split_rest_params(params)
{required, rest_param, guard, body}
{required, rest_param, guards, body}
end
defp parse_clause_with_guard([{:vector, _, params} | body]) do
defp parse_clause_with_guards([{:vector, _, params} | body]) do
{required, rest_param} = split_rest_params(params)
{required, rest_param, nil, body}
end
@@ -630,8 +703,8 @@ defmodule CljElixir.Transformer do
nil ->
{:->, [], [all_param_asts, body_ast]}
guard_form ->
guard_ast = transform(guard_form, ctx)
guard_forms ->
guard_ast = transform_guards(guard_forms, ctx)
guard_params = [{:when, [], all_param_asts ++ [guard_ast]}]
{:->, [], [guard_params, body_ast]}
end
@@ -657,7 +730,7 @@ defmodule CljElixir.Transformer do
{required, rest_param, nil, body}
{:list, _, clause_elements} ->
parse_clause_with_guard(clause_elements)
parse_clause_with_guards(clause_elements)
end)
end
end
@@ -823,8 +896,8 @@ defmodule CljElixir.Transformer do
clauses
|> Enum.chunk_every(2)
|> Enum.map(fn
[pattern, :when | rest] ->
# pattern :when guard body — need to re-chunk
[pattern, :guard | rest] ->
# pattern :guard guard body — need to re-chunk
# This won't happen with chunk_every(2), handle differently
pat_ast = transform(pattern, pattern_ctx)
body_ast = transform(List.last(rest), ctx)
@@ -1550,8 +1623,8 @@ defmodule CljElixir.Transformer do
nil ->
{:->, [], [[pat_ast], body_ast]}
guard_form ->
guard_ast = transform(guard_form, ctx)
guard_forms ->
guard_ast = transform_guards(guard_forms, ctx)
{:->, [], [[{:when, [], [pat_ast, guard_ast]}], body_ast]}
end
end)
@@ -1585,8 +1658,8 @@ defmodule CljElixir.Transformer do
parse_receive_clauses(rest, acc, {timeout, body})
end
defp parse_receive_clauses([pattern, :when, guard, body | rest], acc, after_clause) do
parse_receive_clauses(rest, [{pattern, guard, body} | acc], after_clause)
defp parse_receive_clauses([pattern, :guard, {:vector, _, guards}, body | rest], acc, after_clause) do
parse_receive_clauses(rest, [{pattern, guards, body} | acc], after_clause)
end
defp parse_receive_clauses([pattern, body | rest], acc, after_clause) do
@@ -1959,23 +2032,24 @@ defmodule CljElixir.Transformer do
{{:-, [], [a_ast, 1]}, ctx}
end
# str — concatenate with <> using to_string
# str — Clojure-compatible: nil→"", strings pass through, collections use print repr
defp transform_str(args, ctx) do
t_args = Enum.map(args, fn a -> transform(a, ctx) end)
str_call = fn arg ->
{{:., [], [{:__aliases__, [alias: false], [:CljElixir, :Printer]}, :str_value]}, [], [arg]}
end
ast =
case t_args do
[] ->
""
[single] ->
{{:., [], [{:__aliases__, [alias: false], [:Kernel]}, :to_string]}, [], [single]}
str_call.(single)
_ ->
stringified =
Enum.map(t_args, fn a ->
{{:., [], [{:__aliases__, [alias: false], [:Kernel]}, :to_string]}, [], [a]}
end)
stringified = Enum.map(t_args, str_call)
Enum.reduce(tl(stringified), hd(stringified), fn arg, acc ->
{:<>, [], [acc, arg]}
@@ -1985,12 +2059,12 @@ defmodule CljElixir.Transformer do
{ast, ctx}
end
# println → IO.puts
# println → IO.puts, returns nil (Clojure convention)
defp transform_println(args, ctx) do
t_args = Enum.map(args, fn a -> transform(a, ctx) end)
# If multiple args, join with str first
ast =
io_call =
case t_args do
[single] ->
{{:., [], [{:__aliases__, [alias: false], [:IO]}, :puts]}, [], [single]}
@@ -2001,6 +2075,7 @@ defmodule CljElixir.Transformer do
{{:., [], [{:__aliases__, [alias: false], [:IO]}, :puts]}, [], [str_ast]}
end
ast = {:__block__, [], [io_call, nil]}
{ast, ctx}
end
@@ -2052,11 +2127,11 @@ defmodule CljElixir.Transformer do
end
end)
ast = {:__block__, [], writes}
ast = {:__block__, [], writes ++ [nil]}
{ast, ctx}
end
# (prn val) -> IO.puts(CljElixir.Printer.pr_str(val))
# (prn val) -> IO.puts(CljElixir.Printer.pr_str(val)), returns nil
# Multiple args joined with spaces, then newline
defp transform_prn(args, ctx) do
t_args = Enum.map(args, fn a -> transform(a, ctx) end)
@@ -2079,7 +2154,8 @@ defmodule CljElixir.Transformer do
end)
end
ast = {{:., [], [io_mod, :puts]}, [], [joined]}
io_call = {{:., [], [io_mod, :puts]}, [], [joined]}
ast = {:__block__, [], [io_call, nil]}
{ast, ctx}
end
@@ -2535,6 +2611,16 @@ defmodule CljElixir.Transformer do
{ast, ctx}
end
# update - (update m k f x y ...) => rewrite to (assoc m k (f (get m k) x y ...))
# Rewritten at AST level so f goes through builtin dispatch (e.g. dissoc)
defp transform_update([m, k, f | extra_args], ctx) do
meta = %{line: 0, col: 0}
get_call = {:list, meta, [{:symbol, meta, "get"}, m, k]}
f_call = {:list, meta, [f, get_call | extra_args]}
assoc_call = {:list, meta, [{:symbol, meta, "assoc"}, m, k, f_call]}
do_transform(assoc_call, ctx)
end
# conj
defp transform_conj([c, x], ctx) do
c_ast = transform(c, ctx)
+72
View File
@@ -0,0 +1,72 @@
defmodule Mix.Tasks.Clje.Build do
@moduledoc """
Compile CljElixir files to BEAM bytecode.
## Usage
mix clje.build src/my_module.clje
mix clje.build src/foo.clje src/bar.clje
mix clje.build src/foo.clje -o _build/dev/lib/clj_elixir/ebin
Like `elixirc`. Compiles `.clje` files to `.beam` files without running them.
## Options
* `-o` / `--output` - output directory for .beam files (default: `_build/dev/lib/<app>/ebin`)
"""
use Mix.Task
@shortdoc "Compile .clje files to BEAM bytecode"
@impl Mix.Task
def run(args) do
{opts, files, _} =
OptionParser.parse(args,
switches: [output: :string],
aliases: [o: :output]
)
if files == [] do
Mix.shell().error("Usage: mix clje.build <file.clje> [...] [-o output_dir]")
System.halt(1)
end
Mix.Task.run("compile")
Mix.Task.run("app.start")
output_dir = opts[:output] || Mix.Project.compile_path()
results =
Enum.map(files, fn file ->
Mix.shell().info("Compiling #{file}")
case CljElixir.Compiler.compile_file_to_beam(file, output_dir: output_dir) do
{:ok, modules} ->
Enum.each(modules, fn {mod, _binary} ->
Mix.shell().info(" -> #{mod}")
end)
:ok
{:error, diagnostics} ->
Enum.each(diagnostics, fn diag ->
loc =
case {Map.get(diag, :file), Map.get(diag, :line, 0)} do
{nil, _} -> ""
{_f, 0} -> "#{diag.file}: "
{_f, l} -> "#{diag.file}:#{l}: "
end
Mix.shell().error("#{loc}#{diag.severity}: #{diag.message}")
end)
:error
end
end)
if Enum.any?(results, &(&1 == :error)) do
System.halt(1)
end
end
end
+45
View File
@@ -0,0 +1,45 @@
defmodule Mix.Tasks.Clje.Eval do
@moduledoc """
Evaluate a CljElixir expression from the command line.
## Usage
mix clje.eval '(+ 1 2)'
mix clje.eval '(defn greet [name] (str "hello " name))' '(greet "world")'
Multiple expressions are evaluated in sequence, with bindings persisting.
The result of the last expression is printed.
"""
use Mix.Task
@shortdoc "Evaluate CljElixir expressions"
@impl Mix.Task
def run([]) do
Mix.shell().error("Usage: mix clje.eval '<expression>' [...]")
System.halt(1)
end
def run(exprs) do
Mix.Task.run("compile")
Mix.Task.run("app.start")
{result, _bindings} =
Enum.reduce(exprs, {nil, []}, fn expr, {_prev, bindings} ->
case CljElixir.Compiler.eval_string(expr, bindings: bindings) do
{:ok, result, new_bindings} ->
{result, new_bindings}
{:error, diagnostics} ->
Enum.each(diagnostics, fn diag ->
Mix.shell().error("#{diag.severity}: #{diag.message}")
end)
System.halt(1)
end
end)
IO.puts(CljElixir.Printer.pr_str(result))
end
end
+40 -13
View File
@@ -38,7 +38,8 @@ defmodule Mix.Tasks.Clje.Repl do
end
defp loop(state) do
prompt = "clje:#{state.counter}> "
ns = CljElixir.REPL.current_ns(state)
prompt = "#{ns}:#{state.counter}> "
case read_input(prompt) do
:eof ->
@@ -80,10 +81,28 @@ defmodule Mix.Tasks.Clje.Repl do
end
defp read_input(prompt) do
case IO.gets(prompt) do
:eof -> :eof
{:error, _} -> :eof
data -> data
IO.write(prompt)
read_line()
end
# Read a line character-by-character, treating both \r and \n as line terminators.
# This avoids IO.gets hanging when the terminal sends \r without \n.
defp read_line, do: read_line([])
defp read_line(acc) do
case IO.getn("", 1) do
:eof ->
if acc == [], do: :eof, else: acc |> Enum.reverse() |> IO.iodata_to_binary()
{:error, _} ->
if acc == [], do: :eof, else: acc |> Enum.reverse() |> IO.iodata_to_binary()
<<c>> when c in [?\r, ?\n] ->
IO.write("\n")
acc |> Enum.reverse() |> IO.iodata_to_binary()
char ->
read_line([char | acc])
end
end
@@ -96,16 +115,24 @@ defmodule Mix.Tasks.Clje.Repl do
end
defp read_continuation(acc) do
case IO.gets(" ") do
:eof -> acc
{:error, _} -> acc
line ->
new_acc = acc <> "\n" <> String.trim_trailing(line, "\n")
IO.write(" ")
if CljElixir.REPL.balanced?(new_acc) do
new_acc
case read_line() do
:eof -> acc
line ->
trimmed = String.trim(line)
if trimmed == "" do
# Empty Enter in continuation mode: submit what we have
acc
else
read_continuation(new_acc)
new_acc = acc <> "\n" <> trimmed
if CljElixir.REPL.balanced?(new_acc) do
new_acc
else
read_continuation(new_acc)
end
end
end
end
+103
View File
@@ -0,0 +1,103 @@
defmodule Mix.Tasks.Clje.Run do
@moduledoc """
Compile and run a CljElixir file.
## Usage
mix clje.run examples/chat_room.clje
mix clje.run -e '(println "hello")' script.clje
Like `elixir script.exs` or `bb script.clj`. The file is compiled and
evaluated. Modules defined in the file become available.
## Options
* `-e` / `--eval` - evaluate expression before running the file
* `--no-halt` - keep the system running after execution (useful for spawned processes)
Arguments after `--` are available via `System.argv()`.
"""
use Mix.Task
@shortdoc "Run a CljElixir file"
@impl Mix.Task
def run(args) do
# Split on "--" to separate mix opts from script args
{before_dashdash, script_args} = split_on_dashdash(args)
{opts, positional, _} =
OptionParser.parse(before_dashdash,
switches: [eval: :keep, no_halt: :boolean],
aliases: [e: :eval]
)
Mix.Task.run("compile")
Mix.Task.run("app.start")
# Make script args available via System.argv()
System.argv(script_args)
# Evaluate any -e expressions first
bindings =
opts
|> Keyword.get_values(:eval)
|> Enum.reduce([], fn expr, bindings ->
case CljElixir.Compiler.eval_string(expr, bindings: bindings) do
{:ok, result, new_bindings} ->
IO.puts(CljElixir.Printer.pr_str(result))
new_bindings
{:error, diagnostics} ->
print_diagnostics(diagnostics)
System.halt(1)
end
end)
# Run file(s)
case positional do
[] ->
unless Keyword.has_key?(opts, :eval) do
Mix.shell().error("Usage: mix clje.run [options] <file.clje>")
System.halt(1)
end
files ->
Enum.reduce(files, bindings, fn file, bindings ->
case CljElixir.Compiler.eval_file(file, bindings: bindings) do
{:ok, _result, new_bindings} ->
new_bindings
{:error, diagnostics} ->
print_diagnostics(diagnostics)
System.halt(1)
end
end)
end
if opts[:no_halt] do
Process.sleep(:infinity)
end
end
defp split_on_dashdash(args) do
case Enum.split_while(args, &(&1 != "--")) do
{before, ["--" | rest]} -> {before, rest}
{before, []} -> {before, []}
end
end
defp print_diagnostics(diagnostics) do
Enum.each(diagnostics, fn diag ->
loc =
case {Map.get(diag, :file), Map.get(diag, :line, 0)} do
{nil, _} -> ""
{_f, 0} -> "#{diag.file}: "
{_f, l} -> "#{diag.file}:#{l}: "
end
Mix.shell().error("#{loc}#{diag.severity}: #{diag.message}")
end)
end
end