name: expert-specl description: > Expert for Specl, a modern specification language and model checker for concurrent and distributed systems. Covers the full Specl language, CLI toolchain, distributed protocols (Raft, Paxos, consensus, BFT), concurrent algorithms, and formal verification. Use when: writing/debugging .specl files, modeling distributed systems, verifying safety properties, analyzing invariant violations, optimizing state space exploration, or translating TLA+. Keywords: specl, model checking, formal verification, TLA+, distributed systems, consensus, Raft, Paxos, BFT, invariants, state space, specification language.
Specl — Specification Language & Model Checker
Specl is an exhaustive model checker for concurrent and distributed systems. Write a spec (state machine), and the checker explores every reachable state to verify invariants or produce minimal counterexample traces.
Modern TLA+ alternative with clean syntax, implicit frame semantics, and a fast Rust engine (1M+ states/second).
Quick Reference
Installation
# From Specl repo at /Users/danwt/Documents/repos/specl/specl
cargo install --path crates/specl-cli # `specl` binary
cargo install --path crates/specl-lsp # language server (optional)
VSCode extension: https://marketplace.visualstudio.com/items?itemName=specl.specl
Essential Commands
specl check spec.specl -c N=3 -c MAX=10 # check with constants
specl check spec.specl --no-deadlock # skip deadlock check (for protocols)
specl check spec.specl --fast # fingerprint-only (10x less memory, no traces)
specl check spec.specl --por # partial order reduction
specl check spec.specl --symmetry # symmetry reduction
specl fmt spec.specl --write # auto-format
specl fmt spec.specl --lint # fast syntax + type + compile check
specl info spec.specl -c N=3 # state space analysis + estimates
specl watch spec.specl -c N=3 # re-check on save
specl translate spec.tla -o spec.specl # TLA+ to Specl
specl check spec.tla -c N=3 # auto-translate TLA+ and check
Minimal Example
module Counter
const MAX: 0..10
var count: 0..10
init { count = 0 }
action Inc() { require count < MAX; count = count + 1 }
action Dec() { require count > 0; count = count - 1 }
invariant Bounded { count >= 0 and count <= MAX }
Run: specl check Counter.specl -c MAX=3 → explores all 4 states, verifies invariant.
Critical Language Rules
=assigns next-state (in actions).==compares (in guards/invariants).- Variables not mentioned in an action stay unchanged (implicit frame — no UNCHANGED needed).
- Use
andto update multiple variables atomically in one action. requireis a guard/precondition. If false, action is skipped (not an error).- Checker explores ALL actions × ALL parameter combinations × ALL reachable states via BFS.
- ALL
requirestatements MUST come BEFORE any assignments in an action.
Language Reference
Module Structure
module Name
const C: Int // set at check time: -c C=5
var x: 0..10 // state variable with type bound
init { x = 0 } // initial state (one block)
action Step() { ... } // state transition
func Helper(a) { ... } // pure function (no state modification)
invariant Safe { ... } // must hold in every reachable state
Types
| Type | Example | Notes |
|---|---|---|
Bool |
true, false |
|
Int |
42, -7 |
Unbounded |
Nat |
0, 100 |
Non-negative |
0..10 |
Inclusive range of ints | |
Set[T] |
{1, 2, 3} |
Finite set |
Seq[T] |
[a, b, c] |
Ordered list (0-indexed) |
Dict[K, V] |
{k: 0 for k in 0..N} |
Map from keys to values |
String |
"red" |
String literal |
Operators
| Category | Operators |
|---|---|
| Logical | and, or, not, implies, iff |
| Comparison | ==, !=, <, <=, >, >= |
| Arithmetic | +, -, *, /, % |
| Set | in, not in, union, intersect, diff, subset_of |
| Sequence | ++ (concat), head(s), tail(s), len(s), s[i] (index) |
| Conditional | if ... then ... else ... (expression — always requires else) |
Dicts (The Workhorse)
Model N processes with state via dicts. Create with comprehension, update with | (merge):
var role: Dict[Int, Int]
init { role = {p: 0 for p in 0..N} }
action Promote(i: 0..N) { role = role | {i: 2} }
Multi-key update:
balance = balance | { from: balance[from] - amt, to: balance[to] + amt }
Nested dict update (e.g., votesGranted[i][j]):
votesGranted = votesGranted | {i: votesGranted[i] | {j: true}}
Empty dict initialization (avoid type inference error):
// ❌ WRONG: {} is inferred as Set
var state: Dict[Int, Int]
init { state = {} } // Type error!
// ✅ CORRECT: Use empty range comprehension
init { state = {i: 0 for i in 1..0} }
Parameterized Actions
Checker tries ALL valid parameter combinations in every reachable state:
action Transfer(from: 0..N, to: 0..N, amount: 1..MAX) {
require from != to
require balance[from] >= amount
balance = balance | { from: balance[from] - amount, to: balance[to] + amount }
}
Quantifiers
all x in 0..N: P(x) // universal: true if P holds for ALL x
any x in 0..N: P(x) // existential: true if P holds for SOME x
Work in invariants, guards, and any expression.
Set Comprehensions
{p in 0..N if state[p] == 1} // filter
len({v in 0..N if votedFor[v] == i}) // count matching
len({v in 0..N if voted[v]}) * 2 > N + 1 // majority quorum check
Functions
Pure, no state modification, used for reusable logic:
func LastLogTerm(l) { if len(l) == 0 then 0 else l[len(l) - 1] }
func Quorum(n) { (n / 2) + 1 }
func Min(a, b) { if a <= b then a else b }
Common Modeling Patterns
Process Ensemble
var state: Dict[Int, Int]
init { state = {i: 0 for i in 0..N} }
action Act(p: 0..N) { state = state | {p: newVal} }
Message Passing
Encode messages as sequences (tuples), store in a set:
var msgs: Set[Seq[Int]]
init { msgs = {} }
// Message format: [type, src, dst, payload...]
action Send(src: 0..N, dst: 0..N, term: Int) {
require role[src] == CANDIDATE
msgs = msgs union {[1, src, dst, term]} // RequestVote
}
action Receive(src: 0..N, dst: 0..N, term: Int) {
require [1, src, dst, term] in msgs
// handle message...
}
Quorum Checking
// Simple majority (2f+1 out of N)
len({v in 0..N if votes[v]}) * 2 > N + 1
// Byzantine quorum (2f+1 out of 3f+1)
len({i in 0..N if signed[i]}) >= (N * 2) / 3 + 1
Encoding Enums
Use integer constants with comments:
// Roles: 0=Follower, 1=Candidate, 2=Leader
var role: Dict[Int, 0..2]
action BecomeCandidate(i: 0..N) {
require role[i] == 0 // Follower
role = role | {i: 1} // → Candidate
}
Multiple Variable Updates
Use and to update multiple variables atomically:
action Reset() {
x = 0
and y = 0
and z = 0
}
Modelling Approach
- Identify state. What variables fully describe the system? For N processes, use
Dict[Int, ...]. - Identify actions. What transitions can happen? Use parameters for nondeterminism.
- Write guards.
requirerestricts when actions fire. - Write invariants. Safety properties that must always hold.
- Start small. Use N=2 or N=3, small bounds. State spaces grow exponentially.
CLI Flags Reference
| Flag | Effect | When to Use |
|---|---|---|
-c KEY=VAL |
Set constant value | Always (e.g., -c N=3 -c MaxTerm=2) |
--no-deadlock |
Don't report deadlocks | Protocols with quiescent states (most distributed protocols) |
--fast |
Fingerprint-only (no traces, 8 bytes/state) | Large state spaces (>1M states), or when memory-constrained |
--por |
Partial order reduction | Independent processes/actions (e.g., message passing) |
--symmetry |
Symmetry reduction | Identical processes (homogeneous systems) |
--no-parallel |
Single-threaded | Debugging, deterministic output |
--threads N |
Set thread count | Fine-tune parallelism |
--max-states N |
Stop after N states | Testing, time limits |
--max-depth N |
Limit exploration depth | Bounded verification |
--memory-limit N |
Max memory in MB | Resource limits |
--write |
(format) Write in place | Auto-formatting |
Analyzing Results
OK Result
Result: OK
States explored: 1,580,000
Distinct states: 1,580,000
Max depth: 47
Time: 19.0s (83K states/s)
All reachable states satisfy all invariants. No deadlocks (unless --no-deadlock).
Invariant Violation
Result: INVARIANT VIOLATION
Invariant: Consistency
Trace (5 steps):
0: init
1: BecomeCandidate(i=0)
2: RequestVote(src=0, dst=1)
3: GrantVote(src=1, dst=0)
...
How to debug:
- Read trace step-by-step (each step = one action + resulting state)
- Identify which action caused violation
- Check if guards are too weak (action shouldn't have fired)
- Check if invariant is too strict (async timing issues)
Deadlock
Result: DEADLOCK
Trace (N steps):
...
A reachable state where no action is enabled. For protocols this is often expected (use --no-deadlock). For liveness/progress properties, reveals real bugs.
Common Pitfalls & Solutions
| Pitfall | Solution |
|---|---|
= vs == confusion |
= assigns in actions, == compares in invariants/guards |
Empty dict {} type error |
Use {i: 0 for i in 1..0} for empty dict |
require after assignment |
ALL requires must come before ANY assignments |
No has_key() function |
Pre-populate dict with all keys, use dict[k] == val |
| State space explosion | Start with N=2, reduce ranges, use --fast/--por/--symmetry |
| Deadlock on protocol spec | Add --no-deadlock (quiescent states are expected) |
| Range with arithmetic in param | Can't use 0..N+1, define const MAX instead |
any used as binder |
any x: P(x) is Bool, can't use x outside |
| Double assignment to variable | Model critical section atomically, don't acquire + release |
| Invariant fails due to async timing | Relax invariant or check fundamental property only |
Performance Optimization
State Space Estimation
Factors:
- Number of processes: Exponential impact (N=2 → N=3 can be 100× larger)
- Value ranges: Multiplicative (0..10 vs 0..5)
- Message sets: Combinatorial explosion
- Dict sizes: Cartesian product of all key-value combinations
State space orders of magnitude:
- 10-100 states: Tiny (Counter, simple locks)
- 100-10K states: Small (basic protocols, small N)
- 10K-100K states: Medium (realistic protocols with N=2)
- 100K-1M states: Large (use
--fast) - 1M+ states: Very large (use
--fast+--por+--symmetry)
Reduction Techniques
- Smaller constants: N=2 instead of N=3 (100× reduction)
- Tighter types:
0..3instead ofInt - Bounded collections:
require len(msgs) < 10 - Abstraction: Model at right level (don't model implementation details)
- Symmetry reduction:
--symmetryflag (all processes identical) - Partial order reduction:
--porflag (independent actions) - Fast mode:
--fast(8 bytes/state, no traces)
Optimization Workflow
# 1. Start small, verify correctness
specl check spec.specl -c N=2 -c MaxTerm=1
# 2. Scale up slightly
specl check spec.specl -c N=2 -c MaxTerm=2
# 3. Add partial order reduction if needed
specl check spec.specl -c N=2 -c MaxTerm=2 --por
# 4. Use fast mode for large spaces
specl check spec.specl -c N=3 -c MaxTerm=2 --por --fast
# 5. Add symmetry if applicable
specl check spec.specl -c N=3 -c MaxTerm=2 --por --symmetry --fast
TLA+ Comparison
| TLA+ | Specl |
|---|---|
x' = x + 1 |
x = x + 1 |
x = y (equality) |
x == y |
UNCHANGED <<y, z>> |
(implicit) |
/\, \/, ~ |
and, or, not |
\in, \notin |
in, not in |
\A x \in S: P(x) |
all x in S: P(x) |
\E x \in S: P(x) |
any x in S: P(x) |
[f EXCEPT ![k] = v] |
f | { k: v } |
Init == ... |
init { ... } |
Next == A \/ B |
(implicit — all actions OR'd) |
Key advantages over TLA+:
- Cleaner syntax (no backslash operators)
- Implicit frame semantics (no UNCHANGED)
- Faster model checker (Rust vs Java)
- Better type inference
- Integrated toolchain (formatter, LSP, watch mode)
Examples Repository
Location: /Users/danwt/Documents/repos/specl/specl/examples/
Showcase (examples/showcase/)
Curated protocol specs reviewed for faithfulness and invariant coverage:
raft.specl— Raft consensus (async, Vanlightly model)paxos.specl— Single-decree Paxos (Synod)comet.specl— CometBFT/Tendermint BFT with Byzantine faultssimplex.specl— Simplex consensus (TCC 2023)percolator.specl— Google Percolator (snapshot isolation)mesi.specl— MESI cache coherenceredlock.specl— Redis Redlock bug-finding (Kleppmann attack)swim.specl— SWIM failure detectionchandy-lamport.specl— Consistent snapshot algorithmTwoPhaseCommit.specl— Classic 2PCnarwhal_tusk.specl— Narwhal/Tusk DAG consensus (design reference)
Other (examples/other/)
Additional specs: beginner examples, semaphore puzzles (prefixed sem-), stress tests, simplified protocol models, and more.
Use Cases
✅ Good Fit
- Distributed consensus protocols (Raft, Paxos, Byzantine agreement)
- Distributed transactions (2PC, snapshot isolation, serializable isolation)
- Byzantine fault tolerance (PBFT, BFT consensus)
- Concurrent algorithms (locks, semaphores, barriers, queues)
- Safety property verification (mutual exclusion, consistency, linearizability)
- Cache coherence protocols (MESI, MOESI)
- Network protocols (alternating bit, sliding window)
- Finding minimal counterexamples (shortest path to violation)
- Small-to-medium state spaces (< 10M states, or larger with
--fast)
❌ Not a Good Fit
- Liveness properties (fairness, eventual consistency) — TLA+ with fairness better suited
- Infinite state spaces without abstraction — use theorem proving (Coq, Isabelle)
- Performance modeling — use simulation or queueing theory
- Smart contract logic — use Solidity analyzers (Slither, Mythril)
- Real-time systems — use timed automata (UPPAAL)
- Probabilistic systems — use PRISM or Storm
- Very large state spaces without reduction techniques (>100M states)
Key Insights
Why Specl Over TLA+?
- Cleaner syntax — no backslash operators, implicit frame
- Faster checker — Rust vs Java, 1M+ states/second
- Better tooling — formatter, LSP, watch mode, integrated translator
- Better type inference — less type annotation needed
- Lower barrier to entry — simpler syntax, clearer error messages
When to Use Model Checking
- Finding bugs in protocols — exhaustive search finds corner cases
- Verifying safety properties — proves invariants hold in all reachable states
- Understanding protocol behavior — traces show actual execution paths
- Bounded verification — proves correctness within bounds (term, log length, etc.)
Limitations
- State space explosion — exponential growth with N processes
- Bounded model checking — correctness proven only within bounds
- No liveness — can't prove "eventually" properties (use TLA+ with fairness)
- Abstraction required — must model at right level (not too detailed)
Project Structure
/Users/danwt/Documents/repos/specl/specl/
├── crates/
│ ├── specl-syntax/ # Lexer, parser, AST, pretty-printer
│ ├── specl-types/ # Type checker
│ ├── specl-ir/ # IR, bytecode compiler, guard indexing
│ ├── specl-eval/ # Tree-walk evaluator + bytecode VM
│ ├── specl-mc/ # BFS explorer, parallel via rayon, state store
│ ├── specl-tla/ # TLA+ parser and translator
│ ├── specl-cli/ # CLI (the `specl` binary)
│ └── specl-lsp/ # Language server
├── examples/
│ ├── easy/ # 12 beginner examples
│ ├── realistic/ # 16 mid-complexity examples
│ └── benchmark/ # 18 production protocols
└── editors/ # VSCode extension
/Users/danwt/Documents/repos/specl/examples/
├── semaphores/ # 13 semaphore puzzles
└── realistic/ # narwhal_tusk.specl
Resources
- Specl repo:
/Users/danwt/Documents/repos/specl - Main examples:
/Users/danwt/Documents/repos/specl/specl/examples/ - Semaphore puzzles:
examples/other/sem-*.specl - VSCode extension: https://marketplace.visualstudio.com/items?itemName=specl.specl
- Manual: https://danieltisdall.com/specl
- Announcement: https://danieltisdall.com/blog/2026-02-11-0028