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:
typestatemacro - 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:
FileStateenum withfsClosed,fsOpen,fsErroredFileStatesunion type for generic procsstate()procs for runtime inspection
Transition syntax:
A -> B- Simple transitionA -> 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
-
namestring -
fullReprstring -
typeNameNimNode
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:
Fields
-
fromStatestring -
toStatesseq[string] -
branchTypeNamestring– Empty for non-branching, required for branching -
branchTypeNodeNimNode– Raw AST node for codegen (supports generics like Result[T]) -
isWildcardbool -
declaredAtLineInfo
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
-
fromStatestring -
toModulestring -
toTypestatestring -
toStatestring -
fullDestReprstring -
declaredAtLineInfo
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:
Creates a TypestateGraph with name="File", two states, and two transitions.
Fields
-
namestring -
typeParamsseq[NimNode]– Generic params: @[T] or @[K, V] or @[] -
typeParamDefaultsseq[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. -
statesTable[string, State] -
transitionsseq[Transition] -
bridgesseq[Bridge] -
strictTransitionsbool -
consumeOnTransitionbool– If true, states cannot be copied -
inheritsFromRootObjbool– If true, skip static generic bug check -
opaqueStatesbool– Opt-in cast-bypass lint (CLI-side only) -
initialStatesseq[string]– States that cannot be transitioned TO -
terminalStatesseq[string]– States that cannot transition FROM -
declaredAtLineInfo -
declaredInModulestring
== ¶
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:
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:
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:
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:
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:
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:
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:
- Multiline with commas:
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:
Example AST for * -> Closed (wildcard parsed as nested prefix):
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:
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:
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:
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:
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:
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:
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
-
typestateNamestring -
initialStatestring -
declaredAtLineInfo
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
-
sourceStatestring– The source state name ("Created") -
destinationsseq[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:
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 bydestructorTransitionCorefor 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) -> unchangedp*(nnkPostfix) -> peel Postfixp {.pragma.}(nnkPragmaExpr) -> peel PragmaExprp* {.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:
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:
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:
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:
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 theSrcState -> DstStatespec.template skipCfgAnalysis*()— opt out a single proc from the v0.9.0 CFG analyzer pass.- Per-typestate attachment pragma — auto-emitted by the
typestatemacro 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:
CreatedBranchvariant forCreated -> 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:
For generic typestates like Container[T] with states Empty[T], Full[T]:
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:
For generic typestates like Container[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:
For generic types:
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:
Also emits a $ over the generated state enum that strips the fs prefix:
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:
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:
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:
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 ofCall(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 asvalidNames. 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 expandmatchcorrectly.
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 ofCall(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):move()accepts the l-value directly; no copy hook is invoked. The l-value MUST be avarbinding (e.g.var a = ...; match a:) becausesystem.moverequires avar Tparameter. Alet-bound source emits the standard "expression is immutable, not 'var'" error at the user's call site. -
R-value source (call expressions, etc.):
Materializing the rvalue into avargoes through=sink(sink-on-construction), not=copy, so the{.error.}copy hook on distinct state types is never reached. The temp is required becausemove()demands avarbinding.
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)):
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):
Rewritten to:
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:
- State enum (
FileState) - Union type (
FileStatesorContainerStates[T]) - State procs (
state()for each state) - Copy hooks (
=copyerror hooks when consumeOnTransition = true) - Branch types for branching transitions (user-named via
as TypeName) - Branch constructors (
toTypeName) - Branch operators (
->) - Branch
$overloads - Per-branching-union
matchmacros - Per-state single-target
matchmacros
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:
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