diff --git a/README.md b/README.md index 501d96a..da81b3d 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,210 @@ # SNanoSM -A pure python, minimalistic, typed library for the implementation of Mealy Finite State Machines. +A pure Python, minimalistic, and fully typed library for the implementation of **Mealy Finite State Machines**. -Part of the Nobody Industry's MFFP (made from first principles) set of libraries. +Part of Nobody Industry's **MFFP (Made From First Principles)** set of libraries. -## Features +--- -- Typed -- Full test coverage \ No newline at end of file +## 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. + +In [mealy.py](file:///Users/davidgildegomezperez/PycharmProjects/snanosm/src/snanosm/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](file:///Users/davidgildegomezperez/PycharmProjects/snanosm/pyproject.toml) with the Hatchling build backend, you can install it locally in editable mode or build it using standard tools: + +```bash +# 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](file:///Users/davidgildegomezperez/PycharmProjects/snanosm/examples/inverter.py). + +```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) + +The sequence detector in [detector.py](file:///Users/davidgildegomezperez/PycharmProjects/snanosm/examples/detector.py) searches for the substring `"AB"` within a stream of characters. It illustrates the use of [TransitionInputEnum](file:///Users/davidgildegomezperez/PycharmProjects/snanosm/src/snanosm/mealy.py#L14) to define catch-all transitions when no specific input matches. + +```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 | +| :--- | :--- | +| [InputProtocol](file:///Users/davidgildegomezperez/PycharmProjects/snanosm/src/snanosm/mealy.py#L6) | A typing protocol requiring `__eq__` and `__hash__`. Any hashable, equatable Python object can serve as machine input. | +| [TransitionInputEnum](file:///Users/davidgildegomezperez/PycharmProjects/snanosm/src/snanosm/mealy.py#L14) | Enum containing special transition inputs (e.g., `MATCH_REST`). | +| [State](file:///Users/davidgildegomezperez/PycharmProjects/snanosm/src/snanosm/mealy.py#L22) | Represents a state node in the state machine. | +| [Transition](file:///Users/davidgildegomezperez/PycharmProjects/snanosm/src/snanosm/mealy.py#L33) | Represents an edge between states triggered by a transition input. | +| [Machine](file:///Users/davidgildegomezperez/PycharmProjects/snanosm/src/snanosm/mealy.py#L53) | The core finite state machine runner. | + +--- + +### API Details + +#### [InputProtocol](file:///Users/davidgildegomezperez/PycharmProjects/snanosm/src/snanosm/mealy.py#L6) +```python +class InputProtocol(Protocol): + def __eq__(self, __o: Self) -> bool: ... + def __hash__(self) -> int: ... +``` +Any custom object used as an input to [Machine.process_input](file:///Users/davidgildegomezperez/PycharmProjects/snanosm/src/snanosm/mealy.py#L103) must implement this protocol (or be natively hashable and equatable, e.g. strings, integers, frozen dataclasses). + +#### [TransitionInputEnum](file:///Users/davidgildegomezperez/PycharmProjects/snanosm/src/snanosm/mealy.py#L14) +- `MATCH_REST`: Activates if no matching transition input is found for the current state. Useful for defining default fallback transitions. + +#### [State](file:///Users/davidgildegomezperez/PycharmProjects/snanosm/src/snanosm/mealy.py#L22) +- `get_name() -> str`: Returns the state's name. +- `__str__() -> str`: Returns `[State ]`. + +#### [Transition](file:///Users/davidgildegomezperez/PycharmProjects/snanosm/src/snanosm/mealy.py#L33) +- `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 (, , )]`. + +#### [Machine](file:///Users/davidgildegomezperez/PycharmProjects/snanosm/src/snanosm/mealy.py#L53) +- `__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. + - `transition_input`: An input conforming to [InputProtocol](file:///Users/davidgildegomezperez/PycharmProjects/snanosm/src/snanosm/mealy.py#L6) or [TransitionInputEnum](file:///Users/davidgildegomezperez/PycharmProjects/snanosm/src/snanosm/mealy.py#L14). + - `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`. +- `get_current_state() -> Optional[State]`: Returns the current [State](file:///Users/davidgildegomezperez/PycharmProjects/snanosm/src/snanosm/mealy.py#L22) 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](file:///Users/davidgildegomezperez/PycharmProjects/snanosm/tests/test_mealy.py). To run the test suite, navigate to the project directory and execute: + +```bash +PYTHONPATH=src python -m unittest discover -s tests +``` \ No newline at end of file