Metastatic (Metastatic v0.20.3)

View Source

Metastatic - Cross-language code analysis via unified MetaAST.

Main API

The primary entry points for working with Metastatic:

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

Summary

Functions

Decomposes a function call into {name, args} or :error.

Returns true if the MetaAST represents a purely literal value.

Returns true if the node is a binary or unary operator.

Returns the path to the node for which fun returns a truthy value.

Pipes expr into a function call at the given position.

Performs a depth-first, post-order traversal of the MetaAST (no accumulator).

Performs a depth-first, post-order traversal with accumulator.

Returns an enumerable that lazily traverses the MetaAST in post-order.

Performs a depth-first, pre-order traversal of the MetaAST (no accumulator).

Performs a depth-first, pre-order traversal with accumulator.

Returns an enumerable that lazily traverses the MetaAST in pre-order.

Convert source code to MetaAST.

Converts a MetaAST to a human-readable string.

Traverse a MetaAST, applying pre and post functions.

Generates a unique variable MetaAST node.

Breaks a pipe chain into a flat list of {ast, position} tuples.

Convert MetaAST to source code.

Validates AST structure, returning :ok or {:error, reason}.

Types

language()

@type language() :: :elixir | :erlang | :ruby | :haskell | :python

meta_ast()

@type meta_ast() :: Metastatic.AST.meta_ast()

Functions

decompose_call(ast)

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

literal?(ast)

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

operator?(ast)

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

path(ast, fun)

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]

pipe_into(expr, call, position)

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}]}

postwalk(ast, fun)

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}]}

postwalk(ast, acc, fun)

Performs a depth-first, post-order traversal with accumulator.

Delegates to Metastatic.AST.postwalk/3.

postwalker(ast)

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]

prewalk(ast, fun)

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}]}

prewalk(ast, acc, fun)

Performs a depth-first, pre-order traversal with accumulator.

Delegates to Metastatic.AST.prewalk/3.

prewalker(ast)

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]

quote(code, language)

@spec quote(String.t(), language()) :: {:ok, meta_ast()} | {:error, term()}

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

to_string(ast)

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"

traverse(ast, acc, pre, post)

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

unique_var(prefix)

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

unpipe(ast)

Breaks a pipe chain into a flat list of {ast, position} tuples.

Delegates to Metastatic.AST.unpipe/1.

unquote(ast, language)

@spec unquote(meta_ast(), language()) :: {:ok, String.t()} | {:error, term()}

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"

validate(ast)

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"}}