Packages
metastatic
0.10.4
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.
Uses 3-tuple format: {type, meta, children_or_value}
## Examples
iex> format({:literal, [subtype: :integer], 42}, :plain)
"literal(integer, 42)"
iex> format({:binary_op, [category: :arithmetic, operator: :+], [{:variable, [], "x"}, {:literal, [subtype: :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
# 3-tuple format: {type, meta, children_or_value}
@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
# 3-tuple: {:literal, [subtype: type], value}
defp format_tree_node({:literal, meta, value}, indent, _depth) when is_list(meta) do
type = Keyword.get(meta, :subtype, :unknown)
"#{indent}literal (#{type}): #{inspect(value)}"
end
# 3-tuple: {:variable, meta, name}
defp format_tree_node({:variable, _meta, name}, indent, _depth) do
"#{indent}variable: #{name}"
end
# 3-tuple: {:list, meta, elements}
defp format_tree_node({:list, _meta, elements}, indent, depth) when is_list(elements) do
element_lines = Enum.map(elements, &format_tree(&1, depth + 1))
lines = ["#{indent}list" | element_lines]
Enum.join(lines, "\n")
end
# 3-tuple: {:map, meta, pairs}
defp format_tree_node({:map, _meta, pairs}, indent, depth) when is_list(pairs) do
pair_lines =
Enum.flat_map(pairs, fn
{:pair, _, [key, value]} ->
[
"#{indent} key:",
format_tree(key, depth + 2),
"#{indent} value:",
format_tree(value, depth + 2)
]
{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
# 3-tuple: {:binary_op, [category: cat, operator: op], [left, right]}
defp format_tree_node({:binary_op, meta, [left, right]}, indent, depth) when is_list(meta) do
category = Keyword.get(meta, :category, :unknown)
op = Keyword.get(meta, :operator, :unknown)
lines = [
"#{indent}binary_op (#{category}: #{op})",
format_tree(left, depth + 1),
format_tree(right, depth + 1)
]
Enum.join(lines, "\n")
end
# 3-tuple: {:unary_op, [category: cat, operator: op], [operand]}
defp format_tree_node({:unary_op, meta, [operand]}, indent, depth) when is_list(meta) do
category = Keyword.get(meta, :category, :unknown)
op = Keyword.get(meta, :operator, :unknown)
lines = [
"#{indent}unary_op (#{category}: #{op})",
format_tree(operand, depth + 1)
]
Enum.join(lines, "\n")
end
# 3-tuple: {:function_call, [name: name], args}
defp format_tree_node({:function_call, meta, args}, indent, depth) when is_list(meta) do
name = Keyword.get(meta, :name, "unknown")
arg_lines = Enum.map(args, &format_tree(&1, depth + 1))
lines = ["#{indent}function_call: #{name}" | arg_lines]
Enum.join(lines, "\n")
end
# 3-tuple: {:conditional, meta, [condition, then_branch, else_branch?]}
defp format_tree_node({:conditional, _meta, children}, indent, depth) when is_list(children) do
{condition, then_branch, else_branch} =
case children do
[c, t, e] -> {c, t, e}
[c, t] -> {c, t, nil}
end
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
# 3-tuple: {:block, meta, statements}
defp format_tree_node({:block, _meta, statements}, indent, depth) when is_list(statements) do
statement_lines = Enum.map(statements, &format_tree(&1, depth + 1))
lines = ["#{indent}block" | statement_lines]
Enum.join(lines, "\n")
end
# 3-tuple: {:early_return, meta, [value]}
defp format_tree_node({:early_return, meta, children}, indent, depth) when is_list(meta) do
kind = Keyword.get(meta, :kind, :return)
value = if is_list(children) and children != [], do: hd(children), else: nil
lines = [
"#{indent}early_return (#{kind})"
]
lines =
if value do
lines ++ [format_tree(value, depth + 1)]
else
lines
end
Enum.join(lines, "\n")
end
# 3-tuple: {:loop, [loop_type: :while], [condition, body]}
defp format_tree_node({:loop, meta, children}, indent, depth) when is_list(meta) do
loop_type = Keyword.get(meta, :loop_type, :unknown)
case {loop_type, children} do
{:while, [condition, body]} ->
lines = [
"#{indent}loop (while)",
"#{indent} condition:",
format_tree(condition, depth + 2),
"#{indent} body:",
format_tree(body, depth + 2)
]
Enum.join(lines, "\n")
{kind, [iterator, collection, body]} when kind in [:for, :for_each] ->
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")
_ ->
"#{indent}loop (#{loop_type})"
end
end
# 3-tuple: {:lambda, [params: params, captures: caps], [body]}
defp format_tree_node({:lambda, meta, children}, indent, depth) when is_list(meta) do
params = Keyword.get(meta, :params, [])
captures = Keyword.get(meta, :captures, [])
body = if is_list(children) and children != [], do: List.last(children), else: nil
param_str = Enum.map_join(params, ", ", &format_param/1)
lines = [
"#{indent}lambda (#{param_str})",
"#{indent} captures: #{inspect(captures)}"
]
lines =
if body do
lines ++ ["#{indent} body:", format_tree(body, depth + 2)]
else
lines
end
Enum.join(lines, "\n")
end
# 3-tuple: {:collection_op, [op: :map|:filter], [lambda, collection]}
defp format_tree_node({:collection_op, meta, children}, indent, depth) when is_list(meta) do
op = Keyword.get(meta, :op, :unknown)
case {op, children} do
{op, [lambda, collection]} when op in [:map, :filter] ->
lines = [
"#{indent}collection_op (#{op})",
"#{indent} lambda:",
format_tree(lambda, depth + 2),
"#{indent} collection:",
format_tree(collection, depth + 2)
]
Enum.join(lines, "\n")
{:reduce, [lambda, collection, initial]} ->
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")
_ ->
"#{indent}collection_op (#{op})"
end
end
# 3-tuple: {:exception_handling, meta, [body, rescue_clauses, finally]}
defp format_tree_node({:exception_handling, _meta, children}, indent, depth)
when is_list(children) do
{body, rescue_clauses, finally_clause} =
case children do
[b, r, f] -> {b, r, f}
[b, r] -> {b, r, nil}
[b] -> {b, [], nil}
end
lines = [
"#{indent}exception_handling",
"#{indent} body:",
format_tree(body, depth + 2),
"#{indent} rescue:"
]
rescue_list = if is_list(rescue_clauses), do: rescue_clauses, else: []
rescue_lines =
Enum.flat_map(rescue_list, fn
{:catch_clause, _, [_type, _var, handler]} ->
[
"#{indent} handler:",
format_tree(handler, depth + 3)
]
{:rescue, _, [pattern, handler]} ->
[
"#{indent} pattern:",
format_tree(pattern, depth + 3),
"#{indent} handler:",
format_tree(handler, depth + 3)
]
other ->
["#{indent} #{inspect(other)}"]
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
# 3-tuple: {:language_specific, [language: lang, hint: hint], native_ast}
defp format_tree_node({:language_specific, meta, _native_ast}, indent, _depth)
when is_list(meta) do
language = Keyword.get(meta, :language, :unknown)
hint = Keyword.get(meta, :hint)
"#{indent}language_specific (#{language}, hint: #{inspect(hint)})"
end
# Fallback for any unrecognized 3-tuple
defp format_tree_node({type, meta, _children}, indent, _depth)
when is_atom(type) and is_list(meta) do
"#{indent}#{type}"
end
defp format_tree_node(ast, indent, _depth) do
"#{indent}#{inspect(ast)}"
end
# 3-tuple params: {:param, [pattern: pattern, default: default], name}
defp format_param({:param, meta, name}) when is_binary(name) do
pattern = Keyword.get(meta, :pattern)
default = Keyword.get(meta, :default)
parts = [name]
parts =
if pattern do
parts ++ [" (pattern: #{inspect(pattern)})"]
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
# 3-tuple format: {type, meta, children_or_value}
@spec format_json(meta_ast()) :: String.t()
defp format_json(ast) do
ast
|> ast_to_map()
|> Jason.encode!(pretty: true)
end
# 3-tuple: {:literal, [subtype: type], value}
defp ast_to_map({:literal, meta, value}) when is_list(meta) do
type = Keyword.get(meta, :subtype, :unknown)
%{type: "literal", semantic_type: type, value: value}
end
# 3-tuple: {:variable, meta, name}
defp ast_to_map({:variable, _meta, name}) do
%{type: "variable", name: name}
end
# 3-tuple: {:list, meta, elements}
defp ast_to_map({:list, _meta, elements}) when is_list(elements) do
%{type: "list", elements: Enum.map(elements, &ast_to_map/1)}
end
# 3-tuple: {:map, meta, pairs}
defp ast_to_map({:map, _meta, pairs}) when is_list(pairs) do
%{
type: "map",
pairs:
Enum.map(pairs, fn
{:pair, _, [k, v]} -> %{key: ast_to_map(k), value: ast_to_map(v)}
{k, v} -> %{key: ast_to_map(k), value: ast_to_map(v)}
end)
}
end
# 3-tuple: {:binary_op, [category: cat, operator: op], [left, right]}
defp ast_to_map({:binary_op, meta, [left, right]}) when is_list(meta) do
category = Keyword.get(meta, :category, :unknown)
op = Keyword.get(meta, :operator, :unknown)
%{
type: "binary_op",
category: category,
operator: op,
left: ast_to_map(left),
right: ast_to_map(right)
}
end
# 3-tuple: {:unary_op, [category: cat, operator: op], [operand]}
defp ast_to_map({:unary_op, meta, [operand]}) when is_list(meta) do
category = Keyword.get(meta, :category, :unknown)
op = Keyword.get(meta, :operator, :unknown)
%{
type: "unary_op",
category: category,
operator: op,
operand: ast_to_map(operand)
}
end
# 3-tuple: {:function_call, [name: name], args}
defp ast_to_map({:function_call, meta, args}) when is_list(meta) do
name = Keyword.get(meta, :name, "unknown")
%{
type: "function_call",
name: name,
args: Enum.map(args, &ast_to_map/1)
}
end
# 3-tuple: {:conditional, meta, [condition, then_branch, else_branch?]}
defp ast_to_map({:conditional, _meta, children}) when is_list(children) do
{condition, then_branch, else_branch} =
case children do
[c, t, e] -> {c, t, e}
[c, t] -> {c, t, nil}
end
%{
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
# 3-tuple: {:block, meta, statements}
defp ast_to_map({:block, _meta, statements}) when is_list(statements) do
%{
type: "block",
statements: Enum.map(statements, &ast_to_map/1)
}
end
# 3-tuple: {:language_specific, [language: lang, hint: hint], native_ast}
defp ast_to_map({:language_specific, meta, _native_ast}) when is_list(meta) do
language = Keyword.get(meta, :language, :unknown)
hint = Keyword.get(meta, :hint)
%{
type: "language_specific",
language: language,
hint: hint
}
end
# Fallback for other 3-tuple types
defp ast_to_map({type, meta, children}) when is_atom(type) and is_list(meta) do
%{type: to_string(type), meta: Map.new(meta), children: format_children(children)}
end
defp ast_to_map(ast) do
%{type: "unknown", data: inspect(ast)}
end
defp format_children(children) when is_list(children) do
Enum.map(children, &ast_to_map/1)
end
defp format_children(value), do: value
# Plain format - simple text representation
# 3-tuple format: {type, meta, children_or_value}
@spec format_plain(meta_ast()) :: String.t()
# 3-tuple: {:literal, [subtype: type], value}
defp format_plain({:literal, meta, value}) when is_list(meta) do
type = Keyword.get(meta, :subtype, :unknown)
"literal(#{type}, #{inspect(value)})"
end
# 3-tuple: {:variable, meta, name}
defp format_plain({:variable, _meta, name}) do
"variable(#{name})"
end
# 3-tuple: {:binary_op, [category: cat, operator: op], [left, right]}
defp format_plain({:binary_op, meta, [left, right]}) when is_list(meta) do
category = Keyword.get(meta, :category, :unknown)
op = Keyword.get(meta, :operator, :unknown)
"binary_op(#{category}, #{op}, #{format_plain(left)}, #{format_plain(right)})"
end
# 3-tuple: {:unary_op, [category: cat, operator: op], [operand]}
defp format_plain({:unary_op, meta, [operand]}) when is_list(meta) do
category = Keyword.get(meta, :category, :unknown)
op = Keyword.get(meta, :operator, :unknown)
"unary_op(#{category}, #{op}, #{format_plain(operand)})"
end
# 3-tuple: {:function_call, [name: name], args}
defp format_plain({:function_call, meta, args}) when is_list(meta) do
name = Keyword.get(meta, :name, "unknown")
args_str = Enum.map_join(args, ", ", &format_plain/1)
"function_call(#{name}, [#{args_str}])"
end
# Fallback for other 3-tuples
defp format_plain({type, _meta, _children}) when is_atom(type) do
to_string(type)
end
defp format_plain(ast) do
inspect(ast)
end
end