From 85838c2b325064a5cbd420b514b3faf39044088e Mon Sep 17 00:00:00 2001 From: Dan Cojocaru Date: Sun, 19 Jun 2022 18:23:16 +0300 Subject: [PATCH] Implemented logic expressions Also made truth table horizontally scrollable --- lib/components/logic_expression_field.dart | 72 ++++++ lib/models/project.dart | 4 +- lib/models/project.freezed.dart | 61 ++++- lib/models/project.g.dart | 6 +- lib/pages/edit_component.dart | 243 +++++++++++++++-- lib/state/project.dart | 1 + lib/utils/iterable_extension.dart | 18 ++ lib/utils/logic_expressions.dart | 286 +++++++++++++++++++++ lib/utils/logic_expressions.freezed.dart | 159 ++++++++++++ lib/utils/logic_operators.dart | 138 ++++++++++ 10 files changed, 944 insertions(+), 44 deletions(-) create mode 100644 lib/components/logic_expression_field.dart create mode 100644 lib/utils/logic_expressions.dart create mode 100644 lib/utils/logic_expressions.freezed.dart create mode 100644 lib/utils/logic_operators.dart diff --git a/lib/components/logic_expression_field.dart b/lib/components/logic_expression_field.dart new file mode 100644 index 0000000..37d0861 --- /dev/null +++ b/lib/components/logic_expression_field.dart @@ -0,0 +1,72 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:logic_circuits_simulator/utils/logic_expressions.dart'; +import 'package:logic_circuits_simulator/utils/logic_operators.dart'; + +class LogicExpressionField extends HookWidget { + final ValueListenable> inputsListener; + final String outputName; + final String? initialText; + final void Function(String input, LogicExpression expression)? onChanged; + final void Function()? onInputError; + + const LogicExpressionField({required this.inputsListener, required this.outputName, this.initialText, this.onChanged, this.onInputError, super.key}); + + @override + Widget build(BuildContext context) { + final inputs = useValueListenable(inputsListener); + final controller = useTextEditingController(text: initialText); + final errorText = useState(null); + useValueListenable(controller); + + final onChg = useMemoized(() => (String newValue) { + final trimmed = newValue.trim(); + + try { + if (trimmed.isEmpty) { + onChanged?.call('', LogicExpression(operator: FalseLogicOperator(), arguments: [])); + } + else { + final newLogicExpression = LogicExpression.parse(trimmed); + + // Check if unknown inputs are used + final newInputs = newLogicExpression.inputs; + final unknownInputs = newInputs.where((input) => !inputs.contains(input)).toList(); + if (unknownInputs.isNotEmpty) { + throw Exception('Unknown inputs found: ${unknownInputs.join(", ")}'); + } + + onChanged?.call(trimmed, newLogicExpression); + } + errorText.value = null; + } catch (e) { + errorText.value = e.toString(); + onInputError?.call(); + } + }, [inputs, errorText]); + useEffect( + () { + if (controller.text.isNotEmpty) { + scheduleMicrotask(() { + onChg(controller.text); + }); + } + return null; + }, + [inputs], + ); + + return TextField( + controller: controller, + onChanged: onChg, + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: 'Logic Experssion for $outputName', + errorText: errorText.value, + ), + ); + } +} \ No newline at end of file diff --git a/lib/models/project.dart b/lib/models/project.dart index 686f5eb..b83e9c2 100644 --- a/lib/models/project.dart +++ b/lib/models/project.dart @@ -24,7 +24,9 @@ class ComponentEntry with _$ComponentEntry { @JsonKey(includeIfNull: false) List? truthTable, @JsonKey(includeIfNull: false) - String? logicExpression, + List? logicExpression, + @JsonKey(defaultValue: false) + required bool visualDesigned, }) = _ComponentEntry; factory ComponentEntry.fromJson(Map json) => _$ComponentEntryFromJson(json); diff --git a/lib/models/project.freezed.dart b/lib/models/project.freezed.dart index 5457698..cf77fd3 100644 --- a/lib/models/project.freezed.dart +++ b/lib/models/project.freezed.dart @@ -167,7 +167,9 @@ mixin _$ComponentEntry { @JsonKey(includeIfNull: false) List? get truthTable => throw _privateConstructorUsedError; @JsonKey(includeIfNull: false) - String? get logicExpression => throw _privateConstructorUsedError; + List? get logicExpression => throw _privateConstructorUsedError; + @JsonKey(defaultValue: false) + bool get visualDesigned => throw _privateConstructorUsedError; Map toJson() => throw _privateConstructorUsedError; @JsonKey(ignore: true) @@ -187,7 +189,8 @@ abstract class $ComponentEntryCopyWith<$Res> { List inputs, List outputs, @JsonKey(includeIfNull: false) List? truthTable, - @JsonKey(includeIfNull: false) String? logicExpression}); + @JsonKey(includeIfNull: false) List? logicExpression, + @JsonKey(defaultValue: false) bool visualDesigned}); } /// @nodoc @@ -208,6 +211,7 @@ class _$ComponentEntryCopyWithImpl<$Res> Object? outputs = freezed, Object? truthTable = freezed, Object? logicExpression = freezed, + Object? visualDesigned = freezed, }) { return _then(_value.copyWith( componentId: componentId == freezed @@ -237,7 +241,11 @@ class _$ComponentEntryCopyWithImpl<$Res> logicExpression: logicExpression == freezed ? _value.logicExpression : logicExpression // ignore: cast_nullable_to_non_nullable - as String?, + as List?, + visualDesigned: visualDesigned == freezed + ? _value.visualDesigned + : visualDesigned // ignore: cast_nullable_to_non_nullable + as bool, )); } } @@ -256,7 +264,8 @@ abstract class _$$_ComponentEntryCopyWith<$Res> List inputs, List outputs, @JsonKey(includeIfNull: false) List? truthTable, - @JsonKey(includeIfNull: false) String? logicExpression}); + @JsonKey(includeIfNull: false) List? logicExpression, + @JsonKey(defaultValue: false) bool visualDesigned}); } /// @nodoc @@ -279,6 +288,7 @@ class __$$_ComponentEntryCopyWithImpl<$Res> Object? outputs = freezed, Object? truthTable = freezed, Object? logicExpression = freezed, + Object? visualDesigned = freezed, }) { return _then(_$_ComponentEntry( componentId: componentId == freezed @@ -306,9 +316,13 @@ class __$$_ComponentEntryCopyWithImpl<$Res> : truthTable // ignore: cast_nullable_to_non_nullable as List?, logicExpression: logicExpression == freezed - ? _value.logicExpression + ? _value._logicExpression : logicExpression // ignore: cast_nullable_to_non_nullable - as String?, + as List?, + visualDesigned: visualDesigned == freezed + ? _value.visualDesigned + : visualDesigned // ignore: cast_nullable_to_non_nullable + as bool, )); } } @@ -323,10 +337,12 @@ class _$_ComponentEntry implements _ComponentEntry { required final List inputs, required final List outputs, @JsonKey(includeIfNull: false) final List? truthTable, - @JsonKey(includeIfNull: false) this.logicExpression}) + @JsonKey(includeIfNull: false) final List? logicExpression, + @JsonKey(defaultValue: false) required this.visualDesigned}) : _inputs = inputs, _outputs = outputs, - _truthTable = truthTable; + _truthTable = truthTable, + _logicExpression = logicExpression; factory _$_ComponentEntry.fromJson(Map json) => _$$_ComponentEntryFromJson(json); @@ -362,13 +378,23 @@ class _$_ComponentEntry implements _ComponentEntry { return EqualUnmodifiableListView(value); } + final List? _logicExpression; @override @JsonKey(includeIfNull: false) - final String? logicExpression; + List? get logicExpression { + final value = _logicExpression; + if (value == null) return null; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(value); + } + + @override + @JsonKey(defaultValue: false) + final bool visualDesigned; @override String toString() { - return 'ComponentEntry(componentId: $componentId, componentName: $componentName, componentDescription: $componentDescription, inputs: $inputs, outputs: $outputs, truthTable: $truthTable, logicExpression: $logicExpression)'; + return 'ComponentEntry(componentId: $componentId, componentName: $componentName, componentDescription: $componentDescription, inputs: $inputs, outputs: $outputs, truthTable: $truthTable, logicExpression: $logicExpression, visualDesigned: $visualDesigned)'; } @override @@ -387,7 +413,9 @@ class _$_ComponentEntry implements _ComponentEntry { const DeepCollectionEquality() .equals(other._truthTable, _truthTable) && const DeepCollectionEquality() - .equals(other.logicExpression, logicExpression)); + .equals(other._logicExpression, _logicExpression) && + const DeepCollectionEquality() + .equals(other.visualDesigned, visualDesigned)); } @JsonKey(ignore: true) @@ -400,7 +428,8 @@ class _$_ComponentEntry implements _ComponentEntry { const DeepCollectionEquality().hash(_inputs), const DeepCollectionEquality().hash(_outputs), const DeepCollectionEquality().hash(_truthTable), - const DeepCollectionEquality().hash(logicExpression)); + const DeepCollectionEquality().hash(_logicExpression), + const DeepCollectionEquality().hash(visualDesigned)); @JsonKey(ignore: true) @override @@ -421,7 +450,8 @@ abstract class _ComponentEntry implements ComponentEntry { required final List inputs, required final List outputs, @JsonKey(includeIfNull: false) final List? truthTable, - @JsonKey(includeIfNull: false) final String? logicExpression}) = + @JsonKey(includeIfNull: false) final List? logicExpression, + @JsonKey(defaultValue: false) required final bool visualDesigned}) = _$_ComponentEntry; factory _ComponentEntry.fromJson(Map json) = @@ -443,7 +473,10 @@ abstract class _ComponentEntry implements ComponentEntry { List? get truthTable => throw _privateConstructorUsedError; @override @JsonKey(includeIfNull: false) - String? get logicExpression => throw _privateConstructorUsedError; + List? get logicExpression => throw _privateConstructorUsedError; + @override + @JsonKey(defaultValue: false) + bool get visualDesigned => throw _privateConstructorUsedError; @override @JsonKey(ignore: true) _$$_ComponentEntryCopyWith<_$_ComponentEntry> get copyWith => diff --git a/lib/models/project.g.dart b/lib/models/project.g.dart index 3c43770..f2a8137 100644 --- a/lib/models/project.g.dart +++ b/lib/models/project.g.dart @@ -30,7 +30,10 @@ _$_ComponentEntry _$$_ComponentEntryFromJson(Map json) => truthTable: (json['truthTable'] as List?) ?.map((e) => e as String) .toList(), - logicExpression: json['logicExpression'] as String?, + logicExpression: (json['logicExpression'] as List?) + ?.map((e) => e as String) + .toList(), + visualDesigned: json['visualDesigned'] as bool? ?? false, ); Map _$$_ComponentEntryToJson(_$_ComponentEntry instance) { @@ -50,5 +53,6 @@ Map _$$_ComponentEntryToJson(_$_ComponentEntry instance) { val['outputs'] = instance.outputs; writeNotNull('truthTable', instance.truthTable); writeNotNull('logicExpression', instance.logicExpression); + val['visualDesigned'] = instance.visualDesigned; return val; } diff --git a/lib/pages/edit_component.dart b/lib/pages/edit_component.dart index b24ac3a..67421a3 100644 --- a/lib/pages/edit_component.dart +++ b/lib/pages/edit_component.dart @@ -3,11 +3,14 @@ import 'dart:math'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:logic_circuits_simulator/components/logic_expression_field.dart'; import 'package:logic_circuits_simulator/components/truth_table.dart'; import 'package:logic_circuits_simulator/dialogs/new_ask_for_name.dart'; import 'package:logic_circuits_simulator/models/project.dart'; import 'package:logic_circuits_simulator/state/project.dart'; import 'package:logic_circuits_simulator/utils/iterable_extension.dart'; +import 'package:logic_circuits_simulator/utils/logic_expressions.dart'; +import 'package:logic_circuits_simulator/utils/logic_operators.dart'; import 'package:logic_circuits_simulator/utils/provider_hook.dart'; class EditComponentPage extends HookWidget { @@ -24,13 +27,21 @@ class EditComponentPage extends HookWidget { final projectState = useProvider(); ComponentEntry ce() => projectState.index.components.where((c) => c.componentId == component.componentId).first; final truthTable = useState(ce().truthTable?.toList()); - final logicExpression = useState(ce().logicExpression); + final logicExpressions = useState(ce().logicExpression); + final logicExpressionsParsed = useState( + logicExpressions.value == null + ? null + : List.generate(logicExpressions.value!.length, (index) => null), + ); + final visualDesigned = useState(ce().visualDesigned); final inputs = useState(ce().inputs.toList()); final outputs = useState(ce().outputs.toList()); final componentNameEditingController = useTextEditingController(text: ce().componentName); useValueListenable(componentNameEditingController); final dirty = useMemoized( () { + const le = ListEquality(); + if (componentNameEditingController.text.isEmpty) { // Don't allow saving empty name return false; @@ -43,24 +54,31 @@ class EditComponentPage extends HookWidget { // Don't allow saving empty outputs return false; } - if (truthTable.value == null && logicExpression.value == null) { + if (truthTable.value == null && logicExpressions.value == null && !visualDesigned.value) { // Don't allow saving components without functionality return false; } + if (logicExpressionsParsed.value != null && logicExpressionsParsed.value?.contains(null) != false) { + // Don't allow saving components with errors in parsing logic expressions + return false; + } if (componentNameEditingController.text != ce().componentName) { return true; } - if (!const ListEquality().equals(inputs.value, ce().inputs)) { + if (!le.equals(inputs.value, ce().inputs)) { return true; } - if (!const ListEquality().equals(outputs.value, ce().outputs)) { + if (!le.equals(outputs.value, ce().outputs)) { return true; } - if (!const ListEquality().equals(truthTable.value, ce().truthTable)) { + if (!le.equals(truthTable.value, ce().truthTable)) { return true; } - if (logicExpression.value != ce().logicExpression) { + if (!le.equals(logicExpressions.value, ce().logicExpression)) { + return true; + } + if (visualDesigned.value != ce().visualDesigned) { return true; } return false; @@ -74,9 +92,31 @@ class EditComponentPage extends HookWidget { ce().outputs, truthTable.value, ce().truthTable, + logicExpressions.value, + ce().logicExpression, + visualDesigned.value, + ce().visualDesigned, + logicExpressionsParsed.value, ], ); + final updateTTFromLE = useMemoized( + () { + return () { + if (logicExpressionsParsed.value?.contains(null) != false) { + truthTable.value = null; + } + else { + truthTable.value = logicExpressionsParsed.value!.first!.computeTruthTable(inputs.value).zipWith( + logicExpressionsParsed.value!.skip(1).map((le) => le!.computeTruthTable(inputs.value)), + (args) => args.join(), + ).toList(); + } + }; + }, + [logicExpressions.value, truthTable.value] + ); + return WillPopScope( onWillPop: () async { if (!dirty && !newComponent) { @@ -137,7 +177,7 @@ class EditComponentPage extends HookWidget { ), SliverToBoxAdapter( child: Padding( - padding: const EdgeInsets.all(8.0), + padding: const EdgeInsets.all(16.0), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ @@ -161,7 +201,13 @@ class EditComponentPage extends HookWidget { }, ); if (inputName != null) { - truthTable.value = truthTable.value?.expand((element) => [element, element]).toList(); + if (logicExpressions.value != null) { + // They should update themselves + updateTTFromLE(); + } + else if (truthTable.value != null) { + truthTable.value = truthTable.value?.expand((element) => [element, element]).toList(); + } inputs.value = inputs.value.toList()..add(inputName); } }, @@ -205,7 +251,11 @@ class EditComponentPage extends HookWidget { }, ); if (shouldRemove == true) { - if (truthTable.value != null) { + if (logicExpressions.value != null) { + // They should update themselves + updateTTFromLE(); + } + else if (truthTable.value != null) { final tt = truthTable.value!.toList(); final shiftIndex = inputs.value.length - 1 - idx; final shifted = 1 << shiftIndex; @@ -230,7 +280,7 @@ class EditComponentPage extends HookWidget { ), SliverToBoxAdapter( child: Padding( - padding: const EdgeInsets.all(8.0), + padding: const EdgeInsets.all(16.0), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ @@ -254,7 +304,14 @@ class EditComponentPage extends HookWidget { }, ); if (outputName != null) { - truthTable.value = truthTable.value?.map((e) => '${e}0').toList(); + if (logicExpressions.value != null) { + logicExpressions.value = logicExpressions.value!.followedBy(['']).toList(); + logicExpressionsParsed.value = logicExpressionsParsed.value?.followedBy([LogicExpression.ofZeroOp(FalseLogicOperator())]).toList(); + updateTTFromLE(); + } + else if (truthTable.value != null) { + truthTable.value = truthTable.value?.map((e) => '${e}0').toList(); + } outputs.value = outputs.value.toList()..add(outputName); } }, @@ -298,7 +355,12 @@ class EditComponentPage extends HookWidget { }, ); if (shouldRemove == true) { - if (truthTable.value != null) { + if (logicExpressions.value != null) { + logicExpressions.value = logicExpressions.value?.toList()?..replaceRange(idx, idx+1, []); + logicExpressionsParsed.value = logicExpressionsParsed.value?.toList()?..replaceRange(idx, idx+1, []); + updateTTFromLE(); + } + else if (truthTable.value != null) { for (var i = 0; i < truthTable.value!.length; i++) { truthTable.value!.replaceRange(i, i+1, [truthTable.value![i].replaceRange(idx, idx+1, "")]); } @@ -311,11 +373,37 @@ class EditComponentPage extends HookWidget { childCount: outputs.value.length, ), ), - if (truthTable.value == null && logicExpression.value == null) ...[ + const SliverToBoxAdapter( + child: Divider(), + ), + if (inputs.value.isEmpty && outputs.value.isEmpty) + SliverToBoxAdapter( + child: Text( + 'Add inputs and outputs to continue', + style: Theme.of(context).textTheme.headline4, + textAlign: TextAlign.center, + ), + ) + else if (inputs.value.isEmpty) + SliverToBoxAdapter( + child: Text( + 'Add inputs to continue', + style: Theme.of(context).textTheme.headline4, + textAlign: TextAlign.center, + ), + ) + else if (outputs.value.isEmpty) + SliverToBoxAdapter( + child: Text( + 'Add outputs to continue', + style: Theme.of(context).textTheme.headline4, + textAlign: TextAlign.center, + ), + ) + else if (truthTable.value == null && logicExpressions.value == null && !visualDesigned.value) ...[ SliverToBoxAdapter( child: Column( children: [ - const Divider(), Text( 'Choose component kind', style: Theme.of(context).textTheme.headline4, @@ -324,7 +412,15 @@ class EditComponentPage extends HookWidget { padding: const EdgeInsets.all(8.0), child: OutlinedButton( onPressed: () { - logicExpression.value = ''; + // For each output, a separate logic expression is needed + logicExpressions.value = List.generate( + outputs.value.length, + (index) => '', + ); + logicExpressionsParsed.value = List.generate( + outputs.value.length, + (index) => null, + ); }, child: const Text('Logic Expression'), ), @@ -333,8 +429,13 @@ class EditComponentPage extends HookWidget { padding: const EdgeInsets.all(8.0), child: OutlinedButton( onPressed: () { + // Assign false by default to each output final row = "0" * outputs.value.length; - truthTable.value = List.generate(pow(2, inputs.value.length) as int, (_) => row); + // There are 2^inputs combinations in a truth table + truthTable.value = List.generate( + pow(2, inputs.value.length) as int, + (_) => row, + ); }, child: const Text('Truth Table'), ), @@ -342,7 +443,9 @@ class EditComponentPage extends HookWidget { Padding( padding: const EdgeInsets.all(8.0), child: OutlinedButton( - onPressed: null, + onPressed: () { + visualDesigned.value = true; + }, child: const Text('Visual Designer'), ), ), @@ -350,33 +453,115 @@ class EditComponentPage extends HookWidget { ), ), ], - if (logicExpression.value != null) ...[ - + if (logicExpressions.value != null) ...[ + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Text( + outputs.value.length == 1 ? 'Logic Expression' : 'Logic Expressions', + style: Theme.of(context).textTheme.headline5, + ), + ), + ), + SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: LogicExpressionField( + inputsListener: inputs, + outputName: outputs.value[index], + initialText: logicExpressions.value![index], + onChanged: (newValue, newExpression) { + logicExpressions.value = logicExpressions.value!.toList(); + logicExpressions.value![index] = newValue; + logicExpressionsParsed.value = logicExpressionsParsed.value!.toList(); + logicExpressionsParsed.value![index] = newExpression; + updateTTFromLE(); + }, + onInputError: () { + logicExpressionsParsed.value = logicExpressionsParsed.value!.toList(); + logicExpressionsParsed.value![index] = null; + updateTTFromLE(); + }, + ), + ); + }, + childCount: logicExpressions.value!.length, + ), + ), ], if (truthTable.value != null) ...[ SliverToBoxAdapter( child: Padding( - padding: const EdgeInsets.all(8.0), + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), child: Text( - 'Truth Table', + logicExpressions.value == null ? 'Truth Table' : 'Resulting Truth Table', style: Theme.of(context).textTheme.headline5, ), ), ), + if (logicExpressions.value == null) + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + 'Tap output cells to toggle', + style: Theme.of(context).textTheme.caption, + textAlign: TextAlign.right, + ), + ), + ), SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.all(8.0), - child: TruthTableEditor( - truthTable: truthTable.value!, - inputs: inputs.value, - outputs: outputs.value, - onUpdateTable: (idx, newValue) { - truthTable.value = truthTable.value?.toList()?..replaceRange(idx, idx+1, [newValue]); - }, + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: ConstrainedBox( + constraints: BoxConstraints(minWidth: constraints.maxWidth), + child: TruthTableEditor( + truthTable: truthTable.value!, + inputs: inputs.value, + outputs: outputs.value, + // Only allow updating truth table if it is *NOT* autogenerated by logic expressions + onUpdateTable: logicExpressions.value != null ? null : (idx, newValue) { + truthTable.value = truthTable.value?.toList()?..replaceRange(idx, idx+1, [newValue]); + }, + ), + ), + ); + } ), ), ) ], + if (visualDesigned.value) ...[ + SliverToBoxAdapter( + child: Column( + children: [ + Text( + "Visually Designed Component", + style: Theme.of(context).textTheme.headline4, + textAlign: TextAlign.center, + ), + if (dirty) Text( + "Save the component to open the designer", + style: Theme.of(context).textTheme.titleMedium, + textAlign: TextAlign.center, + ) else Padding( + padding: const EdgeInsets.all(8.0), + child: ElevatedButton( + // TODO: Implement visual designer + onPressed: null, + child: Text('Open Designer'), + ), + ), + ], + ), + ) + ], const SliverPadding( padding: EdgeInsets.only(bottom: 56 + 16 + 16), ), @@ -391,6 +576,8 @@ class EditComponentPage extends HookWidget { inputs: inputs.value, outputs: outputs.value, truthTable: truthTable.value, + logicExpression: logicExpressions.value, + visualDesigned: visualDesigned.value, )); anySave.value = true; // TODO: Implement saving diff --git a/lib/state/project.dart b/lib/state/project.dart index 3948696..2c03db7 100644 --- a/lib/state/project.dart +++ b/lib/state/project.dart @@ -76,6 +76,7 @@ class ProjectState extends ChangeNotifier { componentName: '', inputs: [], outputs: [], + visualDesigned: false, ); await _updateIndex(index.copyWith(components: index.components + [newComponent])); return newComponent; diff --git a/lib/utils/iterable_extension.dart b/lib/utils/iterable_extension.dart index a9c0cde..d7a6303 100644 --- a/lib/utils/iterable_extension.dart +++ b/lib/utils/iterable_extension.dart @@ -3,4 +3,22 @@ extension IndexedMap on Iterable { int index = 0; return map((e) => toElement(index++, e)); } + + Iterable zipWith(Iterable> otherIterables, O Function(List) toElement, {bool allowUnequalLengths = false}) sync* { + final iterators = [this].followedBy(otherIterables).map((e) => e.iterator).toList(); + while (true) { + int ends = iterators.map((it) => it.moveNext()).where((e) => e == false).length; + if (ends != 0) { + // At least one iterator finished + if (ends != iterators.length && !allowUnequalLengths) { + throw Exception('While zipping, $ends iterators ended early'); + } + else { + return; + } + } + + yield toElement(iterators.map((it) => it.current).toList(growable: false)); + } + } } \ No newline at end of file diff --git a/lib/utils/logic_expressions.dart b/lib/utils/logic_expressions.dart new file mode 100644 index 0000000..2e42329 --- /dev/null +++ b/lib/utils/logic_expressions.dart @@ -0,0 +1,286 @@ +import 'dart:math'; + +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:logic_circuits_simulator/utils/iterable_extension.dart'; +import 'package:logic_circuits_simulator/utils/logic_operators.dart'; + +part 'logic_expressions.freezed.dart'; + +@freezed +class LogicExpression with _$LogicExpression { + const LogicExpression._(); + + const factory LogicExpression({ + required LogicOperator operator, + required List arguments, + }) = _LogicExpression; + + factory LogicExpression.ofZeroOp(ZeroOpLogicOperator operator) => LogicExpression(operator: operator, arguments: []); + + static dynamic _classify(String token) { + final operators = [ + FalseLogicOperator(), + TrueLogicOperator(), + NotLogicOperator(), + AndLogicOperator(), + OrLogicOperator(), + XorLogicOperator(), + NandLogicOperator(), + NorLogicOperator(), + XnorLogicOperator(), + ]; + + for (final op in operators) { + if (op.representations.contains(token)) { + return op; + } + } + + final inputStart = RegExp('^[A-Za-z]'); + if (inputStart.hasMatch(token)) { + return token; + } + + throw Exception('Unknown operator: $token'); + } + + static List _tokenize(String input) { + final space = ' '.codeUnits[0]; + final openedParen = '('.codeUnits[0]; + final closedParen = ')'.codeUnits[0]; + final transitionToOperator = RegExp('[^A-Za-z0-9]'); + final transitionToInput = RegExp('[A-Za-z]'); + + List result = []; + final buffer = StringBuffer(); + bool operator = false; + int parenDepth = 0; + + for (final rune in input.runes) { + if (rune == openedParen) { + if (parenDepth == 0 && buffer.isNotEmpty) { + result.add(_classify(buffer.toString())); + buffer.clear(); + } + else if (parenDepth > 0) { + buffer.writeCharCode(rune); + } + parenDepth++; + continue; + } + else if (rune == closedParen) { + parenDepth--; + if (parenDepth == 0) { + result.add(_tokenize(buffer.toString())); + buffer.clear(); + } + else if (parenDepth < 0) { + throw Exception('Unmached parenthesis: too many closed parenthesis'); + } + else { + buffer.writeCharCode(rune); + } + continue; + } + else if (parenDepth > 0) { + // While inside paren, just add stuff to the buffer to be further + // processed recursively and put inside of a list. + // ~(~(A&(A+B))+B& ~A) + // │ │ └───┘│ │ + // │ └───────┘ │ + // └────────────────┘ + buffer.writeCharCode(rune); + continue; + } + else if (rune == space) { + if (buffer.isNotEmpty) { + result.add(_classify(buffer.toString())); + buffer.clear(); + } + } + else { + if (buffer.isNotEmpty) { + // Check if switching from operator to input. + // This allows an expression such as A&B to be valid. + // Switching happens when in the middle of a token + // and changing from [A-Za-z0-9] to [^A-Za-z0-9] + // or from [^A-Za-z] to [A-Za-z]. + // Inputs can't start with digits. + if (!operator && transitionToOperator.hasMatch(String.fromCharCode(rune))) { + result.add(_classify(buffer.toString())); + buffer.clear(); + } + else if (operator && transitionToInput.hasMatch(String.fromCharCode(rune))) { + result.add(_classify(buffer.toString())); + buffer.clear(); + } + } + if (buffer.isEmpty) { + operator = !transitionToInput.hasMatch(String.fromCharCode(rune)); + } + buffer.writeCharCode(rune); + } + } + if (parenDepth != 0) { + throw Exception('Unmached parenthesis: too many open parenthesis'); + } + if (buffer.isNotEmpty) { + result.add(_classify(buffer.toString())); + } + return result; + } + + factory LogicExpression.parse(String input) { + final tokens = _tokenize(input); + + final result = LogicExpression._parse(tokens); + if (result is String) { + return LogicExpression( + operator: OrLogicOperator(), + arguments: [ + result, + LogicExpression.ofZeroOp(FalseLogicOperator()), + ], + ); + } + else { + return result; + } + } + + static dynamic _parse(dynamic input) { + if (input is List) { + final tokens = input; + + final orderedOpGroups = [ + [OrLogicOperator(), NorLogicOperator()], + [XorLogicOperator(), XnorLogicOperator()], + [AndLogicOperator(), NandLogicOperator()], + [NotLogicOperator()], + [FalseLogicOperator(), TrueLogicOperator()], + ]; + + for (final ops in orderedOpGroups) { + for (var i = tokens.length - 1; i >= 0; i--) { + if (ops.contains(tokens[i])) { + if (tokens[i] is ZeroOpLogicOperator) { + // ZeroOp operator should be alone + if (tokens.length != 1) { + throw Exception('ZeroOp operator should be alone'); + } + return LogicExpression.ofZeroOp(tokens[i]); + } + else if (tokens[i] is OneOpLogicOperator) { + // OneOp operator should appear prefix only + // So index should be 0 + if (i != 0) { + throw Exception('OneOp operator should be prefix'); + } + // It should only be possible to get here if there is only one argument + // follows. The only other case is someone writing: + // ~ A B + // which would result in [NotLogicOperator, 'A', 'B']. + // Such syntax is ambiguous and should not be allowed: + // (~ A) & B -or- ~ (A & B) ? + // This is the disadvantage of linear, left-to-right notation (as opposed + // to the notation with a bar above the NOT-ed expression). + if (tokens.length > 2) { + throw Exception('Ambiguous expression: ${tokens[i]} followed by multiple tokens (${tokens.skip(1).toList()})'); + } + else if (tokens.length == 1) { + throw Exception('Unfinished expression'); + } + return LogicExpression( + operator: tokens[0], + arguments: [_parse(tokens[1])], + ); + } + else if (tokens[i] is TwoOpLogicOperator) { + return LogicExpression( + operator: tokens[i], + arguments: [ + _parse(tokens.getRange(0, i).toList()), + _parse(tokens.getRange(i + 1, tokens.length).toList()), + ], + ); + } + else { + throw Exception('Matched with operator that somehow isn\'t Zero/One/TwoOp'); + } + } + } + } + + // No operators were found. This means the only tokens are props. + // If there is only one prop, return it alone: + // A => [A] => A + // If there are multiple props, apply AND: + // A B C => [A, B, C] => A & B & C => (A & B) & C + // Keep in mind the second case is only possible if the props are separated by spaces, + // as the nature of prop names allowing multiple characters only allows multiple props + // to appear one after the other if separated by spaces. + if (tokens.length == 1) { + return tokens[0]; + } + else if (tokens.isEmpty) { + // This happens in unfinished expressions: + // A ^ ! => XOR(A, NOT(?)) + // A ^ => XOR(A, ?) + throw Exception('Unfinished expression'); + } + else { + final and = AndLogicOperator(); + return _parse(tokens.expand((token) => [and, token]).skip(1).toList()); + } + } + else if (input is String) { + // Prop, just return. + // Happens in such cases: + // B & ~ A => & [B, ~ [A]] + // ^ ^ + return input; + } + + } + + Set get inputs { + Set result = {}; + for (final arg in arguments) { + if (arg is String) { + result.add(arg); + } + else if (arg is LogicExpression) { + result.addAll(arg.inputs); + } + else { + throw Exception('Unknown argument type found: ${arg.runtimeType}'); + } + } + return result; + } + + bool evaluate(Map inputs) { + return operator.apply( + arguments + .map( + // If the argument is a logical expression, evaluate recursively + // else it must be an input name, so replace based on supplied mapping. + (e) => e is LogicExpression ? e.evaluate(inputs) : inputs[e]! + ) + .toList(), + ); + } + + List computeTruthTable(List inputs) { + final ttRows = pow(2, inputs.length) as int; + return List.generate( + ttRows, + (index) => evaluate( + { + for (var element in inputs.reversed.indexedMap((index, input) => [index, input])) + element[1] as String : (index & (pow(2, element[0] as int) as int)) != 0 + }, + ) ? '1' : '0', + ); + } +} diff --git a/lib/utils/logic_expressions.freezed.dart b/lib/utils/logic_expressions.freezed.dart new file mode 100644 index 0000000..41740e4 --- /dev/null +++ b/lib/utils/logic_expressions.freezed.dart @@ -0,0 +1,159 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target + +part of 'logic_expressions.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + +/// @nodoc +mixin _$LogicExpression { + LogicOperator get operator => throw _privateConstructorUsedError; + List get arguments => throw _privateConstructorUsedError; + + @JsonKey(ignore: true) + $LogicExpressionCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $LogicExpressionCopyWith<$Res> { + factory $LogicExpressionCopyWith( + LogicExpression value, $Res Function(LogicExpression) then) = + _$LogicExpressionCopyWithImpl<$Res>; + $Res call({LogicOperator operator, List arguments}); +} + +/// @nodoc +class _$LogicExpressionCopyWithImpl<$Res> + implements $LogicExpressionCopyWith<$Res> { + _$LogicExpressionCopyWithImpl(this._value, this._then); + + final LogicExpression _value; + // ignore: unused_field + final $Res Function(LogicExpression) _then; + + @override + $Res call({ + Object? operator = freezed, + Object? arguments = freezed, + }) { + return _then(_value.copyWith( + operator: operator == freezed + ? _value.operator + : operator // ignore: cast_nullable_to_non_nullable + as LogicOperator, + arguments: arguments == freezed + ? _value.arguments + : arguments // ignore: cast_nullable_to_non_nullable + as List, + )); + } +} + +/// @nodoc +abstract class _$$_LogicExpressionCopyWith<$Res> + implements $LogicExpressionCopyWith<$Res> { + factory _$$_LogicExpressionCopyWith( + _$_LogicExpression value, $Res Function(_$_LogicExpression) then) = + __$$_LogicExpressionCopyWithImpl<$Res>; + @override + $Res call({LogicOperator operator, List arguments}); +} + +/// @nodoc +class __$$_LogicExpressionCopyWithImpl<$Res> + extends _$LogicExpressionCopyWithImpl<$Res> + implements _$$_LogicExpressionCopyWith<$Res> { + __$$_LogicExpressionCopyWithImpl( + _$_LogicExpression _value, $Res Function(_$_LogicExpression) _then) + : super(_value, (v) => _then(v as _$_LogicExpression)); + + @override + _$_LogicExpression get _value => super._value as _$_LogicExpression; + + @override + $Res call({ + Object? operator = freezed, + Object? arguments = freezed, + }) { + return _then(_$_LogicExpression( + operator: operator == freezed + ? _value.operator + : operator // ignore: cast_nullable_to_non_nullable + as LogicOperator, + arguments: arguments == freezed + ? _value._arguments + : arguments // ignore: cast_nullable_to_non_nullable + as List, + )); + } +} + +/// @nodoc + +class _$_LogicExpression extends _LogicExpression { + const _$_LogicExpression( + {required this.operator, required final List arguments}) + : _arguments = arguments, + super._(); + + @override + final LogicOperator operator; + final List _arguments; + @override + List get arguments { + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_arguments); + } + + @override + String toString() { + return 'LogicExpression(operator: $operator, arguments: $arguments)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$_LogicExpression && + const DeepCollectionEquality().equals(other.operator, operator) && + const DeepCollectionEquality() + .equals(other._arguments, _arguments)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + const DeepCollectionEquality().hash(operator), + const DeepCollectionEquality().hash(_arguments)); + + @JsonKey(ignore: true) + @override + _$$_LogicExpressionCopyWith<_$_LogicExpression> get copyWith => + __$$_LogicExpressionCopyWithImpl<_$_LogicExpression>(this, _$identity); +} + +abstract class _LogicExpression extends LogicExpression { + const factory _LogicExpression( + {required final LogicOperator operator, + required final List arguments}) = _$_LogicExpression; + const _LogicExpression._() : super._(); + + @override + LogicOperator get operator => throw _privateConstructorUsedError; + @override + List get arguments => throw _privateConstructorUsedError; + @override + @JsonKey(ignore: true) + _$$_LogicExpressionCopyWith<_$_LogicExpression> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/utils/logic_operators.dart b/lib/utils/logic_operators.dart new file mode 100644 index 0000000..c0a5c24 --- /dev/null +++ b/lib/utils/logic_operators.dart @@ -0,0 +1,138 @@ +abstract class LogicOperator { + bool apply(List inputs); + List get representations; + String get defaultRepresentation => representations[0]; + bool fromRepresentation(String repr) => representations.contains(repr); +} + +abstract class ZeroOpLogicOperator extends LogicOperator {} + +class FalseLogicOperator extends ZeroOpLogicOperator { + @override + bool apply(List inputs) => false; + + @override + List get representations => ['0']; + + @override + bool operator==(other) => other is FalseLogicOperator; + + @override + int get hashCode => defaultRepresentation.hashCode; +} + +class TrueLogicOperator extends ZeroOpLogicOperator { + @override + bool apply(List inputs) => true; + + @override + List get representations => ['1']; + + @override + bool operator==(other) => other is TrueLogicOperator; + + @override + int get hashCode => defaultRepresentation.hashCode; +} + +abstract class OneOpLogicOperator extends LogicOperator {} + +class NotLogicOperator extends OneOpLogicOperator { + @override + bool apply(List inputs) => !inputs[0]; + + @override + List get representations => const ['~', '!', '¬']; + + @override + bool operator==(other) => other is NotLogicOperator; + + @override + int get hashCode => defaultRepresentation.hashCode; +} + +abstract class TwoOpLogicOperator extends LogicOperator {} + +class AndLogicOperator extends TwoOpLogicOperator { + @override + bool apply(List inputs) => inputs[0] && inputs[1]; + + @override + List get representations => const ['&', '∧']; + + @override + bool operator==(other) => other is AndLogicOperator; + + @override + int get hashCode => defaultRepresentation.hashCode; +} + +class OrLogicOperator extends TwoOpLogicOperator { + @override + bool apply(List inputs) => inputs[0] || inputs[1]; + + @override + List get representations => const ['|', '∨']; + + @override + bool operator==(other) => other is OrLogicOperator; + + @override + int get hashCode => defaultRepresentation.hashCode; +} + +class XorLogicOperator extends TwoOpLogicOperator { + @override + bool apply(List inputs) => inputs[0] != inputs[1]; + + @override + List get representations => const ['^', '⊕', '⊻']; + + @override + bool operator==(other) => other is XorLogicOperator; + + @override + int get hashCode => defaultRepresentation.hashCode; +} + +class NandLogicOperator extends TwoOpLogicOperator { + @override + bool apply(List inputs) => !(inputs[0] && inputs[1]); + + @override + List get representations => const ['~&', '!&', '¬&', '~∧', '!∧', '¬∧']; + + @override + bool operator==(other) => other is NandLogicOperator; + + @override + int get hashCode => defaultRepresentation.hashCode; +} + +class NorLogicOperator extends TwoOpLogicOperator { + @override + bool apply(List inputs) => !(inputs[0] || inputs[1]); + + @override + List get representations => const ['~|', '!|', '¬|', '~∨', '!∨', '¬∨']; + + @override + bool operator==(other) => other is NorLogicOperator; + + @override + int get hashCode => defaultRepresentation.hashCode; +} + +class XnorLogicOperator extends TwoOpLogicOperator { + @override + bool apply(List inputs) => inputs[0] == inputs[1]; + + @override + List get representations => const ['~^', '!^', '¬^', '~⊕', '!⊕', '¬⊕', '~⊻', '!⊻', '¬⊻']; + + @override + bool operator==(other) => other is XnorLogicOperator; + + @override + int get hashCode => defaultRepresentation.hashCode; +}