Skip to content

API Reference

Auto-generated API documentation from source code.

Main Module

typestates

Compile-time state machine verification for Nim.

This library enforces state machine protocols at compile time through Nim's type system. Programs that compile have been verified to contain no invalid state transitions.

This approach is known as correctness by construction: invalid states become unrepresentable rather than checked at runtime.

Exports:

  • typestate macro - Declare states and transitions
  • {.transition.} pragma - Mark and validate transition procs
  • {.notATransition.} pragma - Mark non-transition procs

typestateImpl

macro typestateImpl(name: untyped; body: untyped; inferredConstraints: static[seq[tuple[name: string, kind: string,
                                      constraint: string]]]): untyped

Internal implementation that receives inferred constraints.

This macro is called after constraint inference to generate the typestate with properly constrained generic parameters.

Parameters
  • name (untyped)
  • body (untyped)
  • inferredConstraints (static[seq[tuple[name: string, kind: string, constraint: string]]])
Returns

untyped

typestate

macro typestate(name: untyped; body: untyped): untyped

Define a typestate with states and valid transitions.

The typestate block declares:

  • states: The distinct types that represent each state
  • transitions: Which state changes are allowed

Basic syntax:

typestate File:
  states Closed, Open, Errored
  transitions:
    Closed -> Open | Errored    # Branching
    Open -> Closed
    * -> Closed                 # Wildcard

Generic typestates with constraint inference:

type
  Base[N: static int] = object
  StateA[N: static int] = distinct Base[N]
  StateB[N: static int] = distinct Base[N]

# N's constraint is automatically inferred from StateA/StateB
typestate Base[N]:
  states StateA[N], StateB[N]
  transitions:
    StateA -> StateB

What it generates:

  • FileState enum with fsClosed, fsOpen, fsErrored
  • FileStates union type for generic procs
  • state() procs for runtime inspection

Transition syntax:

  • A -> B - Simple transition
  • A -> B | C - Branching (can go to B or C)
  • * -> X - Wildcard (any state can go to X)

See also: {.transition.} pragma for implementing transitions

To opt into state-aware error messages on transition misuse, call verifyTypestates() at the bottom of your module. See docs/guide/error-handling.md for details.

Parameters
  • name (untyped) – The base type name (must match your type definition)
  • body (untyped) – The states and transitions declarations
Returns

untyped – Generated helper types (enum, union, state procs)


Submodules

Types

Core type definitions.

types

Core type definitions for the typestate system.

This module defines the internal representation of typestates, states, and transitions used during compile-time validation.

These types are primarily used internally by the typestate macro and {.transition.} pragma. Most users won't interact with them directly.

extractBaseName

proc extractBaseName(stateRepr: string): string

Extract the base type name from a state repr string.

Used for comparing state names when generic parameters may differ:

  • "Empty" -> "Empty"
  • "Empty[T]" -> "Empty"
  • "Container[K, V]" -> "Container"
  • "ref Closed" -> "Closed"
Parameters
  • stateRepr (string) – Full state repr string
Returns

string – Base name without generic parameters

State

type State = object

Represents a single state in a typestate machine.

Each state corresponds to a distinct type that the user defines. States can be simple identifiers or generic types.

Examples:

  • Simple: name="Closed", fullRepr="Closed"
  • Generic: name="Container", fullRepr="Container[T]"
  • Ref type: name="Closed", fullRepr="ref Closed"

Fields

  • name string
  • fullRepr string
  • typeName NimNode

Transition

type Transition = object

Represents a valid state transition in the typestate graph.

Transitions define which state changes are allowed. They can be:

  • Simple: Closed -> Open (one source, one destination)
  • Branching: Closed -> (Open | Errored) as OpenResult (one source, multiple destinations)
  • Wildcard: * -> Closed (any state can transition to Closed)

Example:

# This DSL:
# Closed -> (Open | Errored) as OpenResult
# Becomes:
Transition(
  fromState: "Closed",
  toStates: @["Open", "Errored"],
  branchTypeName: "OpenResult",
  isWildcard: false
)

Fields

  • fromState string
  • toStates seq[string]
  • branchTypeName string – Empty for non-branching, required for branching
  • branchTypeNode NimNode – Raw AST node for codegen (supports generics like Result[T])
  • isWildcard bool
  • declaredAt LineInfo

Bridge

type Bridge = object

Represents a cross-typestate bridge declaration.

Bridges allow terminal states of one typestate to transition into states of a completely different typestate. They enable modeling resource transformation, wrapping, and protocol handoff.

Example:

# In AuthFlow typestate:
# bridges:
#   Authenticated -> Session.Active
# Becomes:
Bridge(
  fromState: "Authenticated",
  toModule: "",  # empty for same-module
  toTypestate: "Session",
  toState: "Active",
  fullDestRepr: "Session.Active"
)

# With module prefix:
# bridges:
#   SourceState -> othermodule.Session.Active
# Becomes:
Bridge(
  fromState: "SourceState",
  toModule: "othermodule",
  toTypestate: "Session",
  toState: "Active",
  fullDestRepr: "othermodule.Session.Active"
)

Fields

  • fromState string
  • toModule string
  • toTypestate string
  • toState string
  • fullDestRepr string
  • declaredAt LineInfo

TypestateGraph

type TypestateGraph = object

The complete graph of states and transitions for a typestate.

