SNanoSM

A pure Python, minimalistic, and fully typed library for the implementation of Mealy Finite State Machines.

Part of Nobody Industry's MFFP (Made From First Principles) set of libraries.


Table of Contents

  1. Overview & Core Concept
  2. Installation
  3. Quick Start (Binary Inverter)
  4. Advanced Usage (Sequence Detector & Fallback Matching)
  5. API Reference
  6. Testing & Verification

Overview & Core Concept

A Mealy State Machine is a finite-state machine whose output values are determined both by its current state and its current inputs.

In mealy.py, this is modeled by:

  • States: Unique nodes in the machine.
  • Transitions: Directed connections between states triggered by specific inputs.
  • Action Functions (Outputs): Arbitrary callbacks associated with transitions that receive a mutable user-defined context object.

Note

By passing a mutable context down to transition actions, you can build rich state-dependent behavior while keeping the machine's state logic simple and decoupled.


Installation

Since the library uses pyproject.toml with the Hatchling build backend, you can install it locally in editable mode or build it using standard tools:

# Install in editable mode
pip install -e .

# Or build the package
python -m build

Quick Start (Binary Inverter)

Here is a simple example demonstrating how to invert a binary string ("0" becomes "1", "1" becomes "0") using inverter.py.

from typing import Tuple, TypedDict
from snanosm.mealy import Machine

# Define a context to hold our state machine's output and metadata
class Context(TypedDict):
    result: str
    n_chars: int

def add_to_result(context: Context, c: str) -> None:
    context["result"] += c
    context["n_chars"] += 1

def inverter(input_string: str) -> Tuple[str, int]:
    # Initialize the mutable context
    context: Context = {
        "result": "",
        "n_chars": 0
    }
    
    # Create the machine with the context
    m = Machine(context)
    
    # Add a start state "S"
    m.add_state("S", is_start_state=True)
    
    # Define transitions: when in state "S" and input is "0", execute action and stay in "S"
    m.add_transition("0", "S", "S", lambda ctx: add_to_result(ctx, "1"))
    m.add_transition("1", "S", "S", lambda ctx: add_to_result(ctx, "0"))
    
    # Process inputs sequentially
    for c in input_string:
        m.process_input(c)
        
    return context["result"], context["n_chars"]

if __name__ == '__main__':
    res, count = inverter("000011110010")
    print(f"Result: {res}, Characters processed: {count}")
    # Output: Result: 111100001101, Characters processed: 12

Advanced Usage (Sequence Detector & Fallback Matching)

The sequence detector in detector.py searches for the substring "AB" within a stream of characters. It illustrates the use of TransitionInputEnum to define catch-all transitions when no specific input matches.

from typing import TypedDict
from snanosm.mealy import Machine, TransitionInputEnum

class Context(TypedDict):
    count: int
    position: int
    matches: int

def dinc(context: Context):
    context["count"] += 1

def dset(context: Context):
    context["position"] = context["count"]
    context["count"] += 1

def dprn(context: Context):
    context["count"] += 1
    print(f"SUBSTRING FOUND AT POSITION: {context['position']}")
    context["matches"] += 1

def detector(input_string: str) -> int:
    initial_context: Context = {
        "count": 0,
        "position": 0,
        "matches": 0,
    }
    
    m = Machine(initial_context)
    m.add_state("Q0", is_start_state=True)
    m.add_state("Q1")
    
    # Q0 -> Q1 on 'A', saving the start position
    m.add_transition("A", "Q0", "Q1", lambda context: dset(context))
    # Catch-all transition: Q0 -> Q0 for any input other than 'A'
    m.add_transition(TransitionInputEnum.MATCH_REST, "Q0", "Q0", lambda context: dinc(context))
    
    # Q1 -> Q0 on 'B', printing match information
    m.add_transition("B", "Q1", "Q0", lambda context: dprn(context))
    # Catch-all transition: Q1 -> Q0 for any input other than 'B'
    m.add_transition(TransitionInputEnum.MATCH_REST, "Q1", "Q0", lambda context: dinc(context))
    
    for c in input_string:
        m.process_input(c)
        
    return initial_context["matches"]

API Reference

Core Abstractions

Class/Type Description
InputProtocol A typing protocol requiring __eq__ and __hash__. Any hashable, equatable Python object can serve as machine input.
TransitionInputEnum Enum containing special transition inputs (e.g., MATCH_REST).
State Represents a state node in the state machine.
Transition Represents an edge between states triggered by a transition input.
Machine The core finite state machine runner.

API Details

InputProtocol

class InputProtocol(Protocol):
    def __eq__(self, __o: Self) -> bool: ...
    def __hash__(self) -> int: ...

Any custom object used as an input to Machine.process_input must implement this protocol (or be natively hashable and equatable, e.g. strings, integers, frozen dataclasses).

TransitionInputEnum

  • MATCH_REST: Activates if no matching transition input is found for the current state. Useful for defining default fallback transitions.

State

  • get_name() -> str: Returns the state's name.
  • __str__() -> str: Returns [State <state_name>].

Transition

  • execute_output(context): Executes the output callback if it was supplied.
  • get_destination_name_hash() -> int: Returns the hash of the destination state name.
  • __str__() -> str: Returns [Transition (<origin>, <destination>, <input>)].

Machine

  • __init__(initial_context: object = None): Initializes the machine. Sets up the configuration using an optional initial context. If not provided, an empty dict {} is instantiated.
  • add_state(state_name: str, is_start_state: bool = False, is_end_state: bool = False) -> None: Registers a new state node in the machine.

    Warning

    State names must not start with # (reserved for internal configurations). A machine cannot have multiple start states.

  • add_transition(transition_input: TransitionInput, origin_name: str, destination_name: str, output_function: Optional[Callable[[Optional[object]], None]]) -> None: Registers a transition edge between two existing states.
  • process_input(i: Input) -> None: Processes a single input token. It evaluates transitions registered under the current state.
    • If a transition matching i is registered, it will be executed.
    • If no matching transition is found but a MATCH_REST transition is registered, that fallback is executed.
    • If no valid transition is found, it raises a ValueError.
  • reset() -> None: Resets the state machine's active state back to the start state, and re-assigns the context back to initial_context.
  • get_current_state() -> Optional[State]: Returns the current State object, or None if the machine has not started or processed any inputs.
  • is_in_final_state() -> bool: Returns True if the machine's current state is registered as an end state.
  • __str__() -> str: Returns a structured string layout of the machine structure, lists of states, and transitions.

Testing & Verification

Unit tests are located in test_mealy.py. To run the test suite, navigate to the project directory and execute:

PYTHONPATH=src python -m unittest discover -s tests
S
Description
No description provided
Readme MIT
50 KiB
Languages
Python 99.4%
Shell 0.6%