Search documentation of Metastatic

# Metastatic.AST (Metastatic v0.2.0)

View Source

M2: Meta-Model for Programming Language Abstract Syntax.

This module defines the meta-types that all language ASTs (M1 level) must conform to. Think of this as the "UML" for programming language ASTs.

## Meta-Modeling Hierarchy

- M3: Elixir type system (`@type`, `@spec`)
- M2: This module (MetaAST) - defines what AST nodes CAN be
- M1: Python AST, JavaScript AST, Elixir AST - what specific code IS
- M0: Runtime execution - what code DOES

## Layer Architecture (within M2)

The three layers represent different granularities of meta-modeling:

- M2.1 (Core): Universal concepts (ALL languages)
- M2.2 (Extended): Common patterns (MOST languages)
- M2.3 (Native): Language-specific escape hatches (embedded M1)

## Semantic Equivalence

Different M1 models can be instances of the same M2 concept:

    # M2: Meta-model definition
    {:binary_op, :arithmetic, :+, left, right}

    # M1 instances (all equivalent at M2 level):
    Python:     BinOp(op=Add(), left=Name('x'), right=Num(5))
    JavaScript: BinaryExpression(operator: '+', left: Identifier('x'), right: Literal(5))
    Elixir:     {:+, [], [{:x, [], nil}, 5]}

All three are INSTANCES of the same M2 concept, differing only in concrete syntax (M1).

## References

- OMG Meta Object Facility (MOF) Specification
- Eclipse Modeling Framework (EMF)
- Abstract Syntax Theory

# Summary

## Types

assignment()

M2 meta-type: Assignment (imperative binding/mutation).

async_operation()

M2 meta-type: Asynchronous operation.

binary_op()

M2 meta-type: Binary operation.

block()

M2 meta-type: Block of statements.

collection_op()

M2 meta-type: Collection operations.

conditional()

M2 meta-type: Conditional expression.

early_return()

M2 meta-type: Early return.

exception_handling()

M2 meta-type: Exception handling.

function_call()

M2 meta-type: Function call.

inline_match()

M2 meta-type: Inline match (pattern matching).

lambda()

M2 meta-type: Lambda/anonymous function.

language_specific()

M2 escape hatch: Language-specific construct.

literal()

M2 meta-type: Literal value.

loop()

M2 meta-type: Loop construct.

meta_ast()

A MetaAST represents a programming language construct at the meta-level (M2).

pattern_match()

M2 meta-type: Pattern matching.

semantic_type()

Semantic type classification for literals.

unary_op()

M2 meta-type: Unary operation.

variable()

M2 meta-type: Variable reference.

## Functions

conforms?(ast)

Validate that a term conforms to the M2 meta-model.

variables(ast)

Extract all variables referenced in an AST.

# Types

# assignment()

    @type assignment() :: {:assignment, target :: meta_ast(), value :: meta_ast()}

M2 meta-type: Assignment (imperative binding/mutation).

Represents imperative assignment in non-BEAM languages where `=` is a mutation/binding operator. The left side is a target that receives a value.

## Semantic Distinction from inline_match

This type is for imperative assignment (Python, JavaScript, Ruby, etc.) where variables can be rebound/mutated. Contrast with `inline_match` which represents declarative pattern matching (Elixir, Erlang).

## M1 instances

- Python: `ast.Assign`, `ast.AugAssign`, `ast.AnnAssign`
- JavaScript: `AssignmentExpression`, `VariableDeclarator`
- Ruby: assignment statements

## Examples

    # Simple assignment: x = 5
    {:assignment, {:variable, "x"}, {:literal, :integer, 5}}

    # Tuple unpacking: x, y = 1, 2
    {:assignment,
     {:tuple, [{:variable, "x"}, {:variable, "y"}]},
     {:tuple, [{:literal, :integer, 1}, {:literal, :integer, 2}]}}

    # Augmented assignment: x += 1 (desugared)
    {:assignment, {:variable, "x"},
     {:binary_op, :arithmetic, :+, {:variable, "x"}, {:literal, :integer, 1}}}

# async_operation()

    @type async_operation() ::
      {:async_operation, :await | :async, operation :: meta_ast()}

M2 meta-type: Asynchronous operation.

Represents async/await, promises, futures.

## M1 instances

- Python: `async`/`await` with asyncio
- JavaScript: `async`/`await` with Promises
- Elixir: `Task.async`/`Task.await`

## Examples

    {:async_operation, :await,
     {:function_call, "fetch_data", [{:literal, :string, "url"}]}}

# binary_op()

    @type binary_op() ::
      {:binary_op, :arithmetic | :comparison | :boolean, op :: atom(),
       left :: meta_ast(), right :: meta_ast()}

M2 meta-type: Binary operation.

This is a meta-concept that abstracts over language-specific binary operations.

## M1 instances

- Python: `ast.BinOp`
- JavaScript: `BinaryExpression`
- Elixir: `{:+, meta, [left, right]}`

