Search documentation of Metastatic

# Metastatic.Adapter behaviour (Metastatic v0.13.2)

View Source

Behaviour for language adapters (M1 ↔ M2 transformations).

Language adapters bridge between:

- M1: Language-specific ASTs (Python, JavaScript, Elixir, etc.)
- M2: MetaAST meta-model

## Meta-Modeling Operations

- `parse/1`: Source → M1 (language-specific parsing)
- `to_meta/1`: M1 → M2 (abstraction to meta-level)
- `from_meta/2`: M2 → M1 (reification from meta-level)
- `unparse/1`: M1 → Source (language-specific unparsing)

## Conformance

Adapters must ensure:

1. M1 instances conform to M2 meta-model
2. M2 → M1 → M2 round-trips preserve semantics
3. Invalid M2 transformations are rejected at M1 level

## Theory

In formal terms, a language adapter is a pair of functions:

    Adapter_L = ⟨α_L, ρ_L⟩

    where:
      α_L: AS_L → MetaAST × Metadata    (abstraction)
      ρ_L: MetaAST × Metadata → AS_L    (reification)

These functions form a Galois connection between M1 and M2 levels.

## Example Implementation

    defmodule Metastatic.Adapters.Python do
      @behaviour Metastatic.Adapter

      @impl true
      def parse(source) do
        # Call Python AST parser (via port or NIF)
        {:ok, python_ast}
      end

      @impl true
      def to_meta(python_ast) do
        # Transform Python AST (M1) to MetaAST (M2)
        # Example: BinOp(op=Add()) → {:binary_op, :arithmetic, :+, ...}
        {:ok, meta_ast, metadata}
      end

      @impl true
      def from_meta(meta_ast, metadata) do
        # Transform MetaAST (M2) back to Python AST (M1)
        {:ok, python_ast}
      end

      @impl true
      def unparse(python_ast) do
        # Convert Python AST back to source code
        {:ok, source}
      end

      @impl true
      def file_extensions, do: [".py"]
    end

## Semantic Equivalence

Different M1 models may map to the same M2 instance:

    # Python (M1)
    BinOp(op=Add(), left=Name('x'), right=Num(5))

    # JavaScript (M1)
    BinaryExpression(operator: '+', left: Identifier('x'), right: Literal(5))

    # Both abstract to the SAME M2 instance:
    {:binary_op, :arithmetic, :+, {:variable, "x"}, {:literal, :integer, 5}}

This is the foundation of universal transformations.

## Summary

### Types

meta_ast()

MetaAST representation (M2 level).

metadata()

Metadata preserving M1-specific information.

native_ast()

Native AST in the language-specific format (M1 level).

source()

Source code as a string.

### Callbacks

extract_children(native_ast)

Extract children from a language-specific AST node.

file_extensions()

Return the file extensions this adapter handles.

from_meta(meta_ast, metadata)

Transform MetaAST back to native AST (M2 → M1 reification).

parse(source)

Parse source code to native AST (Source → M1).

to_meta(native_ast)

Transform native AST to MetaAST (M1 → M2 abstraction).

unparse(native_ast)

Convert native AST back to source code (M1 → Source).

validate_mutation(meta_ast, metadata)

Validate that an M2 transformation produces valid M1.

### Functions

abstract(adapter, source, language)

Abstraction pipeline: Source → M1 → M2 (Document).

detect_language(filename)

Detect language from file extension.

for_language(language)

Get adapter module for a language.

reify(adapter, document)

Reification pipeline: M2 (Document) → M1 → Source.

round_trip(adapter, source)

Full round-trip: Source → M1 → M2 → M1 → Source.

valid_adapter?(adapter)

Validate that an adapter correctly implements the behaviour.

## Types

# meta_ast()

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

MetaAST representation (M2 level).

# metadata()

    @type metadata() :: map()

Metadata preserving M1-specific information.

Contains details that cannot be represented at M2 level but are necessary for high-fidelity round-trips.

# native_ast()

    @type native_ast() :: term()

