Packages
metastatic
0.8.0
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
metastatic
SUPPLEMENTAL_MODULES.md
SUPPLEMENTAL_MODULES.md
# Supplemental Modules Guide
This guide explains Metastatic's supplemental module system - a mechanism for extending MetaAST with language-specific library integrations.
## Table of Contents
- [Overview](#overview)
- [Core Concepts](#core-concepts)
- [Architecture](#architecture)
- [Using Supplementals](#using-supplementals)
- [Available Supplementals](#available-supplementals)
- [Creating Supplementals](#creating-supplementals)
- [Best Practices](#best-practices)
- [API Reference](#api-reference)
## Overview
Supplemental modules bridge the gap between MetaAST's language-agnostic representation and real-world library ecosystems. They enable:
- **Cross-language transformations** - Express actor patterns in Python/Erlang/JavaScript
- **Library integrations** - Map MetaAST constructs to third-party APIs (Pykka, asyncio, etc.)
- **Semantic preservation** - Maintain intent while adapting to language-specific idioms
### Example
```elixir
# MetaAST representing an actor call
ast = {:actor_call, {:variable, "worker"}, "process", [data]}
# Transform using Pykka supplemental for Python
{:ok, python_ast} = Metastatic.Supplemental.Transformer.transform(ast, :python)
# Result: {:function_call, "worker.ask", ["process", data]}
# Same AST, different supplemental for Erlang
{:ok, erlang_ast} = Metastatic.Supplemental.Transformer.transform(ast, :erlang)
# Result: {:function_call, "gen_server:call", [worker, {:process, data}]}
```
## Core Concepts
### M2 Layer Extension
Supplementals operate **at the M2 meta-model level**, adding optional constructs beyond Core/Extended/Native:
```
M2 Core - Universals (literals, variables, operators, conditionals)
M2 Extended - Common patterns (loops, lambdas, collections)
M2 Native - Language escapes
M2 Supplemental - Library-specific extensions (actors, async, etc.)
```
Supplemental constructs are **not part of the base MetaAST grammar** - they're opt-in extensions that require explicit transformation.
### Semantic Contract
Each supplemental defines:
- **Constructs** - Which MetaAST node types it handles (e.g., `:actor_call`, `:async_await`)
- **Target language** - Single language it generates code for (`:python`, `:javascript`, etc.)
- **Dependencies** - External libraries required (`"pykka >= 3.0"`, `"asyncio"`)
- **Transformation** - How to convert supplemental constructs to concrete code
### Registry System
A centralized GenServer registry maintains all available supplementals, enabling:
- Fast lookup by construct type
- Language compatibility checking
- Conflict detection between competing supplementals
- Runtime registration/deregistration
## Architecture
### Components
```mermaid
flowchart TD
subgraph System["Supplemental System"]
Behaviour["Behaviour<br/>(spec)"] --> Info["Info<br/>(metadata)"]
Behaviour --> Registry
Registry["Registry (GenServer)<br/>- by_construct index<br/>- by_language index<br/>- conflict detection"]
Registry --> Transformer
Transformer["Transformer<br/>- lookup + invoke supplementals<br/>- error handling"]
Transformer --> Concrete
Concrete["Concrete Supplementals<br/>- Python.Pykka<br/>- Python.Asyncio<br/>- ... (extensible)"]
end
```
### Data Flow
```mermaid
flowchart TD
A["User AST with supplemental constructs"] --> B["Validator"]
B --> C["Identify required supplementals"]
C --> D["Registry lookup"]
D --> E["Transformer"]
E --> F["Invoke supplemental.transform/3"]
F --> G["Concrete language AST"]
```
## Using Supplementals
### Basic Usage
```elixir
alias Metastatic.Supplemental.Transformer
# Single construct transformation
ast = {:actor_call, {:variable, "worker"}, "process", [data]}
{:ok, result} = Transformer.transform(ast, :python)
# Check availability before transforming
if Transformer.available?(:actor_call, :python) do
{:ok, transformed} = Transformer.transform(ast, :python)
end
# Get all supported constructs for a language
constructs = Transformer.supported_constructs(:python)
# => [:actor_call, :actor_cast, :spawn_actor, :async_await, :async_context, :gather]
```
### With Adapters
Supplementals integrate automatically when using the adapter pipeline:
```elixir
alias Metastatic.Builder
# Source with supplemental constructs
source = """
actor = spawn_actor(Worker, [config])
result = actor_call(actor, "process", [data])
"""
# Adapter automatically uses supplementals during transformation
{:ok, doc} = Builder.from_source(source, :python)
# Round-trip preserves supplemental transformations
{:ok, output} = Builder.to_source(doc)
```
### Configuration
Configure auto-registration in `config/config.exs`:
```elixir
config :metastatic, :supplementals,
auto_register: [
Metastatic.Supplemental.Python.Pykka,
Metastatic.Supplemental.Python.Asyncio
]
```
### Validation
Use the validator to detect required supplementals in a document:
```elixir
alias Metastatic.{Document, Supplemental.Validator}
doc = Document.new(ast, :python)
{:ok, analysis} = Validator.validate(doc)
# Check what supplementals are needed
analysis.required_supplementals
# => [:pykka, :asyncio]
# Get warnings about missing supplementals
analysis.warnings
# => ["Document requires supplemental ':pykka' which is not registered"]
```
## Available Supplementals
### Python.Pykka
**Actor model support via Pykka library**
**Constructs:**
- `:actor_call` - Synchronous actor message (ask pattern)
- `:actor_cast` - Asynchronous actor message (tell pattern)
- `:spawn_actor` - Create new actor instance
**Dependencies:** `pykka >= 3.0`
**Example:**
```elixir
# Actor call
{:actor_call, {:variable, "worker"}, "process", [data]}
# Transforms to: worker.ask({'process': data})
# Actor cast
{:actor_cast, {:variable, "worker"}, "log", [message]}
# Transforms to: worker.tell({'log': message})
# Spawn actor
{:spawn_actor, "WorkerActor", [config]}
# Transforms to: WorkerActor.start(config)
```
### Python.Asyncio
**Async/await patterns via asyncio library**
**Constructs:**
- `:async_await` - Async function execution
- `:async_context` - Async context managers
- `:gather` - Parallel task execution
**Dependencies:** `asyncio` (stdlib)
**Example:**
```elixir
# Async await
{:async_operation, :async_await,
{:function_call, "fetch_data", [url]}}
# Transforms to: asyncio.run(fetch_data(url))
# Gather
{:async_operation, :gather, [
{:function_call, "fetch_user", [1]},
{:function_call, "fetch_posts", [1]}
]}
# Transforms to: asyncio.gather(fetch_user(1), fetch_posts(1))
```
## Creating Supplementals
### Step 1: Implement the Behaviour
```elixir
defmodule MyProject.Supplemental.Python.MyLibrary do
@behaviour Metastatic.Supplemental
alias Metastatic.Supplemental.Info
@impl true
def info do
%Info{
name: :my_library,
language: :python,
constructs: [:my_construct],
requires: ["my-library >= 1.0"],
description: "My library integration"
}
end
@impl true
def transform(ast, language, opts)
def transform({:my_construct, args}, :python, _opts) do
# Transform logic here
result = {:function_call, "my_library.do_thing", args}
{:ok, result}
end
def transform(_ast, :python, _opts) do
{:error, {:unsupported_construct, "..."}}
end
def transform(_ast, language, _opts) do
{:error, {:incompatible_language, "..."}}
end
end
```
### Step 2: Write Tests
```elixir
defmodule MyProject.Supplemental.Python.MyLibraryTest do
use ExUnit.Case, async: true
alias MyProject.Supplemental.Python.MyLibrary
describe "info/0" do
test "returns correct metadata" do
info = MyLibrary.info()
assert info.name == :my_library
assert info.language == :python
end
end
describe "transform/3" do
test "transforms my_construct correctly" do
ast = {:my_construct, [{:literal, :string, "arg"}]}
assert {:ok, result} = MyLibrary.transform(ast, :python)
assert result == {:function_call, "my_library.do_thing",
[{:literal, :string, "arg"}]}
end
test "returns error for wrong language" do
ast = {:my_construct, []}
assert {:error, {:incompatible_language, _}} =
MyLibrary.transform(ast, :javascript)
end
end
end
```
### Step 3: Register
```elixir
# Manual registration
alias Metastatic.Supplemental.Registry
{:ok, _} = Registry.register(MyProject.Supplemental.Python.MyLibrary)
# Or via config (auto-registers on startup)
config :metastatic, :supplementals,
auto_register: [MyProject.Supplemental.Python.MyLibrary]
```
### Step 4: Document
Add module documentation with:
- Clear description of what library/pattern it supports
- List of all constructs handled
- Examples showing MetaAST input and output
- Dependency requirements
- Any caveats or limitations
## Best Practices
### Design Guidelines
1. **Single Responsibility** - One supplemental per library/pattern
2. **Explicit Constructs** - Use distinct construct atoms (`:actor_call` not `:call`)
3. **Language Specific** - Target exactly one language per supplemental
4. **Idiomatic Output** - Generate natural, idiomatic code for target language
5. **Comprehensive Tests** - Test all constructs, error cases, edge cases
### Naming Conventions
**Modules:**
```
Metastatic.Supplemental.<Language>.<Library>
├── Python.Pykka (library name)
├── Python.Asyncio (pattern name)
└── JavaScript.RxJS (library name)
```
**Construct atoms:**
```
:actor_call (prefixed by domain)
:async_await (verb describing action)
:spawn_actor (specific, unambiguous)
```
**Info names:**
```
name: :pykka (short, lowercase atom)
name: :asyncio (match library name)
```
### Error Handling
Return specific errors for different failure modes:
```elixir
# Unsupported construct
{:error, {:unsupported_construct, "Pykka does not support: #{inspect(ast)}"}}
# Wrong language
{:error, {:incompatible_language, "Pykka only supports Python, got: #{language}"}}
# Invalid options
{:error, {:invalid_options, "Unknown option: #{inspect(key)}"}}
```
### Version Constraints
Use semantic versioning in `requires`:
```elixir
requires: [
"pykka >= 3.0, < 4.0", # Major version constraint
"asyncio", # Stdlib, no version needed
"aiohttp >= 3.8" # Minimum version only
]
```
### Testing Strategy
Cover these scenarios:
1. **Info validation** - Correct metadata structure
2. **Happy path** - Each construct transforms correctly
3. **Error cases** - Wrong language, unsupported constructs
4. **Edge cases** - Empty arguments, nested structures, complex types
5. **Options** - Handle all option variations
6. **Idempotency** - Multiple transformations don't break
### Performance Considerations
- Keep transformations fast (<1ms per node)
- Avoid heavy computation in `info/0` (called frequently)
- Use pattern matching for dispatch (faster than conditionals)
- Don't validate AST structure (adapters already do this)
## API Reference
### Behaviour Callbacks
**`info/0`**
Returns metadata about the supplemental.
```elixir
@callback info() :: Info.t()
```
**`transform/3`**
Transforms a supplemental construct to target language AST.
```elixir
@callback transform(
ast :: AST.meta_ast(),
language :: atom(),
opts :: map()
) :: {:ok, AST.meta_ast()} | {:error, term()}
```
### Registry Functions
**`register/1`**
Register a supplemental module.
```elixir
Registry.register(MySupplemental)
# => {:ok, :pykka} | {:error, {:already_registered, :pykka}}
```
**`lookup/2`**
Find supplemental for construct and language.
```elixir
Registry.lookup(:actor_call, :python)
# => {:ok, Metastatic.Supplemental.Python.Pykka} |
# {:error, :not_found}
```
**`all/0`**
List all registered supplementals.
```elixir
Registry.all()
# => [Metastatic.Supplemental.Python.Pykka, ...]
```
### Transformer Functions
**`transform/3`**
Transform a construct using registered supplementals.
```elixir
Transformer.transform(ast, :python)
# => {:ok, transformed_ast} | {:error, reason}
```
**`available?/2`**
Check if a construct is supported for a language.
```elixir
Transformer.available?(:actor_call, :python)
# => true | false
```
**`supported_constructs/1`**
Get all supported constructs for a language.
```elixir
Transformer.supported_constructs(:python)
# => [:actor_call, :actor_cast, :spawn_actor, ...]
```
### Validator Functions
**`validate/1`**
Analyze a document to detect required supplementals.
```elixir
Validator.validate(document)
# => {:ok, %{required_supplementals: [...], warnings: [...]}}
```
## Advanced Topics
### Conflict Resolution
When multiple supplementals claim the same construct+language:
```elixir
# Registry detects conflicts
Registry.register(AlternativePykka)
# => {:error, {:conflict, existing: Pykka, new: AlternativePykka}}
# Unregister old, register new
Registry.unregister(Pykka)
Registry.register(AlternativePykka)
```
### Chaining Transformations
Supplementals can compose:
```elixir
# First supplemental adds intermediate construct
{:ok, ast1} = Supplemental1.transform(input, :python)
# Second supplemental further transforms
{:ok, ast2} = Supplemental2.transform(ast1, :python)
```
### Stateful Transformations
Pass state via options:
```elixir
opts = %{
actor_prefix: "my_app",
async_mode: :gather
}
Transformer.transform(ast, :python, opts)
```
### Testing with Fixtures
Create test fixtures for complex scenarios:
```python
# test/fixtures/python/supplemental/pykka_actors.py
class Worker(pykka.ThreadingActor):
def process(self, data):
return data * 2
worker = Worker.start()
result = worker.ask({'process': 42})
```
## Contributing
Want to add a supplemental?
Use the generator to scaffold a new supplemental:
```bash
mix metastatic.gen.supplemental Python MyLibrary
```
## Future Work
Planned supplementals:
- **Python.Celery** - Distributed task queue
- **JavaScript.RxJS** - Reactive programming
- **Ruby.Concurrent** - Concurrency primitives
- **Go.Channels** - CSP-style communication
- **Rust.Tokio** - Async runtime