Metastatic.AST
(Metastatic v0.20.3)
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
Node Structure
All MetaAST nodes are uniform 3-element tuples:
{type_atom, keyword_meta, children_or_value}Where:
type_atom- Node type (e.g.,:literal,:container,:function_def)keyword_meta- Keyword list containing metadata (line, column, subtype, etc.)children_or_value- Either a value (for leaf nodes) or list of child nodes
graph LR
subgraph "3-Tuple Structure"
A["type_atom"] --- B["keyword_meta"] --- C["children_or_value"]
end
A -->|identifies| D["Node kind"]
B -->|contains| E["Metadata"]
C -->|holds| F["Value or children"]Third Element Semantics
The third element varies by node type:
- Leaf nodes (literal, variable): The actual value (
42,"x") - Composite nodes (binary_op, function_call): List of child AST nodes
- Container nodes (container, function_def): List of body statements
graph TD
subgraph "Leaf Nodes"
L1["{:literal, [subtype: :integer], 42}"]
L2["{:variable, [], 'x'}"]
end
subgraph "Composite Nodes"
C1["{:binary_op, [operator: :+], [left, right]}"]
C2["{:function_call, [name: 'foo'], [arg1, arg2]}"]
end
subgraph "Container Nodes"
S1["{:container, [name: 'MyModule'], [body...]}"]
S2["{:function_def, [name: 'greet'], [body...]}"]
endExamples
# Literal integer
{:literal, [subtype: :integer, line: 10], 42}
# Variable
{:variable, [line: 5], "x"}
# Binary operation
{:binary_op, [category: :arithmetic, operator: :+],
[{:variable, [], "x"}, {:literal, [subtype: :integer], 5}]}
# Map with pairs
{:map, [],
[{:pair, [], [{:literal, [subtype: :symbol], :name},
{:literal, [subtype: :string], "Alice"}]}]}Semantic Enrichment (op_kind)
Function call nodes may include semantic metadata via the op_kind key.
This metadata is added during M1 -> M2 transformation by the semantic
enricher, which detects known patterns (e.g., database operations).
When present, op_kind is a keyword list with:
:domain- Semantic domain (e.g.,:db,:http,:cache):operation- Operation type within the domain (e.g.,:retrieve,:create):target- Optional target entity (e.g.,"User","orders"):async- Whether the operation is asynchronous:framework- Source framework (e.g.,:ecto,:sqlalchemy)
Example:
{:function_call,
[name: "Repo.get", op_kind: [domain: :db, operation: :retrieve, target: "User"]],
[{:variable, [], "User"}, {:variable, [], "id"}]}Analyzers can use op_kind for precise semantic detection instead of heuristics.
See Metastatic.Semantic.OpKind for the full type specification.
Import Semantics
The :import MetaAST type unifies all dependency/module-loading directives
across languages. The import_type metadata key preserves the original
language-specific semantics:
Elixir:
import_type: :import-- brings functions into scope (import MyModule)import_type: :use-- invokes__using__/1macro (use GenServer)import_type: :require-- ensures module is compiled for macros (require Logger)import_type: :alias-- creates a short name (alias MyApp.Repo)
Python: import_type: :import for both import and from...import
Ruby: import_type: :require for require, :include for include
Haskell: import_type: :import for import declarations
Erlang: import_type: :import for -import attributes
All share the common structure:
{:import, [source: "ModuleName", import_type: :import, language: :elixir], []}Adapters producing :import nodes MUST include :source (the module/package
name as a string) and :import_type (the original directive atom). The
:language key enables round-trip reconstruction of the original directive.
Traversal
Use traverse/4 for walking and transforming ASTs:
AST.traverse(ast, acc, &pre/2, &post/2)This mirrors Macro.traverse/4 from Elixir's standard library.
Dependently-Typed / Proof Extensions (Cure v0.18.0 / v0.19.0)
Three MetaAST additions were back-ported from Cure v0.18.0 -- v0.19.0. They live at the M2.2 Extended and M2.2s Structural layers so that any language with the same semantics can reuse the shapes.
:pin-- pattern-position pin operator^x. Shape{:pin, meta, [inner]}.inneris the value to be pin-matched; it is most commonly a:variablenode but may be any expression. In pattern position it constrains the subject to equal the referenced value rather than rebinding the name.:assert_type-- compile-time type assertionassert_type expr : T. Shape{:assert_type, meta, [expr, type_ast]}. Lowered by the host language's type checker; erased by the code generator so it has zero runtime cost.container_type: :proof-- new value of the:containermetadata key. Shape{:container, [container_type: :proof, name: ...], body}. Structurally identical to:module, but every binding is expected to elaborate to an equality / refinement witness.
Record / FSM Extensions (Cure v0.7.0 / v0.15.0)
Two further MetaAST additions were back-ported from earlier Cure releases that the meta-model previously lacked:
:record_update(Cure v0.15.0) -- functional record updateName{base | field: val, ...}. Shape{:record_update, [name: "Name", ...], [base | field_pairs]}. The first child is the expression supplying the original record; the remaining children are:pairnodes describing the per-field overrides.container_type: :fsm(Cure v0.7.0) -- finite state machine containerfsm Name with Payload. Shape{:container, [container_type: :fsm, name: ...], body}. Carries optional FSM-only metadata keys such as:payload,:terminal_states,:invariants,:verify,:timer,:on_transition,:on_enter,:on_exit,:on_failure,:on_timer. Body elements are typically transition:function_callnodes carrying:from,:event,:to, and:event_kindmetadata.
Actor / Supervisor / App Extensions (Cure v0.25.0 / v0.26.0)
Three container_type values were back-ported from Cure v0.25.0 and v0.26.0 into the meta-model:
container_type: :actor(Cure v0.25.0) -- typed GenServer-backed processactor Name [with InitExpr]. Carries optional lifecycle callback metadata keys::on_start,:on_message,:on_stop(each a list of:match_armnodes). Optional:initmetadata holds the initial payload expression. Body is[]; all content is hoisted onto the container meta by the parser.container_type: :supervisor(Cure v0.25.0) -- OTP supervisorsup Name. Carries optional:strategy,:intensity,:periodmetadata (MetaAST literal nodes). Body is a list of:child_specnodes, one per child declared in thechildrenblock.container_type: :app(Cure v0.26.0) -- OTP Application containerapp Name. Carries optional metadata::vsn,:description,:root,:applications,:included_applications,:env,:registered(all MetaAST nodes), plus:on_start,:on_stopcallback clause lists and:on_phaseentries (each[{phase_atom, [match_arm]}]). Body is[]; all declarative content is hoisted onto the container meta.
A companion structural node :child_spec is also added:
:child_spec-- a single supervisor child declarationModule as id [(opts)]. Shape{:child_spec, meta, []}where meta carries::module(binary) -- the child module path (dotted string);:id(binary) -- the local child identifier;:kind(:worker|:supervisor) -- whether the child is a plain worker or a nested supervisor;:restart,:shutdown, etc. -- OTP child-spec options as MetaAST literal nodes. The body is always[].
Bitstring / Comment Extensions (Cure v0.20.0)
Two M2.1 Core additions were back-ported from Cure v0.20.0 -- "The Shape of Things":
:bin_segment-- a single element of a bitstring literal / pattern such as<<x::utf8, rest::binary>>. Shape{:bin_segment, meta, [value]}. Accepted meta keys mirror Elixir's bitstring specifier grammar::type-- one of:integer,:float,:bits,:bitstring,:bytes,:binary,:utf8,:utf16,:utf32,:any;:signedness--:signedor:unsigned;:endianness--:big,:little, or:native;:size-- a MetaAST node (typically:literalinteger or:variable);:unit-- integer1..256.
:literalwithsubtype: :bytesnow accepts two payload shapes: the historicalbinary()value, or a list of:bin_segmentchildren representing the segment grammar. Walkers and path utilities treat the segment-list payload as composite so that analyzers can traverse into each segment value.:comment-- a trivia node representing a source comment. Shape{:comment, meta, text}.:comment_kindmetadata is one of:line(default, plain#or//),:doc(Elixir@doc/ Cure##), or:block(C-style). Analyzers and code generators skip comments; formatters and documentation tooling use them to round-trip source faithfully.
Summary
Types
Container type classification.
Node type atoms for M2.1 Core Layer - universal concepts.
Database operation types for op_kind metadata.
Node type atoms for M2.2 Extended Layer - common patterns.
Clause entry for grouped multi-clause function definitions.
Import type classification for :import nodes.
Semantic subtype for literals.
A MetaAST node is a 3-tuple: {type, metadata, children_or_value}.
Node type atoms for M2.3 Native Layer - language-specific escape hatch.
All valid node type atoms.
Semantic operation kind metadata added to function_call nodes. Provides precise semantic information about what a function does.
Category for binary/unary operators.
Parameter kind for :param nodes.
Semantic domain for op_kind metadata. Domains categorize operations by their high-level purpose.
Node type atoms for M2.2s Structural Layer - organizational constructs.
Scope classification for variable nodes.
Visibility modifier.
Functions
Create a bitstring segment node (Cure v0.20.0+).
Create a binary operation node.
Create a block node.
Extract the children or value from a MetaAST node.
Create a trivia comment node (Cure v0.20.0+).
Create a comprehension node.
Validate that a term conforms to the M2 meta-model.
Create a container node.
Extract the name from a container node.
Decomposes a function call into its name and argument list.
Create a decorator node.
Create a filter node.
Create a function call node.
Create a function definition node.
Extract the name from a function_def node.
Get the visibility of a function_def node.
Create a generator node.
Get a specific metadata value by key.
Check if a container has state (classes typically have state).
Create an inline match node.
Get the layer classification for a node type.
Check if a node is a leaf node (no children to traverse).
Create a literal node.
Returns true if the given MetaAST represents a literal value.
Extract location information from a MetaAST node.
Create a map node with pairs.
Extract the metadata keyword list from a MetaAST node.
Extract all metadata from a MetaAST node as a keyword list.
Extract arity from node metadata.
Extract container name from node metadata.
Extract file path from node metadata.
Extract function name from node metadata.
Extract module name from node metadata.
Extract visibility from node metadata.
Returns true if the node is a binary or unary operator.
Create a key-value pair node for maps.
Returns the path to the node in ast for which fun returns a truthy value.
Create a pipe node.
Pipes expr into the call_args at the given position.
Performs a depth-first, post-order traversal of the MetaAST.
Traverse a MetaAST with only a post function (pre is identity).
Returns an enumerable that traverses the MetaAST in depth-first, post-order.
Performs a depth-first, pre-order traversal of the MetaAST.
Traverse a MetaAST with only a pre function (post is identity).
Returns an enumerable that traverses the MetaAST in depth-first, pre-order.
Put a metadata value by key.
Create a range node.
Create a string interpolation node.
Create a throw/raise node.
Converts a MetaAST node to a human-readable string representation.
Traverse a MetaAST, applying pre and post functions.
Extract the node type from a MetaAST node.
Create a type annotation node.
Generates a unique variable MetaAST node.
Breaks a pipe chain into a flat list of {ast, position} tuples.
Update the children/value of a node.
Update multiple metadata keys at once.
Validates that the given term is a valid MetaAST node.
Create a variable node.
Extract all variable names referenced in an AST.
Merge context metadata into a node's metadata.
Attach location information to a MetaAST node.
Create a yield node.
Types
@type container_type() ::
:module
| :class
| :namespace
| :interface
| :trait
| :protocol
| :enum
| :struct
| :proof
| :fsm
| :actor
| :supervisor
| :app
Container type classification.
:proof is emitted by Cure v0.19.0+ for proof Name.Path containers;
structurally identical to :module, semantically a proposition-level
namespace.
:fsm is emitted by Cure for fsm Name with Payload containers; the
body is a list of transition :function_call nodes (carrying :from,
:event, :to, :event_kind metadata), and FSM-only metadata such
as :payload, :terminal_states, :invariants, :verify, :timer,
:on_transition, :on_enter, :on_exit, :on_failure, :on_timer
is carried on the container's own meta.
:actor is emitted by Cure v0.25.0+ for actor Name [with InitExpr]
containers. Lifecycle callbacks (:on_start, :on_message, :on_stop)
are stored as keyword list entries on meta; the body is always [].
Optional :init metadata carries the initial state expression.
:supervisor is emitted by Cure v0.25.0+ for sup Name containers.
Supervision settings (:strategy, :intensity, :period) are stored
on meta; the body is a list of :child_spec nodes.
:app is emitted by Cure v0.26.0+ for app Name containers. All
declarative content (:vsn, :description, :root, :applications,
:included_applications, :env, :registered, :on_start, :on_stop,
:on_phase) is stored on meta; the body is always [].
@type core_type() ::
:literal
| :variable
| :list
| :map
| :pair
| :tuple
| :binary_op
| :unary_op
| :function_call
| :conditional
| :early_return
| :throw
| :block
| :assignment
| :inline_match
| :range
| :string_interpolation
| :bin_segment
| :comment
Node type atoms for M2.1 Core Layer - universal concepts.
@type db_operation() ::
:retrieve
| :retrieve_all
| :query
| :create
| :update
| :delete
| :transaction
| :preload
| :aggregate
Database operation types for op_kind metadata.
@type extended_type() ::
:loop
| :lambda
| :collection_op
| :pattern_match
| :match_arm
| :exception_handling
| :async_operation
| :yield
| :comprehension
| :generator
| :filter
| :pipe
| :pin
| :assert_type
Node type atoms for M2.2 Extended Layer - common patterns.
Clause entry for grouped multi-clause function definitions.
Used in the clauses: metadata of :function_def nodes when multiple
function clauses (same name/arity) are grouped together.
Fields:
:params- Parameter list for this clause:guard- Guard expression (optional):body- List of body statements
@type import_type() ::
:import | :use | :require | :alias | :include | :from | :module | :export
Import type classification for :import nodes.
Preserves the original language directive so that from_meta can reconstruct the correct syntax. Each language maps its dependency-loading constructs to one of these atoms.
@type literal_subtype() ::
:integer
| :float
| :string
| :boolean
| :null
| :symbol
| :regex
| :char
| :bytes
Semantic subtype for literals.
A MetaAST node is a 3-tuple: {type, metadata, children_or_value}.
The type atom identifies the node kind. The metadata is a keyword list with location, subtype, and other info. The third element is either a value (leaf nodes) or list of children.
@type native_type() :: :language_specific
Node type atoms for M2.3 Native Layer - language-specific escape hatch.
@type node_type() :: core_type() | extended_type() | structural_type() | native_type()
All valid node type atoms.
@type op_kind() :: [ domain: semantic_domain(), operation: atom(), target: String.t() | nil, async: boolean(), framework: atom() | nil ]
Semantic operation kind metadata added to function_call nodes. Provides precise semantic information about what a function does.
Fields:
:domain- High-level semantic category (required):operation- Specific operation within the domain (required):target- Target entity or resource (optional):async- Whether the operation is asynchronous (optional):framework- Source framework that provides this operation (optional)
Example: [domain: :db, operation: :retrieve, target: "User", framework: :ecto]
@type operator_category() ::
:arithmetic | :comparison | :boolean | :bitwise | :range | :string
Category for binary/unary operators.
@type param_kind() :: :positional | :keyword | :variadic | :keyword_variadic
Parameter kind for :param nodes.
Classifies the parameter's calling convention:
:positional- Regular positional argument (xindef f(x)):keyword- Keyword-only argument (Python: after*; JS/TS named params):variadic- Variadic positional argument (*argsin Python,...restin JS):keyword_variadic- Variadic keyword argument (**kwargsin Python)
If not set, :positional is assumed.
@type semantic_domain() :: :db | :http | :cache | :queue | :file | :external
Semantic domain for op_kind metadata. Domains categorize operations by their high-level purpose.
@type structural_type() ::
:container
| :function_def
| :param
| :attribute_access
| :augmented_assignment
| :property
| :import
| :type_annotation
| :decorator
| :record_update
| :child_spec
Node type atoms for M2.2s Structural Layer - organizational constructs.
@type variable_scope() :: :local | :module_attribute | :global | :instance | :class
Scope classification for variable nodes.
Used in the scope metadata key of :variable nodes to distinguish
between different binding contexts:
:local- Regular local variables (e.g.,xin Elixir,xin Python):module_attribute- Module-level attributes (e.g.,@attrin Elixir):global- Global/top-level variables (e.g.,global xin Python):instance- Instance variables (e.g.,@xin Ruby,self.xin Python):class- Class-level variables (e.g.,@@xin Ruby)
Example:
{:variable, [scope: :local, line: 5], "x"}
{:variable, [scope: :module_attribute], "@moduledoc"}
@type visibility() :: :public | :private | :protected
Visibility modifier.
Functions
Create a bitstring segment node (Cure v0.20.0+).
Segments appear as children of {:literal, [subtype: :bytes], [...]}
nodes and mirror Elixir's <<value::type-size(n)-unit(u)-sign-endian>>
grammar. The value is a conforming MetaAST node; the accepted meta
keys are :type, :signedness, :endianness, :size (AST node)
and :unit (integer).
Examples
iex> v = {:variable, [], "x"}
iex> Metastatic.AST.bin_segment(v, type: :utf8)
{:bin_segment, [type: :utf8], [{:variable, [], "x"}]}
iex> v = {:variable, [], "rest"}
iex> Metastatic.AST.bin_segment(v, type: :binary)
{:bin_segment, [type: :binary], [{:variable, [], "rest"}]}
Create a binary operation node.
Examples
iex> left = {:variable, [], "x"}
iex> right = {:literal, [subtype: :integer], 5}
iex> Metastatic.AST.binary_op(:arithmetic, :+, left, right)
{:binary_op, [category: :arithmetic, operator: :+], [{:variable, [], "x"}, {:literal, [subtype: :integer], 5}]}
Create a block node.
Examples
iex> stmts = [{:variable, [], "x"}, {:literal, [subtype: :integer], 42}]
iex> Metastatic.AST.block(stmts)
{:block, [], [{:variable, [], "x"}, {:literal, [subtype: :integer], 42}]}
Extract the children or value from a MetaAST node.
For leaf nodes (literal, variable), returns the value. For composite nodes, returns the list of children.
Examples
iex> Metastatic.AST.children({:literal, [subtype: :integer], 42})
42
iex> left = {:variable, [], "x"}
iex> right = {:literal, [subtype: :integer], 5}
iex> Metastatic.AST.children({:binary_op, [operator: :+], [left, right]})
[{:variable, [], "x"}, {:literal, [subtype: :integer], 5}]
Create a trivia comment node (Cure v0.20.0+).
Comments are trivia: analyzers and code generators skip them, but
formatters and documentation tooling need them back. kind is one
of :line (plain # or //), :doc (Elixir @doc / Cure ##)
or :block (C-style /* ... */).
Examples
iex> Metastatic.AST.comment("TODO: revisit")
{:comment, [comment_kind: :line], "TODO: revisit"}
iex> Metastatic.AST.comment("Public API entry point", :doc, line: 5)
{:comment, [comment_kind: :doc, line: 5], "Public API entry point"}
Create a comprehension node.
Examples
iex> body = {:binary_op, [category: :arithmetic, operator: :*], [{:variable, [], "x"}, {:variable, [], "x"}]}
iex> gen = {:generator, [], [{:variable, [], "x"}, {:function_call, [name: "range"], [{:literal, [subtype: :integer], 10}]}]}
iex> Metastatic.AST.comprehension(body, [gen])
{:comprehension, [], [{:binary_op, [category: :arithmetic, operator: :*], [{:variable, [], "x"}, {:variable, [], "x"}]}, {:generator, [], [{:variable, [], "x"}, {:function_call, [name: "range"], [{:literal, [subtype: :integer], 10}]}]}]}
Validate that a term conforms to the M2 meta-model.
Checks that the structure is a valid 3-tuple MetaAST node with proper type, metadata, and children.
Examples
iex> Metastatic.AST.conforms?({:literal, [subtype: :integer], 42})
true
iex> Metastatic.AST.conforms?({:binary_op, [category: :arithmetic, operator: :+],
...> [{:variable, [], "x"}, {:literal, [subtype: :integer], 5}]})
true
iex> Metastatic.AST.conforms?({:invalid_node, "data"})
false
iex> Metastatic.AST.conforms?("not a tuple")
false
@spec container(container_type(), String.t(), [meta_ast()], keyword()) :: meta_ast()
Create a container node.
Examples
iex> func_def1 = {:function_def, [name: "add", params: [{:param, [], "x"}, {:param, [], "y"}]], []}
iex> func_def2 = {:function_def, [name: "sub", params: [{:param, [], "x"}, {:param, [], "y"}]], []}
iex> Metastatic.AST.container(:module, "MyApp.Math", [func_def1, func_def2])
{:container, [container_type: :module, name: "MyApp.Math"], [{:function_def, [name: "add", params: [{:param, [], "x"}, {:param, [], "y"}]], []}, {:function_def, [name: "sub", params: [{:param, [], "x"}, {:param, [], "y"}]], []}]}
Extract the name from a container node.
Examples
iex> ast = {:container, [container_type: :module, name: "MyApp.Math"], []}
iex> Metastatic.AST.container_name(ast)
"MyApp.Math"
Decomposes a function call into its name and argument list.
Returns {name, args} for :function_call nodes, or :error for
any other node type.
For dotted calls like "Repo.get", returns the full dotted name as
a string. Use String.split(name, ".") to separate receiver and method.
Mirrors Macro.decompose_call/1.
Examples
iex> call = {:function_call, [name: "add"], [{:variable, [], "x"}, {:variable, [], "y"}]}
iex> Metastatic.AST.decompose_call(call)
{"add", [{:variable, [], "x"}, {:variable, [], "y"}]}
iex> Metastatic.AST.decompose_call({:literal, [subtype: :integer], 42})
:error
Create a decorator node.
Examples
iex> Metastatic.AST.decorator("staticmethod")
{:decorator, [name: "staticmethod"], []}
iex> Metastatic.AST.decorator("app.route", [{:literal, [subtype: :string], "/api"}])
{:decorator, [name: "app.route"], [{:literal, [subtype: :string], "/api"}]}
Create a filter node.
Examples
iex> condition = {:binary_op, [category: :comparison, operator: :>], [{:variable, [], "x"}, {:literal, [subtype: :integer], 0}]}
iex> Metastatic.AST.filter(condition)
{:filter, [], [{:binary_op, [category: :comparison, operator: :>], [{:variable, [], "x"}, {:literal, [subtype: :integer], 0}]}]}
Create a function call node.
Examples
iex> args = [{:variable, [], "x"}, {:literal, [subtype: :integer], 5}]
iex> Metastatic.AST.function_call("add", args)
{:function_call, [name: "add"], [{:variable, [], "x"}, {:literal, [subtype: :integer], 5}]}
Create a function definition node.
Examples
iex> body = [{:binary_op, [category: :arithmetic, operator: :+],
...> [{:variable, [], "x"}, {:variable, [], "y"}]}]
iex> Metastatic.AST.function_def("add", ["x", "y"], body)
{:function_def, [name: "add", params: [{:param, [], "x"}, {:param, [], "y"}]], [{:binary_op, [category: :arithmetic, operator: :+], [{:variable, [], "x"}, {:variable, [], "y"}]}]}
Extract the name from a function_def node.
Examples
iex> body = {:binary_op, [operator: :+], [{:variable, [], "x"}, {:variable, [], "y"}]}
iex> ast = {:function_def, [name: "add", params: [{:param, [], "x"}, {:param, [], "y"}]], [body]}
iex> Metastatic.AST.function_name(ast)
"add"
@spec function_visibility(meta_ast()) :: visibility()
Get the visibility of a function_def node.
Examples
iex> body = {:binary_op, [operator: :+], [{:variable, [], "x"}, {:variable, [], "y"}]}
iex> ast = {:function_def, [name: "add", visibility: :public], [body]}
iex> Metastatic.AST.function_visibility(ast)
:public
Create a generator node.
Examples
iex> var = {:variable, [], "x"}
iex> coll = {:function_call, [name: "range"], [{:literal, [subtype: :integer], 10}]}
iex> Metastatic.AST.generator(var, coll)
{:generator, [], [{:variable, [], "x"}, {:function_call, [name: "range"], [{:literal, [subtype: :integer], 10}]}]}
Get a specific metadata value by key.
Examples
iex> ast = {:literal, [subtype: :integer, line: 10], 42}
iex> Metastatic.AST.get_meta(ast, :line)
10
iex> ast = {:variable, [], "x"}
iex> Metastatic.AST.get_meta(ast, :line)
nil
iex> ast = {:variable, [], "x"}
iex> Metastatic.AST.get_meta(ast, :line, 0)
0
Check if a container has state (classes typically have state).
Examples
iex> ast = {:container, [container_type: :class, name: "Counter"], []}
iex> Metastatic.AST.has_state?(ast)
true
iex> ast = {:container, [container_type: :module, name: "Math"], []}
iex> Metastatic.AST.has_state?(ast)
false
Create an inline match node.
Examples
iex> pattern = {:variable, [], "x"}
iex> value = {:literal, [subtype: :integer], 42}
iex> Metastatic.AST.inline_match(pattern, value)
{:inline_match, [], [{:variable, [], "x"}, {:literal, [subtype: :integer], 42}]}
@spec layer(atom()) :: :core | :extended | :structural | :native | :unknown
Get the layer classification for a node type.
Returns :core, :extended, :structural, or :native.
Examples
iex> Metastatic.AST.layer(:literal)
:core
iex> Metastatic.AST.layer(:lambda)
:extended
iex> Metastatic.AST.layer(:container)
:structural
iex> Metastatic.AST.layer(:language_specific)
:native
Check if a node is a leaf node (no children to traverse).
Examples
iex> Metastatic.AST.leaf?({:literal, [subtype: :integer], 42})
true
iex> Metastatic.AST.leaf?({:variable, [], "x"})
true
iex> left = {:variable, [], "x"}
iex> right = {:literal, [subtype: :integer], 5}
iex> Metastatic.AST.leaf?({:binary_op, [], [left, right]})
false
@spec literal(literal_subtype(), term(), keyword()) :: meta_ast()
Create a literal node.
Examples
iex> Metastatic.AST.literal(:integer, 42)
{:literal, [subtype: :integer], 42}
iex> Metastatic.AST.literal(:string, "hello", line: 5)
{:literal, [subtype: :string, line: 5], "hello"}
Returns true if the given MetaAST represents a literal value.
A node is considered literal if it is a :literal node, or a composite
node (:list, :tuple, :map, :pair) whose children are all literals.
Mirrors Macro.quoted_literal?/1.
Examples
iex> Metastatic.AST.literal?({:literal, [subtype: :integer], 42})
true
iex> Metastatic.AST.literal?({:list, [], [{:literal, [subtype: :integer], 1}, {:literal, [subtype: :integer], 2}]})
true
iex> Metastatic.AST.literal?({:variable, [], "x"})
false
iex> Metastatic.AST.literal?({:list, [], [{:variable, [], "x"}]})
false
Extract location information from a MetaAST node.
Returns a map with :line, :col, :end_line, :end_col if present in metadata.
Examples
iex> ast = {:literal, [subtype: :integer, line: 10, col: 5], 42}
iex> Metastatic.AST.location(ast)
%{line: 10, col: 5}
iex> ast = {:variable, [], "x"}
iex> Metastatic.AST.location(ast)
nil
Create a map node with pairs.
Examples
iex> pairs = [Metastatic.AST.pair(
...> {:literal, [subtype: :symbol], :name},
...> {:literal, [subtype: :string], "Alice"})]
iex> Metastatic.AST.map_node(pairs)
{:map, [], [{:pair, [], [{:literal, [subtype: :symbol], :name}, {:literal, [subtype: :string], "Alice"}]}]}
Extract the metadata keyword list from a MetaAST node.
Examples
iex> Metastatic.AST.meta({:literal, [subtype: :integer, line: 10], 42})
[subtype: :integer, line: 10]
iex> Metastatic.AST.meta({:variable, [], "x"})
[]
Extract all metadata from a MetaAST node as a keyword list.
Returns the full metadata including location, M1 context (module, function, arity, etc.), and node-specific metadata (subtype, operator, etc.).
Examples
iex> ast = {:literal, [subtype: :integer, line: 10], 42}
iex> Metastatic.AST.metadata(ast)
[subtype: :integer, line: 10]
iex> ast = {:variable, [line: 5, module: "MyApp", function: "create"], "x"}
iex> Metastatic.AST.metadata(ast)
[line: 5, module: "MyApp", function: "create"]
iex> ast = {:variable, [], "x"}
iex> Metastatic.AST.metadata(ast)
[]
@spec node_arity(meta_ast()) :: non_neg_integer() | nil
Extract arity from node metadata.
Extract container name from node metadata.
Extract file path from node metadata.
Extract function name from node metadata.
Extract module name from node metadata.
Examples
iex> ast = {:variable, [module: "MyApp.Controller"], "x"}
iex> Metastatic.AST.node_module(ast)
"MyApp.Controller"
@spec node_visibility(meta_ast()) :: visibility() | nil
Extract visibility from node metadata.
Returns true if the node is a binary or unary operator.
Examples
iex> Metastatic.AST.operator?({:binary_op, [category: :arithmetic, operator: :+], [{:variable, [], "x"}, {:literal, [subtype: :integer], 1}]})
true
iex> Metastatic.AST.operator?({:unary_op, [category: :arithmetic, operator: :-], [{:variable, [], "x"}]})
true
iex> Metastatic.AST.operator?({:literal, [subtype: :integer], 42})
false
Create a key-value pair node for maps.
Examples
iex> key = {:literal, [subtype: :symbol], :name}
iex> value = {:literal, [subtype: :string], "Alice"}
iex> Metastatic.AST.pair(key, value)
{:pair, [], [{:literal, [subtype: :symbol], :name}, {:literal, [subtype: :string], "Alice"}]}
@spec path(meta_ast(), (meta_ast() -> as_boolean(term()))) :: [meta_ast()] | nil
Returns the path to the node in ast for which fun returns a truthy value.
The path is a list, starting with the node for which fun returns a truthy
value, followed by all of its parents up to the root. Returns nil if no
node matches.
Mirrors Macro.path/2.
Examples
iex> ast = {:binary_op, [category: :arithmetic, operator: :+],
...> [{:variable, [], "x"}, {:literal, [subtype: :integer], 42}]}
iex> path = Metastatic.AST.path(ast, fn
...> {:literal, _, 42} -> true
...> _ -> false
...> end)
iex> Enum.map(path, &Metastatic.AST.type/1)
[:literal, :binary_op]
iex> ast = {:literal, [subtype: :integer], 1}
iex> Metastatic.AST.path(ast, fn {:variable, _, _} -> true; _ -> false end)
nil
Create a pipe node.
Preserves the pipe operator (|>, ->, etc.) as a first-class node
rather than desugaring it into a function call. Useful for languages
where the pipe has distinct syntactic or semantic meaning.
Parameters
left- Left-hand side of the piperight- Right-hand side (function to pipe into)meta- Optional metadata (default:[operator: :|>])
Examples
iex> left = {:variable, [], "x"}
iex> right = {:function_call, [name: "inspect"], []}
iex> Metastatic.AST.pipe(left, right)
{:pipe, [operator: :|>], [{:variable, [], "x"}, {:function_call, [name: "inspect"], []}]}
@spec pipe_into(meta_ast(), meta_ast(), non_neg_integer()) :: meta_ast()
Pipes expr into the call_args at the given position.
Inserts expr as an argument into a function call node at the
specified 0-indexed position. If call_args is a :function_call
node, the expression is injected into its argument list.
Mirrors Macro.pipe/3.
Examples
iex> expr = {:variable, [], "x"}
iex> call = {:function_call, [name: "foo"], [{:literal, [subtype: :integer], 1}]}
iex> Metastatic.AST.pipe_into(expr, call, 0)
{:function_call, [name: "foo"], [{:variable, [], "x"}, {:literal, [subtype: :integer], 1}]}
@spec postwalk(meta_ast() | term(), (meta_ast() | term() -> meta_ast() | term())) :: meta_ast() | term()
Performs a depth-first, post-order traversal of the MetaAST.
Returns a new AST where each node is the result of invoking fun
on each corresponding node. This is the transform-only variant
(no accumulator), mirroring Macro.postwalk/2.
Examples
iex> ast = {:binary_op, [category: :arithmetic, operator: :+],
...> [{:literal, [subtype: :integer], 1}, {:literal, [subtype: :integer], 2}]}
iex> Metastatic.AST.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}]}
@spec postwalk(meta_ast() | term(), acc, (meta_ast() | term(), acc -> {meta_ast() | term(), acc})) :: {meta_ast() | term(), acc} when acc: term()
Traverse a MetaAST with only a post function (pre is identity).
Examples
{new_ast, acc} = AST.postwalk(ast, [], fn node, acc -> {node, acc} end)
@spec postwalker(meta_ast()) :: Enumerable.t()
Returns an enumerable that traverses the MetaAST in depth-first, post-order.
Mirrors Macro.postwalker/1. The returned enumerable lazily yields each node
after its children.
Examples
iex> ast = {:binary_op, [category: :arithmetic, operator: :+],
...> [{:literal, [subtype: :integer], 1}, {:literal, [subtype: :integer], 2}]}
iex> ast |> Metastatic.AST.postwalker() |> Enum.map(&Metastatic.AST.type/1)
[:literal, :literal, :binary_op]
@spec prewalk(meta_ast() | term(), (meta_ast() | term() -> meta_ast() | term())) :: meta_ast() | term()
Performs a depth-first, pre-order traversal of the MetaAST.
Returns a new AST where each node is the result of invoking fun
on each corresponding node. This is the transform-only variant
(no accumulator), mirroring Macro.prewalk/2.
Examples
iex> ast = {:binary_op, [category: :arithmetic, operator: :+],
...> [{:literal, [subtype: :integer], 1}, {:literal, [subtype: :integer], 2}]}
iex> Metastatic.AST.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}]}
@spec prewalk(meta_ast() | term(), acc, (meta_ast() | term(), acc -> {meta_ast() | term(), acc})) :: {meta_ast() | term(), acc} when acc: term()
Traverse a MetaAST with only a pre function (post is identity).
Examples
{new_ast, acc} = AST.prewalk(ast, [], fn node, acc -> {node, acc} end)
@spec prewalker(meta_ast()) :: Enumerable.t()
Returns an enumerable that traverses the MetaAST in depth-first, pre-order.
Mirrors Macro.prewalker/1. The returned enumerable lazily yields each node
before its children.
Examples
iex> ast = {:binary_op, [category: :arithmetic, operator: :+],
...> [{:literal, [subtype: :integer], 1}, {:literal, [subtype: :integer], 2}]}
iex> ast |> Metastatic.AST.prewalker() |> Enum.map(&Metastatic.AST.type/1)
[:binary_op, :literal, :literal]
Put a metadata value by key.
Examples
iex> ast = {:literal, [subtype: :integer], 42}
iex> Metastatic.AST.put_meta(ast, :line, 10)
{:literal, [line: 10, subtype: :integer], 42}
iex> ast = {:variable, [line: 5], "x"}
iex> Metastatic.AST.put_meta(ast, :line, 10)
{:variable, [line: 10], "x"}
Create a range node.
Examples
iex> start = {:literal, [subtype: :integer], 1}
iex> stop = {:literal, [subtype: :integer], 10}
iex> Metastatic.AST.range(start, stop)
{:range, [], [{:literal, [subtype: :integer], 1}, {:literal, [subtype: :integer], 10}]}
iex> start = {:literal, [subtype: :integer], 1}
iex> stop = {:literal, [subtype: :integer], 10}
iex> step = {:literal, [subtype: :integer], 2}
iex> Metastatic.AST.range(start, stop, step: step)
{:range, [step: {:literal, [subtype: :integer], 2}], [{:literal, [subtype: :integer], 1}, {:literal, [subtype: :integer], 10}]}
Create a string interpolation node.
Examples
iex> parts = [{:literal, [subtype: :string], "Hello, "}, {:variable, [], "name"}, {:literal, [subtype: :string], "!"}]
iex> Metastatic.AST.string_interpolation(parts)
{:string_interpolation, [], [{:literal, [subtype: :string], "Hello, "}, {:variable, [], "name"}, {:literal, [subtype: :string], "!"}]}
Create a throw/raise node.
Examples
iex> exception = {:function_call, [name: "ValueError"], [{:literal, [subtype: :string], "bad value"}]}
iex> Metastatic.AST.throw_node(:raise, exception)
{:throw, [kind: :raise], [{:function_call, [name: "ValueError"], [{:literal, [subtype: :string], "bad value"}]}]}
Converts a MetaAST node to a human-readable string representation.
Produces a compact notation that conveys the structure without reproducing full Elixir tuple syntax. Useful for debugging and logging.
Mirrors Macro.to_string/1.
Examples
iex> Metastatic.AST.to_string({:literal, [subtype: :integer], 42})
"42"
iex> Metastatic.AST.to_string({:variable, [], "x"})
"x"
iex> ast = {:binary_op, [category: :arithmetic, operator: :+],
...> [{:variable, [], "x"}, {:literal, [subtype: :integer], 5}]}
iex> Metastatic.AST.to_string(ast)
"x + 5"
iex> ast = {:function_call, [name: "foo"], [{:variable, [], "x"}, {:literal, [subtype: :integer], 1}]}
iex> Metastatic.AST.to_string(ast)
"foo(x, 1)"
iex> ast = {:list, [], [{:literal, [subtype: :integer], 1}, {:literal, [subtype: :integer], 2}]}
iex> Metastatic.AST.to_string(ast)
"[1, 2]"
@spec traverse(meta_ast() | term(), acc, pre_fun, post_fun) :: {meta_ast() | term(), acc} when acc: term(), pre_fun: (meta_ast() | term(), acc -> {meta_ast() | term(), acc}), post_fun: (meta_ast() | term(), acc -> {meta_ast() | term(), acc})
Traverse a MetaAST, applying pre and post functions.
This mirrors Macro.traverse/4 from Elixir's standard library.
preis called before visiting children, receives{ast, acc}, returns{ast, acc}postis called after visiting children, receives{ast, acc}, returns{ast, acc}
The traversal visits children based on the node type:
- Leaf nodes (literal, variable): No children to visit
- Composite nodes: Visits each child in the list
- Non-AST values: Passed through unchanged
Examples
# Count all nodes
{_ast, count} = AST.traverse(ast, 0, fn node, acc -> {node, acc + 1} end, fn node, acc -> {node, acc} end)
# Collect all variable names
{_ast, vars} = AST.traverse(ast, [], fn
{:variable, _, name} = node, acc -> {node, [name | acc]}
node, acc -> {node, acc}
end, fn node, acc -> {node, acc} end)
# Transform all integers by doubling them
{new_ast, _} = AST.traverse(ast, nil, fn node, acc -> {node, acc} end, fn
{:literal, meta, value} = node, acc ->
if Keyword.get(meta, :subtype) == :integer do
{{:literal, meta, value * 2}, acc}
else
{node, acc}
end
node, acc -> {node, acc}
end)
Extract the node type from a MetaAST node.
Examples
iex> Metastatic.AST.type({:literal, [subtype: :integer], 42})
:literal
iex> left = {:variable, [], "x"}
iex> right = {:literal, [subtype: :integer], 5}
iex> Metastatic.AST.type({:binary_op, [operator: :+], [left, right]})
:binary_op
Create a type annotation node.
Examples
iex> target = {:function_call, [name: "add"], []}
iex> type_expr = {:literal, [subtype: :string], "integer() :: integer() -> integer()"}
iex> Metastatic.AST.type_annotation(:spec, target, type_expr)
{:type_annotation, [annotation_type: :spec], [{:function_call, [name: "add"], []}, {:literal, [subtype: :string], "integer() :: integer() -> integer()"}]}
Generates a unique variable MetaAST node.
Each call produces a variable with a unique name based on the given prefix and a monotonically increasing counter. Useful for code transformations that need to introduce fresh bindings.
Mirrors Macro.unique_var/2.
Examples
iex> {:variable, _, name1} = Metastatic.AST.unique_var("tmp")
iex> {:variable, _, name2} = Metastatic.AST.unique_var("tmp")
iex> name1 != name2
true
iex> {:variable, _, name} = Metastatic.AST.unique_var("arg")
iex> String.starts_with?(name, "arg_")
true
@spec unpipe(meta_ast()) :: [{meta_ast(), non_neg_integer()}]
Breaks a pipe chain into a flat list of {ast, position} tuples.
Each tuple contains the piped expression and the argument position
(0-indexed) where it was injected. For the leftmost expression (the
initial value), position is 0.
Mirrors Macro.unpipe/1.
Examples
iex> inner_pipe = {:pipe, [operator: :|>], [
...> {:variable, [], "x"},
...> {:function_call, [name: "foo"], []}
...> ]}
iex> outer_pipe = {:pipe, [operator: :|>], [
...> inner_pipe,
...> {:function_call, [name: "bar"], []}
...> ]}
iex> Metastatic.AST.unpipe(outer_pipe)
...> |> Enum.map(fn {node, pos} -> {Metastatic.AST.type(node), pos} end)
[variable: 0, function_call: 0, function_call: 0]
Update the children/value of a node.
Examples
iex> old_left = {:variable, [], "x"}
iex> old_right = {:literal, [subtype: :integer], 5}
iex> ast = {:binary_op, [operator: :+], [old_left, old_right]}
iex> new_left = {:variable, [], "y"}
iex> new_right = {:literal, [subtype: :integer], 10}
iex> Metastatic.AST.update_children(ast, [new_left, new_right])
{:binary_op, [operator: :+], [{:variable, [], "y"}, {:literal, [subtype: :integer], 10}]}
Update multiple metadata keys at once.
Examples
iex> ast = {:literal, [subtype: :integer], 42}
iex> Metastatic.AST.update_meta(ast, line: 10, col: 5)
{:literal, [subtype: :integer, line: 10, col: 5], 42}
Validates that the given term is a valid MetaAST node.
Returns :ok if valid, or {:error, reason} with a description of
the first encountered problem. Unlike conforms?/1 which returns a
boolean, this provides diagnostic information.
Mirrors Macro.validate/1.
Examples
iex> Metastatic.AST.validate({:literal, [subtype: :integer], 42})
:ok
iex> Metastatic.AST.validate({:literal, [subtype: :integer], "not_an_int"})
{:error, {:invalid_node, {:literal, [subtype: :integer], "not_an_int"}}}
iex> Metastatic.AST.validate("not an ast")
{:error, {:not_an_ast_node, "not an ast"}}
Create a variable node.
Examples
iex> Metastatic.AST.variable("x")
{:variable, [], "x"}
iex> Metastatic.AST.variable("count", line: 10)
{:variable, [line: 10], "count"}
Extract all variable names referenced in an AST.
Uses traverse/4 internally to collect all variable nodes.
Examples
iex> ast = {:binary_op, [category: :arithmetic, operator: :+],
...> [{:variable, [], "x"}, {:variable, [], "y"}]}
iex> Metastatic.AST.variables(ast)
MapSet.new(["x", "y"])
iex> ast = {:literal, [subtype: :integer], 42}
iex> Metastatic.AST.variables(ast)
MapSet.new([])
Merge context metadata into a node's metadata.
Used for attaching M1-level context like module name, function name, etc. Note: Order of merged keys is not guaranteed due to map iteration order.
Examples
iex> ast = {:variable, [line: 10], "x"}
iex> context = %{module: "MyApp"}
iex> {:variable, meta, "x"} = Metastatic.AST.with_context(ast, context)
iex> Keyword.get(meta, :module)
"MyApp"
iex> Keyword.get(meta, :line)
10
Attach location information to a MetaAST node.
Examples
iex> ast = {:literal, [subtype: :integer], 42}
iex> Metastatic.AST.with_location(ast, %{line: 10, col: 5})
{:literal, [subtype: :integer, line: 10, col: 5], 42}
iex> ast = {:variable, [], "x"}
iex> Metastatic.AST.with_location(ast, nil)
{:variable, [], "x"}
Create a yield node.
Examples
iex> value = {:literal, [subtype: :integer], 42}
iex> Metastatic.AST.yield_node(:yield, value)
{:yield, [kind: :yield], [{:literal, [subtype: :integer], 42}]}