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 @[]
  • typeParamDefaults seq[NimNode] – Default-value expressions, parallel to `typeParams`. Each entry is `newEmptyNode()` for params without a default, or a captured AST node for the default expression supplied in the `defaults:` body section. Always the same length as `typeParams` once parsing completes.
  • 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

parseDefaultsBlock

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

Parse the optional defaults: body section.

Captures default-value expressions for the typestate's bracket-head generic parameters. Defaults flow through buildGenericParams into every generated type and proc (state distincts, variant types, the context type, =copy hooks, state() procs, $ overloads, etc.).

Example input:

typestate RegistrationContext[
    MaxThreads: static int,
    CC: static PinScopeCardinality]:
  defaults:
    CC: ccSingle
  states:
    Unregistered, Registered

Validation rules (each rule fires a macro-time error):

  • Each entry must reference a generic param declared in the bracket head; an unknown name is rejected with a clear message.
  • Each entry references only the param's name (no constraint re-declaration). The constraint comes from the bracket head.
  • Duplicate entries (same param named twice) are rejected.

The default-value expression is captured as-is (NimNode) and emitted verbatim at codegen; it is typed by the Nim compiler at the type-instantiation site, mirroring native proc foo[T = Default] semantics.

Parameters
  • graph (var TypestateGraph) – The typestate graph to populate
  • node (NimNode) – AST node of the `defaults` block (nnkCall with StmtList body)

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

AttachmentInfo

type AttachmentInfo = object

Information about a §3.7 typestate-attachment registration (v0.9.0).

When a user-defined object type is bound to a typestate via a {.<typestateName>: <initialState>.} pragma (the typestate-attachment pragma), this record captures the binding so destructorTransition's source resolution (§3.1.1, path (b)) can recover the initial state.

Fields

  • typestateName string
  • initialState string
  • declaredAt LineInfo

typestateAttachments

var typestateAttachments: Table[string, AttachmentInfo]

Maps attached object type names (base names) to their attachment

record. Populated by the per-typestate attachment-pragma macro emitted by the typestate macro (see codegen.generateAttachmentMarker and pragmas.attachTypestateCore).

Type: Table[string, AttachmentInfo]

findAttachmentForType compileTime

proc findAttachmentForType(typeName: string): Option[AttachmentInfo]

Look up the §3.7 attachment record for an object type by base name.

