Search documentation of Metastatic

# Metastatic.AST (Metastatic v0.13.2)

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

## 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

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

## Summary

### Types

container_type()

Container type classification.

core_type()

Node type atoms for M2.1 Core Layer - universal concepts.

db_operation()

Database operation types for op_kind metadata.

extended_type()

Node type atoms for M2.2 Extended Layer - common patterns.

import_type()

Import type classification for `:import` nodes.

literal_subtype()

Semantic subtype for literals.

meta_ast()

A MetaAST node is a 3-tuple: {type, metadata, children_or_value}.

native_type()

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

node_type()

All valid node type atoms.

op_kind()

Semantic operation kind metadata added to function_call nodes. Provides precise semantic information about what a function does.

operator_category()

Category for binary/unary operators.

semantic_domain()

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

structural_type()

Node type atoms for M2.2s Structural Layer - organizational constructs.

variable_scope()

Scope classification for variable nodes.

visibility()

Visibility modifier.

### Functions

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

Create a binary operation node.

block(statements, meta \\ [])

Create a block node.

children(arg)

Extract the children or value from a MetaAST node.

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

Create a comprehension node.

conforms?(ast)

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

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

Create a container node.

container_name(arg1)

Extract the name from a container node.

filter(condition, meta \\ [])

Create a filter node.

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

Create a function call node.

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

Create a function definition node.

function_name(arg1)

Extract the name from a function_def node.

function_visibility(arg1)

Get the visibility of a function_def node.

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

Create a generator node.

get_meta(arg, key, default \\ nil)

Get a specific metadata value by key.

has_state?(arg1)

Check if a container has state (classes typically have state).

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

Create an inline match node.

layer(type)

Get the layer classification for a node type.

leaf?(arg1)

Check if a node is a leaf node (no children to traverse).

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

Create a literal node.

location(arg1)

Extract location information from a MetaAST node.

map_node(pairs, meta \\ [])

Create a map node with pairs.

meta(arg)

Extract the metadata keyword list from a MetaAST node.

metadata(arg1)

Extract all metadata from a MetaAST node as a keyword list.

node_arity(ast)

Extract arity from node metadata.

node_container(ast)

Extract container name from node metadata.

node_file(ast)

Extract file path from node metadata.

node_function(ast)

Extract function name from node metadata.

node_module(ast)

Extract module name from node metadata.

node_visibility(ast)

Extract visibility from node metadata.

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

Create a key-value pair node for maps.

postwalk(ast, acc, fun)

Traverse a MetaAST with only a post function (pre is identity).

prewalk(ast, acc, fun)

Traverse a MetaAST with only a pre function (post is identity).

put_meta(arg, key, value)

Put a metadata value by key.

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

Create a range node.

string_interpolation(parts, meta \\ [])

Create a string interpolation node.

traverse(ast, acc, pre, post)

Traverse a MetaAST, applying pre and post functions.

type(arg)

Extract the node type from a MetaAST node.

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

Create a type annotation node.

update_children(arg, new_children)

Update the children/value of a node.

update_meta(arg, updates)

Update multiple metadata keys at once.

variable(name, meta \\ [])

Create a variable node.

variables(ast)

Extract all variable names referenced in an AST.

with_context(ast, context)

Merge context metadata into a node's metadata.

with_location(ast, loc)

Attach location information to a MetaAST node.

## Types

# container_type()

    @type container_type() :: :module | :class | :namespace

Container type classification.

# core_type()

    @type core_type() ::
      :literal
      | :variable
      | :list
      | :map
      | :pair
      | :tuple
      | :binary_op
      | :unary_op
      | :function_call
      | :conditional
      | :early_return
      | :block
      | :assignment
      | :inline_match
      | :range
      | :string_interpolation

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
      | :comprehension
      | :generator
      | :filter

Node type atoms for M2.2 Extended Layer - common patterns.

# import_type()

    @type import_type() :: :import | :use | :require | :alias | :include

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

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 | :range | :string

Category for binary/unary operators.

# 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

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

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

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

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

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

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

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

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

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

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

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

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