This is the central data structure that holds all information about a typestate declaration. It is built by the parser from the DSL syntax and stored in the compile-time registry for later validation.

Example:

typestate File:
  states Closed, Open
  transitions:
    Closed -> Open
    Open -> Closed

Creates a TypestateGraph with name="File", two states, and two transitions.

Fields

  • name string
  • typeParams seq[NimNode] – Generic params: @[T] or @[K, V] or @[]
  • states Table[string, State]
  • transitions seq[Transition]
  • bridges seq[Bridge]
  • strictTransitions bool
  • consumeOnTransition bool – If true, states cannot be copied
  • inheritsFromRootObj bool – If true, skip static generic bug check
  • opaqueStates bool – Opt-in cast-bypass lint (CLI-side only)
  • initialStates seq[string] – States that cannot be transitioned TO
  • terminalStates seq[string] – States that cannot transition FROM
  • declaredAt LineInfo
  • declaredInModule string

==

proc ==(a, b: Transition): bool

Compare two transitions for equality.

Two transitions are equal if they have the same source state, destination states, and wildcard status. The declaration location is not considered for equality.

Parameters
  • a (Transition) – First transition to compare
  • b (Transition) – Second transition to compare
Returns

bool – `true` if transitions are semantically equivalent

==

proc ==(a, b: Bridge): bool

Compare two bridges for equality.

Two bridges are equal if they have the same source state, destination module, destination typestate, and destination state. The declaration location is not considered for equality.

Parameters
  • a (Bridge) – First bridge to compare
  • b (Bridge) – Second bridge to compare
Returns

bool – `true` if bridges are semantically equivalent

hasTransition

proc hasTransition(graph: TypestateGraph; fromState, toState: string): bool

Check if a transition from fromState to toState is valid.

This proc checks both explicit transitions and wildcard transitions. A transition is valid if there's an explicit transition fromState -> toState, or there's a wildcard transition * -> toState.

Comparisons use base names to support generic types: - hasTransition(g, "Empty", "Full") matches Empty[T] -> Full[T]

Example:

# Given: Closed -> Open, * -> Closed
graph.hasTransition("Closed", "Open")   # true
graph.hasTransition("Open", "Closed")   # true (via wildcard)
graph.hasTransition("Closed", "Closed") # true (via wildcard)
graph.hasTransition("Open", "Open")     # false (not declared)
Parameters
  • graph (TypestateGraph) – The typestate graph to check
  • fromState (string) – The source state name (base name or full repr)
  • toState (string) – The destination state name (base name or full repr)
Returns

bool – `true` if the transition is allowed, `false` otherwise

validDestinations

proc validDestinations(graph: TypestateGraph; fromState: string): seq[string]

Get all valid destination states from a given state.

This includes both explicit transitions from fromState and destinations reachable via wildcard transitions.

Comparisons use base names to support generic types. Returns base names for clearer error messages.

Example:

# Given: Closed -> Open | Errored, * -> Closed
graph.validDestinations("Closed")  # @["Open", "Errored", "Closed"]
graph.validDestinations("Open")    # @["Closed"]
Parameters
  • graph (TypestateGraph) – The typestate graph to query
  • fromState (string) – The source state to check transitions from
Returns

seq[string] – A sequence of state base names that can be transitioned to

hasBridge

proc hasBridge(graph: TypestateGraph; fromState, toModule, toTypestate, toState: string): bool

Check if a bridge from fromState to toModule.toTypestate.toState is declared.

Comparisons use base names to support generic types. Module matching: both empty = match, both non-empty and equal = match.

Example:

# Given: Authenticated -> Session.Active
graph.hasBridge("Authenticated", "", "Session", "Active")  # true (same module)

# Given: StateA -> othermodule.Session.Active
graph.hasBridge("StateA", "othermodule", "Session", "Active")  # true
graph.hasBridge("StateA", "", "Session", "Active")  # false (module mismatch)
Parameters
  • graph (TypestateGraph) – The typestate graph to check
  • fromState (string) – The source state name
  • toModule (string) – The destination module name (empty string for same module)
  • toTypestate (string) – The destination typestate name
  • toState (string) – The destination state name
Returns

bool – `true` if the bridge is declared, `false` otherwise

hasBridge

proc hasBridge(graph: TypestateGraph; fromState, toTypestate, toState: string): bool

Check if a bridge from fromState to any *.toTypestate.toState is declared.

This version matches bridges regardless of the module prefix, making it suitable for validation where we only know the destination typestate and state.

Comparisons use base names to support generic types.

Example:

# Given: Authenticated -> Session.Active
graph.hasBridge("Authenticated", "Session", "Active")  # true

# Given: StateA -> mymodule.Session.Active
graph.hasBridge("StateA", "Session", "Active")  # true (matches regardless of module)
Parameters
  • graph (TypestateGraph) – The typestate graph to check
  • fromState (string) – The source state name
  • toTypestate (string) – The destination typestate name
  • toState (string) – The destination state name
Returns

bool – `true` if the bridge is declared, `false` otherwise

validBridges

proc validBridges(graph: TypestateGraph; fromState: string): seq[string]

Get all valid bridge destinations from a given state.

Returns dotted notation strings like "Session.Active" or "module.Session.Active".

Example:

# Given: Authenticated -> Session.Active, Failed -> othermodule.ErrorLog.Entry
graph.validBridges("Authenticated")  # @["Session.Active"]
graph.validBridges("Failed")         # @["othermodule.ErrorLog.Entry"]
Parameters
  • graph (TypestateGraph) – The typestate graph to query
  • fromState (string) – The source state to check bridges from
