Bootstrap compiler (reader, analyzer, transformer, compiler, Mix plugin), core protocols (16 protocols for Map/List/Tuple/BitString), PersistentVector (bit-partitioned trie), domain tools (clojurify/elixirify), BEAM concurrency (receive, spawn, GenServer), control flow & macros (threading, try/catch, destructuring, defmacro with quasiquote/auto-gensym), and Malli schema adapter (m/=> specs, auto @type, recursive schemas, cross-references). 537 compiler tests + 55 Malli unit tests + 15 integration tests = 607 total. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
3278 lines
100 KiB
Elixir
3278 lines
100 KiB
Elixir
defmodule CljElixir.Transformer do
|
|
@moduledoc """
|
|
Transforms CljElixir AST (from the reader) into Elixir AST (quoted expressions).
|
|
|
|
CljElixir AST nodes:
|
|
- Literals: integers, floats, strings, atoms, booleans, nil
|
|
- {:symbol, meta, "name"}
|
|
- {:list, meta, [elements]}
|
|
- {:vector, meta, [elements]}
|
|
- {:map, meta, [k1, v1, k2, v2, ...]}
|
|
- {:set, meta, [elements]}
|
|
- {:tuple, meta, [elements]}
|
|
- {:regex, meta, "pattern"}
|
|
- {:quote, meta, inner}
|
|
- {:with_meta, meta, {metadata, target}}
|
|
- {:anon_fn, meta, body}
|
|
- {:quasiquote, meta, form}
|
|
- {:unquote, meta, form}
|
|
- {:splice_unquote, meta, form}
|
|
- {:deref, meta, form}
|
|
|
|
Elixir AST: {atom, keyword_list_meta, args_list} for calls, literals for literals.
|
|
"""
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Context
|
|
# ---------------------------------------------------------------------------
|
|
|
|
defmodule Context do
|
|
@moduledoc false
|
|
defstruct module_name: nil,
|
|
function_name: nil,
|
|
function_arity: nil,
|
|
loop_var: nil,
|
|
loop_arity: nil,
|
|
in_pattern: false,
|
|
records: %{},
|
|
gensym_counter: 0,
|
|
vector_as_list: false,
|
|
macros: %{},
|
|
in_macro: false,
|
|
schemas: %{}
|
|
end
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Public API
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@doc """
|
|
Transform a list of CljElixir AST forms into a single Elixir AST node.
|
|
Returns a __block__ wrapping all transformed forms (or a single form if only one).
|
|
"""
|
|
def transform(forms, ctx \\ %Context{})
|
|
|
|
def transform(forms, ctx) when is_list(forms) do
|
|
{elixir_forms, _ctx} =
|
|
Enum.map_reduce(forms, ctx, fn form, acc ->
|
|
{ast, new_ctx} = transform_form(form, acc)
|
|
{ast, new_ctx}
|
|
end)
|
|
|
|
# Filter out nil (from defmacro which produces no runtime code)
|
|
elixir_forms = Enum.filter(elixir_forms, &(&1 != nil))
|
|
|
|
case elixir_forms do
|
|
[] -> nil
|
|
[single] -> single
|
|
multiple -> {:__block__, [], multiple}
|
|
end
|
|
end
|
|
|
|
def transform(form, ctx) do
|
|
{ast, _ctx} = transform_form(form, ctx)
|
|
ast
|
|
end
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Main dispatch
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@doc false
|
|
def transform_form(form, ctx) do
|
|
do_transform(form, ctx)
|
|
end
|
|
|
|
# --- Literals pass through ---
|
|
defp do_transform(n, ctx) when is_integer(n), do: {n, ctx}
|
|
defp do_transform(n, ctx) when is_float(n), do: {n, ctx}
|
|
defp do_transform(s, ctx) when is_binary(s), do: {s, ctx}
|
|
defp do_transform(true, ctx), do: {true, ctx}
|
|
defp do_transform(false, ctx), do: {false, ctx}
|
|
defp do_transform(nil, ctx), do: {nil, ctx}
|
|
defp do_transform(a, ctx) when is_atom(a), do: {a, ctx}
|
|
|
|
# --- Symbols ---
|
|
defp do_transform({:symbol, meta, name}, ctx) when is_binary(name) do
|
|
transform_symbol(name, meta, ctx)
|
|
end
|
|
|
|
# --- Lists (function calls and special forms) ---
|
|
defp do_transform({:list, meta, elements}, ctx) do
|
|
transform_list(elements, meta, ctx)
|
|
end
|
|
|
|
# --- Vectors ---
|
|
defp do_transform({:vector, meta, elements}, ctx) do
|
|
transform_vector(elements, meta, ctx)
|
|
end
|
|
|
|
# --- Maps ---
|
|
defp do_transform({:map, _meta, pairs}, ctx) do
|
|
transform_map(pairs, ctx)
|
|
end
|
|
|
|
# --- Sets ---
|
|
defp do_transform({:set, _meta, elements}, ctx) do
|
|
transform_set(elements, ctx)
|
|
end
|
|
|
|
# --- Tuples ---
|
|
defp do_transform({:tuple, _meta, elements}, ctx) do
|
|
transformed = Enum.map(elements, fn e -> transform(e, ctx) end)
|
|
{{:{}, [], transformed}, ctx}
|
|
end
|
|
|
|
# --- Regex ---
|
|
defp do_transform({:regex, _meta, pattern}, ctx) do
|
|
ast =
|
|
{:sigil_r, [delimiter: "/"],
|
|
[{:<<>>, [], [pattern]}, []]}
|
|
|
|
{ast, ctx}
|
|
end
|
|
|
|
# --- Quote ---
|
|
defp do_transform({:quote, _meta, inner}, ctx) do
|
|
{quote_form(inner), ctx}
|
|
end
|
|
|
|
# --- Metadata ---
|
|
defp do_transform({:with_meta, _meta, {_metadata, target}}, ctx) do
|
|
# For now, ignore metadata and transform the target
|
|
do_transform(target, ctx)
|
|
end
|
|
|
|
# --- Anon fn shorthand #(...) ---
|
|
defp do_transform({:anon_fn, _meta, body}, ctx) do
|
|
transform_anon_fn(body, ctx)
|
|
end
|
|
|
|
# --- Quasiquote ---
|
|
defp do_transform({:quasiquote, _meta, form}, ctx) do
|
|
if ctx.in_macro do
|
|
# In macro body: produce code that constructs CljElixir AST
|
|
{gensyms, gensym_map} = collect_gensyms(form)
|
|
gensym_bindings = generate_gensym_bindings(gensyms)
|
|
qq_ast = transform_macro_quasiquote(form, ctx, gensym_map)
|
|
|
|
ast =
|
|
if gensym_bindings == [] do
|
|
qq_ast
|
|
else
|
|
{:__block__, [], gensym_bindings ++ [qq_ast]}
|
|
end
|
|
|
|
{ast, ctx}
|
|
else
|
|
# Normal mode: produce Elixir runtime data
|
|
{transform_quasiquote(form, ctx), ctx}
|
|
end
|
|
end
|
|
|
|
# --- Unquote ---
|
|
defp do_transform({:unquote, _meta, form}, ctx) do
|
|
do_transform(form, ctx)
|
|
end
|
|
|
|
# --- Splice-unquote ---
|
|
defp do_transform({:splice_unquote, _meta, form}, ctx) do
|
|
do_transform(form, ctx)
|
|
end
|
|
|
|
# --- Deref ---
|
|
defp do_transform({:deref, _meta, form}, ctx) do
|
|
{inner, ctx} = do_transform(form, ctx)
|
|
# deref: Agent.get(ref, fn s -> s end)
|
|
ast = {{:., [], [{:__aliases__, [alias: false], [:Agent]}, :get]}, [],
|
|
[inner, {:fn, [], [{:->, [], [[{:s, [], nil}], {:s, [], nil}]}]}]}
|
|
{ast, ctx}
|
|
end
|
|
|
|
# Catch-all for unknown forms
|
|
defp do_transform(other, ctx) do
|
|
{other, ctx}
|
|
end
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Symbol transformation
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# Dynamic vars
|
|
defp transform_symbol("*self*", _meta, ctx) do
|
|
{{:self, [], []}, ctx}
|
|
end
|
|
|
|
defp transform_symbol("*node*", _meta, ctx) do
|
|
{{:node, [], []}, 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, 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, [], 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, ctx) do
|
|
{mod_ast, fun_atom} = parse_module_function(name)
|
|
# Bare qualified symbol → zero-arg call (e.g., Enum/count as value)
|
|
ast = {{:., [], [mod_ast, fun_atom]}, [], []}
|
|
{ast, ctx}
|
|
end
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# List (call) transformation — the core dispatch
|
|
# ---------------------------------------------------------------------------
|
|
|
|
defp transform_list([], _meta, ctx) do
|
|
{[], ctx}
|
|
end
|
|
|
|
defp transform_list([head | args], meta, ctx) do
|
|
case head do
|
|
# --- Special forms (symbols) ---
|
|
{:symbol, _, "defmodule"} -> transform_defmodule(args, meta, ctx)
|
|
{:symbol, _, "defn"} -> transform_defn(args, meta, ctx, :def)
|
|
{:symbol, _, "defn-"} -> transform_defn(args, meta, ctx, :defp)
|
|
{:symbol, _, "fn"} -> transform_fn(args, meta, ctx)
|
|
{:symbol, _, "let"} -> transform_let(args, meta, ctx)
|
|
{:symbol, _, "if"} -> transform_if(args, meta, ctx)
|
|
{:symbol, _, "when"} -> transform_when(args, meta, ctx)
|
|
{:symbol, _, "cond"} -> transform_cond(args, meta, ctx)
|
|
{:symbol, _, "case"} -> transform_case(args, meta, ctx)
|
|
{:symbol, _, "do"} -> transform_do(args, meta, ctx)
|
|
{:symbol, _, "loop"} -> transform_loop(args, meta, ctx)
|
|
{:symbol, _, "recur"} -> transform_recur(args, meta, ctx)
|
|
{:symbol, _, "def"} -> transform_def(args, meta, ctx)
|
|
{:symbol, _, "defprotocol"} -> transform_defprotocol(args, meta, ctx)
|
|
{:symbol, _, "defrecord"} -> transform_defrecord(args, meta, ctx)
|
|
{:symbol, _, "extend-type"} -> transform_extend_type(args, meta, ctx)
|
|
{:symbol, _, "extend-protocol"} -> transform_extend_protocol(args, meta, ctx)
|
|
{:symbol, _, "reify"} -> transform_reify(args, meta, ctx)
|
|
{:symbol, _, "with"} -> transform_with(args, meta, ctx)
|
|
{:symbol, _, "receive"} -> transform_receive(args, meta, ctx)
|
|
{:symbol, _, "monitor"} -> transform_monitor(args, meta, ctx)
|
|
{:symbol, _, "link"} -> transform_link(args, meta, ctx)
|
|
{:symbol, _, "unlink"} -> transform_unlink(args, meta, ctx)
|
|
{:symbol, _, "alive?"} -> transform_alive?(args, meta, ctx)
|
|
{:symbol, _, "for"} -> transform_for(args, meta, ctx)
|
|
{:symbol, _, "doseq"} -> transform_doseq(args, meta, ctx)
|
|
{:symbol, _, "if-let"} -> transform_if_let(args, meta, ctx)
|
|
{:symbol, _, "when-let"} -> transform_when_let(args, meta, ctx)
|
|
{:symbol, _, "if-some"} -> transform_if_some(args, meta, ctx)
|
|
{:symbol, _, "when-some"} -> transform_when_some(args, meta, ctx)
|
|
{:symbol, _, "use"} -> transform_use(args, meta, ctx)
|
|
{:symbol, _, "require"} -> transform_require(args, meta, ctx)
|
|
{:symbol, _, "import"} -> transform_import(args, meta, ctx)
|
|
{:symbol, _, "alias"} -> transform_alias(args, meta, ctx)
|
|
{:symbol, _, "quote"} -> transform_quote_form(args, meta, ctx)
|
|
{:symbol, _, "defmacro"} -> transform_defmacro(args, meta, ctx)
|
|
|
|
# --- Phase 7: Malli schema annotations ---
|
|
{:symbol, _, "m/=>"} -> transform_schema_spec(args, meta, ctx)
|
|
|
|
# --- Operators and builtins ---
|
|
{:symbol, _, "+"} -> transform_arith(:+, args, ctx)
|
|
{:symbol, _, "-"} -> transform_arith(:-, args, ctx)
|
|
{:symbol, _, "*"} -> transform_arith(:*, args, ctx)
|
|
{:symbol, _, "/"} -> transform_arith(:/, args, ctx)
|
|
{:symbol, _, ">"} -> transform_comparison(:>, args, ctx)
|
|
{:symbol, _, "<"} -> transform_comparison(:<, args, ctx)
|
|
{:symbol, _, ">="} -> transform_comparison(:>=, args, ctx)
|
|
{:symbol, _, "<="} -> transform_comparison(:<=, args, ctx)
|
|
{:symbol, _, "="} -> transform_equality(args, ctx)
|
|
{:symbol, _, "=="} -> transform_numeric_equality(args, ctx)
|
|
{:symbol, _, "not="} -> transform_not_equal(args, ctx)
|
|
{:symbol, _, "!="} -> transform_not_equal(args, ctx)
|
|
{:symbol, _, "not"} -> transform_not(args, ctx)
|
|
{:symbol, _, "and"} -> transform_bool_op(:and, args, ctx)
|
|
{:symbol, _, "or"} -> transform_bool_op(:or, args, ctx)
|
|
{:symbol, _, "inc"} -> transform_inc(args, ctx)
|
|
{:symbol, _, "dec"} -> transform_dec(args, ctx)
|
|
{:symbol, _, "str"} -> transform_str(args, ctx)
|
|
{:symbol, _, "println"} -> transform_println(args, ctx)
|
|
{:symbol, _, "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, 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, 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, ctx)
|
|
|
|
String.starts_with?(name, "map->") ->
|
|
transform_map_constructor(name, args, ctx)
|
|
|
|
true ->
|
|
transform_unqualified_call(name, args, ctx)
|
|
end
|
|
end
|
|
end
|
|
|
|
# If head is itself a list (e.g., ((fn [x] x) 42)), transform and call
|
|
_ ->
|
|
{head_ast, ctx} = do_transform(head, ctx)
|
|
t_args = Enum.map(args, fn a -> transform(a, ctx) end)
|
|
{{:., [], [head_ast]} |> then(fn dot -> {dot, [], t_args} end), ctx}
|
|
end
|
|
end
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 1. defmodule
|
|
# ---------------------------------------------------------------------------
|
|
|
|
defp transform_defmodule(args, _meta, ctx) do
|
|
{name_form, rest} = extract_name(args)
|
|
mod_alias = module_name_ast(name_form)
|
|
|
|
# Check for docstring
|
|
{moduledoc, body_forms} =
|
|
case rest do
|
|
[doc | body] when is_binary(doc) ->
|
|
{doc, body}
|
|
|
|
_ ->
|
|
{nil, rest}
|
|
end
|
|
|
|
new_ctx = %{ctx | module_name: mod_alias}
|
|
|
|
{body_asts, _final_ctx} =
|
|
Enum.map_reduce(body_forms, new_ctx, fn form, acc ->
|
|
{ast, new_acc} = transform_form(form, acc)
|
|
{ast, new_acc}
|
|
end)
|
|
|
|
# Filter out nil (from defmacro which produces no runtime code)
|
|
body_asts = Enum.filter(body_asts, &(&1 != nil))
|
|
|
|
doc_ast =
|
|
if moduledoc do
|
|
[{:@, [], [{:moduledoc, [], [moduledoc]}]}]
|
|
else
|
|
[]
|
|
end
|
|
|
|
inner = doc_ast ++ body_asts
|
|
|
|
block =
|
|
case inner do
|
|
[single] -> single
|
|
multiple -> {:__block__, [], multiple}
|
|
end
|
|
|
|
ast =
|
|
{:defmodule, [context: Elixir],
|
|
[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
|
|
|
|
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, [], [call_with_args(fun_name, param_asts), [do: body_ast]]}
|
|
|
|
guard_form ->
|
|
guard_ast = transform(guard_form, fn_ctx)
|
|
|
|
{def_kind, [],
|
|
[
|
|
{:when, [],
|
|
[call_with_args(fun_name, param_asts), guard_ast]},
|
|
[do: body_ast]
|
|
]}
|
|
end
|
|
|
|
clause
|
|
end)
|
|
|
|
result = doc_ast ++ clause_asts
|
|
|
|
ast =
|
|
case result do
|
|
[single] -> single
|
|
multiple -> {:__block__, [], multiple}
|
|
end
|
|
|
|
{ast, ctx}
|
|
end
|
|
|
|
defp parse_defn_clauses(rest) do
|
|
case rest do
|
|
# Single-arity: (defn f [params] body...)
|
|
[{:vector, _, _} = params_vec | body] ->
|
|
params = vector_elements(params_vec)
|
|
{required, rest_param} = split_rest_params(params)
|
|
[{required, rest_param, nil, body}]
|
|
|
|
# Multi-clause: (defn f ([params1] body1) ([params2] body2) ...)
|
|
clauses when is_list(clauses) ->
|
|
Enum.map(clauses, fn
|
|
{:list, _, [{:vector, _, params} | body]} ->
|
|
{required, rest_param} = split_rest_params(params)
|
|
{required, rest_param, nil, body}
|
|
|
|
{:list, _, clause_elements} ->
|
|
# Might have guard: ([params] :when guard body)
|
|
parse_clause_with_guard(clause_elements)
|
|
end)
|
|
end
|
|
end
|
|
|
|
defp parse_clause_with_guard([{:vector, _, params}, :when, guard | body]) do
|
|
{required, rest_param} = split_rest_params(params)
|
|
{required, rest_param, guard, body}
|
|
end
|
|
|
|
defp parse_clause_with_guard([{:vector, _, params} | body]) do
|
|
{required, rest_param} = split_rest_params(params)
|
|
{required, rest_param, nil, body}
|
|
end
|
|
|
|
# Split a param list at `&` into {required_params, rest_param | nil}
|
|
# e.g. [a b & rest] → {[a, b], rest_symbol}
|
|
defp split_rest_params(params) do
|
|
case Enum.split_while(params, fn
|
|
{:symbol, _, "&"} -> false
|
|
_ -> true
|
|
end) do
|
|
{required, [{:symbol, _, "&"}, rest_param]} ->
|
|
{required, rest_param}
|
|
|
|
{required, [{:symbol, _, "&"}, rest_param | _extra]} ->
|
|
{required, rest_param}
|
|
|
|
{all, []} ->
|
|
{all, nil}
|
|
end
|
|
end
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 3. fn
|
|
# ---------------------------------------------------------------------------
|
|
|
|
defp transform_fn(args, _meta, ctx) do
|
|
clauses = parse_fn_clauses(args)
|
|
|
|
fn_clauses =
|
|
Enum.flat_map(clauses, fn {params, rest_param, guard, body_forms} ->
|
|
pattern_ctx = %{ctx | in_pattern: true}
|
|
param_asts = Enum.map(params, fn p -> transform(p, pattern_ctx) end)
|
|
body_ast = transform_body(body_forms, ctx)
|
|
|
|
# Build the param list, adding rest param as a regular parameter if present
|
|
all_param_asts =
|
|
case rest_param do
|
|
nil -> param_asts
|
|
rest_sym ->
|
|
rest_var_ast = transform(rest_sym, pattern_ctx)
|
|
param_asts ++ [rest_var_ast]
|
|
end
|
|
|
|
clause =
|
|
case guard do
|
|
nil ->
|
|
{:->, [], [all_param_asts, body_ast]}
|
|
|
|
guard_form ->
|
|
guard_ast = transform(guard_form, ctx)
|
|
guard_params = [{:when, [], all_param_asts ++ [guard_ast]}]
|
|
{:->, [], [guard_params, body_ast]}
|
|
end
|
|
|
|
[clause]
|
|
end)
|
|
|
|
{{:fn, [], fn_clauses}, ctx}
|
|
end
|
|
|
|
defp parse_fn_clauses(args) do
|
|
case args do
|
|
# Single-arity: (fn [params] body...)
|
|
[{:vector, _, params} | body] ->
|
|
{required, rest_param} = split_rest_params(params)
|
|
[{required, rest_param, nil, body}]
|
|
|
|
# Multi-arity: (fn ([p1] b1) ([p2 p3] b2) ...)
|
|
clauses ->
|
|
Enum.map(clauses, fn
|
|
{:list, _, [{:vector, _, params} | body]} ->
|
|
{required, rest_param} = split_rest_params(params)
|
|
{required, rest_param, nil, body}
|
|
|
|
{:list, _, clause_elements} ->
|
|
parse_clause_with_guard(clause_elements)
|
|
end)
|
|
end
|
|
end
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 4. #() anonymous shorthand
|
|
# ---------------------------------------------------------------------------
|
|
|
|
defp transform_anon_fn(body, ctx) do
|
|
# Walk body to find %1, %2, ... or % (= %1)
|
|
max_arg = find_max_anon_arg(body)
|
|
arity = max(max_arg, 1)
|
|
|
|
params =
|
|
Enum.map(1..arity, fn i ->
|
|
{String.to_atom("p#{i}"), [], nil}
|
|
end)
|
|
|
|
# Replace % / %1 / %2 etc in body with p1, p2 etc
|
|
transformed_body = replace_anon_args(body)
|
|
body_ast = transform(transformed_body, ctx)
|
|
|
|
fn_ast = {:fn, [], [{:->, [], [params, body_ast]}]}
|
|
{fn_ast, ctx}
|
|
end
|
|
|
|
defp find_max_anon_arg(form) do
|
|
case form do
|
|
{:symbol, _, "%"} -> 1
|
|
{:symbol, _, "%" <> rest} ->
|
|
case Integer.parse(rest) do
|
|
{n, ""} -> n
|
|
_ -> 0
|
|
end
|
|
|
|
{:list, _, elements} ->
|
|
elements |> Enum.map(&find_max_anon_arg/1) |> Enum.max(fn -> 0 end)
|
|
|
|
{:vector, _, elements} ->
|
|
elements |> Enum.map(&find_max_anon_arg/1) |> Enum.max(fn -> 0 end)
|
|
|
|
{:map, _, pairs} ->
|
|
pairs |> Enum.map(&find_max_anon_arg/1) |> Enum.max(fn -> 0 end)
|
|
|
|
{:set, _, elements} ->
|
|
elements |> Enum.map(&find_max_anon_arg/1) |> Enum.max(fn -> 0 end)
|
|
|
|
{:tuple, _, elements} ->
|
|
elements |> Enum.map(&find_max_anon_arg/1) |> Enum.max(fn -> 0 end)
|
|
|
|
_ -> 0
|
|
end
|
|
end
|
|
|
|
defp replace_anon_args(form) do
|
|
case form do
|
|
{:symbol, meta, "%"} ->
|
|
{:symbol, meta, "p1"}
|
|
|
|
{:symbol, meta, "%" <> rest} ->
|
|
case Integer.parse(rest) do
|
|
{_n, ""} -> {:symbol, meta, "p#{rest}"}
|
|
_ -> form
|
|
end
|
|
|
|
{:list, meta, elements} ->
|
|
{:list, meta, Enum.map(elements, &replace_anon_args/1)}
|
|
|
|
{:vector, meta, elements} ->
|
|
{:vector, meta, Enum.map(elements, &replace_anon_args/1)}
|
|
|
|
{:map, meta, pairs} ->
|
|
{:map, meta, Enum.map(pairs, &replace_anon_args/1)}
|
|
|
|
{:set, meta, elements} ->
|
|
{:set, meta, Enum.map(elements, &replace_anon_args/1)}
|
|
|
|
{:tuple, meta, elements} ->
|
|
{:tuple, meta, Enum.map(elements, &replace_anon_args/1)}
|
|
|
|
other -> other
|
|
end
|
|
end
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 5. let
|
|
# ---------------------------------------------------------------------------
|
|
|
|
defp transform_let([{:vector, _, bindings} | body], _meta, ctx) do
|
|
binding_pairs = Enum.chunk_every(bindings, 2)
|
|
|
|
{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 = {:=, [], [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__, [], 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, [], [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, [], [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, [], [[do: clauses]]}
|
|
{ast, ctx}
|
|
end
|
|
|
|
defp transform_case([val | clauses], _meta, ctx) do
|
|
val_ast = transform(val, ctx)
|
|
pattern_ctx = %{ctx | in_pattern: true}
|
|
|
|
case_clauses =
|
|
clauses
|
|
|> Enum.chunk_every(2)
|
|
|> Enum.map(fn
|
|
[pattern, :when | rest] ->
|
|
# pattern :when guard body — need to re-chunk
|
|
# This won't happen with chunk_every(2), handle differently
|
|
pat_ast = transform(pattern, pattern_ctx)
|
|
body_ast = transform(List.last(rest), ctx)
|
|
{:->, [], [[pat_ast], body_ast]}
|
|
|
|
[pattern, body] ->
|
|
pat_ast = transform(pattern, pattern_ctx)
|
|
body_ast = transform(body, ctx)
|
|
{:->, [], [[pat_ast], body_ast]}
|
|
end)
|
|
|
|
ast = {:case, [], [val_ast, [do: case_clauses]]}
|
|
{ast, ctx}
|
|
end
|
|
|
|
defp transform_do(exprs, _meta, ctx) do
|
|
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)
|
|
|
|
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, [], [{:->, [], [fn_params, body_ast]}]}
|
|
|
|
assign = {:=, [], [loop_var, fn_ast]}
|
|
invoke = {{:., [], [loop_var]}, [], [loop_var | init_vals]}
|
|
|
|
ast = {:__block__, [], [assign, invoke]}
|
|
{ast, ctx}
|
|
end
|
|
|
|
defp transform_recur(args, _meta, 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)
|
|
|
|
# Check if value looks like a schema definition
|
|
case detect_schema(value) do
|
|
nil ->
|
|
val_ast = transform(value, ctx)
|
|
def_ast = {:def, [], [{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, [], [{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)
|
|
ast = {:def, [], [{fun_name, [], []}, [do: nil]]}
|
|
{ast, ctx}
|
|
end
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 9. Module/function calls (FFI)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
defp transform_module_call(name, args, ctx) do
|
|
{mod_ast, fun_atom} = parse_module_function(name)
|
|
t_args = Enum.map(args, fn a -> transform(a, ctx) end)
|
|
ast = {{:., [], [mod_ast, fun_atom]}, [], 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, 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, [], 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, 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], [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], [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, 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, 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, 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, [], with_clauses ++ [opts]}
|
|
{ast, ctx}
|
|
end
|
|
|
|
defp split_with_else(forms) do
|
|
case Enum.split_while(forms, fn f -> f != :else end) do
|
|
{body, [:else | else_clauses]} -> {body, else_clauses}
|
|
{body, []} -> {body, []}
|
|
end
|
|
end
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 18. receive
|
|
# ---------------------------------------------------------------------------
|
|
|
|
defp transform_receive(clauses, _meta, ctx) do
|
|
pattern_ctx = %{ctx | in_pattern: true}
|
|
|
|
{case_clauses, after_clause} = parse_receive_clauses(clauses)
|
|
|
|
transformed_clauses =
|
|
Enum.map(case_clauses, fn
|
|
{pattern, guard, body} ->
|
|
pat_ast = transform(pattern, pattern_ctx)
|
|
body_ast = transform(body, ctx)
|
|
|
|
case guard do
|
|
nil ->
|
|
{:->, [], [[pat_ast], body_ast]}
|
|
|
|
guard_form ->
|
|
guard_ast = transform(guard_form, ctx)
|
|
{:->, [], [[{:when, [], [pat_ast, guard_ast]}], body_ast]}
|
|
end
|
|
end)
|
|
|
|
opts = [do: transformed_clauses]
|
|
|
|
opts =
|
|
case after_clause do
|
|
nil ->
|
|
opts
|
|
|
|
{timeout, body} ->
|
|
timeout_ast = transform(timeout, ctx)
|
|
body_ast = transform(body, ctx)
|
|
opts ++ [after: [{:->, [], [[timeout_ast], body_ast]}]]
|
|
end
|
|
|
|
ast = {:receive, [], [opts]}
|
|
{ast, ctx}
|
|
end
|
|
|
|
defp parse_receive_clauses(forms) do
|
|
parse_receive_clauses(forms, [], nil)
|
|
end
|
|
|
|
defp parse_receive_clauses([], acc, after_clause) do
|
|
{Enum.reverse(acc), after_clause}
|
|
end
|
|
|
|
defp parse_receive_clauses([:after, timeout, body | rest], acc, _after) do
|
|
parse_receive_clauses(rest, acc, {timeout, body})
|
|
end
|
|
|
|
defp parse_receive_clauses([pattern, :when, guard, body | rest], acc, after_clause) do
|
|
parse_receive_clauses(rest, [{pattern, guard, body} | acc], after_clause)
|
|
end
|
|
|
|
defp parse_receive_clauses([pattern, body | rest], acc, after_clause) do
|
|
parse_receive_clauses(rest, [{pattern, nil, body} | acc], after_clause)
|
|
end
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 18b. Process primitives: monitor, link, unlink, alive?
|
|
# ---------------------------------------------------------------------------
|
|
|
|
defp process_mod_ast do
|
|
{:__aliases__, [alias: false], [:Process]}
|
|
end
|
|
|
|
# (monitor pid) → Process.monitor(pid)
|
|
# (monitor :process pid) → :erlang.monitor(:process, pid)
|
|
defp transform_monitor([pid], _meta, ctx) do
|
|
pid_ast = transform(pid, ctx)
|
|
ast = {{:., [], [process_mod_ast(), :monitor]}, [], [pid_ast]}
|
|
{ast, ctx}
|
|
end
|
|
|
|
defp transform_monitor([type, pid], _meta, ctx) do
|
|
type_ast = transform(type, ctx)
|
|
pid_ast = transform(pid, ctx)
|
|
ast = {{:., [], [:erlang, :monitor]}, [], [type_ast, pid_ast]}
|
|
{ast, ctx}
|
|
end
|
|
|
|
# (link pid) → Process.link(pid)
|
|
defp transform_link([pid], _meta, ctx) do
|
|
pid_ast = transform(pid, ctx)
|
|
ast = {{:., [], [process_mod_ast(), :link]}, [], [pid_ast]}
|
|
{ast, ctx}
|
|
end
|
|
|
|
# (unlink pid) → Process.unlink(pid)
|
|
defp transform_unlink([pid], _meta, ctx) do
|
|
pid_ast = transform(pid, ctx)
|
|
ast = {{:., [], [process_mod_ast(), :unlink]}, [], [pid_ast]}
|
|
{ast, ctx}
|
|
end
|
|
|
|
# (alive? pid) → Process.alive?(pid)
|
|
defp transform_alive?([pid], _meta, ctx) do
|
|
pid_ast = transform(pid, ctx)
|
|
ast = {{:., [], [process_mod_ast(), :alive?]}, [], [pid_ast]}
|
|
{ast, ctx}
|
|
end
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 19. for / doseq
|
|
# ---------------------------------------------------------------------------
|
|
|
|
defp transform_for([{:vector, _, bindings} | body], _meta, ctx) do
|
|
{generators, filters} = parse_comprehension_bindings(bindings, ctx)
|
|
body_ast = transform_body(body, ctx)
|
|
|
|
args = generators ++ filters ++ [[do: body_ast]]
|
|
ast = {:for, [], 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, [], args}
|
|
{ast, ctx}
|
|
end
|
|
|
|
defp parse_comprehension_bindings(bindings, ctx) do
|
|
parse_comp_bindings(bindings, ctx, [], [])
|
|
end
|
|
|
|
defp parse_comp_bindings([], _ctx, gens, filters) do
|
|
{Enum.reverse(gens), Enum.reverse(filters)}
|
|
end
|
|
|
|
defp parse_comp_bindings([:when, pred | rest], ctx, gens, filters) do
|
|
pred_ast = transform(pred, ctx)
|
|
parse_comp_bindings(rest, ctx, gens, [pred_ast | filters])
|
|
end
|
|
|
|
defp parse_comp_bindings([:let, {:vector, _, let_bindings} | rest], ctx, gens, filters) do
|
|
# :let bindings in for comprehension
|
|
pairs = Enum.chunk_every(let_bindings, 2)
|
|
let_asts = Enum.map(pairs, fn [p, e] ->
|
|
pattern_ctx = %{ctx | in_pattern: true}
|
|
{transform(p, pattern_ctx), transform(e, ctx)}
|
|
end)
|
|
parse_comp_bindings(rest, ctx, gens, filters ++ Enum.map(let_asts, fn {p, e} -> {:=, [], [p, e]} end))
|
|
end
|
|
|
|
defp parse_comp_bindings([pattern, coll | rest], ctx, gens, filters) do
|
|
pattern_ctx = %{ctx | in_pattern: true}
|
|
pat_ast = transform(pattern, pattern_ctx)
|
|
coll_ast = transform(coll, ctx)
|
|
gen = {:<-, [], [pat_ast, coll_ast]}
|
|
parse_comp_bindings(rest, ctx, [gen | gens], filters)
|
|
end
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 20. if-let / when-let / if-some / when-some
|
|
# ---------------------------------------------------------------------------
|
|
|
|
defp transform_if_let([{:vector, _, [pattern, expr]} | body], _meta, ctx) do
|
|
pattern_ctx = %{ctx | in_pattern: true}
|
|
pat_ast = transform(pattern, pattern_ctx)
|
|
expr_ast = transform(expr, ctx)
|
|
|
|
{then_form, else_form} =
|
|
case body do
|
|
[t, e] -> {t, e}
|
|
[t] -> {t, nil}
|
|
end
|
|
|
|
then_ast = transform(then_form, ctx)
|
|
else_ast = if else_form, do: transform(else_form, ctx), else: nil
|
|
|
|
# Use a temporary variable to check truthiness (not nil and not false)
|
|
temp_var = {:clje_if_let_val, [], nil}
|
|
|
|
ast =
|
|
{:__block__, [],
|
|
[
|
|
{:=, [], [temp_var, expr_ast]},
|
|
{:if, [],
|
|
[
|
|
{:and, [],
|
|
[{:not, [], [{:is_nil, [], [temp_var]}]},
|
|
{:!=, [], [temp_var, false]}]},
|
|
[
|
|
do: {:__block__, [], [{:=, [], [pat_ast, temp_var]}, then_ast]},
|
|
else: else_ast
|
|
]
|
|
]}
|
|
]}
|
|
|
|
{ast, ctx}
|
|
end
|
|
|
|
defp transform_when_let([{:vector, _, [pattern, expr]} | body], _meta, ctx) do
|
|
pattern_ctx = %{ctx | in_pattern: true}
|
|
pat_ast = transform(pattern, pattern_ctx)
|
|
expr_ast = transform(expr, ctx)
|
|
body_ast = transform_body(body, ctx)
|
|
|
|
temp_var = {:clje_when_let_val, [], nil}
|
|
|
|
ast =
|
|
{:__block__, [],
|
|
[
|
|
{:=, [], [temp_var, expr_ast]},
|
|
{:if, [],
|
|
[
|
|
{:and, [],
|
|
[{:not, [], [{:is_nil, [], [temp_var]}]},
|
|
{:!=, [], [temp_var, false]}]},
|
|
[do: {:__block__, [], [{:=, [], [pat_ast, temp_var]}, body_ast]}]
|
|
]}
|
|
]}
|
|
|
|
{ast, ctx}
|
|
end
|
|
|
|
defp transform_if_some([{:vector, _, [pattern, expr]} | body], _meta, ctx) do
|
|
pattern_ctx = %{ctx | in_pattern: true}
|
|
pat_ast = transform(pattern, pattern_ctx)
|
|
expr_ast = transform(expr, ctx)
|
|
|
|
{then_form, else_form} =
|
|
case body do
|
|
[t, e] -> {t, e}
|
|
[t] -> {t, nil}
|
|
end
|
|
|
|
then_ast = transform(then_form, ctx)
|
|
else_ast = if else_form, do: transform(else_form, ctx), else: nil
|
|
|
|
temp_var = {:clje_if_some_val, [], nil}
|
|
|
|
ast =
|
|
{:__block__, [],
|
|
[
|
|
{:=, [], [temp_var, expr_ast]},
|
|
{:if, [],
|
|
[
|
|
{:not, [], [{:is_nil, [], [temp_var]}]},
|
|
[
|
|
do: {:__block__, [], [{:=, [], [pat_ast, temp_var]}, then_ast]},
|
|
else: else_ast
|
|
]
|
|
]}
|
|
]}
|
|
|
|
{ast, ctx}
|
|
end
|
|
|
|
defp transform_when_some([{:vector, _, [pattern, expr]} | body], _meta, ctx) do
|
|
pattern_ctx = %{ctx | in_pattern: true}
|
|
pat_ast = transform(pattern, pattern_ctx)
|
|
expr_ast = transform(expr, ctx)
|
|
body_ast = transform_body(body, ctx)
|
|
|
|
temp_var = {:clje_when_some_val, [], nil}
|
|
|
|
ast =
|
|
{:__block__, [],
|
|
[
|
|
{:=, [], [temp_var, expr_ast]},
|
|
{:if, [],
|
|
[
|
|
{:not, [], [{:is_nil, [], [temp_var]}]},
|
|
[do: {:__block__, [], [{:=, [], [pat_ast, temp_var]}, body_ast]}]
|
|
]}
|
|
]}
|
|
|
|
{ast, ctx}
|
|
end
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 21. use / require / import / alias
|
|
# ---------------------------------------------------------------------------
|
|
|
|
defp transform_use(args, _meta, ctx) do
|
|
{mod_asts, opts} = parse_directive_args(args, ctx)
|
|
|
|
ast =
|
|
case opts do
|
|
[] -> {:use, [], mod_asts}
|
|
_ -> {:use, [], mod_asts ++ [opts]}
|
|
end
|
|
|
|
{ast, ctx}
|
|
end
|
|
|
|
defp transform_require(args, _meta, ctx) do
|
|
{mod_asts, opts} = parse_directive_args(args, ctx)
|
|
|
|
ast =
|
|
case opts do
|
|
[] -> {:require, [], mod_asts}
|
|
_ -> {:require, [], mod_asts ++ [opts]}
|
|
end
|
|
|
|
{ast, ctx}
|
|
end
|
|
|
|
defp transform_import(args, _meta, ctx) do
|
|
{mod_asts, opts} = parse_directive_args(args, ctx)
|
|
|
|
ast =
|
|
case opts do
|
|
[] -> {:import, [], mod_asts}
|
|
_ -> {:import, [], mod_asts ++ [opts]}
|
|
end
|
|
|
|
{ast, ctx}
|
|
end
|
|
|
|
defp transform_alias(args, _meta, ctx) do
|
|
{mod_asts, opts} = parse_directive_args(args, ctx)
|
|
|
|
ast =
|
|
case opts do
|
|
[] -> {:alias, [], mod_asts}
|
|
_ -> {:alias, [], mod_asts ++ [opts]}
|
|
end
|
|
|
|
{ast, ctx}
|
|
end
|
|
|
|
defp parse_directive_args(args, ctx) do
|
|
# First arg is a module name, rest might be options
|
|
case args do
|
|
[name_form | rest] ->
|
|
mod_ast = resolve_type_name(name_form)
|
|
opts = Enum.map(rest, fn o -> transform(o, ctx) end)
|
|
{[mod_ast], opts}
|
|
end
|
|
end
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 22. Operators and builtins
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# Arithmetic: variadic
|
|
defp transform_arith(op, args, ctx) do
|
|
t_args = Enum.map(args, fn a -> transform(a, ctx) end)
|
|
|
|
ast =
|
|
case t_args do
|
|
[single] when op == :- ->
|
|
# Unary minus
|
|
{:-, [], [0, single]}
|
|
|
|
[single] ->
|
|
single
|
|
|
|
_ ->
|
|
Enum.reduce(tl(t_args), hd(t_args), fn arg, acc ->
|
|
{op, [], [acc, arg]}
|
|
end)
|
|
end
|
|
|
|
{ast, ctx}
|
|
end
|
|
|
|
# Comparisons
|
|
defp transform_comparison(op, [a, b], ctx) do
|
|
a_ast = transform(a, ctx)
|
|
b_ast = transform(b, ctx)
|
|
{{op, [], [a_ast, b_ast]}, ctx}
|
|
end
|
|
|
|
# Equality: = maps to CljElixir.Equality.equiv for cross-type equality
|
|
defp transform_equality([a, b], ctx) do
|
|
a_ast = transform(a, ctx)
|
|
b_ast = transform(b, ctx)
|
|
equiv_mod = {:__aliases__, [alias: false], [:CljElixir, :Equality]}
|
|
ast = {{:., [], [equiv_mod, :equiv]}, [], [a_ast, b_ast]}
|
|
{ast, ctx}
|
|
end
|
|
|
|
# Numeric equality: == maps to ==
|
|
defp transform_numeric_equality([a, b], ctx) do
|
|
a_ast = transform(a, ctx)
|
|
b_ast = transform(b, ctx)
|
|
{{:==, [], [a_ast, b_ast]}, ctx}
|
|
end
|
|
|
|
# not=, !=
|
|
defp transform_not_equal([a, b], ctx) do
|
|
a_ast = transform(a, ctx)
|
|
b_ast = transform(b, ctx)
|
|
equiv_mod = {:__aliases__, [alias: false], [:CljElixir, :Equality]}
|
|
equiv_call = {{:., [], [equiv_mod, :equiv]}, [], [a_ast, b_ast]}
|
|
{{:not, [], [equiv_call]}, ctx}
|
|
end
|
|
|
|
# not
|
|
defp transform_not([a], ctx) do
|
|
a_ast = transform(a, ctx)
|
|
{{:not, [], [a_ast]}, ctx}
|
|
end
|
|
|
|
# and, or (variadic)
|
|
defp transform_bool_op(op, args, ctx) do
|
|
t_args = Enum.map(args, fn a -> transform(a, ctx) end)
|
|
|
|
ast =
|
|
Enum.reduce(tl(t_args), hd(t_args), fn arg, acc ->
|
|
{op, [], [acc, arg]}
|
|
end)
|
|
|
|
{ast, ctx}
|
|
end
|
|
|
|
# inc
|
|
defp transform_inc([a], ctx) do
|
|
a_ast = transform(a, ctx)
|
|
{{:+, [], [a_ast, 1]}, ctx}
|
|
end
|
|
|
|
# dec
|
|
defp transform_dec([a], ctx) do
|
|
a_ast = transform(a, ctx)
|
|
{{:-, [], [a_ast, 1]}, ctx}
|
|
end
|
|
|
|
# str — concatenate with <> using to_string
|
|
defp transform_str(args, ctx) do
|
|
t_args = Enum.map(args, fn a -> transform(a, ctx) end)
|
|
|
|
ast =
|
|
case t_args do
|
|
[] ->
|
|
""
|
|
|
|
[single] ->
|
|
{{:., [], [{:__aliases__, [alias: false], [:Kernel]}, :to_string]}, [], [single]}
|
|
|
|
_ ->
|
|
stringified =
|
|
Enum.map(t_args, fn a ->
|
|
{{:., [], [{:__aliases__, [alias: false], [:Kernel]}, :to_string]}, [], [a]}
|
|
end)
|
|
|
|
Enum.reduce(tl(stringified), hd(stringified), fn arg, acc ->
|
|
{:<>, [], [acc, arg]}
|
|
end)
|
|
end
|
|
|
|
{ast, ctx}
|
|
end
|
|
|
|
# println → IO.puts
|
|
defp transform_println(args, ctx) do
|
|
t_args = Enum.map(args, fn a -> transform(a, ctx) end)
|
|
|
|
# If multiple args, join with str first
|
|
ast =
|
|
case t_args do
|
|
[single] ->
|
|
{{:., [], [{:__aliases__, [alias: false], [:IO]}, :puts]}, [], [single]}
|
|
|
|
_ ->
|
|
# Concatenate all args as strings first
|
|
{str_ast, _} = transform_str(args, ctx)
|
|
{{:., [], [{:__aliases__, [alias: false], [:IO]}, :puts]}, [], [str_ast]}
|
|
end
|
|
|
|
{ast, ctx}
|
|
end
|
|
|
|
# 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, 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)
|
|
|
|
# Call Module.new(args...)
|
|
ast = {{:., [], [mod_alias, :new]}, [], t_args}
|
|
{ast, ctx}
|
|
end
|
|
|
|
# Map constructor: (map->Name {:field val ...})
|
|
defp transform_map_constructor(name, args, 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)
|
|
|
|
# Use Kernel.struct!/2
|
|
ast = {{:., [], [{:__aliases__, [alias: false], [:Kernel]}, :struct!]}, [], [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
|
|
|
|
# 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, [], [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
|
|
end
|