All three are INSTANCES of this M2 concept.

## Examples

    {:binary_op, :arithmetic, :+, {:variable, "x"}, {:literal, :integer, 5}}
    {:binary_op, :comparison, :>, {:variable, "age"}, {:literal, :integer, 18}}
    {:binary_op, :boolean, :and, condition1, condition2}

# block()

    @type block() :: {:block, statements :: [meta_ast()]}

M2 meta-type: Block of statements.

Represents a sequence of statements/expressions.

## Examples

    {:block, [stmt1, stmt2, stmt3]}

# collection_op()

    @type collection_op() ::
      {:collection_op, :map | :filter, fn_or_pred :: meta_ast(),
       collection :: meta_ast()}
      | {:collection_op, :reduce, fn_or_pred :: meta_ast(),
         collection :: meta_ast(), initial :: meta_ast()}

M2 meta-type: Collection operations.

These represent functional transformations that exist across languages:

## M1 instances

- Python: list comprehensions, `map()`, `filter()`
- JavaScript: `Array.prototype.map/filter/reduce`
- Elixir: `Enum.map/2`, `Enum.filter/2`, `Enum.reduce/3`

All are instances of the same M2 concept.

## Examples

    {:collection_op, :map,
     {:lambda, ["x"], [], {:binary_op, :arithmetic, :*, {:variable, "x"}, {:literal, :integer, 2}}},
     {:variable, "numbers"}}

# conditional()

    @type conditional() ::
      {:conditional, condition :: meta_ast(), then_branch :: meta_ast(),
       else_branch :: meta_ast() | nil}

M2 meta-type: Conditional expression.

Represents if/else or ternary conditional logic.

## M1 instances

- Python: `ast.If`, `ast.IfExp`
- JavaScript: `IfStatement`, `ConditionalExpression`
- Elixir: `if`, `cond`

## Examples

    {:conditional,
     {:binary_op, :comparison, :>, {:variable, "x"}, {:literal, :integer, 0}},
     {:literal, :string, "positive"},
     {:literal, :string, "non-positive"}}

# early_return()

    @type early_return() :: {:early_return, value :: meta_ast()}

M2 meta-type: Early return.

Represents control flow that exits early from a block.

## Examples

    {:early_return, {:variable, "result"}}

# exception_handling()

    @type exception_handling() ::
      {:exception_handling, try_block :: meta_ast(),
       rescue_clauses :: [
         {exception :: atom(), var :: meta_ast(), body :: meta_ast()}
       ], finally_block :: meta_ast() | nil}

M2 meta-type: Exception handling.

Represents try/catch/finally constructs.

## Examples

    {:exception_handling,
     {:block, [{:function_call, "risky", []}]},
     [{:error, {:variable, "e"}, {:function_call, "handle", [{:variable, "e"}]}}],
     {:function_call, "cleanup", []}}

# function_call()

    @type function_call() :: {:function_call, name :: String.t(), args :: [meta_ast()]}

M2 meta-type: Function call.

Represents invocation of a function or method.

## M1 instances

- Python: `ast.Call`
- JavaScript: `CallExpression`
- Elixir: function application

## Examples

    {:function_call, "add", [{:variable, "x"}, {:variable, "y"}]}
    {:function_call, "max", [a, b]}

# inline_match()

    @type inline_match() :: {:inline_match, pattern :: meta_ast(), value :: meta_ast()}

M2 meta-type: Inline match (pattern matching).

Represents inline pattern matching in BEAM languages where `=` is a match operator, not assignment. The left side is a pattern that must unify with the right side value.

## Semantic Distinction from assignment

This type is for declarative pattern matching (Elixir, Erlang) where:

- Elixir: Variables bind on first occurrence, rebind on subsequent use
- Erlang: Single-assignment semantics, match fails if variable already bound to different value

Contrast with `assignment` which represents imperative binding/mutation.

## M1 instances

- Elixir: `=` operator in match context
- Erlang: `=` operator (single-assignment)

## Examples

    # Simple match: x = 5 (Elixir/Erlang)
    {:inline_match, {:variable, "x"}, {:literal, :integer, 5}}

    # Tuple destructuring: {x, y} = {1, 2}
    {:inline_match,
     {:tuple, [{:variable, "x"}, {:variable, "y"}]},
     {:tuple, [{:literal, :integer, 1}, {:literal, :integer, 2}]}}

    # List pattern: [head | tail] = list
    {:inline_match,
     {:cons_pattern, {:variable, "head"}, {:variable, "tail"}},
     {:variable, "list"}}

    # Pin operator (Elixir): ^x = 5
    {:inline_match,
     {:pin, {:variable, "x"}},
     {:literal, :integer, 5}}

# lambda()

    @type lambda() ::
      {:lambda, params :: [String.t()], captures :: [String.t()],
       body :: meta_ast()}

M2 meta-type: Lambda/anonymous function.

Represents function literals.

## M1 instances