Used by destructorTransitionCore (pragmas.nim) as the fallback source-resolution path when findTypestateForState does not match (i.e., the destructor's var T parameter is not itself a registered typestate state, but is an object type bound to a typestate via the attachment pragma).

Parameters
  • typeName (string) – Object type name (extractBaseName already applied)
Returns

Option[AttachmentInfo] – `some(info)` if registered, `none` otherwise

addAttachment compileTime

proc addAttachment(typeName: string; info: AttachmentInfo)

Register a typestate-attachment binding for an object type (§3.7).

Stores under the base name of typeName (generic params stripped). Callers are responsible for emitting TA-004 if the key is already present; this proc unconditionally overwrites, so duplicate detection must happen BEFORE the call.

Parameters
  • typeName (string) – Attached object type name (will be base-extracted)
  • info (AttachmentInfo) – The attachment record to store

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

v0.9.0 additions (also auto-documented above):

  • type AttachmentInfo* — record of a {.<TypestateName>: <InitialState>.} binding (typestate name, initial state, declaredAt).
  • var typestateAttachments* {.compileTime.}: Table[string, AttachmentInfo] — compile-time registry keyed by attached object type base name.
  • proc findAttachmentForType*(typeName: string): Option[AttachmentInfo] — lookup helper used by destructorTransitionCore for path (b) source resolution.
  • proc addAttachment*(typeName: string, info: AttachmentInfo) — internal register helper used by the per-typestate attachment-pragma macro.

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.

TypestateOp

type TypestateOp = object

Implicit effect tag injected into every {.transition.} proc.

Under {.experimental: "strictEffects".} this enables {.forbids: [TypestateOp].} regions to statically assert that no typestate transition reaches them — even transitively through untagged intermediate callers. The injection is additive (merges into any existing tags: [...] list) and idempotent (no duplicate entry if the user has already named TypestateOp themselves). Outside strictEffects Nim does not enforce tag propagation, so existing callers see zero behaviour change.

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.

transitionError pragma

template transitionError(msg: string)

Author-site diagnostic-string override for {.transition.} and

{.destructorTransition.} declaration-time errors.

Use as a sibling pragma to pin a custom error message that the Nim compiler emits (verbatim, with the standard file:line prefix) when a transition declaration is invalid (e.g., the source/destination edge is not in the typestate's transition graph, or the destructor's terminal target is not declared).

The pragma itself is a no-op marker — its EFFECT is realized by the transition and destructorTransitionCore macros, which scan the host proc's pragma list for a sibling transitionError: "msg" and, when present, substitute the literal string for the built-in diagnostic at every transition-validity error(...) site.

Static-literal only. The right-hand side must be a string literal (no concatenation, no fmt, no runtime var). All static string-literal forms are accepted: plain (nnkStrLit), raw (r"...", nnkRStrLit), and triple-quoted multi-line ("""...""", nnkTripleStrLit). The extractor fires a compile-time error if the rhs is none of these: transitionError must be a static string literal (no concatenation, no fmt).

Backwards compatible. Omitting transitionError preserves every existing diagnostic byte-for-byte.

Author-site only. This pragma customizes the declaration-time error message emitted when the typestate author writes an invalid transition. It does NOT customize the consumer-call-site CFG-001 diagnostic emitted from verify.nim:validateExitEdge when a caller invokes a proc whose required entry state does not match the current state of the typestate-attached value. Consumer-call-site substitution would require CFG-analyzer changes and is out of scope for v0.9.3.

Example:

proc lock(f: Open): Locked {.transition, transitionError:
    "Cannot lock an open file: call close() first".} =
  Locked(f)

proc `=destroy`(c: var Halfopen)
    {.destructorTransition: Halfopen -> Closed,
      transitionError: "Halfopen must close before destruction".} =
  discard

v0.9.3 sibling-pragma sweep. A search of pragmas.nim for other macro/template decls that emit transition-validity diagnostics found only transition and destructorTransition (via destructorTransitionCore). The sibling pragmas transparentWrapper, skipCfgAnalysis, and notATransition do NOT emit transition-validity diagnostics and are out of scope. attachTypestateCore emits TA-001..TA-004 attachment-validity diagnostics (not transition-validity); attachment-error customization is a separate feature, not included in v0.9.3.

Parameters
  • msg (string)

extractTransitionErrorPragma compileTime

proc extractTransitionErrorPragma(pragmaNode: NimNode): string

Scan a proc's nnkPragma node for a sibling transitionError: "msg"

pragma and return the literal string, or "" if absent.

Enforces static-literal: when the LHS is transitionError but the RHS is not a string literal (nnkStrLit, nnkRStrLit, or nnkTripleStrLit), fires a compile-time error.

Parameters
  • pragmaNode (NimNode) – The `procDef.pragma` node (kind `nnkPragma` or `nnkEmpty` for procs with no pragmas)
Returns

string – The extracted string, or `""` if no `transitionError:` sibling is present.

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

peelNameWrappers compileTime

proc peelNameWrappers(n: NimNode): NimNode

Peel nnkPragmaExpr then nnkPostfix wrappers off a name-position

node and return the underlying identifier-bearing node.

Param names, proc names, and type-decl heads can nest wrappers in either order:

  • p (nnkIdent / nnkSym) -> unchanged
  • p* (nnkPostfix) -> peel Postfix
  • p {.pragma.} (nnkPragmaExpr) -> peel PragmaExpr
  • p* {.pragma.} (nnkPragmaExpr(Postfix(*, p))) -> peel both
  • `name` (nnkAccQuoted) -> unchanged

AccQuoted, BracketExpr, and other non-wrapper leaves pass through untouched — callers must dispatch on the returned node's kind.

The loop form (vs. a single PragmaExpr-then-Postfix pass) handles arbitrary wrapper nesting and the inverse Postfix(PragmaExpr(...)) order. Nim's parser only produces the PragmaExpr(Postfix(...)) shape for T* {.pragma.}, but downstream macros that hand-build TypeDef / ProcDef / IdentDefs ASTs may emit either order or recurse deeper; round-18 defensive consistency.

Parameters
  • n (NimNode)
Returns

NimNode

accQuotedToStr compileTime

proc accQuotedToStr(n: NimNode): string

Reassemble the string form of a backticked identifier from its

nnkAccQuoted AST.

Walks the children of n and concatenates strVal for each nnkIdent/nnkSym child. Operator-idents like =destroy parse as AccQuoted(Ident("="), Ident("destroy")) and round-trip back to "=destroy".

Parameters
  • n (NimNode) – A node with `n.kind == nnkAccQuoted`
Returns

string – The reassembled identifier string. Empty when no Ident/Sym children are present.

extractTypestatedParams compileTime

proc extractTypestatedParams(procDef: NimNode): seq[TypestatedParam]

Walk a procDef's formal parameters and capture every typestate-bearing

var T or sink T parameter's name + state type + owning graph name + ownership flag.

Round-2 Finding #2: the CFG analyzer (verify.nim, runCfgAnalyzer) pre-populates LiveState with var T entries at proc entry so a proc that takes var f: Open and returns early without consuming f correctly fires CFG-001.

Round-9 Finding #1: the entry set now ALSO includes sink T typestate-bearing params (with isSink=true). The source-state-aware overload lookup (findTransitionByCalleeAndArgStates) needs every typestate-bearing param's positional index + source state to disambiguate overloads whose only difference is the source state of a trailing sink param (e.g., proc tx(a: int, b: sink Open): Closed vs. proc tx(a: int, b: sink HalfOpen): Closed). Position-0 sink params are already disambiguated by RegisteredProc.sourceState, but trailing-position sink params were silently excluded by the var-only filter — argStates[paramIndex] couldn't constrain the search and the last-registered overload won by name-only countdown.

Parameter shape: nnkFormalParams is [returnType, identDefs1, identDefs2, ...]. Each nnkIdentDefs is [name1, name2, ..., type, default]. Grouped leading names (e.g. proc f(a, b: var Open)) share a single type slot at index ^2.

Recognised param-kind shapes: - var T: nnkVarTy(T). isSink=false. Pre-populated AND used for overload disambiguation. - sink T: nnkCommand(ident("sink"), T). isSink=true. Pre-populated AND used for overload disambiguation. Round-14 reversed the round-9 skip on sink params; canonical conversion bodies (e.g. result = Dst(s.Base)) still verify cleanly because applyCallTransitions' conversion-consume path drops the sink param from tracking when the body produces its result via the canonical conversion. Symmetric tracking with var T closes the gap where a sink-T transition that never references its sink param silently passed the analyzer.

Explicitly EXCLUDED param-kind shapes (no entry produced): - bare value T (e.g., proc f(p: Open)): no Nim modifier wrapper; the param is a copy/move local to the proc with no caller-visible post-state and no positional disambiguation requirement at present (value-T transitions are uncommon in the test suite; if needed, extending the matcher to detect bare typestate-bearing idents would be a future round). The current source-state-aware lookup only exercises var-T and sink-T shapes. - ref T / ptr T / lent T: reference-like modifiers; the analyzer does not model heap aliasing today. - static T, typedesc[T]: compile-time-only; never a runtime typestate-bearing local.

Union-source param types (e.g. var (A | B), sink (A | B)) are skipped: the param's "current state" is ambiguous until the overload is resolved at the call site.

Resolution: each leading name's declared type is run through extractTypeName (peels generic brackets, etc.) and looked up against the typestate registry via findTypestateForState. Non-typestate-bearing params are skipped.

Parameters
  • procDef (NimNode)
Returns

seq[TypestatedParam]

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

skipCfgAnalysis pragma

template skipCfgAnalysis()

Marker pragma: suppress the v0.9.0 CFG analyzer for this proc.

When applied to a proc registered via {.transition.} or {.destructorTransition.}, the registered proc's skipCfg flag is set to true, telling the CFG analyzer (§3.3) to skip per-local terminal-reachability checks for the proc body. Useful as an escape hatch when the analyzer cannot model a proc's control flow (e.g., opaque exit via FFI / setjmp-style continuations / asyncdispatch bodies the analyzer doesn't yet understand).

The pragma itself is a no-op marker — its EFFECT is realized in the destructorTransition / transition macro's pragma-scan pass, which inspects procDef.pragma as AST nodes (NOT a stringified CLI substring scan, which would mis-handle combined pragmas like {.raises: [], skipCfgAnalysis.}).

Example:

proc tricky(x: A): B {.transition, skipCfgAnalysis.} =
  ## CFG analyzer skips this proc.
  B(x)

destructorTransition

macro destructorTransition(destrDef: untyped): untyped

Mark a =destroy hook as a terminal state transition (single-arg form).

Use when the typestate declares exactly one terminal state OR when the destructor consumes the union of all terminals; the destination is inferred as the typestate's terminalStates set.

Example:

typestate Connection:
  states Open, Closed
  terminal: Closed
  transitions:
    Open -> Closed

proc `=destroy`(c: var Open) {.destructorTransition.} =
  discard

See: §3.1 of design-destructortransition-cfg-analyzer-20260516.md

Parameters
  • destrDef (untyped)
Returns

untyped

destructorTransition

macro destructorTransition(spec: untyped; destrDef: untyped): untyped

Mark a =destroy hook as a terminal state transition (two-arg form).

Use when the typestate declares multiple terminal states and the destructor pins exactly one. The spec syntax SrcState -> DstState mirrors {.transition: A -> B.} for visual symmetry.

Example:

proc `=destroy`(c: var Open)
    {.destructorTransition: Open -> Closed.} =
  discard

See: §3.1 of design-destructortransition-cfg-analyzer-20260516.md

Parameters
  • spec (untyped)
  • destrDef (untyped)
Returns

untyped

extractTypeDeclName compileTime

proc extractTypeDeclName(typeDef: NimNode): string

Extract the base type name from a nnkTypeDef node, stripping

visibility postfix (*) and generic params ([T]).

Examples (input node[0] shape -> result): - PinnedScope (nnkIdent) -> "PinnedScope" - PinnedScope* (nnkPostfix) -> "PinnedScope" - PinnedScope[MT, CC] (nnkBracketExpr) -> "PinnedScope" - PinnedScope*[MT, CC] (nnkPragmaExpr->Postfix) -> "PinnedScope"

NOTE: when a pragma is on a type decl, typeDef[0] may be wrapped in an nnkPragmaExpr whose [0] is the name node and [1] is the pragma. We accept typeDef here so the caller can pass the raw TypeDef without prior unwrapping.

Parameters
  • typeDef (NimNode)
Returns

string

attachTypestateCore compileTime

proc attachTypestateCore(typestateName: string; initial: NimNode; typeDef: NimNode): NimNode

Shared core for the per-typestate attachment-pragma macros emitted

by the typestate macro (see codegen.generateAttachmentMarker).

Implements §3.7 verification rules TA-001..TA-004 and registers the binding in typestateAttachments so destructorTransition's path (b) source resolution and CFG analyzer scope detection can recover the initial state from the attached object type name.

TA-001 is unreachable through this entry point — the per-typestate macro emitter only runs WHEN the typestate is declared, so an undeclared <TypestateName> surfaces as Nim's "undeclared identifier" error at parse time, attributed to the pragma site. We still emit the TA-001 message defensively in case the registry is concurrently mutated.

Parameters
  • typestateName (string) – Name of the typestate whose marker pragma fired
  • initial (NimNode) – The initial-state argument as written in the pragma
  • typeDef (NimNode) – The TypeDef AST the pragma decorates
Returns

NimNode – `typeDef` unchanged (the pragma is registration-only)

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

v0.9.0 additions (also auto-documented above):

  • macro destructorTransition*(destrDef: untyped) — single-arg form. Destination state inferred as the typestate's terminal-state set.
  • macro destructorTransition*(spec, destrDef: untyped) — two-arg form. Destination state explicit via the SrcState -> DstState spec.
  • template skipCfgAnalysis*() — opt out a single proc from the v0.9.0 CFG analyzer pass.
  • Per-typestate attachment pragma — auto-emitted by the typestate macro as {.<TypestateName>: <InitialState>.}, used on object type decls to bind a distinct-name type to a typestate.

See Destructor Transitions and CFG Analyzer for usage.


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]; defaults: 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)

