Packages
metastatic
0.20.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/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.
## New 3-Tuple Format
All MetaAST nodes are uniform 3-element tuples:
{type_atom, keyword_meta, children_or_value}
## Conformance Rules (Definition 8 from THEORETICAL_FOUNDATIONS.md)
A term `t` in 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 Decision Tree
```mermaid
flowchart TD
Input["Input: Document / AST"] --> Structure{"Structure valid?<br/>AST.conforms?"}
Structure -->|No| Err1["error: invalid_structure"]
Structure -->|Yes| Analyze["Single-pass analysis<br/>collect metrics"]
Analyze --> Level{"Classify level"}
Level -->|"has :language_specific"| Native[":native"]
Level -->|"has extended types"| Extended[":extended"]
Level -->|"core types only"| Core[":core"]
Native --> Mode{"Check mode"}
Extended --> Mode
Core --> Mode
Mode -->|":strict + native > 0"| Err2["error: native_constructs_not_allowed"]
Mode -->|"depth > max"| Err3["error: max_depth_exceeded"]
Mode -->|"vars > max"| Err4["error: too_many_variables"]
Mode -->|"all ok"| OK["{:ok, metadata}"]
```
## 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, [category: :arithmetic, operator: :+],
[{:variable, [], "x"}, {:literal, [subtype: :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, [subtype: :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, [subtype: :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, [subtype: :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 - single-pass traversal collecting all metrics at once.
# Previous implementation used 7+ separate traversals; this collects
# level, native count, variables, depth, and node count in one pass.
# Extended layer types (M2.2 + M2.2s)
@extended_types [
:loop,
:lambda,
:collection_op,
:pattern_match,
:match_arm,
:exception_handling,
:async_operation,
:yield,
# M2.2s: Structural/Organizational layer
:container,
:function_def,
:attribute_access,
:augmented_assignment,
:property,
:decorator
]
# Native layer types (M2.3)
@native_types [:language_specific]
defp analyze_ast(ast) do
initial_acc = %{
native_count: 0,
has_extended: false,
variables: MapSet.new(),
current_depth: 0,
max_depth: 0,
node_count: 0
}
{_ast, acc} = AST.traverse(ast, initial_acc, &analyze_pre/2, &analyze_post/2)
level =
cond do
acc.native_count > 0 -> :native
acc.has_extended -> :extended
true -> :core
end
warnings = build_warnings(acc)
meta = %{
level: level,
native_constructs: acc.native_count,
warnings: warnings,
variables: acc.variables,
depth: acc.max_depth,
node_count: acc.node_count
}
{:ok, meta}
end
# Pre: entering a node - track depth, count nodes, classify type, collect variables
defp analyze_pre({type, meta, _children} = node, acc)
when is_atom(type) and is_list(meta) do
new_depth = acc.current_depth + 1
acc =
acc
|> Map.update!(:current_depth, fn _ -> new_depth end)
|> Map.update!(:max_depth, &max(&1, new_depth))
|> Map.update!(:node_count, &(&1 + 1))
# Classify node type
acc =
cond do
type in @native_types -> Map.update!(acc, :native_count, &(&1 + 1))
type in @extended_types -> Map.put(acc, :has_extended, true)
true -> acc
end
# Collect variables
acc =
if type == :variable and is_binary(elem(node, 2)) do
Map.update!(acc, :variables, &MapSet.put(&1, elem(node, 2)))
else
acc
end
{node, acc}
end
defp analyze_pre(node, acc), do: {node, acc}
# Post: leaving a node - decrement depth
defp analyze_post({type, meta, _children} = node, acc)
when is_atom(type) and is_list(meta) do
{node, Map.update!(acc, :current_depth, &max(0, &1 - 1))}
end
defp analyze_post(node, acc), do: {node, acc}
# Build warnings from accumulated metrics
defp build_warnings(acc) do
warnings = []
warnings =
if acc.native_count > 0 do
[{:native_constructs_present, acc.native_count} | warnings]
else
warnings
end
warnings =
if acc.max_depth > 100 do
[{:deep_nesting, acc.max_depth} | warnings]
else
warnings
end
warnings =
if acc.node_count > 1000 do
[{:large_ast, acc.node_count} | 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
end