Returns

seq[string] – A sequence of dotted destination names

isInitialState

proc isInitialState(graph: TypestateGraph; stateName: string): bool

Check if a state is declared as initial.

Initial states can only be constructed, not transitioned to. Comparisons use base names to support generic types.

Example:

# Given: initial: Disconnected
graph.isInitialState("Disconnected")  # true
graph.isInitialState("Connected")     # false
Parameters
  • graph (TypestateGraph) – The typestate graph to check
  • stateName (string) – The state name to check
Returns

bool – `true` if the state is initial, `false` otherwise

isTerminalState

proc isTerminalState(graph: TypestateGraph; stateName: string): bool

Check if a state is declared as terminal.

Terminal states are end states with no outgoing transitions. Comparisons use base names to support generic types.

Example:

# Given: terminal: Closed
graph.isTerminalState("Closed")  # true
graph.isTerminalState("Open")    # false
Parameters
  • graph (TypestateGraph) – The typestate graph to check
  • stateName (string) – The state name to check
Returns

bool – `true` if the state is terminal, `false` otherwise


Parser

DSL parser for typestate blocks.

parser

Parser for the typestate DSL.

This module transforms the AST from a typestate macro invocation into a TypestateGraph structure. It handles parsing of:

  • State declarations (states Closed, Open, Errored)
  • Transition declarations (Closed -> Open | Errored)
  • Wildcard transitions (* -> Closed)

The parser operates at compile-time within macro context.

Internal module - most users won't interact with this directly.

parseStates

proc parseStates(graph: var TypestateGraph; node: NimNode)

Parse a states declaration and add states to the graph.

Accepts multiple syntax forms:

  • Inline: states Closed, Open, Errored
  • Multiline block:
    states:
      Closed
      Open
      Errored
    
  • Multiline with commas:
    states:
      Closed,
      Open,
      Errored
    

States can be any valid Nim type expression:

  • Simple identifiers: Closed, Open
  • Generic types: Container[T], Map[K, V]
  • Ref types: ref Closed
  • Qualified names: mymodule.State

Example AST inputs:

# Simple: states Closed, Open
Command
  Ident "states"
  Ident "Closed"
  Ident "Open"

# Generic: states Empty[T], Full[T]
Command
  Ident "states"
  BracketExpr
    Ident "Empty"
    Ident "T"
  BracketExpr
    Ident "Full"
    Ident "T"

# Multiline: states:
#             Closed
#             Open
Call
  Ident "states"
  StmtList
    Ident "Closed"
    Ident "Open"
Parameters
  • graph (var TypestateGraph) – The typestate graph to populate
  • node (NimNode) – AST node of the states declaration

parseTransition

proc parseTransition(node: NimNode): Transition

Parse a single transition declaration.

Supports three forms:

  • Simple: Closed -> Open
  • Branching: Closed -> Open | Errored
  • Wildcard: * -> Closed

Example AST for Closed -> Open | Errored:

Infix
  Ident "->"
  Ident "Closed"
  Infix
    Ident "|"
    Ident "Open"
    Ident "Errored"

Example AST for * -> Closed (wildcard parsed as nested prefix):

Prefix
  Ident "*"
  Prefix
    Ident "->"
    Ident "Closed"
Parameters
  • node (NimNode) – AST node of the transition expression
Returns

Transition – A `Transition` object

parseBridgesBlock

proc parseBridgesBlock(graph: var TypestateGraph; node: NimNode)

Parse the bridges block and add all bridges to the graph.

Example input:

bridges:
  Authenticated -> Session.Active
  Failed -> ErrorLog.Entry
  * -> Shutdown.Terminal
Parameters
  • graph (var TypestateGraph) – The typestate graph to populate
  • node (NimNode) – AST node of the bridges block

parseInitialBlock

proc parseInitialBlock(graph: var TypestateGraph; node: NimNode)

Parse the initial states block.

Initial states can only be constructed, not transitioned to.

Example input:

initial: Disconnected
# or
initial: Disconnected, Starting
# or
initial:
  Disconnected
  Starting
Parameters
  • graph (var TypestateGraph) – The typestate graph to populate
  • node (NimNode) – AST node of the initial block

parseTerminalBlock

proc parseTerminalBlock(graph: var TypestateGraph; node: NimNode)

Parse the terminal states block.

Terminal states are end states with no outgoing transitions.

Example input:

terminal: Closed
# or
terminal: Closed, Failed
# or
terminal:
  Closed
  Failed
Parameters
  • graph (var TypestateGraph) – The typestate graph to populate
  • node (NimNode) – AST node of the terminal block

parseTypestateBody

proc parseTypestateBody(name: NimNode; body: NimNode): TypestateGraph

Parse a complete typestate block body into a TypestateGraph.

This is the main entry point for parsing. It processes the full body of a typestate macro invocation.

The typestate name can be a simple identifier or a generic type:

  • Simple: typestate File:
  • Generic: typestate Container[T]:

Examples:

typestate File:          # name = "File"
  states Closed, Open
  transitions:
    Closed -> Open

typestate Container[T]:  # name = "Container", with type param T
  states Empty[T], Full[T]
  transitions:
    Empty[T] -> Full[T]
