Compare commits

..

No commits in common. "2a469e10e819c9b8866b94805c908c86b25f0486" and "96edecdf2aa5539bea162d6a998ce429acd7aa74" have entirely different histories.

9 changed files with 242 additions and 104 deletions

View File

@ -1,15 +1,4 @@
include: package:very_good_analysis/analysis_options.yaml include: package:flutter_lints/flutter.yaml
linter:
rules:
public_member_api_docs: false
lines_longer_than_80_chars: false
use_setters_to_change_properties: false
avoid_positional_boolean_parameters: false
avoid_bool_literals_in_conditional_expressions: false
analyzer: # Additional information about this file can be found at
exclude: # https://dart.dev/guides/language/analysis-options
- "**/*.g.dart"
- "**/*.freezed.dart"
- "test/"
- "integration_test/"

View File

@ -2,11 +2,11 @@
"nodes": { "nodes": {
"flake-utils": { "flake-utils": {
"locked": { "locked": {
"lastModified": 1667395993, "lastModified": 1649676176,
"narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=", "narHash": "sha256-OWKJratjt2RW151VUlJPRALb7OU2S5s+f0vLj4o1bHM=",
"owner": "numtide", "owner": "numtide",
"repo": "flake-utils", "repo": "flake-utils",
"rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f", "rev": "a4b154ebbdc88c8498a5c7b01589addc9e9cb678",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -17,16 +17,16 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1667610399, "lastModified": 1650034868,
"narHash": "sha256-XZd0f4ZWAY0QOoUSdiNWj/eFiKb4B9CJPtl9uO9SYY4=", "narHash": "sha256-OAaf5BdWKGXTXvYnbvJuoQjSWnVKgt1cIOChF0MFt2o=",
"owner": "NixOS", "owner": "PapaTutuWawa",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "1dd8696f96db47156e1424a49578fe7dd4ce99a4", "rev": "13a5646d450052b88067cab37b198f8a2737e431",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "NixOS", "owner": "PapaTutuWawa",
"ref": "nixpkgs-unstable", "ref": "nixos-unstable-flutter-2.13.0-0.1.pre",
"repo": "nixpkgs", "repo": "nixpkgs",
"type": "github" "type": "github"
} }

View File

@ -1,17 +1,14 @@
{ {
description = "moxlib"; description = "moxlib";
inputs = { inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; nixpkgs.url = "github:PapaTutuWawa/nixpkgs/nixos-unstable-flutter-2.13.0-0.1.pre";
flake-utils.url = "github:numtide/flake-utils"; flake-utils.url = "github:numtide/flake-utils";
}; };
outputs = { self, nixpkgs, flake-utils }: flake-utils.lib.eachDefaultSystem (system: let outputs = { self, nixpkgs, flake-utils }: flake-utils.lib.eachDefaultSystem (system: let
pkgs = import nixpkgs { pkgs = import nixpkgs {
inherit system; inherit system;
config = { config.android_sdk.accept_license = true;
android_sdk.accept_license = true;
allowUnfree = true;
};
}; };
android = pkgs.androidenv.composeAndroidPackages { android = pkgs.androidenv.composeAndroidPackages {
# TODO: Find a way to pin these # TODO: Find a way to pin these
@ -29,11 +26,11 @@
useGoogleAPIs = false; useGoogleAPIs = false;
useGoogleTVAddOns = false; useGoogleTVAddOns = false;
}; };
pinnedJDK = pkgs.jdk; pinnedJDK = pkgs.jdk11;
in { in {
devShell = pkgs.mkShell { devShell = pkgs.mkShell {
buildInputs = with pkgs; [ buildInputs = with pkgs; [
flutter pinnedJDK android.platform-tools dart # Flutter flutterPackages.beta pinnedJDK android.platform-tools flutterPackages.dart-beta # Flutter
gitlint jq # Code hygiene gitlint jq # Code hygiene
ripgrep # General utilities ripgrep # General utilities
]; ];

99
lib/automaton.dart Normal file
View File

@ -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<T, I> {
/// The current state of the DFA
T _state;
/// The edges of the DFA: State x Input -> State
Map<T, Map<I, T>> _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<T, I> = void Function(T oldState, I input);
class MealyAutomaton<T, I> {
/// The base automaton
final DeterministicFiniteAutomaton<T, I> _automaton;
/// Mapping of State x Input -> Output callback
Map<T, Map<I, MealyAutomatonCallback<T, I>>> _outputs;
/// Trap state
MealyAutomatonCallback<T, I>? 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<T, I> 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);
}
}

View File

@ -1,43 +1,39 @@
import 'dart:async'; import "dart:async";
import 'package:logging/logging.dart';
import 'package:meta/meta.dart'; import "package:synchronized/synchronized.dart";
import 'package:synchronized/synchronized.dart'; import "package:uuid/uuid.dart";
import 'package:uuid/uuid.dart'; import "package:logging/logging.dart";
import "package:meta/meta.dart";
/// Interface to allow arbitrary data to be sent as long as it can be /// Interface to allow arbitrary data to be sent as long as it can be
/// JSON serialized/deserialized. /// JSON serialized/deserialized.
class JsonImplementation { class JsonImplementation {
JsonImplementation(); JsonImplementation();
// ignore: avoid_unused_constructor_parameters Map<String, dynamic> toJson() => {};
factory JsonImplementation.fromJson(Map<String, dynamic> json) { factory JsonImplementation.fromJson(Map<String, dynamic> json) {
return JsonImplementation(); return JsonImplementation();
} }
Map<String, dynamic> toJson() => {};
} }
/// Wrapper class that adds an ID to the data packet to be sent. /// Wrapper class that adds an ID to the data packet to be sent.
class DataWrapper<T extends JsonImplementation> { class DataWrapper<T extends JsonImplementation> {
final String id;
final T data;
const DataWrapper( const DataWrapper(
this.id, this.id,
this.data, this.data
); );
/// The id of the data packet.
final String id;
/// The actual data.
final T data;
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
'id': id, "id": id,
'data': data.toJson() "data": data.toJson()
}; };
static DataWrapper fromJson<T extends JsonImplementation>(Map<String, dynamic> json) => DataWrapper<T>( static DataWrapper fromJson<T extends JsonImplementation>(Map<String, dynamic> json) => DataWrapper<T>(
json['id']! as String, json["id"]! as String,
json['data']! as T, json["data"]! as T
); );
DataWrapper reply(T newData) => DataWrapper(id, newData); DataWrapper reply(T newData) => DataWrapper(id, newData);
@ -53,23 +49,16 @@ abstract class AwaitableDataSender<
S extends JsonImplementation, S extends JsonImplementation,
R extends JsonImplementation R extends JsonImplementation
> { > {
final Lock _lock;
final Map<String, Completer<R>> _awaitables;
final Uuid _uuid;
final Logger _log;
@mustCallSuper @mustCallSuper
AwaitableDataSender(); AwaitableDataSender() : _awaitables = {}, _uuid = const Uuid(), _lock = Lock(), _log = Logger("AwaitableDataSender");
/// A mapping of ID to completer for pending requests.
final Map<String, Completer<R>> _awaitables = {};
/// Critical section for accessing [AwaitableDataSender._awaitables].
final Lock _lock = Lock();
/// A UUID object for generating UUIDs.
final Uuid _uuid = const Uuid();
/// A logger.
final Logger _log = Logger('AwaitableDataSender');
@visibleForTesting @visibleForTesting
Map<String, Completer<R>> getAwaitables() => _awaitables; Map<String, Completer> getAwaitables() => _awaitables;
/// Called after an awaitable has been added. /// Called after an awaitable has been added.
@visibleForTesting @visibleForTesting
@ -83,12 +72,11 @@ abstract class AwaitableDataSender<
/// Future will be returned that can be used to await a response. If it /// Future will be returned that can be used to await a response. If it
/// is false, then null will be imediately resolved. /// is false, then null will be imediately resolved.
Future<R?> sendData(S data, { bool awaitable = true, @visibleForTesting String? id }) async { Future<R?> sendData(S data, { bool awaitable = true, @visibleForTesting String? id }) async {
// ignore: no_leading_underscores_for_local_identifiers
final _id = id ?? _uuid.v4(); final _id = id ?? _uuid.v4();
var future = Future<R?>.value(); Future<R?> future = Future.value(null);
_log.fine('sendData: Waiting to acquire lock...'); _log.fine("sendData: Waiting to acquire lock...");
await _lock.synchronized(() async { await _lock.synchronized(() async {
_log.fine('sendData: Done'); _log.fine("sendData: Done");
if (awaitable) { if (awaitable) {
_awaitables[_id] = Completer(); _awaitables[_id] = Completer();
onAdd(); onAdd();
@ -97,15 +85,15 @@ abstract class AwaitableDataSender<
await sendDataImpl( await sendDataImpl(
DataWrapper<S>( DataWrapper<S>(
_id, _id,
data, data
), )
); );
if (awaitable) { if (awaitable) {
future = _awaitables[_id]!.future; future = _awaitables[_id]!.future;
} }
_log.fine('sendData: Releasing lock...'); _log.fine("sendData: Releasing lock...");
}); });
return future; return future;
@ -114,21 +102,24 @@ abstract class AwaitableDataSender<
/// Should be called when a [DataWrapper] has been received. Will resolve /// Should be called when a [DataWrapper] has been received. Will resolve
/// the promise received from [sendData]. /// the promise received from [sendData].
Future<bool> onData(DataWrapper<R> data) async { Future<bool> onData(DataWrapper<R> data) async {
_log.fine('onData: Waiting to acquire lock...'); bool found = false;
final completer = await _lock.synchronized(() async { Completer? completer;
_log.fine('onData: Done'); _log.fine("onData: Waiting to acquire lock...");
final c = _awaitables[data.id]; await _lock.synchronized(() async {
if (c != null) { _log.fine("onData: Done");
_awaitables.remove(data.id); completer = _awaitables[data.id];
return c; if (completer != null) {
} _awaitables.remove(data.id);
found = true;
}
_log.fine('onData: Releasing lock'); _log.fine("onData: Releasing lock");
return null;
}); });
completer?.complete(data.data); if (found) {
completer!.complete(data.data);
}
return completer != null; return found;
} }
} }

View File

@ -1,16 +0,0 @@
/// A wrapper around List<T>.firstWhere that does not throw but instead just
/// returns true if [test] returns true for an element or false if [test] never
/// returned true.
bool listContains<T>(List<T> list, bool Function(T element) test) {
return firstWhereOrNull<T>(list, test) != null;
}
/// A wrapper around [List<T>.firstWhere] that does not throw but instead just
/// return null if [test] never returned true
T? firstWhereOrNull<T>(List<T> list, bool Function(T element) test) {
try {
return list.firstWhere(test);
} catch(e) {
return null;
}
}

View File

@ -1,5 +1,5 @@
library moxlib; library moxlib;
export 'awaitabledatasender.dart'; export "awaitabledatasender.dart";
export 'lists.dart'; export "automaton.dart";
export 'math.dart'; export "math.dart";

View File

@ -1,6 +1,6 @@
name: moxlib name: moxlib
description: A collection of code for sharing between various moxxy libraries. Not inteded for outside use. description: A collection of code for sharing between various moxxy libraries. Not inteded for outside use.
version: 0.1.5 version: 0.1.4
homepage: https://codeberg.org/moxxy/moxlib homepage: https://codeberg.org/moxxy/moxlib
publish_to: https://git.polynom.me/api/packages/Moxxy/pub publish_to: https://git.polynom.me/api/packages/Moxxy/pub
@ -9,11 +9,10 @@ environment:
dependencies: dependencies:
logging: ^1.0.2 logging: ^1.0.2
meta: ^1.7.0
synchronized: ^3.0.0 synchronized: ^3.0.0
uuid: ^3.0.5 uuid: ^3.0.5
meta: ^1.7.0
dev_dependencies: dev_dependencies:
flutter_lints: ^2.0.0 flutter_lints: ^2.0.0
test: ^1.20.1 test: ^1.20.1
very_good_analysis: ^3.0.1

79
test/automaton_test.dart Normal file
View File

@ -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, int>(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, int>(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, int>(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, int>(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);
});
}