skills/verification/program-to-tlaplus-spec-generator/SKILL.md
Extracts a TLA+ specification from concurrent or distributed code, modeling the state machine, actions, and fairness conditions for model checking with TLC. Use when verifying concurrency properties of production code, when designing a protocol and wanting to check it before implementation, or when the user has a race condition and needs to prove the fix.
npx skillsauth add santosomar/general-secure-coding-agent-skills program-to-tlaplus-spec-generatorInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
TLA+ models state machines, not code. The translation isn't line-by-line — it's "what are the states, and what are the atomic transitions between them?" This is an abstraction exercise.
What persists between steps? That's your VARIABLES.
| Code construct | TLA+ variable |
| ----------------------------- | ------------------------------------------------------------ |
| Shared mutable state (locks, queues, counters) | Direct — queue, lock_holder |
| Per-thread/process local state | A function: pc \in [Proc -> {"idle", "waiting", "cs"}] |
| Network messages in flight | A set or bag: msgs \subseteq Message |
| The program counter | A pc variable per process — where in the code each one is |
Do not model: pure function arguments, loop counters that don't cross await points, anything that's deterministic from the rest.
An action is one indivisible step. In code, atomicity boundaries are:
await points / blocking calls: one actionEach action becomes a disjunct in Next:
Next == \/ Action1
\/ Action2
\/ \E p \in Proc : ProcessStep(p)
ActionName ==
/\ <guard — when is this enabled?>
/\ <effect — primed variables>
/\ UNCHANGED <<everything else>>
The UNCHANGED is not optional. TLA+ makes you say what doesn't change.
Code (Go-ish, two goroutines, shared counter, broken):
var counter int
var mu sync.Mutex
func increment() { // called by 2 goroutines
tmp := counter // ← read outside lock!
mu.Lock()
counter = tmp + 1
mu.Unlock()
}
State: counter, mu (who holds it or NULL), pc[p] per process, tmp[p] per process (the local).
Atomicity: The read tmp := counter is its own action (no lock). The lock-write-unlock is one action (atomic under the lock).
TLA+:
---- MODULE Counter ----
EXTENDS Integers
CONSTANTS Proc
VARIABLES counter, mu, pc, tmp
Init ==
/\ counter = 0
/\ mu = "free"
/\ pc = [p \in Proc |-> "start"]
/\ tmp = [p \in Proc |-> 0]
Read(p) ==
/\ pc[p] = "start"
/\ tmp' = [tmp EXCEPT ![p] = counter]
/\ pc' = [pc EXCEPT ![p] = "locked_write"]
/\ UNCHANGED <<counter, mu>>
LockedWrite(p) ==
/\ pc[p] = "locked_write"
/\ mu = "free" \* guard: can acquire
/\ mu' = "free" \* lock+write+unlock atomic → mu unchanged net
/\ counter' = tmp[p] + 1
/\ pc' = [pc EXCEPT ![p] = "done"]
/\ UNCHANGED tmp
Next == \E p \in Proc : Read(p) \/ LockedWrite(p)
Inv == (\A p \in Proc : pc[p] = "done") => counter = Cardinality(Proc)
====
TLC finds: Inv violated. Trace: P1 reads (tmp=0), P2 reads (tmp=0), P1 writes (counter=1), P2 writes (counter=1). Two increments, counter=1. The read-outside-lock is the bug; TLA+ found it because Read and LockedWrite are separate actions.
| Keep | Abstract away | | ------------------------------------- | --------------------------------------------------------- | | All shared state | Local variables that don't survive an atomic boundary | | Concurrency structure (who runs when) | Sequential business logic inside an atomic block | | Message contents that affect protocol | Message payloads that are just data | | Failure modes (crash, partition) | Performance, timing (unless you're proving real-time) |
TLC explores every interleaving. Every variable you keep multiplies the state space. → tlaplus-model-reduction if TLC runs out of memory.
UNCHANGED. Omitting it means "anything can happen to this variable" — you'll get spurious counterexamples.counter \in Nat → TLC never finishes. Use counter \in 0..N in the .cfg.Inv that passes without also checking you modeled the actions right. Sanity-check: does TLC find a state where pc[p] = "done" for all p? If not, your model never terminates and the invariant is vacuously true.## State
VARIABLES <list> — <each maps to what code construct>
## Actions
<Action> — atomic because <lock region | CAS | between awaits>
...
## Spec
<TLA+ module>
## .cfg
<constants, invariants, state constraints for TLC>
## Abstractions made
<what you dropped and why it's sound to drop>
development
Extracts human-readable pseudocode from a verified formal artifact (Dafny, Lean, TLA+) while preserving the verified properties as annotations, so the proof-carrying logic can be reimplemented in a production language. Use when porting verified code to an unverified target, when documenting what a formal spec actually does, or when handing a verified algorithm to an implementer.
development
Translates natural-language or pseudocode descriptions of concurrent and distributed systems into TLA+ specifications ready for the TLC model checker. Identifies state variables, actions, type invariants, safety properties, and liveness properties from the description. Use when formalizing a protocol, when the user describes a distributed algorithm to verify, when designing a consensus or locking scheme, or when starting formal verification of a concurrent system.
testing
Reduces a TLA+ model so TLC can actually check it — shrinks constants, adds state constraints, abstracts data, or applies symmetry — when the state space is too large to enumerate. Use when TLC runs out of memory, when checking takes hours, or when a spec works at N=2 and you need confidence at larger scale.
development
TLA+-specific instance of model-guided repair — reads a TLC error trace, identifies the enabling condition that should have been false, strengthens the corresponding action, and maps the fix to source code. Use when TLC reports an invariant violation or deadlock and you have the code-to-TLA+ mapping from extraction.