Parameters
  • name (NimNode) – The typestate name (identifier or bracket expression)
  • body (NimNode) – The statement list containing states and transitions
Returns

TypestateGraph – A fully populated `TypestateGraph`


Registry

Compile-time typestate storage.

registry

Compile-time registry for typestate definitions.

This module provides a global compile-time registry that stores all declared typestates. The registry enables:

  • Looking up typestates by name
  • Finding which typestate a state type belongs to
  • Extending typestates across modules

The registry is used by the {.transition.} pragma to validate that transitions are allowed.

Internal module - most users won't interact with this directly.

typestateRegistry

var typestateRegistry: Table[string, TypestateGraph]

Global compile-time storage for all registered typestates.

Maps typestate names (e.g., "File") to their graph definitions. This variable is populated by the typestate macro and queried by the {.transition.} pragma.

Type: Table[string, TypestateGraph]

registerTypestate

template registerTypestate(graph: TypestateGraph)

Register a typestate graph in the compile-time registry.

Each typestate can only be defined once. Attempting to register a typestate with the same name twice results in a compile error.

Example:

typestate File:
  states Closed, Open
  transitions:
    Closed -> Open
    Open -> Closed
Parameters
  • graph (TypestateGraph) – The typestate graph to register

hasTypestate

template hasTypestate(name: string): bool

Check if a typestate with the given name exists in the registry.

Parameters
  • name (string) – The typestate name to look up
Returns

bool – `true` if registered, `false` otherwise

getTypestate

template getTypestate(name: string): TypestateGraph

Retrieve a typestate graph by name.

Parameters
  • name (string) – The typestate name to look up
Returns

TypestateGraph – The `TypestateGraph` for the typestate

findTypestateForState compileTime

proc findTypestateForState(stateName: string): Option[TypestateGraph]

Find which typestate a given state belongs to.

Searches all registered typestates to find one containing the specified state. Used by the {.transition.} pragma to determine which typestate graph to validate against.

Lookups use base names to support generic types: - findTypestateForState("Empty") finds typestate Container with Empty[T]

Example:

# If File typestate has states Closed, Open:
findTypestateForState("Closed")  # some(FileGraph)
findTypestateForState("Unknown") # none

# If Container typestate has states Empty[T], Full[T]:
findTypestateForState("Empty")   # some(ContainerGraph)
Parameters
  • stateName (string) – The state type name (base name, e.g., "Closed", "Empty")
Returns

Option[TypestateGraph] – `some(graph)` if found, `none` if state is not in any typestate

BranchTypeInfo

type BranchTypeInfo = object

Information about a user-defined branch type.

When a branching transition like Created -> (Approved | Declined) as ProcessResult is declared, the user provides the type name. This object captures the relationship between the branch type name and the original transition.

Fields

  • sourceState string – The source state name ("Created")
  • destinations seq[string] – The destination states (["Approved", "Declined"])

findBranchTypeInfo compileTime

proc findBranchTypeInfo(typeName: string): Option[BranchTypeInfo]

Check if a type name is a user-defined branch type.

Branch types are named by the user via as TypeName syntax in branching transitions.

This function searches all registered typestates for branching transitions that declare the given branch type name.

Example:

# If typestate has: Created -> (Approved | Declined) as ProcessResult
findBranchTypeInfo("ProcessResult")
# Returns: some(BranchTypeInfo(sourceState: "Created",
#                              destinations: @["Approved", "Declined"]))

findBranchTypeInfo("NotABranch")
# Returns: none(BranchTypeInfo)
Parameters
  • typeName (string) – The type name to check
Returns

Option[BranchTypeInfo] – `some(info)` if it's a branch type, `none` otherwise


Pragmas

Pragma implementations for transition validation.

pragmas

Pragmas for marking and validating state transitions.

This module provides the pragmas that users apply to their procs:

  • {.transition.} - Mark a proc as a state transition (validated)
  • {.notATransition.} - Mark a proc as intentionally not a transition

The {.transition.} pragma performs compile-time validation to ensure that only declared transitions are implemented.

sealedTypestateModules

var sealedTypestateModules: Table[string, seq[string]]

Maps module filename -> list of state type names from sealed typestates

Type: Table[string, seq[string]]

transparentWrappers

var transparentWrappers: HashSet[string] = toHashSet(["Result", "Option", "Future"])

Type: HashSet[string]

registerSealedStates compileTime

proc registerSealedStates(modulePath: string; stateNames: seq[string])

Register states from a sealed typestate for external checking.

Parameters
  • modulePath (string) – The module filename where the typestate is defined
  • stateNames (seq[string]) – List of state type names to register

isStateFromSealedTypestate compileTime

proc isStateFromSealedTypestate(stateName: string; currentModule: string): Option[string]

Check if a state is from a sealed typestate defined in another module.

Parameters
  • stateName (string) – The state type name to check
  • currentModule (string) – The current module's filename
Returns

Option[string] – `some(modulePath)` if from external sealed typestate, `none` otherwise

transparentWrapper pragma

template transparentWrapper()

Marker pragma (cosmetic, does NOT register). Apply to a generic type

and follow with an explicit static: registerTransparentWrapper("YourType") call to register it as transparent for {.transition.} return-type validation.

