Packages
metastatic
0.14.0
0.26.0
0.25.0
0.24.1
0.24.0
0.23.0
0.22.2
0.22.1
0.22.0
0.21.3
0.21.2
0.21.1
0.21.0
0.20.3
0.20.2
0.20.1
0.20.0
0.19.0
0.18.0
0.17.0
0.16.0
0.15.1
0.15.0
0.14.2
0.14.1
0.14.0
0.13.3
0.13.2
0.13.1
0.13.0
0.12.0
0.11.0
0.10.4
0.10.3
0.10.2
0.10.1
0.10.0
0.9.2
0.9.1
0.9.0
0.8.6
0.8.5
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.7
0.7.6
0.7.5
0.7.4
0.7.3
0.7.1
0.7.0
0.6.1
0.6.0
0.5.2
0.5.1
0.5.0
0.4.2
0.4.1
0.4.0
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.0
0.1.3
0.1.2
0.1.1
0.1.0
Cross-language code meta-model library using unified MetaAST representation. Parse, transform, and translate code across Python, Elixir, Ruby, Erlang, Haskell, and more via a shared three-tuple AST format.
Current section
Files
Jump to
Current section
Files
lib/metastatic/analysis/purity.ex
defmodule Metastatic.Analysis.Purity do
@moduledoc """
Function purity analysis at the MetaAST level.
Analyzes code to determine if it's pure (no side effects) or impure
(has side effects like I/O, mutations, random operations, etc.).
Works across all supported languages by operating on the unified MetaAST
representation.
## Purity Definition
A **pure function**:
- Always returns the same output for the same input (deterministic)
- Has no side effects (no I/O, no mutations, no global state access)
- Doesn't depend on external state
An **impure function** has one or more of:
- I/O operations (print, file access, network, database)
- Mutations (modifying variables, especially in loops)
- Non-deterministic operations (random, time/date)
- Exception handling (raising/catching exceptions)
## Usage
alias Metastatic.{Document, Analysis.Purity}
# Analyze a document
ast = {:binary_op, :arithmetic, :+, {:variable, "x"}, {:literal, :integer, 5}}
doc = Document.new(ast, :elixir)
{:ok, result} = Purity.analyze(doc)
result.pure? # => true
result.effects # => []
result.confidence # => :high
result.summary # => "Function is pure"
## Examples
# Pure arithmetic
iex> ast = {:binary_op, [category: :arithmetic, operator: :+], [{:literal, [subtype: :integer], 1}, {:literal, [subtype: :integer], 2}]}
iex> doc = Metastatic.Document.new(ast, :python)
iex> {:ok, result} = Metastatic.Analysis.Purity.analyze(doc)
iex> result.pure?
true
# Impure I/O
iex> ast = {:function_call, [name: "print"], [{:literal, [subtype: :string], "hello"}]}
iex> doc = Metastatic.Document.new(ast, :python)
iex> {:ok, result} = Metastatic.Analysis.Purity.analyze(doc)
iex> result.pure?
false
iex> result.effects
[:io]
"""
alias Metastatic.Analysis.Purity.{Effects, Result}
alias Metastatic.Document
use Metastatic.Document.Analyzer,
doc: """
Analyzes a document for purity.
Accepts either:
- A `Metastatic.Document` struct
- A `{language, native_ast}` tuple
Returns `{:ok, result}` where result is a `Metastatic.Analysis.Purity.Result` struct.
## Examples
# Using Document
iex> ast = {:literal, [subtype: :integer], 42}
iex> doc = Metastatic.Document.new(ast, :elixir)
iex> {:ok, result} = Metastatic.Analysis.Purity.analyze(doc)
iex> result.pure?
true
# Using {language, native_ast} tuple
iex> python_ast = %{"_type" => "Constant", "value" => 42}
iex> {:ok, result} = Metastatic.Analysis.Purity.analyze(:python, python_ast)
iex> result.pure?
true
"""
@impl Metastatic.Document.Analyzer
def handle_analyze(%Document{ast: ast}, _opts \\ []) do
result =
ast
|> walk(%{in_loop: false, effects: [], locations: [], unknown: []})
|> build_result()
{:ok, result}
end
# Private implementation
defp walk(ast, ctx) do
# Detect effects in current node
effects = Effects.detect(ast)
ctx = add_effects(ctx, effects)
# Recurse based on node type
walk_node(ast, ctx)
end
# Binary op (3-tuple)
defp walk_node({:binary_op, _meta, [left, right]}, ctx) do
ctx = walk(left, ctx)
walk(right, ctx)
end
# Unary op (3-tuple)
defp walk_node({:unary_op, _meta, [operand]}, ctx), do: walk(operand, ctx)
# Conditional (3-tuple)
defp walk_node({:conditional, _meta, [cond_expr, then_br, else_br]}, ctx) do
ctx = walk(cond_expr, ctx)
ctx = walk(then_br, ctx)
walk(else_br, ctx)
end
# Block (3-tuple)
defp walk_node({:block, _meta, stmts}, ctx) when is_list(stmts) do
Enum.reduce(stmts, ctx, fn stmt, c -> walk(stmt, c) end)
end
# Loop (3-tuple)
defp walk_node({:loop, meta, children}, ctx) when is_list(meta) do
loop_type = Keyword.get(meta, :loop_type)
loop_ctx = %{ctx | in_loop: true}
case {loop_type, children} do
{:while, [cond_expr, body]} ->
loop_ctx = walk(cond_expr, loop_ctx)
walk(body, loop_ctx)
{_, [iter, coll, body]} ->
loop_ctx = walk(iter, loop_ctx)
loop_ctx = walk(coll, loop_ctx)
walk(body, loop_ctx)
_ ->
Enum.reduce(children, loop_ctx, fn child, c -> walk(child, c) end)
end
end
# Assignment (3-tuple)
defp walk_node({:assignment, _meta, [target, value]}, ctx) do
ctx = if ctx.in_loop, do: add_effects(ctx, [:mutation]), else: ctx
ctx = walk(target, ctx)
walk(value, ctx)
end
# Inline match (3-tuple)
defp walk_node({:inline_match, _meta, [pattern, value]}, ctx) do
ctx = walk(pattern, ctx)
walk(value, ctx)
end
# Function call (3-tuple)
defp walk_node({:function_call, meta, args}, ctx) when is_list(meta) and is_list(args) do
name = Keyword.get(meta, :name)
ctx =
if Effects.detect({:function_call, meta, args}) == [] and is_binary(name) do
%{ctx | unknown: [name | ctx.unknown]}
else
ctx
end
Enum.reduce(args, ctx, fn arg, c -> walk(arg, c) end)
end
# Lambda (3-tuple)
defp walk_node({:lambda, _meta, [body]}, ctx), do: walk(body, ctx)
# Collection operations (3-tuple)
defp walk_node({:collection_op, _meta, children}, ctx) when is_list(children) do
Enum.reduce(children, ctx, fn child, c -> walk(child, c) end)
end
# Exception handling (3-tuple)
defp walk_node({:exception_handling, _meta, [try_b, catches, else_b]}, ctx) do
ctx = walk(try_b, ctx)
catches_list = if is_list(catches), do: catches, else: []
ctx = Enum.reduce(catches_list, ctx, fn catch_clause, c -> walk(catch_clause, c) end)
walk(else_b, ctx)
end
# Early return (3-tuple)
defp walk_node({:early_return, _meta, [value]}, ctx), do: walk(value, ctx)
# List (3-tuple)
defp walk_node({:list, _meta, elems}, ctx) when is_list(elems) do
Enum.reduce(elems, ctx, fn elem, c -> walk(elem, c) end)
end
# Map (3-tuple)
defp walk_node({:map, _meta, pairs}, ctx) when is_list(pairs) do
Enum.reduce(pairs, ctx, fn
{:pair, _, [key, value]}, c ->
c = walk(key, c)
walk(value, c)
{key, value}, c ->
c = walk(key, c)
walk(value, c)
other, c ->
walk(other, c)
end)
end
# Language-specific (3-tuple)
defp walk_node({:language_specific, _meta, _native_ast}, ctx), do: ctx
# Literals and variables (3-tuple)
defp walk_node({:literal, _meta, _value}, ctx), do: ctx
defp walk_node({:variable, _meta, _name}, ctx), do: ctx
# Pair (3-tuple)
defp walk_node({:pair, _meta, [key, value]}, ctx) do
ctx = walk(key, ctx)
walk(value, ctx)
end
# Fallback
defp walk_node(_, ctx), do: ctx
defp add_effects(ctx, []), do: ctx
defp add_effects(ctx, effects), do: %{ctx | effects: ctx.effects ++ effects}
defp build_result(%{effects: [], unknown: []}), do: Result.pure()
defp build_result(%{effects: [], unknown: unknown}) when unknown != [],
do: Result.unknown(Enum.uniq(unknown))
defp build_result(%{effects: effects, locations: _}), do: Result.impure(Enum.uniq(effects), [])
end