Examples¶
Real-world patterns where typestates prevent expensive bugs.
Running the Examples
All examples are complete, runnable files in the examples/ directory.
Payment Processing¶
Payment processing requires strict ordering: authorize before capture, capture before refund. Typestates prevent costly mistakes like double-capture or refunds before capture.
Bugs prevented: Capturing before authorization, double-capture, refunding before capture, operations on settled payments.
## Payment Processing with Typestates
##
## The payment processing flow is a perfect typestate example because mistakes
## are EXPENSIVE. Charge before authorization? Chargeback. Refund twice?
## Money gone. Capture an expired authorization? Failed transaction.
##
## This example models the standard payment flow:
## Created -> Authorized -> Captured -> (Refunded | Settled)
##
## The typestate ensures you CANNOT:
## - Capture without authorizing first
## - Refund before capturing
## - Capture an already-captured payment
## - Authorize an already-authorized payment
import ../src/typestates
type
Payment = object
id: string
amount: int # cents, to avoid float issues
currency: string
cardToken: string
authCode: string
capturedAt: int64
refundedAmount: int
# States represent where in the lifecycle this payment is
Created = distinct Payment ## Just created, not yet authorized
Authorized = distinct Payment ## Card charged, funds held, not yet captured
Captured = distinct Payment ## Funds transferred to merchant
PartiallyRefunded = distinct Payment ## Some amount refunded
FullyRefunded = distinct Payment ## Entire amount refunded
Settled = distinct Payment ## Batch settled, funds in bank
Voided = distinct Payment ## Authorization cancelled before capture
typestate Payment:
# Payments may need status checks and can be refunded after completion.
consumeOnTransition = false
states Created, Authorized, Captured, PartiallyRefunded, FullyRefunded, Settled, Voided
transitions:
Created -> Authorized
Authorized -> (Captured | Voided) as AuthResult
Captured -> (PartiallyRefunded | FullyRefunded | Settled) as CaptureResult
PartiallyRefunded -> (PartiallyRefunded | FullyRefunded | Settled) as RefundResult
FullyRefunded -> Settled
# ============================================================================
# Transition procedures - each one enforces the state machine
# ============================================================================
proc authorize(p: Created, cardToken: string): Authorized {.transition.} =
## Authorize payment against the card.
## This places a hold on the customer's funds but doesn't transfer them.
## In production, this would call your payment processor API.
var payment = p.Payment
payment.cardToken = cardToken
payment.authCode = "AUTH_" & payment.id
echo " [GATEWAY] Authorized $", payment.amount, " on card ending in ****"
result = Authorized(payment)
proc capture(p: Authorized): Captured {.transition.} =
## Capture the authorized funds - money moves to merchant.
## Must happen within auth window (usually 7 days).
var payment = p.Payment
payment.capturedAt = 1234567890 # In real code: current timestamp
echo " [GATEWAY] Captured $", payment.amount, " (auth: ", payment.authCode, ")"
result = Captured(payment)
proc void(p: Authorized): Voided {.transition.} =
## Cancel the authorization before capture.
## Releases the hold on customer's card - no money moved.
echo " [GATEWAY] Voided authorization ", p.Payment.authCode
result = Voided(p.Payment)
proc partialRefund(p: Captured, amount: int): PartiallyRefunded {.transition.} =
## Refund part of the captured amount.
var payment = p.Payment
payment.refundedAmount = amount
echo " [GATEWAY] Partial refund: $", amount, " of $", payment.amount
result = PartiallyRefunded(payment)
proc fullRefund(p: Captured): FullyRefunded {.transition.} =
## Refund the entire captured amount.
var payment = p.Payment
payment.refundedAmount = payment.amount
echo " [GATEWAY] Full refund: $", payment.amount
result = FullyRefunded(payment)
proc additionalRefund(p: PartiallyRefunded, amount: int): RefundResult {.transition.} =
## Add more refund to a partially refunded payment.
var payment = p.Payment
payment.refundedAmount += amount
echo " [GATEWAY] Additional refund: $", amount
if payment.refundedAmount >= payment.amount:
result = RefundResult -> FullyRefunded(payment)
else:
result = RefundResult -> PartiallyRefunded(payment)
proc settle(p: Captured): Settled {.transition.} =
## Batch settlement - funds deposited to merchant bank.
echo " [GATEWAY] Settled $", p.Payment.amount, " to merchant account"
result = Settled(p.Payment)
proc settle(p: PartiallyRefunded): Settled {.transition.} =
## Settle a partially refunded payment (net amount).
let net = p.Payment.amount - p.Payment.refundedAmount
echo " [GATEWAY] Settled $", net, " (after refunds) to merchant account"
result = Settled(p.Payment)
proc settle(p: FullyRefunded): Settled {.transition.} =
## Settle a fully refunded payment ($0 net).
echo " [GATEWAY] Settled $0 (fully refunded) - closing payment"
result = Settled(p.Payment)
# ============================================================================
# Non-transition operations - query state without changing it
# ============================================================================
func amount(p: PaymentStates): int =
## Get the payment amount in cents.
p.Payment.amount
func refundedAmount(p: PartiallyRefunded): int =
## How much has been refunded so far?
p.Payment.refundedAmount
func remainingAmount(p: PartiallyRefunded): int =
## How much can still be refunded?
p.Payment.amount - p.Payment.refundedAmount
# ============================================================================
# Example usage showing compile-time safety
# ============================================================================
when isMainModule:
echo "=== Payment Processing Demo ===\n"
echo "1. Creating payment for $99.99..."
var payment = Created(Payment(
id: "pay_abc123",
amount: 9999, # $99.99 in cents
currency: "USD"
))
echo "\n2. Authorizing payment..."
let authorized = payment.authorize("card_tok_visa_4242")
echo "\n3. Capturing funds..."
let captured = authorized.capture()
echo "\n4. Customer requests $25 refund..."
let refunded = captured.partialRefund(2500)
echo " Remaining: $", refunded.remainingAmount()
echo "\n5. End of day settlement..."
let settled = refunded.settle()
echo "\n=== Payment lifecycle complete! ===\n"
# =========================================================================
# COMPILE-TIME ERRORS - These are the bugs the typestate PREVENTS:
# =========================================================================
echo "The following bugs are caught at COMPILE TIME:\n"
# BUG 1: Capturing without authorization
# "Oops, we forgot to auth - charged the wrong amount!"
# let oops1 = payment.capture()
echo " [PREVENTED] capture() on Created payment"
# BUG 2: Double-capture
# "Customer charged twice!"
# let oops2 = captured.capture()
echo " [PREVENTED] capture() on already-Captured payment"
# BUG 3: Refunding before capture
# "Refunded money we never had!"
# let oops3 = authorized.partialRefund(1000)
echo " [PREVENTED] partialRefund() on Authorized payment"
# BUG 4: Refunding a settled payment
# "Accounting nightmare - refund after books closed!"
# let oops4 = settled.partialRefund(500)
echo " [PREVENTED] partialRefund() on Settled payment"
# BUG 5: Voiding after capture
# "Tried to void but money already moved!"
# let oops5 = captured.void()
echo " [PREVENTED] void() on Captured payment"
echo "\nUncomment any of the 'oops' lines above to see the compile error!"
Database Connection Pool¶
Connection pools have invariants that are easy to violate: don't query pooled connections, don't return connections mid-transaction, don't commit without a transaction.
Bugs prevented: Query on pooled connection, returning connection while in transaction, committing without transaction, nested transactions.
## Database Connection Pool with Typestates
##
## Connection pool bugs are among the most painful to debug:
## - Query on a closed connection: "connection already closed"
## - Return connection to pool while query is running: data corruption
## - Use connection after returning to pool: race conditions
## - Forget to return connection: pool exhaustion
##
## This example ensures compile-time safety for connection lifecycle.
import ../src/typestates
type
DbConnection = object
id: int
host: string
port: int
database: string
inTransaction: bool
queryCount: int
# Connection states
Pooled = distinct DbConnection ## In the pool, available for checkout
CheckedOut = distinct DbConnection ## Checked out, not in transaction
InTransaction = distinct DbConnection ## Active transaction
Closed = distinct DbConnection ## Permanently closed
typestate DbConnection:
# Connections are pooled and reused, so we disable ownership enforcement.
# This allows checkout -> use -> release -> checkout cycles.
consumeOnTransition = false
states Pooled, CheckedOut, InTransaction, Closed
transitions:
Pooled -> (CheckedOut | Closed) as CheckoutResult # Checkout or close idle connection
CheckedOut -> (Pooled | InTransaction | Closed) as CheckoutAction # Return, begin tx, or close
InTransaction -> CheckedOut # Commit/rollback ends transaction
* -> Closed # Can always force-close
# ============================================================================
# Connection Pool Operations
# ============================================================================
proc checkout(conn: Pooled): CheckedOut {.transition.} =
## Get a connection from the pool for exclusive use.
echo " [POOL] Checked out connection #", conn.DbConnection.id
result = CheckedOut(conn.DbConnection)
proc release(conn: CheckedOut): Pooled {.transition.} =
## Return connection to the pool.
echo " [POOL] Released connection #", conn.DbConnection.id, " (", conn.DbConnection.queryCount, " queries)"
var c = conn.DbConnection
c.queryCount = 0
result = Pooled(c)
proc close(conn: Pooled): Closed {.transition.} =
## Close an idle connection permanently.
echo " [POOL] Closed idle connection #", conn.DbConnection.id
result = Closed(conn.DbConnection)
proc close(conn: CheckedOut): Closed {.transition.} =
## Close a checked-out connection (emergency/error case).
echo " [POOL] Force-closed connection #", conn.DbConnection.id
result = Closed(conn.DbConnection)
# ============================================================================
# Transaction Operations
# ============================================================================
proc beginTransaction(conn: CheckedOut): InTransaction {.transition.} =
## Start a database transaction.
echo " [DB] BEGIN TRANSACTION"
var c = conn.DbConnection
c.inTransaction = true
result = InTransaction(c)
proc commit(conn: InTransaction): CheckedOut {.transition.} =
## Commit the current transaction.
echo " [DB] COMMIT"
var c = conn.DbConnection
c.inTransaction = false
result = CheckedOut(c)
proc rollback(conn: InTransaction): CheckedOut {.transition.} =
## Rollback the current transaction.
echo " [DB] ROLLBACK"
var c = conn.DbConnection
c.inTransaction = false
result = CheckedOut(c)
# ============================================================================
# Query Operations (no state change)
# ============================================================================
proc execute(conn: CheckedOut, sql: string): CheckedOut {.notATransition.} =
## Execute a SQL statement (outside transaction).
var c = conn.DbConnection
c.queryCount += 1
echo " [DB] Execute: ", sql
result = CheckedOut(c)
proc execute(conn: InTransaction, sql: string): InTransaction {.notATransition.} =
## Execute a SQL statement (inside transaction).
var c = conn.DbConnection
c.queryCount += 1
echo " [DB] Execute (in tx): ", sql
result = InTransaction(c)
func isInTransaction(conn: DbConnectionStates): bool =
## Check if connection has active transaction.
conn.DbConnection.inTransaction
# ============================================================================
# Example Usage
# ============================================================================
when isMainModule:
echo "=== Database Connection Demo ===\n"
# Simulate a connection pool
var pooledConn = Pooled(DbConnection(
id: 42,
host: "localhost",
port: 5432,
database: "myapp"
))
echo "1. Checkout connection from pool..."
let conn = pooledConn.checkout()
echo "\n2. Execute some queries..."
let conn2 = conn.execute("SELECT * FROM users WHERE id = 1")
let conn3 = conn2.execute("UPDATE users SET last_login = NOW() WHERE id = 1")
echo "\n3. Start a transaction for batch insert..."
let tx = conn3.beginTransaction()
echo "\n4. Execute transactional queries..."
let tx2 = tx.execute("INSERT INTO audit_log VALUES (1, 'login', NOW())")
let tx3 = tx2.execute("INSERT INTO sessions VALUES (1, 'abc123', NOW())")
echo "\n5. Commit the transaction..."
let afterTx = tx3.commit()
echo "\n6. Return connection to pool..."
let returned = afterTx.release()
echo "\n=== Connection lifecycle complete! ===\n"
# =========================================================================
# COMPILE-TIME ERRORS - These bugs are prevented:
# =========================================================================
echo "The following bugs are caught at COMPILE TIME:\n"
# BUG 1: Query on pooled (not checked out) connection
# let bad1 = returned.execute("SELECT 1")
echo " [PREVENTED] execute() on Pooled connection"
# BUG 2: Return connection while in transaction
# let bad2 = tx.release()
echo " [PREVENTED] release() on InTransaction connection"
# BUG 3: Commit without starting transaction
# let bad3 = conn.commit()
echo " [PREVENTED] commit() on CheckedOut connection (no transaction)"
# BUG 4: Double checkout (already checked out)
# let bad4 = conn.checkout()
echo " [PREVENTED] checkout() on CheckedOut connection"
# BUG 5: Begin transaction inside transaction
# let bad5 = tx.beginTransaction()
echo " [PREVENTED] beginTransaction() on InTransaction connection"
echo "\nUncomment any of the 'bad' lines above to see the compile error!"
HTTP Request Lifecycle¶
HTTP requests follow a strict sequence: set headers, send headers, send body, await response. Typestates enforce this ordering at compile time.
Bugs prevented: Adding headers after sent, sending body before headers, reading response before request complete.
## HTTP Request Lifecycle with Typestates
##
## HTTP requests have a strict lifecycle that's easy to mess up:
## - Writing body after sending headers
## - Reading response before sending request
## - Sending headers twice
## - Using a connection after it's closed
##
## This example models the full request/response lifecycle.
import ../src/typestates
type
HttpRequest = object
meth: string
path: string
headers: seq[(string, string)]
body: string
responseCode: int
responseBody: string
# Request states
Building = distinct HttpRequest ## Accumulating headers
HeadersSent = distinct HttpRequest ## Headers sent, can send body
RequestSent = distinct HttpRequest ## Full request sent, awaiting response
ResponseReceived = distinct HttpRequest ## Response received, can read
Closed = distinct HttpRequest ## Connection closed
typestate HttpRequest:
# HTTP connections support keep-alive (reuse after response).
consumeOnTransition = false
states Building, HeadersSent, RequestSent, ResponseReceived, Closed
transitions:
Building -> HeadersSent
HeadersSent -> RequestSent # Send body/finalize request
RequestSent -> ResponseReceived # Receive response
ResponseReceived -> (Closed | Building) as ResponseAction # Close or reuse for keep-alive
* -> Closed # Can always abort
# ============================================================================
# Building the request
# ============================================================================
proc newRequest(meth: string, path: string): Building =
## Create a new HTTP request builder.
echo " [HTTP] ", meth, " ", path
result = Building(HttpRequest(meth: meth, path: path))
proc header(req: Building, key: string, value: string): Building {.notATransition.} =
## Add a header to the request.
var r = req.HttpRequest
r.headers.add((key, value))
echo " [HTTP] Header: ", key, ": ", value
result = Building(r)
proc sendHeaders(req: Building): HeadersSent {.transition.} =
## Finalize and send the headers.
echo " [HTTP] >>> Sending headers..."
result = HeadersSent(req.HttpRequest)
# ============================================================================
# Sending the request
# ============================================================================
proc sendBody(req: HeadersSent, body: string): RequestSent {.transition.} =
## Send the request body (for POST, PUT, etc.).
var r = req.HttpRequest
r.body = body
echo " [HTTP] >>> Sending body (", body.len, " bytes)"
result = RequestSent(r)
proc finish(req: HeadersSent): RequestSent {.transition.} =
## Finish request without body (for GET, DELETE, etc.).
echo " [HTTP] >>> Request complete (no body)"
result = RequestSent(req.HttpRequest)
# ============================================================================
# Receiving the response
# ============================================================================
proc awaitResponse(req: RequestSent): ResponseReceived {.transition.} =
## Wait for and receive the response.
var r = req.HttpRequest
# Simulate response
r.responseCode = 200
r.responseBody = """{"status": "ok", "data": [1, 2, 3]}"""
echo " [HTTP] <<< Response: ", r.responseCode
result = ResponseReceived(r)
func statusCode(resp: ResponseReceived): int =
## Get the HTTP status code.
resp.HttpRequest.responseCode
func body(resp: ResponseReceived): string =
## Get the response body.
resp.HttpRequest.responseBody
func isSuccess(resp: ResponseReceived): bool =
## Check if response indicates success (2xx).
let code = resp.HttpRequest.responseCode
code >= 200 and code < 300
# ============================================================================
# Closing or reusing
# ============================================================================
proc close(resp: ResponseReceived): Closed {.transition.} =
## Close the connection.
echo " [HTTP] Connection closed"
result = Closed(resp.HttpRequest)
proc reuse(resp: ResponseReceived): Building {.transition.} =
## Reuse connection for another request (keep-alive).
echo " [HTTP] Reusing connection (keep-alive)"
var r = HttpRequest() # Fresh request on same connection
result = Building(r)
# ============================================================================
# Example Usage
# ============================================================================
when isMainModule:
echo "=== HTTP Request Demo ===\n"
echo "1. Building GET request..."
let req1 = newRequest("GET", "/api/users")
.header("Accept", "application/json")
.header("Authorization", "Bearer token123")
echo "\n2. Sending headers..."
let headersSent = req1.sendHeaders()
echo "\n3. Finishing request (no body for GET)..."
let sent = headersSent.finish()
echo "\n4. Awaiting response..."
let response = sent.awaitResponse()
echo "\n5. Reading response..."
echo " Status: ", response.statusCode()
echo " Body: ", response.body()
echo " Success: ", response.isSuccess()
echo "\n6. Closing connection..."
let closed = response.close()
echo "\n=== Request lifecycle complete! ===\n"
# =========================================================================
# COMPILE-TIME ERRORS - These bugs are prevented:
# =========================================================================
echo "The following bugs are caught at COMPILE TIME:\n"
# BUG 1: Adding headers after they're sent
# let bad1 = headersSent.header("X-Late", "header")
echo " [PREVENTED] header() after sendHeaders()"
# BUG 2: Sending body on GET (before sending headers)
# let bad2 = req1.sendBody("data")
echo " [PREVENTED] sendBody() before sendHeaders()"
# BUG 3: Reading response before request is sent
# let bad3 = headersSent.statusCode()
echo " [PREVENTED] statusCode() before response received"
# BUG 4: Sending more data after request is complete
# let bad4 = sent.sendBody("more data")
echo " [PREVENTED] sendBody() after request sent"
# BUG 5: Using closed connection
# let bad5 = closed.reuse()
echo " [PREVENTED] reuse() on Closed connection"
echo "\nUncomment any of the 'bad' lines above to see the compile error!"
OAuth Authentication¶
OAuth requires authenticated tokens for API calls and refresh tokens to renew expired access. Typestates prevent calls with missing or expired credentials.
Bugs prevented: API calls without authentication, API calls with expired token, refreshing non-expired token.
## OAuth 2.0 Authentication Flow with Typestates
##
## OAuth flows are notoriously easy to get wrong:
## - Using an expired token
## - Calling API before authenticating
## - Refreshing with an invalid refresh token
## - Skipping PKCE verification
##
## This example models the Authorization Code + PKCE flow.
import ../src/typestates
type
OAuthSession = object
clientId: string
redirectUri: string
codeVerifier: string # PKCE
codeChallenge: string # PKCE
authCode: string
accessToken: string
refreshToken: string
expiresAt: int64
# OAuth states
Unauthenticated = distinct OAuthSession ## No tokens yet
AwaitingCallback = distinct OAuthSession ## Auth URL generated, waiting for callback
Authenticated = distinct OAuthSession ## Have valid access token
TokenExpired = distinct OAuthSession ## Access token expired, need refresh
RefreshFailed = distinct OAuthSession ## Refresh failed, need re-auth
typestate OAuthSession:
# OAuth tokens may be read multiple times and refreshed.
consumeOnTransition = false
states Unauthenticated, AwaitingCallback, Authenticated, TokenExpired, RefreshFailed
transitions:
Unauthenticated -> AwaitingCallback # Start auth flow
AwaitingCallback -> Authenticated # Callback received, tokens exchanged
Authenticated -> TokenExpired # Token expired
TokenExpired -> Authenticated # Refresh succeeded
TokenExpired -> RefreshFailed # Refresh failed
RefreshFailed -> AwaitingCallback # Start over
* -> Unauthenticated # Logout
# ============================================================================
# Starting the flow
# ============================================================================
proc startAuth(session: Unauthenticated, clientId: string, redirectUri: string): AwaitingCallback {.transition.} =
## Generate authorization URL and PKCE challenge.
var s = session.OAuthSession
s.clientId = clientId
s.redirectUri = redirectUri
s.codeVerifier = "random_verifier_string_43_chars_min" # In prod: secure random
s.codeChallenge = "hashed_challenge" # In prod: SHA256(verifier)
let authUrl = "https://auth.example.com/authorize?" &
"client_id=" & clientId &
"&redirect_uri=" & redirectUri &
"&code_challenge=" & s.codeChallenge &
"&code_challenge_method=S256"
echo " [OAUTH] Authorization URL generated"
echo " [OAUTH] Redirect user to: ", authUrl[0..50], "..."
result = AwaitingCallback(s)
# ============================================================================
# Handling the callback
# ============================================================================
proc handleCallback(session: AwaitingCallback, authCode: string): Authenticated {.transition.} =
## Exchange authorization code for tokens.
var s = session.OAuthSession
s.authCode = authCode
# In production: POST to token endpoint with code + code_verifier
echo " [OAUTH] Exchanging auth code for tokens..."
echo " [OAUTH] Verifying PKCE: code_verifier=", s.codeVerifier[0..10], "..."
s.accessToken = "eyJhbGc..." & authCode[0..5]
s.refreshToken = "refresh_" & authCode[0..5]
s.expiresAt = 1234567890 + 3600
echo " [OAUTH] Access token received (expires in 1h)"
result = Authenticated(s)
# ============================================================================
# Using the API
# ============================================================================
proc callApi(session: Authenticated, endpoint: string): string {.notATransition.} =
## Make an authenticated API call.
echo " [API] GET ", endpoint
echo " [API] Authorization: Bearer ", session.OAuthSession.accessToken[0..10], "..."
result = """{"user": "alice", "email": "alice@example.com"}"""
proc getAccessToken(session: Authenticated): string =
## Get the current access token for manual use.
session.OAuthSession.accessToken
# ============================================================================
# Token expiration and refresh
# ============================================================================
proc tokenExpired(session: Authenticated): TokenExpired {.transition.} =
## Mark the access token as expired.
echo " [OAUTH] Access token expired!"
result = TokenExpired(session.OAuthSession)
proc refresh(session: TokenExpired): Authenticated {.transition.} =
## Refresh the access token using the refresh token.
var s = session.OAuthSession
# In production: POST to token endpoint with refresh_token
echo " [OAUTH] Refreshing token using: ", s.refreshToken[0..10], "..."
s.accessToken = "eyJhbGc...refreshed"
s.expiresAt = 1234567890 + 7200
echo " [OAUTH] New access token received"
result = Authenticated(s)
proc refreshFailed(session: TokenExpired): RefreshFailed {.transition.} =
## Handle refresh failure (e.g., refresh token revoked).
echo " [OAUTH] Refresh failed! Token may be revoked."
result = RefreshFailed(session.OAuthSession)
proc restartAuth(session: RefreshFailed): AwaitingCallback {.transition.} =
## Start authentication flow again after refresh failure.
var s = session.OAuthSession
s.accessToken = ""
s.refreshToken = ""
s.codeVerifier = "new_verifier_for_retry"
s.codeChallenge = "new_challenge"
echo " [OAUTH] Starting fresh authentication..."
result = AwaitingCallback(s)
# ============================================================================
# Logout
# ============================================================================
proc logout(session: Authenticated): Unauthenticated {.transition.} =
## Log out and revoke tokens.
echo " [OAUTH] Logging out, revoking tokens..."
result = Unauthenticated(OAuthSession())
# ============================================================================
# Example Usage
# ============================================================================
when isMainModule:
echo "=== OAuth 2.0 Authentication Demo ===\n"
echo "1. Creating unauthenticated session..."
let session = Unauthenticated(OAuthSession())
echo "\n2. Starting OAuth flow (PKCE)..."
let awaiting = session.startAuth(
clientId = "my-app-client-id",
redirectUri = "myapp://callback"
)
echo "\n3. User authorizes, handling callback..."
let authed = awaiting.handleCallback(authCode = "abc123xyz")
echo "\n4. Making authenticated API calls..."
let userData = authed.callApi("/api/user/me")
echo " Response: ", userData
echo "\n5. Simulating token expiration..."
let expired = authed.tokenExpired()
echo "\n6. Refreshing access token..."
let refreshed = expired.refresh()
echo "\n7. Making another API call with new token..."
let moreData = refreshed.callApi("/api/user/settings")
echo "\n8. Logging out..."
let loggedOut = refreshed.logout()
echo "\n=== OAuth flow complete! ===\n"
# =========================================================================
# COMPILE-TIME ERRORS - These bugs are prevented:
# =========================================================================
echo "The following bugs are caught at COMPILE TIME:\n"
# BUG 1: API call without authentication
# let bad1 = session.callApi("/api/secret")
echo " [PREVENTED] callApi() on Unauthenticated session"
# BUG 2: API call with expired token
# let bad2 = expired.callApi("/api/data")
echo " [PREVENTED] callApi() on TokenExpired session"
# BUG 3: Refresh without expiration
# let bad3 = authed.refresh()
echo " [PREVENTED] refresh() on Authenticated session (not expired)"
# BUG 4: Handle callback twice
# let bad4 = authed.handleCallback("another_code")
echo " [PREVENTED] handleCallback() on Authenticated session"
# BUG 5: Use logged out session
# let bad5 = loggedOut.getAccessToken()
echo " [PREVENTED] getAccessToken() on Unauthenticated session"
echo "\nUncomment any of the 'bad' lines above to see the compile error!"
Robot Arm Controller¶
Hardware control requires strict operation sequences. Moving without homing can crash into limits; powering off during movement can damage motors.
Bugs prevented: Moving without homing, power off while moving, continuing after emergency stop.
## Robot Arm Controller with Typestates
##
## Hardware control is where typestates REALLY shine. Wrong operation order
## can damage expensive equipment or cause safety hazards:
## - Moving arm before homing: crash into limits
## - Operating without calibration: inaccurate positioning
## - Emergency stop not handled: damage or injury
## - Power off while moving: motor damage
##
## This example models a robotic arm controller with safety states.
import ../src/typestates
type
RobotArm = object
x, y, z: float # Current position
homeX, homeY, homeZ: float # Home position
speed: float # Movement speed
toolAttached: bool
emergencyReason: string
# Robot arm states
PoweredOff = distinct RobotArm ## No power to motors
Initializing = distinct RobotArm ## Powering up, running diagnostics
NeedsHoming = distinct RobotArm ## Powered but position unknown
Homing = distinct RobotArm ## Currently finding home position
Ready = distinct RobotArm ## Homed and ready for commands
Moving = distinct RobotArm ## Currently executing movement
EmergencyStop = distinct RobotArm ## E-stop triggered, frozen
typestate RobotArm:
# Robot arm state needs to be inspected for position, calibration, etc.
consumeOnTransition = false
states PoweredOff, Initializing, NeedsHoming, Homing, Ready, Moving, EmergencyStop
transitions:
PoweredOff -> Initializing
Initializing -> NeedsHoming
NeedsHoming -> Homing
Homing -> Ready
Ready -> (Moving | PoweredOff) as ReadyAction
Moving -> (Ready | EmergencyStop) as MovementResult
EmergencyStop -> (NeedsHoming | PoweredOff) as EmergencyAction # Must re-home after E-stop
# ============================================================================
# Power and Initialization
# ============================================================================
proc powerOn(arm: PoweredOff): Initializing {.transition.} =
## Power on the robot arm and start initialization.
echo " [ARM] Powering on..."
echo " [ARM] Running motor diagnostics..."
result = Initializing(arm.RobotArm)
proc completeInit(arm: Initializing): NeedsHoming {.transition.} =
## Complete initialization, now needs homing.
echo " [ARM] Diagnostics passed"
echo " [ARM] WARNING: Position unknown - homing required!"
result = NeedsHoming(arm.RobotArm)
proc powerOff(arm: Ready): PoweredOff {.transition.} =
## Safely power off the arm when ready.
echo " [ARM] Powering off safely..."
result = PoweredOff(arm.RobotArm)
proc powerOffEmergency(arm: EmergencyStop): PoweredOff {.transition.} =
## Power off after emergency stop.
echo " [ARM] Emergency power off"
result = PoweredOff(arm.RobotArm)
# ============================================================================
# Homing Operations
# ============================================================================
proc startHoming(arm: NeedsHoming): Homing {.transition.} =
## Begin the homing sequence to find reference position.
echo " [ARM] Starting homing sequence..."
echo " [ARM] Moving to limit switches at low speed..."
result = Homing(arm.RobotArm)
proc homingComplete(arm: Homing, homeX, homeY, homeZ: float): Ready {.transition.} =
## Complete homing and set reference position.
var a = arm.RobotArm
a.x = homeX
a.y = homeY
a.z = homeZ
a.homeX = homeX
a.homeY = homeY
a.homeZ = homeZ
echo " [ARM] Homing complete. Position: (", homeX, ", ", homeY, ", ", homeZ, ")"
echo " [ARM] Ready for commands!"
result = Ready(a)
proc resetAfterEmergency(arm: EmergencyStop): NeedsHoming {.transition.} =
## Reset emergency stop - position is now uncertain.
echo " [ARM] E-stop reset. Position uncertain - must re-home!"
var a = arm.RobotArm
a.emergencyReason = ""
result = NeedsHoming(a)
# ============================================================================
# Movement Operations
# ============================================================================
proc moveTo(arm: Ready, x, y, z: float): Moving {.transition.} =
## Start moving to target position.
echo " [ARM] Moving to (", x, ", ", y, ", ", z, ")..."
result = Moving(arm.RobotArm)
proc moveComplete(arm: Moving, x, y, z: float): Ready {.transition.} =
## Movement completed successfully.
var a = arm.RobotArm
a.x = x
a.y = y
a.z = z
echo " [ARM] Reached position (", x, ", ", y, ", ", z, ")"
result = Ready(a)
proc emergencyStop(arm: Moving, reason: string): EmergencyStop {.transition.} =
## Trigger emergency stop during movement!
var a = arm.RobotArm
a.emergencyReason = reason
echo " [ARM] !!! EMERGENCY STOP !!!"
echo " [ARM] Reason: ", reason
echo " [ARM] Motors locked. Manual intervention required."
result = EmergencyStop(a)
# ============================================================================
# Status and Configuration (no state change)
# ============================================================================
func position(arm: Ready): tuple[x, y, z: float] =
## Get current position (only valid when Ready).
(arm.RobotArm.x, arm.RobotArm.y, arm.RobotArm.z)
proc setSpeed(arm: Ready, speed: float): Ready {.notATransition.} =
## Configure movement speed.
var a = arm.RobotArm
a.speed = speed
echo " [ARM] Speed set to ", speed, " mm/s"
result = Ready(a)
proc attachTool(arm: Ready, toolName: string): Ready {.notATransition.} =
## Attach a tool to the arm.
var a = arm.RobotArm
a.toolAttached = true
echo " [ARM] Tool attached: ", toolName
result = Ready(a)
# ============================================================================
# Example Usage
# ============================================================================
when isMainModule:
echo "=== Robot Arm Controller Demo ===\n"
echo "1. Starting with powered-off arm..."
var arm = PoweredOff(RobotArm())
echo "\n2. Powering on..."
let initializing = arm.powerOn()
echo "\n3. Completing initialization..."
let needsHoming = initializing.completeInit()
echo "\n4. Starting homing sequence..."
let homing = needsHoming.startHoming()
echo "\n5. Homing complete..."
let ready = homing.homingComplete(0.0, 0.0, 100.0)
echo "\n6. Configuring arm..."
let configured = ready
.setSpeed(50.0)
.attachTool("gripper")
echo "\n7. Moving to pick position..."
let moving1 = configured.moveTo(100.0, 50.0, 20.0)
let atPick = moving1.moveComplete(100.0, 50.0, 20.0)
echo "\n8. Moving to place position..."
let moving2 = atPick.moveTo(200.0, 50.0, 20.0)
let atPlace = moving2.moveComplete(200.0, 50.0, 20.0)
echo "\n9. Returning home and powering off..."
let moving3 = atPlace.moveTo(0.0, 0.0, 100.0)
let atHome = moving3.moveComplete(0.0, 0.0, 100.0)
let off = atHome.powerOff()
echo "\n=== Demo complete! ===\n"
# =========================================================================
# COMPILE-TIME ERRORS - These dangerous bugs are prevented:
# =========================================================================
echo "The following DANGEROUS bugs are caught at COMPILE TIME:\n"
# BUG 1: Moving without homing (position unknown!)
# let bad1 = needsHoming.moveTo(100.0, 0.0, 0.0)
echo " [PREVENTED] moveTo() without homing - could crash into limits!"
# BUG 2: Power off while moving (motor damage!)
# let bad2 = moving1.powerOff()
echo " [PREVENTED] powerOff() while moving - motor damage risk!"
# BUG 3: Continue after emergency stop
# let estop = moving2.emergencyStop("Obstacle detected")
# let bad3 = estop.moveTo(0.0, 0.0, 0.0)
echo " [PREVENTED] moveTo() after E-stop - dangerous!"
# BUG 4: Skip initialization
# let bad4 = initializing.startHoming()
echo " [PREVENTED] startHoming() before initialization complete"
# BUG 5: Configure speed while moving
# let bad5 = moving1.setSpeed(100.0)
echo " [PREVENTED] setSpeed() while moving - could cause issues"
echo "\nUncomment any of the 'bad' lines to see the compile error!"
Order Fulfillment¶
Order fulfillment has a fixed sequence: place, pay, ship, deliver. Typestates ensure orders can't be shipped before payment or shipped twice.
Bugs prevented: Ship before payment, double-ship, operations on unplaced cart.
## E-commerce Order Fulfillment with Typestates
##
## Order fulfillment bugs are expensive and embarrassing:
## - Shipping before payment: lost merchandise
## - Refunding unshipped orders: process confusion
## - Double-shipping: inventory nightmare
## - Cancelling already-shipped: customer confusion
##
## This example models the complete order lifecycle.
import ../src/typestates
import std/hashes
type
Order = object
id: string
customerId: string
items: seq[(string, int, int)] # (sku, qty, price)
total: int
paymentId: string
trackingNumber: string
cancelReason: string
# Order states
Cart = distinct Order ## Items being added, not yet placed
Placed = distinct Order ## Order submitted, pending payment
Paid = distinct Order ## Payment received
Picking = distinct Order ## Warehouse picking items
Packed = distinct Order ## Items packed, ready to ship
Shipped = distinct Order ## Handed to carrier
Delivered = distinct Order ## Customer received package
Cancelled = distinct Order ## Order cancelled
Returned = distinct Order ## Items returned by customer
typestate Order:
# Orders need to be inspected at various stages (for status, totals, etc.).
consumeOnTransition = false
states Cart, Placed, Paid, Picking, Packed, Shipped, Delivered, Cancelled, Returned
transitions:
Cart -> Placed # Submit order
Placed -> (Paid | Cancelled) as PaymentResult # Pay or cancel
Paid -> (Picking | Cancelled) as FulfillmentAction # Start fulfillment or cancel (refund)
Picking -> Packed # Finish picking
Packed -> Shipped # Hand to carrier
Shipped -> Delivered # Delivery confirmed
Delivered -> Returned # Customer returns
* -> Cancelled # Can always cancel (with appropriate handling)
# ============================================================================
# Cart Operations
# ============================================================================
proc newOrder(customerId: string): Cart =
## Create a new shopping cart.
echo " [ORDER] New cart for customer: ", customerId
result = Cart(Order(customerId: customerId))
proc addItem(order: Cart, sku: string, qty: int, price: int): Cart {.notATransition.} =
## Add an item to the cart.
var o = order.Order
o.items.add((sku, qty, price))
o.total += qty * price
echo " [ORDER] Added ", qty, "x ", sku, " @ $", price, " = $", qty * price
result = Cart(o)
proc placeOrder(order: Cart): Placed {.transition.} =
## Submit the order for processing.
var o = order.Order
o.id = "ORD-" & $hash(o.customerId) # Simplified ID generation
echo " [ORDER] Order placed: ", o.id, " (total: $", o.total, ")"
result = Placed(o)
# ============================================================================
# Payment
# ============================================================================
proc pay(order: Placed, paymentId: string): Paid {.transition.} =
## Record payment for the order.
var o = order.Order
o.paymentId = paymentId
echo " [ORDER] Payment received: ", paymentId
result = Paid(o)
proc cancelUnpaid(order: Placed, reason: string): Cancelled {.transition.} =
## Cancel an unpaid order (no refund needed).
var o = order.Order
o.cancelReason = reason
echo " [ORDER] Order cancelled (unpaid): ", reason
result = Cancelled(o)
proc cancelWithRefund(order: Paid, reason: string): Cancelled {.transition.} =
## Cancel a paid order and process refund.
var o = order.Order
o.cancelReason = reason
echo " [ORDER] Order cancelled with refund"
echo " [ORDER] Refunding payment: ", o.paymentId
result = Cancelled(o)
# ============================================================================
# Fulfillment
# ============================================================================
proc startPicking(order: Paid): Picking {.transition.} =
## Start warehouse picking process.
echo " [WAREHOUSE] Starting pick for order: ", order.Order.id
for (sku, qty, _) in order.Order.items:
echo " [WAREHOUSE] Pick ", qty, "x ", sku
result = Picking(order.Order)
proc finishPacking(order: Picking): Packed {.transition.} =
## Finish packing the order.
echo " [WAREHOUSE] Order packed and ready for shipping"
result = Packed(order.Order)
proc ship(order: Packed, trackingNumber: string): Shipped {.transition.} =
## Hand order to carrier.
var o = order.Order
o.trackingNumber = trackingNumber
echo " [SHIPPING] Order shipped!"
echo " [SHIPPING] Tracking: ", trackingNumber
result = Shipped(o)
proc confirmDelivery(order: Shipped): Delivered {.transition.} =
## Confirm customer received the order.
echo " [SHIPPING] Delivery confirmed for: ", order.Order.id
result = Delivered(order.Order)
# ============================================================================
# Returns
# ============================================================================
proc initiateReturn(order: Delivered, reason: string): Returned {.transition.} =
## Customer initiates a return.
echo " [RETURNS] Return initiated for: ", order.Order.id
echo " [RETURNS] Reason: ", reason
echo " [RETURNS] Send to: 123 Returns Center, Warehouse City"
result = Returned(order.Order)
# ============================================================================
# Status Queries
# ============================================================================
func orderId(order: OrderStates): string =
## Get order ID.
order.Order.id
func trackingNumber(order: Shipped): string =
## Get tracking number.
order.Order.trackingNumber
func total(order: OrderStates): int =
## Get order total.
order.Order.total
# ============================================================================
# Example Usage
# ============================================================================
when isMainModule:
echo "=== E-commerce Order Fulfillment Demo ===\n"
echo "1. Customer adds items to cart..."
let cart = newOrder("cust_12345")
.addItem("SKU-LAPTOP", 1, 99900)
.addItem("SKU-MOUSE", 2, 2500)
.addItem("SKU-CABLE", 3, 1000)
echo "\n2. Customer places order..."
let placed = cart.placeOrder()
echo "\n3. Payment processing..."
let paid = placed.pay("pay_ch_abc123")
echo "\n4. Warehouse picks items..."
let picking = paid.startPicking()
echo "\n5. Items packed..."
let packed = picking.finishPacking()
echo "\n6. Shipped to customer..."
let shipped = packed.ship("1Z999AA10123456784")
echo "\n7. Customer receives package..."
let delivered = shipped.confirmDelivery()
echo "\n=== Order complete! ===\n"
# =========================================================================
# COMPILE-TIME ERRORS - These business logic bugs are prevented:
# =========================================================================
echo "The following bugs are caught at COMPILE TIME:\n"
# BUG 1: Ship before payment
# let bad1 = placed.ship("TRACKING")
echo " [PREVENTED] ship() before payment - lost merchandise!"
# BUG 2: Refund an order that wasn't paid
# let bad2 = placed.cancelWithRefund("changed mind")
echo " [PREVENTED] cancelWithRefund() on unpaid order"
# BUG 3: Ship already-shipped order (double ship)
# let bad3 = shipped.ship("ANOTHER-TRACKING")
echo " [PREVENTED] ship() twice - inventory nightmare!"
# BUG 4: Return before delivery
# let bad4 = shipped.initiateReturn("don't want it")
echo " [PREVENTED] initiateReturn() before delivery"
# BUG 5: Continue fulfillment after cancellation
# let cancelled = paid.cancelWithRefund("out of stock")
# let bad5 = cancelled.startPicking()
echo " [PREVENTED] startPicking() after cancellation"
# BUG 6: Pay for cart (not yet an order)
# let bad6 = cart.pay("payment")
echo " [PREVENTED] pay() on Cart - order not submitted"
echo "\nUncomment any of the 'bad' lines to see the compile error!"
Document Workflow¶
Document publishing enforces a review process: draft, review, approve, publish. Typestates prevent publishing without approval or editing published content.
Bugs prevented: Publishing without approval, editing published content, skipping review process.
## Document Workflow with Typestates
##
## Content publishing workflows have strict rules:
## - Publishing drafts without review: quality issues
## - Editing published content: audit/compliance problems
## - Approving your own work: process violation
## - Skipping required reviews: legal risk
##
## This example models a multi-stage document review workflow.
import ../src/typestates
import std/hashes
type
Document = object
id: string
title: string
content: string
author: string
reviewers: seq[string]
approver: string
version: int
publishedAt: int64
# Document states
Draft = distinct Document ## Being written/edited
InReview = distinct Document ## Submitted for review
ChangesRequested = distinct Document ## Reviewer requested changes
Approved = distinct Document ## Passed review, ready to publish
Published = distinct Document ## Live/public
Archived = distinct Document ## Removed from public, preserved
typestate Document:
# Documents need to be inspected at various stages without consuming them.
consumeOnTransition = false
states Draft, InReview, ChangesRequested, Approved, Published, Archived
transitions:
Draft -> InReview # Submit for review
InReview -> (Approved | ChangesRequested) as ReviewResult # Review decision
ChangesRequested -> InReview # Resubmit after changes
Approved -> Published # Go live
Published -> (Archived | Draft) as PublishAction # Archive or create new version
Archived -> Draft # Restore for new version
# ============================================================================
# Creating and Editing
# ============================================================================
proc newDocument(title: string, author: string): Draft =
## Create a new document draft.
echo " [DOC] Created: '", title, "' by ", author
result = Draft(Document(
id: "doc_" & $hash(title),
title: title,
author: author,
version: 1
))
proc edit(doc: Draft, content: string): Draft {.notATransition.} =
## Edit the document content.
var d = doc.Document
d.content = content
echo " [DOC] Updated content (", content.len, " chars)"
result = Draft(d)
proc setTitle(doc: Draft, title: string): Draft {.notATransition.} =
## Update the document title.
var d = doc.Document
d.title = title
echo " [DOC] Title changed to: '", title, "'"
result = Draft(d)
# ============================================================================
# Review Process
# ============================================================================
proc submitForReview(doc: Draft, reviewers: seq[string]): InReview {.transition.} =
## Submit document for review.
var d = doc.Document
d.reviewers = reviewers
echo " [DOC] Submitted for review"
echo " [DOC] Reviewers: ", reviewers
result = InReview(d)
proc approve(doc: InReview, approver: string): Approved {.transition.} =
## Approve the document.
var d = doc.Document
d.approver = approver
echo " [DOC] Approved by: ", approver
result = Approved(d)
proc requestChanges(doc: InReview, feedback: string): ChangesRequested {.transition.} =
## Request changes to the document.
echo " [DOC] Changes requested: ", feedback
result = ChangesRequested(doc.Document)
proc updateContent(doc: ChangesRequested, newContent: string): ChangesRequested {.notATransition.} =
## Update content while in ChangesRequested state.
var d = doc.Document
d.content = newContent
echo " [DOC] Content updated (", newContent.len, " chars)"
result = ChangesRequested(d)
proc resubmit(doc: ChangesRequested): InReview {.transition.} =
## Resubmit after making changes.
echo " [DOC] Resubmitted for review"
result = InReview(doc.Document)
# ============================================================================
# Publishing
# ============================================================================
proc publish(doc: Approved): Published {.transition.} =
## Publish the approved document.
var d = doc.Document
d.publishedAt = 1234567890 # In real code: current timestamp
echo " [DOC] Published! '", d.title, "'"
result = Published(d)
proc archive(doc: Published, reason: string): Archived {.transition.} =
## Archive a published document.
echo " [DOC] Archived: ", reason
result = Archived(doc.Document)
proc createNewVersion(doc: Published): Draft {.transition.} =
## Create a new draft version from published.
var d = doc.Document
d.version += 1
d.approver = ""
d.publishedAt = 0
echo " [DOC] New version ", d.version, " created from published"
result = Draft(d)
proc restore(doc: Archived): Draft {.transition.} =
## Restore archived document as new draft.
var d = doc.Document
d.version += 1
echo " [DOC] Restored as version ", d.version
result = Draft(d)
# ============================================================================
# Status Queries
# ============================================================================
func title(doc: DocumentStates): string =
doc.Document.title
func author(doc: DocumentStates): string =
doc.Document.author
func version(doc: DocumentStates): int =
doc.Document.version
func isPublished(doc: Published): bool = true
# ============================================================================
# Example Usage
# ============================================================================
when isMainModule:
echo "=== Document Workflow Demo ===\n"
echo "1. Creating new document..."
let draft = newDocument("Q4 Strategy Document", "alice@company.com")
.edit("# Q4 Strategy\n\nOur goals for Q4 are...")
.setTitle("Q4 2024 Strategy Document")
echo "\n2. Submitting for review..."
let review = draft.submitForReview(@["bob@company.com", "carol@company.com"])
echo "\n3. Reviewer requests changes..."
let needsChanges = review.requestChanges("Please add budget section")
echo "\n4. Author makes changes and resubmits..."
let resubmitted = needsChanges.resubmit()
echo "\n5. Document approved..."
let approved = resubmitted.approve("carol@company.com")
echo "\n6. Publishing document..."
let published = approved.publish()
echo "\n7. Later, creating new version for updates..."
let v2Draft = published.createNewVersion()
let v2 = v2Draft.edit("# Q4 2024 Strategy\n\nUpdated with Q3 results...")
echo "\n=== Workflow complete! ===\n"
# =========================================================================
# COMPILE-TIME ERRORS - These process violations are prevented:
# =========================================================================
echo "The following process violations are caught at COMPILE TIME:\n"
# BUG 1: Publishing without approval
# let bad1 = draft.publish()
echo " [PREVENTED] publish() Draft - must be approved first!"
# BUG 2: Publishing during review
# let bad2 = review.publish()
echo " [PREVENTED] publish() InReview - review not complete!"
# BUG 3: Editing published content
# let bad3 = published.edit("hacked content")
echo " [PREVENTED] edit() Published - audit violation!"
# BUG 4: Approving draft (skipping review)
# let bad4 = draft.approve("alice")
echo " [PREVENTED] approve() Draft - must go through review!"
# BUG 5: Double-publishing
# let bad5 = published.publish()
echo " [PREVENTED] publish() Published - already live!"
# BUG 6: Requesting changes on approved doc
# let bad6 = approved.requestChanges("wait, one more thing")
echo " [PREVENTED] requestChanges() Approved - too late!"
echo "\nUncomment any of the 'bad' lines to see the compile error!"
Single-Use Token (Ownership)¶
Some resources should only be used once: password reset tokens, one-time payment links, event tickets. This example uses consumeOnTransition = true (the default) to enforce that tokens cannot be copied or reused.
Bugs prevented: Double consumption, copying to bypass single-use, using after consumption.
## Single-Use Token with Ownership Enforcement
##
## Some resources should only be used once:
## - Password reset tokens
## - One-time payment links
## - Single-use API keys
## - Event tickets
##
## This example uses consumeOnTransition = true (the default) to enforce
## that tokens cannot be copied or reused after consumption.
##
## Run: nim c -r examples/single_use_token.nim
import ../src/typestates
type
Token = object
id: string
value: string
createdAt: string
# Token states
Valid = distinct Token ## Token is valid, can be used
Used = distinct Token ## Token has been consumed
Expired = distinct Token ## Token has expired
Revoked = distinct Token ## Token was manually revoked
typestate Token:
# DEFAULT: consumeOnTransition = true
# This enforces ownership - tokens cannot be copied after creation.
# Each token can only follow ONE path through the state machine.
states Valid, Used, Expired, Revoked
initial: Valid
terminal: Used
transitions:
Valid -> Used # Consume the token
Valid -> Expired # Token expires
Valid -> Revoked # Token is revoked
# ============================================================================
# Token Operations
# ============================================================================
proc createToken(id: string, value: string): Valid =
## Create a new single-use token.
echo " [TOKEN] Created: ", id
Valid(Token(id: id, value: value, createdAt: "now"))
proc consume(token: sink Valid): Used {.transition.} =
## Use the token (one-time only).
## sink + consumeOnTransition = true prevents copying, enforcing single-use.
let t = Token(token)
echo " [TOKEN] Consumed: ", t.id
Used(t)
proc expire(token: Valid): Expired {.transition.} =
## Mark token as expired.
echo " [TOKEN] Expired: ", Token(token).id
Expired(Token(token))
proc revoke(token: Valid): Revoked {.transition.} =
## Revoke the token.
echo " [TOKEN] Revoked: ", Token(token).id
Revoked(Token(token))
proc getValue(token: Valid): string {.notATransition.} =
## Read the token value (only when valid).
Token(token).value
# ============================================================================
# Example: Password Reset Token
# ============================================================================
when isMainModule:
echo "=== Single-Use Token Demo ===\n"
echo "1. Creating and immediately consuming a password reset token..."
# With consumeOnTransition = true, tokens flow directly through transitions
let usedToken = createToken("reset-abc123", "secret-reset-value").consume()
echo "\n=== Token consumed! ===\n"
# =========================================================================
# COMPILE-TIME ERRORS - Ownership enforcement prevents these bugs:
# =========================================================================
echo "The following bugs are caught at COMPILE TIME:\n"
# BUG 1: Storing a token and then trying to use it twice
# let token = createToken("test", "value")
# let used1 = token.consume()
# let used2 = token.consume() # ERROR: token was already moved
echo " [PREVENTED] Double consumption of token"
# BUG 2: Copying token to bypass single-use
# let token = createToken("test", "value")
# let backup = token # ERROR: =copy is not available for Valid
echo " [PREVENTED] Copying token to bypass single-use"
# BUG 3: Reading token value then consuming (uses token twice)
# let token = createToken("test", "value")
# echo token.getValue()
# discard token.consume() # ERROR: token was not last read in getValue()
echo " [PREVENTED] Reading token value prevents later consumption"
# BUG 4: Using terminal state
# let reused = usedToken.consume() # ERROR: Used is a terminal state
echo " [PREVENTED] Transitioning from terminal state"
echo "\nUncomment any of the 'bad' lines above to see the compile error!"
echo "\n=== Ownership enforcement ensures tokens are truly single-use ==="
Shared Session (ref types)¶
When multiple parts of code need access to the same stateful object, use ref types. Common for session objects, connection pools, and shared resources in async code.
Bugs prevented: Operations on wrong session state, accessing expired sessions.
## Shared Session with ref Types
##
## When multiple parts of your code need access to the same stateful object,
## use `ref` types. This is common for:
## - Session objects shared across handlers
## - Connection pools
## - Shared resources in async code
##
## This example shows how typestates work with heap-allocated ref types.
##
## Run: nim c -r examples/shared_session.nim
import ../src/typestates
type
Session = object
id: string
userId: int
data: string
# Session states
Unauthenticated = distinct Session
Authenticated = distinct Session
Expired = distinct Session
typestate Session:
# Shared sessions need to be read from multiple places
consumeOnTransition = false
states Unauthenticated, Authenticated, Expired
transitions:
Unauthenticated -> Authenticated
Authenticated -> Expired
# ============================================================================
# Session Operations (ref types)
# ============================================================================
proc newSession(id: string): ref Unauthenticated =
## Create a new heap-allocated session.
result = new(Unauthenticated)
result[] = Unauthenticated(Session(id: id))
echo " [SESSION] Created: ", id
proc authenticate(session: ref Unauthenticated, userId: int): ref Authenticated {.transition.} =
## Authenticate the session - works with ref types.
var s = Session(session[])
s.userId = userId
result = new(Authenticated)
result[] = Authenticated(s)
echo " [SESSION] Authenticated user ", userId
proc expire(session: ref Authenticated): ref Expired {.transition.} =
## Expire the session.
result = new(Expired)
result[] = Expired(Session(session[]))
echo " [SESSION] Expired"
proc setData(session: ref Authenticated, data: string) =
## Modify session data (only when authenticated).
var s = Session(session[])
s.data = data
session[] = Authenticated(s)
echo " [SESSION] Data set: ", data
proc getData(session: ref Authenticated): string =
## Read session data.
Session(session[]).data
proc getUserId(session: ref Authenticated): int =
## Get the authenticated user ID.
Session(session[]).userId
# ============================================================================
# Example: Multiple references to same session
# ============================================================================
when isMainModule:
echo "=== Shared Session Demo (ref types) ===\n"
echo "1. Creating session..."
let session = newSession("sess-abc123")
echo "\n2. Authenticating..."
let authSession = session.authenticate(42)
echo "\n3. Multiple parts of code can access the same session..."
# Simulate different parts of the application using the session
authSession.setData("user preferences")
echo " Handler A reads: ", authSession.getData()
echo " Handler B reads user: ", authSession.getUserId()
echo "\n4. Expiring session..."
let expiredSession = authSession.expire()
echo "\n=== Session lifecycle complete! ===\n"
# =========================================================================
# COMPILE-TIME ERRORS
# =========================================================================
echo "The following bugs are caught at COMPILE TIME:\n"
# BUG 1: Setting data on unauthenticated session
# session.setData("hack") # ERROR: no matching proc for ref Unauthenticated
echo " [PREVENTED] setData() on unauthenticated session"
# BUG 2: Getting user ID from expired session
# echo expiredSession.getUserId() # ERROR: no matching proc for ref Expired
echo " [PREVENTED] getUserId() on expired session"
# BUG 3: Authenticating already-authenticated session
# discard authSession.authenticate(99) # ERROR: no matching proc
echo " [PREVENTED] authenticate() on already-authenticated session"
echo "\nRef types work seamlessly with typestates!"
Hardware Register (ptr types)¶
When interfacing with hardware or memory-mapped I/O, typestates can enforce correct access patterns with raw pointers.
Bugs prevented: Reading uninitialized registers, modifying locked registers.
## Hardware Register Access with ptr Types
##
## When interfacing with hardware or memory-mapped I/O, you often work with
## raw pointers. Typestates can enforce correct access patterns:
## - Registers must be initialized before use
## - Some registers are read-only after configuration
## - Certain sequences must be followed
##
## This example shows how typestates work with ptr types for low-level code.
##
## Run: nim c -r examples/hardware_register.nim
import std/strutils
import ../src/typestates
type
Register = object
address: uint32
value: uint32
# Register states
Uninitialized = distinct Register
Configured = distinct Register
Locked = distinct Register
typestate Register:
# Hardware registers are accessed via pointers
consumeOnTransition = false
states Uninitialized, Configured, Locked
transitions:
Uninitialized -> Configured
Configured -> Configured # Can reconfigure
Configured -> Locked # Lock to prevent further changes
# ============================================================================
# Register Operations (ptr types)
# ============================================================================
proc initRegister(reg: ptr Uninitialized, value: uint32): ptr Configured {.transition.} =
## Initialize a hardware register with a value.
echo " [REG 0x", reg[].Register.address.toHex, "] Init: 0x", value.toHex
var r = Register(reg[])
r.value = value
reg[] = Uninitialized(r)
cast[ptr Configured](../../examples/reg)
proc configure(reg: ptr Configured, value: uint32): ptr Configured {.transition.} =
## Reconfigure a register (only when not locked).
echo " [REG 0x", reg[].Register.address.toHex, "] Configure: 0x", value.toHex
var r = Register(reg[])
r.value = value
reg[] = Configured(r)
reg
proc lock(reg: ptr Configured): ptr Locked {.transition.} =
## Lock the register to prevent further modifications.
echo " [REG 0x", reg[].Register.address.toHex, "] LOCKED"
cast[ptr Locked](../../examples/reg)
proc read(reg: ptr Configured): uint32 =
## Read value from configured register.
Register(reg[]).value
proc read(reg: ptr Locked): uint32 =
## Read value from locked register.
Register(reg[]).value
# ============================================================================
# Example: GPIO Configuration
# ============================================================================
when isMainModule:
echo "=== Hardware Register Demo (ptr types) ===\n"
# Simulate memory-mapped registers
var gpioModeReg = Uninitialized(Register(address: 0x4002_0000'u32, value: 0))
var gpioSpeedReg = Uninitialized(Register(address: 0x4002_0008'u32, value: 0))
echo "1. Initializing GPIO registers..."
let modePtr = addr(gpioModeReg).initRegister(0x0000_0001) # Output mode
let speedPtr = addr(gpioSpeedReg).initRegister(0x0000_0003) # High speed
echo "\n2. Reading configured values..."
echo " Mode register: 0x", modePtr.read().toHex
echo " Speed register: 0x", speedPtr.read().toHex
echo "\n3. Reconfiguring mode register..."
let modePtr2 = modePtr.configure(0x0000_0002) # Alternate function mode
echo " New mode: 0x", modePtr2.read().toHex
echo "\n4. Locking speed register..."
let lockedSpeed = speedPtr.lock()
echo " Locked value: 0x", lockedSpeed.read().toHex
echo "\n=== Register configuration complete! ===\n"
# =========================================================================
# COMPILE-TIME ERRORS
# =========================================================================
echo "The following bugs are caught at COMPILE TIME:\n"
# BUG 1: Reading uninitialized register
# var uninit = Uninitialized(Register(address: 0xDEAD, value: 0))
# echo addr(uninit).read() # ERROR: no matching proc for ptr Uninitialized
echo " [PREVENTED] read() on uninitialized register"
# BUG 2: Configuring locked register
# discard lockedSpeed.configure(0xFF) # ERROR: no matching proc for ptr Locked
echo " [PREVENTED] configure() on locked register"
# BUG 3: Locking uninitialized register
# var uninit2 = Uninitialized(Register(address: 0xBEEF, value: 0))
# discard addr(uninit2).lock() # ERROR: no matching proc for ptr Uninitialized
echo " [PREVENTED] lock() on uninitialized register"
echo "\nPtr types enable type-safe hardware access!"
Generic Patterns¶
Reusable typestate patterns using generics. See Generic Typestates for more details.
Resource[T] Pattern¶
A reusable pattern for any resource requiring acquire/release semantics.
## Generic Resource[T] Pattern
##
## A reusable typestate pattern for any resource that must be acquired
## before use and released after. Works with file handles, locks,
## connections, memory allocations, or any RAII-style resource.
##
## Run: nim c -r examples/generic_resource.nim
import ../src/typestates
# =============================================================================
# Generic Resource Pattern
# =============================================================================
type
Resource*[T] = object
## Base type holding any resource.
handle*: T
name*: string # For diagnostics
Released*[T] = distinct Resource[T]
## Resource is not held - cannot be used.
Acquired*[T] = distinct Resource[T]
## Resource is held - can be used, must be released.
typestate Resource[T]:
# Resources can be acquired, released, and re-acquired (RAII pattern).
consumeOnTransition = false
states Released[T], Acquired[T]
transitions:
Released[T] -> Acquired[T]
Acquired[T] -> Released[T]
proc acquire*[T](../../examples/r: Released[T], handle: T): Acquired[T] {.transition.} =
## Acquire the resource with the given handle.
var res = Resource[T](../../examples/r)
res.handle = handle
echo "[", res.name, "] Acquired"
result = Acquired[T](../../examples/res)
proc release*[T](../../examples/r: Acquired[T]): Released[T] {.transition.} =
## Release the resource back.
echo "[", Resource[T](../../examples/r).name, "] Released"
result = Released[T](../../examples/Resource[T](r))
proc use*[T](../../examples/r: Acquired[T]): T {.notATransition.} =
## Access the underlying handle (only when acquired).
Resource[T](../../examples/r).handle
proc withResource*[T, R](r: Released[T], handle: T,
body: proc(h: T): R): (R, Released[T]) =
## RAII-style helper: acquire, use, release automatically.
let acquired = r.acquire(handle)
let res = body(acquired.use())
let released = acquired.release()
result = (res, released)
# =============================================================================
# Example 1: File Handle
# =============================================================================
type FileHandle = object
fd: int
path: string
proc openFile(path: string): FileHandle =
echo " Opening: ", path
FileHandle(fd: 42, path: path)
proc closeFile(fh: FileHandle) =
echo " Closing: ", fh.path
proc readFile(fh: FileHandle): string =
echo " Reading from fd=", fh.fd
"file contents"
block fileExample:
echo "\n=== File Handle Example ==="
# Create a released resource
var file = Released[FileHandle](../../examples/Resource[FileHandle](name: "config.txt"))
# Acquire it
let handle = openFile("/etc/config.txt")
let acquired = file.acquire(handle)
# Use it
let contents = acquired.use().readFile()
echo " Got: ", contents
# Release it
let released = acquired.release()
closeFile(handle)
# COMPILE ERROR if uncommented:
# discard released.use() # Can't use released resource!
# =============================================================================
# Example 2: Database Connection
# =============================================================================
type DbConn = object
connString: string
connected: bool
proc connect(connString: string): DbConn =
echo " Connecting to: ", connString
DbConn(connString: connString, connected: true)
proc disconnect(conn: DbConn) =
echo " Disconnecting"
proc query(conn: DbConn, sql: string): seq[string] =
echo " Query: ", sql
@["row1", "row2", "row3"]
block dbExample:
echo "\n=== Database Connection Example ==="
var db = Released[DbConn](../../examples/Resource[DbConn](name: "postgres"))
# Manual acquire/release
let conn = connect("postgresql://localhost/mydb")
let acquired = db.acquire(conn)
let rows = acquired.use().query("SELECT * FROM users")
echo " Results: ", rows
let released = acquired.release()
disconnect(conn)
# =============================================================================
# Example 3: Lock/Mutex simulation
# =============================================================================
type SimpleLock = object
id: int
proc lock(id: int): SimpleLock =
echo " Locking mutex #", id
SimpleLock(id: id)
proc unlock(l: SimpleLock) =
echo " Unlocking mutex #", l.id
block lockExample:
echo "\n=== Lock Example ==="
var mutex = Released[SimpleLock](../../examples/Resource[SimpleLock](name: "mutex"))
# Using withResource for RAII-style usage
let (result, mutexReleased) = mutex.withResource(lock(1)) do (l: SimpleLock) -> int:
echo " Critical section with lock #", l.id
42 # Return value from critical section
echo " Result from critical section: ", result
unlock(SimpleLock(id: 1))
# =============================================================================
# Example 4: Memory Pool Allocation
# =============================================================================
type PooledBuffer = object
size: int
data: ptr UncheckedArray[byte]
proc allocFromPool(size: int): PooledBuffer =
echo " Allocating ", size, " bytes from pool"
# In real code, this would allocate from a pool
PooledBuffer(size: size, data: nil)
proc returnToPool(buf: PooledBuffer) =
echo " Returning ", buf.size, " bytes to pool"
block memoryExample:
echo "\n=== Memory Pool Example ==="
var buffer = Released[PooledBuffer](../../examples/Resource[PooledBuffer](name: "buffer"))
let mem = allocFromPool(4096)
let acquired = buffer.acquire(mem)
echo " Using buffer of size: ", acquired.use().size
let released = acquired.release()
returnToPool(mem)
# =============================================================================
# Summary
# =============================================================================
echo "\n=== Summary ==="
echo "The Resource[T] pattern ensures:"
echo " - Resources must be acquired before use"
echo " - Resources must be released after use"
echo " - Compile-time prevention of use-after-release"
echo " - Works with ANY resource type via generics"
echo "\nAll examples passed!"
Pipeline[T] Pattern¶
A reusable pattern for entities that progress through a fixed sequence of stages.
## Generic Pipeline[T] Pattern
##
## A reusable typestate pattern for entities that progress through
## a fixed sequence of stages. Works for orders, documents, builds,
## deployments, or any linear workflow.
##
## Run: nim c -r examples/generic_pipeline.nim
import ../src/typestates
# =============================================================================
# Generic Pipeline Pattern (4-stage)
# =============================================================================
type
Pipeline*[T] = object
## Base type holding the entity progressing through stages.
entity*: T
startedAt*: string
Stage1*[T] = distinct Pipeline[T]
## Initial stage - entity just entered the pipeline.
Stage2*[T] = distinct Pipeline[T]
## Second stage - first transition complete.
Stage3*[T] = distinct Pipeline[T]
## Third stage - nearing completion.
Stage4*[T] = distinct Pipeline[T]
## Final stage - pipeline complete.
typestate Pipeline[T]:
# Pipeline entities flow through stages and may be inspected at each stage.
consumeOnTransition = false
states Stage1[T], Stage2[T], Stage3[T], Stage4[T]
transitions:
Stage1[T] -> Stage2[T]
Stage2[T] -> Stage3[T]
Stage3[T] -> Stage4[T]
proc start*[T](entity: T, timestamp: string): Stage1[T] =
## Enter the pipeline at stage 1.
Stage1[T](../../examples/Pipeline[T](entity: entity, startedAt: timestamp))
proc advance12*[T](../../examples/p: Stage1[T]): Stage2[T] {.transition.} =
## Advance from stage 1 to stage 2.
Stage2[T](../../examples/Pipeline[T](p))
proc advance23*[T](../../examples/p: Stage2[T]): Stage3[T] {.transition.} =
## Advance from stage 2 to stage 3.
Stage3[T](../../examples/Pipeline[T](p))
proc advance34*[T](../../examples/p: Stage3[T]): Stage4[T] {.transition.} =
## Advance from stage 3 to stage 4 (complete).
Stage4[T](../../examples/Pipeline[T](p))
proc entity*[T](../../examples/p: Stage1[T]): T {.notATransition.} = Pipeline[T](../../examples/p).entity
proc entity*[T](../../examples/p: Stage2[T]): T {.notATransition.} = Pipeline[T](../../examples/p).entity
proc entity*[T](../../examples/p: Stage3[T]): T {.notATransition.} = Pipeline[T](../../examples/p).entity
proc entity*[T](../../examples/p: Stage4[T]): T {.notATransition.} = Pipeline[T](../../examples/p).entity
# =============================================================================
# Example 1: Order Fulfillment
# =============================================================================
type
Order = object
id: string
items: seq[string]
total: int
# Semantic aliases for order stages
OrderCart = Stage1[Order]
OrderPaid = Stage2[Order]
OrderShipped = Stage3[Order]
OrderDelivered = Stage4[Order]
proc addItem(cart: OrderCart, item: string, price: int): OrderCart {.notATransition.} =
var order = cart.entity()
order.items.add(item)
order.total += price
Stage1[Order](../../examples/Pipeline[Order](entity: order, startedAt: Pipeline[Order](cart).startedAt))
proc pay(cart: OrderCart): OrderPaid =
echo " Payment received: $", cart.entity().total
cart.advance12()
proc ship(order: OrderPaid, tracking: string): OrderShipped =
echo " Shipped with tracking: ", tracking
order.advance23()
proc deliver(order: OrderShipped): OrderDelivered =
echo " Delivered!"
order.advance34()
block orderExample:
echo "\n=== Order Fulfillment Example ==="
let cart = start(Order(id: "ORD-001"), "2024-01-15")
.addItem("Laptop", 999)
.addItem("Mouse", 29)
.addItem("Keyboard", 79)
echo " Cart total: $", cart.entity().total
let paid = cart.pay()
let shipped = paid.ship("1Z999AA10123456784")
let delivered = shipped.deliver()
echo " Order ", delivered.entity().id, " complete!"
# COMPILE ERRORS if uncommented:
# discard cart.ship("TRACK") # Can't ship unpaid cart
# discard shipped.pay() # Can't pay already-shipped order
# discard delivered.advance34() # Can't advance past final stage
# =============================================================================
# Example 2: CI/CD Build Pipeline
# =============================================================================
type
Build = object
repo: string
commit: string
artifacts: seq[string]
# Semantic aliases for build stages
BuildQueued = Stage1[Build]
BuildCompiling = Stage2[Build]
BuildTesting = Stage3[Build]
BuildDeployed = Stage4[Build]
proc startBuild(repo, commit: string): BuildQueued =
echo " Build queued for ", repo, "@", commit[0..6]
start(Build(repo: repo, commit: commit), "now")
proc compile(build: BuildQueued): BuildCompiling =
echo " Compiling..."
var b = build.entity()
b.artifacts.add("app.bin")
let pipeline = Pipeline[Build](entity: b, startedAt: Pipeline[Build](build).startedAt)
Stage2[Build](../../examples/pipeline)
proc test(build: BuildCompiling): BuildTesting =
echo " Running tests..."
build.advance23()
proc deploy(build: BuildTesting): BuildDeployed =
echo " Deploying artifacts: ", build.entity().artifacts
build.advance34()
block buildExample:
echo "\n=== CI/CD Build Pipeline Example ==="
let deployed = startBuild("github.com/user/project", "abc123def456")
.compile()
.test()
.deploy()
echo " Build complete for ", deployed.entity().repo
# COMPILE ERRORS:
# discard startBuild("repo", "commit").deploy() # Can't skip stages!
# =============================================================================
# Example 3: Document Review
# =============================================================================
type
Document = object
title: string
content: string
reviewer: string
approver: string
# Semantic aliases
DocDraft = Stage1[Document]
DocInReview = Stage2[Document]
DocApproved = Stage3[Document]
DocPublished = Stage4[Document]
proc createDraft(title: string): DocDraft =
start(Document(title: title), "draft-created")
proc edit(doc: DocDraft, content: string): DocDraft {.notATransition.} =
var d = doc.entity()
d.content = content
Stage1[Document](../../examples/Pipeline[Document](entity: d, startedAt: Pipeline[Document](doc).startedAt))
proc submitForReview(doc: DocDraft, reviewer: string): DocInReview =
var d = doc.entity()
d.reviewer = reviewer
echo " Submitted to ", reviewer, " for review"
let pipeline = Pipeline[Document](entity: d, startedAt: Pipeline[Document](doc).startedAt)
Stage2[Document](../../examples/pipeline)
proc approve(doc: DocInReview, approver: string): DocApproved =
var d = doc.entity()
d.approver = approver
echo " Approved by ", approver
let pipeline = Pipeline[Document](entity: d, startedAt: Pipeline[Document](doc).startedAt)
Stage3[Document](../../examples/pipeline)
proc publish(doc: DocApproved): DocPublished =
echo " Published: ", doc.entity().title
doc.advance34()
block docExample:
echo "\n=== Document Review Example ==="
let published = createDraft("Q4 Strategy")
.edit("Our goals for Q4 include...")
.submitForReview("alice@company.com")
.approve("bob@company.com")
.publish()
echo " Document '", published.entity().title, "' is live!"
# COMPILE ERRORS:
# discard createDraft("Doc").publish() # Can't publish without review!
# =============================================================================
# Example 4: Deployment Pipeline
# =============================================================================
type
Deployment = object
service: string
version: string
environment: string
# Semantic aliases
DeployStaging = Stage1[Deployment]
DeployCanary = Stage2[Deployment]
DeployPartial = Stage3[Deployment]
DeployFull = Stage4[Deployment]
proc deployToStaging(service, version: string): DeployStaging =
echo " Deploying ", service, " v", version, " to staging"
start(Deployment(service: service, version: version, environment: "staging"), "now")
proc promoteToCanary(d: DeployStaging): DeployCanary =
echo " Promoting to canary (1% traffic)"
var dep = d.entity()
dep.environment = "canary"
Stage2[Deployment](../../examples/Pipeline[Deployment](entity: dep, startedAt: "now"))
proc expandToPartial(d: DeployCanary): DeployPartial =
echo " Expanding to 25% traffic"
var dep = d.entity()
dep.environment = "partial"
Stage3[Deployment](../../examples/Pipeline[Deployment](entity: dep, startedAt: "now"))
proc rolloutFull(d: DeployPartial): DeployFull =
echo " Full rollout to 100% traffic"
var dep = d.entity()
dep.environment = "production"
Stage4[Deployment](../../examples/Pipeline[Deployment](entity: dep, startedAt: "now"))
block deployExample:
echo "\n=== Deployment Pipeline Example ==="
let production = deployToStaging("api-server", "2.3.0")
.promoteToCanary()
.expandToPartial()
.rolloutFull()
echo " ", production.entity().service, " v", production.entity().version,
" is now in ", production.entity().environment
# =============================================================================
# Summary
# =============================================================================
echo "\n=== Summary ==="
echo "The Pipeline[T] pattern ensures:"
echo " - Entities must progress through stages in order"
echo " - No stage can be skipped"
echo " - Operations are only valid at appropriate stages"
echo " - Works with ANY entity type via generics"
echo ""
echo "Common applications:"
echo " - Order fulfillment (Cart -> Paid -> Shipped -> Delivered)"
echo " - CI/CD builds (Queue -> Compile -> Test -> Deploy)"
echo " - Document review (Draft -> Review -> Approve -> Publish)"
echo " - Canary deployments (Staging -> Canary -> Partial -> Full)"
echo "\nAll examples passed!"
Tips for Designing Typestates¶
1. Start with the State Diagram¶
Draw your states and transitions first. Each arrow becomes a transition declaration.
2. One Responsibility Per State¶
Each state should represent one clear condition. If a state has multiple meanings, split it.
3. Use Wildcards Sparingly¶
* -> X is powerful but can hide bugs. Use it only for truly universal operations like "reset" or "emergency stop".
4. Consider Error States¶
Many real systems need error/failure states. Plan for them upfront.
5. Document State Meanings¶
Even with types enforcing transitions, document what each state means: