State in Rust: Enums and Typestate¶
State changes which operations are valid as an object's lifecycle progresses. Rust enums can check that lifecycle at runtime, while state-specific types can make some operations unavailable at compile time. Neither approach proves that an external connection is working or that an operation cannot fail.
Rust State requirements and transition contract¶
Review enums and patterns, visibility, and moves. The independent fixtures use stable Rust edition 2024 and C++20 without external libraries.
Build an in-memory recording session, not a socket or hardware driver. Idle can open to Ready; Ready can accept integer readings and close to Closed. Opening a Ready or Closed runtime session is rejected. Sending outside Ready and closing outside Ready are rejected without changing recorded data. The typed API exposes only the operations appropriate to each type.
The state sequence is Idle → Ready → Closed. Closed retains the recorded values for inspection. Record zero and -500 in that order; neither value is an error or an empty state. Both examples produce:
Runtime misuse produces a normal failure result. Typed misuse is a separate compile-time failure and is not part of the successful program's output.
C++: runtime State and state-specific classes¶
Save this complete program as main.cpp. The first design uses virtual State objects; the context owns the recorded data. A transition creates its successor before replacing the old state, so no state method destroys itself while executing. The second design exposes different methods on different classes.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 | |
Run with assertions enabled, because some assertions execute the required transitions:
The virtual State interface separates behaviour by state, but illegal operations remain callable on Runtime and must be rejected. Null here means a disallowed transition, not a missing recorded value. This context deliberately deletes copying and moving to preserve its always-present state owner; an owning pointer can be moved instead if the application needs to transfer the context. Allocation and vector growth can fail independently; false only reports the state rule in this fixture.
State-specific C++ classes can restrict method availability too: Closed has no send method. However, std::move does not invalidate the old variable as Rust does. Our C++ typed example calls each transition once; its rvalue-qualified methods are not a complete one-shot protocol. A production C++ API must define and enforce permitted operations on former state objects, rather than assume their lifetime has ended.
Rust: runtime enum and consuming typestate transitions¶
Create a project with cargo new state_contract --edition 2024. Replace src/main.rs with the complete program. A module boundary prevents callers from manufacturing a Ready session by writing its private fields directly.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 | |
Within the project:
The enum keeps each runtime state with its appropriate payload. The state-specific implementations expose send only on Session
The fixture's unwrap calls assert known valid transitions, not a recommendation to panic on user commands. A command dispatcher should match Result and report disallowed operations normally. Real opening, sending and closing may fail for reasons unrelated to the state type.
Changed requirements: fallible transitions and runtime storage¶
If opening becomes fallible and the caller must retry, consuming self and returning only an error can discard the old session. Design the error branch to return the retained old owner, for example Result
If one collection must hold sessions in any phase, distinct Session types do not fit one homogeneous vector directly. A closed enum wrapper can retain those alternatives at runtime. That is not a failure of typestate: runtime command routing and typed internal operations can be combined at a deliberate boundary.
If state implementations must be added externally, a trait-object state family may be suitable. An enum makes the closed alternatives visible to exhaustive matching, while an open family shifts some checks into the interface contract. Adding traits does not make every transition correct, and compile-time availability cannot validate the behaviour of remote devices.
Intentional failure: send is not an Idle operation¶
This independent Rust program isolates method availability and must fail to compile:
Use the working module's consuming open transition, then send on its returned Ready session. Do not make send available on all Session types merely to remove the error. The miniature failure program omits the production module's privacy boundary; its purpose is just to demonstrate method selection.
C++ can also reject a method absent from an Idle-specific type:
Use the complete C++ Idle::open/Ready API for the repair, remembering that the former moved-from C++ object still exists. The similarity in available method names does not make consuming transitions identical between languages.
Selection criteria and exercises¶
| Requirement | Candidate | Important boundary |
|---|---|---|
| Commands arrive with phase unknown at runtime | Enum or State interface | Reject invalid operations without corrupting data |
| Known lifecycle sequence with phase-specific operations | Typestate / state-specific classes | Control construction, ownership and former-state use |
| Fallible transition with retry | Explicit success and retained-owner error branches | Define which owner survives failure |
| One container of mixed phases | Runtime tagged wrapper | Validate phase before accessing a typed operation |
| Externally extensible state behaviour | Trait/virtual interface | Document transitions and invariant responsibilities |
- Try opening twice after a recorded zero; preserve both the Ready phase and that reading.
- Remove the Ready guard in runtime send and retain the invalid-operation tests. The broken contract must be detected without touching hardware.
- Add a simulated failed opening that returns its Idle owner, then retry successfully. Explain what differs if the failure discards self.
- Attempt send on Closed, reuse Ready after close, and forge Session
from outside the module. Treat each compiler rejection separately from runtime user-input errors. - Add a Paused phase. Identify which enum matches, State implementations and typed transitions need changing, without calling either design universally superior.
Verification and next step¶
On October 10, 2026, the final extracted sources passed on the authorised Raspberry Pi 4B with 64-bit user space, kernel 6.18.50+rpt-rpi-v8, Rust/Cargo 1.99.0 and GCC g++ 14.2.0. Rust check, exact-source formatting, five debug/release tests and exact outputs passed. The valid-open repair and failed-open retained-owner/retry fixture passed assertions. Idle/Closed send, reused consumed Ready and forged private construction were rejected with E0599/E0382/E0451. A deliberate illegal Idle send compiled but failed the guard test. C++20 normal output and valid typed-open/empty-close assertions passed at -O0/-O2; the repair also demonstrated that a former C++ Ready object remains callable after close. The missing Idle send was rejected. Sources/logs were retained and generated Cargo targets cleaned. This is a lifecycle fixture, not a network, shutdown or performance test.
Next: Factory and Builder, constructing valid values behind a public API.