Contract for wrapper authors: the unwrap logic assumes the wrapped state type is the first generic argument of the wrapper. For Wrapper[State, ...Extras] this picks up State; for Wrapper[A, B] with no clear "primary" arg, only A is validated as a typestate destination. This matches the built-in seeds (Result[T, E], Option[T], Future[T]). If your wrapper puts the state anywhere other than position 0, do NOT register it — write a non-transparent wrapper and validate the transition at the call site instead.

Example:

type MyResult*[T] {.transparentWrapper.} = object
  value: T
static:
  registerTransparentWrapper("MyResult")

The pragma itself is a no-op in v1; actual registration is the separate registerTransparentWrapper call. The two-step form keeps type-level AST interactions simple and consistent with the notATransition marker pattern.

registerTransparentWrapper compileTime

proc registerTransparentWrapper(name: string)

Register a generic wrapper type as transparent for {.transition.}

return-type validation. Name may be a base name ("MyResult") or a module-qualified name ("mymod.MyResult"); the lookup checks both forms via isTransparentWrapper.

Parameters
  • name (string)

unregisterTransparentWrapper compileTime

proc unregisterTransparentWrapper(name: string)

Remove a wrapper from the registry. Intended for users whose

project-local type of the same name is itself a typestate STATE (rather than an error/option wrapper) and must be validated as the destination — not unwrapped. Call from a static: block near the typestate declaration, before the {.transition.} procs that use it.

Parameters
  • name (string)

isTransparentWrapper compileTime

proc isTransparentWrapper(name: string): bool

Predicate: is this type name registered as a transparent wrapper?

Checks both the full name as given and the extracted base name, so module-qualified forms (results.Result) and bare forms (Result) both hit the registry.

Parameters
  • name (string)
Returns

bool

transition

macro transition(procDef: untyped): untyped

Mark a proc as a state transition and verify it at compile time.

The compiler checks that the transition from the input state type to the return state type is declared in the corresponding typestate. If not, compilation fails with a diagnostic.

This provides compile-time protocol enforcement: only declared transitions can be implemented.

Example:

proc open(f: Closed): Open {.transition.} =
  result = Open(f)

proc close(f: Open): Closed {.transition.} =
  result = Closed(f)

Error example:

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.
Parameters
  • procDef (untyped)
Returns

untyped

notATransition pragma

template notATransition()

Mark a proc as intentionally not a state transition.

Use this pragma for procs that operate on state types but don't change the state. This is required when strictTransitions is enabled on the typestate.

When to use:

  • Procs that read from a state type
  • Procs that perform I/O without changing state
  • Procs that modify the underlying data without state transition

Example:

# Side effects without state change
proc write(f: Open, data: string) {.notATransition.} =
  rawWrite(f.handle, data)

# Pure functions don't need this (use `func` instead)
func path(f: Open): string = f.File.path

Code Generation

Code generation for helper types.

codegen

Code generation for typestate helper types.

This module generates the helper types and procs that make typestates easier to use at runtime:

  • State enum: FileState = enum fsClosed, fsOpen, ...
  • Union type: FileStates = Closed | Open | ...
  • State procs: proc state(f: Closed): FileState
  • Branch types: CreatedBranch variant for Created -> Approved | Declined
  • Branch constructors: toCreatedBranch(s: Approved): CreatedBranch

These are generated automatically by the typestate macro.

buildGenericParams

proc buildGenericParams(typeParams: seq[NimNode]): NimNode

Build a generic params node for proc/type definitions.

For @[T], generates: [T] For @[K, V], generates: [K, V] For @[N: static int], generates: [N: static int] For @[T: SomeInteger], generates: [T: SomeInteger] For @[], returns empty node (non-generic)

Parameters
  • typeParams (seq[NimNode]) – Sequence of type parameter nodes
Returns

NimNode – nnkGenericParams node or newEmptyNode()

extractTypeParams

proc extractTypeParams(node: NimNode): seq[NimNode]

Extract type parameters from a type node.

For FillResult[T], returns @[T] For Map[K, V], returns @[K, V] For Simple, returns @[]

Parameters
  • node (NimNode) – A type AST node (ident or bracket expr)
Returns

seq[NimNode] – Sequence of type parameter nodes

generateStateEnum

proc generateStateEnum(graph: TypestateGraph): NimNode

Generate a runtime enum representing all states.

For a typestate named File with states Closed, Open, Errored, generates:

type FileState* = enum
  fsClosed, fsOpen, fsErrored

For generic typestates like Container[T] with states Empty[T], Full[T]:

type ContainerState* = enum
  fsEmpty, fsFull

The enum values use base names (without type params) prefixed with fs.

Parameters
  • graph (TypestateGraph) – The typestate graph to generate from
Returns

NimNode – AST for the enum type definition

generateUnionType

proc generateUnionType(graph: TypestateGraph): NimNode

Generate a type alias for "any state" using Nim's union types.

For a typestate named File with states Closed, Open, Errored, generates:

type FileStates* = Closed | Open | Errored

For generic typestates like Container[T]:

type ContainerStates*[T] = Empty[T] | Full[T]

This union type is useful for procs that can accept any state.

Parameters
  • graph (TypestateGraph) – The typestate graph to generate from
Returns

NimNode – AST for the union type definition

generateStateProcs

proc generateStateProcs(graph: TypestateGraph): NimNode

Generate state() procs for runtime state inspection.

For each state, generates a proc that returns the enum value:

proc state*(f: Closed): FileState = fsClosed
proc state*(f: Open): FileState = fsOpen

For generic types:

