Current section

Files

Jump to
metastatic lib metastatic.ex
Raw

lib/metastatic.ex

defmodule Metastatic do
@moduledoc """
Metastatic - Cross-language code analysis via unified MetaAST.
## Main API
The primary entry points for working with Metastatic:
- `quote/2` - Convert source code to MetaAST
- `unquote/2` - Convert MetaAST back to source code
- `prewalk/2`, `prewalk/3` - Pre-order AST traversal
- `postwalk/2`, `postwalk/3` - Post-order AST traversal
- `traverse/4` - Full pre+post AST traversal with accumulator
- `path/2` - Find path from root to matching node
- `to_string/1` - Human-readable AST representation
- `decompose_call/1` - Decompose function call nodes
- `validate/1` - Validate AST structure with diagnostics
- `unpipe/1` - Decompose pipe chains
- `pipe_into/3` - Inject expression into function call
- `literal?/1` - Check if AST is purely literal
- `operator?/1` - Check if node is an operator
- `prewalker/1`, `postwalker/1` - Lazy enumerable traversals
- `unique_var/1` - Generate unique variable nodes
## Examples
# Parse Python code to MetaAST
iex> {:ok, {:binary_op, meta, _}} = Metastatic.quote("x + 5", :python)
iex> Keyword.get(meta, :category) == :arithmetic and Keyword.get(meta, :operator) == :+
true
# Convert MetaAST back to Python source
iex> ast = {:binary_op, [category: :arithmetic, operator: :+], [{:variable, [], "x"}, {:literal, [subtype: :integer], 5}]}
iex> Metastatic.unquote(ast, :python)
{:ok, "x + 5"}
# Round-trip demonstration
iex> {:ok, my_ast} = Metastatic.quote("x + 5", :python)
iex> {:ok, source} = Metastatic.unquote(my_ast, :python)
iex> source
"x + 5"
## Cross-Language Translation
Since MetaAST is language-independent, you can parse in one language
and generate in another:
# Parse from Python, generate Elixir code (same MetaAST!)
iex> {:ok, py_ast} = Metastatic.quote("x + 5", :python)
iex> {:ok, _source} = Metastatic.unquote(py_ast, :elixir)
iex> true
true
"""
alias Metastatic.{AST, Builder, Document}
@type language :: :elixir | :erlang | :ruby | :haskell | :python
@type meta_ast :: AST.meta_ast()
@languages ~w|elixir erlang ruby haskell python|a
@doc false
def languages, do: @languages
@doc false
def supported?(lang) when lang in @languages, do: true
def supported?(_), do: false
@doc false
def adapter_for_language(language)
for lang <- @languages do
mod = Module.concat([Metastatic.Adapters, lang |> Atom.to_string() |> Macro.camelize()])
def adapter_for_language(unquote(lang)), do: {:ok, unquote(mod)}
end
def adapter_for_language(lang),
do: {:error, {:unsupported_language, "No adapter found for language: #{inspect(lang)}"}}
@doc """
Convert source code to MetaAST.
Performs Source → M1 → M2 transformation, returning just the MetaAST
without the Document wrapper.
## Parameters
- `code` - Source code string
- `language` - Language atom (`:python`, `:elixir`, `:ruby`, `:erlang`, `:haskell`)
## Returns
- `{:ok, meta_ast}` - Successfully parsed and abstracted to M2
- `{:error, reason}` - Parsing or abstraction failed
## Examples
iex> {:ok, {:binary_op, meta, _}} = Metastatic.quote("x + 5", :python)
iex> Keyword.get(meta, :category) == :arithmetic and Keyword.get(meta, :operator) == :+
true
iex> {:ok, {:binary_op, meta, _}} = Metastatic.quote("x + 5", :elixir)
iex> Keyword.get(meta, :category) == :arithmetic and Keyword.get(meta, :operator) == :+
true
iex> {:ok, {:list, _, items}} = Metastatic.quote("[1, 2, 3]", :python)
iex> length(items) == 3
true
"""
@spec quote(String.t(), language()) :: {:ok, meta_ast()} | {:error, term()}
def quote(code, language) when is_binary(code) and is_atom(language) do
with {:ok, %Document{ast: ast}} <- Builder.from_source(code, language) do
{:ok, ast}
end
end
@doc """
Convert MetaAST to source code.
Performs M2 → M1 → Source transformation, generating source code in the
specified target language.
## Parameters
- `ast` - MetaAST structure (M2 level)
- `language` - Target language atom (`:python`, `:elixir`, `:ruby`, `:erlang`, `:haskell`)
## Returns
- `{:ok, source}` - Successfully generated source code
- `{:error, reason}` - Generation failed
## Examples
iex> ast = {:binary_op, [category: :arithmetic, operator: :+], [{:variable, [], "x"}, {:literal, [subtype: :integer], 5}]}
iex> Metastatic.unquote(ast, :python)
{:ok, "x + 5"}
iex> ast = {:binary_op, [category: :arithmetic, operator: :+], [{:variable, [], "x"}, {:literal, [subtype: :integer], 5}]}
iex> Metastatic.unquote(ast, :elixir)
{:ok, "x + 5"}
iex> ast = {:literal, [subtype: :integer], 42}
iex> Metastatic.unquote(ast, :python)
{:ok, "42"}
iex> ast = {:list, [], [{:literal, [subtype: :integer], 1}, {:literal, [subtype: :integer], 2}]}
iex> Metastatic.unquote(ast, :python)
{:ok, "[1, 2]"}
## Cross-Language Translation
Since MetaAST is language-independent, you can parse from one language
and generate in another:
iex> {:ok, ast} = Metastatic.quote("x + 5", :python)
iex> {:ok, source} = Metastatic.unquote(ast, :elixir)
iex> source
"x + 5"
iex> {:ok, ast} = Metastatic.quote("42", :ruby)
iex> {:ok, source} = Metastatic.unquote(ast, :python)
iex> source
"42"
"""
@spec unquote(meta_ast(), language()) :: {:ok, String.t()} | {:error, term()}
def unquote(ast, language) when is_atom(language) do
# Create minimal document wrapper (language will be used for target)
doc = %Document{ast: ast, language: language, metadata: %{}, original_source: nil}
Builder.to_source(doc)
end
# ----- Traversal (mirrors Macro.traverse/4, Macro.prewalk, Macro.postwalk) -----
@doc """
Traverse a MetaAST, applying pre and post functions.
Delegates to `Metastatic.AST.traverse/4`. See that module for full documentation.
## Examples
iex> ast = {:binary_op, [category: :arithmetic, operator: :+],
...> [{:literal, [subtype: :integer], 1}, {:literal, [subtype: :integer], 2}]}
iex> {_ast, count} = Metastatic.traverse(ast, 0, fn node, acc -> {node, acc + 1} end, fn node, acc -> {node, acc} end)
iex> count
3
"""
defdelegate traverse(ast, acc, pre, post), to: AST
@doc """
Performs a depth-first, pre-order traversal of the MetaAST (no accumulator).
Delegates to `Metastatic.AST.prewalk/2`.
## Examples
iex> ast = {:binary_op, [category: :arithmetic, operator: :+],
...> [{:literal, [subtype: :integer], 1}, {:literal, [subtype: :integer], 2}]}
iex> Metastatic.prewalk(ast, fn
...> {:literal, meta, value} when is_integer(value) -> {:literal, meta, value * 10}
...> node -> node
...> end)
{:binary_op, [category: :arithmetic, operator: :+],
[{:literal, [subtype: :integer], 10}, {:literal, [subtype: :integer], 20}]}
"""
defdelegate prewalk(ast, fun), to: AST
@doc """
Performs a depth-first, pre-order traversal with accumulator.
Delegates to `Metastatic.AST.prewalk/3`.
"""
defdelegate prewalk(ast, acc, fun), to: AST
@doc """
Performs a depth-first, post-order traversal of the MetaAST (no accumulator).
Delegates to `Metastatic.AST.postwalk/2`.
## Examples
iex> ast = {:binary_op, [category: :arithmetic, operator: :+],
...> [{:literal, [subtype: :integer], 1}, {:literal, [subtype: :integer], 2}]}
iex> Metastatic.postwalk(ast, fn
...> {:literal, meta, value} when is_integer(value) -> {:literal, meta, value * 10}
...> node -> node
...> end)
{:binary_op, [category: :arithmetic, operator: :+],
[{:literal, [subtype: :integer], 10}, {:literal, [subtype: :integer], 20}]}
"""
defdelegate postwalk(ast, fun), to: AST
@doc """
Performs a depth-first, post-order traversal with accumulator.
Delegates to `Metastatic.AST.postwalk/3`.
"""
defdelegate postwalk(ast, acc, fun), to: AST
# ----- Enumerable Walkers (mirrors Macro.prewalker/1, Macro.postwalker/1) -----
@doc """
Returns an enumerable that lazily traverses the MetaAST in pre-order.
Delegates to `Metastatic.AST.prewalker/1`.
## Examples
iex> ast = {:binary_op, [category: :arithmetic, operator: :+],
...> [{:literal, [subtype: :integer], 1}, {:literal, [subtype: :integer], 2}]}
iex> ast |> Metastatic.prewalker() |> Enum.map(&Metastatic.AST.type/1)
[:binary_op, :literal, :literal]
"""
defdelegate prewalker(ast), to: AST
@doc """
Returns an enumerable that lazily traverses the MetaAST in post-order.
Delegates to `Metastatic.AST.postwalker/1`.
## Examples
iex> ast = {:binary_op, [category: :arithmetic, operator: :+],
...> [{:literal, [subtype: :integer], 1}, {:literal, [subtype: :integer], 2}]}
iex> ast |> Metastatic.postwalker() |> Enum.map(&Metastatic.AST.type/1)
[:literal, :literal, :binary_op]
"""
defdelegate postwalker(ast), to: AST
# ----- Path (mirrors Macro.path/2) -----
@doc """
Returns the path to the node for which `fun` returns a truthy value.
The path is a list starting with the matched node, followed by its
parents up to the root. Returns `nil` if no match found.
Delegates to `Metastatic.AST.path/2`.
## Examples
iex> ast = {:binary_op, [category: :arithmetic, operator: :+],
...> [{:variable, [], "x"}, {:literal, [subtype: :integer], 42}]}
iex> path = Metastatic.path(ast, fn
...> {:literal, _, 42} -> true
...> _ -> false
...> end)
iex> Enum.map(path, &Metastatic.AST.type/1)
[:literal, :binary_op]
"""
defdelegate path(ast, fun), to: AST
# ----- Pipe Utilities (mirrors Macro.unpipe/1, Macro.pipe/3) -----
@doc """
Breaks a pipe chain into a flat list of `{ast, position}` tuples.
Delegates to `Metastatic.AST.unpipe/1`.
"""
defdelegate unpipe(ast), to: AST
@doc """
Pipes `expr` into a function call at the given position.
Delegates to `Metastatic.AST.pipe_into/3`.
## Examples
iex> expr = {:variable, [], "x"}
iex> call = {:function_call, [name: "foo"], [{:literal, [subtype: :integer], 1}]}
iex> Metastatic.pipe_into(expr, call, 0)
{:function_call, [name: "foo"], [{:variable, [], "x"}, {:literal, [subtype: :integer], 1}]}
"""
defdelegate pipe_into(expr, call, position), to: AST
# ----- Call Decomposition (mirrors Macro.decompose_call/1) -----
@doc """
Decomposes a function call into `{name, args}` or `:error`.
Delegates to `Metastatic.AST.decompose_call/1`.
## Examples
iex> call = {:function_call, [name: "add"], [{:variable, [], "x"}, {:variable, [], "y"}]}
iex> Metastatic.decompose_call(call)
{"add", [{:variable, [], "x"}, {:variable, [], "y"}]}
iex> Metastatic.decompose_call({:literal, [subtype: :integer], 42})
:error
"""
defdelegate decompose_call(ast), to: AST
# ----- String Representation (mirrors Macro.to_string/1) -----
@doc """
Converts a MetaAST to a human-readable string.
Delegates to `Metastatic.AST.to_string/1`.
## Examples
iex> ast = {:binary_op, [category: :arithmetic, operator: :+],
...> [{:variable, [], "x"}, {:literal, [subtype: :integer], 5}]}
iex> Metastatic.to_string(ast)
"x + 5"
"""
defdelegate to_string(ast), to: AST
# ----- Predicates (mirrors Macro.quoted_literal?/1, Macro.operator?/2) -----
@doc """
Returns `true` if the MetaAST represents a purely literal value.
Delegates to `Metastatic.AST.literal?/1`.
## Examples
iex> Metastatic.literal?({:literal, [subtype: :integer], 42})
true
iex> Metastatic.literal?({:variable, [], "x"})
false
"""
defdelegate literal?(ast), to: AST
@doc """
Returns `true` if the node is a binary or unary operator.
Delegates to `Metastatic.AST.operator?/1`.
## Examples
iex> Metastatic.operator?({:binary_op, [category: :arithmetic, operator: :+], [{:variable, [], "x"}, {:literal, [subtype: :integer], 1}]})
true
iex> Metastatic.operator?({:literal, [subtype: :integer], 42})
false
"""
defdelegate operator?(ast), to: AST
# ----- Validation (mirrors Macro.validate/1) -----
@doc """
Validates AST structure, returning `:ok` or `{:error, reason}`.
Delegates to `Metastatic.AST.validate/1`.
## Examples
iex> Metastatic.validate({:literal, [subtype: :integer], 42})
:ok
iex> Metastatic.validate("not an ast")
{:error, {:not_an_ast_node, "not an ast"}}
"""
defdelegate validate(ast), to: AST
# ----- Unique Variables (mirrors Macro.unique_var/2) -----
@doc """
Generates a unique variable MetaAST node.
Delegates to `Metastatic.AST.unique_var/1`.
## Examples
iex> {:variable, _, name} = Metastatic.unique_var("tmp")
iex> String.starts_with?(name, "tmp_")
true
"""
defdelegate unique_var(prefix), to: AST
end