2026-07-10 08:52:52 +03:00
# SNanoSM
2026-07-16 18:48:29 +03:00
A pure Python, minimalistic, and fully typed library for the implementation of **Mealy Finite State Machines** .
2026-07-10 08:52:52 +03:00
2026-07-16 18:48:29 +03:00
Part of Nobody Industry's **MFFP (Made From First Principles)** set of libraries.
2026-07-10 08:52:52 +03:00
2026-07-16 18:48:29 +03:00
---
2026-07-10 08:52:52 +03:00
2026-07-16 18:48:29 +03:00
## Table of Contents
1. [Overview & Core Concept ](#overview--core-concept )
2. [Installation ](#installation )
3. [Quick Start (Binary Inverter) ](#quick-start-binary-inverter )
4. [Advanced Usage (Sequence Detector & Fallback Matching) ](#advanced-usage-sequence-detector--fallback-matching )
5. [API Reference ](#api-reference )
6. [Testing & Verification ](#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.
2026-07-16 18:51:43 +03:00
In [mealy.py ](src/snanosm/mealy.py ), this is modeled by:
2026-07-16 18:48:29 +03:00
- **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
2026-07-16 18:51:43 +03:00
Since the library uses [pyproject.toml ](pyproject.toml ) with the Hatchling build backend, you can install it locally in editable mode or build it using standard tools:
2026-07-16 18:48:29 +03:00
```bash
# Install in editable mode
pip install -e .
# Or build the package
python -m build
```
---
## Quick Start (Binary Inverter)
2026-07-16 18:51:43 +03:00
Here is a simple example demonstrating how to invert a binary string (`"0"` becomes `"1"` , `"1"` becomes `"0"` ) using [inverter.py ](examples/inverter.py ).
2026-07-16 18:48:29 +03:00
```python
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)
2026-07-16 18:51:43 +03:00
The sequence detector in [detector.py ](examples/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.
2026-07-16 18:48:29 +03:00
```python
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 |
| :--- | :--- |
2026-07-16 18:51:43 +03:00
| `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. |
2026-07-16 18:48:29 +03:00
---
### API Details
2026-07-16 18:51:43 +03:00
#### `InputProtocol`
2026-07-16 18:48:29 +03:00
```python
class InputProtocol ( Protocol ):
def __eq__ ( self , __o : Self ) -> bool : ...
def __hash__ ( self ) -> int : ...
```
2026-07-16 18:51:43 +03:00
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).
2026-07-16 18:48:29 +03:00
2026-07-16 18:51:43 +03:00
#### `TransitionInputEnum`
2026-07-16 18:48:29 +03:00
- `MATCH_REST` : Activates if no matching transition input is found for the current state. Useful for defining default fallback transitions.
2026-07-16 18:51:43 +03:00
#### `State`
2026-07-16 18:48:29 +03:00
- `get_name() -> str` : Returns the state's name.
- `__str__() -> str` : Returns `[State <state_name>]` .
2026-07-16 18:51:43 +03:00
#### `Transition`
2026-07-16 18:48:29 +03:00
- `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>)]` .
2026-07-16 18:51:43 +03:00
#### `Machine`
2026-07-16 18:48:29 +03:00
- `__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.
2026-07-16 18:51:43 +03:00
- `transition_input` : An input conforming to `InputProtocol` or `TransitionInputEnum` .
2026-07-16 18:48:29 +03:00
- `output_function` : A callable accepting context, run upon transitioning.
- `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` .
2026-07-16 18:51:43 +03:00
- `get_current_state() -> Optional[State]` : Returns the current `State` object, or `None` if the machine has not started or processed any inputs.
2026-07-16 18:48:29 +03:00
- `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
2026-07-16 18:51:43 +03:00
Unit tests are located in [test_mealy.py ](tests/test_mealy.py ). To run the test suite, navigate to the project directory and execute:
2026-07-16 18:48:29 +03:00
```bash
PYTHONPATH = src python -m unittest discover -s tests
```