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...]}"]
    end

Examples

# 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__/1 macro (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]}. inner is the value to be pin-matched; it is most commonly a :variable node 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 assertion assert_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 :container metadata 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 update Name{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 :pair nodes describing the per-field overrides.
  • container_type: :fsm (Cure v0.7.0) -- finite state machine container fsm 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_call nodes carrying :from, :event, :to, and :event_kind metadata.

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 process actor Name [with InitExpr]. Carries optional lifecycle callback metadata keys: :on_start, :on_message, :on_stop (each a list of :match_arm nodes). Optional :init metadata 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 supervisor sup Name. Carries optional :strategy, :intensity, :period metadata (MetaAST literal nodes). Body is a list of :child_spec nodes, one per child declared in the children block.

  • container_type: :app (Cure v0.26.0) -- OTP Application container app Name. Carries optional metadata: :vsn, :description, :root, :applications, :included_applications, :env, :registered (all MetaAST nodes), plus :on_start, :on_stop callback clause lists and :on_phase entries (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 declaration Module 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 -- :signed or :unsigned;
    • :endianness -- :big, :little, or :native;
    • :size -- a MetaAST node (typically :literal integer or :variable);
    • :unit -- integer 1..256.
  • :literal with subtype: :bytes now accepts two payload shapes: the historical binary() value, or a list of :bin_segment children 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_kind metadata 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 block node.

Extract the children or value from a MetaAST node.

Create a trivia comment node (Cure v0.20.0+).

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

Extract the name from a container node.

Decomposes a function call into its name and argument list.

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.

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).

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.

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.

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.

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.

Types

container_type()

@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 [].

core_type()

@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.

db_operation()

@type db_operation() ::
  :retrieve
  | :retrieve_all
  | :query
  | :create
  | :update
  | :delete
  | :transaction
  | :preload
  | :aggregate

Database operation types for op_kind metadata.

extended_type()

@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.

function_clause()

@type function_clause() :: %{
  params: [meta_ast()],
  guard: meta_ast() | nil,
  body: [meta_ast()]
}

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

import_type()

@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.

literal_subtype()

@type literal_subtype() ::
  :integer
  | :float
  | :string
  | :boolean
  | :null
  | :symbol
  | :regex
  | :char
  | :bytes

Semantic subtype for literals.

meta_ast()

@type meta_ast() :: {atom(), keyword(), term()}

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.

native_type()

@type native_type() :: :language_specific

Node type atoms for M2.3 Native Layer - language-specific escape hatch.

node_type()

@type node_type() :: core_type() | extended_type() | structural_type() | native_type()

All valid node type atoms.

op_kind()

@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]

operator_category()

@type operator_category() ::
  :arithmetic | :comparison | :boolean | :bitwise | :range | :string

Category for binary/unary operators.

param_kind()

@type param_kind() :: :positional | :keyword | :variadic | :keyword_variadic

Parameter kind for :param nodes.

Classifies the parameter's calling convention:

  • :positional - Regular positional argument (x in def f(x))
  • :keyword - Keyword-only argument (Python: after *; JS/TS named params)
  • :variadic - Variadic positional argument (*args in Python, ...rest in JS)
  • :keyword_variadic - Variadic keyword argument (**kwargs in Python)

If not set, :positional is assumed.

semantic_domain()

@type semantic_domain() :: :db | :http | :cache | :queue | :file | :external

Semantic domain for op_kind metadata. Domains categorize operations by their high-level purpose.

structural_type()

@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.

variable_scope()

@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., x in Elixir, x in Python)
  • :module_attribute - Module-level attributes (e.g., @attr in Elixir)
  • :global - Global/top-level variables (e.g., global x in Python)
  • :instance - Instance variables (e.g., @x in Ruby, self.x in Python)
  • :class - Class-level variables (e.g., @@x in Ruby)

Example:

{:variable, [scope: :local, line: 5], "x"}
{:variable, [scope: :module_attribute], "@moduledoc"}

visibility()

@type visibility() :: :public | :private | :protected

Visibility modifier.

Functions

bin_segment(value, meta \\ [])

@spec bin_segment(
  meta_ast(),
  keyword()
) :: meta_ast()

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"}]}

binary_op(category, operator, left, right, extra_meta \\ [])

@spec binary_op(operator_category(), atom(), meta_ast(), meta_ast(), keyword()) ::
  meta_ast()

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}]}

block(statements, meta \\ [])

@spec block(
  [meta_ast()],
  keyword()
) :: meta_ast()

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}]}

children(arg)

@spec children(meta_ast()) :: term()

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}]

comment(text, kind \\ :line, extra_meta \\ [])

@spec comment(String.t(), atom(), keyword()) :: meta_ast()

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"}

comprehension(body, generators_and_filters, meta \\ [])

@spec comprehension(meta_ast(), [meta_ast()], keyword()) :: meta_ast()

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}]}]}]}

conforms?(ast)

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

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

container(type, name, body, extra_meta \\ [])

@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"}]], []}]}

container_name(arg1)

@spec container_name(meta_ast()) :: String.t() | nil

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"

decompose_call(arg1)

@spec decompose_call(meta_ast()) :: {String.t(), [meta_ast()]} | :error

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

decorator(name, args \\ [], extra_meta \\ [])

@spec decorator(String.t(), [meta_ast()], keyword()) :: meta_ast()

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"}]}

filter(condition, meta \\ [])

@spec filter(
  meta_ast(),
  keyword()
) :: meta_ast()

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}]}]}