proc state*[T](f: Empty[T]): ContainerState = fsEmpty
proc state*[T](f: Full[T]): ContainerState = fsFull
Parameters
  • graph (TypestateGraph) – The typestate graph to generate from
Returns

NimNode – AST for all state() proc definitions

generateStateDollar

proc generateStateDollar(graph: TypestateGraph): NimNode

Generate $ overload for each leaf state type and the state enum.

For each state, emits a proc returning the bare state name:

proc `$`*(s: Closed): string = "Closed"
proc `$`*[T](s: Empty[T]): string = "Empty"

Also emits a $ over the generated state enum that strips the fs prefix:

proc `$`*(s: FileState): string =
  case s
  of fsClosed: "Closed"
  of fsOpen: "Open"
Parameters
  • graph (TypestateGraph) – The typestate graph
Returns

NimNode – AST for `$` overloads (one per state + one over the enum)

hasGenericStates

proc hasGenericStates(graph: TypestateGraph): bool

Check if any states use generic type parameters.

Parameters
  • graph (TypestateGraph)
Returns

bool

getBranchingTransitions

proc getBranchingTransitions(graph: TypestateGraph): seq[Transition]

Get all transitions that have multiple destinations (branching).

A branching transition is one where toStates.len > 1, like: Created -> (Approved | Declined)

Parameters
  • graph (TypestateGraph) – The typestate graph to query
Returns

seq[Transition] – Sequence of branching transitions

generateBranchTypes

proc generateBranchTypes(graph: TypestateGraph): NimNode

Generate variant types for branching transitions.

For a transition like Created -> (Approved | Declined) as ProcessResult, generates:

type
  ProcessResultKind* = enum pApproved, pDeclined
  ProcessResult* = object
    case kind*: ProcessResultKind
    of pApproved: approved*: Approved
    of pDeclined: declined*: Declined

For generic types like Empty[T] -> Full[T] | Error[T] as FillResult[T]:

type
  FillResultKind* = enum fFull, fError
  FillResult*[T] = object
    case kind*: FillResultKind
    of fFull: full*: Full[T]
    of fError: error*: Error[T]

The type name comes from the as TypeName syntax in the DSL. Enum prefixes are derived from the first letter of the type name.

Parameters
  • graph (TypestateGraph) – The typestate graph to generate from
Returns

NimNode – AST for all branch type definitions

generateBranchConstructors

proc generateBranchConstructors(graph: TypestateGraph): NimNode

Generate constructor procs for branch types.

For Created -> (Approved | Declined) as ProcessResult, generates:

proc toProcessResult*(s: Approved): ProcessResult =
  ProcessResult(kind: pApproved, approved: s)

proc toProcessResult*(s: Declined): ProcessResult =
  ProcessResult(kind: pDeclined, declined: s)

For generic types:

proc toFillResult*[T](s: Full[T]): FillResult[T] =
  FillResult[T](kind: fFull, full: s)
Parameters
  • graph (TypestateGraph) – The typestate graph to generate from
Returns

NimNode – AST for all constructor proc definitions

generateCopyHooks

proc generateCopyHooks(graph: TypestateGraph): NimNode

Generate =copy error hooks to prevent state copying.

When consumeOnTransition = true, generates:

proc `=copy`*(dest: var Closed, src: Closed) {.error: "State 'Closed' cannot be copied. Transitions consume the input state.".}

This enforces linear/affine typing - each state value can only be used once.

Parameters
  • graph (TypestateGraph) – The typestate graph to generate from
Returns

NimNode – AST for all copy hook definitions

hasStaticGenericParam

proc hasStaticGenericParam(graph: TypestateGraph): bool

Check if typestate has any static generic parameters (e.g., N: static int).

These are vulnerable to a codegen bug in Nim < 2.2.8 when combined with =copy hooks on distinct types. Affects ARC, ORC, AtomicARC, and any memory manager using hooks.

Parameters
  • graph (TypestateGraph) – The typestate graph to check
Returns

bool – `true` if any type parameter uses `static`

hasHookCodegenBugConditions

proc hasHookCodegenBugConditions(graph: TypestateGraph): bool

Check if this typestate has conditions that trigger a codegen bug in Nim < 2.2.8.

The bug occurs when all these conditions are met: 1. Distinct types (implicit - all typestate states are distinct) 2. Plain object (not inheriting from RootObj) 3. Generic with static parameter (e.g., N: static int) 4. Lifecycle hooks are generated (consumeOnTransition = true)