When defaults is non-empty it must be the same length as typeParams. Each entry is either newEmptyNode() (no default for that param) or a captured AST node for the default expression. The default is emitted into the nnkIdentDefs default-value slot for that param, mirroring native Nim proc foo[T = Default] semantics. The Nim compiler types the default expression at type-instantiation time.

Parameters
  • typeParams (seq[NimNode]) – Sequence of type parameter nodes
  • defaults (seq[NimNode]) – Optional parallel sequence of default expressions. Empty seq means "no defaults for any param" (back-compat).
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

generateAttachmentMarker

proc generateAttachmentMarker(graph: TypestateGraph): NimNode

Generate the per-typestate attachment-pragma macro (§3.7).

For typestate <Name>, emits:

when not declared(<Name>):
  macro <Name>*(initial: untyped, typeDef: untyped): untyped =
    attachTypestateCore("<Name>", initial, typeDef)

The when not declared(<Name>) guard prevents a redefinition error when the typestate name collides with an existing identifier in scope — the established convention pairs typestate Resource: with type Resource = object. In that case the attachment-pragma macro is silently skipped; the typestate still works for state-typed destructor params (path (a) in §3.1), it just can't be used as an attachment-pragma target. Users who want attachment-pragma support should name their typestate distinctly from any underlying type (e.g. PinnedScopeContext paired with type PinnedScope = object, per the nim-debra 0.8.0 convention).

When the guard fires (no collision) and a user later writes type T {.<Name>: <InitialState>.} = object, Nim invokes this macro with (initial = <InitialState>, typeDef = <T's TypeDef>). The macro delegates to attachTypestateCore (pragmas.nim) which validates TA-002..TA-004 and registers the attachment (TA-001 is unreachable through this code path — see attachTypestateCore's doc comment for the unreachable-defense rationale).

Parameters
  • graph (TypestateGraph) – The typestate graph (`graph.name` is the macro name)
Returns

NimNode – A `nnkStmtList` wrapping the guarded macro definition

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