You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
286 lines
9.0 KiB
286 lines
9.0 KiB
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', |
|
); |
|
} |
|
}
|
|
|