Current section

Files

Jump to
metastatic lib metastatic adapters python to_meta.ex
Raw

lib/metastatic/adapters/python/to_meta.ex

defmodule Metastatic.Adapters.Python.ToMeta do
@moduledoc """
Transform Python AST (M1) to MetaAST (M2).
Implements the abstraction function α_Python that lifts Python-specific
AST structures to the meta-level representation.
## Transformation Strategy
Python's AST is represented as nested maps with "_type" keys. We pattern
match on these structures and transform them to MetaAST tuples.
### M2.1 (Core Layer)
- Literals: Constant nodes → {:literal, type, value}
- Variables: Name nodes → {:variable, name}
- Binary operators: BinOp → {:binary_op, category, op, left, right}
- Unary operators: UnaryOp → {:unary_op, category, op, operand}
- Function calls: Call → {:function_call, name, args}
- Conditionals: If, IfExp → {:conditional, condition, then, else}
- Blocks: Module, multiple statements → {:block, statements}
- Early returns: Return, Break, Continue → {:early_return, kind, value}
### M2.2 (Extended Layer)
- Loops: While → {:loop, :while, condition, body}
- Loops: For → {:loop, :for_each, iterator, collection, body}
- Lambdas: Lambda → {:lambda, params, captures, body}
- Collection ops: Simple ListComp → {:collection_op, :map, lambda, collection}
- Exception handling: Try → {:exception_handling, try_block, rescue_clauses, finally_block}
## Metadata Preservation
The transformation preserves Python-specific information:
- `:lineno` - line number
- `:col_offset` - column offset
- `:python_node_type` - original Python AST node type
This enables high-fidelity round-trips (M1 → M2 → M1).
"""
alias Metastatic.AST
@doc """
Transform Python AST to MetaAST.
Returns `{:ok, meta_ast, metadata}` on success or `{:error, reason}` on failure.
## New 3-Tuple Format
All MetaAST nodes are uniform 3-element tuples:
{type_atom, keyword_meta, children_or_value}
## Examples
iex> transform(%{"_type" => "Constant", "value" => 42})
{:ok, {:literal, [subtype: :integer], 42}, %{}}
iex> transform(%{"_type" => "Name", "id" => "x"})
{:ok, {:variable, [], "x"}, %{}}
iex> transform(%{"_type" => "BinOp", "op" => %{"_type" => "Add"}, ...})
{:ok, {:binary_op, [category: :arithmetic, operator: :+], [left, right]}, %{}}
"""
@spec transform(map()) :: {:ok, term(), map()} | {:error, String.t()}
# Module - top-level container
def transform(%{"_type" => "Module", "body" => body}) do
with {:ok, statements} <- transform_list(body) do
case statements do
[] -> {:ok, {:block, [], []}, %{}}
[single] -> {:ok, single, %{}}
multiple -> {:ok, {:block, [], multiple}, %{}}
end
end
end
# Expression statement - unwrap the value
def transform(%{"_type" => "Expr", "value" => value}) do
transform(value)
end
# Literals - M2.1 Core Layer
# Constant node (Python 3.8+)
def transform(%{"_type" => "Constant", "value" => value} = node) do
literal = infer_literal_type(value)
{:ok, add_location(literal, node), %{}}
end
# Legacy literal nodes (Python 3.7 compatibility)
def transform(%{"_type" => "Num", "n" => value}) when is_integer(value) do
{:ok, {:literal, [subtype: :integer], value}, %{}}
end
def transform(%{"_type" => "Num", "n" => value}) when is_float(value) do
{:ok, {:literal, [subtype: :float], value}, %{}}
end
def transform(%{"_type" => "Str", "s" => value}) do
{:ok, {:literal, [subtype: :string], value}, %{}}
end
def transform(%{"_type" => "NameConstant", "value" => true}) do
{:ok, {:literal, [subtype: :boolean], true}, %{}}
end
def transform(%{"_type" => "NameConstant", "value" => false}) do
{:ok, {:literal, [subtype: :boolean], false}, %{}}
end
def transform(%{"_type" => "NameConstant", "value" => nil}) do
{:ok, {:literal, [subtype: :null], nil}, %{}}
end
# Variables - M2.1 Core Layer
def transform(%{"_type" => "Name", "id" => name} = node) do
{:ok, add_location({:variable, [], name}, node), %{}}
end
# Binary Operators - M2.1 Core Layer
def transform(%{"_type" => "BinOp", "op" => op, "left" => left, "right" => right} = node) do
with {:ok, op_meta} <- transform_binop(op),
{:ok, left_meta, _} <- transform(left),
{:ok, right_meta, _} <- transform(right) do
{category, operator} = op_meta
ast = {:binary_op, [category: category, operator: operator], [left_meta, right_meta]}
{:ok, add_location(ast, node), %{}}
end
end
# Comparison Operators - M2.1 Core Layer
def transform(
%{"_type" => "Compare", "left" => left, "ops" => [op], "comparators" => [right]} = node
) do
with {:ok, op_meta} <- transform_compare_op(op),
{:ok, left_meta, _} <- transform(left),
{:ok, right_meta, _} <- transform(right) do
ast = {:binary_op, [category: :comparison, operator: op_meta], [left_meta, right_meta]}
{:ok, add_location(ast, node), %{}}
end
end
# Multiple comparisons (a < b < c) - chain them
def transform(%{
"_type" => "Compare",
"left" => left,
"ops" => ops,
"comparators" => comparators
})
when length(ops) > 1 do
# For now, treat as language_specific
native = %{"_type" => "Compare", "left" => left, "ops" => ops, "comparators" => comparators}
{:ok, {:language_specific, [language: :python, hint: :chained_comparison], native}, %{}}
end
# Boolean Operators - M2.1 Core Layer
def transform(%{"_type" => "BoolOp", "op" => op, "values" => [left, right]} = node) do
with {:ok, op_meta} <- transform_bool_op(op),
{:ok, left_meta, _} <- transform(left),
{:ok, right_meta, _} <- transform(right) do
ast = {:binary_op, [category: :boolean, operator: op_meta], [left_meta, right_meta]}
{:ok, add_location(ast, node), %{}}
end
end
# Multiple boolean operations - chain them
def transform(%{"_type" => "BoolOp", "op" => op, "values" => values}) when length(values) > 2 do
with {:ok, op_meta} <- transform_bool_op(op),
{:ok, transformed_values} <- transform_list(values) do
# Chain: a and b and c → (a and b) and c
[first, second | rest] = transformed_values
initial = {:binary_op, [category: :boolean, operator: op_meta], [first, second]}
result =
Enum.reduce(rest, initial, fn value, acc ->
{:binary_op, [category: :boolean, operator: op_meta], [acc, value]}
end)
{:ok, result, %{}}
end
end
# Unary Operators - M2.1 Core Layer
def transform(%{"_type" => "UnaryOp", "op" => op, "operand" => operand} = node) do
with {:ok, {category, operator}} <- transform_unary_op(op),
{:ok, operand_meta, _} <- transform(operand) do
ast = {:unary_op, [category: category, operator: operator], [operand_meta]}
{:ok, add_location(ast, node), %{}}
end
end
# Function Calls - M2.1 Core Layer
def transform(%{"_type" => "Call", "func" => func, "args" => args} = node) do
with {:ok, func_name} <- extract_function_name(func),
{:ok, args_meta} <- transform_list(args) do
ast = {:function_call, [name: func_name], args_meta}
{:ok, add_location(ast, node), %{}}
end
end
# Conditionals - M2.1 Core Layer
# If statement
def transform(%{"_type" => "If", "test" => test, "body" => body, "orelse" => orelse} = node) do
with {:ok, test_meta, _} <- transform(test),
{:ok, body_meta, _} <- transform_body(body),
{:ok, else_meta, _} <- transform_body_or_nil(orelse) do
ast = {:conditional, [], [test_meta, body_meta, else_meta]}
{:ok, add_location(ast, node), %{}}
end
end
# If expression (ternary)
def transform(%{"_type" => "IfExp", "test" => test, "body" => body, "orelse" => orelse} = node) do
with {:ok, test_meta, _} <- transform(test),
{:ok, body_meta, _} <- transform(body),
{:ok, else_meta, _} <- transform(orelse) do
ast = {:conditional, [], [test_meta, body_meta, else_meta]}
{:ok, add_location(ast, node), %{}}
end
end
# Early Returns - M2.1 Core Layer
def transform(%{"_type" => "Return", "value" => value} = node) do
with {:ok, value_meta, _} <- transform_or_nil(value) do
ast = {:early_return, [kind: :return], [value_meta]}
{:ok, add_location(ast, node), %{}}
end
end
def transform(%{"_type" => "Break"}) do
{:ok, {:early_return, [kind: :break], [nil]}, %{}}
end
def transform(%{"_type" => "Continue"}) do
{:ok, {:early_return, [kind: :continue], [nil]}, %{}}
end
# Assignment - M2.1 Core Layer
# In Python, = is assignment (imperative binding/mutation)
# Simple assignment: x = 5
def transform(%{"_type" => "Assign", "targets" => [target], "value" => value} = node) do
with {:ok, target_meta, target_metadata} <- transform(target),
{:ok, value_meta, value_metadata} <- transform(value) do
metadata = %{
target_metadata: target_metadata,
value_metadata: value_metadata
}
ast = {:assignment, [], [target_meta, value_meta]}
{:ok, add_location(ast, node), metadata}
end
end
# Multiple assignment: x = y = 5
# Python allows chaining: a = b = c = 5
# Transform to nested assignments: a = (b = (c = 5))
def transform(%{"_type" => "Assign", "targets" => targets, "value" => value})
when length(targets) > 1 do
with {:ok, value_meta, value_metadata} <- transform(value) do
# Build nested assignments from right to left
result =
targets
|> Enum.reverse()
|> Enum.reduce({:ok, value_meta, value_metadata}, fn target, {:ok, current_value, _} ->
with {:ok, target_meta, target_metadata} <- transform(target) do
metadata = %{
target_metadata: target_metadata,
value_metadata: %{}
}
{:ok, {:assignment, [], [target_meta, current_value]}, metadata}
end
end)
result
end
end
# Augmented assignment: x += 1, x *= 2, etc.
# Desugar to: x = x + 1
def transform(%{"_type" => "AugAssign", "target" => target, "op" => op, "value" => value}) do
with {:ok, target_meta, target_metadata} <- transform(target),
{:ok, {category, operator}} <- transform_binop(op),
{:ok, value_meta, _} <- transform(value) do
# Desugar: x += 1 becomes x = x + 1
desugared_value =
{:binary_op, [category: category, operator: operator], [target_meta, value_meta]}
metadata = %{
target_metadata: target_metadata,
value_metadata: %{},
augmented_op: operator
}
{:ok, {:assignment, [], [target_meta, desugared_value]}, metadata}
end
end
# Annotated assignment: x: int = 5 (Python 3.6+)
# Ignore the annotation for now
def transform(%{"_type" => "AnnAssign", "target" => target, "value" => value})
when not is_nil(value) do
with {:ok, target_meta, target_metadata} <- transform(target),
{:ok, value_meta, value_metadata} <- transform(value) do
metadata = %{
target_metadata: target_metadata,
value_metadata: value_metadata
}
{:ok, {:assignment, [], [target_meta, value_meta]}, metadata}
end
end
# Annotated assignment without value: x: int (declaration only)
def transform(%{"_type" => "AnnAssign", "target" => _target, "value" => nil} = node) do
# Type-only annotation, treat as language-specific
{:ok, {:language_specific, [language: :python, hint: :type_annotation], node}, %{}}
end
# Lists - M2.1 Core Layer
def transform(%{"_type" => "List", "elts" => elements} = node) do
with {:ok, elements_meta} <- transform_list(elements) do
ast = {:list, [], elements_meta}
{:ok, add_location(ast, node), %{}}
end
end
# Dicts (Maps) - M2.1 Core Layer
def transform(%{"_type" => "Dict", "keys" => keys, "values" => values} = node) do
with {:ok, keys_meta} <- transform_list(keys),
{:ok, values_meta} <- transform_list(values) do
# Transform pairs to {:pair, [], [key, value]} format
pairs = Enum.zip_with(keys_meta, values_meta, fn k, v -> {:pair, [], [k, v]} end)
ast = {:map, [], pairs}
{:ok, add_location(ast, node), %{}}
end
end
# Tuples - Used in patterns and values
def transform(%{"_type" => "Tuple", "elts" => elements}) do
with {:ok, elements_meta} <- transform_list(elements) do
{:ok, {:tuple, [], elements_meta}, %{}}
end
end
# Loops - M2.2 Extended Layer
# While loop
def transform(%{"_type" => "While", "test" => test, "body" => body} = node) do
with {:ok, test_meta, _} <- transform(test),
{:ok, body_meta, _} <- transform_body(body) do
ast = {:loop, [loop_type: :while], [test_meta, body_meta]}
{:ok, add_location(ast, node), %{}}
end
end
# For loop
def transform(%{"_type" => "For", "target" => target, "iter" => iter, "body" => body} = node) do
with {:ok, target_meta, _} <- transform(target),
{:ok, iter_meta, _} <- transform(iter),
{:ok, body_meta, _} <- transform_body(body) do
ast = {:loop, [loop_type: :for_each], [target_meta, iter_meta, body_meta]}
{:ok, add_location(ast, node), %{}}
end
end
# Lambdas - M2.2 Extended Layer
def transform(%{"_type" => "Lambda", "args" => args, "body" => body}) do
with {:ok, params} <- extract_lambda_params(args),
{:ok, body_meta, _} <- transform(body) do
# Lambdas don't capture variables explicitly in Python AST
{:ok, {:lambda, [params: params, captures: []], [body_meta]}, %{}}
end
end
# Collection Operations - M2.2 Extended Layer
# Simple list comprehension without filters → map
def transform(%{
"_type" => "ListComp",
"elt" => elt,
"generators" => [%{"target" => target, "iter" => iter, "ifs" => []}]
}) do
with {:ok, target_meta, _} <- transform(target),
{:ok, iter_meta, _} <- transform(iter),
{:ok, elt_meta, _} <- transform(elt) do
# Convert to: {:collection_op, [op_type: :map], [lambda, collection]}
param_name = extract_variable_name(target_meta)
lambda = {:lambda, [params: [param_name], captures: []], [elt_meta]}
{:ok, {:collection_op, [op_type: :map], [lambda, iter_meta]}, %{}}
end
end
# Complex list comprehension with filters → language_specific
def transform(%{"_type" => "ListComp"} = comp) do
{:ok, {:language_specific, [language: :python, hint: :list_comprehension], comp}, %{}}
end
# Exception Handling - M2.2 Extended Layer
def transform(%{
"_type" => "Try",
"body" => body,
"handlers" => handlers,
"orelse" => _orelse,
"finalbody" => finalbody
}) do
with {:ok, body_meta, _} <- transform_body(body),
{:ok, rescue_clauses} <- transform_exception_handlers(handlers),
{:ok, finally_meta, _} <- transform_body_or_nil(finalbody) do
{:ok, {:exception_handling, [], [body_meta, rescue_clauses, finally_meta]}, %{}}
end
end
# Native Layer - M2.3: Python-Specific Constructs
# Function definitions with decorators
def transform(%{"_type" => "FunctionDef", "decorator_list" => [_ | _] = _decorators} = node) do
{:ok, {:language_specific, [language: :python, hint: :function_with_decorators], node}, %{}}
end
# Generator functions (FunctionDef containing Yield/YieldFrom)
def transform(%{"_type" => "FunctionDef", "body" => body} = node) do
if contains_yield?(body) do
{:ok, {:language_specific, [language: :python, hint: :function_with_generator], node}, %{}}
else
# Regular function - not implemented yet, falls through to catch-all
{:error, "Unsupported Python AST construct: #{inspect(node)}"}
end
end
# Async function definitions
def transform(%{"_type" => "AsyncFunctionDef"} = node) do
{:ok, {:language_specific, [language: :python, hint: :async_function], node}, %{}}
end
# Class definitions
def transform(%{"_type" => "ClassDef"} = node) do
{:ok, {:language_specific, [language: :python, hint: :class], node}, %{}}
end
# Context managers (with statement)
def transform(%{"_type" => "With"} = node) do
{:ok, {:language_specific, [language: :python, hint: :context_manager], node}, %{}}
end
# Async context managers (async with)
def transform(%{"_type" => "AsyncWith"} = node) do
{:ok, {:language_specific, [language: :python, hint: :async_context_manager], node}, %{}}
end
# Generators (yield)
def transform(%{"_type" => "Yield"} = node) do
{:ok, {:language_specific, [language: :python, hint: :yield], node}, %{}}
end
# Yield from (Python 3.3+)
def transform(%{"_type" => "YieldFrom"} = node) do
{:ok, {:language_specific, [language: :python, hint: :yield_from], node}, %{}}
end
# Await expressions
def transform(%{"_type" => "Await"} = node) do
{:ok, {:language_specific, [language: :python, hint: :await], node}, %{}}
end
# Async for loops
def transform(%{"_type" => "AsyncFor"} = node) do
{:ok, {:language_specific, [language: :python, hint: :async_for], node}, %{}}
end
# Import statements
def transform(%{"_type" => "Import"} = node) do
{:ok, {:language_specific, [language: :python, hint: :import], node}, %{}}
end
# Import from statements
def transform(%{"_type" => "ImportFrom"} = node) do
{:ok, {:language_specific, [language: :python, hint: :import_from], node}, %{}}
end
# Dict comprehensions
def transform(%{"_type" => "DictComp"} = node) do
{:ok, {:language_specific, [language: :python, hint: :dict_comprehension], node}, %{}}
end
# Set comprehensions
def transform(%{"_type" => "SetComp"} = node) do
{:ok, {:language_specific, [language: :python, hint: :set_comprehension], node}, %{}}
end
# Generator expressions
def transform(%{"_type" => "GeneratorExp"} = node) do
{:ok, {:language_specific, [language: :python, hint: :generator_expression], node}, %{}}
end
# Match statements (Python 3.10+)
def transform(%{"_type" => "Match"} = node) do
{:ok, {:language_specific, [language: :python, hint: :pattern_match], node}, %{}}
end
# Walrus operator (Python 3.8+)
def transform(%{"_type" => "NamedExpr"} = node) do
{:ok, {:language_specific, [language: :python, hint: :named_expr], node}, %{}}
end
# Global/nonlocal declarations
def transform(%{"_type" => "Global"} = node) do
{:ok, {:language_specific, [language: :python, hint: :global], node}, %{}}
end
def transform(%{"_type" => "Nonlocal"} = node) do
{:ok, {:language_specific, [language: :python, hint: :nonlocal], node}, %{}}
end
# Assert statements
def transform(%{"_type" => "Assert"} = node) do
{:ok, {:language_specific, [language: :python, hint: :assert], node}, %{}}
end
# Raise statements
def transform(%{"_type" => "Raise"} = node) do
{:ok, {:language_specific, [language: :python, hint: :raise], node}, %{}}
end
# Delete statements
def transform(%{"_type" => "Delete"} = node) do
{:ok, {:language_specific, [language: :python, hint: :delete], node}, %{}}
end
# Pass statement
def transform(%{"_type" => "Pass"} = node) do
{:ok, {:language_specific, [language: :python, hint: :pass], node}, %{}}
end
# Catch-all for unsupported constructs
def transform(unsupported) do
{:error, "Unsupported Python AST construct: #{inspect(unsupported)}"}
end
# Helper Functions
defp transform_list(items) when is_list(items) do
items
|> Enum.reduce_while({:ok, []}, fn item, {:ok, acc} ->
case transform(item) do
{:ok, meta, _} -> {:cont, {:ok, [meta | acc]}}
{:error, _} = err -> {:halt, err}
end
end)
|> case do
{:ok, items} -> {:ok, Enum.reverse(items)}
error -> error
end
end
defp transform_or_nil(nil), do: {:ok, nil, %{}}
defp transform_or_nil(value), do: transform(value)
defp transform_body([]), do: {:ok, {:block, [], []}, %{}}
defp transform_body([single]), do: transform(single)
defp transform_body(statements) when is_list(statements) do
with {:ok, transformed} <- transform_list(statements) do
{:ok, {:block, [], transformed}, %{}}
end
end
defp transform_body_or_nil([]), do: {:ok, nil, %{}}
defp transform_body_or_nil(body), do: transform_body(body)
defp infer_literal_type(value) when is_integer(value),
do: {:literal, [subtype: :integer], value}
defp infer_literal_type(value) when is_float(value), do: {:literal, [subtype: :float], value}
defp infer_literal_type(value) when is_binary(value), do: {:literal, [subtype: :string], value}
defp infer_literal_type(true), do: {:literal, [subtype: :boolean], true}
defp infer_literal_type(false), do: {:literal, [subtype: :boolean], false}
defp infer_literal_type(nil), do: {:literal, [subtype: :null], nil}
# Operator transformations
defp transform_binop(%{"_type" => "Add"}), do: {:ok, {:arithmetic, :+}}
defp transform_binop(%{"_type" => "Sub"}), do: {:ok, {:arithmetic, :-}}
defp transform_binop(%{"_type" => "Mult"}), do: {:ok, {:arithmetic, :*}}
defp transform_binop(%{"_type" => "Div"}), do: {:ok, {:arithmetic, :/}}
defp transform_binop(%{"_type" => "FloorDiv"}), do: {:ok, {:arithmetic, :div}}
defp transform_binop(%{"_type" => "Mod"}), do: {:ok, {:arithmetic, :rem}}
defp transform_binop(%{"_type" => "Pow"}), do: {:ok, {:arithmetic, :**}}
defp transform_binop(op), do: {:error, "Unsupported binary operator: #{inspect(op)}"}
defp transform_compare_op(%{"_type" => "Eq"}), do: {:ok, :==}
defp transform_compare_op(%{"_type" => "NotEq"}), do: {:ok, :!=}
defp transform_compare_op(%{"_type" => "Lt"}), do: {:ok, :<}
defp transform_compare_op(%{"_type" => "LtE"}), do: {:ok, :<=}
defp transform_compare_op(%{"_type" => "Gt"}), do: {:ok, :>}
defp transform_compare_op(%{"_type" => "GtE"}), do: {:ok, :>=}
defp transform_compare_op(%{"_type" => "Is"}), do: {:ok, :===}
defp transform_compare_op(%{"_type" => "IsNot"}), do: {:ok, :!==}
defp transform_compare_op(op), do: {:error, "Unsupported comparison operator: #{inspect(op)}"}
defp transform_bool_op(%{"_type" => "And"}), do: {:ok, :and}
defp transform_bool_op(%{"_type" => "Or"}), do: {:ok, :or}
defp transform_bool_op(op), do: {:error, "Unsupported boolean operator: #{inspect(op)}"}
defp transform_unary_op(%{"_type" => "Not"}), do: {:ok, {:boolean, :not}}
defp transform_unary_op(%{"_type" => "USub"}), do: {:ok, {:arithmetic, :-}}
defp transform_unary_op(%{"_type" => "UAdd"}), do: {:ok, {:arithmetic, :+}}
defp transform_unary_op(op), do: {:error, "Unsupported unary operator: #{inspect(op)}"}
# Check if body contains Yield or YieldFrom expressions
defp contains_yield?(body) when is_list(body) do
Enum.any?(body, &contains_yield_node?/1)
end
defp contains_yield?(node), do: contains_yield_node?(node)
defp contains_yield_node?(%{"_type" => "Yield"}), do: true
defp contains_yield_node?(%{"_type" => "YieldFrom"}), do: true
defp contains_yield_node?(%{} = node) when is_map(node) do
# Recursively check all values in the node
Enum.any?(Map.values(node), fn
value when is_list(value) -> contains_yield?(value)
value when is_map(value) -> contains_yield_node?(value)
_ -> false
end)
end
defp contains_yield_node?(_), do: false
defp extract_function_name(%{"_type" => "Name", "id" => name}), do: {:ok, name}
defp extract_function_name(%{"_type" => "Attribute", "value" => obj, "attr" => attr}) do
case extract_function_name(obj) do
{:ok, obj_name} -> {:ok, "#{obj_name}.#{attr}"}
error -> error
end
end
defp extract_function_name(func) do
{:error, "Unsupported function reference: #{inspect(func)}"}
end
# Lambda parameter extraction
defp extract_lambda_params(%{"args" => args}) when is_list(args) do
params =
Enum.map(args, fn
%{"arg" => name} -> name
%{"id" => name} -> name
_ -> "_"
end)
{:ok, params}
end
defp extract_lambda_params(_), do: {:ok, []}
# Exception handler transformation
defp transform_exception_handlers(handlers) when is_list(handlers) do
handlers
|> Enum.reduce_while({:ok, []}, fn handler, {:ok, acc} ->
case transform_exception_handler(handler) do
{:ok, clause} -> {:cont, {:ok, [clause | acc]}}
{:error, _} = err -> {:halt, err}
end
end)
|> case do
{:ok, clauses} -> {:ok, Enum.reverse(clauses)}
error -> error
end
end
defp transform_exception_handler(%{"type" => type, "name" => name, "body" => body}) do
with {:ok, exception_type} <- extract_exception_type(type),
{:ok, var_meta, _} <- transform_or_name(name),
{:ok, body_meta, _} <- transform_body(body) do
{:ok, {exception_type, var_meta, body_meta}}
end
end
defp transform_exception_handler(%{"type" => type, "body" => body}) do
with {:ok, exception_type} <- extract_exception_type(type),
{:ok, body_meta, _} <- transform_body(body) do
{:ok, {exception_type, nil, body_meta}}
end
end
@python_exceptions %{
"Exception" => :error,
"ValueError" => :error,
"TypeError" => :error,
"KeyError" => :error,
"IndexError" => :error
}
defp extract_exception_type(nil), do: {:ok, :error}
defp extract_exception_type(%{"_type" => "Name", "id" => name}) do
# Map common Python exceptions to generic types
exception_atom = Map.get(@python_exceptions, name, String.to_atom(name))
{:ok, exception_atom}
end
defp extract_exception_type(_), do: {:ok, :error}
defp transform_or_name(nil), do: {:ok, nil, %{}}
defp transform_or_name(name) when is_binary(name), do: {:ok, {:variable, [], name}, %{}}
defp transform_or_name(node), do: transform(node)
# Extract variable name from MetaAST node
defp extract_variable_name({:variable, _, name}), do: name
defp extract_variable_name(_), do: "_x"
# Add location information from Python AST node to MetaAST
defp add_location(ast, %{"lineno" => line} = node) do
loc = %{line: line}
loc =
if Map.has_key?(node, "col_offset"), do: Map.put(loc, :col, node["col_offset"]), else: loc
loc =
if Map.has_key?(node, "end_lineno"),
do: Map.put(loc, :end_line, node["end_lineno"]),
else: loc
loc =
if Map.has_key?(node, "end_col_offset"),
do: Map.put(loc, :end_col, node["end_col_offset"]),
else: loc
AST.with_location(ast, loc)
end
defp add_location(ast, _node), do: ast
end