Packages
metastatic
0.7.1
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/validator.ex
defmodule Metastatic.Validator do
@moduledoc """
Conformance validation for MetaAST.
This module provides formal M1 → M2 conformance checking and validation
of MetaAST structures according to the theoretical foundations.
## Conformance Rules (Definition 8 from THEORETICAL_FOUNDATIONS.md)
A term `t ∈ M1` conforms to M2 if it can be represented by M2 meta-types
without loss of essential semantic information. Validation checks:
1. **Structural conformance** - AST structure matches M2 type definitions
2. **Type safety** - All type tags are valid M2 types
3. **Semantic preservation** - Required semantic information is present
4. **Native escape hatches** - M2.3 used only when necessary
## Validation Levels
- **Strict** - No M2.3 native constructs allowed (M2.1 + M2.2 + M2.2s only)
- **Standard** - M2.3 allowed but discouraged
- **Permissive** - All M2 levels accepted
Note: M2.2s (Structural/Organizational layer) is part of the extended layer
and includes container, function_def, attribute_access, augmented_assignment,
and property types.
## Usage
# Validate a MetaAST document
alias Metastatic.Validator
doc = %Metastatic.Document{
ast: {:binary_op, :arithmetic, :+, {:variable, "x"}, {:literal, :integer, 5}},
language: :python,
metadata: %{}
}
Validator.validate(doc)
# => {:ok, %{level: :core, native_constructs: 0, warnings: []}}
# Strict validation (reject native constructs)
Validator.validate(doc, mode: :strict)
# => {:ok, %{...}} or {:error, :native_constructs_not_allowed}
## Validation Result
Returns:
- `{:ok, metadata}` - Valid MetaAST with validation metadata
- `{:error, reason}` - Invalid structure
Metadata includes:
- `:level` - Highest M2 level used (`:core`, `:extended`, `:native`)
- `:native_constructs` - Count of M2.3 language_specific nodes
- `:warnings` - List of validation warnings
- `:variables` - Set of all variables referenced
- `:depth` - Maximum AST depth
"""
alias Metastatic.{AST, Document}
@type validation_mode :: :strict | :standard | :permissive
@type validation_result :: {:ok, map()} | {:error, term()}
@doc """
Validate a MetaAST document.
## Options
- `:mode` - Validation mode (`:strict`, `:standard`, `:permissive`)
- `:max_depth` - Maximum allowed AST depth (default: 1000)
- `:max_variables` - Maximum unique variables (default: 10000)
## Examples
iex> doc = %Metastatic.Document{ast: {:literal, :integer, 42}, language: :python, metadata: %{}}
iex> {:ok, meta} = Metastatic.Validator.validate(doc)
iex> meta.level
:core
iex> invalid_doc = %Metastatic.Document{ast: {:invalid_type, "oops"}, language: :python, metadata: %{}}
iex> Metastatic.Validator.validate(invalid_doc)
{:error, {:invalid_structure, {:invalid_type, "oops"}}}
"""
@spec validate(Document.t(), keyword()) :: validation_result()
def validate(%Document{} = document, opts \\ []) do
mode = Keyword.get(opts, :mode, :standard)
max_depth = Keyword.get(opts, :max_depth, 1000)
max_variables = Keyword.get(opts, :max_variables, 10_000)
with {:ok, _} <- validate_structure(document.ast),
{:ok, meta} <- analyze_ast(document.ast),
:ok <- check_constraints(meta, mode, max_depth, max_variables) do
{:ok, meta}
end
end
@doc """
Quick validation check - returns boolean.
## Examples
iex> doc = %Metastatic.Document{ast: {:literal, :integer, 42}, language: :python, metadata: %{}}
iex> Metastatic.Validator.valid?(doc)
true
iex> invalid_doc = %Metastatic.Document{ast: {:bad, "ast"}, language: :python, metadata: %{}}
iex> Metastatic.Validator.valid?(invalid_doc)
false
"""
@spec valid?(Document.t(), keyword()) :: boolean()
def valid?(%Document{} = document, opts \\ []) do
case validate(document, opts) do
{:ok, _} -> true
{:error, _} -> false
end
end
@doc """
Validate just the AST structure (without Document wrapper).
## Examples
iex> {:ok, meta} = Metastatic.Validator.validate_ast({:literal, :integer, 42})
iex> meta.level
:core
iex> Metastatic.Validator.validate_ast({:invalid, "nope"})
{:error, {:invalid_structure, {:invalid, "nope"}}}
"""
@spec validate_ast(AST.meta_ast(), keyword()) :: validation_result()
def validate_ast(ast, opts \\ []) do
mode = Keyword.get(opts, :mode, :standard)
max_depth = Keyword.get(opts, :max_depth, 1000)
max_variables = Keyword.get(opts, :max_variables, 10_000)
with {:ok, _} <- validate_structure(ast),
{:ok, meta} <- analyze_ast(ast),
:ok <- check_constraints(meta, mode, max_depth, max_variables) do
{:ok, meta}
end
end
# Structural validation
defp validate_structure(ast) do
if AST.conforms?(ast) do
{:ok, ast}
else
{:error, {:invalid_structure, ast}}
end
end
# AST analysis
defp analyze_ast(ast) do
meta = %{
level: determine_level(ast),
native_constructs: count_native(ast),
warnings: generate_warnings(ast),
variables: AST.variables(ast),
depth: calculate_depth(ast),
node_count: count_nodes(ast)
}
{:ok, meta}
end
# Determine highest M2 level used
defp determine_level(ast) do
cond do
has_native?(ast) -> :native
has_extended?(ast) -> :extended
true -> :core
end
end
defp has_native?(ast) do
count_native(ast) > 0
end
defp has_extended?(ast) do
walk_ast(ast, false, fn
# M2.2: Extended layer - Common patterns with variations
{:loop, _, _, _}, _acc -> true
{:lambda, _, _, _}, _acc -> true
{:collection_op, _, _, _}, _acc -> true
{:pattern_match, _, _}, _acc -> true
{:exception_handling, _, _, _}, _acc -> true
{:async_operation, _, _}, _acc -> true
# M2.2s: Structural/Organizational layer
# NEW format: {:container, type, name, parent, type_params, implements, body}
{:container, _, _, _, _, _, _}, _acc -> true
# NEW format: {:function_def, name, params, ret_type, opts, body}
{:function_def, _, _, _, _, _}, _acc -> true
{:attribute_access, _, _}, _acc -> true
{:augmented_assignment, _, _, _}, _acc -> true
{:property, _, _, _, _}, _acc -> true
# M2.1 Core: list and map are NOT extended
# {:list, _}, _acc -> false
# {:map, _}, _acc -> false
_node, acc -> acc
end)
end
# Count M2.3 native constructs
defp count_native(ast) do
walk_ast(ast, 0, fn
{:language_specific, _, _}, acc -> acc + 1
_node, acc -> acc
end)
end
# Generate validation warnings
defp generate_warnings(ast) do
warnings = []
warnings =
if has_native?(ast) do
[{:native_constructs_present, count_native(ast)} | warnings]
else
warnings
end
warnings =
if calculate_depth(ast) > 100 do
[{:deep_nesting, calculate_depth(ast)} | warnings]
else
warnings
end
warnings =
if count_nodes(ast) > 1000 do
[{:large_ast, count_nodes(ast)} | warnings]
else
warnings
end
Enum.reverse(warnings)
end
# Check validation constraints
defp check_constraints(meta, mode, max_depth, max_variables) do
with :ok <- check_mode(meta, mode),
:ok <- check_depth(meta.depth, max_depth) do
check_variables(meta.variables, max_variables)
end
end
defp check_mode(meta, :strict) do
if meta.native_constructs > 0 do
{:error, :native_constructs_not_allowed}
else
:ok
end
end
defp check_mode(_meta, _mode), do: :ok
defp check_depth(depth, max_depth) do
if depth > max_depth do
{:error, {:max_depth_exceeded, depth, max_depth}}
else
:ok
end
end
defp check_variables(variables, max_variables) do
count = MapSet.size(variables)
if count > max_variables do
{:error, {:too_many_variables, count, max_variables}}
else
:ok
end
end
# AST traversal utilities
defp calculate_depth(ast, current_depth \\ 0)
defp calculate_depth({type, _} = ast, current_depth)
when type in [:literal, :variable] do
max(current_depth + 1, depth_of_children(ast, current_depth + 1))
end
defp calculate_depth(ast, current_depth) when is_tuple(ast) do
max(current_depth + 1, depth_of_children(ast, current_depth + 1))
end
defp calculate_depth(_ast, current_depth), do: current_depth
defp depth_of_children(ast, current_depth) when is_tuple(ast) do
ast
|> Tuple.to_list()
|> Enum.reduce(current_depth, fn
child, max_depth when is_tuple(child) ->
max(max_depth, calculate_depth(child, current_depth))
child, max_depth when is_list(child) ->
child_depths =
Enum.map(child, fn item ->
if is_tuple(item), do: calculate_depth(item, current_depth), else: current_depth
end)
max(max_depth, Enum.max(child_depths ++ [current_depth]))
_other, max_depth ->
max_depth
end)
end
defp depth_of_children(_ast, current_depth), do: current_depth
defp count_nodes(ast) do
walk_ast(ast, 0, fn _node, acc -> acc + 1 end)
end
defp walk_ast(ast, acc, fun) when is_tuple(ast) do
acc = fun.(ast, acc)
ast
|> Tuple.to_list()
|> Enum.reduce(acc, fn
child, acc when is_tuple(child) -> walk_ast(child, acc, fun)
children, acc when is_list(children) -> walk_ast_list(children, acc, fun)
_other, acc -> acc
end)
end
defp walk_ast(ast, acc, fun) when is_list(ast) do
walk_ast_list(ast, acc, fun)
end
defp walk_ast(_ast, acc, _fun), do: acc
defp walk_ast_list(list, acc, fun) do
Enum.reduce(list, acc, fn
item, acc when is_tuple(item) -> walk_ast(item, acc, fun)
item, acc when is_list(item) -> walk_ast_list(item, acc, fun)
_other, acc -> acc
end)
end
end