From f92bef517fea736764c28ce9ac049766514db72c Mon Sep 17 00:00:00 2001 From: "Alexander \"PapaTutuWawa" Date: Fri, 13 May 2022 13:40:52 +0200 Subject: [PATCH] feat: Implement a DFA and Mealy Automaton --- CHANGELOG.md | 4 ++ lib/automaton.dart | 99 ++++++++++++++++++++++++++++++++++++++++ lib/moxlib.dart | 1 + pubspec.yaml | 2 +- test/automaton_test.dart | 79 ++++++++++++++++++++++++++++++++ 5 files changed, 184 insertions(+), 1 deletion(-) create mode 100644 lib/automaton.dart create mode 100644 test/automaton_test.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fabc94..aaa5df1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,3 +5,7 @@ ## 0.1.1 * Switch to selfhosted pub repository + +## 0.1.2 + +* Add [DeterministicFiniteAutomaton] and [MealyAutomaton] diff --git a/lib/automaton.dart b/lib/automaton.dart new file mode 100644 index 0000000..04cf116 --- /dev/null +++ b/lib/automaton.dart @@ -0,0 +1,99 @@ +class NoTransitionPossibleException implements Exception { + @override + String errMsg() => "The transition graph allows no transition"; +} + +/// A deterministic finite automaton. [T] is the state type while +/// [I] is the input type. +/// Edges of the node must be added with [addTransition]. If a trap state +/// is required, it can be set in the constructor. +class DeterministicFiniteAutomaton { + /// The current state of the DFA + T _state; + /// The edges of the DFA: State x Input -> State + Map> _transitions; + /// Trap state + T? trapState; + + /// The argument is the initial state + DeterministicFiniteAutomaton(this._state, { this.trapState }) : _transitions = {}; + + T get state => _state; + + void addTransition(T oldState, I input, T newState) { + assert(oldState != trapState); + // These are handled implicitly if no transition has been found + assert(newState != trapState); + + if (!_transitions.containsKey(oldState)) { + _transitions[oldState] = {}; + } + + _transitions[oldState]![input] = newState; + } + + /// Transition the DFA based on its current state and the input [input]. + void onInput(I input) { + final newState = _transitions[_state]?[input]; + if (newState == null) { + // Go to the trap state if we can + if (trapState != null) { + _state = trapState!; + return; + } else { + throw NoTransitionPossibleException(); + } + } + + _state = newState; + } + + /// Returns where [input] would take the automaton to. Returns null if no transition + /// is possible, ignoring trap transitions. + T? peekTransition(I input) { + if (!_transitions.containsKey(_state) || !_transitions[_state]!.containsKey(input)) { + return null; + } + + return _transitions[_state]![input]!; + } +} + +typedef MealyAutomatonCallback = void Function(T oldState, I input); +class MealyAutomaton { + /// The base automaton + final DeterministicFiniteAutomaton _automaton; + /// Mapping of State x Input -> Output callback + Map>> _outputs; + /// Trap state + MealyAutomatonCallback? trapCallback; + + // TODO: Assert that trapState != null implies trapCallback != null. + MealyAutomaton(T initialState, { T? trapState, this.trapCallback }) + : _outputs = {}, + _automaton = DeterministicFiniteAutomaton(initialState, trapState: trapState); + + T get state => _automaton.state; + + void addTransition(T oldState, I input, T newState, MealyAutomatonCallback callback) { + _automaton.addTransition(oldState, input, newState); + + if (!_outputs.containsKey(oldState)) { + _outputs[oldState] = {}; + } + + _outputs[oldState]![input] = callback; + } + + void onInput(I input) { + final _state = _automaton.state; + if (_automaton.peekTransition(input) == null && trapCallback == null) { + throw new NoTransitionPossibleException(); + } + + final callback = _outputs[_state]?[input] ?? trapCallback!; + + _automaton.onInput(input); + callback(_state, input); + } +} diff --git a/lib/moxlib.dart b/lib/moxlib.dart index b4ea786..e6c6194 100644 --- a/lib/moxlib.dart +++ b/lib/moxlib.dart @@ -1,3 +1,4 @@ library moxlib; export "awaitabledatasender.dart"; +export "automaton.dart"; diff --git a/pubspec.yaml b/pubspec.yaml index 36345e8..9e23672 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: moxlib description: A collection of code for sharing between various moxxy libraries. Not inteded for outside use. -version: 0.1.1 +version: 0.1.2 homepage: https://codeberg.org/moxxy/moxlib publish_to: https://pub.polynom.me diff --git a/test/automaton_test.dart b/test/automaton_test.dart new file mode 100644 index 0000000..70a1f97 --- /dev/null +++ b/test/automaton_test.dart @@ -0,0 +1,79 @@ +import "package:moxlib/automaton.dart"; + +import "package:test/test.dart"; + +enum States { + a, b, c, trap +} + +void main() { + test("Test a simple DFA", () { + final automaton = DeterministicFiniteAutomaton(States.a); + automaton.addTransition(States.a, 1, States.b); + automaton.addTransition(States.b, 2, States.c); + automaton.addTransition(States.c, 3, States.a); + + expect(automaton.state, States.a); + automaton.onInput(1); + expect(automaton.state, States.b); + automaton.onInput(2); + expect(automaton.state, States.c); + automaton.onInput(3); + expect(automaton.state, States.a); + }); + + test("Test a simple DFA with a trap state", () { + final automaton = DeterministicFiniteAutomaton(States.a, trapState: States.trap); + automaton.addTransition(States.a, 1, States.b); + automaton.addTransition(States.b, 2, States.c); + automaton.addTransition(States.c, 3, States.a); + + expect(automaton.state, States.a); + automaton.onInput(1); + expect(automaton.state, States.b); + automaton.onInput(2); + expect(automaton.state, States.c); + automaton.onInput(4); + expect(automaton.state, States.trap); + + // Transitioning away from the trap state should not be possible + automaton.onInput(5); + expect(automaton.state, States.trap); + }); + + test("Test a simple Mealy Automaton", () { + bool called = false; + final callback = (state, input) { + called = true; + }; + final automaton = MealyAutomaton(States.a); + + automaton.addTransition(States.a, 1, States.b, callback); + + automaton.onInput(1); + + expect(automaton.state, States.b); + expect(called, true); + }); + + test("Test a simple Mealy Automaton with a trap state", () { + bool called = false; + bool trapCalled = false; + final callback = (state, input) { + called = true; + }; + final trapCallback = (state, input) { + trapCalled = true; + }; + final automaton = MealyAutomaton(States.a, trapState: States.trap, trapCallback: trapCallback); + + automaton.addTransition(States.a, 1, States.b, callback); + + automaton.onInput(1); + expect(called, true); + + automaton.onInput(1); + expect(automaton.state, States.trap); + expect(trapCalled, true); + }); +}