Current section

Files

Jump to
metastatic lib metastatic cli translator.ex
Raw

lib/metastatic/cli/translator.ex

defmodule Metastatic.CLI.Translator do
@moduledoc """
Cross-language translation orchestration for Metastatic CLI.
Handles translation of individual files or entire directories from one
programming language to another using MetaAST as the intermediate representation.
## Translation Pipeline
1. Parse source code to native AST (M1)
2. Transform to MetaAST (M2)
3. Transform MetaAST to target language AST (M1')
4. Unparse to target language source code
## Example
iex> translate("example.py", :python, :elixir, "example.ex")
{:ok, "example.ex"}
"""
alias Metastatic.{Builder, CLI}
@type language :: atom()
@type file_path :: String.t()
@type translate_result :: {:ok, file_path()} | {:error, String.t()}
@doc """
Translate a single file from one language to another.
## Parameters
- `source_path` - Path to source file
- `from_lang` - Source language (`:python`, `:elixir`, `:erlang`)
- `to_lang` - Target language (`:python`, `:elixir`, `:erlang`)
- `output_path` - Path for output file
## Examples
iex> translate("hello.py", :python, :elixir, "hello.ex")
{:ok, "hello.ex"}
iex> translate("invalid.py", :python, :elixir, "out.ex")
{:error, "Parse error: ..."}
"""
@spec translate(file_path(), language(), language(), file_path()) :: translate_result()
def translate(source_path, from_lang, to_lang, output_path) do
with {:ok, source} <- CLI.read_file(source_path),
{:ok, from_adapter} <- CLI.get_adapter(from_lang),
{:ok, to_adapter} <- CLI.get_adapter(to_lang),
{:ok, doc} <- parse_to_meta(source, from_adapter, from_lang),
{:ok, target_source} <- meta_to_source(doc, to_adapter, to_lang),
:ok <- CLI.write_file(output_path, target_source) do
{:ok, output_path}
end
end
@doc """
Translate a single file, auto-detecting output path.
Output path is generated by changing the file extension to match
the target language.
## Examples
iex> translate_with_auto_output("hello.py", :python, :elixir)
{:ok, "hello.ex"}
iex> translate_with_auto_output("src/foo.ex", :elixir, :python)
{:ok, "src/foo.py"}
"""
@spec translate_with_auto_output(file_path(), language(), language()) :: translate_result()
def translate_with_auto_output(source_path, from_lang, to_lang) do
output_path = generate_output_path(source_path, to_lang)
translate(source_path, from_lang, to_lang, output_path)
end
@doc """
Translate a directory of files from one language to another.
Recursively processes all files with matching extensions in the source
directory and writes translated files to the output directory, preserving
the directory structure.
## Parameters
- `source_dir` - Source directory path
- `from_lang` - Source language
- `to_lang` - Target language
- `output_dir` - Output directory path
## Examples
iex> translate_directory("src/python", :python, :elixir, "src/elixir")
{:ok, ["src/elixir/foo.ex", "src/elixir/bar.ex"]}
"""
@spec translate_directory(file_path(), language(), language(), file_path()) ::
{:ok, [file_path()]} | {:error, String.t()}
def translate_directory(source_dir, from_lang, to_lang, output_dir) do
with {:ok, files} <- list_source_files(source_dir, from_lang) do
results =
Enum.map(files, fn source_path ->
relative_path = Path.relative_to(source_path, source_dir)
output_path = Path.join(output_dir, change_extension(relative_path, to_lang))
translate(source_path, from_lang, to_lang, output_path)
end)
errors = Enum.filter(results, &match?({:error, _}, &1))
if Enum.empty?(errors) do
{:ok, Enum.map(results, fn {:ok, path} -> path end)}
else
{:error, format_errors(errors)}
end
end
end
# Private functions
@spec parse_to_meta(String.t(), module(), language()) ::
{:ok, Metastatic.Document.t()} | {:error, String.t()}
defp parse_to_meta(source, _adapter, language) do
# Builder.from_source expects a language atom, not an adapter module
case Builder.from_source(source, language) do
{:ok, doc} -> {:ok, doc}
{:error, reason} -> {:error, "Failed to parse #{language}: #{inspect(reason)}"}
end
end
@spec meta_to_source(Metastatic.Document.t(), module(), language()) ::
{:ok, String.t()} | {:error, String.t()}
defp meta_to_source(doc, adapter, language) do
with {:ok, native_ast} <- adapter.from_meta(doc.ast, doc.metadata),
{:ok, source} <- adapter.unparse(native_ast) do
{:ok, source}
else
{:error, reason} -> {:error, "Failed to generate #{language}: #{inspect(reason)}"}
end
end
@spec generate_output_path(file_path(), language()) :: file_path()
defp generate_output_path(source_path, target_lang) do
base = Path.rootname(source_path)
ext = extension_for_language(target_lang)
base <> ext
end
@spec change_extension(file_path(), language()) :: file_path()
defp change_extension(path, target_lang) do
base = Path.rootname(path)
ext = extension_for_language(target_lang)
base <> ext
end
@spec extension_for_language(language()) :: String.t()
defp extension_for_language(lang) do
case Metastatic.Languages.extension_for_language(lang) do
{:ok, ext} -> ext
{:error, _} -> raise "No extension known for language: #{lang}"
end
end
@spec list_source_files(file_path(), language()) :: {:ok, [file_path()]} | {:error, String.t()}
defp list_source_files(dir, language) do
pattern = Path.join(dir, "**/*" <> extension_for_language(language))
case Path.wildcard(pattern) do
[] -> {:error, "No source files found in #{dir}"}
files -> {:ok, Enum.filter(files, &File.regular?/1)}
end
end
@spec format_errors([{:error, String.t()}]) :: String.t()
defp format_errors(errors) do
error_messages = Enum.map_join(errors, "\n", fn {:error, msg} -> " - #{msg}" end)
"Translation failed:\n#{error_messages}"
end
end