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 # Check if file has explicit defmodule forms (ns won't auto-wrap if so) has_defmodule = Enum.any?(forms, fn {:list, _, [{:symbol, _, "defmodule"} | _]} -> true _ -> false end) {elixir_forms, final_ctx} = Enum.map_reduce(forms, ctx, fn form, acc -> {ast, new_ctx} = transform_form(form, acc) # Tag each transformed form with whether the source was a def-like form {{ast, def_form?(form)}, new_ctx} end) # Filter out nil (from ns, defmacro which produce no runtime code) elixir_forms = Enum.filter(elixir_forms, fn {ast, _} -> ast != nil end) # If ns declared a module and there are no explicit defmodule forms, # separate def-forms (inside module) from expressions (after module) elixir_forms = if final_ctx.module_name != nil and ctx.module_name == nil and not has_defmodule do {defs, exprs} = Enum.split_with(elixir_forms, fn {_ast, is_def} -> is_def end) def_asts = Enum.map(defs, fn {ast, _} -> ast end) expr_asts = Enum.map(exprs, fn {ast, _} -> ast end) block = case def_asts do [] -> nil [single] -> single multiple -> {:__block__, [], multiple} end module_ast = if block do [{:defmodule, [context: Elixir], [final_ctx.module_name, [do: block]]}] else [] end module_ast ++ expr_asts else Enum.map(elixir_forms, fn {ast, _} -> ast end) end case elixir_forms do [] -> nil [single] -> single multiple -> {:__block__, [], multiple} end end def transform(form, ctx) do {ast, _ctx} = transform_form(form, ctx) ast end # Transform a list of guard forms into a single ANDed Elixir guard AST. # [:guard [(> x 0) (< x 10)]] → {:and, [], [guard1, guard2]} defp transform_guards(guard_forms, ctx) do guard_asts = Enum.map(guard_forms, &transform(&1, ctx)) case guard_asts do [single] -> single [first | rest] -> Enum.reduce(rest, first, fn g, acc -> {:and, [context: Elixir], [acc, g]} end) end end # Is this CljElixir AST form a definition (goes inside defmodule)? defp def_form?({:list, _, [{:symbol, _, name} | _]}) when name in ~w(defn defn- def defprotocol defrecord extend-type extend-protocol reify defmacro use), do: true # m/=> schema annotations defp def_form?({:list, _, [{:symbol, _, "m/=>"} | _]}), do: true defp def_form?(_), do: false # --------------------------------------------------------------------------- # Main dispatch # --------------------------------------------------------------------------- @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, _, "ns"} -> transform_ns(args, meta, ctx) {:symbol, _, "defmodule"} -> transform_defmodule(args, meta, ctx) {:symbol, _, "defn"} -> transform_defn(args, meta, ctx, :def) {:symbol, _, "defn-"} -> transform_defn(args, meta, ctx, :defp) {: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 # --------------------------------------------------------------------------- # 0. ns — module declaration (sets ctx.module_name for auto-wrapping) # --------------------------------------------------------------------------- defp transform_ns([name_form | _rest], _meta, ctx) do mod_alias = module_name_ast(name_form) {nil, %{ctx | module_name: mod_alias}} end defp transform_ns([], _meta, ctx), do: {nil, ctx} # --------------------------------------------------------------------------- # 1. defmodule # --------------------------------------------------------------------------- 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_forms -> guard_ast = transform_guards(guard_forms, 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] :guard guard body) parse_clause_with_guards(clause_elements) end) end end defp parse_clause_with_guards([{:vector, _, params}, :guard, {:vector, _, guards} | body]) do {required, rest_param} = split_rest_params(params) {required, rest_param, guards, body} end defp parse_clause_with_guards([{: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_forms -> guard_ast = transform_guards(guard_forms, 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_guards(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, :guard | rest] -> # pattern :guard guard body — need to re-chunk # This won't happen with chunk_every(2), handle differently pat_ast = transform(pattern, pattern_ctx) body_ast = transform(List.last(rest), ctx) {:->, [], [[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_forms -> guard_ast = transform_guards(guard_forms, 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, :guard, {:vector, _, guards}, body | rest], acc, after_clause) do parse_receive_clauses(rest, [{pattern, guards, body} | acc], after_clause) end defp parse_receive_clauses([pattern, body | rest], acc, after_clause) do 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 — Clojure-compatible: nil→"", strings pass through, collections use print repr defp transform_str(args, ctx) do t_args = Enum.map(args, fn a -> transform(a, ctx) end) str_call = fn arg -> {{:., [], [{:__aliases__, [alias: false], [:CljElixir, :Printer]}, :str_value]}, [], [arg]} end ast = case t_args do [] -> "" [single] -> str_call.(single) _ -> stringified = Enum.map(t_args, str_call) Enum.reduce(tl(stringified), hd(stringified), fn arg, acc -> {:<>, [], [acc, arg]} end) end {ast, ctx} end # println → IO.puts, returns nil (Clojure convention) defp transform_println(args, ctx) do t_args = Enum.map(args, fn a -> transform(a, ctx) end) # If multiple args, join with str first io_call = 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 = {:__block__, [], [io_call, nil]} {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 ++ [nil]} {ast, ctx} end # (prn val) -> IO.puts(CljElixir.Printer.pr_str(val)), returns nil # Multiple args joined with spaces, then newline defp transform_prn(args, ctx) do t_args = Enum.map(args, fn a -> transform(a, ctx) end) 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 io_call = {{:., [], [io_mod, :puts]}, [], [joined]} ast = {:__block__, [], [io_call, nil]} {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, "e_form/1) {:vector, _, elements} -> Enum.map(elements, "e_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, "e_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 # update - (update m k f x y ...) => rewrite to (assoc m k (f (get m k) x y ...)) # Rewritten at AST level so f goes through builtin dispatch (e.g. dissoc) defp transform_update([m, k, f | extra_args], ctx) do meta = %{line: 0, col: 0} get_call = {:list, meta, [{:symbol, meta, "get"}, m, k]} f_call = {:list, meta, [f, get_call | extra_args]} assoc_call = {:list, meta, [{:symbol, meta, "assoc"}, m, k, f_call]} do_transform(assoc_call, ctx) end # conj defp transform_conj([c, x], ctx) do c_ast = transform(c, ctx) 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