Current section

Files

Jump to
eyeon lib eyeon macro_expander.ex
Raw

lib/eyeon/macro_expander.ex

defmodule Eyeon.MacroExpander do
@moduledoc false
alias Eyeon.Escape
@spec expand(term()) :: {:ok, term()} | {:error, String.t()}
def expand(ast) when is_list(ast) do
case expand_top_level(ast) do
{:error, _} = err -> err
expanded -> {:ok, expanded}
end
end
def expand(other), do: {:ok, other}
# Top-level: list of AST nodes, flatten compound tuples then expand
defp expand_top_level(nodes) do
flat = Enum.flat_map(nodes, &flatten_compound/1)
Enum.reduce_while(flat, [], fn node, acc ->
case expand_node(node) do
{:error, _} = err -> {:halt, err}
{:splice, values} -> {:cont, acc ++ values}
expanded -> {:cont, acc ++ [expanded]}
end
end)
end
# Flatten compound 2-tuples from parser shift/reduce conflicts
defp flatten_compound({first, second})
when is_tuple(first) and tuple_size(first) == 3 do
case first do
{tag, line, _} when is_atom(tag) and is_integer(line) ->
flatten_compound(first) ++ flatten_compound(second)
_ ->
[{first, second}]
end
end
defp flatten_compound(node), do: [node]
# Macro invocation node
defp expand_node({:macro, name_token, args}) do
name = extract_macro_name(name_token)
case expand_args(args) do
{:error, _} = err ->
err
expanded_args ->
case dispatch_macro(name, expanded_args, name_token) do
{:error, _} = err -> err
result -> result
end
end
end
# List container
defp expand_node({:list, items}) do
case expand_and_flatten(items) do
{:error, _} = err -> err
expanded -> {:list, expanded}
end
end
# Sexp containers
defp expand_node({:sexp}) do
{:sexp}
end
defp expand_node({:sexp, values}) do
case expand_sexp_values(values) do
{:error, _} = err -> err
expanded -> {:sexp, expanded}
end
end
defp expand_node({:sexp, values, last}) do
case expand_sexp_values(values) do
{:error, _} = err ->
err
expanded_values ->
case expand_node(last) do
{:error, _} = err ->
err
{:splice, spliced} ->
{:sexp, expanded_values ++ spliced}
expanded_last ->
{:sexp, expanded_values ++ [expanded_last]}
end
end
end
# Struct container
defp expand_node({:struct, fields}) do
case expand_struct_fields(fields) do
{:error, _} = err -> err
expanded -> {:struct, expanded}
end
end
# Annotation
defp expand_node({:annotated, annotation, value}) do
case expand_node(value) do
{:error, _} = err -> err
expanded -> {:annotated, annotation, expanded}
end
end
# Everything else (terminals, etc.) passes through unchanged
defp expand_node(node), do: node
# Expand a list of macro args, returning flat list of expanded nodes
defp expand_args(args) do
Enum.reduce_while(args, [], fn arg, acc ->
case expand_node(arg) do
{:error, _} = err -> {:halt, err}
{:splice, values} -> {:cont, acc ++ values}
expanded -> {:cont, acc ++ [expanded]}
end
end)
end
# Expand items in a list/values context, flattening spliced results
defp expand_and_flatten(items) do
Enum.reduce_while(items, [], fn item, acc ->
case expand_node(item) do
{:error, _} = err -> {:halt, err}
{:splice, values} -> {:cont, acc ++ values}
expanded -> {:cont, acc ++ [expanded]}
end
end)
end
# Expand sexp values — each entry may be a list of operators, compound tuple, or single value
defp expand_sexp_values(values) do
Enum.reduce_while(values, [], fn value, acc ->
case expand_sexp_entry(value) do
{:error, _} = err -> {:halt, err}
expanded_list -> {:cont, acc ++ expanded_list}
end
end)
end
defp expand_sexp_entry(value) when is_list(value) do
# Operator list or list of tokens — expand each
case expand_and_flatten(value) do
{:error, _} = err -> err
expanded -> [expanded]
end
end
defp expand_sexp_entry({first, second}) when not is_atom(first) do
# Compound tuple from parser
case expand_node(first) do
{:error, _} = err ->
err
{:splice, values} ->
case expand_sexp_entry(second) do
{:error, _} = err -> err
rest -> Enum.map(values, &[&1]) ++ rest
end
expanded_first ->
case expand_sexp_entry(second) do
{:error, _} = err -> err
rest -> [[expanded_first] | rest]
end
end
end
defp expand_sexp_entry(value) do
case expand_node(value) do
{:error, _} = err -> err
{:splice, values} -> Enum.map(values, &[&1])
expanded -> [[expanded]]
end
end
# Expand struct fields
defp expand_struct_fields(fields) do
Enum.reduce_while(fields, [], fn field, acc ->
case expand_struct_field(field) do
{:error, _} = err -> {:halt, err}
expanded_fields -> {:cont, acc ++ expanded_fields}
end
end)
end
# Macro in field position: { (:macro_name ...) }
defp expand_struct_field({:macro_field, macro_node}) do
case expand_node(macro_node) do
{:error, _} = err ->
err
{:splice, values} ->
# Each spliced value should be a struct whose fields get merged
extract_fields_from_spliced(values)
{:struct, inner_fields} ->
inner_fields
_other ->
[]
end
end
# Regular field: {field_name, value}
defp expand_struct_field({field_name, value}) do
case expand_node(value) do
{:error, _} = err ->
err
{:splice, []} ->
# :none in field value position — remove the field
[]
{:splice, [single]} ->
# Single value from :values — use as field value
[{field_name, single}]
{:splice, multiple} ->
# Multiple values from :values in field value position — produce multiple fields with same key
Enum.map(multiple, fn v -> {field_name, v} end)
expanded ->
[{field_name, expanded}]
end
end
# Extract fields from spliced struct values
defp extract_fields_from_spliced(values) do
Enum.reduce_while(values, [], fn value, acc ->
case value do
{:struct, inner_fields} ->
{:cont, acc ++ inner_fields}
_ ->
{:halt, {:error, "values macro in struct field position must contain structs"}}
end
end)
end
# System macro dispatch
defp dispatch_macro("none", _args, _name_token) do
{:splice, []}
end
defp dispatch_macro("values", args, _name_token) do
{:splice, args}
end
defp dispatch_macro("make_string", args, name_token) do
case concat_string_args(args) do
{:error, _} = err -> err
result -> {:short_string, extract_line(name_token), String.to_charlist("\"#{result}\"")}
end
end
defp dispatch_macro(name, _args, _name_token) do
{:error, "unknown macro: #{name}"}
end
# Concatenate string/symbol args for make_string
defp concat_string_args(args) do
Enum.reduce_while(args, "", fn arg, acc ->
case extract_text(arg) do
{:error, _} = err -> {:halt, err}
text -> {:cont, acc <> text}
end
end)
end
defp extract_text({:short_string, _line, chars}) do
chars |> List.to_string() |> Escape.unescape_string()
end
defp extract_text({:long_string, _line, chars}) do
chars |> List.to_string() |> Escape.unescape_string()
end
defp extract_text({:id_symbol, _line, chars}) do
List.to_string(chars)
end
defp extract_text({:quoted_symbol, _line, chars}) do
chars |> List.to_string() |> String.trim("'") |> Escape.unescape_string()
end
defp extract_text({:type, _line, chars}) do
List.to_string(chars)
end
defp extract_text(other) do
{:error, "make_string: unsupported argument type: #{inspect(other)}"}
end
defp extract_macro_name({:id_symbol, _line, chars}), do: List.to_string(chars)
defp extract_macro_name({:type, _line, chars}), do: List.to_string(chars)
defp extract_line({_tag, line, _chars}), do: line
end