Packages
Virtual time extension to GenServer and GenStateMachine allowing testing time-based actor systems orders of magnitude faster than in wallclock-time. Includes actor simulation DSL with statistics, tracing, and code generation into other Actor Model implementations in C++, Pony, Go, Rust, Java.
Retired package: Deprecated - superfluous
Current section
Files
Jump to
Current section
Files
lib/actor_simulation/pony_generator.ex
defmodule ActorSimulation.PonyGenerator do
@moduledoc """
Generates Pony actor code from ActorSimulation DSL.
Pony is a capabilities-secure, actor-model language that provides:
- Type safety and memory safety
- Data-race freedom and deadlock freedom
- Zero-cost actors with async message passing
- Built-in testing with PonyTest
This generator creates production-ready Pony projects with:
- Actor implementations with behaviors
- Callback traits for custom behavior
- PonyTest test suites
- Corral dependency management
- CI/CD pipeline
- Complete documentation
## Example
simulation = ActorSimulation.new()
|> ActorSimulation.add_actor(:sender,
send_pattern: {:periodic, 100, :msg},
targets: [:receiver])
|> ActorSimulation.add_actor(:receiver)
{:ok, files} = PonyGenerator.generate(simulation,
project_name: "my_actors",
enable_callbacks: true)
PonyGenerator.write_to_directory(files, "pony_output/")
"""
@doc """
Generates complete Pony project files from an ActorSimulation.
## Options
- `:project_name` (required) - Name of the Pony project (snake_case)
- `:enable_callbacks` (default: true) - Generate callback traits
## Returns
`{:ok, files}` where files is a list of `{filename, content}` tuples
"""
def generate(simulation, opts \\ []) do
project_name = Keyword.fetch!(opts, :project_name)
enable_callbacks = Keyword.get(opts, :enable_callbacks, true)
actors = simulation.actors
files =
[]
|> add_actor_files(actors, enable_callbacks)
|> add_main_file(actors, project_name)
|> add_test_files(actors, project_name)
|> add_corral_file(project_name)
|> add_ci_pipeline(project_name)
|> add_readme(project_name)
{:ok, files}
end
@doc """
Writes generated files to a directory.
Creates the directory and all subdirectories as needed.
"""
def write_to_directory(files, output_dir) do
ActorSimulation.GeneratorUtils.write_to_directory(files, output_dir)
end
# Private functions
defp generate_console_logger do
"""
// Generated from ActorSimulation DSL
// Thread-safe console logger actor
//
// Based on best practices from Pony actor systems
// See: https://github.com/d-led/DDDwithActorsPony/blob/master/Receiver.pony
actor ConsoleLogger
\"\"\"
Thread-safe console logger that uses env.out for output.
All logging goes through this actor to avoid race conditions.
\"\"\"
let _out: OutStream
new create(out: OutStream) =>
_out = out
be log(msg: String) =>
\"\"\"
Log a message to console.
This is thread-safe as it's processed sequentially by the actor.
\"\"\"
_out.print(msg)
"""
end
defp add_actor_files(files, actors, enable_callbacks) do
# Add console logger first
logger_file = generate_console_logger()
files = [{"console_logger.pony", logger_file} | files]
Enum.reduce(actors, files, fn {name, actor_info}, acc ->
case actor_info.type do
:simulated ->
definition = actor_info.definition
actor_name = actor_snake_case(name)
actor_file = generate_actor_file(name, definition, enable_callbacks)
new_files = [{"#{actor_name}.pony", actor_file}]
new_files =
if enable_callbacks do
callback_trait = generate_callback_trait(name, definition, enable_callbacks)
new_files ++ [{"#{actor_name}_callbacks.pony", callback_trait}]
else
new_files
end
new_files ++ acc
:real_process ->
# Skip real processes for Pony generation
acc
end
end)
end
defp add_main_file(files, actors, project_name) do
content = generate_main(actors, project_name)
[{"main.pony", content} | files]
end
defp add_test_files(files, actors, project_name) do
test_content = generate_test_file(actors, project_name)
[{"test/test.pony", test_content} | files]
end
defp add_corral_file(files, project_name) do
content = generate_corral(project_name)
[{"corral.json", content} | files]
end
defp add_ci_pipeline(files, project_name) do
content = generate_ci_pipeline(project_name)
[{".github/workflows/ci.yml", content} | files]
end
defp add_readme(files, project_name) do
content = generate_readme(project_name)
[{"README.md", content} | files]
end
defp generate_actor_file(name, definition, enable_callbacks) do
actor_name = actor_class_name(name)
callback_field =
if enable_callbacks do
"""
let _callbacks: #{actor_class_name(name)}Callbacks
"""
else
""
end
callback_init =
if enable_callbacks do
"""
_callbacks = #{actor_class_name(name)}CallbacksImpl(logger)
"""
else
""
end
timer_code = generate_timer_code(definition)
behaviors = generate_behaviors(name, definition, enable_callbacks)
"""
// Generated from ActorSimulation DSL
// Actor: #{name}
use \"collections\"
use \"time\"
actor #{actor_name}
let _env: Env
let _timers: Timers = Timers
let _targets: Array[#{actor_class_name(name)}] = Array[#{actor_class_name(name)}]
let logger: ConsoleLogger
#{callback_field}
new create(env: Env, logger': ConsoleLogger, targets: Array[#{actor_class_name(name)}] val = recover Array[#{actor_class_name(name)}] end) =>
_env = env
logger = logger'
_targets.append(targets)
#{callback_init}#{timer_code}
#{behaviors}
"""
end
defp generate_timer_code(definition) do
case definition.send_pattern do
nil ->
""
{:periodic, interval_ms, message} ->
interval_ns = trunc(interval_ms * 1_000_000)
msg_name = message_name(message)
"""
let timer = Timer(#{actor_class_name(msg_name)}Timer(this), #{interval_ns}, #{interval_ns})
_timers(consume timer)
"""
{:rate, per_second, message} ->
interval_ns = trunc(1000.0 / per_second * 1_000_000)
msg_name = message_name(message)
"""
let timer = Timer(#{actor_class_name(msg_name)}Timer(this), #{interval_ns}, #{interval_ns})
_timers(consume timer)
"""
{:burst, count, interval_ms, message} ->
interval_ns = trunc(interval_ms * 1_000_000)
msg_name = message_name(message)
"""
let timer = Timer(#{actor_class_name(msg_name)}BurstTimer(this, #{count}), #{interval_ns}, #{interval_ns})
_timers(consume timer)
"""
{:self_message, delay_ms, message} ->
delay_ns = trunc(delay_ms * 1_000_000)
msg_name = message_name(message)
"""
// One-shot self-message timer
let timer = Timer(#{actor_class_name(msg_name)}OneShotTimer(this), #{delay_ns}, 0)
_timers(consume timer)
"""
end
end
defp generate_behaviors(name, definition, enable_callbacks) do
messages = extract_messages_from_pattern(definition.send_pattern)
behavior_defs =
Enum.map(messages, fn msg ->
msg_name = message_name(msg)
callback_call =
if enable_callbacks do
"""
_callbacks.on_#{msg_name}()
"""
else
"""
// Message received: #{msg}
"""
end
"""
be #{msg_name}() =>
#{callback_call} // Send to targets
for target in _targets.values() do
target.#{msg_name}()
end
"""
end)
timer_classes = generate_timer_classes(name, definition)
Enum.join(behavior_defs, "\n") <> "\n" <> timer_classes
end
defp generate_timer_classes(name, definition) do
case definition.send_pattern do
nil ->
""
{:periodic, _interval_ms, message} ->
msg_name = message_name(message)
actor_name = actor_class_name(name)
"""
class #{actor_class_name(msg_name)}Timer is TimerNotify
let _actor: #{actor_name} tag
new iso create(actor': #{actor_name}) =>
_actor = actor'
fun ref apply(timer: Timer, count: U64): Bool =>
_actor.#{msg_name}()
true // Keep timer running
"""
{:rate, _per_second, message} ->
msg_name = message_name(message)
actor_name = actor_class_name(name)
"""
class #{actor_class_name(msg_name)}Timer is TimerNotify
let _actor: #{actor_name} tag
new iso create(actor': #{actor_name}) =>
_actor = actor'
fun ref apply(timer: Timer, count: U64): Bool =>
_actor.#{msg_name}()
true // Keep timer running
"""
{:burst, _count, _interval_ms, message} ->
msg_name = message_name(message)
actor_name = actor_class_name(name)
"""
class #{actor_class_name(msg_name)}BurstTimer is TimerNotify
let _actor: #{actor_name} tag
let _burst_count: USize
new iso create(actor': #{actor_name}, burst_count: USize) =>
_actor = actor'
_burst_count = burst_count
fun ref apply(timer: Timer, count: U64): Bool =>
var i: USize = 0
while i < _burst_count do
_actor.#{msg_name}()
i = i + 1
end
true // Keep timer running
"""
{:self_message, _delay_ms, message} ->
msg_name = message_name(message)
actor_name = actor_class_name(name)
"""
class #{actor_class_name(msg_name)}OneShotTimer is TimerNotify
let _actor: #{actor_name} tag
new iso create(actor': #{actor_name}) =>
_actor = actor'
fun ref apply(timer: Timer, count: U64): Bool =>
_actor.#{msg_name}()
false // Don't repeat (one-shot)
"""
end
end
defp generate_callback_trait(name, definition, _enable_callbacks) do
actor_name = actor_class_name(name)
messages = extract_messages_from_pattern(definition.send_pattern)
methods =
Enum.map(messages, fn msg ->
msg_name = message_name(msg)
" fun ref on_#{msg_name}()"
end)
methods_str =
if length(methods) > 0 do
Enum.join(methods, "\n")
else
" fun ref on_message()"
end
impl_methods =
Enum.map(messages, fn msg ->
msg_name = message_name(msg)
# If actor has send_pattern, it's the sender (publisher)
action = if definition.send_pattern, do: "Sending", else: "Received"
"""
fun ref on_#{msg_name}() =>
// TODO: Implement custom behavior for #{msg}
_logger.log("#{actor_name}: #{action} #{msg} message")
"""
end)
impl_methods_str =
if length(impl_methods) > 0 do
Enum.join(impl_methods, "\n")
else
"""
fun ref on_message() =>
// TODO: Implement custom behavior
_logger.log("#{actor_name}: Processing message")
"""
end
"""
// Generated from ActorSimulation DSL
// Callback trait for: #{name}
//
// Implement this trait to add custom behavior!
trait #{actor_name}Callbacks
#{methods_str}
class #{actor_name}CallbacksImpl is #{actor_name}Callbacks
\"\"\"
Default implementation of #{actor_name} callbacks.
CUSTOMIZE THIS CLASS to add your own behavior!
The generated actor code will call these methods.
\"\"\"
let _logger: ConsoleLogger
new create(logger: ConsoleLogger) =>
_logger = logger
#{impl_methods_str}
"""
end
defp generate_main(actors, project_name) do
simulated_actors =
actors
|> Enum.filter(fn {_name, info} -> info.type == :simulated end)
|> Enum.map(fn {name, info} -> {name, info.definition} end)
spawn_code = generate_spawn_code(simulated_actors)
"""
// Generated from ActorSimulation DSL
// Main entry point for #{project_name}
actor Main
new create(env: Env) =>
\"\"\"
Start the actor system.
\"\"\"
// Create thread-safe console logger
let logger = ConsoleLogger(env.out)
// Spawn all actors
#{spawn_code}
env.out.print("Actor system started. Press Ctrl+C to exit.")
"""
end
defp generate_spawn_code(actors) do
actors
|> Enum.map_join("\n", fn {name, _def} ->
actor_name = actor_snake_case(name)
class_name = actor_class_name(name)
" let #{actor_name} = #{class_name}(env, logger)"
end)
end
defp generate_test_file(actors, project_name) do
simulated_actors =
actors
|> Enum.filter(fn {_name, info} -> info.type == :simulated end)
|> Enum.map(fn {name, info} -> {name, info.definition} end)
test_classes = generate_test_classes(simulated_actors)
"""
// Generated from ActorSimulation DSL
// PonyTest tests for #{project_name}
use \"pony_test\"
use \"..\" // Import parent package (all actor files and console_logger)
actor Main is TestList
new create(env: Env) => PonyTest(env, this)
new make() => None
fun tag tests(test: PonyTest) =>
test(_TestActorSystem)
#{test_classes}
"""
end
defp generate_test_classes(actors) do
actors
|> Enum.map_join("", fn {name, _def} ->
class_name = actor_class_name(name)
"""
test(_Test#{class_name})
"""
end)
|> then(fn tests ->
actor_test_cases =
Enum.map(actors, fn {name, _def} ->
class_name = actor_class_name(name)
var_name = actor_snake_case(name)
"""
class iso _Test#{class_name} is UnitTest
\"\"\"Test that #{class_name} actor can be created.\"\"\"
fun name(): String => "#{class_name} actor"
fun apply(h: TestHelper) =>
h.long_test(2_000_000_000) // 2 second timeout
// Actor creation test
let logger = ConsoleLogger(h.env.out)
let #{var_name} = #{class_name}(h.env, logger)
h.complete(true)
"""
end)
tests <>
"""
class iso _TestActorSystem is UnitTest
\"\"\"Test that the actor system can be initialized.\"\"\"
fun name(): String => "Actor System"
fun apply(h: TestHelper) =>
h.long_test(2_000_000_000) // 2 second timeout
h.complete(true)
""" <> Enum.join(actor_test_cases, "")
end)
end
defp generate_corral(project_name) do
# Use the library version for generated projects
version = GenServerVirtualTime.version()
"""
{
"info": {
"name": "#{project_name}",
"description": "Generated from ActorSimulation DSL",
"version": "#{version}",
"license": "MIT"
},
"deps": []
}
"""
end
defp generate_ci_pipeline(_project_name) do
"""
name: CI
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
steps:
- uses: actions/checkout@v3
- name: Install Pony
run: |
if [ "$RUNNER_OS" == "Linux" ]; then
sudo apt-get update
sudo apt-get install -y ponyup
ponyup update ponyc release
elif [ "$RUNNER_OS" == "macOS" ]; then
brew install ponyup
ponyup update ponyc release
fi
shell: bash
- name: Install Corral
run: |
ponyup update corral release
- name: Cache Corral packages
uses: actions/cache@v3
with:
path: |
_corral
.corral
key: ${{ runner.os }}-corral-${{ hashFiles('**/corral.json') }}
restore-keys: |
${{ runner.os }}-corral-
- name: Build
run: |
corral fetch
ponyc .
DIR_NAME=$(basename "$PWD")
OS_NAME=$(uname -s | tr '[:upper:]' '[:lower:]')
PROJECT_NAME=$(grep -o '"name": *"[^"]*"' corral.json | head -1 | sed 's/"name": *"\\([^"]*\\)"/\\1/')
BINARY="${PROJECT_NAME}.pony.${OS_NAME}"
mv "$DIR_NAME" "$BINARY" 2>/dev/null || true
- name: Build and Run tests
run: |
ponyc test
if [ -f ./test1 ]; then timeout 1 ./test1 || true; elif [ -f ./test ]; then timeout 1 ./test || true; fi
- name: Run Demo Application
run: |
# Determine binary name: {project}.pony.{os}
OS_NAME=$(uname -s | tr '[:upper:]' '[:lower:]')
PROJECT_NAME=$(grep -o '"name": *"[^"]*"' corral.json | head -1 | sed 's/"name": *"\\([^"]*\\)"/\\1/')
BINARY="${PROJECT_NAME}.pony.${OS_NAME}"
timeout 5 ./"${BINARY}" || true
"""
end
defp generate_readme(project_name) do
"""
# #{project_name}
Generated from ActorSimulation DSL using Pony.
## About
This project uses [Pony](https://www.ponylang.io/), a capabilities-secure,
actor-model language that provides:
- **Type Safety** - No null pointers, no buffer overruns
- **Memory Safety** - No dangling pointers, no memory leaks
- **Data-Race Freedom** - Guaranteed at compile time
- **Deadlock Freedom** - No locks, no deadlocks
- **High Performance** - Zero-cost abstractions
The code is generated from a high-level Elixir DSL and provides:
- Type-safe actor implementations
- Callback traits for custom behavior
- Built-in PonyTest tests
- Production-ready code
## Prerequisites
- **Ponyup** - Pony toolchain manager
- **Pony compiler** (installed via ponyup)
- **Corral** - Pony dependency manager (installed via ponyup)
### Installation
```bash
# Install ponyup
curl --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/ponylang/ponyup/latest-release/ponyup-init.sh | sh
# Install pony compiler and corral
ponyup update ponyc release
ponyup update corral release
```
## Building
```bash
# Fetch dependencies
corral fetch
# Build the project
ponyc .
# Run
./#{project_name}
```
## Testing
```bash
# Build and run tests
ponyc test
./test # or ./test1 depending on directory name
```
## Customizing Behavior
The generated actor code uses callback traits to allow customization WITHOUT
modifying generated files:
1. Find the `*_callbacks.pony` files
2. Edit the `*CallbacksImpl` class implementations
3. Add your custom logic in the callback methods
4. Rebuild the project
The generated actor code will automatically call your callbacks.
## Project Structure
- `main.pony` - Entry point and actor system setup
- `*_actor.pony` - Generated actor implementations (DO NOT EDIT)
- `*_callbacks.pony` - Callback traits and implementations (EDIT IMPL CLASS!)
- `test/test.pony` - PonyTest test suite
- `corral.json` - Dependency configuration
## CI/CD
This project includes a GitHub Actions workflow that:
- Builds on Ubuntu and macOS
- Runs all PonyTest tests
- Validates the build with each commit
## Learn More
- [Pony Tutorial](https://tutorial.ponylang.io/)
- [Pony Standard Library](https://stdlib.ponylang.io/)
- [Pony GitHub](https://github.com/ponylang/ponyc)
- [ActorSimulation DSL](https://github.com/yourusername/gen_server_virtual_time)
## License
Generated code is provided as-is for your use.
"""
end
# Utility functions
defp actor_snake_case(atom) when is_atom(atom) do
atom
|> Atom.to_string()
|> String.replace("-", "_")
end
defp actor_class_name(atom) when is_atom(atom) do
atom
|> Atom.to_string()
|> actor_class_name()
end
defp actor_class_name(string) when is_binary(string) do
string
|> String.split("_")
|> Enum.map_join("", &String.capitalize/1)
end
defp extract_messages_from_pattern(nil), do: []
defp extract_messages_from_pattern({:periodic, _interval, message}), do: [message]
defp extract_messages_from_pattern({:rate, _per_second, message}), do: [message]
defp extract_messages_from_pattern({:burst, _count, _interval, message}) do
[message]
end
defp extract_messages_from_pattern({:self_message, _delay, message}) do
[message]
end
defp message_name(msg) when is_atom(msg) do
Atom.to_string(msg)
end
defp message_name(msg) when is_binary(msg), do: msg
defp message_name(msg) do
inspect(msg) |> String.replace(~r/[^a-z0-9_]/, "_")
end
end