Browse Source

Implemented logic expressions

Also made truth table horizontally scrollable
master
Kenneth Bruen 2 years ago
parent
commit
85838c2b32
Signed by: kbruen
GPG Key ID: C1980A470C3EE5B1
  1. 72
      lib/components/logic_expression_field.dart
  2. 4
      lib/models/project.dart
  3. 61
      lib/models/project.freezed.dart
  4. 6
      lib/models/project.g.dart
  5. 243
      lib/pages/edit_component.dart
  6. 1
      lib/state/project.dart
  7. 18
      lib/utils/iterable_extension.dart
  8. 286
      lib/utils/logic_expressions.dart
  9. 159
      lib/utils/logic_expressions.freezed.dart
  10. 138
      lib/utils/logic_operators.dart

72
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<List<String>> 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<String?>(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,
),
);
}
}

4
lib/models/project.dart

@ -24,7 +24,9 @@ class ComponentEntry with _$ComponentEntry {
@JsonKey(includeIfNull: false)
List<String>? truthTable,
@JsonKey(includeIfNull: false)
String? logicExpression,
List<String>? logicExpression,
@JsonKey(defaultValue: false)
required bool visualDesigned,
}) = _ComponentEntry;
factory ComponentEntry.fromJson(Map<String, Object?> json) => _$ComponentEntryFromJson(json);

61
lib/models/project.freezed.dart

@ -167,7 +167,9 @@ mixin _$ComponentEntry {
@JsonKey(includeIfNull: false)
List<String>? get truthTable => throw _privateConstructorUsedError;
@JsonKey(includeIfNull: false)
String? get logicExpression => throw _privateConstructorUsedError;
List<String>? get logicExpression => throw _privateConstructorUsedError;
@JsonKey(defaultValue: false)
bool get visualDesigned => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
@ -187,7 +189,8 @@ abstract class $ComponentEntryCopyWith<$Res> {
List<String> inputs,
List<String> outputs,
@JsonKey(includeIfNull: false) List<String>? truthTable,
@JsonKey(includeIfNull: false) String? logicExpression});
@JsonKey(includeIfNull: false) List<String>? 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<String>?,
visualDesigned: visualDesigned == freezed
? _value.visualDesigned
: visualDesigned // ignore: cast_nullable_to_non_nullable
as bool,
));
}
}
@ -256,7 +264,8 @@ abstract class _$$_ComponentEntryCopyWith<$Res>
List<String> inputs,
List<String> outputs,
@JsonKey(includeIfNull: false) List<String>? truthTable,
@JsonKey(includeIfNull: false) String? logicExpression});
@JsonKey(includeIfNull: false) List<String>? 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<String>?,
logicExpression: logicExpression == freezed
? _value.logicExpression
? _value._logicExpression
: logicExpression // ignore: cast_nullable_to_non_nullable
as String?,
as List<String>?,
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<String> inputs,
required final List<String> outputs,
@JsonKey(includeIfNull: false) final List<String>? truthTable,
@JsonKey(includeIfNull: false) this.logicExpression})
@JsonKey(includeIfNull: false) final List<String>? logicExpression,
@JsonKey(defaultValue: false) required this.visualDesigned})
: _inputs = inputs,
_outputs = outputs,
_truthTable = truthTable;
_truthTable = truthTable,
_logicExpression = logicExpression;
factory _$_ComponentEntry.fromJson(Map<String, dynamic> json) =>
_$$_ComponentEntryFromJson(json);
@ -362,13 +378,23 @@ class _$_ComponentEntry implements _ComponentEntry {
return EqualUnmodifiableListView(value);
}
final List<String>? _logicExpression;
@override
@JsonKey(includeIfNull: false)
final String? logicExpression;
List<String>? 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<String> inputs,
required final List<String> outputs,
@JsonKey(includeIfNull: false) final List<String>? truthTable,
@JsonKey(includeIfNull: false) final String? logicExpression}) =
@JsonKey(includeIfNull: false) final List<String>? logicExpression,
@JsonKey(defaultValue: false) required final bool visualDesigned}) =
_$_ComponentEntry;
factory _ComponentEntry.fromJson(Map<String, dynamic> json) =
@ -443,7 +473,10 @@ abstract class _ComponentEntry implements ComponentEntry {
List<String>? get truthTable => throw _privateConstructorUsedError;
@override
@JsonKey(includeIfNull: false)
String? get logicExpression => throw _privateConstructorUsedError;
List<String>? get logicExpression => throw _privateConstructorUsedError;
@override
@JsonKey(defaultValue: false)
bool get visualDesigned => throw _privateConstructorUsedError;
@override
@JsonKey(ignore: true)
_$$_ComponentEntryCopyWith<_$_ComponentEntry> get copyWith =>

6
lib/models/project.g.dart

@ -30,7 +30,10 @@ _$_ComponentEntry _$$_ComponentEntryFromJson(Map<String, dynamic> json) =>
truthTable: (json['truthTable'] as List<dynamic>?)
?.map((e) => e as String)
.toList(),
logicExpression: json['logicExpression'] as String?,
logicExpression: (json['logicExpression'] as List<dynamic>?)
?.map((e) => e as String)
.toList(),
visualDesigned: json['visualDesigned'] as bool? ?? false,
);
Map<String, dynamic> _$$_ComponentEntryToJson(_$_ComponentEntry instance) {
@ -50,5 +53,6 @@ Map<String, dynamic> _$$_ComponentEntryToJson(_$_ComponentEntry instance) {
val['outputs'] = instance.outputs;
writeNotNull('truthTable', instance.truthTable);
writeNotNull('logicExpression', instance.logicExpression);
val['visualDesigned'] = instance.visualDesigned;
return val;
}

243
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<ProjectState>();
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<LogicExpression?>.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

1
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;

18
lib/utils/iterable_extension.dart

@ -3,4 +3,22 @@ extension IndexedMap<T> on Iterable<T> {
int index = 0;
return map((e) => toElement(index++, e));
}
Iterable<O> zipWith<O>(Iterable<Iterable<T>> otherIterables, O Function(List<T>) 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));
}
}
}

