DSL Reference¶
Complete reference for the nim-typestates DSL syntax.
Typestate Block¶
States Declaration¶
List all state types that participate in this typestate:
Or use multiline format for readability:
Each state must be a distinct type of the base type:
Transitions Block¶
Declare valid state transitions using -> syntax:
Transition Syntax¶
Simple Transitions¶
One source state to one destination:
Branching Transitions¶
One source state to multiple possible destinations using |, with a required result type name:
The as TypeName syntax names the generated branch type. This is required for all branching transitions.
This means a proc taking Closed can return either Open or Errored, wrapped in an OpenResult variant type.
Wildcard Transitions¶
Any state can transition to a destination using *:
Wildcards are useful for "reset" or "cleanup" operations that work from any state.
Initial and Terminal States¶
Declare entry and exit points for your typestate:
Initial States¶
States that can only be constructed, not transitioned TO:
Or multiple:
Attempting to transition TO an initial state produces a compile-time error:
Error: Cannot transition TO initial state 'Disconnected'.
Initial states can only be constructed, not transitioned to.
Terminal States¶
States that cannot transition FROM (end states):
Or multiple:
Attempting to transition FROM a terminal state produces a compile-time error:
Error: Cannot transition FROM terminal state 'Closed'.
Terminal states are end states with no outgoing transitions.
Complete Example¶
typestate Connection:
states Disconnected, Connected, Closed
initial: Disconnected
terminal: Closed
transitions:
Disconnected -> Connected
Connected -> Closed
Flags¶
Configuration options for the typestate. Flags can appear anywhere in the block.
strictTransitions¶
When true (default), all procs that take a state type must be marked with either {.transition.} or {.notATransition.}.
typestate File:
strictTransitions = false # Allow unmarked procs
states Closed, Open
transitions:
Closed -> Open
consumeOnTransition¶
When true (default), generates =copy hooks that prevent copying state values. This enforces ownership semantics - each state value can only be used once.
typestate File:
consumeOnTransition = false # Allow copying states
states Closed, Open
transitions:
Closed -> Open
When consumeOnTransition = true (default):
State values must be consumed, not copied. Use these patterns:
-
Chain operations directly (preferred):
-
Use
move()for branch types: -
Use
sinkparameters in your transition procs (already generated by the macro).
Common errors with consumeOnTransition = true:
'=copy' is not available for type <State>: You're trying to copy a state value. Solutions:- Use
move()to explicitly consume the value - Chain operations without intermediate variables
-
If you need to reuse values, set
consumeOnTransition = false(see below) -
expression is immutable, not 'var': Usingmove()on aletbinding. Change tovar. -
'=copy' is not available... requires a copy because it's not the last readwhen calling$xbefore a transition: The generated$proc reads the value by copy, soecho $xfollowed byx.transition()triggers a copy hook error on the second read. The same applies tostate(x). Use one of: - Print after the final transition:
let final = x.transition(); echo $final - Print the state enum constant directly:
echo $fsClosed(no value read) - Set
consumeOnTransition = falseon the typestate if you need to inspect values without consuming them
When to use consumeOnTransition = false:
Set consumeOnTransition = false when:
- Value wrappers: Validated values that are stored and reused multiple times (e.g., validated integers, range-checked values, parsed configurations)
- Shared state: States that need to be read by multiple consumers
- Debugging: Temporarily disable to simplify debugging, then re-enable
Implications of consumeOnTransition = false:
- States can be copied freely (no ownership enforcement)
- A state value can be used multiple times
- No compile-time protection against "use after transition"
- Suitable for data validation, not operation sequencing
Recommendation: Use consumeOnTransition = true (default) for operation-based typestates that model workflows, protocols, or resource lifecycles. Use consumeOnTransition = false for value-validation typestates.
Cross-module bridging: When state values are passed between typestates in different modules, use consumeOnTransition = false on both typestates. See Bridges: Cross-Module Considerations.
opaqueStates¶
When true, enables a CLI-side lint that warns on raw distinct casts that construct non-initial states outside of {.transition.} proc bodies. Default is false. The flag is opt-in and has zero behavioral impact at compile time when off.
typestate Payment:
opaqueStates = true
states Created, Authorized, Captured
initial Created
transitions:
Created -> Authorized
Authorized -> Captured
The lint runs as part of typestates verify and emits warnings, not errors. Promote the warnings to errors in CI if you want hard enforcement. An initial: declaration is required when opaqueStates = true; otherwise the lint emits a configuration warning and skips the typestate.
See Cast Protection for the full caught/missed table, known false-positive sources, and limitations.
inheritsFromRootObj¶
Set to true if your base type inherits from RootObj. This suppresses a compile-time error for static generic typestates on Nim < 2.2.8.
type
Buffer[N: static int] = object of RootObj # Note: inherits from RootObj
data: array[N, byte]
Empty[N: static int] = distinct Buffer[N]
Full[N: static int] = distinct Buffer[N]
typestate Buffer[N: static int]:
inheritsFromRootObj = true # Required for static generics with RootObj on Nim < 2.2.8
states Empty[N], Full[N]
transitions:
Empty[N] -> Full[N]
See the Nim codegen bug for details.
Bridges¶
Cross-typestate transitions declared with dotted notation.
Syntax¶
bridges:
SourceState -> DestTypestate.DestState
SourceState -> module.DestTypestate.DestState # Module-qualified
SourceState -> (DestTypestate.State1 | DestTypestate.State2) # Branching
* -> DestTypestate.DestState # Wildcard
Requirements¶
- Destination typestate must be imported
- Destination typestate and state must exist
- Bridge must be declared before implementation
Examples¶
Simple bridge:
Module-qualified bridge:
Branching bridge:
Wildcard bridge:
See Bridges for full documentation.
Pragmas¶
{.transition.}¶
Mark a proc as a state transition. The compiler validates that the transition is declared.
Validation rules:
- First parameter must be a registered state type (plain, a generic instance, or a union — see below)
- Return type must be a valid transition target (plain, a union, or a registered transparent wrapper — see below)
- Transition must be declared in the typestate block
- Automatically gets
{.raises: [].}added if not specified - errors should be states, not exceptions - If you explicitly write
{.raises: [SomeError].}, compilation will fail
See Error Handling for patterns on modeling errors as states.
Error on invalid transition:
Error: Undeclared transition: Open -> Locked
Typestate 'File' does not declare this transition.
Valid transitions from 'Open': @["Closed"]
Hint: Add 'Open -> Locked' to the transitions block.
Union Source Parameters¶
When multiple source states share a common transition target, a single proc can accept a union of source types. Every branch of the union is validated against the transition graph independently; if any is missing an edge to the declared destination, the diagnostic names the specific failing source.
typestate Order:
states Open, PartiallyFilled, Cancelling
transitions:
Open -> Cancelling
PartiallyFilled -> Cancelling
# One proc covers both Open -> Cancelling and PartiallyFilled -> Cancelling.
proc cancel(o: Open | PartiallyFilled): Cancelling {.transition.} =
Cancelling(Order(o))
Modifiers and parentheses are peeled before the union split, so
sink (A | B), var (A | B), and (A | B) all work.
Transparent-Wrapper Return Types¶
Procs returning Result[T, E], Option[T], or Future[T] (and
combinations) have the wrapper transparently unwrapped when the
destination state is looked up:
import results
proc preCheck(p: Proposed): Result[PreChecked, string] {.transition.} =
ok(PreChecked(Order(p)))
The pragma validates Proposed -> PreChecked, not a transition to
the opaque "Result". For async procs, {.async, transition.} with
Future[T] and Future[Result[T, E]] returns is fully supported.
You can also register custom wrappers. See
Transparent Wrappers for full details.
{.notATransition.}¶
Mark a proc as intentionally NOT a transition. Use for procs that operate on state types but don't change state:
proc write(f: Open, data: string) {.notATransition.} =
# Writes data but stays in Open state
rawWrite(f.File.handle, data)
proc read(f: Open, count: int): string {.notATransition.} =
# Reads data but stays in Open state
result = rawRead(f.File.handle, count)
For pure functions (no side effects), use func instead - no pragma needed:
Generated Types¶
For typestate File: with states Closed, Open, Errored:
State Enum¶
Enum values are prefixed with fs (for "file state") to avoid name collisions.
Union Type¶
Useful for generic procs that accept any state:
proc describe[S: FileStates](f: S): string =
case f.state
of fsClosed: "closed"
of fsOpen: "open"
of fsErrored: "errored"
State Procs¶
proc state*(f: Closed): FileState = fsClosed
proc state*(f: Open): FileState = fsOpen
proc state*(f: Errored): FileState = fsErrored
Branch Types¶
For branching transitions like Created -> (Approved | Declined | Review) as ProcessResult, the macro generates types and helpers for returning multiple possible states.
Usage with the -> operator:
proc process(c: Created): ProcessResult {.transition.} =
if c.Payment.amount > 100:
ProcessResult -> Approved(c.Payment)
elif c.Payment.amount > 50:
ProcessResult -> Review(c.Payment)
else:
ProcessResult -> Declined(c.Payment)
The -> operator takes the branch type on the left and the destination state on the right. This mirrors the DSL syntax and is unambiguous even when the same state appears in multiple branch types.
Pattern matching on the result:
let result = process(created)
case result.kind
of pApproved: echo "Approved: ", result.approved.Payment.amount
of pDeclined: echo "Declined"
of pReview: echo "Needs review"
Pattern matching with match:
Each branching union gets an auto-generated match macro that dispatches on
the variant kind with compile-time exhaustiveness checking.
var r = payment.capture()
match r:
Settled(s): echo "settled: ", s.Payment.id
PartiallyRefunded(p): echo "partial: ", p.Payment.id
FullyRefunded(f): echo "refunded: ", f.Payment.id
Three constraints apply:
- The matched value must be
var. Branch fields are extracted withmove(), which requires a mutable binding. - Use
StateName(bind):, notof StateName as bind:. The latter is a parser-level error and is not emitted by the macro. - All branches must be covered. The macro errors at compile time if a
branch is missing or names a state outside the union — there is no
else:escape hatch.
match works at module top level and inside the body of generic procs and
generic templates; arm-head identifiers may be resolved to symbols or
sym-choices by sema and the macro accepts all of those forms.
What gets generated:
The -> operator is syntactic sugar around constructor procs. For each branching transition with as TypeName, the macro generates:
-
Enum -
ProcessResultKind = enum pApproved, pDeclined, pReview(prefix is first letter of type name, e.g.,pfor ProcessResult) -
Variant object -
ProcessResultholding the result -
Constructor procs -
toProcessResult(s: Approved): ProcessResultetc. -
->operator -template ->(T: typedesc[ProcessResult], s: Approved)etc.
You can use the constructors directly if preferred:
See Returning Union Types for more examples.
Complete Example¶
import typestates
type
Connection = object
host: string
port: int
socket: int
Disconnected = distinct Connection
Connecting = distinct Connection
Connected = distinct Connection
Errored = distinct Connection
typestate Connection:
states Disconnected, Connecting, Connected, Errored
transitions:
Disconnected -> Connecting
Connecting -> (Connected | Errored) as ConnectResult
Connected -> Disconnected
Errored -> Disconnected
* -> Disconnected # Can always disconnect
proc connect(c: Disconnected, host: string, port: int): Connecting {.transition.} =
var conn = c.Connection
conn.host = host
conn.port = port
result = Connecting(conn)
proc waitForConnection(c: Connecting): ConnectResult {.transition.} =
# In real code, this would do async I/O
if true: # Pretend success
ConnectResult -> Connected(c.Connection)
else:
ConnectResult -> Errored(c.Connection)
# Wildcard transitions require separate procs for each source state.
# The {.transition.} pragma validates each at compile time.
proc disconnect(c: Disconnected): Disconnected {.transition.} =
result = c # Already disconnected
proc disconnect(c: Connecting): Disconnected {.transition.} =
var conn = c.Connection
conn.socket = 0
result = Disconnected(conn)
proc disconnect(c: Connected): Disconnected {.transition.} =
var conn = c.Connection
conn.socket = 0
result = Disconnected(conn)
proc disconnect(c: Errored): Disconnected {.transition.} =
var conn = c.Connection
conn.socket = 0
result = Disconnected(conn)
proc send(c: Connected, data: string) {.notATransition.} =
# Send data, stay connected
discard
Tips¶
Accessing the Base Type¶
State types are distinct, so you need to convert to access fields:
Returning Union Types¶
For branching transitions, use the -> operator with the generated branch type:
# Branching transition: Connecting -> (Connected | Errored) as ConnectResult
proc waitForConnection(c: Connecting): ConnectResult {.transition.} =
if success:
ConnectResult -> Connected(c.Connection)
else:
ConnectResult -> Errored(c.Connection)
Then pattern match on the result:
let result = conn.waitForConnection()
case result.kind
of cConnected:
echo "Connected!"
sendData(result.connected)
of cErrored:
echo "Error: ", result.errored.message
Why branch types? Nim's A | B syntax creates a generic type constraint, not a runtime sum type. You cannot actually return different types from if/else branches. The generated branch types solve this by wrapping the result in an object variant.
Generic Over All States¶
Use the generated union type for generic procs: