# tla_connect
TLA+/Apalache integration for model-based testing in Elixir.
## Overview
`tla_connect` provides tools for integrating [TLA+](https://lamport.azurewebsites.net/tla/tla.html) and [Apalache](https://apalache.informal.systems/) model checking into Elixir test suites. It enables:
- **Trace validation**: Verify that your implementation matches TLA+ specifications
- **Model-based testing**: Generate test cases from TLA+ models
- **Counterexample replay**: Automatically reproduce bugs found by model checkers
## Architecture
```mermaid
flowchart TB
subgraph TLA["TLA+ Specification"]
Spec[MySpec.tla]
TraceSpec[TraceSpec.tla]
end
subgraph Apalache["Apalache Model Checker"]
CLI[apalache-mc CLI]
RPC[JSON-RPC Server]
end
subgraph Elixir["Elixir Implementation"]
Driver[Driver behaviour]
Emitter[StateEmitter]
end
subgraph Traces["Trace Formats"]
ITF[ITF Traces]
NDJSON[NDJSON Trace]
end
%% Approach 1: Batch Replay
Spec -->|"generate_traces()"| CLI
CLI -->|produces| ITF
ITF -->|"replay_traces()"| Driver
%% Approach 2: Interactive RPC
Spec -->|"interactive_test()"| RPC
RPC <-->|step-by-step| Driver
%% Approach 3: Post-hoc Validation
Driver -->|records| Emitter
Emitter -->|writes| NDJSON
NDJSON -->|"validate_trace()"| CLI
CLI -->|checks against| TraceSpec
classDef tla fill:#e1f5fe,stroke:#01579b
classDef apalache fill:#fff3e0,stroke:#e65100
classDef elixir fill:#f3e5f5,stroke:#6a1b9a
classDef trace fill:#fce4ec,stroke:#880e4f
class Spec,TraceSpec tla
class CLI,RPC apalache
class Driver,Emitter elixir
class ITF,NDJSON trace
```
| Approach | Direction | Catches |
|----------|-----------|---------|
| 1. Batch Replay | Spec -> Implementation | Implementation doesn't handle a case the spec allows |
| 2. Interactive RPC | Spec <-> Implementation | Implementation doesn't handle a case the spec allows |
| 3. Post-hoc Validation | Implementation -> Spec | Implementation does something the spec doesn't allow |
## Features
- ITF (Informal Trace Format) parsing and validation
- Apalache CLI integration for trace generation
- Apalache JSON-RPC client for interactive symbolic testing
- Trace generation from TLA+ specifications
- State comparison and diff output for debugging mismatches
- Support for both file-based and RPC-based workflows
- Parallel trace replay via `Task.async_stream`
## Installation
Add `tla_connect` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:tla_connect, "~> 0.1.0"}
]
end
```
## Quick Start
### Approach 1: Batch Trace Replay
Generate traces with Apalache, then replay against your implementation:
```elixir
defmodule CounterDriver do
@behaviour TlaConnect.Driver
@impl true
def init, do: %{counter: 0}
@impl true
def step(_state, %{action: "init"}), do: {:ok, %{counter: 0}}
def step(state, %{action: "increment"}), do: {:ok, %{counter: state.counter + 1}}
def step(state, %{action: "decrement"}), do: {:ok, %{counter: state.counter - 1}}
def step(_state, %{action: action}), do: {:error, "unknown action: #{action}"}
@impl true
def extract_state(state), do: %{"counter" => state.counter}
end
# Generate traces from TLA+ spec
{:ok, %{traces: traces}} = TlaConnect.generate_traces(%{
spec: "specs/Counter.tla",
inv: "TraceComplete"
})
# Replay all traces against the driver
TlaConnect.replay_traces(CounterDriver, traces)
```
### Approach 2: Interactive Symbolic Testing
Step-by-step symbolic execution via Apalache's explorer server:
```elixir
client = TlaConnect.Rpc.Client.new("http://localhost:8822")
config = %{
spec: "specs/Counter.tla",
init: "Init",
next: "Next",
num_runs: 100,
max_steps: 50,
seed: 42 # Reproducible runs
}
{:ok, stats, _client} = TlaConnect.interactive_test(CounterDriver, client, config)
IO.puts("Completed #{stats.runs_completed} runs, #{stats.total_steps} steps")
```
### Approach 3: Post-hoc Trace Validation
Record your implementation's execution, then validate against the spec:
```elixir
alias TlaConnect.Emitter
# Record execution trace
emitter = Emitter.new("trace.ndjson")
emitter = Emitter.emit(emitter, "init", %{"counter" => 0})
emitter = Emitter.emit(emitter, "increment", %{"counter" => 1})
emitter = Emitter.emit(emitter, "increment", %{"counter" => 2})
{_count, _emitter} = Emitter.finish(emitter)
# Validate against TLA+ spec
config = %{
trace_spec: "specs/CounterTrace.tla",
init: "Init",
next: "Next",
inv: "Inv"
}
{:ok, result} = TlaConnect.validate_trace(config, "trace.ndjson")
case result do
%{valid: true} -> IO.puts("Trace is valid!")
%{valid: false, reason: reason} -> IO.puts("Invalid: #{reason}")
end
```
## The Driver Behaviour
The `TlaConnect.Driver` behaviour connects your Elixir code to TLA+ specs. It defines three callbacks:
```elixir
@callback init() :: term()
@callback step(driver_state :: term(), step :: step()) ::
{:ok, new_state :: term()} | {:error, term()}
@callback extract_state(driver_state :: term()) :: map()
```
**Key design decisions:**
- **Functional state threading** -- driver state is passed in and returned, not mutated
- **Pattern matching for dispatch** -- use Elixir's native pattern matching instead of a `switch` macro
- **Projection-based comparison** -- only keys returned by `extract_state/1` are compared against the spec, so you can track a subset of spec variables
- **String keys** -- `extract_state/1` should return string keys matching TLA+ variable names
## ITF Value Decoding
Apalache ITF traces use special encodings that are automatically decoded:
| ITF Encoding | Elixir Type |
|---|---|
| `{"#bigint": "42"}` | integer |
| `{"#set": [...]}` | `MapSet` |
| `{"#tup": [...]}` | list |
| `{"#map": [[k, v], ...]}` | `Map` |
## Parallel Replay
For large trace sets, use parallel replay:
```elixir
TlaConnect.replay_traces_parallel(CounterDriver, traces,
max_concurrency: System.schedulers_online()
)
```
## Requirements
- Elixir 1.15 or later
- Apalache (if using trace generation or validation features)
## License
MIT License. See [LICENSE](LICENSE) for details.