Packages
metastatic
0.3.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/mix/tasks/metastatic.validate_equivalence.ex
defmodule Mix.Tasks.Metastatic.ValidateEquivalence do
@shortdoc "Validate semantic equivalence between two files"
@moduledoc """
Validate that two source files have semantically equivalent MetaAST representations.
This demonstrates cross-language semantic equivalence - showing that code in
different languages can represent the same abstract computation at the M2 level.
## Usage
mix metastatic.validate-equivalence FILE1 FILE2 [OPTIONS]
## Arguments
- `FILE1` - First source file
- `FILE2` - Second source file
## Options
- `--lang1 LANG` - Language of first file (optional, auto-detected)
- `--lang2 LANG` - Language of second file (optional, auto-detected)
- `--verbose` - Show detailed comparison
## Examples
Compare Python and Elixir implementations:
mix metastatic.validate-equivalence hello.py hello.ex
Compare with explicit language specification:
mix metastatic.validate-equivalence file1.txt file2.txt --lang1 python --lang2 elixir
Show detailed differences:
mix metastatic.validate-equivalence hello.py hello.ex --verbose
## Equivalence Checking
Two files are considered semantically equivalent if their MetaAST representations
are identical. This means:
- Same structure and operations
- Same variable names
- Same literal values
- Same control flow
Differences allowed:
- Comments and documentation
- Whitespace and formatting
- Language-specific syntax sugar
- Type annotations (when not semantically meaningful)
## Exit Codes
- 0 - Files are semantically equivalent
- 1 - Files are not equivalent or error occurred
"""
use Mix.Task
alias Metastatic.{Builder, CLI}
@switches [
lang1: :string,
lang2: :string,
verbose: :boolean
]
@impl Mix.Task
def run(args) do
Mix.Task.run("app.start")
{opts, paths, _invalid} = OptionParser.parse(args, strict: @switches)
case validate_args(opts, paths) do
{:ok, file1, file2, options} ->
validate_equivalence(file1, file2, options)
{:error, message} ->
CLI.fatal(message)
end
end
# Validation
@spec validate_args(keyword(), [String.t()]) ::
{:ok, String.t(), String.t(), keyword()} | {:error, String.t()}
defp validate_args(opts, paths) do
with {:ok, file1, file2} <- validate_file_paths(paths),
{:ok, lang1} <- validate_language(opts, :lang1, file1),
{:ok, lang2} <- validate_language(opts, :lang2, file2) do
options = [
lang1: lang1,
lang2: lang2,
verbose: Keyword.get(opts, :verbose, false)
]
{:ok, file1, file2, options}
end
end
@spec validate_file_paths([String.t()]) :: {:ok, String.t(), String.t()} | {:error, String.t()}
defp validate_file_paths(paths) when length(paths) < 2 do
{:error, "Two files required for equivalence checking"}
end
defp validate_file_paths([file1, file2 | _]) do
cond do
!File.exists?(file1) ->
{:error, "First file does not exist: #{file1}"}
!File.exists?(file2) ->
{:error, "Second file does not exist: #{file2}"}
File.dir?(file1) ->
{:error, "First path is a directory: #{file1}"}
File.dir?(file2) ->
{:error, "Second path is a directory: #{file2}"}
true ->
{:ok, file1, file2}
end
end
@spec validate_language(keyword(), atom(), String.t()) :: {:ok, atom()} | {:error, String.t()}
defp validate_language(opts, opt_key, file_path) do
case Keyword.get(opts, opt_key) do
nil ->
case CLI.detect_language(file_path) do
{:ok, lang} ->
{:ok, lang}
{:error, _} ->
{:error, "Cannot detect language for #{file_path}. Use --#{opt_key} option"}
end
lang_str ->
case String.downcase(lang_str) do
"python" ->
{:ok, :python}
"elixir" ->
{:ok, :elixir}
"erlang" ->
{:ok, :erlang}
_ ->
{:error,
"Invalid language for #{opt_key}: #{lang_str}. Supported: python, elixir, erlang"}
end
end
end
# Equivalence validation
@spec validate_equivalence(String.t(), String.t(), keyword()) :: no_return()
defp validate_equivalence(file1, file2, options) do
lang1 = Keyword.fetch!(options, :lang1)
lang2 = Keyword.fetch!(options, :lang2)
verbose = Keyword.fetch!(options, :verbose)
Mix.shell().info(CLI.format_info("Comparing:"))
Mix.shell().info(" File 1: #{file1} (#{lang1})")
Mix.shell().info(" File 2: #{file2} (#{lang2})")
Mix.shell().info("")
with {:ok, source1} <- CLI.read_file(file1),
{:ok, source2} <- CLI.read_file(file2),
{:ok, doc1} <- Builder.from_source(source1, lang1),
{:ok, doc2} <- Builder.from_source(source2, lang2) do
if doc1.ast == doc2.ast do
Mix.shell().info(CLI.format_success("Semantically equivalent"))
if verbose do
show_details(doc1, doc2)
end
exit({:shutdown, 0})
else
Mix.shell().error(CLI.format_error("Not semantically equivalent"))
if verbose do
show_differences(doc1, doc2)
else
Mix.shell().info("")
Mix.shell().info("Run with --verbose to see detailed differences")
end
exit({:shutdown, 1})
end
else
{:error, reason} ->
CLI.fatal("Comparison failed: #{inspect(reason)}")
end
end
@spec show_details(Metastatic.Document.t(), Metastatic.Document.t()) :: :ok
defp show_details(doc1, doc2) do
Mix.shell().info("")
Mix.shell().info("Details:")
Mix.shell().info(" Variables:")
vars1 = Metastatic.AST.variables(doc1.ast)
vars2 = Metastatic.AST.variables(doc2.ast)
all_vars = MapSet.union(vars1, vars2) |> Enum.sort()
Enum.each(all_vars, fn var ->
Mix.shell().info(" #{var}")
end)
Mix.shell().info("")
Mix.shell().info(" MetaAST structure: identical")
end
@spec show_differences(Metastatic.Document.t(), Metastatic.Document.t()) :: :ok
defp show_differences(doc1, doc2) do
Mix.shell().info("")
Mix.shell().info("Differences:")
Mix.shell().info("")
# Show AST representations
Mix.shell().info("File 1 MetaAST:")
Mix.shell().info(" #{inspect(doc1.ast, pretty: true, width: 80)}")
Mix.shell().info("")
Mix.shell().info("File 2 MetaAST:")
Mix.shell().info(" #{inspect(doc2.ast, pretty: true, width: 80)}")
Mix.shell().info("")
# Show variable differences
vars1 = Metastatic.AST.variables(doc1.ast)
vars2 = Metastatic.AST.variables(doc2.ast)
only_in_1 = MapSet.difference(vars1, vars2)
only_in_2 = MapSet.difference(vars2, vars1)
if MapSet.size(only_in_1) > 0 do
Mix.shell().info("Variables only in file 1:")
Enum.each(only_in_1, fn var ->
Mix.shell().info(" #{var}")
end)
Mix.shell().info("")
end
if MapSet.size(only_in_2) > 0 do
Mix.shell().info("Variables only in file 2:")
Enum.each(only_in_2, fn var ->
Mix.shell().info(" #{var}")
end)
Mix.shell().info("")
end
end
end