Kenneth Bruen
2 years ago
10 changed files with 944 additions and 44 deletions
@ -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, |
||||
), |
||||
); |
||||
} |
||||
} |
@ -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', |
||||
); |
||||
} |
||||
} |
@ -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; |
||||
} |
@ -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…
Reference in new issue