Note: Condition 1 is always true for typestates. Condition 2 is checked via the inheritsFromRootObj flag (we can't detect inheritance at macro time).

Affects ARC, ORC, AtomicARC, and any memory manager using hooks. Fixed in Nim commit 099ee1ce4a308024781f6f39ddfcb876f4c3629c (>= 2.2.8). See: https://github.com/nim-lang/Nim/issues/25341

Parameters
  • graph (TypestateGraph) – The typestate graph to check
Returns

bool – `true` if vulnerable conditions are present

generateBranchOperators

proc generateBranchOperators(graph: TypestateGraph): NimNode

Generate -> operator templates for branch types.

The -> operator provides syntactic sugar for branch construction. It takes the branch type on the left and the state value on the right:

# Usage (for: Created -> Approved | Declined as ProcessResult):
ProcessResult -> Approved(c.Payment)

# Equivalent to:
toProcessResult(Approved(c.Payment))

For generic types:

FillResult[int] -> Full[int](container)

Generated templates:

template `->`*(T: typedesc[ProcessResult], s: Approved): ProcessResult =
  toProcessResult(s)

template `->`*[T](T: typedesc[FillResult[T]], s: Full[T]): FillResult[T] =
  toFillResult(s)

The typedesc parameter disambiguates when the same state appears in multiple branch types.

Parameters
  • graph (TypestateGraph) – The typestate graph to generate from
Returns

NimNode – AST for all operator template definitions

generateBranchDollar

proc generateBranchDollar(graph: TypestateGraph): NimNode

Generate $ overload for each branching union type.

For a branching transition Created -> A | B | C as Result, emits:

proc `$`*(r: Result): string =
  case r.kind
  of pA: "A"
  of pB: "B"
  of pC: "C"

For generic typestates the union type includes the typestate's type parameters so the proc binds correctly under generic instantiation.

Parameters
  • graph (TypestateGraph) – The typestate graph
Returns

NimNode – AST for `$` overloads over branching union types (one per union)

buildMatchCase

proc buildMatchCase(value: NimNode; arms: NimNode; validNames: seq[string]; kindSyms: seq[NimNode]): NimNode

INTERNAL: this helper is exported for use by the generated match macro

via bindSym. User code should not call it directly.

Helper used by every generated match macro to rewrite an arms block into a case value.kind statement.

  • value: NimNode for the matched union value (passed to the macro).
  • arms: NimNode for the StmtList of Call(StateIdent, bindIdent, body) arms.
  • validNames: bare base names of the union's branches (e.g. @["Approved", "Declined"]).
  • kindSyms: pre-resolved sym nodes for each branch's kind-enum field, in the same order as validNames. Resolved at the typestate-decl call site (where the kind enum is in scope) so consumer modules that do not import the kind enum directly can still expand match correctly.

Errors at the user's call site for malformed arms or unknown-branch names. Exhaustiveness is enforced by Nim's case-statement checker once the resulting AST is sema-checked.

Parameters
  • value (NimNode)
  • arms (NimNode)
  • validNames (seq[string])
  • kindSyms (seq[NimNode])
Returns

NimNode

buildSingleTargetMatchCase

proc buildSingleTargetMatchCase(value: NimNode; arms: NimNode; validStateName: string): NimNode

INTERNAL: this helper is exported for use by the generated single-target

match macro via bindSym. User code should not call it directly.

Helper used by every generated single-target match macro to rewrite an arms block of the shape StateName(bindName): body into a hygienic block: statement that moves the matched value into the bound name and then runs the body.

  • value: NimNode for the matched state value (passed to the macro).
  • arms: NimNode for the StmtList of Call(StateIdent, bindIdent, body) arms.
  • validStateName: bare base name of the only valid state (e.g. "Approved").

Two-path AST emit driven by value.kind:

  • L-value source (nnkIdent/nnkSym/nnkDotExpr/nnkBracketExpr):

    block:
      let bind = move(value)
      body
    
    move() accepts the l-value directly; no copy hook is invoked. The l-value MUST be a var binding (e.g. var a = ...; match a:) because system.move requires a var T parameter. A let-bound source emits the standard "expression is immutable, not 'var'" error at the user's call site.

  • R-value source (call expressions, etc.):

    block:
      var valTmp`gensym = value
      let bind = move(valTmp`gensym)
      body
    
    Materializing the rvalue into a var goes through =sink (sink-on-construction), not =copy, so the {.error.} copy hook on distinct state types is never reached. The temp is required because move() demands a var binding.

The block: wrapper provides hygiene so adjacent matches with the same bind name don't collide.

Errors at the user's call site for malformed arms or a state-name mismatch.

Parameters
  • value (NimNode)
  • arms (NimNode)
  • validStateName (string)
Returns

NimNode

generateBranchMatch

proc generateBranchMatch(graph: TypestateGraph): NimNode

Generate a match macro for each branching union type.

For a transition Created -> (Approved | Declined) as ProcessResult, emits a macro with this signature:

macro match*(value: ProcessResult; body: untyped): untyped =
  ## Pattern-match on a branching union; rewrites into a `case` over the
  ## kind discriminator.

Call-site syntax (the body is a list of Call(StateIdent, bindIdent, body)):

match r:
  Approved(a): doSomething(a)
  Declined(d): handleDecline(d)

Rewritten to:

case value.kind
of pApproved:
  let a = move(value.approved)
  doSomething(a)
of pDeclined:
  let d = move(value.declined)
  handleDecline(d)

Exhaustiveness is provided by Nim's case statement: missing branches produce "not all cases are covered" at compile time. Branches naming a state outside the union produce an explicit "unknown branch" error.

Multiple branching unions in the same module each get their own match macro; Nim disambiguates via the typed first parameter (verified by the F4.A0 probe).

Parameters
  • graph (TypestateGraph) – The typestate graph
Returns

NimNode – AST for one `match` macro per branching union

generateSingleTargetMatch

proc generateSingleTargetMatch(graph: TypestateGraph): NimNode

Generate a match macro for each state, supporting single-target match.

For every state in the graph, emits a macro with this signature:

macro match*(value: <StateType>; arms: untyped): untyped =
  ## Single-target pattern match; rewrites to `block: let bind = move(value); body`.

Call-site syntax (exactly one arm naming the state):

match a:
  Approved(x):
    useApproved(x)

Rewritten to:

block:
  let x = move(a)
  useApproved(x)

R-value sources (e.g. call expressions) are first materialized into a gensym'd let so move() has an l-value. Sink-on-construction avoids the =copy error hook on distinct state types.

The per-state match overloads coexist with the per-branching-union match overloads emitted by generateBranchMatch; Nim disambiguates by the typed first parameter. The parser-side collision validator prevents same-name overload duplication between a state and a branch wrapper type.

Parameters
  • graph (TypestateGraph) – The typestate graph
Returns

NimNode – AST for one `match` macro per state

generateAll

proc generateAll(graph: TypestateGraph): NimNode

Generate all helper types and procs for a typestate.

This is the main entry point called by the typestate macro. It generates:

  1. State enum (FileState)
  2. Union type (FileStates or ContainerStates[T])
  3. State procs (state() for each state)
  4. Copy hooks (=copy error hooks when consumeOnTransition = true)
  5. Branch types for branching transitions (user-named via as TypeName)
  6. Branch constructors (toTypeName)
  7. Branch operators (->)
  8. Branch $ overloads
  9. Per-branching-union match macros
  10. Per-state single-target match macros

For generic typestates like Container[T], all generated types and procs include proper type parameters.

Parameters
  • graph (TypestateGraph) – The typestate graph to generate from
Returns

NimNode – AST containing all generated definitions


CLI

Command-line tool functionality.

cli

Command-line tool for typestates.

Usage:

typestates verify [paths...]
typestates dot [paths...]

Parses source files using Nim's AST parser and verifies typestate rules or generates DOT output.

Note: Files must be valid Nim syntax. Parse errors cause verification to fail loudly with a clear error message.

SplineMode

type SplineMode = enum

Edge routing mode for DOT output.

Values

  • smSpline = "spline" – Curved splines (default, best edge separation)
  • smOrtho = "ortho" – Right-angle edges only
  • smPolyline = "polyline" – Straight line segments
  • smLine = "line" – Direct straight lines

parseTypestates

proc parseTypestates(paths: seq[string]): ParseResult

Parse all Nim files in the given paths for typestates.

Uses Nim's AST parser for accurate extraction. Fails loudly on files with syntax errors.

Parameters
  • paths (seq[string]) – List of file or directory paths to scan
Returns

ParseResult – All parsed typestates and total file count

generateDot

proc generateDot(ts: ParsedTypestate; noStyle: bool = false; splineMode: SplineMode = smSpline): string

Generate GraphViz DOT output for a typestate.

Creates a directed graph representation suitable for rendering with dot, neato, or other GraphViz tools.

Parameters
  • ts (ParsedTypestate) – The parsed typestate to visualize
  • noStyle (bool) – If true, output bare DOT structure with no styling
  • splineMode (SplineMode) – Edge routing mode (spline, ortho, polyline, line)
Returns

string – DOT format string

generateUnifiedDot

proc generateUnifiedDot(typestates: seq[ParsedTypestate]; noStyle: bool = false; splineMode: SplineMode = smSpline): string

Generate a unified GraphViz DOT output showing all typestates.

Creates subgraphs for each typestate with cross-cluster edges for bridges.

Parameters
  • typestates (seq[ParsedTypestate]) – List of parsed typestates to visualize
  • noStyle (bool) – If true, output bare DOT structure with no styling
  • splineMode (SplineMode) – Edge routing mode (spline, ortho, polyline, line)
Returns

string – DOT format string

generateSeparateDot

proc generateSeparateDot(ts: ParsedTypestate; noStyle: bool = false; splineMode: SplineMode = smSpline): string

Generate GraphViz DOT output for a single typestate.

Bridges are shown as terminal nodes with dashed edges.

Parameters
  • ts (ParsedTypestate) – The parsed typestate to visualize
  • noStyle (bool) – If true, output bare DOT structure with no styling
  • splineMode (SplineMode) – Edge routing mode (spline, ortho, polyline, line)
Returns

string – DOT format string

generateCode

proc generateCode(ts: ParsedTypestate): string

Generate Nim code for a typestate's helper types and procs.

Generates: - State enum (FileState = enum fsClosed, fsOpen, ...) - Union type (FileStates = Closed | Open | ...) - State procs (proc state(f: Closed): FileState) - Branch types for branching transitions - Branch constructors and operators

Parameters
  • ts (ParsedTypestate) – The parsed typestate to generate code for
Returns

string – Generated Nim code as a string

generateCodeForAll

proc generateCodeForAll(typestates: seq[ParsedTypestate]): string

Generate code for all typestates.

Parameters
  • typestates (seq[ParsedTypestate]) – All parsed typestates
Returns

string – Combined generated Nim code

verify

proc verify(paths: seq[string]): VerifyResult

Verify all Nim files in the given paths.

Uses Nim's AST parser to extract typestates, then checks that all procs operating on state types are properly marked with {.transition.} or {.notATransition.}. Also runs reachability analysis when initial: / terminal: blocks are declared and the opaque-states cast-bypass lint, appending all results to result.findings.

Files with syntax errors no longer abort the pipeline: they surface as fcParseError findings routed through the normal output formatters. A parse error in one file does NOT abort verification of other files — typestates verify --format=github src/ produces annotations for every problem the user has, not just the first.

Parameters
  • paths (seq[string]) – List of file or directory paths to verify
Returns

VerifyResult – Verification results with structured findings and counts