Native AST in the language-specific format (M1 level).

This is an opaque term - the structure varies by language.

# source()

    @type source() :: String.t()

Source code as a string.

## Callbacks

# extract_children(native_ast)

(optional)

    @callback extract_children(native_ast()) :: [native_ast()]

Extract children from a language-specific AST node.

Given a language-specific AST node (M1), return a list of its child nodes that should be traversed during analysis.

This is used by analyzers that need to traverse language-specific nodes embedded in `:language_specific` MetaAST nodes.

## Default Implementation

If not implemented, the runner will use a generic fallback that attempts to extract children based on common AST patterns.

## Examples

    # Elixir adapter
    extract_children({:defmodule, _meta, [_name, [do: body]]})
    # => [body]

    # Python adapter
    extract_children(%{"_type" => "FunctionDef", "body" => statements})
    # => statements

# file_extensions()

    @callback file_extensions() :: [String.t()]

Return the file extensions this adapter handles.

Used for automatic language detection.

## Examples

    # Python adapter
    file_extensions()
    # => [".py"]

    # JavaScript/TypeScript adapter
    file_extensions()
    # => [".js", ".jsx", ".ts", ".tsx"]

# from_meta(meta_ast, metadata)

    @callback from_meta(meta_ast(), metadata()) ::
      {:ok, native_ast()} | {:error, reason :: term()}

Transform MetaAST back to native AST (M2 → M1 reification).

This is the reification operation that instantiates the meta-model (M2) into a concrete language AST (M1).

Uses metadata to restore M1-specific information that was preserved during abstraction (e.g., formatting, type annotations, etc.).

## Conformance Validation

Implementation must ensure the resulting M1 AST:

1. Is valid for the target language
2. Preserves the semantics of the M2 instance
3. Can be round-tripped (M1 → M2 → M1 ≈ M1)

## Examples

    # MetaAST to Python BinOp
    from_meta(
      {:binary_op, :arithmetic, :+, {:variable, "x"}, {:literal, :integer, 5}},
      %{native_lang: :python}
    )
    # => {:ok, %{"_type" => "BinOp", "op" => %{"_type" => "Add"}, ...}}

# parse(source)

    @callback parse(source()) :: {:ok, native_ast()} | {:error, reason :: term()}

Parse source code to native AST (Source → M1).

This is a language-specific operation that produces the M1 representation.

## Implementation Notes

- May spawn external processes (Python, Node.js, etc.)
- May use native Elixir parsing (for Elixir adapter)
- Should validate syntax and return clear error messages

## Examples

    # Python adapter
    parse("x + 5")
    # => {:ok, %{"_type" => "BinOp", "op" => %{"_type" => "Add"}, ...}}

    # JavaScript adapter
    parse("x + 5")
    # => {:ok, %{"type" => "BinaryExpression", "operator" => "+", ...}}

# to_meta(native_ast)

    @callback to_meta(native_ast()) ::
      {:ok, meta_ast(), metadata()} | {:error, reason :: term()}

Transform native AST to MetaAST (M1 → M2 abstraction).

This is the abstraction operation that lifts language-specific AST (M1) to the meta-level (M2).

## Semantic Equivalence

Different M1 models may map to the same M2 instance:

- Python: `BinOp(op=Add)` → `{:binary_op, :arithmetic, :+, ...}`
- JavaScript: `BinaryExpression(operator: '+')` → same M2 instance
- Elixir: `{:+, [], [...]}` → same M2 instance

## Return Value

Returns a tuple of `{meta_ast, metadata}` where:

- `meta_ast` is the M2 representation
- `metadata` preserves M1-specific information for round-tripping

## Examples

    # Python BinOp to MetaAST
    to_meta(%{"_type" => "BinOp", "op" => %{"_type" => "Add"}, ...})
    # => {:ok,
    #     {:binary_op, :arithmetic, :+, {:variable, "x"}, {:literal, :integer, 5}},
    #     %{native_lang: :python, ...}}

