zigfsm

repository·main·Indexed 41 days ago

https://github.com/cryptocode/zigfsm

A high-performance, zero-allocation finite state machine library for Zig (versions 0.12.x to 0.16). It supports hybrid execution at comptime and runtime, allows state machine definitions via programmatic setup or Graphviz/libfsm imports, and provides features for transition validation, event listeners, and Graphviz DOT export.

Tokens
2.1K
Snippets
7
Records
8
Agent score
2%

What's inside zigfsm

  1. Overview of zigfsm features

    main

    zigfsm is a finite state machine library for Zig that supports versions from 0.12.x up to 0.16. It is designed for high performance and low overhead. Key features include:

    • Zero Allocation: The library never performs heap allocations.
    • Hybrid Execution: Works at both comptime and runtime.
    • Efficiency: Fast transition validation and a compact memory representation.
    • Visualization: State machines can be exported to Graphviz DOT format for visualization.
    • Flexible Definition: Define machines programmatically or by importing Graphviz or libfsm text (even at comptime).
    • Automation: Imported machines can autogenerate state and event enums.
    • Extensibility: Optional event listeners allow for adding functionality or canceling transitions.
    • Advanced Patterns: Easy to wrap for push-down automata (e.g., GameState pattern).

    Supported platforms: Linux, macOS, FreeBSD, and Windows.

  2. Install zigfsm as a dependency

    main

    To use zigfsm in your project, add it as a Zig package using zig fetch.

    1. Fetch the dependency:
    zig fetch --save git+https://github.com/cryptocode/zigfsm
    1. Update your build.zig to include the module:
    exe.root_module.addImport("zigfsm", b.dependency("zigfsm", .{ .module = "zigfsm" }).module("zigfsm"));
  3. Handle state machine transitions with custom handlers

    main

    You can intercept state transitions by providing a list of Handler objects via setTransitionHandlers(). Handlers are called during transitionTo() or do() calls.

    Each handler's onTransition function receives the event (if any), the source state, and the destination state. The return value of the handler determines the outcome:

    • HandlerResult.Continue: The transition proceeds normally.
    • HandlerResult.Cancel: The transition is aborted, and the method returns StateError.Canceled.
    • HandlerResult.CancelNoError: The transition is aborted, but no error is returned.

    Example

    const MyHandler = struct {
        onTransition: *const fn (self: *MyHandler, event: ?Event, from: State, to: State) HandlerResult,
    };
    
    var my_handler = MyHandler{
        .onTransition = struct {
            fn handle(self: *MyHandler, event: ?Event, from: State, to: State) HandlerResult {
                std.debug.print("Transitioning from {s} to {s}\n", .{ @tagName(from), @tagName(to) });
                return .Continue;
            }
        }.handle,
    };
    
    fsm.setTransitionHandlers(&.{ &my_handler });
    // Define a handler implementation
    const MyHandler = struct {
        onTransition: *const fn (self: *MyHandler, event: ?Event, from: State, to: State) HandlerResult,
    };
    
    // Create an instance of the handler
    var handler_inst = MyHandler{
        .onTransition = struct {
            fn onTransition(self: *MyHandler, event: ?Event, from: State, to: State) HandlerResult {
                if (to == .error_state) return .Cancel;
                return .Continue;
            }
        }.onTransition,
    };
    
    fsm.setTransitionHandlers(&.{ &handler_inst });
  4. Import state machines from Graphviz or libfsm text

    main

    You can define state machines using a simple text format (compatible with Graphviz or libfsm) and import them at runtime or compile-time.

    Supported Formats

    • Transitions: a -> b, "a" -> "b", a -> b [label="event"], a -> b "event"
    • Start State: start: a; or start: -> "a";
    • Final States: end: "a" b c; or end: -> "X" Y;

    Methods

    1. initFrom(input: []const u8): Creates a new instance at runtime by parsing the provided text buffer.
    2. instanceFromText(comptime input: []const u8): A powerful compile-time function. It parses the text and generates the necessary State and Event enum types for you. This is ideal for using @embedFile to bake a state machine definition directly into your binary.

    Example (Compile-time generation)

    const FSM_TYPE = @import(
    

    // Using @embedFile to generate a type and instance at compile time const MyFSM = @import(

  5. Iterate through possible next states

    main

    If you need to discover which states are reachable from the current state, use the validNextStatesIterator().

    It returns a PossibleNextStateIterator which allows you to step through all valid destination states defined in the state map.

    Example

    var it = fsm.validNextStatesIterator();
    while (it.next()) |next_state| {
        std.debug.print("Can move to: {s}\n", .{@tagName(next_state)});
    }
    var it = fsm.validNextStatesIterator();
    while (it.next()) |next_state| {
        // next_state is the enum value of a valid destination
        std.debug.print("Possible next state: {s}\n", .{@tagName(next_state)});
    }
  6. Trigger transitions with events using `do()`

    main

    To move the state machine from its current state to a new state based on an event, use the do(event) method. This method returns a Transition object containing the event, the source state, and the destination state.

    If the event is not defined for the current state, it returns StateError.Invalid.

    Example

    try fsm.do(.start_event);
    // Assuming fsm is an instance of a StateMachine with EventType defined
    const transition = try fsm.do(.start_event);
    // transition.from == .idle
    // transition.to == .running
    // transition.event == .start_event
  7. Perform direct state transitions with `transitionTo()`

    main

    If you want to move to a specific state without an intervening event, use transitionTo(new_state). This method checks if the transition is valid according to the defined state map.

    If the transition is not allowed, it returns StateError.Invalid.

    Silent Transitions

    If you need to change the state without triggering any registered transition_handlers, use transitionToSilently(new_state, check_valid_transition). Set check_valid_transition to true to ensure the move is valid, or false to force the state change.

    Example

    try fsm.transitionTo(.running);
    
    // Force a state change without handlers and without validation
    fsm.transitionToSilently(.stopped, false) catch unreachable;
    // Standard transition
    try fsm.transitionTo(.running);
    
    // Silent transition (no handlers fired)
    fsm.transitionToSilently(.stopped, true) catch unreachable;
  8. Manage state machine lifecycle and state queries

    main

    The following methods allow you to inspect and control the current state of an FSM instance:

    • currentState(): Returns the current StateType.
    • isCurrently(requested_state): Returns true if the current state matches the requested one.
    • isInStartState(): Returns true if the current state is the initial state.
    • isInFinalState(): Returns true if the current state is one of the defined final states.
    • isFinalState(state): Checks if a specific state is marked as a final state.
    • restart(): Unconditionally resets the current state to the initial start state.
    • safeRestart(): Resets to the start state, but returns StateError.Invalid if the machine is not currently in a final state or the start state.
    • setStartState(state): Changes the start state and immediately sets it as the current state.

    Example

    if (fsm.isInFinalState()) {
        fsm.restart();
    }
    if (fsm.isCurrently(.running)) {
        // do something
    }
    
    if (fsm.isInFinalState()) {
        fsm.safeRestart() catch |err| {
            // handle error if not in a valid state to restart
        };
    }