Packages
metastatic
0.7.5
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/cli/formatter.ex
defmodule Metastatic.CLI.Formatter do
@moduledoc """
Output formatting utilities for Metastatic CLI.
Supports multiple output formats:
- Tree format (default) - Human-readable tree structure
- JSON format - Machine-readable JSON
- Plain format - Simple text representation
"""
alias Metastatic.{AST, Document}
@type format :: :tree | :json | :plain
@type meta_ast :: AST.meta_ast()
@doc """
Format a MetaAST node for display.
## Examples
iex> format({:literal, :integer, 42}, :plain)
"literal(integer, 42)"
iex> format({:binary_op, :arithmetic, :+, {:variable, "x"}, {:literal, :integer, 5}}, :tree)
"binary_op (arithmetic: +)\\n├─ variable: x\\n└─ literal (integer): 5"
"""
@spec format(meta_ast(), format()) :: String.t()
def format(ast, :tree), do: format_tree(ast, 0)
def format(ast, :json), do: format_json(ast)
def format(ast, :plain), do: format_plain(ast)
@doc """
Format a Document for display.
Includes metadata about language, validation status, etc.
"""
@spec format_document(Document.t(), format()) :: String.t()
def format_document(%Document{} = doc, format) do
header = [
"Language: #{doc.language}",
"AST:"
]
ast_output = format(doc.ast, format)
Enum.join(header, "\n") <> "\n" <> ast_output
end
# Tree format - human-readable with tree structure
@spec format_tree(meta_ast(), non_neg_integer()) :: String.t()
defp format_tree(ast, depth) do
indent = String.duplicate(" ", depth)
format_tree_node(ast, indent, depth)
end
# Handle location-aware nodes by stripping location and recursing
defp format_tree_node({:literal, type, value, _loc}, indent, depth) do
format_tree_node({:literal, type, value}, indent, depth)
end
defp format_tree_node({:literal, type, value}, indent, _depth) do
"#{indent}literal (#{type}): #{inspect(value)}"
end
defp format_tree_node({:variable, name, _loc}, indent, depth) do
format_tree_node({:variable, name}, indent, depth)
end
defp format_tree_node({:variable, name}, indent, _depth) do
"#{indent}variable: #{name}"
end
defp format_tree_node({:list, elements}, indent, depth) do
element_lines = Enum.map(elements, &format_tree(&1, depth + 1))
lines = ["#{indent}list" | element_lines]
Enum.join(lines, "\n")
end
defp format_tree_node({:map, pairs}, indent, depth) do
pair_lines =
Enum.flat_map(pairs, fn {key, value} ->
[
"#{indent} key:",
format_tree(key, depth + 2),
"#{indent} value:",
format_tree(value, depth + 2)
]
end)
lines = ["#{indent}map" | pair_lines]
Enum.join(lines, "\n")
end
defp format_tree_node({:binary_op, category, op, left, right, _loc}, indent, depth) do
format_tree_node({:binary_op, category, op, left, right}, indent, depth)
end
defp format_tree_node({:binary_op, category, op, left, right}, indent, depth) do
lines = [
"#{indent}binary_op (#{category}: #{op})",
format_tree(left, depth + 1),
format_tree(right, depth + 1)
]
Enum.join(lines, "\n")
end
defp format_tree_node({:unary_op, category, op, operand, _loc}, indent, depth) do
format_tree_node({:unary_op, category, op, operand}, indent, depth)
end
defp format_tree_node({:unary_op, category, op, operand}, indent, depth) do
lines = [
"#{indent}unary_op (#{category}: #{op})",
format_tree(operand, depth + 1)
]
Enum.join(lines, "\n")
end
defp format_tree_node({:function_call, name, args, _loc}, indent, depth) do
format_tree_node({:function_call, name, args}, indent, depth)
end
defp format_tree_node({:function_call, name, args}, indent, depth) do
arg_lines = Enum.map(args, &format_tree(&1, depth + 1))
lines = ["#{indent}function_call: #{name}" | arg_lines]
Enum.join(lines, "\n")
end
defp format_tree_node({:conditional, condition, then_branch, else_branch, _loc}, indent, depth) do
format_tree_node({:conditional, condition, then_branch, else_branch}, indent, depth)
end
defp format_tree_node({:conditional, condition, then_branch, else_branch}, indent, depth) do
lines = [
"#{indent}conditional",
"#{indent} condition:",
format_tree(condition, depth + 2),
"#{indent} then:",
format_tree(then_branch, depth + 2)
]
lines =
if else_branch do
lines ++
[
"#{indent} else:",
format_tree(else_branch, depth + 2)
]
else
lines
end
Enum.join(lines, "\n")
end
defp format_tree_node({:block, statements, _loc}, indent, depth) do
format_tree_node({:block, statements}, indent, depth)
end
defp format_tree_node({:block, statements}, indent, depth) do
statement_lines = Enum.map(statements, &format_tree(&1, depth + 1))
lines = ["#{indent}block" | statement_lines]
Enum.join(lines, "\n")
end
defp format_tree_node({:early_return, kind, value, _loc}, indent, depth) do
format_tree_node({:early_return, kind, value}, indent, depth)
end
defp format_tree_node({:early_return, kind, value}, indent, depth) do
lines = [
"#{indent}early_return (#{kind})"
]
lines =
if value do
lines ++ [format_tree(value, depth + 1)]
else
lines
end
Enum.join(lines, "\n")
end
defp format_tree_node({:loop, :while, condition, body, _loc}, indent, depth) do
format_tree_node({:loop, :while, condition, body}, indent, depth)
end
defp format_tree_node({:loop, :while, condition, body}, indent, depth) do
lines = [
"#{indent}loop (while)",
"#{indent} condition:",
format_tree(condition, depth + 2),
"#{indent} body:",
format_tree(body, depth + 2)
]
Enum.join(lines, "\n")
end
defp format_tree_node({:loop, kind, iterator, collection, body, _loc}, indent, depth)
when kind in [:for, :for_each] do
format_tree_node({:loop, kind, iterator, collection, body}, indent, depth)
end
defp format_tree_node({:loop, kind, iterator, collection, body}, indent, depth)
when kind in [:for, :for_each] do
lines = [
"#{indent}loop (#{kind})",
"#{indent} iterator:",
format_tree(iterator, depth + 2),
"#{indent} collection:",
format_tree(collection, depth + 2),
"#{indent} body:",
format_tree(body, depth + 2)
]
Enum.join(lines, "\n")
end
defp format_tree_node({:lambda, params, captures, body, _loc}, indent, depth) do
format_tree_node({:lambda, params, captures, body}, indent, depth)
end
defp format_tree_node({:lambda, params, captures, body}, indent, depth) do
param_str = Enum.map_join(params, ", ", &format_param/1)
lines = [
"#{indent}lambda (#{param_str})",
"#{indent} captures: #{inspect(captures)}",
"#{indent} body:",
format_tree(body, depth + 2)
]
Enum.join(lines, "\n")
end
defp format_tree_node({:collection_op, op, lambda, collection, _loc}, indent, depth)
when op in [:map, :filter] do
format_tree_node({:collection_op, op, lambda, collection}, indent, depth)
end
defp format_tree_node({:collection_op, op, lambda, collection}, indent, depth)
when op in [:map, :filter] do
lines = [
"#{indent}collection_op (#{op})",
"#{indent} lambda:",
format_tree(lambda, depth + 2),
"#{indent} collection:",
format_tree(collection, depth + 2)
]
Enum.join(lines, "\n")
end
defp format_tree_node(
{:collection_op, :reduce, lambda, collection, initial, _loc},
indent,
depth
) do
format_tree_node({:collection_op, :reduce, lambda, collection, initial}, indent, depth)
end
defp format_tree_node({:collection_op, :reduce, lambda, collection, initial}, indent, depth) do
lines = [
"#{indent}collection_op (reduce)",
"#{indent} lambda:",
format_tree(lambda, depth + 2),
"#{indent} collection:",
format_tree(collection, depth + 2),
"#{indent} initial:",
format_tree(initial, depth + 2)
]
Enum.join(lines, "\n")
end
defp format_tree_node(
{:exception_handling, body, rescue_clauses, finally_clause, _loc},
indent,
depth
) do
format_tree_node({:exception_handling, body, rescue_clauses, finally_clause}, indent, depth)
end
defp format_tree_node(
{:exception_handling, body, rescue_clauses, finally_clause},
indent,
depth
) do
lines = [
"#{indent}exception_handling",
"#{indent} body:",
format_tree(body, depth + 2),
"#{indent} rescue:"
]
rescue_lines =
Enum.flat_map(rescue_clauses, fn {:rescue, pattern, handler} ->
[
"#{indent} pattern:",
format_tree(pattern, depth + 3),
"#{indent} handler:",
format_tree(handler, depth + 3)
]
end)
lines = lines ++ rescue_lines
lines =
if finally_clause do
lines ++
[
"#{indent} finally:",
format_tree(finally_clause, depth + 2)
]
else
lines
end
Enum.join(lines, "\n")
end
defp format_tree_node({:language_specific, language, _native_ast, hint}, indent, _depth) do
"#{indent}language_specific (#{language}, hint: #{inspect(hint)})"
end
defp format_tree_node(ast, indent, _depth) do
"#{indent}#{inspect(ast)}"
end
defp format_param({:param, name, type_hint, default}) do
parts = [name]
parts =
if type_hint do
parts ++ [": #{type_hint}"]
else
parts
end
parts =
if default do
parts ++ [" = #{inspect(default)}"]
else
parts
end
Enum.join(parts, "")
end
# Handle string params (e.g. from Python lambda)
defp format_param(name) when is_binary(name), do: name
defp format_param(other), do: inspect(other)
# JSON format - machine-readable
@spec format_json(meta_ast()) :: String.t()
defp format_json(ast) do
ast
|> ast_to_map()
|> Jason.encode!(pretty: true)
end
defp ast_to_map({:literal, type, value, _loc}) do
ast_to_map({:literal, type, value})
end
defp ast_to_map({:literal, type, value}) do
%{type: "literal", semantic_type: type, value: value}
end
defp ast_to_map({:variable, name, _loc}) do
ast_to_map({:variable, name})
end
defp ast_to_map({:variable, name}) do
%{type: "variable", name: name}
end
defp ast_to_map({:list, elements}) do
%{type: "list", elements: Enum.map(elements, &ast_to_map/1)}
end
defp ast_to_map({:map, pairs}) do
%{
type: "map",
pairs: Enum.map(pairs, fn {k, v} -> %{key: ast_to_map(k), value: ast_to_map(v)} end)
}
end
defp ast_to_map({:binary_op, category, op, left, right, _loc}) do
ast_to_map({:binary_op, category, op, left, right})
end
defp ast_to_map({:binary_op, category, op, left, right}) do
%{
type: "binary_op",
category: category,
operator: op,
left: ast_to_map(left),
right: ast_to_map(right)
}
end
defp ast_to_map({:unary_op, category, op, operand, _loc}) do
ast_to_map({:unary_op, category, op, operand})
end
defp ast_to_map({:unary_op, category, op, operand}) do
%{
type: "unary_op",
category: category,
operator: op,
operand: ast_to_map(operand)
}
end
defp ast_to_map({:function_call, name, args, _loc}) do
ast_to_map({:function_call, name, args})
end
defp ast_to_map({:function_call, name, args}) do
%{
type: "function_call",
name: name,
args: Enum.map(args, &ast_to_map/1)
}
end
defp ast_to_map({:conditional, condition, then_branch, else_branch, _loc}) do
ast_to_map({:conditional, condition, then_branch, else_branch})
end
defp ast_to_map({:conditional, condition, then_branch, else_branch}) do
%{
type: "conditional",
condition: ast_to_map(condition),
then_branch: ast_to_map(then_branch),
else_branch: if(else_branch, do: ast_to_map(else_branch), else: nil)
}
end
defp ast_to_map({:block, statements, _loc}) do
ast_to_map({:block, statements})
end
defp ast_to_map({:block, statements}) do
%{
type: "block",
statements: Enum.map(statements, &ast_to_map/1)
}
end
defp ast_to_map({:language_specific, language, _native_ast, hint}) do
%{
type: "language_specific",
language: language,
hint: hint
}
end
defp ast_to_map(ast) do
%{type: "unknown", data: inspect(ast)}
end
# Plain format - simple text representation
@spec format_plain(meta_ast()) :: String.t()
defp format_plain({:literal, type, value, _loc}) do
format_plain({:literal, type, value})
end
defp format_plain({:literal, type, value}) do
"literal(#{type}, #{inspect(value)})"
end
defp format_plain({:variable, name, _loc}) do
format_plain({:variable, name})
end
defp format_plain({:variable, name}) do
"variable(#{name})"
end
defp format_plain({:binary_op, category, op, left, right, _loc}) do
format_plain({:binary_op, category, op, left, right})
end
defp format_plain({:binary_op, category, op, left, right}) do
"binary_op(#{category}, #{op}, #{format_plain(left)}, #{format_plain(right)})"
end
defp format_plain({:unary_op, category, op, operand, _loc}) do
format_plain({:unary_op, category, op, operand})
end
defp format_plain({:unary_op, category, op, operand}) do
"unary_op(#{category}, #{op}, #{format_plain(operand)})"
end
defp format_plain({:function_call, name, args, _loc}) do
format_plain({:function_call, name, args})
end
defp format_plain({:function_call, name, args}) do
args_str = Enum.map_join(args, ", ", &format_plain/1)
"function_call(#{name}, [#{args_str}])"
end
defp format_plain(ast) do
inspect(ast)
end
end