Rustdis
In this walkthrough you write seven markdown specs and get back a wire-compatible Redis clone in a single std-only Rust binary: RESP2, strings, expiration, lists, hashes, sets, and a concurrency architecture chosen by measurement. The same formal IR that checks the specs also generates property tests and Kani proofs over the finished code.
Claude Code2026-07-09claude-opus-4-8
cost (reported) $45
tokens45.3M in / 617K out
wall time2h 51m
Approximate figures from the recorded build run on the date shown.
dist/rustdis · 6.8 MB Build it
Seven specs, one command. Overplane raises each spec into a formal IR
(ir.yaml plus consistency.smt2),
proves the specs self-consistent with Z3, and only then hands each spec to Claude Code inside the
project sandbox, with the whole
IR tree mounted read-only at /ir. The sandbox also carries a
real
redis-server and redis-tools, so the agent tests
wire compatibility and benchmarks against the real thing while it builds.
mkdir rustdis && cd rustdis
overplane init
overplane sandbox build
overplane spec new # 001-resp-server.md
overplane spec new # 002-strings-keyspace.md
overplane spec new # 003-expiration.md
overplane spec new # 004-collections.md
overplane spec new # 005-concurrency-performance.md
overplane spec new # 006-ir-property-tests.md
overplane spec new # 007-model-checking.md
overplane build all
The output is a complete Cargo project with an empty
[dependencies] section; cargo build --release
produces the single dist/rustdis binary shown in the screencast
above.
The specs
This is everything the agent was given. Specs one through five shape the server: protocol, keyspace, expiration, collections, and a performance spec that demands the concurrency architecture be chosen by measurement. Specs six and seven describe no new features. They turn the already-raised IR of the first five specs into executable verification.
001-resp-server.md
---
id: '#0001'
title: RESP Server Core
tags: [core]
---
# RESP Server Core
## Goal
Create a Rust Cargo project in the working directory that builds a single
binary called `rustdis`: a wire-compatible Redis clone. This spec covers the
RESP2 protocol layer and the TCP server shell. The runtime must use only the
Rust standard library (no external crates in `[dependencies]`), so the result
is one small binary with zero runtime dependencies. (Dev-dependencies for
testing are allowed and arrive in a later spec.)
## Requirements
* CLI: `rustdis [--port N] [--bind ADDR]`, defaulting to `127.0.0.1:6379`.
`rustdis --help` prints polished, colorized usage. `rustdis --version`
prints the version.
* RESP2 protocol, both directions, as its own module with a clean boundary:
- Parse client requests as RESP arrays of bulk strings, plus the inline
command format (a bare line like `PING\r\n`) for telnet compatibility.
- Serialize simple strings, errors, integers, bulk strings (including null),
and arrays (including nested and null).
- The parser must be a pure, panic-free function from bytes to
`Result<Command, ProtocolError>`: malformed input never panics and never
kills the connection loop. Enforce sane limits (max bulk length 512 MB,
max array elements 1M) with clean RESP errors.
* Commands in this spec: `PING [msg]`, `ECHO msg`, `QUIT`, and a stub
`COMMAND` (and `COMMAND DOCS`) that returns an empty array so `redis-cli`
connects without complaint. Unknown commands get
`-ERR unknown command 'name'`. Command names are case-insensitive.
* Startup banner on stderr: name, version, bind address, with tasteful ANSI
color. Log one concise line per connection accept/close. Keep stdout clean.
* Structure for growth: a `Command` enum, a dispatch table, and a storage
module boundary that later specs extend.
## Acceptance
- `cargo build --release` succeeds with an empty `[dependencies]` section.
- With the server running, `redis-cli -p 6379 PING` returns `PONG`,
`redis-cli ECHO hello` returns `hello`.
- `printf 'PING\r\n' | timeout 2 nc 127.0.0.1 6379` (inline command) returns
`+PONG`.
- Malformed bytes (e.g. `printf '*1\r\n$4\r\nPING'` truncated, or random
garbage) never crash the server; the connection is answered with a RESP
error or closed cleanly. 002-strings-keyspace.md
---
id: '#0002'
title: Strings & Keyspace
tags: [core]
---
# Strings & Keyspace
## Goal
Give `rustdis` its keyspace: binary-safe string values with Redis-compatible
command semantics, exact reply types, and exact error messages where Redis
documents them.
## Requirements
* String commands: `GET`, `SET` (with `EX`, `PX`, `NX`, `XX`, `GET` options),
`SETNX`, `GETSET`, `MGET`, `MSET`, `APPEND`, `STRLEN`, `GETRANGE`,
`INCR`, `DECR`, `INCRBY`, `DECRBY`.
* Keyspace commands: `DEL`, `EXISTS` (variadic, counts duplicates), `TYPE`,
`KEYS pattern` (glob: `*`, `?`, `[...]`, `\` escape), `DBSIZE`, `FLUSHDB`,
`FLUSHALL`, `RANDOMKEY`, `RENAME`, `SELECT` (accept index 0 only; other
indexes get `-ERR DB index is out of range`).
* Semantics to match Redis exactly:
- Keys and values are arbitrary byte strings (binary-safe, no UTF-8
assumption anywhere in the data path).
- `INCR` family: values are parsed as signed 64-bit integers;
non-numeric values answer `-ERR value is not an integer or out of range`;
overflow answers `-ERR increment or decrement would overflow`.
- `SET key val NX` on an existing key returns null bulk; `XX` on a missing
key returns null bulk; otherwise `+OK`.
- Wrong-type operations (arriving with later specs' types) answer
`-WRONGTYPE Operation against a key holding the wrong kind of value`.
- Wrong arity answers `-ERR wrong number of arguments for 'cmd' command`.
## Acceptance
- A `redis-cli` session exercising each command above against `rustdis` and
against a real `redis-server` (both are installed in the sandbox) produces
byte-identical replies for a scripted command list; commit that script as
`compat/compat.sh` and make `compat/compat.sh` runnable against any port.
- `redis-cli SET k v NX` twice: first `OK`, second `(nil)`.
- `redis-cli INCR k` on a non-integer value returns the documented error. 003-expiration.md
---
id: '#0003'
title: Expiration
tags: [core]
---
# Expiration
## Goal
Add Redis-compatible key expiration to `rustdis`: per-key absolute deadlines,
TTL introspection, lazy expiry on access, and an active background sweep.
## Requirements
* Commands: `EXPIRE`, `PEXPIRE`, `EXPIREAT`, `PEXPIREAT`, `TTL`, `PTTL`,
`PERSIST`. `SET` with `EX`/`PX` uses the same machinery; plain `SET` on an
existing key clears any TTL (Redis semantics). `GETEX` with
`EX`/`PX`/`PERSIST` options.
* Semantics to match Redis exactly:
- `TTL`/`PTTL` return `-2` for a missing key, `-1` for a key with no TTL.
- `EXPIRE` with a non-positive timeout deletes the key immediately and
returns `1`; `EXPIRE` on a missing key returns `0`.
- Expiry deadlines are absolute monotonic-clock instants computed once at
set time; a key past its deadline is indistinguishable from a missing key
to every command (lazy expiry), regardless of whether the sweeper has
collected it yet.
* Active expiry: a background sweep that samples keys with TTLs and removes
expired ones, bounded so it never stalls foreground traffic. Document the
chosen sampling strategy in a code comment referencing how Redis does it.
* Invariants (these will be verified against the IR in a later spec):
- A key never resurrects: once expired, no read may observe the old value.
- `PERSIST` then any delay never expires the key.
- TTL is monotonically non-increasing between writes to the key's TTL.
## Acceptance
- `redis-cli SET k v PX 100`, then `GET k` after 200 ms returns `(nil)` and
`TTL k` returns `-2`.
- `redis-cli SET k v EX 100; PERSIST k; TTL k` returns `-1`.
- `compat/compat.sh` is extended with an expiration section and still
produces byte-identical output against real `redis-server` (allowing a
tolerance mechanism for timing-dependent TTL remainders). 004-collections.md
---
id: '#0004'
title: Collections
tags: [core]
---
# Collections
## Goal
Extend the `rustdis` value model beyond strings: lists, hashes, and sets,
with Redis-compatible reply shapes and edge-case behavior.
## Requirements
* Lists: `LPUSH`, `RPUSH`, `LPOP`, `RPOP` (with optional count), `LLEN`,
`LRANGE` (negative indexes, out-of-range clamping), `LINDEX`, `LSET`,
`LTRIM`.
* Hashes: `HSET` (variadic field/value pairs), `HGET`, `HMGET`, `HDEL`,
`HGETALL`, `HEXISTS`, `HLEN`, `HKEYS`, `HVALS`, `HINCRBY`.
* Sets: `SADD`, `SREM`, `SISMEMBER`, `SMEMBERS`, `SCARD`, `SPOP` (optional
count), `SINTER`, `SUNION`, `SDIFF`.
* Semantics to match Redis exactly:
- Deleting the last element of a collection deletes the key: an empty
collection is never observable (`EXISTS` returns `0`, `TYPE` says none).
- All collection commands on a key of the wrong type answer the standard
`-WRONGTYPE` error; `TYPE` reports `list`, `hash`, `set` correctly.
- `LPUSH`/`RPUSH` return the new length; `SADD` returns the number of
members actually added; `HSET` returns the number of new fields.
- Expiration from the previous spec applies uniformly to every type.
* Keep the storage module's type dispatch table-driven so the IR's
entity/variant structure maps visibly onto the code (one value enum,
one variant per type).
## Acceptance
- `compat/compat.sh` gains list/hash/set sections and still produces
byte-identical output against real `redis-server` (sort set replies where
Redis ordering is unspecified).
- `redis-cli RPUSH l a b c; LRANGE l 0 -1` returns a, b, c;
`LPOP l 3` empties the list and `EXISTS l` then returns `0`.
- `redis-cli HSET h f1 v1 f2 v2; HGETALL h` returns all pairs;
`SADD s a b; SADD s b` returns `1`. 005-concurrency-performance.md
---
id: '#0005'
title: Concurrency & Performance
tags: [performance]
---
# Concurrency & Performance
## Goal
Make `rustdis` fast under real load. Choose the concurrency architecture by
measurement, not fashion, and make the server a first-class `redis-benchmark`
target.
## Requirements
* Concurrency architecture, chosen empirically (still std-only):
- Candidates: (a) thread-per-connection over a sharded store,
(b) a fixed worker pool over a sharded store, (c) a single-threaded
event loop. Prototype at least two, measure with
`redis-benchmark -t set,get -n 100000 -c 50 -P 16` in the sandbox, keep
the winner, and record the numbers and the decision in `ARCHITECTURE.md`.
- The store must be sharded (hash-partitioned across N locks or N owned
shards) so writes scale on multi-core; a single global mutex is
unacceptable.
- The expiration sweeper from spec #0003 must respect the chosen
architecture (no global stop-the-world).
* Pipelining: many commands per read buffer must be parsed and answered
in order without per-command syscalls; responses are batched per read.
This is what `redis-benchmark -P` exercises.
* `redis-benchmark` compatibility: implement enough of `CONFIG GET`
(`save`, `appendonly`; return standard stub values), `INFO` (at least
`server`, `clients`, `memory`, `stats` sections with honest values),
`DEBUG JMAP`/`DEBUG SLEEP` may be rejected, but `redis-benchmark`'s
default and `-t set,get,incr,lpush,rpush,sadd,hset` suites must run to
completion.
* Performance bar, measured in the sandbox against the real `redis-server`
installed there (same machine, default configs, `-n 100000 -c 50`):
`rustdis` must reach at least 70% of Redis's requests/sec on `SET` and
`GET`, both unpipelined and with `-P 16`. Record the comparison table in
`ARCHITECTURE.md`.
## Acceptance
- `redis-benchmark -t set,get,incr,lpush,rpush,sadd,hset -n 100000 -c 50 -q`
completes against `rustdis` with zero errors.
- `ARCHITECTURE.md` contains the measured candidate comparison and the
final rustdis-vs-redis table meeting the 70% bar.
- `redis-cli INFO server` shows `rustdis` identity fields alongside
Redis-compatible field names (`redis_version` present for tool compat). 006-ir-property-tests.md
---
id: '#0006'
title: IR-Derived Property Tests
tags: [verification]
---
# IR-Derived Property Tests
## Goal
Turn the formal IR that Overplane raised from specs #0001–#0005 into an
executable property-based test suite. The IR directories are mounted
read-only at `/ir/<spec-file>/` (`ir.yaml`, `consistency.smt2`); treat them
as the authoritative source of properties: do not invent properties that
have no IR node, and do not skip IR nodes that are runtime-checkable.
## Requirements
* Add `proptest` (latest 1.x) as a dev-dependency only; `[dependencies]`
stays empty.
* Traceability is the point. Create `tests/ir_properties.rs` where every
property test is named `prop_<spec>_<ir_node_id>` (e.g.
`prop_0003_no_resurrection`) and carries a doc comment quoting the IR
node's `description` and assertion. Cover every `invariant`, `constraint`,
and `rule` node from the IR of specs #0001–#0005 that is checkable at the
library level; list any node that is not runtime-checkable (e.g. purely
visual or timing-hardware ones) in a `SKIPPED` table in
`VERIFICATION.md` with a one-line reason each.
* Required property classes (all derived from IR nodes):
- RESP round-trip: for arbitrary commands built from arbitrary binary
keys/values, serialize → parse is the identity, and the parser never
panics on arbitrary byte input (fuzz-style property over raw bytes).
- Model-based command sequences: generate random sequences of commands
(across strings, expiry, lists, hashes, sets) and run them against both
the real store and a naive reference model (`BTreeMap`-based, written
for clarity not speed); the observable replies must be identical. Use a
virtual clock so expiry properties are deterministic.
- Expiry invariants: no resurrection after expiry, `PERSIST` durability,
TTL monotonicity, as stated in the #0003 IR.
- Collection invariants: empty collections are never observable; type
errors are total (every command × wrong-type combination answers
`-WRONGTYPE`).
* The store and protocol layers must be testable in-process (no TCP) via a
library-level API; refactor if needed, without changing the binary's
behavior.
* `VERIFICATION.md` documents the full verification story: the IR → SMT
consistency check that already ran at build time, the IR-node → property
mapping table (node id, property name, one-line description), the SKIPPED
table, and how to run everything.
## Acceptance
- `cargo test` passes; the proptest suite runs at least 256 cases per
property by default.
- Every runtime-checkable IR node id from specs #0001–#0005 appears either
in `tests/ir_properties.rs` or in the `VERIFICATION.md` SKIPPED table;
no third bucket.
- Reverting the spec #0003 "plain SET clears TTL" behavior by hand makes
`prop_` tests fail (spot-check that the suite has teeth; describe the
mutation you tried in `VERIFICATION.md`). 007-model-checking.md
---
id: '#0007'
title: Model Checking the Core
tags: [verification]
---
# Model Checking the Core
## Goal
Go beyond sampling: use the Kani model checker to *prove* (for all inputs up
to a bound, not just tested ones) that the pure core of `rustdis` cannot
panic and that key IR invariants hold. Kani was chosen over KLEE (research-
grade Rust support, pinned old toolchains) and Lean (proves a hand-translated
model, not this code); record that rationale in `VERIFICATION.md`.
## Requirements
* Add Kani proof harnesses under `src/` behind `#[cfg(kani)]` (they must be
invisible to normal builds and `cargo test`). Name each harness
`verify_<spec>_<ir_node_id>` mirroring the proptest naming so the three
verification layers (SMT / proptest / Kani) line up in the mapping table.
* Harnesses to include, derived from the IR:
- RESP parser total safety: for any byte buffer up to a fixed bound
(e.g. 64 bytes), parsing never panics and either yields a command or a
`ProtocolError` (#0001's parser purity/panic-freedom node).
- RESP length-prefix arithmetic: no overflow for any advertised bulk/array
length (bounded).
- TTL arithmetic: deadline computation never overflows and preserves
ordering for any `EXPIRE`/`PEXPIRE` argument (#0003 TTL monotonicity
node, bounded).
- Integer command arithmetic: `INCRBY`/`DECRBY` overflow detection is
exact: errors if and only if the mathematical result exits i64
(#0002's overflow node).
* Bounds and any `kani::assume` preconditions must be stated in doc comments
with a one-line justification.
* Tooling: `verify.sh` at the project root installs Kani if absent
(`cargo install --locked kani-verifier && cargo kani setup`) and runs
`cargo kani`. Add the harness list and expected `VERIFICATION SUCCESSFUL`
output to `VERIFICATION.md`. If Kani cannot complete inside the sandbox,
the harnesses must still be committed and `verify.sh` must work on a
standard Linux host; note the attempt's outcome in `VERIFICATION.md`.
## Acceptance
- `cargo build --release` and `cargo test` are unaffected by the harness
files ([dependencies] still empty; `kani` appears nowhere in Cargo.toml;
attributes are cfg-gated only).
- `verify.sh` runs `cargo kani` and every harness reports
`VERIFICATION:- SUCCESSFUL` on a host with Kani available.
- `VERIFICATION.md`'s mapping table now has three columns per IR node:
SMT (build-time), proptest, Kani, with `—` where a layer does not apply. Three verification layers from one IR
Every Overplane build already includes layer one: each spec is raised to a typed IR and an SMT-LIB2 encoding, and Z3 proves the constraint set satisfiable (per spec and merged) before a line of code is generated. This example adds two more layers derived from that same IR:
- proptest (spec six): every runtime-checkable
invariant/constraint/rulenode in the IR becomes a named property test (prop_<spec>_<ir_node_id>): RESP round-trips over arbitrary binary data, no-panic parsing of arbitrary bytes, expiry monotonicity and no-resurrection under a virtual clock, and model-based random command sequences checked against a naive reference oracle. - Kani (spec seven): bounded model checking
proves panic-freedom and overflow-exactness of the pure core (RESP
parser and length arithmetic, TTL deadline arithmetic,
INCRBYoverflow detection) for all inputs up to a bound, not just sampled ones. Harnesses mirror the proptest names, soVERIFICATION.mdcarries one table mapping IR node → SMT → proptest → Kani.
Why these two? Property testing (proptest) is the best-practice runtime layer: it executes the real store. Kani is the mainstream Rust model checker, with high automation and no toolchain forks. KLEE was rejected because its Rust path is research-grade (Rust-2018-era tooling, pinned old LLVM, no concurrency); Lean was rejected because it would verify a hand-translated model rather than this code. The spec-level proof work stays with Z3, where the IR already lives. The Codegen with IR page walks through the prompt fragments and generated tests in detail.
Live results from this machine: all 48 properties pass (cargo test, 256+ cases each), and three of the four Kani harnesses report
VERIFICATION:- SUCCESSFUL in about seventeen seconds. The fourth,
total panic-freedom of the RESP parser over every
possible 64-byte input, is an honest illustration of the "bounded" in bounded
model checking: CBMC was still expanding the SAT problem after four hours and
17 GB of RAM, while the same IR node is covered in milliseconds by the fuzz-style
proptest over arbitrary bytes. Three layers, three different trade-offs.
Live benchmarks vs. real Redis
Measured on the build machine (16 cores, same host, default configs) against
Redis 7.0.15, with
redis-benchmark -n 100000 -c 50 -q, unpipelined and with
-P 16. The spec demanded at least 70% of Redis throughput; the
sharded thread-per-connection design lands at 89–96% unpipelined and
ahead of Redis on several pipelined workloads:
| Command | rustdis (req/s) | Redis 7.0.15 (req/s) | ratio | rustdis, P=16 | Redis, P=16 | ratio |
|---|---|---|---|---|---|---|
| SET | 198,020 | 207,900 | 95% | 1,694,915 | 2,000,000 | 85% |
| GET | 199,601 | 208,333 | 96% | 2,564,103 | 2,325,581 | 110% |
| INCR | 200,803 | 213,220 | 94% | 1,612,903 | 2,222,222 | 73% |
| LPUSH | 190,476 | 214,133 | 89% | 2,439,024 | 1,923,077 | 127% |
| RPUSH | 198,807 | 212,314 | 94% | 2,631,579 | 2,083,333 | 126% |
| SADD | 198,413 | 211,417 | 94% | 2,439,024 | 2,173,913 | 112% |
| HSET | 201,207 | 213,220 | 94% | 1,785,714 | 1,923,077 | 93% |
Wire compatibility was checked the same way the agent checked it during the
build: compat/compat.sh replays a scripted conversation against both
servers and diffs the raw replies: 332 commands, byte-identical against real
redis-server.
The result
One small binary that redis-cli and
redis-benchmark can't tell apart from the real thing on the supported
command set. The screencast above shows a live session: the server banner, a redis-cli conversation across strings, lists, and TTLs, and a pipelined redis-benchmark run.