Packages
metastatic
0.4.2
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/ast.ex
defmodule Metastatic.AST do
@moduledoc """
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
"""
# ----- M2 Node Type Definition -----
@typedoc """
A MetaAST represents a programming language construct at the meta-level (M2).
This is the union of all meta-types across the three layers.
"""
# M2.1: Core layer - Universal programming concepts
@type meta_ast ::
literal()
| variable()
| list_literal()
| map_literal()
| binary_op()
| unary_op()
| function_call()
| conditional()
| early_return()
| block()
| assignment()
| inline_match()
# M2.2: Extended layer - Common patterns with variations
| loop()
| lambda()
| collection_op()
| pattern_match()
| exception_handling()
| async_operation()
# M2.2s: Structural/Organizational layer - Top-level constructs
| container()
| function_def()
| attribute_access()
| augmented_assignment()
| property()
# M2.3: Native layer - M1 escape hatch
| language_specific()
# ----- M2.1: Core Layer - Universal Meta-Types -----
# These represent concepts that exist in ALL programming languages
@typedoc """
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}
"""
@type literal :: {:literal, semantic_type(), value :: term()}
@typedoc """
Semantic type classification for literals.
These categories exist across all languages, though syntax varies.
"""
@type semantic_type ::
:integer
| :float
| :string
| :boolean
| :null
| :symbol
| :regex
| :collection
@typedoc """
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"}
"""
@type variable :: {:variable, name :: String.t()}
@typedoc """
M2 meta-type: List collection.
Represents ordered sequences of elements. Lists are fundamental data structures
present in all programming languages, making them universal (M2.1 Core).
## M1 instances
- Python: `ast.List`
- JavaScript: `Array`
- Elixir: list literal `[1, 2, 3]`
- Ruby: `Array`
- Erlang: list
## Examples
{:list, []}
{:list, [{:literal, :integer, 1}, {:literal, :integer, 2}]}
{:list, [{:variable, "x"}, {:variable, "y"}]}
"""
@type list_literal :: {:list, elements :: [meta_ast()]}
@typedoc """
M2 meta-type: Map/Dictionary collection.
Represents key-value mappings. Maps/dictionaries are fundamental data structures
present in all modern programming languages, making them universal (M2.1 Core).
## M1 instances
- Python: `ast.Dict`
- JavaScript: `Object` literal
- Elixir: map `%{key => value}`
- Ruby: `Hash`
- Erlang: map
## Examples
{:map, []}
{:map, [{{:literal, :string, "name"}, {:literal, :string, "Alice"}}]}
{:map, [{{:variable, "key"}, {:variable, "value"}}]}
"""
@type map_literal :: {:map, pairs :: [{key :: meta_ast(), value :: meta_ast()}]}
@typedoc """
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}
"""
@type binary_op ::
{:binary_op, :arithmetic | :comparison | :boolean, op :: atom(), left :: meta_ast(),
right :: meta_ast()}
@typedoc """
M2 meta-type: Unary operation.
Represents operations with a single operand.
## Examples
{:unary_op, :arithmetic, :-, {:variable, "x"}}
{:unary_op, :boolean, :not, {:variable, "flag"}}
"""
@type unary_op ::
{:unary_op, :arithmetic | :boolean, op :: atom(), operand :: meta_ast()}
@typedoc """
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]}
"""
@type function_call :: {:function_call, name :: String.t(), args :: [meta_ast()]}
@typedoc """
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"}}
"""
@type conditional ::
{:conditional, condition :: meta_ast(), then_branch :: meta_ast(),
else_branch :: meta_ast() | nil}
@typedoc """
M2 meta-type: Early return.
Represents control flow that exits early from a block.
## Examples
{:early_return, {:variable, "result"}}
"""
@type early_return :: {:early_return, value :: meta_ast()}
@typedoc """
M2 meta-type: Block of statements.
Represents a sequence of statements/expressions.
## Examples
{:block, [stmt1, stmt2, stmt3]}
"""
@type block :: {:block, statements :: [meta_ast()]}
@typedoc """
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}}}
"""
@type assignment :: {:assignment, target :: meta_ast(), value :: meta_ast()}
@typedoc """
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}}
"""
@type inline_match :: {:inline_match, pattern :: meta_ast(), value :: meta_ast()}
# ----- M2.2: Extended Layer - Common Meta-Patterns -----
# These represent concepts that exist in MOST languages, with variations
@typedoc """
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"}]}}
"""
@type loop ::
{:loop, :while, condition :: meta_ast(), body :: meta_ast()}
| {:loop, :for | :for_each, item :: meta_ast(), collection :: meta_ast(),
body :: meta_ast()}
@typedoc """
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"}}}
"""
@type lambda ::
{:lambda, params :: [String.t()], captures :: [String.t()], body :: meta_ast()}
@typedoc """
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"}}
"""
@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()}
@typedoc """
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"}}
]}
"""
@type pattern_match ::
{:pattern_match, scrutinee :: meta_ast(),
arms :: [{pattern :: meta_ast(), body :: meta_ast()}]}
@typedoc """
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", []}}
"""
@type exception_handling ::
{:exception_handling, try_block :: meta_ast(),
rescue_clauses :: [{exception :: atom(), var :: meta_ast(), body :: meta_ast()}],
finally_block :: meta_ast() | nil}
@typedoc """
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"}]}}
"""
@type async_operation :: {:async_operation, :await | :async, operation :: meta_ast()}
# ----- M2.2s: Structural/Organizational Layer -----
# Top-level constructs: modules, classes, function definitions
@typedoc """
M2 meta-type: Container (module/class/namespace).
Represents organizational units that group related code.
## M1 instances
- Python: `ast.Module`, `ast.ClassDef`
- JavaScript: Module, `ClassDeclaration`
- Elixir: `defmodule`
- Ruby: `class`, `module`
## Metadata Structure
The metadata map contains language-specific organizational information:
- `:source_language` - atom identifying source language (`:python`, `:elixir`, etc.)
- `:has_state` - boolean indicating if container manages mutable state (classes typically true)
- `:visibility` - visibility map: `%{public: [{name, arity}], private: [{name, arity}], protected: [{name, arity}]}`
- `:superclass` - string name of direct superclass (nil for no inheritance)
Note: Currently only stores direct superclass. TODO: Full inheritance chain would be better in future.
- `:organizational_model` - `:oop` or `:fp` to indicate programming paradigm
- `:original_ast` - the original M1 AST for 100% round-trip fidelity
- `:decorators` - list of decorator MetaAST nodes (Python `@decorator`, etc.)
- `:type_params` - list of generic type parameters for generic classes
- `:module_attributes` - module-level attributes (@moduledoc in Elixir, etc.)
- `:constructor` - reference to constructor function (by name)
- `:is_nested` - boolean indicating if this is a nested container
## Examples
# Python class
{:container, :class, "Calculator",
%{source_language: :python,
has_state: true,
visibility: %{public: [{"add", 2}, {"subtract", 2}], private: [{"_validate", 1}], protected: []},
superclass: "BaseCalculator",
organizational_model: :oop,
original_ast: python_class_ast},
[function_def1, function_def2]}
# Elixir module
{:container, :module, "MyApp.Calculator",
%{source_language: :elixir,
has_state: false,
visibility: %{public: [{"add", 2}], private: [{"validate", 1}], protected: []},
superclass: nil,
organizational_model: :fp,
original_ast: elixir_module_ast},
[function_def1, function_def2]}
"""
@type container ::
{:container, container_type(), name :: String.t(), metadata :: map(),
members :: [meta_ast()]}
@typedoc """
Container type classification.
"""
@type container_type :: :module | :class | :namespace
@typedoc """
M2 meta-type: Function definition.
Represents a named function/method definition (not invocation).
## M1 instances
- Python: `ast.FunctionDef`, `ast.AsyncFunctionDef`
- JavaScript: `FunctionDeclaration`, Method in `ClassDeclaration`
- Elixir: `def`, `defp`
- Ruby: `def`
## Parameters
The params list can contain:
- Simple string names: `"x"`, `"count"`
- Pattern parameters: `{:pattern, meta_ast}` for destructuring
- Default parameters: `{:default, "name", default_value}`
## Metadata Structure
The metadata map can contain:
- `:guards` - guard clause as MetaAST (Elixir/Erlang when clauses, etc.)
- `:arity` - integer arity of the function
- `:return_type` - type annotation for return value
- `:decorators` - list of decorator MetaAST nodes
- `:is_async` - boolean for async/await functions
- `:is_static` - boolean for static methods (in OOP)
- `:is_abstract` - boolean for abstract methods
- `:specs` - function specifications (@spec in Elixir, type hints in Python)
- `:doc` - documentation string
- `:original_ast` - original M1 AST for round-trip fidelity
Note: Guards are stored in metadata (not as separate parameter) per design decision Q1.2.
Note: Self parameter is removed at M2 level per design decision Q3.1.
Note: For multi-clause functions (Elixir), store as single function_def with pattern match in body per Q2.1.
## Examples
# Simple function: def add(x, y), do: x + y
{:function_def, :public, "add", ["x", "y"],
%{arity: 2, guards: nil},
{:binary_op, :arithmetic, :+, {:variable, "x"}, {:variable, "y"}}}
# Function with guard: def positive?(x) when x > 0
{:function_def, :public, "positive?", ["x"],
%{arity: 1, guards: {:binary_op, :comparison, :>, {:variable, "x"}, {:literal, :integer, 0}}},
{:literal, :boolean, true}}
# Function with pattern matching parameter
{:function_def, :public, "get_first", [{:pattern, {:tuple, [{:variable, "x"}, :_]}}],
%{arity: 1},
{:variable, "x"}}
# Function with default parameter
{:function_def, :public, "greet", [{:default, "name", {:literal, :string, "World"}}],
%{arity: 1},
{:function_call, "puts", [{:literal, :string, "Hello"}]}}
"""
@type function_def ::
{:function_def, visibility(), name :: String.t(), params :: [param()],
metadata :: map(), body :: meta_ast()}
@typedoc """
Visibility classification for functions and members.
"""
@type visibility :: :public | :private | :protected
@typedoc """
Function parameter types.
"""
@type param ::
String.t()
| {:pattern, meta_ast()}
| {:default, String.t(), meta_ast()}
@typedoc """
M2 meta-type: Attribute access.
Represents accessing a field/property/attribute on an object or module.
## M1 instances
- Python: `ast.Attribute`
- JavaScript: `MemberExpression`
- Elixir: Module.function (converted to function_call), struct.field
- Ruby: `object.method`, `object.field`
## Examples
# Python: obj.value
{:attribute_access, {:variable, "obj"}, "value"}
# JavaScript: user.name
{:attribute_access, {:variable, "user"}, "name"}
# Chained access: user.address.street
{:attribute_access,
{:attribute_access, {:variable, "user"}, "address"},
"street"}
"""
@type attribute_access ::
{:attribute_access, receiver :: meta_ast(), attribute :: String.t()}
@typedoc """
M2 meta-type: Augmented assignment.
Represents compound assignment operators (+=, -=, *=, etc.) in their
non-desugared form to preserve source structure.
## M1 instances
- Python: `ast.AugAssign`
- JavaScript: `AssignmentExpression` with operators like `+=`
- Ruby: `+=`, `-=` operators
Note: While these CAN be desugared to `{:assignment, target, {:binary_op, ...}}`,
preserving them as a distinct type maintains source fidelity and enables
augmented-assignment-specific analysis.
## Examples
# Python: x += 5
{:augmented_assignment, :+, {:variable, "x"}, {:literal, :integer, 5}}
# JavaScript: count *= 2
{:augmented_assignment, :*, {:variable, "count"}, {:literal, :integer, 2}}
# Ruby: total -= discount
{:augmented_assignment, :-, {:variable, "total"}, {:variable, "discount"}}
"""
@type augmented_assignment ::
{:augmented_assignment, operator :: atom(), target :: meta_ast(), value :: meta_ast()}
@typedoc """
M2 meta-type: Property.
Represents property declarations with getters/setters (Python @property,
Ruby attr_accessor, C# properties, etc.).
## M1 instances
- Python: `@property` decorator pattern
- Ruby: `attr_accessor`, `attr_reader`, `attr_writer`
- C#: property declarations
- JavaScript: getter/setter in classes
## Metadata Structure
- `:original_ast` - original M1 AST
- `:is_read_only` - boolean (true for attr_reader/getter-only)
- `:is_write_only` - boolean (rare, but possible)
- `:backing_field` - name of underlying field if applicable
## Examples
# Python property with getter and setter
{:property, "temperature",
{:function_def, :public, "temperature", [], %{}, {:variable, "_temp"}},
{:function_def, :public, "temperature", ["value"], %{},
{:assignment, {:variable, "_temp"}, {:variable, "value"}}},
%{is_read_only: false}}
# Ruby attr_reader (read-only)
{:property, "name",
{:function_def, :public, "name", [], %{}, {:variable, "@name"}},
nil,
%{is_read_only: true}}
"""
@type property ::
{:property, name :: String.t(), getter :: function_def() | nil,
setter :: function_def() | nil, metadata :: map()}
# ----- M2.3: Native Layer - M1 Escape Hatch -----
# When M1 cannot be lifted to M2, preserve as-is with semantic hints
@typedoc """
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)]"}}
"""
@type language_specific :: {:language_specific, language :: atom(), native_info :: map()}
# ----- M2 Conformance Validation -----
@doc """
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
"""
@spec conforms?(term()) :: boolean()
def conforms?(ast) do
case ast do
# M2.1: Core
{:literal, type, _value}
when type in [:integer, :float, :string, :boolean, :null, :symbol, :regex, :collection] ->
true
{:variable, name} when is_binary(name) ->
true
:_ ->
true
{:list, elements} when is_list(elements) ->
Enum.all?(elements, &conforms?/1)
{:map, pairs} when is_list(pairs) ->
Enum.all?(pairs, fn {key, value} -> conforms?(key) and conforms?(value) end)
{:binary_op, category, _op, left, right}
when category in [:arithmetic, :comparison, :boolean] ->
conforms?(left) and conforms?(right)
{:unary_op, category, _op, operand} when category in [:arithmetic, :boolean] ->
conforms?(operand)
{:function_call, name, args} when is_binary(name) and is_list(args) ->
Enum.all?(args, &conforms?/1)
{:conditional, condition, then_branch, else_branch} ->
conforms?(condition) and conforms?(then_branch) and
(is_nil(else_branch) or conforms?(else_branch))
{:early_return, value} ->
conforms?(value)
{:block, statements} when is_list(statements) ->
Enum.all?(statements, &conforms?/1)
{:assignment, target, value} ->
conforms?(target) and conforms?(value)
{:inline_match, pattern, value} ->
conforms?(pattern) and conforms?(value)
# Tuple (used in patterns and destructuring)
{:tuple, elements} when is_list(elements) ->
Enum.all?(elements, &conforms?/1)
# M2.2: Extended
{:loop, :while, condition, body} ->
conforms?(condition) and conforms?(body)
{:loop, kind, _item, collection, body} when kind in [:for, :for_each] ->
conforms?(collection) and conforms?(body)
{:lambda, params, captures, body}
when is_list(params) and is_list(captures) ->
conforms?(body)
{:collection_op, kind, fn_or_pred, collection} when kind in [:map, :filter] ->
conforms?(fn_or_pred) and conforms?(collection)
{:collection_op, kind, fn_or_pred, collection, initial} when kind == :reduce ->
conforms?(fn_or_pred) and conforms?(collection) and conforms?(initial)
{:pattern_match, scrutinee, arms} when is_list(arms) ->
conforms?(scrutinee) and
Enum.all?(arms, fn
{pattern, body} -> conforms?(pattern) and conforms?(body)
_other -> false
end)
{:exception_handling, try_block, rescue_clauses, finally_block}
when is_list(rescue_clauses) ->
conforms?(try_block) and
Enum.all?(rescue_clauses, fn {_ex, var, body} -> conforms?(var) and conforms?(body) end) and
(is_nil(finally_block) or conforms?(finally_block))
{:async_operation, kind, operation} when kind in [:await, :async] ->
conforms?(operation)
# M2.2s: Structural/Organizational
{:container, type, name, metadata, members}
when type in [:module, :class, :namespace] and is_binary(name) and is_map(metadata) and
is_list(members) ->
Enum.all?(members, &conforms?/1)
{:function_def, visibility, name, params, metadata, body}
when visibility in [:public, :private, :protected] and is_binary(name) and
is_list(params) and is_map(metadata) ->
Enum.all?(params, &valid_param?/1) and conforms?(body)
{:attribute_access, receiver, attribute} when is_binary(attribute) ->
conforms?(receiver)
{:augmented_assignment, _operator, target, value} ->
conforms?(target) and conforms?(value)
{:property, name, getter, setter, metadata}
when is_binary(name) and is_map(metadata) ->
(is_nil(getter) or conforms?(getter)) and (is_nil(setter) or conforms?(setter))
# M2.3: Native
# 5-tuple with embedded metadata (preferred format)
{:language_specific, language, _native_info, hint, metadata}
when is_atom(language) and is_atom(hint) and is_map(metadata) ->
true
# 4-tuple without embedded metadata
{:language_specific, language, _native_info, hint}
when is_atom(language) and is_atom(hint) ->
true
# Legacy 3-tuple format (for backward compatibility)
{:language_specific, language, native_info}
when is_atom(language) and is_map(native_info) ->
true
_ ->
false
end
end
# ----- Helper Functions -----
@doc """
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"])
"""
@spec variables(meta_ast()) :: MapSet.t(String.t())
def variables(ast) do
collect_variables(ast, MapSet.new())
end
@doc """
Extract the container name from a container node.
## Examples
iex> ast = {:container, :module, "MyApp.Math", %{}, []}
iex> Metastatic.AST.container_name(ast)
"MyApp.Math"
"""
@spec container_name(container()) :: String.t()
def container_name({:container, _type, name, _metadata, _members}), do: name
@doc """
Extract the function name from a function_def node.
## Examples
iex> ast = {:function_def, :public, "add", ["x", "y"], %{}, {:binary_op, :arithmetic, :+, {:variable, "x"}, {:variable, "y"}}}
iex> Metastatic.AST.function_name(ast)
"add"
"""
@spec function_name(function_def()) :: String.t()
def function_name({:function_def, _visibility, name, _params, _metadata, _body}), do: name
@doc """
Get the visibility of a function_def.
## Examples
iex> ast = {:function_def, :public, "add", [], %{}, {:literal, :integer, 0}}
iex> Metastatic.AST.function_visibility(ast)
:public
"""
@spec function_visibility(function_def()) :: visibility()
def function_visibility({:function_def, visibility, _name, _params, _metadata, _body}),
do: visibility
@doc """
Check if a container has state (mutable or immutable).
Looks at the `:has_state` metadata key.
## Examples
iex> ast = {:container, :class, "Counter", %{has_state: true}, []}
iex> Metastatic.AST.has_state?(ast)
true
iex> ast = {:container, :module, "Math", %{has_state: false}, []}
iex> Metastatic.AST.has_state?(ast)
false
"""
@spec has_state?(container()) :: boolean()
def has_state?({:container, _type, _name, metadata, _members}) do
Map.get(metadata, :has_state, false)
end
# Validate parameter structure for function_def
defp valid_param?(param) when is_binary(param), do: true
defp valid_param?({:pattern, ast}), do: conforms?(ast)
defp valid_param?({:default, name, default}) when is_binary(name), do: conforms?(default)
defp valid_param?(_), do: false
defp collect_variables({:variable, name}, acc), do: MapSet.put(acc, name)
defp collect_variables({:binary_op, _, _, left, right}, acc) do
acc = collect_variables(left, acc)
collect_variables(right, acc)
end
defp collect_variables({:unary_op, _, _, operand}, acc) do
collect_variables(operand, acc)
end
defp collect_variables({:function_call, _, args}, acc) do
Enum.reduce(args, acc, fn arg, a -> collect_variables(arg, a) end)
end
defp collect_variables({:conditional, condition, then_branch, else_branch}, acc) do
acc = collect_variables(condition, acc)
acc = collect_variables(then_branch, acc)
if else_branch, do: collect_variables(else_branch, acc), else: acc
end
defp collect_variables({:block, statements}, acc) do
Enum.reduce(statements, acc, fn stmt, a -> collect_variables(stmt, a) end)
end
defp collect_variables({:assignment, target, value}, acc) do
acc = collect_variables(target, acc)
collect_variables(value, acc)
end
defp collect_variables({:inline_match, pattern, value}, acc) do
acc = collect_variables(pattern, acc)
collect_variables(value, acc)
end
defp collect_variables({:tuple, elements}, acc) do
Enum.reduce(elements, acc, fn el, a -> collect_variables(el, a) end)
end
defp collect_variables({:list, elements}, acc) do
Enum.reduce(elements, acc, fn el, a -> collect_variables(el, a) end)
end
defp collect_variables({:map, pairs}, acc) do
Enum.reduce(pairs, acc, fn {key, value}, a ->
a = collect_variables(key, a)
collect_variables(value, a)
end)
end
defp collect_variables({:loop, :while, condition, body}, acc) do
acc = collect_variables(condition, acc)
collect_variables(body, acc)
end
defp collect_variables({:loop, _, item, collection, body}, acc) do
acc = collect_variables(item, acc)
acc = collect_variables(collection, acc)
collect_variables(body, acc)
end
defp collect_variables({:lambda, params, captures, body}, acc) do
acc = Enum.reduce(params, acc, fn p, a -> MapSet.put(a, p) end)
acc = Enum.reduce(captures, acc, fn c, a -> MapSet.put(a, c) end)
collect_variables(body, acc)
end
defp collect_variables({:collection_op, _, fn_or_pred, collection}, acc) do
acc = collect_variables(fn_or_pred, acc)
collect_variables(collection, acc)
end
defp collect_variables({:collection_op, _, fn_or_pred, collection, initial}, acc) do
acc = collect_variables(fn_or_pred, acc)
acc = collect_variables(collection, acc)
collect_variables(initial, acc)
end
defp collect_variables({:pattern_match, scrutinee, arms}, acc) do
acc = collect_variables(scrutinee, acc)
Enum.reduce(arms, acc, fn
{:match_arm, pattern, guard, body}, a ->
a = collect_variables(pattern, a)
a = if guard, do: collect_variables(guard, a), else: a
collect_variables(body, a)
{pattern, body}, a ->
a = collect_variables(pattern, a)
collect_variables(body, a)
end)
end
defp collect_variables({:exception_handling, try_block, rescue_clauses, finally_block}, acc) do
acc = collect_variables(try_block, acc)
acc =
Enum.reduce(rescue_clauses, acc, fn {_, var, body}, a ->
a = collect_variables(var, a)
collect_variables(body, a)
end)
if finally_block, do: collect_variables(finally_block, acc), else: acc
end
defp collect_variables({:async_operation, _, operation}, acc) do
collect_variables(operation, acc)
end
defp collect_variables({:early_return, value}, acc) do
collect_variables(value, acc)
end
# M2.2s: Structural/Organizational
defp collect_variables({:container, _, _, metadata, members}, acc) do
# Collect variables from members
acc = Enum.reduce(members, acc, fn member, a -> collect_variables(member, a) end)
# Also check decorators in metadata if present
case Map.get(metadata, :decorators) do
nil -> acc
decorators -> Enum.reduce(decorators, acc, fn dec, a -> collect_variables(dec, a) end)
end
end
defp collect_variables({:function_def, _, _, params, metadata, body}, acc) do
# Collect from parameters (including patterns and defaults)
acc =
Enum.reduce(params, acc, fn
param, a when is_binary(param) -> MapSet.put(a, param)
{:pattern, pattern}, a -> collect_variables(pattern, a)
{:default, name, default}, a -> collect_variables(default, MapSet.put(a, name))
end)
# Collect from guards in metadata
acc =
case Map.get(metadata, :guards) do
nil -> acc
guards -> collect_variables(guards, acc)
end
# Collect from decorators in metadata
acc =
case Map.get(metadata, :decorators) do
nil -> acc
decorators -> Enum.reduce(decorators, acc, fn dec, a -> collect_variables(dec, a) end)
end
# Collect from body
collect_variables(body, acc)
end
defp collect_variables({:attribute_access, receiver, _attribute}, acc) do
collect_variables(receiver, acc)
end
defp collect_variables({:augmented_assignment, _op, target, value}, acc) do
acc = collect_variables(target, acc)
collect_variables(value, acc)
end
defp collect_variables({:property, _name, getter, setter, _metadata}, acc) do
acc = if getter, do: collect_variables(getter, acc), else: acc
if setter, do: collect_variables(setter, acc), else: acc
end
# Language-specific: traverse embedded body if present
defp collect_variables({:language_specific, _, _, _, metadata}, acc) when is_map(metadata) do
case Map.get(metadata, :body) do
nil -> acc
body -> collect_variables(body, acc)
end
end
defp collect_variables({:language_specific, _, _, _}, acc), do: acc
defp collect_variables({:language_specific, _, _}, acc), do: acc
# Match arms with guards
defp collect_variables({:match_arm, pattern, guard, body}, acc) do
acc = collect_variables(pattern, acc)
acc = if guard, do: collect_variables(guard, acc), else: acc
collect_variables(body, acc)
end
defp collect_variables(_, acc), do: acc
end