286
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<dynamic> 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<dynamic> _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<dynamic> 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<String> get inputs {
Set<String> 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<String, bool> 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<String> computeTruthTable(List<String> 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',
);
}
}

159
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>(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<dynamic> get arguments => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$LogicExpressionCopyWith<LogicExpression> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $LogicExpressionCopyWith<$Res> {
factory $LogicExpressionCopyWith(
LogicExpression value, $Res Function(LogicExpression) then) =
_$LogicExpressionCopyWithImpl<$Res>;
$Res call({LogicOperator operator, List<dynamic> 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<dynamic>,
));
}
}
/// @nodoc
abstract class _$$_LogicExpressionCopyWith<$Res>
implements $LogicExpressionCopyWith<$Res> {
factory _$$_LogicExpressionCopyWith(
_$_LogicExpression value, $Res Function(_$_LogicExpression) then) =
__$$_LogicExpressionCopyWithImpl<$Res>;
@override
$Res call({LogicOperator operator, List<dynamic> 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<dynamic>,
));
}
}
/// @nodoc
class _$_LogicExpression extends _LogicExpression {
const _$_LogicExpression(
{required this.operator, required final List<dynamic> arguments})
: _arguments = arguments,
super._();
@override
final LogicOperator operator;
final List<dynamic> _arguments;
@override
List<dynamic> 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<dynamic> arguments}) = _$_LogicExpression;
const _LogicExpression._() : super._();
@override
LogicOperator get operator => throw _privateConstructorUsedError;
@override
List<dynamic> get arguments => throw _privateConstructorUsedError;
@override
@JsonKey(ignore: true)
_$$_LogicExpressionCopyWith<_$_LogicExpression> get copyWith =>
throw _privateConstructorUsedError;
}

138
lib/utils/logic_operators.dart

@ -0,0 +1,138 @@
abstract class LogicOperator {
bool apply(List<bool> inputs);
List<String> 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<bool> inputs) => false;
@override
List<String> get representations => ['0'];
@override
bool operator==(other) => other is FalseLogicOperator;
@override
int get hashCode => defaultRepresentation.hashCode;
}
class TrueLogicOperator extends ZeroOpLogicOperator {
@override
bool apply(List<bool> inputs) => true;
@override
List<String> 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<bool> inputs) => !inputs[0];
@override
List<String> 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<bool> inputs) => inputs[0] && inputs[1];
@override
List<String> get representations => const ['&', ''];
@override
bool operator==(other) => other is AndLogicOperator;
@override
int get hashCode => defaultRepresentation.hashCode;
}
class OrLogicOperator extends TwoOpLogicOperator {
@override
bool apply(List<bool> inputs) => inputs[0] || inputs[1];
@override
List<String> get representations => const ['|', ''];
@override
bool operator==(other) => other is OrLogicOperator;
@override
int get hashCode => defaultRepresentation.hashCode;
}
class XorLogicOperator extends TwoOpLogicOperator {
@override
bool apply(List<bool> inputs) => inputs[0] != inputs[1];
@override
List<String> get representations => const ['^', '', ''];
@override
bool operator==(other) => other is XorLogicOperator;
@override
int get hashCode => defaultRepresentation.hashCode;
}
class NandLogicOperator extends TwoOpLogicOperator {
@override
bool apply(List<bool> inputs) => !(inputs[0] && inputs[1]);
@override
List<String> get representations => const ['~&', '!&', '¬&', '~∧', '!∧', '¬∧'];
@override
bool operator==(other) => other is NandLogicOperator;
@override
int get hashCode => defaultRepresentation.hashCode;
}
class NorLogicOperator extends TwoOpLogicOperator {
@override
bool apply(List<bool> inputs) => !(inputs[0] || inputs[1]);
@override
List<String> get representations => const ['~|', '!|', '¬|', '~∨', '!∨', '¬∨'];
@override
bool operator==(other) => other is NorLogicOperator;
@override
int get hashCode => defaultRepresentation.hashCode;
}
class XnorLogicOperator extends TwoOpLogicOperator {
@override
bool apply(List<bool> inputs) => inputs[0] == inputs[1];
@override
List<String> get representations => const ['~^', '!^', '¬^', '~⊕', '!⊕', '¬⊕', '~⊻', '!⊻', '¬⊻'];
@override
bool operator==(other) => other is XnorLogicOperator;
@override
int get hashCode => defaultRepresentation.hashCode;
}
Loading…
Cancel
Save