cassandra/.claude/skills/tla-plus/references/patterns/code-to-spec.md

203 lines
6.0 KiB
Markdown

# Code-to-Spec and Spec-to-Code Patterns
## When to Write a Spec From Code
1. **Concurrent code**: Locks, channels, async workflows, thread pools
2. **Distributed systems**: Consensus, replication, failover
3. **State machines**: Protocol handlers, workflow engines, parsers
4. **Complex invariants**: Financial calculations, resource allocation
5. **Bug reproduction**: Reproduce race conditions or deadlocks
## Code → Spec Translation Strategy
### Step 1: Identify the Abstraction Level
Don't model every line. Model the **concurrency-relevant** operations:
- Shared state mutations
- Message sends/receives
- Lock acquisitions/releases
- State transitions
- Decision points
### Step 2: Map Code Constructs to TLA+
| Code Construct | TLA+ Equivalent |
|----------------|-----------------|
| Thread / goroutine / async task | `process` |
| Mutex lock | `await lock = NULL; lock := self` |
| Channel send (buffered) | `await Len(chan) < cap; chan := Append(chan, msg)` |
| Channel send (unbuffered) | `procedure` with two-phase handshake |
| Condition variable / await | `await predicate` |
| Atomic CAS | Single label with conditional update |
| try-finally / defer | Explicit label for cleanup |
| Loop | `while` |
| Map/Dict | `function [key \in Keys |-> val]` |
| Enum / tagged union | Set of strings + discriminator field |
| Null / None / nil | `NULL` constant (model value) |
| Thread-local variable | Process-local `variables` |
| Global shared state | Module-level `variables` |
### Step 3: Write the Spec
```tla
\* Example: Go worker pool with bounded channel
---- MODULE WorkerPool ----
EXTENDS Integers, Sequences, TLC
CONSTANTS NumWorkers, NumJobs, ChanSize, NULL
Workers == 1..NumWorkers
Jobs == 1..NumJobs
(*--algorithm worker_pool
variables
jobChan = <<>>;
results = [j \in Jobs |-> NULL];
submitted = 0;
completed = 0;
define
TypeInvariant ==
/\ Len(jobChan) <= ChanSize
/\ submitted <= NumJobs
/\ completed <= submitted
AllJobsDone == completed = NumJobs
Correct == AllJobsDone => \A j \in Jobs: results[j] # NULL
end define;
process producer = 0
variables nextJob = 1;
begin
Submit:
while nextJob <= NumJobs do
await Len(jobChan) < ChanSize;
jobChan := Append(jobChan, nextJob);
submitted := submitted + 1;
nextJob := nextJob + 1;
end while;
end process;
fair process worker \in Workers
variables job = NULL;
begin
Work:
while completed < NumJobs do
Dequeue:
await jobChan # <<>>;
job := Head(jobChan);
jobChan := Tail(jobChan);
Process:
results[job] := job * 10; \* abstract computation
completed := completed + 1;
end while;
end process;
end algorithm; *)
====
```
## Spec → Code Verification
### Finding Differences Between Spec and Code
1. **List all state variables** in the spec. Map each to code variables/fields.
2. **List all actions** in the spec. Map each to code functions/methods.
3. **Check atomicity**: Does the code perform what one TLA+ label does in a single atomic step?
4. **Check ordering**: Does the code enforce the same ordering constraints?
5. **Check completeness**: Does the code handle all `either/or` branches?
### Common Divergence Points
| Spec Says | Code Does | Bug? |
|-----------|-----------|------|
| Atomic label | Multiple statements without lock | Race condition |
| `await condition` | Polling without check | Missed wakeup |
| `either A or B` | Only implements A | Missing error handling |
| `\E x \in S` | Picks first/hardcoded | Correctness depends on choice |
| Set of messages | FIFO queue | May hide ordering bugs |
| Bounded variable | Unbounded counter | Overflow |
| `UNCHANGED rest` | Modifies extra state | Side effect bug |
| Fair process | Thread can starve | Liveness violation |
### Systematic Code Review Against Spec
```markdown
## Verification Checklist
### State Mapping
- Variable X in spec → field Y in code
- Type constraints match (spec range ⊆ code type range)
### Action Mapping
- Action A in spec → method M in code
- Precondition (await/guard) correctly checked
- State update matches
- Atomicity level matches
- All UNCHANGED variables truly unchanged
### Invariant Mapping
- TypeInvariant → type system + assertions
- SafetyInvariant → defensive checks / tests
- Liveness → timeout / retry / monitoring
### Edge Cases
- Empty collections handled (spec: `\A x \in {}: TRUE`)
- Concurrent access protected
- Error/exception paths modeled
```
## Spotting Bugs in Code Using a Spec
### Step 1: Write a Minimal Spec
Focus on the suspected buggy area. You don't need to model the entire system.
### Step 2: Run with Small Constants
```
CONSTANT
NumNodes = 3
MaxMsgs = 5
NULL = NULL
```
### Step 3: Look for These Symptoms
| TLC Says | Likely Code Bug |
|----------|----------------|
| Deadlock | Missing wakeup, lock ordering issue |
| Invariant violated | Logic error, missing check |
| Liveness violated | Starvation, missing fairness, live-lock |
| State space explosion | Model too detailed — abstract more |
| Assert failed | Impossible state reached — logic error |
### Step 4: Translate Error Trace to Code Path
TLC gives you a step-by-step state sequence. Walk through your code following the same sequence of events to reproduce the bug.
## Modeling Existing Protocols
When verifying an implementation of a known protocol (Raft, Paxos, 2PC, etc.):
1. Start from the paper's pseudocode or description
2. Model one step at a time, checking after each addition
3. Use the **same variable names** as the paper when possible
4. Write invariants from the paper's correctness claims
5. Run with small constants first, increase gradually
### Bounding the Model
Real systems have unbounded state. TLA+ models must be finite.
```tla
CONSTANTS
MaxTerm = 3 \* bound election terms
MaxLogLen = 4 \* bound log length
MaxMsgs = 10 \* bound messages in flight
\* Use as state constraint in .cfg:
\* CONSTRAINT StateConstraint
StateConstraint ==
/\ \A n \in Nodes: term[n] <= MaxTerm
/\ \A n \in Nodes: Len(log[n]) <= MaxLogLen
/\ Cardinality(msgs) <= MaxMsgs
```