Packages
metastatic
0.18.0
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/document.ex
defmodule Metastatic.Document do
@moduledoc """
A MetaAST Document wraps an M2 AST with metadata and language information.
This structure represents the result of abstraction (M1 → M2) and serves
as input for reification (M2 → M1).
## Fields
- `ast` - The M2 MetaAST representation
- `metadata` - Language-specific information preserved from M1
- `language` - Source language (:python, :javascript, :elixir, etc.)
- `original_source` - Optional: original source code (for debugging/comparison)
## Metadata
Metadata contains M1-specific information that cannot be represented at M2 level:
- Formatting preferences (indentation, spacing)
- Comments and documentation
- Type annotations (TypeScript, Python type hints)
- Language-specific hints (async models, iterator styles, etc.)
This enables high-fidelity M2 → M1 round-trips while maintaining semantic equivalence.
## Examples
# After abstraction from Python (new 3-tuple format)
%Metastatic.Document{
language: :python,
ast: {:binary_op, [category: :arithmetic, operator: :+],
[{:variable, [], "x"}, {:literal, [subtype: :integer], 5}]},
metadata: %{
native_lang: :python,
type_hints: %{"x" => "int"},
formatting: %{indent: 4}
},
original_source: "x + 5"
}
"""
alias Metastatic.AST
@enforce_keys [:ast, :language]
defstruct [:ast, :metadata, :language, :original_source]
@typedoc """
A Document containing M2 AST with associated metadata.
"""
@type t :: %__MODULE__{
ast: AST.meta_ast(),
metadata: map(),
language: atom(),
original_source: String.t() | nil
}
@doc """
Create a new MetaAST document.
## Examples
iex> ast = {:literal, [subtype: :integer], 42}
iex> Metastatic.Document.new(ast, :python)
%Metastatic.Document{
ast: {:literal, [subtype: :integer], 42},
language: :python,
metadata: %{},
original_source: nil
}
iex> ast = {:variable, [], "x"}
iex> metadata = %{type_hint: "str"}
iex> Metastatic.Document.new(ast, :python, metadata, "x")
%Metastatic.Document{
ast: {:variable, [], "x"},
language: :python,
metadata: %{type_hint: "str"},
original_source: "x"
}
"""
@spec new(AST.meta_ast(), atom(), map(), String.t() | nil) :: t()
def new(ast, language, metadata \\ %{}, original_source \\ nil) do
%__MODULE__{
ast: ast,
language: language,
metadata: metadata,
original_source: original_source
}
end
@doc """
Validate that a document's AST conforms to M2 meta-model.
## Examples
iex> doc = Metastatic.Document.new({:literal, [subtype: :integer], 42}, :python)
iex> Metastatic.Document.valid?(doc)
true
iex> doc = %Metastatic.Document{
...> ast: {:invalid_node, [], "data"},
...> language: :python,
...> metadata: %{}
...> }
iex> Metastatic.Document.valid?(doc)
false
"""
@spec valid?(t()) :: boolean()
def valid?(%__MODULE__{ast: ast}) do
AST.conforms?(ast)
end
@doc """
Update the AST in a document while preserving metadata and language.
Useful for transformations that operate on the M2 level.
## Examples
iex> doc = Metastatic.Document.new({:literal, [subtype: :integer], 42}, :python)
iex> new_ast = {:literal, [subtype: :integer], 100}
iex> updated = Metastatic.Document.update_ast(doc, new_ast)
iex> updated.ast
{:literal, [subtype: :integer], 100}
iex> updated.language
:python
"""
@spec update_ast(t(), AST.meta_ast()) :: t()
def update_ast(%__MODULE__{} = doc, new_ast) do
%{doc | ast: new_ast}
end
@doc """
Update document metadata (merges with existing metadata).
## Examples
iex> doc = Metastatic.Document.new({:variable, [], "x"}, :python, %{type: "int"})
iex> updated = Metastatic.Document.update_metadata(doc, %{mutable: false})
iex> updated.metadata
%{type: "int", mutable: false}
"""
@spec update_metadata(t(), map()) :: t()
def update_metadata(%__MODULE__{metadata: metadata} = doc, new_metadata) do
%{doc | metadata: Map.merge(metadata, new_metadata)}
end
@doc """
Get the language of a document.
## Examples
iex> doc = Metastatic.Document.new({:literal, [subtype: :integer], 42}, :python)
iex> Metastatic.Document.language(doc)
:python
"""
@spec language(t()) :: atom()
def language(%__MODULE__{language: lang}), do: lang
@doc """
Check if two documents have semantically equivalent ASTs.
This compares the M2 AST structures, ignoring metadata and original source.
## Examples
iex> doc1 = Metastatic.Document.new({:literal, [subtype: :integer], 42}, :python)
iex> doc2 = Metastatic.Document.new({:literal, [subtype: :integer], 42}, :javascript)
iex> Metastatic.Document.equivalent?(doc1, doc2)
true
iex> doc1 = Metastatic.Document.new({:literal, [subtype: :integer], 42}, :python)
iex> doc2 = Metastatic.Document.new({:literal, [subtype: :string], "42"}, :python)
iex> Metastatic.Document.equivalent?(doc1, doc2)
false
"""
@spec equivalent?(t(), t()) :: boolean()
def equivalent?(%__MODULE__{ast: ast1}, %__MODULE__{ast: ast2}) do
strip_meta(ast1) == strip_meta(ast2)
end
# Strip metadata from AST nodes for structural comparison.
# Preserves type atoms, children structure, and values but replaces
# keyword metadata with only the semantically significant keys.
@semantic_keys [
:subtype,
:category,
:operator,
:name,
:op_type,
:loop_type,
:params,
:captures,
:container_type,
:visibility,
:pattern,
:construct,
:collection_type,
:original_form,
:language,
:hint,
:source,
:names,
:import_type,
:alias
]
defp strip_meta({type, meta, children}) when is_atom(type) and is_list(meta) do
stripped = Keyword.take(meta, @semantic_keys)
stripped_children = strip_meta(children)
{type, stripped, stripped_children}
end
defp strip_meta(list) when is_list(list) do
Enum.map(list, &strip_meta/1)
end
defp strip_meta(other), do: other
@doc """
Extract all variables referenced in the document's AST.
Delegates to `Metastatic.AST.variables/1`.
## Examples
iex> ast = {:binary_op, [category: :arithmetic, operator: :+],
...> [{:variable, [], "x"}, {:variable, [], "y"}]}
iex> doc = Metastatic.Document.new(ast, :python)
iex> Metastatic.Document.variables(doc)
MapSet.new(["x", "y"])
"""
@spec variables(t()) :: MapSet.t(String.t())
def variables(%__MODULE__{ast: ast}) do
AST.variables(ast)
end
@doc """
Normalize input to a Document.
Accepts either:
- A `Metastatic.Document` struct (returned as-is)
- A `{language, native_ast}` tuple (converted to Document using the appropriate adapter)
This enables analyzers to accept both formats for convenience.
## Examples
# Already a Document - returns as-is
iex> doc = Metastatic.Document.new({:literal, [subtype: :integer], 42}, :python)
iex> Metastatic.Document.normalize(doc)
{:ok, doc}
# {lang, native_ast} tuple - converts using adapter
# (Python adapter doctest skipped until adapter is updated to 3-tuple format)
# python_ast = %{"_type" => "Constant", "value" => 42}
# {:ok, doc} = Metastatic.Document.normalize({:python, python_ast})
# doc.ast => {:literal, [subtype: :integer], 42}
"""
@spec normalize(t() | {atom(), term()}) :: {:ok, t()} | {:error, term()}
def normalize(%__MODULE__{} = doc), do: {:ok, doc}
def normalize({language, source}) when is_atom(language) and is_binary(source) do
with {:ok, adapter} <- Metastatic.adapter_for_language(language) do
case adapter.parse(source) do
{:ok, ast} ->
normalize({language, ast})
{:error, reason} ->
{:error, {:parsing_source_failed, reason}}
end
end
end
def normalize({language, native_ast}) when is_atom(language) do
with {:ok, adapter} <- Metastatic.adapter_for_language(language) do
case adapter.to_meta(native_ast) do
{:ok, meta_ast, metadata} ->
{:ok, new(meta_ast, language, metadata)}
{:error, reason} ->
{:error, {:transformation_failed, reason}}
end
end
end
def normalize(other) do
{:error,
{:invalid_input, "Expected Document.t() or {language, native_ast}, got: #{inspect(other)}"}}
end
@doc """
Normalize input to a Document, raising on error.
## Examples
iex> doc = Metastatic.Document.new({:literal, [subtype: :integer], 42}, :python)
iex> Metastatic.Document.normalize!(doc)
doc
"""
@spec normalize!(t() | {atom(), term()}) :: t()
def normalize!(input) do
case normalize(input) do
{:ok, doc} -> doc
{:error, reason} -> raise "Failed to normalize input: #{inspect(reason)}"
end
end
end