Packages
metastatic
0.21.1
0.26.0
0.25.0
0.24.1
0.24.0
0.23.0
0.22.2
0.22.1
0.22.0
0.21.3
0.21.2
0.21.1
0.21.0
0.20.3
0.20.2
0.20.1
0.20.0
0.19.0
0.18.0
0.17.0
0.16.0
0.15.1
0.15.0
0.14.2
0.14.1
0.14.0
0.13.3
0.13.2
0.13.1
0.13.0
0.12.0
0.11.0
0.10.4
0.10.3
0.10.2
0.10.1
0.10.0
0.9.2
0.9.1
0.9.0
0.8.6
0.8.5
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.7
0.7.6
0.7.5
0.7.4
0.7.3
0.7.1
0.7.0
0.6.1
0.6.0
0.5.2
0.5.1
0.5.0
0.4.2
0.4.1
0.4.0
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.0
0.1.3
0.1.2
0.1.1
0.1.0
Cross-language code meta-model library using unified MetaAST representation. Parse, transform, and translate code across Python, Elixir, Ruby, Erlang, Haskell, and more via a shared three-tuple AST format.
Current section
Files
Jump to
Current section
Files
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