# unparse(native_ast)

    @callback unparse(native_ast()) :: {:ok, source()} | {:error, reason :: term()}

Convert native AST back to source code (M1 → Source).

This is the final step in the M2 → M1 → Source pipeline.

## Implementation Notes

- May use language-specific unparsers/pretty-printers
- Should attempt to preserve formatting where possible
- May normalize formatting to language conventions

## Examples

    # Python AST to source
    unparse(%{"_type" => "BinOp", ...})
    # => {:ok, "x + 5"}

# validate_mutation(meta_ast, metadata)

(optional)

    @callback validate_mutation(meta_ast(), metadata()) ::
      :ok | {:error, validation_error :: String.t()}

Validate that an M2 transformation produces valid M1.

After a mutation at M2 level, validate that the result:

1. Conforms to M2 meta-model structurally
2. Can be instantiated in this language (M1 conformance)
3. Satisfies language-specific constraints

## Examples

- Rust: Reject mutations that violate ownership/borrowing
- TypeScript: Reject mutations that violate type constraints
- Python: Most mutations valid (dynamic typing)

This is M2 → M1 semantic conformance validation.

## Default Implementation

If not implemented, assumes all M2-valid transformations are M1-valid.

## Functions

# abstract(adapter, source, language)

    @spec abstract(module(), source(), atom()) ::
      {:ok, Metastatic.Document.t()} | {:error, term()}

Abstraction pipeline: Source → M1 → M2 (Document).

Convenience function that combines parse and to_meta.

## Examples

    iex> Metastatic.Adapter.abstract(MyAdapter, "x + 5")
    {:ok, %Metastatic.Document{
      ast: {:binary_op, :arithmetic, :+, ...},
      language: :python,
      metadata: %{...},
      original_source: "x + 5"
    }}

# detect_language(filename)

    @spec detect_language(String.t()) :: {:ok, atom()} | {:error, :unknown_extension}

Detect language from file extension.

## Examples

    iex> Metastatic.Adapter.detect_language("script.py")
    {:ok, :python}

    iex> Metastatic.Adapter.detect_language("app.js")
    {:ok, :javascript}

    iex> Metastatic.Adapter.detect_language("unknown.xyz")
    {:error, :unknown_extension}

# for_language(language)

    @spec for_language(atom()) :: {:ok, module()} | {:error, :no_adapter_found}

Get adapter module for a language.

Returns the appropriate adapter module for a given language atom.

## Examples

    iex> Metastatic.Adapter.for_language(:python)
    {:ok, Metastatic.Adapters.Python}

    iex> Metastatic.Adapter.for_language(:unknown)
    {:error, :no_adapter_found}

# reify(adapter, document)

    @spec reify(module(), Metastatic.Document.t()) :: {:ok, source()} | {:error, term()}

Reification pipeline: M2 (Document) → M1 → Source.

Convenience function that combines from_meta and unparse.

## Examples

    iex> doc = %Metastatic.Document{
    ...>   ast: {:binary_op, :arithmetic, :+, {:variable, "x"}, {:literal, :integer, 5}},
    ...>   language: :python,
    ...>   metadata: %{}
    ...> }
    iex> Metastatic.Adapter.reify(MyAdapter, doc)
    {:ok, "x + 5"}

# round_trip(adapter, source)

    @spec round_trip(module(), source()) :: {:ok, source()} | {:error, term()}

Full round-trip: Source → M1 → M2 → M1 → Source.

Useful for testing adapter fidelity.

## Examples

    iex> source = "x + 5"
    iex> Metastatic.Adapter.round_trip(MyAdapter, source)
    {:ok, "x + 5"}  # May have normalized formatting

# valid_adapter?(adapter)

    @spec valid_adapter?(module()) :: boolean()

Validate that an adapter correctly implements the behaviour.

Checks that all required callbacks are defined.

## Examples

    iex> Metastatic.Adapter.valid_adapter?(MyPythonAdapter)
    true

    iex> Metastatic.Adapter.valid_adapter?(InvalidModule)
    false