init commit
This commit is contained in:
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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 ++
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user