function_call(name, args, extra_meta \\ [])

@spec function_call(String.t(), [meta_ast()], keyword()) :: meta_ast()

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}]}

function_def(name, params, body, extra_meta \\ [])

@spec function_def(String.t(), [term()], [meta_ast()], keyword()) :: meta_ast()

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"}]}]}

function_name(arg1)

@spec function_name(meta_ast()) :: String.t() | nil

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"

function_visibility(arg1)

@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

generator(variable, collection, meta \\ [])

@spec generator(meta_ast(), meta_ast(), keyword()) :: meta_ast()

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_meta(arg, key, default \\ nil)

@spec get_meta(meta_ast(), atom(), term()) :: term()

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

has_state?(arg1)

@spec has_state?(meta_ast()) :: boolean()

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

inline_match(pattern, value, meta \\ [])

@spec inline_match(meta_ast(), meta_ast(), keyword()) :: meta_ast()

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}]}

layer(type)

@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

leaf?(arg1)

@spec leaf?(meta_ast()) :: boolean()

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

literal(subtype, value, extra_meta \\ [])

@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"}

literal?(arg1)

@spec literal?(meta_ast()) :: boolean()

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

location(arg1)

@spec location(meta_ast()) :: map() | nil

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

map_node(pairs, meta \\ [])

@spec map_node(
  [meta_ast()],
  keyword()
) :: meta_ast()

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"}]}]}

meta(arg)

@spec meta(meta_ast()) :: keyword()

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"})
[]

metadata(arg1)

@spec metadata(meta_ast()) :: keyword()

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)
[]

node_arity(ast)

@spec node_arity(meta_ast()) :: non_neg_integer() | nil

Extract arity from node metadata.

node_container(ast)

@spec node_container(meta_ast()) :: String.t() | nil

Extract container name from node metadata.

node_file(ast)

@spec node_file(meta_ast()) :: String.t() | nil

Extract file path from node metadata.

node_function(ast)

@spec node_function(meta_ast()) :: String.t() | nil

Extract function name from node metadata.

node_module(ast)

@spec node_module(meta_ast()) :: String.t() | nil

Extract module name from node metadata.

Examples

iex> ast = {:variable, [module: "MyApp.Controller"], "x"}
iex> Metastatic.AST.node_module(ast)
"MyApp.Controller"

node_visibility(ast)

@spec node_visibility(meta_ast()) :: visibility() | nil

Extract visibility from node metadata.

operator?(arg1)

@spec operator?(meta_ast()) :: boolean()

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

pair(key, value, meta \\ [])

@spec pair(meta_ast(), meta_ast(), keyword()) :: meta_ast()

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"}]}

path(ast, fun)

@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

pipe(left, right, meta \\ [operator: :|>])

@spec pipe(meta_ast(), meta_ast(), keyword()) :: meta_ast()

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 pipe
  • right - 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"], []}]}

pipe_into(expr, arg, position)

@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}]}

postwalk(ast, fun)

@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}]}

postwalk(ast, acc, fun)

@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)

postwalker(ast)

@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]

prewalk(ast, fun)

@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}]}

prewalk(ast, acc, fun)

@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)

prewalker(ast)

@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_meta(arg, key, value)

@spec put_meta(meta_ast(), atom(), term()) :: meta_ast()

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"}

range(start, stop, meta \\ [])

@spec range(meta_ast(), meta_ast(), keyword()) :: meta_ast()

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}]}

string_interpolation(parts, meta \\ [])

@spec string_interpolation(
  [meta_ast()],
  keyword()
) :: meta_ast()

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], "!"}]}

throw_node(kind \\ :raise, value \\ nil, extra_meta \\ [])

@spec throw_node(atom(), meta_ast() | nil, keyword()) :: meta_ast()

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"}]}]}

to_string(ast)

@spec to_string(meta_ast()) :: String.t()

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]"

traverse(ast, acc, pre, post)

@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.

  • pre is called before visiting children, receives {ast, acc}, returns {ast, acc}
  • post is 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)

type(arg)

@spec type(meta_ast()) :: atom()

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

type_annotation(annotation_type, target, type_expr, extra_meta \\ [])

@spec type_annotation(atom(), meta_ast(), meta_ast(), keyword()) :: meta_ast()

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()"}]}

unique_var(prefix, meta \\ [])

@spec unique_var(
  String.t(),
  keyword()
) :: meta_ast()

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

unpipe(other)

@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_children(arg, new_children)

@spec update_children(meta_ast(), term()) :: meta_ast()

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_meta(arg, updates)

@spec update_meta(
  meta_ast(),
  keyword()
) :: meta_ast()

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}

validate(ast)

@spec validate(term()) :: :ok | {:error, term()}

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"}}

variable(name, meta \\ [])

@spec variable(
  String.t(),
  keyword()
) :: meta_ast()

Create a variable node.

Examples

iex> Metastatic.AST.variable("x")
{:variable, [], "x"}

iex> Metastatic.AST.variable("count", line: 10)
{:variable, [line: 10], "count"}

variables(ast)

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

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([])

with_context(ast, context)

@spec with_context(meta_ast(), map()) :: meta_ast()

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

with_location(ast, loc)

@spec with_location(meta_ast(), map() | nil) :: meta_ast()

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"}

yield_node(kind \\ :yield, value \\ nil, extra_meta \\ [])

@spec yield_node(atom(), meta_ast() | nil, keyword()) :: meta_ast()

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}]}