Files
CljElixir/lib/clj_elixir/transformer.ex
Adam 7e82efd7ec Phase 8: REPL, printing, source maps, and nREPL server
- 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>
2026-03-08 11:03:10 -04:00

3411 lines
105 KiB
Elixir

defmodule CljElixir.Transformer do
@moduledoc """
Transforms CljElixir AST (from the reader) into Elixir AST (quoted expressions).
CljElixir AST nodes:
- Literals: integers, floats, strings, atoms, booleans, nil
- {:symbol, meta, "name"}
- {:list, meta, [elements]}
- {:vector, meta, [elements]}
- {:map, meta, [k1, v1, k2, v2, ...]}
- {:set, meta, [elements]}
- {:tuple, meta, [elements]}
- {:regex, meta, "pattern"}
- {:quote, meta, inner}
- {:with_meta, meta, {metadata, target}}
- {:anon_fn, meta, body}
- {:quasiquote, meta, form}
- {:unquote, meta, form}
- {:splice_unquote, meta, form}
- {:deref, meta, form}
Elixir AST: {atom, keyword_list_meta, args_list} for calls, literals for literals.
"""
# ---------------------------------------------------------------------------
# Context
# ---------------------------------------------------------------------------
defmodule Context do
@moduledoc false
defstruct module_name: nil,
function_name: nil,
function_arity: nil,
loop_var: nil,
loop_arity: nil,
in_pattern: false,
records: %{},
gensym_counter: 0,
vector_as_list: false,
macros: %{},
in_macro: false,
schemas: %{}
end
# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------
@doc """
Transform a list of CljElixir AST forms into a single Elixir AST node.
Returns a __block__ wrapping all transformed forms (or a single form if only one).
"""
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}
end)
# Filter out nil (from defmacro which produces no runtime code)
elixir_forms = Enum.filter(elixir_forms, &(&1 != nil))
case elixir_forms do
[] -> nil
[single] -> single
multiple -> {:__block__, [], multiple}
end
end
def transform(form, ctx) do
{ast, _ctx} = transform_form(form, ctx)
ast
end
# ---------------------------------------------------------------------------
# Main dispatch
# ---------------------------------------------------------------------------
@doc false
def transform_form(form, ctx) do
do_transform(form, ctx)
end
# --- Literals pass through ---
defp do_transform(n, ctx) when is_integer(n), do: {n, ctx}
defp do_transform(n, ctx) when is_float(n), do: {n, ctx}
defp do_transform(s, ctx) when is_binary(s), do: {s, ctx}
defp do_transform(true, ctx), do: {true, ctx}
defp do_transform(false, ctx), do: {false, ctx}
defp do_transform(nil, ctx), do: {nil, ctx}
defp do_transform(a, ctx) when is_atom(a), do: {a, ctx}
# --- Symbols ---
defp do_transform({:symbol, meta, name}, ctx) when is_binary(name) do
transform_symbol(name, meta, ctx)
end
# --- Lists (function calls and special forms) ---
defp do_transform({:list, meta, elements}, ctx) do
transform_list(elements, meta, ctx)
end
# --- Vectors ---
defp do_transform({:vector, meta, elements}, ctx) do
transform_vector(elements, meta, ctx)
end
# --- Maps ---
defp do_transform({:map, _meta, pairs}, ctx) do
transform_map(pairs, ctx)
end
# --- Sets ---
defp do_transform({:set, _meta, elements}, ctx) do
transform_set(elements, ctx)
end
# --- Tuples ---
defp do_transform({:tuple, _meta, elements}, ctx) do
transformed = Enum.map(elements, fn e -> transform(e, ctx) end)
{{:{}, [], transformed}, ctx}
end
# --- Regex ---
defp do_transform({:regex, _meta, pattern}, ctx) do
ast =
{:sigil_r, [delimiter: "/"],
[{:<<>>, [], [pattern]}, []]}
{ast, ctx}
end
# --- Quote ---
defp do_transform({:quote, _meta, inner}, ctx) do
{quote_form(inner), ctx}
end
# --- Metadata ---
defp do_transform({:with_meta, _meta, {_metadata, target}}, ctx) do
# For now, ignore metadata and transform the target
do_transform(target, ctx)
end
# --- Anon fn shorthand #(...) ---
defp do_transform({:anon_fn, _meta, body}, ctx) do
transform_anon_fn(body, ctx)
end
# --- Quasiquote ---
defp do_transform({:quasiquote, _meta, form}, ctx) do
if ctx.in_macro do
# In macro body: produce code that constructs CljElixir AST
{gensyms, gensym_map} = collect_gensyms(form)
gensym_bindings = generate_gensym_bindings(gensyms)
qq_ast = transform_macro_quasiquote(form, ctx, gensym_map)
ast =
if gensym_bindings == [] do
qq_ast
else
{:__block__, [], gensym_bindings ++ [qq_ast]}
end
{ast, ctx}
else
# Normal mode: produce Elixir runtime data
{transform_quasiquote(form, ctx), ctx}
end
end
# --- Unquote ---
defp do_transform({:unquote, _meta, form}, ctx) do
do_transform(form, ctx)
end
# --- Splice-unquote ---
defp do_transform({:splice_unquote, _meta, form}, ctx) do
do_transform(form, ctx)
end
# --- Deref ---
defp do_transform({:deref, _meta, form}, ctx) do
{inner, ctx} = do_transform(form, ctx)
# deref: Agent.get(ref, fn s -> s end)
ast = {{:., [], [{:__aliases__, [alias: false], [:Agent]}, :get]}, [],
[inner, {:fn, [], [{:->, [], [[{:s, [], nil}], {:s, [], nil}]}]}]}
{ast, ctx}
end
# Catch-all for unknown forms
defp do_transform(other, ctx) do
{other, ctx}
end
# ---------------------------------------------------------------------------
# Symbol transformation
# ---------------------------------------------------------------------------
# Dynamic vars
defp transform_symbol("*self*", meta, ctx) do
{{:self, elixir_meta(meta), []}, ctx}
end
defp transform_symbol("*node*", meta, ctx) do
{{:node, elixir_meta(meta), []}, ctx}
end
# true/false/nil are also possible as symbols
defp transform_symbol("true", _meta, ctx), do: {true, ctx}
defp transform_symbol("false", _meta, ctx), do: {false, ctx}
defp transform_symbol("nil", _meta, ctx), do: {nil, ctx}
# Module-qualified symbol like Enum/map or io/format
defp transform_symbol(name, meta, ctx) when is_binary(name) do
cond do
String.contains?(name, "/") ->
transform_module_call_symbol(name, meta, ctx)
module_reference?(name) ->
# Bare module reference (e.g., CljElixir.SubVector as a value)
{parse_module_name(name), ctx}
true ->
# Plain variable
munged = munge_name(name)
atom_name = String.to_atom(munged)
{{atom_name, elixir_meta(meta), nil}, ctx}
end
end
# Detect bare module references: uppercase start with dots (e.g., CljElixir.SubVector)
defp module_reference?(name) do
first_char = String.first(name)
first_char == String.upcase(first_char) and
first_char != String.downcase(first_char) and
String.contains?(name, ".")
end
defp transform_module_call_symbol(name, meta, ctx) do
{mod_ast, fun_atom} = parse_module_function(name)
em = elixir_meta(meta)
# Bare qualified symbol → zero-arg call (e.g., Enum/count as value)
ast = {{:., em, [mod_ast, fun_atom]}, em, []}
{ast, ctx}
end
# ---------------------------------------------------------------------------
# List (call) transformation — the core dispatch
# ---------------------------------------------------------------------------
defp transform_list([], _meta, ctx) do
{[], ctx}
end
defp transform_list([head | args], meta, ctx) do
case head do
# --- Special forms (symbols) ---
{:symbol, _, "defmodule"} -> transform_defmodule(args, meta, ctx)
{:symbol, _, "defn"} -> transform_defn(args, meta, ctx, :def)
{:symbol, _, "defn-"} -> transform_defn(args, meta, ctx, :defp)
{:symbol, _, "fn"} -> transform_fn(args, meta, ctx)
{:symbol, _, "let"} -> transform_let(args, meta, ctx)
{:symbol, _, "if"} -> transform_if(args, meta, ctx)
{:symbol, _, "when"} -> transform_when(args, meta, ctx)
{:symbol, _, "cond"} -> transform_cond(args, meta, ctx)
{:symbol, _, "case"} -> transform_case(args, meta, ctx)
{:symbol, _, "do"} -> transform_do(args, meta, ctx)
{:symbol, _, "loop"} -> transform_loop(args, meta, ctx)
{:symbol, _, "recur"} -> transform_recur(args, meta, ctx)
{:symbol, _, "def"} -> transform_def(args, meta, ctx)
{:symbol, _, "defprotocol"} -> transform_defprotocol(args, meta, ctx)
{:symbol, _, "defrecord"} -> transform_defrecord(args, meta, ctx)
{:symbol, _, "extend-type"} -> transform_extend_type(args, meta, ctx)
{:symbol, _, "extend-protocol"} -> transform_extend_protocol(args, meta, ctx)
{:symbol, _, "reify"} -> transform_reify(args, meta, ctx)
{:symbol, _, "with"} -> transform_with(args, meta, ctx)
{:symbol, _, "receive"} -> transform_receive(args, meta, ctx)
{:symbol, _, "monitor"} -> transform_monitor(args, meta, ctx)
{:symbol, _, "link"} -> transform_link(args, meta, ctx)
{:symbol, _, "unlink"} -> transform_unlink(args, meta, ctx)
{:symbol, _, "alive?"} -> transform_alive?(args, meta, ctx)
{:symbol, _, "for"} -> transform_for(args, meta, ctx)
{:symbol, _, "doseq"} -> transform_doseq(args, meta, ctx)
{:symbol, _, "if-let"} -> transform_if_let(args, meta, ctx)
{:symbol, _, "when-let"} -> transform_when_let(args, meta, ctx)
{:symbol, _, "if-some"} -> transform_if_some(args, meta, ctx)
{:symbol, _, "when-some"} -> transform_when_some(args, meta, ctx)
{:symbol, _, "use"} -> transform_use(args, meta, ctx)
{:symbol, _, "require"} -> transform_require(args, meta, ctx)
{:symbol, _, "import"} -> transform_import(args, meta, ctx)
{:symbol, _, "alias"} -> transform_alias(args, meta, ctx)
{:symbol, _, "quote"} -> transform_quote_form(args, meta, ctx)
{:symbol, _, "defmacro"} -> transform_defmacro(args, meta, ctx)
# --- Phase 7: Malli schema annotations ---
{:symbol, _, "m/=>"} -> transform_schema_spec(args, meta, ctx)
# --- Operators and builtins ---
{:symbol, _, "+"} -> transform_arith(:+, args, ctx)
{:symbol, _, "-"} -> transform_arith(:-, args, ctx)
{:symbol, _, "*"} -> transform_arith(:*, args, ctx)
{:symbol, _, "/"} -> transform_arith(:/, args, ctx)
{:symbol, _, ">"} -> transform_comparison(:>, args, ctx)
{:symbol, _, "<"} -> transform_comparison(:<, args, ctx)
{:symbol, _, ">="} -> transform_comparison(:>=, args, ctx)
{:symbol, _, "<="} -> transform_comparison(:<=, args, ctx)
{:symbol, _, "="} -> transform_equality(args, ctx)
{:symbol, _, "=="} -> transform_numeric_equality(args, ctx)
{:symbol, _, "not="} -> transform_not_equal(args, ctx)
{:symbol, _, "!="} -> transform_not_equal(args, ctx)
{:symbol, _, "not"} -> transform_not(args, ctx)
{:symbol, _, "and"} -> transform_bool_op(:and, args, ctx)
{:symbol, _, "or"} -> transform_bool_op(:or, args, ctx)
{:symbol, _, "inc"} -> transform_inc(args, ctx)
{:symbol, _, "dec"} -> transform_dec(args, ctx)
{:symbol, _, "str"} -> transform_str(args, ctx)
{:symbol, _, "println"} -> transform_println(args, ctx)
{:symbol, _, "pr-str"} -> transform_pr_str(args, ctx)
{:symbol, _, "pr"} -> transform_pr(args, ctx)
{:symbol, _, "prn"} -> transform_prn(args, ctx)
{:symbol, _, "print-str"} -> transform_print_str(args, ctx)
{:symbol, _, "nil?"} -> transform_nil_check(args, ctx)
{:symbol, _, "throw"} -> transform_throw(args, ctx)
{:symbol, _, "count"} -> transform_count(args, ctx)
{:symbol, _, "hd"} -> transform_hd(args, ctx)
{:symbol, _, "tl"} -> transform_tl(args, ctx)
{:symbol, _, "cons"} -> transform_cons(args, ctx)
# --- Phase 2: Protocol-backed core functions ---
{:symbol, _, "get"} -> transform_get(args, ctx)
{:symbol, _, "assoc"} -> transform_assoc(args, ctx)
{:symbol, _, "dissoc"} -> transform_dissoc(args, ctx)
{:symbol, _, "update"} -> transform_update(args, ctx)
{:symbol, _, "conj"} -> transform_conj(args, ctx)
{:symbol, _, "contains?"} -> transform_contains(args, ctx)
{:symbol, _, "empty?"} -> transform_empty(args, ctx)
{:symbol, _, "nth"} -> transform_nth(args, ctx)
{:symbol, _, "first"} -> transform_first(args, ctx)
{:symbol, _, "rest"} -> transform_rest(args, ctx)
{:symbol, _, "seq"} -> transform_seq(args, ctx)
{:symbol, _, "reduce"} -> transform_reduce(args, ctx)
{:symbol, _, "reduce-kv"} -> transform_reduce_kv(args, ctx)
{:symbol, _, "map"} -> transform_map_fn(args, ctx)
{:symbol, _, "filter"} -> transform_filter(args, ctx)
{:symbol, _, "concat"} -> transform_concat(args, ctx)
{:symbol, _, "take"} -> transform_take(args, ctx)
{:symbol, _, "drop"} -> transform_drop(args, ctx)
{:symbol, _, "sort"} -> transform_sort(args, ctx)
{:symbol, _, "sort-by"} -> transform_sort_by(args, ctx)
{:symbol, _, "group-by"} -> transform_group_by(args, ctx)
{:symbol, _, "frequencies"} -> transform_frequencies(args, ctx)
{:symbol, _, "distinct"} -> transform_distinct(args, ctx)
{:symbol, _, "mapcat"} -> transform_mapcat(args, ctx)
{:symbol, _, "partition"} -> transform_partition(args, ctx)
{:symbol, _, "keys"} -> transform_keys(args, ctx)
{:symbol, _, "vals"} -> transform_vals(args, ctx)
{:symbol, _, "merge"} -> transform_merge(args, ctx)
{:symbol, _, "select-keys"} -> transform_select_keys(args, ctx)
{:symbol, _, "into"} -> transform_into(args, ctx)
{:symbol, _, "get-in"} -> transform_get_in(args, ctx)
{:symbol, _, "assoc-in"} -> transform_assoc_in(args, ctx)
{:symbol, _, "update-in"} -> transform_update_in(args, ctx)
{:symbol, _, "list"} -> transform_list_call(args, ctx)
# --- Phase 3: Vector builtins ---
{:symbol, _, "vec"} -> transform_vec(args, ctx)
{:symbol, _, "vector"} -> transform_vector_call(args, ctx)
{:symbol, _, "subvec"} -> transform_subvec_call(args, ctx)
{:symbol, _, "peek"} -> transform_peek(args, ctx)
{:symbol, _, "pop"} -> transform_pop(args, ctx)
{:symbol, _, "vector?"} -> transform_vector_check(args, ctx)
# --- Phase 4: Domain tools ---
{:symbol, _, "tuple"} -> transform_tuple_fn(args, ctx)
{:symbol, _, "clojurify"} -> transform_clojurify(args, ctx)
{:symbol, _, "elixirify"} -> transform_elixirify(args, ctx)
# --- Phase 6: Threading macros ---
{:symbol, _, "->"} -> transform_thread_first(args, meta, ctx)
{:symbol, _, "->>"} -> transform_thread_last(args, meta, ctx)
# --- Phase 6: Exception handling ---
{:symbol, _, "try"} -> transform_try(args, meta, ctx)
# --- Keyword-as-function: (:name user) ---
kw when is_atom(kw) ->
transform_keyword_call(kw, args, meta, ctx)
# --- Module/function calls from symbol with / ---
{:symbol, _, name} when is_binary(name) ->
if Map.has_key?(ctx.macros, name) do
expand_macro(name, args, ctx)
else
if String.contains?(name, "/") do
transform_module_call(name, args, meta, ctx)
else
# Check for ->Constructor and map->Constructor
cond do
String.starts_with?(name, "->") and not String.starts_with?(name, "->>") ->
transform_positional_constructor(name, args, meta, ctx)
String.starts_with?(name, "map->") ->
transform_map_constructor(name, args, meta, ctx)
true ->
transform_unqualified_call(name, args, meta, ctx)
end
end
end
# If head is itself a list (e.g., ((fn [x] x) 42)), transform and call
_ ->
{head_ast, ctx} = do_transform(head, ctx)
t_args = Enum.map(args, fn a -> transform(a, ctx) end)
{{:., [], [head_ast]} |> then(fn dot -> {dot, [], t_args} end), ctx}
end
end
# ---------------------------------------------------------------------------
# 1. defmodule
# ---------------------------------------------------------------------------
defp transform_defmodule(args, meta, ctx) do
{name_form, rest} = extract_name(args)
mod_alias = module_name_ast(name_form)
# Check for docstring
{moduledoc, body_forms} =
case rest do
[doc | body] when is_binary(doc) ->
{doc, body}
_ ->
{nil, rest}
end
new_ctx = %{ctx | module_name: mod_alias}
{body_asts, _final_ctx} =
Enum.map_reduce(body_forms, new_ctx, fn form, acc ->
{ast, new_acc} = transform_form(form, acc)
{ast, new_acc}
end)
# Filter out nil (from defmacro which produces no runtime code)
body_asts = Enum.filter(body_asts, &(&1 != nil))
doc_ast =
if moduledoc do
[{:@, [], [{:moduledoc, [], [moduledoc]}]}]
else
[]
end
inner = doc_ast ++ body_asts
block =
case inner do
[single] -> single
multiple -> {:__block__, [], multiple}
end
ast =
{:defmodule, [context: Elixir] ++ elixir_meta(meta),
[mod_alias, [do: block]]}
{ast, ctx}
end
# ---------------------------------------------------------------------------
# 2. defn / defn-
# ---------------------------------------------------------------------------
defp transform_defn(args, meta, ctx, kind) do
{name_form, rest} = extract_name(args)
fun_name = symbol_to_atom(name_form)
# Check for docstring
{doc, rest} =
case rest do
[d | r] when is_binary(d) -> {d, r}
_ -> {nil, rest}
end
# Determine: single-arity vs multi-clause/multi-arity
# Multi-clause: rest is a list of lists, each starting with a vector
clauses = parse_defn_clauses(rest)
def_kind = if kind == :def, do: :def, else: :defp
em = elixir_meta(meta)
doc_ast =
if doc do
[{:@, [], [{:doc, [], [doc]}]}]
else
[]
end
clause_asts =
Enum.map(clauses, fn {params_vec, rest_param, guard, body_forms} ->
# Arity includes the rest param slot if present
arity = length(params_vec) + if(rest_param, do: 1, else: 0)
fn_ctx = %{ctx | function_name: fun_name, function_arity: arity, in_pattern: false}
pattern_ctx = %{fn_ctx | in_pattern: true}
param_asts = Enum.map(params_vec, fn p -> transform(p, pattern_ctx) end)
# If there's a rest param, add it as a parameter with default \\ []
param_asts =
case rest_param do
nil ->
param_asts
rest_sym ->
rest_var_ast = transform(rest_sym, pattern_ctx)
rest_with_default = {:"\\\\", [], [rest_var_ast, []]}
param_asts ++ [rest_with_default]
end
body_ast = transform_body(body_forms, fn_ctx)
clause =
case guard do
nil ->
{def_kind, em, [call_with_args(fun_name, param_asts), [do: body_ast]]}
guard_form ->
guard_ast = transform(guard_form, fn_ctx)
{def_kind, em,
[
{:when, [],
[call_with_args(fun_name, param_asts), guard_ast]},
[do: body_ast]
]}
end
clause
end)
result = doc_ast ++ clause_asts
ast =
case result do
[single] -> single
multiple -> {:__block__, [], multiple}
end
{ast, ctx}
end
defp parse_defn_clauses(rest) do
case rest do
# Single-arity: (defn f [params] body...)
[{:vector, _, _} = params_vec | body] ->
params = vector_elements(params_vec)
{required, rest_param} = split_rest_params(params)
[{required, rest_param, nil, body}]
# Multi-clause: (defn f ([params1] body1) ([params2] body2) ...)
clauses when is_list(clauses) ->
Enum.map(clauses, fn
{:list, _, [{:vector, _, params} | body]} ->
{required, rest_param} = split_rest_params(params)
{required, rest_param, nil, body}
{:list, _, clause_elements} ->
# Might have guard: ([params] :when guard body)
parse_clause_with_guard(clause_elements)
end)
end
end
defp parse_clause_with_guard([{:vector, _, params}, :when, guard | body]) do
{required, rest_param} = split_rest_params(params)
{required, rest_param, guard, body}
end
defp parse_clause_with_guard([{:vector, _, params} | body]) do
{required, rest_param} = split_rest_params(params)
{required, rest_param, nil, body}
end
# Split a param list at `&` into {required_params, rest_param | nil}
# e.g. [a b & rest] → {[a, b], rest_symbol}
defp split_rest_params(params) do
case Enum.split_while(params, fn
{:symbol, _, "&"} -> false
_ -> true
end) do
{required, [{:symbol, _, "&"}, rest_param]} ->
{required, rest_param}
{required, [{:symbol, _, "&"}, rest_param | _extra]} ->
{required, rest_param}
{all, []} ->
{all, nil}
end
end
# ---------------------------------------------------------------------------
# 3. fn
# ---------------------------------------------------------------------------
defp transform_fn(args, meta, ctx) do
clauses = parse_fn_clauses(args)
em = elixir_meta(meta)
fn_clauses =
Enum.flat_map(clauses, fn {params, rest_param, guard, body_forms} ->
pattern_ctx = %{ctx | in_pattern: true}
param_asts = Enum.map(params, fn p -> transform(p, pattern_ctx) end)
body_ast = transform_body(body_forms, ctx)
# Build the param list, adding rest param as a regular parameter if present
all_param_asts =
case rest_param do
nil -> param_asts
rest_sym ->
rest_var_ast = transform(rest_sym, pattern_ctx)
param_asts ++ [rest_var_ast]
end
clause =
case guard do
nil ->
{:->, [], [all_param_asts, body_ast]}
guard_form ->
guard_ast = transform(guard_form, ctx)
guard_params = [{:when, [], all_param_asts ++ [guard_ast]}]
{:->, [], [guard_params, body_ast]}
end
[clause]
end)
{{:fn, em, fn_clauses}, ctx}
end
defp parse_fn_clauses(args) do
case args do
# Single-arity: (fn [params] body...)
[{:vector, _, params} | body] ->
{required, rest_param} = split_rest_params(params)
[{required, rest_param, nil, body}]
# Multi-arity: (fn ([p1] b1) ([p2 p3] b2) ...)
clauses ->
Enum.map(clauses, fn
{:list, _, [{:vector, _, params} | body]} ->
{required, rest_param} = split_rest_params(params)
{required, rest_param, nil, body}
{:list, _, clause_elements} ->
parse_clause_with_guard(clause_elements)
end)
end
end
# ---------------------------------------------------------------------------
# 4. #() anonymous shorthand
# ---------------------------------------------------------------------------
defp transform_anon_fn(body, ctx) do
# Walk body to find %1, %2, ... or % (= %1)
max_arg = find_max_anon_arg(body)
arity = max(max_arg, 1)
params =
Enum.map(1..arity, fn i ->
{String.to_atom("p#{i}"), [], nil}
end)
# Replace % / %1 / %2 etc in body with p1, p2 etc
transformed_body = replace_anon_args(body)
body_ast = transform(transformed_body, ctx)
fn_ast = {:fn, [], [{:->, [], [params, body_ast]}]}
{fn_ast, ctx}
end
defp find_max_anon_arg(form) do
case form do
{:symbol, _, "%"} -> 1
{:symbol, _, "%" <> rest} ->
case Integer.parse(rest) do
{n, ""} -> n
_ -> 0
end
{:list, _, elements} ->
elements |> Enum.map(&find_max_anon_arg/1) |> Enum.max(fn -> 0 end)
{:vector, _, elements} ->
elements |> Enum.map(&find_max_anon_arg/1) |> Enum.max(fn -> 0 end)
{:map, _, pairs} ->
pairs |> Enum.map(&find_max_anon_arg/1) |> Enum.max(fn -> 0 end)
{:set, _, elements} ->
elements |> Enum.map(&find_max_anon_arg/1) |> Enum.max(fn -> 0 end)
{:tuple, _, elements} ->
elements |> Enum.map(&find_max_anon_arg/1) |> Enum.max(fn -> 0 end)
_ -> 0
end
end
defp replace_anon_args(form) do
case form do
{:symbol, meta, "%"} ->
{:symbol, meta, "p1"}
{:symbol, meta, "%" <> rest} ->
case Integer.parse(rest) do
{_n, ""} -> {:symbol, meta, "p#{rest}"}
_ -> form
end
{:list, meta, elements} ->
{:list, meta, Enum.map(elements, &replace_anon_args/1)}
{:vector, meta, elements} ->
{:vector, meta, Enum.map(elements, &replace_anon_args/1)}
{:map, meta, pairs} ->
{:map, meta, Enum.map(pairs, &replace_anon_args/1)}
{:set, meta, elements} ->
{:set, meta, Enum.map(elements, &replace_anon_args/1)}
{:tuple, meta, elements} ->
{:tuple, meta, Enum.map(elements, &replace_anon_args/1)}
other -> other
end
end
# ---------------------------------------------------------------------------
# 5. let
# ---------------------------------------------------------------------------
defp transform_let([{:vector, _, bindings} | body], meta, ctx) do
binding_pairs = Enum.chunk_every(bindings, 2)
em = elixir_meta(meta)
{binding_asts, final_ctx} =
Enum.map_reduce(binding_pairs, ctx, fn [pattern, expr], acc ->
pattern_ctx = %{acc | in_pattern: true}
pat_ast = transform(pattern, pattern_ctx)
expr_ast = transform(expr, acc)
match_ast = {:=, em, [pat_ast, expr_ast]}
{match_ast, acc}
end)
body_ast = transform_body(body, final_ctx)
all = binding_asts ++ [body_ast]
ast =
case all do
[single] -> single
multiple -> {:__block__, em, multiple}
end
{ast, ctx}
end
# ---------------------------------------------------------------------------
# 6. if / when / cond / case / do
# ---------------------------------------------------------------------------
defp transform_if([test, then_form | else_form], meta, ctx) do
test_ast = transform(test, ctx)
then_ast = transform(then_form, ctx)
else_ast =
case else_form do
[e] -> transform(e, ctx)
[] -> nil
end
ast = {:if, elixir_meta(meta), [test_ast, [do: then_ast, else: else_ast]]}
{ast, ctx}
end
defp transform_when([test | body], meta, ctx) do
test_ast = transform(test, ctx)
body_ast = transform_body(body, ctx)
ast = {:if, elixir_meta(meta), [test_ast, [do: body_ast]]}
{ast, ctx}
end
defp transform_cond(pairs, meta, ctx) do
clauses =
pairs
|> Enum.chunk_every(2)
|> Enum.map(fn [condition, result] ->
cond_ast =
case condition do
:else -> true
_ -> transform(condition, ctx)
end
result_ast = transform(result, ctx)
{:->, [], [[cond_ast], result_ast]}
end)
ast = {:cond, elixir_meta(meta), [[do: clauses]]}
{ast, ctx}
end
defp transform_case([val | clauses], meta, ctx) do
val_ast = transform(val, ctx)
pattern_ctx = %{ctx | in_pattern: true}
case_clauses =
clauses
|> Enum.chunk_every(2)
|> Enum.map(fn
[pattern, :when | rest] ->
# pattern :when 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)
{:->, [], [[pat_ast], body_ast]}
[pattern, body] ->
pat_ast = transform(pattern, pattern_ctx)
body_ast = transform(body, ctx)
{:->, [], [[pat_ast], body_ast]}
end)
ast = {:case, elixir_meta(meta), [val_ast, [do: case_clauses]]}
{ast, ctx}
end
defp transform_do(exprs, _meta, ctx) do
# transform_body handles its own block wrapping; meta not needed here
body_ast = transform_body(exprs, ctx)
{body_ast, ctx}
end
# ---------------------------------------------------------------------------
# 7. loop / recur
# ---------------------------------------------------------------------------
defp transform_loop([{:vector, _, bindings} | body], meta, ctx) do
binding_pairs = Enum.chunk_every(bindings, 2)
arity = length(binding_pairs)
em = elixir_meta(meta)
loop_var = unique_var(:loop_fn, ctx)
loop_ctx = %{ctx | loop_var: loop_var, loop_arity: arity}
{param_names, init_vals} =
Enum.map(binding_pairs, fn [name, val] ->
pattern_ctx = %{loop_ctx | in_pattern: true}
{transform(name, pattern_ctx), transform(val, ctx)}
end)
|> Enum.unzip()
body_ast = transform_body(body, loop_ctx)
# Pattern: loop_fn = fn loop_fn, bindings... -> body end; loop_fn.(loop_fn, init_vals...)
fn_params = [loop_var | param_names]
fn_ast = {:fn, em, [{:->, [], [fn_params, body_ast]}]}
assign = {:=, em, [loop_var, fn_ast]}
invoke = {{:., em, [loop_var]}, em, [loop_var | init_vals]}
ast = {:__block__, em, [assign, invoke]}
{ast, ctx}
end
defp transform_recur(args, _meta_unused, ctx) do
t_args = Enum.map(args, fn a -> transform(a, ctx) end)
ast =
if ctx.loop_var do
# recur in loop: call the loop fn with itself + new args
{{:., [], [ctx.loop_var]}, [], [ctx.loop_var | t_args]}
else
# recur in defn: call the current function recursively
if ctx.function_name do
{ctx.function_name, [], t_args}
else
raise "recur outside of loop or defn"
end
end
{ast, ctx}
end
# ---------------------------------------------------------------------------
# 8. def (top-level binding)
# ---------------------------------------------------------------------------
defp transform_def([name_form, value], meta, ctx) do
fun_name = symbol_to_atom(name_form)
em = elixir_meta(meta)
# Check if value looks like a schema definition
case detect_schema(value) do
nil ->
val_ast = transform(value, ctx)
def_ast = {:def, em, [{fun_name, [], []}, [do: val_ast]]}
{def_ast, ctx}
schema_data ->
# For schema defs, use the plain data as the runtime value
# (avoids trying to transform schema references like PositiveInt as variables)
val_ast = Macro.escape(schema_data)
def_ast = {:def, em, [{fun_name, [], []}, [do: val_ast]]}
# Generate type name: "User" -> :user, "PositiveInt" -> :positive_int
schema_name = symbol_name(name_form)
type_name = schema_name_to_type_atom(schema_name)
# Store in context for cross-references
new_schemas = Map.put(ctx.schemas, schema_name, type_name)
new_ctx = %{ctx | schemas: new_schemas}
# Generate @type AST
opts = [known_types: new_schemas]
type_asts = case schema_data do
[:schema, %{registry: _} | _] ->
CljElixir.Malli.type_ast(type_name, schema_data, opts)
_ ->
[CljElixir.Malli.type_ast(type_name, schema_data, opts)]
end
# Return block: @type(s) + def
all = type_asts ++ [def_ast]
ast = {:__block__, [], all}
{ast, new_ctx}
end
end
defp transform_def([name_form], meta, ctx) do
fun_name = symbol_to_atom(name_form)
em = elixir_meta(meta)
ast = {:def, em, [{fun_name, [], []}, [do: nil]]}
{ast, ctx}
end
# ---------------------------------------------------------------------------
# 9. Module/function calls (FFI)
# ---------------------------------------------------------------------------
defp transform_module_call(name, args, meta, ctx) do
{mod_ast, fun_atom} = parse_module_function(name)
t_args = Enum.map(args, fn a -> transform(a, ctx) end)
em = elixir_meta(meta)
ast = {{:., em, [mod_ast, fun_atom]}, em, t_args}
{ast, ctx}
end
defp parse_module_function(name) do
# Split on first / only (module part may contain dots)
{mod_str, fun_str} =
case String.split(name, "/", parts: 2) do
[m, f] -> {m, f}
[m] -> {m, nil}
end
fun_atom = if fun_str, do: String.to_atom(munge_ffi_name(fun_str)), else: nil
mod_ast = parse_module_name(mod_str)
{mod_ast, fun_atom}
end
defp parse_module_name(mod_str) do
first_char = String.first(mod_str)
if first_char == String.upcase(first_char) and first_char != String.downcase(first_char) do
# Uppercase = Elixir module
# Could be dotted like MyApp.Greeter
parts = String.split(mod_str, ".")
atoms = Enum.map(parts, &String.to_atom/1)
{:__aliases__, [alias: false], atoms}
else
# Lowercase = Erlang module
String.to_atom(mod_str)
end
end
# ---------------------------------------------------------------------------
# 10. Unqualified function calls
# ---------------------------------------------------------------------------
defp transform_unqualified_call(name, args, meta, ctx) do
munged = munge_name(name)
fun_atom = String.to_atom(munged)
t_args = Enum.map(args, fn a -> transform(a, ctx) end)
ast = {fun_atom, elixir_meta(meta), t_args}
{ast, ctx}
end
# ---------------------------------------------------------------------------
# 11. Data literals
# ---------------------------------------------------------------------------
defp transform_vector(elements, _meta, ctx) do
if ctx.in_pattern do
pattern_ctx = %{ctx | in_pattern: true}
if has_ampersand?(elements) do
# Sequential destructuring: [a b & rest] → list cons pattern [a, b | rest]
transform_sequential_destructuring(elements, pattern_ctx, ctx)
else
# Normal pattern position: vectors become tuple matches
transformed = Enum.map(elements, fn e -> transform(e, pattern_ctx) end)
{{:{}, [], transformed}, ctx}
end
else
transformed = Enum.map(elements, fn e -> transform(e, ctx) end)
if ctx.vector_as_list do
# Runtime .clje files: vectors stay as Elixir lists (bootstrap mode)
{transformed, ctx}
else
# User code: vectors become PersistentVector
pv_mod = {:__aliases__, [alias: false], [:CljElixir, :PersistentVector]}
ast = {{:., [], [pv_mod, :from_list]}, [], [transformed]}
{ast, ctx}
end
end
end
# Check if elements contain an ampersand symbol (for sequential destructuring)
defp has_ampersand?(elements) do
Enum.any?(elements, fn
{:symbol, _, "&"} -> true
_ -> false
end)
end
# Transform sequential destructuring: [a b & rest] → [a, b | rest]
# Also supports :as: [a b & rest :as all]
defp transform_sequential_destructuring(elements, pattern_ctx, ctx) do
{before_amp, after_amp} =
Enum.split_while(elements, fn
{:symbol, _, "&"} -> false
_ -> true
end)
# after_amp starts with & symbol
[_amp | rest_and_maybe_as] = after_amp
{rest_sym, as_binding} =
case rest_and_maybe_as do
[rest_s, :as, as_s] -> {rest_s, as_s}
[rest_s] -> {rest_s, nil}
end
# Transform required elements
required = Enum.map(before_amp, fn e -> transform(e, pattern_ctx) end)
# Transform rest binding
rest_var = transform(rest_sym, pattern_ctx)
# Build [a, b | rest] as Elixir AST.
# Elixir represents [a, b | rest] as: [a, {:|, [], [b, rest]}]
# i.e., a proper list where the last element is a {:|, [], [head, tail]} tuple.
list_pattern = build_cons_list_ast(required, rest_var)
# Handle :as if present
case as_binding do
nil ->
{list_pattern, ctx}
as_sym ->
as_var = transform(as_sym, pattern_ctx)
{{:=, [], [list_pattern, as_var]}, ctx}
end
end
# Build [a, b | rest] in Elixir AST format.
# [a, b | rest] is represented as [a, {:|, [], [b, rest]}]
# [a | rest] is represented as [{:|, [], [a, rest]}]
# [| rest] with no heads is just rest itself
defp build_cons_list_ast([], rest_var) do
# No required elements, just the rest — but this shouldn't normally happen
# with & destructuring. If it does, treat as the rest variable itself.
rest_var
end
defp build_cons_list_ast(required, rest_var) do
{heads, [last]} = Enum.split(required, -1)
heads ++ [{:|, [], [last, rest_var]}]
end
defp transform_map(pairs, ctx) do
if ctx.in_pattern do
transform_destructuring_map(pairs, ctx)
else
kv_pairs =
pairs
|> Enum.chunk_every(2)
|> Enum.map(fn [k, v] ->
k_ast = transform(k, ctx)
v_ast = transform(v, ctx)
{k_ast, v_ast}
end)
ast = {:%{}, [], kv_pairs}
{ast, ctx}
end
end
# Transform a map in pattern position as Clojure-style destructuring.
# pairs is the flat list [k1, v1, k2, v2, ...] from {:map, meta, pairs}
#
# Clojure destructuring conventions:
# - :keys [a b] → atom keys :a, :b with bindings a, b
# - :strs [a b] → string keys "a", "b" with bindings a, b
# - :as name → also bind the whole map to name
# - {binding key ...} → literal pairs: first is binding form, second is lookup key
defp transform_destructuring_map(pairs, ctx) do
kv_chunks = Enum.chunk_every(pairs, 2)
{keys_bindings, strs_bindings, as_binding, literal_pairs} =
parse_destructuring_directives(kv_chunks)
pattern_pairs = []
# :keys -> atom keys
pattern_pairs =
pattern_pairs ++
Enum.map(keys_bindings, fn name ->
# Key atom preserves hyphens (per spec), variable is munged
atom_key = String.to_atom(name)
var_name = String.to_atom(munge_name(name))
var = {var_name, [], nil}
{atom_key, var}
end)
# :strs -> string keys
pattern_pairs =
pattern_pairs ++
Enum.map(strs_bindings, fn name ->
var_name = String.to_atom(munge_name(name))
var = {var_name, [], nil}
{name, var}
end)
# Literal pairs: Clojure convention is {binding key}, so first=binding, second=key
pattern_pairs =
pattern_pairs ++
Enum.map(literal_pairs, fn [binding_form, key_form] ->
# key_form is the map key (atom, string, etc.) - transform without pattern context
k_ast = transform(key_form, %{ctx | in_pattern: false})
# binding_form is the binding pattern (symbol or nested destructuring)
v_ast = transform(binding_form, ctx)
{k_ast, v_ast}
end)
map_ast = {:%{}, [], pattern_pairs}
# Wrap with :as if present
case as_binding do
nil ->
{map_ast, ctx}
as_name ->
as_var = {String.to_atom(munge_name(as_name)), [], nil}
{{:=, [], [map_ast, as_var]}, ctx}
end
end
# Parse destructuring directives from chunked kv pairs.
# Returns {keys_names, strs_names, as_name | nil, literal_pairs}
defp parse_destructuring_directives(kv_chunks) do
Enum.reduce(kv_chunks, {[], [], nil, []}, fn
[:keys, {:vector, _, symbols}], {keys, strs, as_b, lits} ->
names = Enum.map(symbols, fn {:symbol, _, n} -> n end)
{keys ++ names, strs, as_b, lits}
[:strs, {:vector, _, symbols}], {keys, strs, as_b, lits} ->
names = Enum.map(symbols, fn {:symbol, _, n} -> n end)
{keys, strs ++ names, as_b, lits}
[:as, {:symbol, _, name}], {keys, strs, _as_b, lits} ->
{keys, strs, name, lits}
other, {keys, strs, as_b, lits} ->
{keys, strs, as_b, lits ++ [other]}
end)
end
defp transform_set(elements, ctx) do
transformed = Enum.map(elements, fn e -> transform(e, ctx) end)
ast = {{:., [], [{:__aliases__, [alias: false], [:MapSet]}, :new]}, [], [transformed]}
{ast, ctx}
end
# ---------------------------------------------------------------------------
# 12. Keyword-as-function
# ---------------------------------------------------------------------------
defp transform_keyword_call(kw, args, _meta, ctx) do
case args do
[map_form] ->
map_ast = transform(map_form, ctx)
ast = protocol_call([:CljElixir, :ILookup], :lookup, [map_ast, kw])
{ast, ctx}
[map_form, default] ->
map_ast = transform(map_form, ctx)
default_ast = transform(default, ctx)
ast = protocol_call([:CljElixir, :ILookup], :lookup, [map_ast, kw, default_ast])
{ast, ctx}
_ ->
# Just return the keyword itself if no args (shouldn't happen)
{kw, ctx}
end
end
# ---------------------------------------------------------------------------
# 13. defprotocol
# ---------------------------------------------------------------------------
defp transform_defprotocol(args, meta, ctx) do
{name_form, rest} = extract_name(args)
proto_alias = module_name_ast(name_form)
# Optional docstring
{doc, fn_sigs} =
case rest do
[d | sigs] when is_binary(d) -> {d, sigs}
_ -> {nil, rest}
end
doc_ast =
if doc do
[{:@, [], [{:moduledoc, [], [doc]}]}]
else
[]
end
sig_asts =
Enum.map(fn_sigs, fn
{:list, _, [{:symbol, _, fname} | sig_args]} ->
fun_atom = String.to_atom(munge_name(strip_leading_dash(fname)))
# Each sig_arg is a vector with the params pattern
param_lists =
Enum.map(sig_args, fn
{:vector, _, params} -> params
# Might also have a docstring after the vector
_other -> nil
end)
|> Enum.filter(&is_list/1)
# Generate def heads for each arity
Enum.map(param_lists, fn params ->
param_asts = Enum.map(params, fn p -> transform(p, ctx) end)
{:def, [], [{fun_atom, [], param_asts}]}
end)
_ -> []
end)
|> List.flatten()
inner = doc_ast ++ sig_asts
block =
case inner do
[single] -> single
multiple -> {:__block__, [], multiple}
end
ast = {:defprotocol, [context: Elixir] ++ elixir_meta(meta), [proto_alias, [do: block]]}
{ast, ctx}
end
# ---------------------------------------------------------------------------
# 14. defrecord
# ---------------------------------------------------------------------------
defp transform_defrecord(args, meta, ctx) do
{name_form, rest} = extract_name(args)
record_alias = module_name_ast(name_form)
record_name = symbol_name(name_form)
# Optional docstring
{doc, rest} =
case rest do
[d | r] when is_binary(d) -> {d, r}
_ -> {nil, rest}
end
# Fields vector
{fields, proto_impls} =
case rest do
[{:vector, _, field_list} | impl_rest] ->
field_names = Enum.map(field_list, fn
{:symbol, _, n} -> String.to_atom(munge_name(n))
a when is_atom(a) -> a
end)
{field_names, impl_rest}
_ ->
{[], rest}
end
# Track record in context
new_ctx = %{ctx | records: Map.put(ctx.records, record_name, fields)}
doc_ast =
if doc do
[{:@, [], [{:moduledoc, [], [doc]}]}]
else
[]
end
# defstruct
struct_ast = {:defstruct, [], [fields]}
# Positional constructor: new/N
field_vars = Enum.map(fields, fn f -> {f, [], nil} end)
map_pairs = Enum.zip(fields, field_vars) |> Enum.map(fn {k, v} -> {k, v} end)
new_fn_ast =
{:def, [],
[{:new, [], field_vars},
[do: {:%, [], [{:__MODULE__, [], Elixir}, {:%{}, [], map_pairs}]}]]}
# Separate defn/defn-/def forms from protocol implementations
{fn_forms, proto_forms} =
Enum.split_with(proto_impls, fn
{:list, _, [{:symbol, _, "defn"} | _]} -> true
{:list, _, [{:symbol, _, "defn-"} | _]} -> true
{:list, _, [{:symbol, _, "def"} | _]} -> true
_ -> false
end)
# Transform function definitions inside the record module
fn_asts =
Enum.map(fn_forms, fn form ->
{ast, _ctx} = do_transform(form, new_ctx)
ast
end)
# Protocol implementations
impl_asts = transform_inline_impls(proto_forms, record_alias, new_ctx)
inner = doc_ast ++ [struct_ast, new_fn_ast] ++ fn_asts ++ impl_asts
block =
case inner do
[single] -> single
multiple -> {:__block__, [], multiple}
end
ast = {:defmodule, [context: Elixir] ++ elixir_meta(meta), [record_alias, [do: block]]}
{ast, new_ctx}
end
defp transform_inline_impls(forms, _for_type, ctx) do
# Parse: ProtocolName (fn-name [params] body)... ProtocolName ...
# Use __MODULE__ so the impl resolves correctly even when the record is nested
parse_protocol_groups(forms)
|> Enum.flat_map(fn {proto_name, fns} ->
proto_alias = module_name_ast(proto_name)
fn_asts = transform_protocol_fns(fns, ctx)
block =
case fn_asts do
[single] -> single
multiple -> {:__block__, [], multiple}
end
[{:defimpl, [context: Elixir], [proto_alias, [for: {:__MODULE__, [], Elixir}], [do: block]]}]
end)
end
# ---------------------------------------------------------------------------
# 15. extend-type / extend-protocol
# ---------------------------------------------------------------------------
defp transform_extend_type([type_form | rest], _meta_unused, ctx) do
type_alias = resolve_type_name(type_form)
groups = parse_protocol_groups(rest)
impls =
Enum.map(groups, fn {proto_name, fns} ->
proto_alias = module_name_ast(proto_name)
fn_asts = transform_protocol_fns(fns, ctx)
block =
case fn_asts do
[single] -> single
multiple -> {:__block__, [], multiple}
end
{:defimpl, [context: Elixir], [proto_alias, [for: type_alias], [do: block]]}
end)
ast =
case impls do
[single] -> single
multiple -> {:__block__, [], multiple}
end
{ast, ctx}
end
defp transform_extend_protocol([proto_form | rest], _meta_unused, ctx) do
proto_alias = resolve_type_name(proto_form)
# Parse: TypeName (fn ...) TypeName (fn ...)
groups = parse_type_groups(rest)
impls =
Enum.map(groups, fn {type_name, fns} ->
type_alias = resolve_type_name(type_name)
fn_asts = transform_protocol_fns(fns, ctx)
block =
case fn_asts do
[single] -> single
multiple -> {:__block__, [], multiple}
end
{:defimpl, [context: Elixir], [proto_alias, [for: type_alias], [do: block]]}
end)
ast =
case impls do
[single] -> single
multiple -> {:__block__, [], multiple}
end
{ast, ctx}
end
# ---------------------------------------------------------------------------
# 16. reify
# ---------------------------------------------------------------------------
defp transform_reify(args, _meta_unused, ctx) do
{counter, new_ctx} = bump_gensym(ctx)
mod_name = String.to_atom("CljElixir.Reify_#{counter}")
mod_alias = {:__aliases__, [alias: false], [mod_name]}
groups = parse_protocol_groups(args)
impl_asts =
Enum.map(groups, fn {proto_name, fns} ->
proto_alias = module_name_ast(proto_name)
fn_asts = transform_protocol_fns(fns, new_ctx)
block =
case fn_asts do
[single] -> single
multiple -> {:__block__, [], multiple}
end
{:defimpl, [context: Elixir], [proto_alias, [for: mod_alias], [do: block]]}
end)
struct_ast = {:defstruct, [], [[]]}
inner =
case [struct_ast | impl_asts] do
[single] -> single
multiple -> {:__block__, [], multiple}
end
defmod = {:defmodule, [context: Elixir], [mod_alias, [do: inner]]}
instance = {:%, [], [mod_alias, {:%{}, [], []}]}
ast = {:__block__, [], [defmod, instance]}
{ast, new_ctx}
end
# ---------------------------------------------------------------------------
# 17. with
# ---------------------------------------------------------------------------
defp transform_with([{:vector, _, bindings} | body_and_else], meta, ctx) do
binding_pairs = Enum.chunk_every(bindings, 2)
pattern_ctx = %{ctx | in_pattern: true}
with_clauses =
Enum.map(binding_pairs, fn [pattern, expr] ->
pat_ast = transform(pattern, pattern_ctx)
expr_ast = transform(expr, ctx)
{:<-, [], [pat_ast, expr_ast]}
end)
# Split body from :else
{body_forms, else_clauses} = split_with_else(body_and_else)
body_ast = transform_body(body_forms, ctx)
opts =
if else_clauses == [] do
[do: body_ast]
else
else_pairs =
else_clauses
|> Enum.chunk_every(2)
|> Enum.map(fn [pattern, result] ->
pat_ast = transform(pattern, pattern_ctx)
res_ast = transform(result, ctx)
{:->, [], [[pat_ast], res_ast]}
end)
[do: body_ast, else: else_pairs]
end
ast = {:with, elixir_meta(meta), with_clauses ++ [opts]}
{ast, ctx}
end
defp split_with_else(forms) do
case Enum.split_while(forms, fn f -> f != :else end) do
{body, [:else | else_clauses]} -> {body, else_clauses}
{body, []} -> {body, []}
end
end
# ---------------------------------------------------------------------------
# 18. receive
# ---------------------------------------------------------------------------
defp transform_receive(clauses, meta, ctx) do
pattern_ctx = %{ctx | in_pattern: true}
{case_clauses, after_clause} = parse_receive_clauses(clauses)
transformed_clauses =
Enum.map(case_clauses, fn
{pattern, guard, body} ->
pat_ast = transform(pattern, pattern_ctx)
body_ast = transform(body, ctx)
case guard do
nil ->
{:->, [], [[pat_ast], body_ast]}
guard_form ->
guard_ast = transform(guard_form, ctx)
{:->, [], [[{:when, [], [pat_ast, guard_ast]}], body_ast]}
end
end)
opts = [do: transformed_clauses]
opts =
case after_clause do
nil ->
opts
{timeout, body} ->
timeout_ast = transform(timeout, ctx)
body_ast = transform(body, ctx)
opts ++ [after: [{:->, [], [[timeout_ast], body_ast]}]]
end
ast = {:receive, elixir_meta(meta), [opts]}
{ast, ctx}
end
defp parse_receive_clauses(forms) do
parse_receive_clauses(forms, [], nil)
end
defp parse_receive_clauses([], acc, after_clause) do
{Enum.reverse(acc), after_clause}
end
defp parse_receive_clauses([:after, timeout, body | rest], acc, _after) 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)
end
defp parse_receive_clauses([pattern, body | rest], acc, after_clause) do
parse_receive_clauses(rest, [{pattern, nil, body} | acc], after_clause)
end
# ---------------------------------------------------------------------------
# 18b. Process primitives: monitor, link, unlink, alive?
# ---------------------------------------------------------------------------
defp process_mod_ast do
{:__aliases__, [alias: false], [:Process]}
end
# (monitor pid) → Process.monitor(pid)
# (monitor :process pid) → :erlang.monitor(:process, pid)
defp transform_monitor([pid], _meta, ctx) do
pid_ast = transform(pid, ctx)
ast = {{:., [], [process_mod_ast(), :monitor]}, [], [pid_ast]}
{ast, ctx}
end
defp transform_monitor([type, pid], _meta, ctx) do
type_ast = transform(type, ctx)
pid_ast = transform(pid, ctx)
ast = {{:., [], [:erlang, :monitor]}, [], [type_ast, pid_ast]}
{ast, ctx}
end
# (link pid) → Process.link(pid)
defp transform_link([pid], _meta, ctx) do
pid_ast = transform(pid, ctx)
ast = {{:., [], [process_mod_ast(), :link]}, [], [pid_ast]}
{ast, ctx}
end
# (unlink pid) → Process.unlink(pid)
defp transform_unlink([pid], _meta, ctx) do
pid_ast = transform(pid, ctx)
ast = {{:., [], [process_mod_ast(), :unlink]}, [], [pid_ast]}
{ast, ctx}
end
# (alive? pid) → Process.alive?(pid)
defp transform_alive?([pid], _meta, ctx) do
pid_ast = transform(pid, ctx)
ast = {{:., [], [process_mod_ast(), :alive?]}, [], [pid_ast]}
{ast, ctx}
end
# ---------------------------------------------------------------------------
# 19. for / doseq
# ---------------------------------------------------------------------------
defp transform_for([{:vector, _, bindings} | body], meta, ctx) do
{generators, filters} = parse_comprehension_bindings(bindings, ctx)
body_ast = transform_body(body, ctx)
args = generators ++ filters ++ [[do: body_ast]]
ast = {:for, elixir_meta(meta), args}
{ast, ctx}
end
defp transform_doseq([{:vector, _, bindings} | body], meta, ctx) do
{generators, filters} = parse_comprehension_bindings(bindings, ctx)
body_ast = transform_body(body, ctx)
args = generators ++ filters ++ [[do: body_ast]]
ast = {:for, elixir_meta(meta), args}
{ast, ctx}
end
defp parse_comprehension_bindings(bindings, ctx) do
parse_comp_bindings(bindings, ctx, [], [])
end
defp parse_comp_bindings([], _ctx, gens, filters) do
{Enum.reverse(gens), Enum.reverse(filters)}
end
defp parse_comp_bindings([:when, pred | rest], ctx, gens, filters) do
pred_ast = transform(pred, ctx)
parse_comp_bindings(rest, ctx, gens, [pred_ast | filters])
end
defp parse_comp_bindings([:let, {:vector, _, let_bindings} | rest], ctx, gens, filters) do
# :let bindings in for comprehension
pairs = Enum.chunk_every(let_bindings, 2)
let_asts = Enum.map(pairs, fn [p, e] ->
pattern_ctx = %{ctx | in_pattern: true}
{transform(p, pattern_ctx), transform(e, ctx)}
end)
parse_comp_bindings(rest, ctx, gens, filters ++ Enum.map(let_asts, fn {p, e} -> {:=, [], [p, e]} end))
end
defp parse_comp_bindings([pattern, coll | rest], ctx, gens, filters) do
pattern_ctx = %{ctx | in_pattern: true}
pat_ast = transform(pattern, pattern_ctx)
coll_ast = transform(coll, ctx)
gen = {:<-, [], [pat_ast, coll_ast]}
parse_comp_bindings(rest, ctx, [gen | gens], filters)
end
# ---------------------------------------------------------------------------
# 20. if-let / when-let / if-some / when-some
# ---------------------------------------------------------------------------
defp transform_if_let([{:vector, _, [pattern, expr]} | body], _meta, ctx) do
pattern_ctx = %{ctx | in_pattern: true}
pat_ast = transform(pattern, pattern_ctx)
expr_ast = transform(expr, ctx)
{then_form, else_form} =
case body do
[t, e] -> {t, e}
[t] -> {t, nil}
end
then_ast = transform(then_form, ctx)
else_ast = if else_form, do: transform(else_form, ctx), else: nil
# Use a temporary variable to check truthiness (not nil and not false)
temp_var = {:clje_if_let_val, [], nil}
ast =
{:__block__, [],
[
{:=, [], [temp_var, expr_ast]},
{:if, [],
[
{:and, [],
[{:not, [], [{:is_nil, [], [temp_var]}]},
{:!=, [], [temp_var, false]}]},
[
do: {:__block__, [], [{:=, [], [pat_ast, temp_var]}, then_ast]},
else: else_ast
]
]}
]}
{ast, ctx}
end
defp transform_when_let([{:vector, _, [pattern, expr]} | body], _meta, ctx) do
pattern_ctx = %{ctx | in_pattern: true}
pat_ast = transform(pattern, pattern_ctx)
expr_ast = transform(expr, ctx)
body_ast = transform_body(body, ctx)
temp_var = {:clje_when_let_val, [], nil}
ast =
{:__block__, [],
[
{:=, [], [temp_var, expr_ast]},
{:if, [],
[
{:and, [],
[{:not, [], [{:is_nil, [], [temp_var]}]},
{:!=, [], [temp_var, false]}]},
[do: {:__block__, [], [{:=, [], [pat_ast, temp_var]}, body_ast]}]
]}
]}
{ast, ctx}
end
defp transform_if_some([{:vector, _, [pattern, expr]} | body], _meta, ctx) do
pattern_ctx = %{ctx | in_pattern: true}
pat_ast = transform(pattern, pattern_ctx)
expr_ast = transform(expr, ctx)
{then_form, else_form} =
case body do
[t, e] -> {t, e}
[t] -> {t, nil}
end
then_ast = transform(then_form, ctx)
else_ast = if else_form, do: transform(else_form, ctx), else: nil
temp_var = {:clje_if_some_val, [], nil}
ast =
{:__block__, [],
[
{:=, [], [temp_var, expr_ast]},
{:if, [],
[
{:not, [], [{:is_nil, [], [temp_var]}]},
[
do: {:__block__, [], [{:=, [], [pat_ast, temp_var]}, then_ast]},
else: else_ast
]
]}
]}
{ast, ctx}
end
defp transform_when_some([{:vector, _, [pattern, expr]} | body], _meta, ctx) do
pattern_ctx = %{ctx | in_pattern: true}
pat_ast = transform(pattern, pattern_ctx)
expr_ast = transform(expr, ctx)
body_ast = transform_body(body, ctx)
temp_var = {:clje_when_some_val, [], nil}
ast =
{:__block__, [],
[
{:=, [], [temp_var, expr_ast]},
{:if, [],
[
{:not, [], [{:is_nil, [], [temp_var]}]},
[do: {:__block__, [], [{:=, [], [pat_ast, temp_var]}, body_ast]}]
]}
]}
{ast, ctx}
end
# ---------------------------------------------------------------------------
# 21. use / require / import / alias
# ---------------------------------------------------------------------------
defp transform_use(args, _meta, ctx) do
{mod_asts, opts} = parse_directive_args(args, ctx)
ast =
case opts do
[] -> {:use, [], mod_asts}
_ -> {:use, [], mod_asts ++ [opts]}
end
{ast, ctx}
end
defp transform_require(args, _meta, ctx) do
{mod_asts, opts} = parse_directive_args(args, ctx)
ast =
case opts do
[] -> {:require, [], mod_asts}
_ -> {:require, [], mod_asts ++ [opts]}
end
{ast, ctx}
end
defp transform_import(args, _meta, ctx) do
{mod_asts, opts} = parse_directive_args(args, ctx)
ast =
case opts do
[] -> {:import, [], mod_asts}
_ -> {:import, [], mod_asts ++ [opts]}
end
{ast, ctx}
end
defp transform_alias(args, _meta, ctx) do
{mod_asts, opts} = parse_directive_args(args, ctx)
ast =
case opts do
[] -> {:alias, [], mod_asts}
_ -> {:alias, [], mod_asts ++ [opts]}
end
{ast, ctx}
end
defp parse_directive_args(args, ctx) do
# First arg is a module name, rest might be options
case args do
[name_form | rest] ->
mod_ast = resolve_type_name(name_form)
opts = Enum.map(rest, fn o -> transform(o, ctx) end)
{[mod_ast], opts}
end
end
# ---------------------------------------------------------------------------
# 22. Operators and builtins
# ---------------------------------------------------------------------------
# Arithmetic: variadic
defp transform_arith(op, args, ctx) do
t_args = Enum.map(args, fn a -> transform(a, ctx) end)
ast =
case t_args do
[single] when op == :- ->
# Unary minus
{:-, [], [0, single]}
[single] ->
single
_ ->
Enum.reduce(tl(t_args), hd(t_args), fn arg, acc ->
{op, [], [acc, arg]}
end)
end
{ast, ctx}
end
# Comparisons
defp transform_comparison(op, [a, b], ctx) do
a_ast = transform(a, ctx)
b_ast = transform(b, ctx)
{{op, [], [a_ast, b_ast]}, ctx}
end
# Equality: = maps to CljElixir.Equality.equiv for cross-type equality
defp transform_equality([a, b], ctx) do
a_ast = transform(a, ctx)
b_ast = transform(b, ctx)
equiv_mod = {:__aliases__, [alias: false], [:CljElixir, :Equality]}
ast = {{:., [], [equiv_mod, :equiv]}, [], [a_ast, b_ast]}
{ast, ctx}
end
# Numeric equality: == maps to ==
defp transform_numeric_equality([a, b], ctx) do
a_ast = transform(a, ctx)
b_ast = transform(b, ctx)
{{:==, [], [a_ast, b_ast]}, ctx}
end
# not=, !=
defp transform_not_equal([a, b], ctx) do
a_ast = transform(a, ctx)
b_ast = transform(b, ctx)
equiv_mod = {:__aliases__, [alias: false], [:CljElixir, :Equality]}
equiv_call = {{:., [], [equiv_mod, :equiv]}, [], [a_ast, b_ast]}
{{:not, [], [equiv_call]}, ctx}
end
# not
defp transform_not([a], ctx) do
a_ast = transform(a, ctx)
{{:not, [], [a_ast]}, ctx}
end
# and, or (variadic)
defp transform_bool_op(op, args, ctx) do
t_args = Enum.map(args, fn a -> transform(a, ctx) end)
ast =
Enum.reduce(tl(t_args), hd(t_args), fn arg, acc ->
{op, [], [acc, arg]}
end)
{ast, ctx}
end
# inc
defp transform_inc([a], ctx) do
a_ast = transform(a, ctx)
{{:+, [], [a_ast, 1]}, ctx}
end
# dec
defp transform_dec([a], ctx) do
a_ast = transform(a, ctx)
{{:-, [], [a_ast, 1]}, ctx}
end
# str — concatenate with <> using to_string
defp transform_str(args, ctx) do
t_args = Enum.map(args, fn a -> transform(a, ctx) end)
ast =
case t_args do
[] ->
""
[single] ->
{{:., [], [{:__aliases__, [alias: false], [:Kernel]}, :to_string]}, [], [single]}
_ ->
stringified =
Enum.map(t_args, fn a ->
{{:., [], [{:__aliases__, [alias: false], [:Kernel]}, :to_string]}, [], [a]}
end)
Enum.reduce(tl(stringified), hd(stringified), fn arg, acc ->
{:<>, [], [acc, arg]}
end)
end
{ast, ctx}
end
# println → IO.puts
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 =
case t_args do
[single] ->
{{:., [], [{:__aliases__, [alias: false], [:IO]}, :puts]}, [], [single]}
_ ->
# Concatenate all args as strings first
{str_ast, _} = transform_str(args, ctx)
{{:., [], [{:__aliases__, [alias: false], [:IO]}, :puts]}, [], [str_ast]}
end
{ast, ctx}
end
# (pr-str val) -> CljElixir.Printer.pr_str(val)
# (pr-str a b c) -> join pr_str of each with space
defp transform_pr_str(args, ctx) do
t_args = Enum.map(args, fn a -> transform(a, ctx) end)
printer_mod = {:__aliases__, [alias: false], [:CljElixir, :Printer]}
case t_args do
[single] ->
ast = {{:., [], [printer_mod, :pr_str]}, [], [single]}
{ast, ctx}
multiple ->
strs =
Enum.map(multiple, fn a ->
{{:., [], [printer_mod, :pr_str]}, [], [a]}
end)
joined =
Enum.reduce(tl(strs), hd(strs), fn s, acc ->
{:<>, [], [acc, {:<>, [], [" ", s]}]}
end)
{joined, ctx}
end
end
# (pr val) -> IO.write(CljElixir.Printer.pr_str(val))
# Multiple args separated by spaces
defp transform_pr(args, ctx) do
t_args = Enum.map(args, fn a -> transform(a, ctx) end)
printer_mod = {:__aliases__, [alias: false], [:CljElixir, :Printer]}
io_mod = {:__aliases__, [alias: false], [:IO]}
writes =
t_args
|> Enum.with_index()
|> Enum.flat_map(fn {a, i} ->
pr_call = {{:., [], [printer_mod, :pr_str]}, [], [a]}
write_call = {{:., [], [io_mod, :write]}, [], [pr_call]}
if i > 0 do
space_call = {{:., [], [io_mod, :write]}, [], [" "]}
[space_call, write_call]
else
[write_call]
end
end)
ast = {:__block__, [], writes}
{ast, ctx}
end
# (prn val) -> IO.puts(CljElixir.Printer.pr_str(val))
# Multiple args joined with spaces, then newline
defp transform_prn(args, ctx) do
t_args = Enum.map(args, fn a -> transform(a, ctx) end)
printer_mod = {:__aliases__, [alias: false], [:CljElixir, :Printer]}
io_mod = {:__aliases__, [alias: false], [:IO]}
strs =
Enum.map(t_args, fn a ->
{{:., [], [printer_mod, :pr_str]}, [], [a]}
end)
joined =
case strs do
[single] ->
single
multiple ->
Enum.reduce(tl(multiple), hd(multiple), fn s, acc ->
{:<>, [], [acc, {:<>, [], [" ", s]}]}
end)
end
ast = {{:., [], [io_mod, :puts]}, [], [joined]}
{ast, ctx}
end
# (print-str val) -> CljElixir.Printer.print_str(val)
# (print-str a b c) -> join print_str of each with space
defp transform_print_str(args, ctx) do
t_args = Enum.map(args, fn a -> transform(a, ctx) end)
printer_mod = {:__aliases__, [alias: false], [:CljElixir, :Printer]}
case t_args do
[single] ->
ast = {{:., [], [printer_mod, :print_str]}, [], [single]}
{ast, ctx}
multiple ->
strs =
Enum.map(multiple, fn a ->
{{:., [], [printer_mod, :print_str]}, [], [a]}
end)
joined =
Enum.reduce(tl(strs), hd(strs), fn s, acc ->
{:<>, [], [acc, {:<>, [], [" ", s]}]}
end)
{joined, ctx}
end
end
# nil?
defp transform_nil_check([a], ctx) do
a_ast = transform(a, ctx)
{{:is_nil, [], [a_ast]}, ctx}
end
# throw → raise
defp transform_throw([a], ctx) do
a_ast = transform(a, ctx)
{{:raise, [], [a_ast]}, ctx}
end
# count → ICounted.count (Phase 2 protocol)
defp transform_count([a], ctx) do
a_ast = transform(a, ctx)
ast = protocol_call([:CljElixir, :ICounted], :count, [a_ast])
{ast, ctx}
end
# hd
defp transform_hd([a], ctx) do
a_ast = transform(a, ctx)
{{:hd, [], [a_ast]}, ctx}
end
# tl
defp transform_tl([a], ctx) do
a_ast = transform(a, ctx)
{{:tl, [], [a_ast]}, ctx}
end
# cons → [h | t]
defp transform_cons([h, t], ctx) do
h_ast = transform(h, ctx)
t_ast = transform(t, ctx)
ast = [{:|, [], [h_ast, t_ast]}]
{ast, ctx}
end
# ---------------------------------------------------------------------------
# Positional constructor: (->Name arg1 arg2 ...)
# ---------------------------------------------------------------------------
defp transform_positional_constructor(name, args, meta, ctx) do
# "->Name" → strip ->
record_name = String.slice(name, 2..-1//1)
mod_alias = parse_module_name(record_name)
t_args = Enum.map(args, fn a -> transform(a, ctx) end)
em = elixir_meta(meta)
# Call Module.new(args...)
ast = {{:., em, [mod_alias, :new]}, em, t_args}
{ast, ctx}
end
# Map constructor: (map->Name {:field val ...})
defp transform_map_constructor(name, args, meta, ctx) do
record_name = String.slice(name, 5..-1//1)
mod_alias = parse_module_name(record_name)
t_args = Enum.map(args, fn a -> transform(a, ctx) end)
em = elixir_meta(meta)
# Use Kernel.struct!/2
ast = {{:., em, [{:__aliases__, [alias: false], [:Kernel]}, :struct!]}, em, [mod_alias | t_args]}
{ast, ctx}
end
# ---------------------------------------------------------------------------
# Quote (explicit form)
# ---------------------------------------------------------------------------
defp transform_quote_form([inner], _meta, ctx) do
{quote_form(inner), ctx}
end
defp quote_form(form) do
case form do
{:list, _, elements} ->
Enum.map(elements, &quote_form/1)
{:vector, _, elements} ->
Enum.map(elements, &quote_form/1)
{:symbol, _, name} ->
String.to_atom(name)
{:map, _, pairs} ->
kv =
pairs
|> Enum.chunk_every(2)
|> Enum.map(fn [k, v] -> {quote_form(k), quote_form(v)} end)
{:%{}, [], kv}
{:tuple, _, elements} ->
{:{}, [], Enum.map(elements, &quote_form/1)}
other ->
other
end
end
# ---------------------------------------------------------------------------
# Quasiquote
# ---------------------------------------------------------------------------
defp transform_quasiquote(form, ctx) do
case form do
{:unquote, _, inner} ->
transform(inner, ctx)
{:list, meta, elements} ->
transform_quasiquote_list(elements, meta, ctx)
{:vector, _, elements} ->
Enum.map(elements, fn e -> transform_quasiquote(e, ctx) end)
{:map, _, pairs} ->
kv =
pairs
|> Enum.chunk_every(2)
|> Enum.map(fn [k, v] ->
{transform_quasiquote(k, ctx), transform_quasiquote(v, ctx)}
end)
{:%{}, [], kv}
{:symbol, _, name} ->
String.to_atom(name)
other ->
other
end
end
defp transform_quasiquote_list(elements, _meta, ctx) do
# Handle splice-unquote within list elements
parts =
Enum.map(elements, fn
{:splice_unquote, _, inner} ->
{:splice, transform(inner, ctx)}
elem ->
{:elem, transform_quasiquote(elem, ctx)}
end)
# Build the list, splicing where needed
if Enum.any?(parts, fn {t, _} -> t == :splice end) do
# Need to concat
segments =
Enum.map(parts, fn
{:splice, ast} -> ast
{:elem, ast} -> [ast]
end)
Enum.reduce(Enum.reverse(segments), [], fn
segment, [] when is_list(segment) -> segment
segment, acc when is_list(segment) ->
{{:., [], [{:__aliases__, [alias: false], [:Enum]}, :concat]}, [],
[segment, acc]}
segment, [] ->
segment
segment, acc ->
{{:., [], [{:__aliases__, [alias: false], [:Enum]}, :concat]}, [],
[segment, acc]}
end)
else
Enum.map(parts, fn {_, ast} -> ast end)
end
end
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
# Extract name from args (first symbol or with_meta wrapping)
defp extract_name([{:with_meta, _, {_meta_form, name_form}} | rest]) do
{name_form, rest}
end
defp extract_name([name_form | rest]) do
{name_form, rest}
end
# Convert a CljElixir name form to an Elixir module alias AST
defp module_name_ast({:symbol, _, name}) do
parse_module_name(name)
end
defp module_name_ast(name) when is_binary(name) do
parse_module_name(name)
end
defp module_name_ast(name) when is_atom(name) do
{:__aliases__, [alias: false], [name]}
end
# Get the string name from a symbol/name form
defp symbol_name({:symbol, _, name}), do: name
defp symbol_name(name) when is_atom(name), do: Atom.to_string(name)
# Convert symbol to atom for function names
defp symbol_to_atom({:symbol, _, name}), do: String.to_atom(munge_name(name))
defp symbol_to_atom(name) when is_atom(name), do: name
# Get vector elements
defp vector_elements({:vector, _, elements}), do: elements
# Name munging: hyphens → underscores for identifiers
@doc "Convert CljElixir names (with hyphens) to Elixir names (with underscores)"
def munge_name(name) when is_binary(name) do
name
|> String.replace("-", "_")
|> String.replace("?", "_qmark")
|> String.replace("!", "_bang")
end
defp munge_ffi_name(name) when is_binary(name) do
String.replace(name, "-", "_")
end
# Create a function call AST node
defp call_with_args(name, args) do
{name, [], args}
end
# Transform body: list of forms → block or single form
defp transform_body(forms, ctx) do
asts = Enum.map(forms, fn f -> transform(f, ctx) end)
case asts do
[single] -> single
multiple -> {:__block__, [], multiple}
end
end
# Generate a unique variable
defp unique_var(prefix, _ctx) do
# Use a unique reference to avoid collisions
{prefix, [], nil}
end
# Bump gensym counter
defp bump_gensym(ctx) do
counter = ctx.gensym_counter + 1
{counter, %{ctx | gensym_counter: counter}}
end
# Strip leading dash from protocol function names
defp strip_leading_dash("-" <> rest), do: rest
defp strip_leading_dash(name), do: name
# Resolve type names for extend-type etc
defp resolve_type_name({:symbol, _, name}), do: parse_module_name(name)
defp resolve_type_name(name) when is_binary(name), do: parse_module_name(name)
defp resolve_type_name(name) when is_atom(name), do: name
# Parse protocol groups from extend-type/defrecord inline impls
# Input: [ProtoName, (fn_form1), (fn_form2), ProtoName2, ...]
defp parse_protocol_groups(forms) do
do_parse_protocol_groups(forms, nil, [], [])
end
defp do_parse_protocol_groups([], nil, _fns, acc) do
Enum.reverse(acc)
end
defp do_parse_protocol_groups([], current_proto, fns, acc) do
Enum.reverse([{current_proto, Enum.reverse(fns)} | acc])
end
# A symbol that starts with uppercase or looks like a protocol name
defp do_parse_protocol_groups([{:symbol, _, name} = sym | rest], current_proto, fns, acc) do
first = String.first(name)
if first == String.upcase(first) and first != String.downcase(first) and
not String.starts_with?(name, "-") do
# This is a new protocol name
new_acc =
if current_proto do
[{current_proto, Enum.reverse(fns)} | acc]
else
acc
end
do_parse_protocol_groups(rest, symbol_name(sym), [], new_acc)
else
# This is a function definition form (should be a list, but handle symbol gracefully)
do_parse_protocol_groups(rest, current_proto, [sym | fns], acc)
end
end
defp do_parse_protocol_groups([form | rest], current_proto, fns, acc) do
do_parse_protocol_groups(rest, current_proto, [form | fns], acc)
end
# Parse type groups for extend-protocol
defp parse_type_groups(forms) do
do_parse_type_groups(forms, nil, [], [])
end
defp do_parse_type_groups([], nil, _fns, acc) do
Enum.reverse(acc)
end
defp do_parse_type_groups([], current_type, fns, acc) do
Enum.reverse([{current_type, Enum.reverse(fns)} | acc])
end
defp do_parse_type_groups([{:symbol, _, _name} = sym | rest], current_type, fns, acc) do
# Check if this looks like a type name (next form is a list/function def)
# Heuristic: if the next form is a list (function definition), this is a type name
is_type =
case rest do
[{:list, _, _} | _] -> true
# Also could be the last type with no functions following
[] -> false
_ -> false
end
if is_type or (current_type == nil) do
new_acc =
if current_type do
[{current_type, Enum.reverse(fns)} | acc]
else
acc
end
do_parse_type_groups(rest, sym, [], new_acc)
else
do_parse_type_groups(rest, current_type, [sym | fns], acc)
end
end
defp do_parse_type_groups([form | rest], current_type, fns, acc) do
do_parse_type_groups(rest, current_type, [form | fns], acc)
end
# Transform protocol function definitions
defp transform_protocol_fns(fns, ctx) do
Enum.flat_map(fns, fn
{:list, _, [{:symbol, _, fname} | clause_args]} ->
fun_atom = String.to_atom(munge_name(strip_leading_dash(fname)))
transform_protocol_fn_clauses(fun_atom, clause_args, ctx)
_ ->
[]
end)
end
defp transform_protocol_fn_clauses(fun_atom, args, ctx) do
case args do
# Single clause: (-fname [params] body...)
[{:vector, _, params} | body] ->
pattern_ctx = %{ctx | in_pattern: true}
param_asts = Enum.map(params, fn p -> transform(p, pattern_ctx) end)
body_ast = transform_body(body, ctx)
[{:def, [], [{fun_atom, [], param_asts}, [do: body_ast]]}]
# Multi-clause: (-fname ([p1] b1) ([p2 p3] b2))
clauses ->
Enum.map(clauses, fn
{:list, _, [{:vector, _, params} | body]} ->
pattern_ctx = %{ctx | in_pattern: true}
param_asts = Enum.map(params, fn p -> transform(p, pattern_ctx) end)
body_ast = transform_body(body, ctx)
{:def, [], [{fun_atom, [], param_asts}, [do: body_ast]]}
end)
end
end
# ---------------------------------------------------------------------------
# Phase 2: Protocol-backed core functions
# ---------------------------------------------------------------------------
defp protocol_call(mod_parts, fun, args) do
mod_ast = {:__aliases__, [alias: false], mod_parts}
{{:., [], [mod_ast, fun]}, [], args}
end
# get
defp transform_get([m, k], ctx) do
m_ast = transform(m, ctx)
k_ast = transform(k, ctx)
ast = protocol_call([:CljElixir, :ILookup], :lookup, [m_ast, k_ast])
{ast, ctx}
end
defp transform_get([m, k, nf], ctx) do
m_ast = transform(m, ctx)
k_ast = transform(k, ctx)
nf_ast = transform(nf, ctx)
ast = protocol_call([:CljElixir, :ILookup], :lookup, [m_ast, k_ast, nf_ast])
{ast, ctx}
end
# assoc
defp transform_assoc([m, k, v], ctx) do
m_ast = transform(m, ctx)
k_ast = transform(k, ctx)
v_ast = transform(v, ctx)
ast = protocol_call([:CljElixir, :IAssociative], :assoc, [m_ast, k_ast, v_ast])
{ast, ctx}
end
# dissoc
defp transform_dissoc([m, k], ctx) do
m_ast = transform(m, ctx)
k_ast = transform(k, ctx)
ast = protocol_call([:CljElixir, :IMap], :dissoc, [m_ast, k_ast])
{ast, ctx}
end
# update - (update m k f) => assoc(m, k, f.(lookup(m, k)))
defp transform_update([m, k, f], ctx) do
m_ast = transform(m, ctx)
k_ast = transform(k, ctx)
f_ast = transform(f, ctx)
lookup_ast = protocol_call([:CljElixir, :ILookup], :lookup, [m_ast, k_ast])
apply_ast = {{:., [], [f_ast]}, [], [lookup_ast]}
ast = protocol_call([:CljElixir, :IAssociative], :assoc, [m_ast, k_ast, apply_ast])
{ast, ctx}
end
# conj
defp transform_conj([c, x], ctx) do
c_ast = transform(c, ctx)
x_ast = transform(x, ctx)
ast = protocol_call([:CljElixir, :ICollection], :conj, [c_ast, x_ast])
{ast, ctx}
end
# contains?
defp transform_contains([m, k], ctx) do
m_ast = transform(m, ctx)
k_ast = transform(k, ctx)
ast = protocol_call([:CljElixir, :IAssociative], :contains_key_qmark, [m_ast, k_ast])
{ast, ctx}
end
# empty?
defp transform_empty([c], ctx) do
c_ast = transform(c, ctx)
count_ast = protocol_call([:CljElixir, :ICounted], :count, [c_ast])
ast = {:==, [context: Elixir, imports: [{2, Kernel}]], [count_ast, 0]}
{ast, ctx}
end
# nth
defp transform_nth([c, n], ctx) do
c_ast = transform(c, ctx)
n_ast = transform(n, ctx)
ast = protocol_call([:CljElixir, :IIndexed], :nth, [c_ast, n_ast])
{ast, ctx}
end
defp transform_nth([c, n, nf], ctx) do
c_ast = transform(c, ctx)
n_ast = transform(n, ctx)
nf_ast = transform(nf, ctx)
ast = protocol_call([:CljElixir, :IIndexed], :nth, [c_ast, n_ast, nf_ast])
{ast, ctx}
end
# first
defp transform_first([c], ctx) do
c_ast = transform(c, ctx)
ast = protocol_call([:CljElixir, :ISeq], :first, [c_ast])
{ast, ctx}
end
# rest
defp transform_rest([c], ctx) do
c_ast = transform(c, ctx)
ast = protocol_call([:CljElixir, :ISeq], :rest, [c_ast])
{ast, ctx}
end
# seq
defp transform_seq([c], ctx) do
c_ast = transform(c, ctx)
ast = protocol_call([:CljElixir, :ISeqable], :seq, [c_ast])
{ast, ctx}
end
# reduce - 2 or 3 arg form
defp transform_reduce([f, coll], ctx) do
f_ast = transform(f, ctx)
coll_ast = transform(coll, ctx)
ast = {{:., [], [{:__aliases__, [alias: false], [:Enum]}, :reduce]}, [], [coll_ast, f_ast]}
{ast, ctx}
end
defp transform_reduce([f, init, coll], ctx) do
f_ast = transform(f, ctx)
init_ast = transform(init, ctx)
coll_ast = transform(coll, ctx)
ast = {{:., [], [{:__aliases__, [alias: false], [:Enum]}, :reduce]}, [], [coll_ast, init_ast, f_ast]}
{ast, ctx}
end
# reduce-kv
defp transform_reduce_kv([f, init, coll], ctx) do
f_ast = transform(f, ctx)
init_ast = transform(init, ctx)
coll_ast = transform(coll, ctx)
ast = protocol_call([:CljElixir, :IKVReduce], :kv_reduce, [coll_ast, f_ast, init_ast])
{ast, ctx}
end
# map (as function, not data literal)
defp transform_map_fn([f, coll], ctx) do
f_ast = transform(f, ctx)
coll_ast = transform(coll, ctx)
ast = {{:., [], [{:__aliases__, [alias: false], [:Enum]}, :map]}, [], [coll_ast, f_ast]}
{ast, ctx}
end
# filter
defp transform_filter([pred, coll], ctx) do
pred_ast = transform(pred, ctx)
coll_ast = transform(coll, ctx)
ast = {{:., [], [{:__aliases__, [alias: false], [:Enum]}, :filter]}, [], [coll_ast, pred_ast]}
{ast, ctx}
end
# concat
defp transform_concat([a, b], ctx) do
a_ast = transform(a, ctx)
b_ast = transform(b, ctx)
ast = {{:., [], [{:__aliases__, [alias: false], [:Enum]}, :concat]}, [], [a_ast, b_ast]}
{ast, ctx}
end
# take
defp transform_take([n, coll], ctx) do
n_ast = transform(n, ctx)
coll_ast = transform(coll, ctx)
ast = {{:., [], [{:__aliases__, [alias: false], [:Enum]}, :take]}, [], [coll_ast, n_ast]}
{ast, ctx}
end
# drop
defp transform_drop([n, coll], ctx) do
n_ast = transform(n, ctx)
coll_ast = transform(coll, ctx)
ast = {{:., [], [{:__aliases__, [alias: false], [:Enum]}, :drop]}, [], [coll_ast, n_ast]}
{ast, ctx}
end
# sort
defp transform_sort([coll], ctx) do
coll_ast = transform(coll, ctx)
ast = {{:., [], [{:__aliases__, [alias: false], [:Enum]}, :sort]}, [], [coll_ast]}
{ast, ctx}
end
# sort-by
defp transform_sort_by([f, coll], ctx) do
f_ast = transform(f, ctx)
coll_ast = transform(coll, ctx)
ast = {{:., [], [{:__aliases__, [alias: false], [:Enum]}, :sort_by]}, [], [coll_ast, f_ast]}
{ast, ctx}
end
# group-by
defp transform_group_by([f, coll], ctx) do
f_ast = transform(f, ctx)
coll_ast = transform(coll, ctx)
ast = {{:., [], [{:__aliases__, [alias: false], [:Enum]}, :group_by]}, [], [coll_ast, f_ast]}
{ast, ctx}
end
# frequencies
defp transform_frequencies([coll], ctx) do
coll_ast = transform(coll, ctx)
ast = {{:., [], [{:__aliases__, [alias: false], [:Enum]}, :frequencies]}, [], [coll_ast]}
{ast, ctx}
end
# distinct
defp transform_distinct([coll], ctx) do
coll_ast = transform(coll, ctx)
ast = {{:., [], [{:__aliases__, [alias: false], [:Enum]}, :uniq]}, [], [coll_ast]}
{ast, ctx}
end
# mapcat
defp transform_mapcat([f, coll], ctx) do
f_ast = transform(f, ctx)
coll_ast = transform(coll, ctx)
ast = {{:., [], [{:__aliases__, [alias: false], [:Enum]}, :flat_map]}, [], [coll_ast, f_ast]}
{ast, ctx}
end
# partition
defp transform_partition([n, coll], ctx) do
n_ast = transform(n, ctx)
coll_ast = transform(coll, ctx)
ast = {{:., [], [{:__aliases__, [alias: false], [:Enum]}, :chunk_every]}, [], [coll_ast, n_ast]}
{ast, ctx}
end
# keys
defp transform_keys([m], ctx) do
m_ast = transform(m, ctx)
ast = {{:., [], [{:__aliases__, [alias: false], [:Map]}, :keys]}, [], [m_ast]}
{ast, ctx}
end
# vals
defp transform_vals([m], ctx) do
m_ast = transform(m, ctx)
ast = {{:., [], [{:__aliases__, [alias: false], [:Map]}, :values]}, [], [m_ast]}
{ast, ctx}
end
# merge
defp transform_merge([m1, m2], ctx) do
m1_ast = transform(m1, ctx)
m2_ast = transform(m2, ctx)
ast = {{:., [], [{:__aliases__, [alias: false], [:Map]}, :merge]}, [], [m1_ast, m2_ast]}
{ast, ctx}
end
# select-keys
defp transform_select_keys([m, ks], ctx) do
m_ast = transform(m, ctx)
ks_ast = transform(ks, ctx)
ast = {{:., [], [{:__aliases__, [alias: false], [:Map]}, :take]}, [], [m_ast, ks_ast]}
{ast, ctx}
end
# into
defp transform_into([c1, c2], ctx) do
c1_ast = transform(c1, ctx)
c2_ast = transform(c2, ctx)
ast = {{:., [], [{:__aliases__, [alias: false], [:Enum]}, :into]}, [], [c2_ast, c1_ast]}
{ast, ctx}
end
# get-in
defp transform_get_in([m, ks], ctx) do
m_ast = transform(m, ctx)
ks_ast = transform(ks, ctx)
ast = protocol_call([:CljElixir, :Core], :get_in, [m_ast, ks_ast])
{ast, ctx}
end
# assoc-in
defp transform_assoc_in([m, ks, v], ctx) do
m_ast = transform(m, ctx)
ks_ast = transform(ks, ctx)
v_ast = transform(v, ctx)
ast = protocol_call([:CljElixir, :Core], :assoc_in, [m_ast, ks_ast, v_ast])
{ast, ctx}
end
# update-in
defp transform_update_in([m, ks, f], ctx) do
m_ast = transform(m, ctx)
ks_ast = transform(ks, ctx)
f_ast = transform(f, ctx)
ast = protocol_call([:CljElixir, :Core], :update_in, [m_ast, ks_ast, f_ast])
{ast, ctx}
end
# list - creates an Elixir list from arguments
defp transform_list_call(args, ctx) do
t_args = Enum.map(args, fn a -> transform(a, ctx) end)
{t_args, ctx}
end
# ---------------------------------------------------------------------------
# Phase 3: Vector builtins
# ---------------------------------------------------------------------------
# (vec coll) — convert any collection to PersistentVector
defp transform_vec([coll], ctx) do
coll_ast = transform(coll, ctx)
pv_mod = {:__aliases__, [alias: false], [:CljElixir, :PersistentVector]}
enum_mod = {:__aliases__, [alias: false], [:Enum]}
to_list = {{:., [], [enum_mod, :to_list]}, [], [coll_ast]}
ast = {{:., [], [pv_mod, :from_list]}, [], [to_list]}
{ast, ctx}
end
# (vector & args) — create PersistentVector from arguments
defp transform_vector_call(args, ctx) do
t_args = Enum.map(args, fn a -> transform(a, ctx) end)
pv_mod = {:__aliases__, [alias: false], [:CljElixir, :PersistentVector]}
ast = {{:., [], [pv_mod, :from_list]}, [], [t_args]}
{ast, ctx}
end
# (subvec v start) or (subvec v start end)
defp transform_subvec_call([v, start], ctx) do
v_ast = transform(v, ctx)
start_ast = transform(start, ctx)
sv_mod = {:__aliases__, [alias: false], [:CljElixir, :SubVector]}
ast = {{:., [], [sv_mod, :sv_new]}, [], [v_ast, start_ast]}
{ast, ctx}
end
defp transform_subvec_call([v, start, end_], ctx) do
v_ast = transform(v, ctx)
start_ast = transform(start, ctx)
end_ast = transform(end_, ctx)
sv_mod = {:__aliases__, [alias: false], [:CljElixir, :SubVector]}
ast = {{:., [], [sv_mod, :sv_new]}, [], [v_ast, start_ast, end_ast]}
{ast, ctx}
end
# (peek coll) — protocol dispatch to IStack
defp transform_peek([coll], ctx) do
coll_ast = transform(coll, ctx)
ast = protocol_call([:CljElixir, :IStack], :peek, [coll_ast])
{ast, ctx}
end
# (pop coll) — protocol dispatch to IStack
defp transform_pop([coll], ctx) do
coll_ast = transform(coll, ctx)
ast = protocol_call([:CljElixir, :IStack], :pop, [coll_ast])
{ast, ctx}
end
# (vector? x) — check if x is a PersistentVector
defp transform_vector_check([x], ctx) do
x_ast = transform(x, ctx)
pv_mod = {:__aliases__, [alias: false], [:CljElixir, :PersistentVector]}
ast = {{:., [], [{:__aliases__, [alias: false], [:Kernel]}, :is_struct]}, [], [x_ast, pv_mod]}
{ast, ctx}
end
# ---------------------------------------------------------------------------
# Phase 4: Domain tools
# ---------------------------------------------------------------------------
# (tuple & args) — create a BEAM tuple from arguments
defp transform_tuple_fn(args, ctx) do
t_args = Enum.map(args, fn a -> transform(a, ctx) end)
ast = {:{}, [], t_args}
{ast, ctx}
end
# (clojurify x) — protocol dispatch to IClojurify
defp transform_clojurify([x], ctx) do
x_ast = transform(x, ctx)
ast = protocol_call([:CljElixir, :IClojurify], :clojurify, [x_ast])
{ast, ctx}
end
# (elixirify x) — protocol dispatch to IElixirify
defp transform_elixirify([x], ctx) do
x_ast = transform(x, ctx)
ast = protocol_call([:CljElixir, :IElixirify], :elixirify, [x_ast])
{ast, ctx}
end
# ---------------------------------------------------------------------------
# Phase 6: Threading macros (-> and ->>)
# ---------------------------------------------------------------------------
# (-> x) → x
# (-> x form1 form2 ...) → thread x through forms, inserting as first arg
defp transform_thread_first([], _meta, _ctx) do
raise "-> requires at least one argument"
end
defp transform_thread_first([x], _meta, ctx) do
do_transform(x, ctx)
end
defp transform_thread_first([x | forms], meta, ctx) do
threaded = Enum.reduce(forms, x, fn form, acc ->
thread_form(acc, form, meta, :first)
end)
do_transform(threaded, ctx)
end
# (->> x) → x
# (->> x form1 form2 ...) → thread x through forms, inserting as last arg
defp transform_thread_last([], _meta, _ctx) do
raise "->> requires at least one argument"
end
defp transform_thread_last([x], _meta, ctx) do
do_transform(x, ctx)
end
defp transform_thread_last([x | forms], meta, ctx) do
threaded = Enum.reduce(forms, x, fn form, acc ->
thread_form(acc, form, meta, :last)
end)
do_transform(threaded, ctx)
end
# Rewrite a single threading step at the CljElixir AST level.
# - Bare symbol: wrap as (sym prev) or (sym prev)
# - Keyword (atom): wrap as (kw prev)
# - List (head ...args): insert prev as first or last arg
defp thread_form(prev, {:symbol, s_meta, _name} = sym, _meta, _position) do
{:list, s_meta, [sym, prev]}
end
defp thread_form(prev, {:list, l_meta, [head | args]}, _meta, :first) do
{:list, l_meta, [head, prev | args]}
end
defp thread_form(prev, {:list, l_meta, [head | args]}, _meta, :last) do
{:list, l_meta, [head | args ++ [prev]]}
end
defp thread_form(prev, kw, _meta, _position) when is_atom(kw) do
{:list, %{}, [kw, prev]}
end
defp thread_form(prev, other, _meta, _position) do
# For any other form (vector, map, etc.), just use as-is — likely an error
# but let the transformer handle it
{:list, %{}, [other, prev]}
end
# ---------------------------------------------------------------------------
# try / catch / finally
# ---------------------------------------------------------------------------
# (try body... (catch ...) ... (finally ...))
defp transform_try(args, meta, ctx) do
{body_forms, catch_clauses, finally_clause} = partition_try_args(args)
# Transform body
body_ast = transform_body(body_forms, ctx)
# Build the try keyword list
try_opts = [do: body_ast]
# Process catch clauses into rescue and catch groups
{rescue_clauses, catch_clauses_ast} = classify_catch_clauses(catch_clauses, ctx)
try_opts =
if rescue_clauses != [] do
try_opts ++ [rescue: rescue_clauses]
else
try_opts
end
try_opts =
if catch_clauses_ast != [] do
try_opts ++ [catch: catch_clauses_ast]
else
try_opts
end
try_opts =
case finally_clause do
nil ->
try_opts
forms ->
after_ast = transform_body(forms, ctx)
try_opts ++ [after: after_ast]
end
ast = {:try, elixir_meta(meta), [try_opts]}
{ast, ctx}
end
# Separate try args into body forms, catch clauses, and an optional finally clause.
defp partition_try_args(args) do
{body, catches, finally} =
Enum.reduce(args, {[], [], nil}, fn form, {body_acc, catch_acc, finally_acc} ->
case form do
{:list, _, [{:symbol, _, "catch"} | catch_args]} ->
{body_acc, catch_acc ++ [catch_args], finally_acc}
{:list, _, [{:symbol, _, "finally"} | finally_args]} ->
{body_acc, catch_acc, finally_args}
_ ->
{body_acc ++ [form], catch_acc, finally_acc}
end
end)
{body, catches, finally}
end
# Classify each catch clause as either a rescue or catch clause.
# Returns {rescue_clauses_ast, catch_clauses_ast}
defp classify_catch_clauses(catch_clauses, ctx) do
Enum.reduce(catch_clauses, {[], []}, fn clause, {rescue_acc, catch_acc} ->
case clause do
# (catch :error e body...) / (catch :exit e body...) / (catch :throw e body...)
[kind, {:symbol, _, binding_name} | body]
when kind in [:error, :exit, :throw] ->
binding_var = {String.to_atom(munge_name(binding_name)), [], nil}
body_ast = transform_body(body, ctx)
clause_ast = {:->, [], [[kind, binding_var], body_ast]}
{rescue_acc, catch_acc ++ [clause_ast]}
# (catch ExType e body...) — uppercase symbol = rescue with type
[{:symbol, _, type_name}, {:symbol, _, binding_name} | body] ->
if uppercase_start?(type_name) do
type_ast = parse_module_name(type_name)
binding_var = {String.to_atom(munge_name(binding_name)), [], nil}
body_ast = transform_body(body, ctx)
rescue_pattern = {:in, [], [binding_var, [type_ast]]}
clause_ast = {:->, [], [[rescue_pattern], body_ast]}
{rescue_acc ++ [clause_ast], catch_acc}
else
# lowercase symbol as first arg, no type — rescue any
# Treat the first symbol as the binding, rest as body
binding_var = {String.to_atom(munge_name(type_name)), [], nil}
rest_body = [{:symbol, %{}, binding_name} | body]
body_ast = transform_body(rest_body, ctx)
clause_ast = {:->, [], [[binding_var], body_ast]}
{rescue_acc ++ [clause_ast], catch_acc}
end
# (catch e body...) — single lowercase symbol = rescue any exception
[{:symbol, _, binding_name} | body] ->
binding_var = {String.to_atom(munge_name(binding_name)), [], nil}
body_ast = transform_body(body, ctx)
clause_ast = {:->, [], [[binding_var], body_ast]}
{rescue_acc ++ [clause_ast], catch_acc}
_ ->
raise "Invalid catch clause: #{inspect(clause)}"
end
end)
end
defp uppercase_start?(name) do
first = String.first(name)
first == String.upcase(first) and first != String.downcase(first)
end
# ---------------------------------------------------------------------------
# Phase 6: defmacro
# ---------------------------------------------------------------------------
defp transform_defmacro(args, _meta, ctx) do
{name_form, rest} = extract_name(args)
macro_name = symbol_name(name_form)
# Parse params and body (same structure as defn, single-arity only)
[{required_params, rest_param, _guard, body_forms}] = parse_defn_clauses(rest)
required_count = length(required_params)
has_rest = rest_param != nil
# Build parameter variable ASTs for the anonymous function
param_vars =
Enum.map(required_params, fn
{:symbol, _, pname} -> {String.to_atom(munge_name(pname)), [], nil}
end)
rest_var =
case rest_param do
{:symbol, _, rname} -> {String.to_atom(munge_name(rname)), [], nil}
nil -> nil
end
all_param_vars = if rest_var, do: param_vars ++ [rest_var], else: param_vars
# Transform the macro body with in_macro: true
# This causes quasiquote to produce CljElixir AST constructors
macro_ctx = %{ctx | in_macro: true, in_pattern: false}
body_ast = transform_body(body_forms, macro_ctx)
# Build the fn AST
fn_clause = {:->, [], [all_param_vars, body_ast]}
fn_ast = {:fn, [], [fn_clause]}
# Evaluate the function to get a runtime callable
{fun, _bindings} = Code.eval_quoted(fn_ast)
# Store in context
new_macros = Map.put(ctx.macros, macro_name, {fun, required_count, has_rest})
new_ctx = %{ctx | macros: new_macros}
# defmacro produces no runtime code
{nil, new_ctx}
end
# ---------------------------------------------------------------------------
# Macro expansion
# ---------------------------------------------------------------------------
defp expand_macro(name, args, ctx) do
{fun, required_count, has_rest} = ctx.macros[name]
# Call the macro function with CljElixir AST args
expanded =
if has_rest do
{required_args, rest_args} = Enum.split(args, required_count)
apply(fun, required_args ++ [rest_args])
else
apply(fun, args)
end
# The macro returns CljElixir AST — transform it normally
do_transform(expanded, ctx)
end
# ---------------------------------------------------------------------------
# Macro quasiquote: produces code that constructs CljElixir AST
# ---------------------------------------------------------------------------
# Override quasiquote behavior when in_macro is true
# This is handled in do_transform for {:quasiquote, ...}
defp transform_macro_quasiquote(form, ctx, gensym_map) do
m = {:%{}, [], [{:line, 0}, {:col, 0}]}
case form do
{:unquote, _, inner} ->
# Evaluate inner normally — produces a CljElixir AST value at expansion time
transform(inner, %{ctx | in_macro: false})
{:symbol, _, name} ->
if String.ends_with?(name, "#") do
# Auto-gensym: use the pre-generated variable
base = String.trim_trailing(name, "#")
var_name = Map.get(gensym_map, base)
# Reference the gensym variable
{var_name, [], nil}
else
# Literal symbol: construct {:symbol, %{line: 0, col: 0}, name}
{:{}, [], [:symbol, m, name]}
end
{:list, _, elements} ->
transform_macro_quasiquote_list(elements, ctx, gensym_map, m)
{:vector, _, elements} ->
elems = Enum.map(elements, &transform_macro_quasiquote(&1, ctx, gensym_map))
{:{}, [], [:vector, m, elems]}
{:map, _, pairs} ->
pairs_asts = Enum.map(pairs, &transform_macro_quasiquote(&1, ctx, gensym_map))
{:{}, [], [:map, m, pairs_asts]}
{:tuple, _, elements} ->
elems = Enum.map(elements, &transform_macro_quasiquote(&1, ctx, gensym_map))
{:{}, [], [:tuple, m, elems]}
{:set, _, elements} ->
elems = Enum.map(elements, &transform_macro_quasiquote(&1, ctx, gensym_map))
{:{}, [], [:set, m, elems]}
# Literals pass through as-is (they're valid CljElixir AST)
n when is_integer(n) -> n
n when is_float(n) -> n
s when is_binary(s) -> s
a when is_atom(a) -> a
true -> true
false -> false
nil -> nil
end
end
defp transform_macro_quasiquote_list(elements, ctx, gensym_map, m) do
has_splice =
Enum.any?(elements, fn
{:splice_unquote, _, _} -> true
_ -> false
end)
if has_splice do
# Build segments: fixed elements wrapped in lists, splices as-is
segments =
Enum.map(elements, fn
{:splice_unquote, _, inner} ->
# Splice: evaluates to a list of CljElixir AST nodes
{:splice, transform(inner, %{ctx | in_macro: false})}
elem ->
{:single, transform_macro_quasiquote(elem, ctx, gensym_map)}
end)
# Group consecutive singles, keep splices separate
chunks = chunk_splice_segments(segments)
# Build concatenation expression
concat_expr = build_splice_concat(chunks)
{:{}, [], [:list, m, concat_expr]}
else
elems = Enum.map(elements, &transform_macro_quasiquote(&1, ctx, gensym_map))
{:{}, [], [:list, m, elems]}
end
end
defp chunk_splice_segments(segments) do
# Group consecutive :single items into lists, keep :splice items separate
Enum.chunk_by(segments, fn {type, _} -> type end)
|> Enum.flat_map(fn
[{:splice, _} | _] = splices ->
# Each splice is its own chunk
Enum.map(splices, fn {:splice, ast} -> {:splice, ast} end)
singles ->
[{:list, Enum.map(singles, fn {:single, ast} -> ast end)}]
end)
end
defp build_splice_concat(chunks) do
case chunks do
[{:list, elems}] ->
elems
_ ->
# Use Enum.concat with list of segments
enum_mod = {:__aliases__, [alias: false], [:Enum]}
segments =
Enum.map(chunks, fn
{:splice, ast} -> ast
{:list, elems} -> elems
end)
{{:., [], [enum_mod, :concat]}, [], [segments]}
end
end
# ---------------------------------------------------------------------------
# Auto-gensym support
# ---------------------------------------------------------------------------
defp collect_gensyms(form) do
# Walk the form and find all symbols ending in #
names = do_collect_gensyms(form, MapSet.new())
# Generate unique variable atoms for each
gensym_map =
Enum.reduce(names, %{}, fn base, acc ->
var_atom = String.to_atom("gensym_#{base}")
Map.put(acc, base, var_atom)
end)
{MapSet.to_list(names), gensym_map}
end
defp do_collect_gensyms(form, acc) do
case form do
{:symbol, _, name} ->
if String.ends_with?(name, "#") do
MapSet.put(acc, String.trim_trailing(name, "#"))
else
acc
end
{:list, _, elements} ->
Enum.reduce(elements, acc, &do_collect_gensyms/2)
{:vector, _, elements} ->
Enum.reduce(elements, acc, &do_collect_gensyms/2)
{:map, _, pairs} ->
Enum.reduce(pairs, acc, &do_collect_gensyms/2)
{:tuple, _, elements} ->
Enum.reduce(elements, acc, &do_collect_gensyms/2)
{:set, _, elements} ->
Enum.reduce(elements, acc, &do_collect_gensyms/2)
# Don't look inside unquotes — they're evaluated, not quoted
{:unquote, _, _} -> acc
{:splice_unquote, _, _} -> acc
_ -> acc
end
end
defp generate_gensym_bindings(gensym_names) do
# For each gensym base name, generate a binding:
# __gensym_foo__ = {:symbol, %{line: 0, col: 0}, "foo__" <> Integer.to_string(:erlang.unique_integer([:positive]))}
m = {:%{}, [], [{:line, 0}, {:col, 0}]}
Enum.map(gensym_names, fn base ->
var_atom = String.to_atom("gensym_#{base}")
var_ref = {var_atom, [], nil}
# Generate: {:symbol, %{line: 0, col: 0}, base <> "__" <> Integer.to_string(:erlang.unique_integer([:positive]))}
unique_call =
{{:., [], [:erlang, :unique_integer]}, [], [[:positive]]}
to_string_call =
{{:., [], [{:__aliases__, [alias: false], [:Integer]}, :to_string]}, [], [unique_call]}
name_expr = {:<>, [], [base <> "__", to_string_call]}
symbol_tuple = {:{}, [], [:symbol, m, name_expr]}
{:=, [], [var_ref, symbol_tuple]}
end)
end
# ---------------------------------------------------------------------------
# Phase 7: Malli schema integration
# ---------------------------------------------------------------------------
# Transform (m/=> fname schema) into @spec AST
defp transform_schema_spec(args, _meta, ctx) do
[fn_name_form, schema_form] = args
# Get the function name as an atom (with name munging)
fun_name = symbol_to_atom(fn_name_form)
# Convert CljElixir AST schema to plain Elixir data
schema_data = clje_ast_to_data(schema_form)
# Convert schema references using known schemas
opts = [known_types: ctx.schemas]
# Generate @spec AST nodes
spec_asts = CljElixir.Malli.spec_ast(fun_name, schema_data, opts)
# Return as a block if multiple specs
ast = case spec_asts do
[single] -> single
multiple -> {:__block__, [], multiple}
end
{ast, ctx}
end
# Convert CljElixir AST nodes to plain Elixir data for Malli processing
defp clje_ast_to_data(form) do
case form do
{:vector, _, elements} ->
Enum.map(elements, &clje_ast_to_data/1)
{:map, _, pairs} ->
pairs
|> Enum.chunk_every(2)
|> Enum.into(%{}, fn [k, v] -> {clje_ast_to_data(k), clje_ast_to_data(v)} end)
{:symbol, _, name} ->
# Symbol reference - could be a schema name like "User"
# Return as string for schema_to_typespec to resolve via known_types
name
{:list, _, elements} ->
Enum.map(elements, &clje_ast_to_data/1)
{:set, _, elements} ->
MapSet.new(Enum.map(elements, &clje_ast_to_data/1))
{:tuple, _, elements} ->
List.to_tuple(Enum.map(elements, &clje_ast_to_data/1))
# Atoms (keywords), numbers, strings, booleans, nil pass through
other -> other
end
end
# Detect if a CljElixir AST form looks like a Malli schema definition
@schema_heads [:map, :"map-of", :and, :or, :maybe, :enum, :tuple, :list,
:vector, :set, :schema, :"=>", :=, :function]
defp detect_schema({:vector, _, [first | _]} = form) when is_atom(first) do
if first in @schema_heads do
clje_ast_to_data(form)
else
nil
end
end
defp detect_schema(_), do: nil
# Convert a schema name (CamelCase or kebab-case) to a type atom
# "User" -> :user, "PositiveInt" -> :positive_int, "my-type" -> :my_type
defp schema_name_to_type_atom(name) do
name
|> String.replace(~r/([a-z])([A-Z])/, "\\1_\\2")
|> String.downcase()
|> String.replace("-", "_")
|> String.to_atom()
end
# ---------------------------------------------------------------------------
# Source-mapping: CljElixir meta → Elixir AST metadata
# ---------------------------------------------------------------------------
# Convert CljElixir meta map to Elixir AST keyword list metadata.
# Line must be >= 1 for the Erlang compiler annotation layer;
# a zero line/col is treated as absent.
defp elixir_meta(%{line: line, col: col}) when line > 0 and col > 0,
do: [line: line, column: col]
defp elixir_meta(%{line: line}) when line > 0,
do: [line: line]
defp elixir_meta(_), do: []
end