- Python: `ast.Lambda`
- JavaScript: `ArrowFunctionExpression`, `FunctionExpression`
- Elixir: `fn` expressions

## Examples

    {:lambda, ["x", "y"], [], {:binary_op, :arithmetic, :+, {:variable, "x"}, {:variable, "y"}}}

# language_specific()

    @type language_specific() ::
      {:language_specific, language :: atom(), native_info :: map()}

M2 escape hatch: Language-specific construct.

When an M1 construct cannot be abstracted to M2, we preserve the M1 AST directly with semantic hints.

This allows:

1. Round-trip fidelity (M1 → M2 → M1)
2. Partial analysis at M2 level (using semantic info)
3. Language-specific transformations when needed

## Examples

    {:language_specific, :python,
     %{construct: :list_comprehension, data: "[x for x in range(10)]"}}

# literal()

    @type literal() :: {:literal, semantic_type(), value :: term()}

M2 meta-type: Literal value.

Represents constant values that appear in source code.

## M1 instances

- Python: `ast.Constant`, `ast.Num`, `ast.Str`
- JavaScript: `Literal`
- Elixir: integer, string, atom literals

## Examples

    {:literal, :integer, 42}
    {:literal, :string, "hello"}
    {:literal, :boolean, true}
    {:literal, :null, nil}

# loop()

    @type loop() ::
      {:loop, :while, condition :: meta_ast(), body :: meta_ast()}
      | {:loop, :for | :for_each, item :: meta_ast(), collection :: meta_ast(),
         body :: meta_ast()}

M2 meta-type: Loop construct.

Represents iterative execution patterns.

## M1 instances

- Python: `ast.For`, `ast.While`
- JavaScript: `ForStatement`, `WhileStatement`
- Elixir: `Enum.each/2` (functional iteration)

## Examples

    {:loop, :while,
     {:binary_op, :comparison, :>, {:variable, "x"}, {:literal, :integer, 0}},
     {:block, [{:variable, "x"}]}}

# meta_ast()

    @type meta_ast() ::
      literal()
      | variable()
      | binary_op()
      | unary_op()
      | function_call()
      | conditional()
      | early_return()
      | block()
      | assignment()
      | inline_match()
      | loop()
      | lambda()
      | collection_op()
      | pattern_match()
      | exception_handling()
      | async_operation()
      | language_specific()

A MetaAST represents a programming language construct at the meta-level (M2).

This is the union of all meta-types across the three layers.

# pattern_match()

    @type pattern_match() ::
      {:pattern_match, scrutinee :: meta_ast(),
       arms :: [{pattern :: meta_ast(), body :: meta_ast()}]}

M2 meta-type: Pattern matching.

Represents switch/case/match constructs.

## M1 instances

- Python: `ast.Match` (3.10+)
- JavaScript: `SwitchStatement`
- Elixir: `case`, pattern matching in function heads

## Examples

    {:pattern_match, {:variable, "value"},
     [
       {{:literal, :integer, 0}, {:literal, :string, "zero"}},
       {{:literal, :integer, 1}, {:literal, :string, "one"}},
       {:_, {:literal, :string, "other"}}
     ]}

# semantic_type()

    @type semantic_type() ::
      :integer
      | :float
      | :string
      | :boolean
      | :null
      | :symbol
      | :regex
      | :collection

Semantic type classification for literals.

These categories exist across all languages, though syntax varies.

# unary_op()

    @type unary_op() ::
      {:unary_op, :arithmetic | :boolean, op :: atom(), operand :: meta_ast()}

M2 meta-type: Unary operation.

Represents operations with a single operand.

## Examples

    {:unary_op, :arithmetic, :-, {:variable, "x"}}
    {:unary_op, :boolean, :not, {:variable, "flag"}}

# variable()

    @type variable() :: {:variable, name :: String.t()}

M2 meta-type: Variable reference.

Represents an identifier that refers to a value in scope.

## M1 instances

- Python: `ast.Name`
- JavaScript: `Identifier`
- Elixir: `{:var, meta, context}`

## Examples

    {:variable, "x"}
    {:variable, "userName"}

# Functions

# conforms?(ast)

    @spec conforms?(term()) :: boolean()

Validate that a term conforms to the M2 meta-model.

This performs M1 → M2 conformance checking, ensuring that the structure is a valid MetaAST.

## Examples

    iex> Metastatic.AST.conforms?({:literal, :integer, 42})
    true

    iex> Metastatic.AST.conforms?({:binary_op, :arithmetic, :+, {:variable, "x"}, {:literal, :integer, 5}})
    true

    iex> Metastatic.AST.conforms?({:invalid_node, "data"})
    false

# variables(ast)

    @spec variables(meta_ast()) :: MapSet.t(String.t())

Extract all variables referenced in an AST.

Useful for analyzing dependencies and scope.

## Examples

    iex> ast = {:binary_op, :arithmetic, :+, {:variable, "x"}, {:variable, "y"}}
    iex> Metastatic.AST.variables(ast)
    MapSet.new(["x", "y"])