Packages
metastatic
0.8.2
0.26.0
0.25.0
0.24.1
0.24.0
0.23.0
0.22.2
0.22.1
0.22.0
0.21.3
0.21.2
0.21.1
0.21.0
0.20.3
0.20.2
0.20.1
0.20.0
0.19.0
0.18.0
0.17.0
0.16.0
0.15.1
0.15.0
0.14.2
0.14.1
0.14.0
0.13.3
0.13.2
0.13.1
0.13.0
0.12.0
0.11.0
0.10.4
0.10.3
0.10.2
0.10.1
0.10.0
0.9.2
0.9.1
0.9.0
0.8.6
0.8.5
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.7
0.7.6
0.7.5
0.7.4
0.7.3
0.7.1
0.7.0
0.6.1
0.6.0
0.5.2
0.5.1
0.5.0
0.4.2
0.4.1
0.4.0
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.0
0.1.3
0.1.2
0.1.1
0.1.0
Cross-language code meta-model library using unified MetaAST representation. Parse, transform, and translate code across Python, Elixir, Ruby, Erlang, Haskell, and more via a shared three-tuple AST format.
Current section
Files
Jump to
Current section
Files
lib/metastatic/analysis/control_flow/result.ex
defmodule Metastatic.Analysis.ControlFlow.Result do
@moduledoc """
Result structure for control flow graph analysis.
Contains the control flow graph with nodes, edges, entry/exit points,
and analysis results like reachability and cycles.
## Fields
- `:cfg` - The control flow graph structure
- `:entry_node` - ID of the entry node
- `:exit_nodes` - List of exit node IDs
- `:reachable_nodes` - Set of reachable node IDs
- `:has_cycles?` - Boolean indicating if graph contains cycles
- `:node_count` - Total number of nodes
- `:edge_count` - Total number of edges
## Examples
iex> cfg = %{nodes: %{0 => %{type: :entry}}, edges: [], entry: 0, exits: [0]}
iex> result = Metastatic.Analysis.ControlFlow.Result.new(cfg)
iex> result.node_count
1
"""
@type node_id :: non_neg_integer()
@type cfg_node :: %{
id: node_id(),
type: :entry | :exit | :statement | :conditional | :loop,
ast: term(),
predecessors: [node_id()],
successors: [node_id()]
}
@type cfg_edge :: {from :: node_id(), to :: node_id(), condition :: atom() | nil}
@type cfg :: %{
nodes: %{node_id() => cfg_node()},
edges: [cfg_edge()],
entry: node_id(),
exits: [node_id()]
}
@type t :: %__MODULE__{
cfg: cfg(),
entry_node: node_id(),
exit_nodes: [node_id()],
reachable_nodes: MapSet.t(node_id()),
has_cycles?: boolean(),
node_count: non_neg_integer(),
edge_count: non_neg_integer()
}
defstruct cfg: %{nodes: %{}, edges: [], entry: 0, exits: []},
entry_node: 0,
exit_nodes: [],
reachable_nodes: MapSet.new(),
has_cycles?: false,
node_count: 0,
edge_count: 0
@doc """
Creates a new result from a CFG.
## Examples
iex> cfg = %{nodes: %{0 => %{id: 0, type: :entry, ast: nil, predecessors: [], successors: []}}, edges: [], entry: 0, exits: [0]}
iex> result = Metastatic.Analysis.ControlFlow.Result.new(cfg)
iex> result.entry_node
0
"""
@spec new(cfg()) :: t()
def new(cfg) do
reachable = compute_reachable_nodes(cfg)
has_cycles = detect_cycles(cfg)
%__MODULE__{
cfg: cfg,
entry_node: cfg.entry,
exit_nodes: cfg.exits,
reachable_nodes: reachable,
has_cycles?: has_cycles,
node_count: map_size(cfg.nodes),
edge_count: length(cfg.edges)
}
end
@doc """
Converts result to JSON-compatible map.
## Options
- `:format` - Output format (`:standard` or `:d3`). Default: `:standard`
## Examples
iex> cfg = %{nodes: %{}, edges: [], entry: 0, exits: []}
iex> result = Metastatic.Analysis.ControlFlow.Result.new(cfg)
iex> map = Metastatic.Analysis.ControlFlow.Result.to_map(result)
iex> is_map(map)
true
iex> cfg = %{nodes: %{0 => %{id: 0, type: :entry, ast: nil, predecessors: [], successors: []}}, edges: [], entry: 0, exits: [0]}
iex> result = Metastatic.Analysis.ControlFlow.Result.new(cfg)
iex> map = Metastatic.Analysis.ControlFlow.Result.to_map(result, format: :d3)
iex> Map.has_key?(map, :nodes) and Map.has_key?(map, :links)
true
"""
@spec to_map(t(), keyword()) :: map()
def to_map(%__MODULE__{} = result, opts \\ []) do
case Keyword.get(opts, :format, :standard) do
:d3 ->
to_d3_json(result)
_ ->
%{
entry_node: result.entry_node,
exit_nodes: result.exit_nodes,
reachable_nodes: MapSet.to_list(result.reachable_nodes),
has_cycles: result.has_cycles?,
node_count: result.node_count,
edge_count: result.edge_count,
nodes: result.cfg.nodes,
edges: result.cfg.edges
}
end
end
@doc """
Exports CFG to D3.js-compatible JSON format.
Returns a map with `nodes` and `links` arrays suitable for D3.js force-directed graphs.
## Examples
iex> cfg = %{nodes: %{0 => %{id: 0, type: :entry, ast: nil, predecessors: [], successors: [1]}, 1 => %{id: 1, type: :exit, ast: nil, predecessors: [0], successors: []}}, edges: [{0, 1, nil}], entry: 0, exits: [1]}
iex> result = Metastatic.Analysis.ControlFlow.Result.new(cfg)
iex> json = Metastatic.Analysis.ControlFlow.Result.to_d3_json(result)
iex> is_list(json.nodes)
true
iex> is_list(json.links)
true
"""
@spec to_d3_json(t()) :: map()
def to_d3_json(%__MODULE__{cfg: cfg}) do
nodes =
cfg.nodes
|> Enum.map(fn {id, node} ->
%{
id: id,
label: format_node_label(node),
type: node.type,
group: node_group(node.type)
}
end)
links =
cfg.edges
|> Enum.map(fn {source, target, condition} ->
%{
source: source,
target: target,
label: condition,
type: edge_type(condition)
}
end)
%{
nodes: nodes,
links: links
}
end
@doc """
Exports CFG to DOT format for visualization.
## Examples
iex> cfg = %{nodes: %{0 => %{id: 0, type: :entry, ast: nil, predecessors: [], successors: [1]}, 1 => %{id: 1, type: :exit, ast: nil, predecessors: [0], successors: []}}, edges: [{0, 1, nil}], entry: 0, exits: [1]}
iex> result = Metastatic.Analysis.ControlFlow.Result.new(cfg)
iex> dot = Metastatic.Analysis.ControlFlow.Result.to_dot(result)
iex> String.contains?(dot, "digraph")
true
"""
@spec to_dot(t()) :: String.t()
def to_dot(%__MODULE__{cfg: cfg}) do
header = "digraph CFG {\n"
footer = "}\n"
nodes =
Enum.map_join(cfg.nodes, "\n", fn {id, node} ->
label = format_node_label(node)
shape = node_shape(node.type)
~s( #{id} [label="#{label}", shape=#{shape}];)
end)
edges =
Enum.map_join(cfg.edges, "\n", fn {from, to, condition} ->
label = if condition, do: ~s([label="#{condition}"]), else: ""
~s( #{from} -> #{to}#{label};)
end)
header <> nodes <> "\n" <> edges <> "\n" <> footer
end
# Private helpers
defp compute_reachable_nodes(cfg) do
visit_reachable(cfg, cfg.entry, MapSet.new())
end
defp visit_reachable(cfg, node_id, visited) do
if MapSet.member?(visited, node_id) do
visited
else
visited = MapSet.put(visited, node_id)
node = Map.get(cfg.nodes, node_id)
if node do
Enum.reduce(node.successors, visited, fn succ, acc ->
visit_reachable(cfg, succ, acc)
end)
else
visited
end
end
end
defp detect_cycles(cfg) do
# Simple cycle detection using DFS
detect_cycle_dfs(cfg, cfg.entry, MapSet.new(), MapSet.new())
end
defp detect_cycle_dfs(_cfg, node_id, _visited, _rec_stack) when node_id == nil, do: false
defp detect_cycle_dfs(cfg, node_id, visited, rec_stack) do
if MapSet.member?(rec_stack, node_id) do
true
else
if MapSet.member?(visited, node_id) do
false
else
visited = MapSet.put(visited, node_id)
rec_stack = MapSet.put(rec_stack, node_id)
node = Map.get(cfg.nodes, node_id)
if node do
Enum.any?(node.successors, fn succ ->
detect_cycle_dfs(cfg, succ, visited, rec_stack)
end)
else
false
end
end
end
end
defp format_node_label(%{type: :entry}), do: "ENTRY"
defp format_node_label(%{type: :exit}), do: "EXIT"
defp format_node_label(%{type: type, id: id}), do: "#{type}_#{id}"
defp node_shape(:entry), do: "ellipse"
defp node_shape(:exit), do: "ellipse"
defp node_shape(:conditional), do: "diamond"
defp node_shape(:loop), do: "box"
defp node_shape(_), do: "rectangle"
# D3.js helpers
defp node_group(:entry), do: 1
defp node_group(:exit), do: 2
defp node_group(:conditional), do: 3
defp node_group(:loop), do: 4
defp node_group(_), do: 5
defp edge_type(:then), do: "conditional"
defp edge_type(:else), do: "conditional"
defp edge_type(:loop_back), do: "loop"
defp edge_type(:exit), do: "exit"
defp edge_type(_), do: "normal"
end