4-variable-simplifier/main.py

458 lines
14 KiB
Python
Executable file

#!/usr/bin/env python3
import os;
import readline
import argparse
import pickle;
import sys;
def parse_args(argv):
parser = argparse.ArgumentParser(prog = '4-variable-simplifier');
parser.add_argument('-c', '--command', help = '''
Provide a boolean expression to simpliy on the command-line.
Without this option, boolean expressions are read stdin.
''')
parser.add_argument('-e', '--extended-operators', action='store_true', help='''
Use more than just the standard three boolean operators (not, or, and).
This options allows use of: nor, nand, xor, nxor, andn, orn.
''')
parser.add_argument('-o', '--custom-operators', help='''
Pick and choose which operators the simplifier can use. Comma seperated.
You can refer to them using their names or their symbols.
''')
parser.add_argument("-n", '--use-names', help='''
Print the operator's names in the expression tree, rather than the
symbols.
''')
parser.add_argument('-p', "--printout", action = 'store_true', help='''
Print out all simplified expressions for the given expressions and
quit.
''')
parser.add_argument("--simplifications-file", \
default = ".simplifications.bin", help='''
Path to pickle file that stores the cached simplifications.
''')
parser.add_argument("-y", "--yes", action = 'store_true', help='''
Do not ask before generating simplification cache.
''')
parser.add_argument("-q", "--quiet", action = 'store_true', help='''
Do not print anything but the simplification. Assumes '--yes'.
''')
parser.add_argument("-C", "--color", default = 'auto', \
choices = ["off", "on", "auto"], help='''
Select whether to use terminal colors. Default is to use if it is
supported. use '--color=on' to force it on. '--color=off' otherwise.
''')
return parser.parse_args(argv[1:]);
symbol_to_name = {
"!": "not",
"||": "or",
"&&": "and",
"!|": "nor",
"!&": "nand",
"!=": "xor",
"==": "nxor",
"|!": "orn",
"&!": "andn",
};
def determine_available_operators(args):
standard = ("!", "||", "&&");
extended = ("!|", "!&", "!=", "==", "|!", "&!");
if args.extended_operators:
return set(standard + extended);
elif args.custom_operators:
available_operators = set();
for o in args.custom_operators.split(","):
if o in symbol_to_name:
available_operators.add(o);
elif o in symbol_to_name.values():
for k, v in symbol_to_name.items():
if v == o:
available_operators.add(k);
else:
raise BaseException(f"not an operator: '{o}'");
return available_operators;
else:
return set(standard);
def pretty(exp):
match exp:
case ("literal", x):
return str(x);
case ("variable", x):
return x
case (op, inner):
return f"({op}{pretty(inner)})"
case (op, left, right):
return f"({pretty(left)} {op} {pretty(right)})"
case _:
print(exp);
assert(not "TODO");
W = 0b0101_0101_0101_0101
X = 0b0011_0011_0011_0011
Y = 0b0000_1111_0000_1111
Z = 0b0000_0000_1111_1111
M = 0b1111_1111_1111_1111
def calculate_simplifications(args, available_operators):
print(f'available_operators = {available_operators}')
lookup = dict() # truthtable -> expression
costs = dict() # truthtable -> cost
todo = [set() for _ in range(100)] # indexed by cost, set of truthtables.
todo_count = [0]; # I have to wrap in an array because python is dumb.
def prequeue(truthtable, expression, cost):
lookup[truthtable] = expression;
costs[truthtable] = cost;
todo[cost].add(truthtable);
todo_count[0] += 1;
# literals:
prequeue(0b1111_1111_1111_1111, ("literal", 1), cost = 1);
prequeue(0b0000_0000_0000_0000, ("literal", 0), cost = 1);
# variables:
prequeue(W, ("variable", "w"), cost = 0);
prequeue(X, ("variable", "x"), cost = 0);
prequeue(Y, ("variable", "y"), cost = 0);
prequeue(Z, ("variable", "z"), cost = 0);
# Completely unnecessary critera, alphabetical variables is more
# aesthetically pleasing. Thanks Benson.
def is_aesthetically_better(this, that):
def extract_variables(exp):
match exp:
case ("literal", x):
return ();
case ("variable", x):
return (x, )
case (_, inner):
return extract_variables(inner);
case (_, left, right):
return extract_variables(left) + extract_variables(right);
case _:
print(exp);
assert(not "TODO");
return extract_variables(this) < extract_variables(that);
unary_operators = {
'!': lambda x: ~x,
}
binary_operators = {
'||': lambda x, y: (x | y) & M,
'&&': lambda x, y: (x & y) & M,
'!|': lambda x, y: ~(x | y) & M,
'!&': lambda x, y: ~(x & y) & M,
'&!': lambda x, y: (~x & y) & M,
'|!': lambda x, y: (~x | y) & M,
'!=': lambda x, y: (x ^ y) & M,
'==': lambda x, y: (~(x ^ y)) & M,
}
def print_status():
numerator = 65536 - todo_count[0];
denominator = 65536
line = f'{numerator} of 65536';
line += f' ({numerator / denominator * 100:.2f}%):'
line += f' [{my_cost}]';
line += f' {pretty(my_expression)}';
print(line);
min_cost = 0;
while todo_count[0]:
truthtables = todo[min_cost];
if not truthtables:
min_cost += 1;
continue;
assert(todo_count[0] <= 65536);
my_truthtable = min(truthtables);
truthtables.discard(my_truthtable);
my_cost = min_cost;
my_expression = lookup[my_truthtable];
todo_count[0] -= 1;
if not args.quiet:
print_status();
def consider(new_truthtable, new_expression, new_cost):
if new_truthtable not in costs:
todo[new_cost].add(new_truthtable);
costs[new_truthtable] = new_cost
lookup[new_truthtable] = new_expression
todo_count[0] += 1;
elif new_cost < costs[new_truthtable] or \
(new_cost == costs[new_truthtable] and \
is_aesthetically_better(new_expression, \
lookup[new_truthtable])):
current_cost = costs[new_truthtable];
assert(new_cost >= min_cost);
todo[current_cost].discard(new_truthtable);
todo[new_cost].add(new_truthtable);
costs[new_truthtable] = new_cost
lookup[new_truthtable] = new_expression
# consider unary operators:
for name, function in sorted(unary_operators.items()):
if name in available_operators:
unary_truthtable = function(my_truthtable) & M;
unary_expression = (name, my_expression);
unary_cost = my_cost + 1;
consider(unary_truthtable, unary_expression, unary_cost);
# consider binary operators:
for name, function in sorted(binary_operators.items()):
if name in available_operators:
for other_truthtable, other_expression in sorted(lookup.items()):
# x + y
binary_truthtable = function(my_truthtable, other_truthtable);
binary_truthtable = binary_truthtable & M
binary_expression = (name, my_expression, other_expression);
binary_cost = my_cost + 1 + costs[other_truthtable];
consider(binary_truthtable, binary_expression, binary_cost);
# y + x
binary_truthtable = function(other_truthtable, my_truthtable);
binary_truthtable = binary_truthtable & M
binary_expression = (name, other_expression, my_expression);
binary_cost = my_cost + 1 + costs[other_truthtable];
consider(binary_truthtable, binary_expression, binary_cost);
return costs, lookup
pathname = "simplifications.bin"
def get_simplifications(args, available_operators):
available_operators = tuple(sorted(available_operators));
try:
with open(pathname, "rb") as stream:
cache = pickle.load(stream);
except FileNotFoundError:
cache = dict();
if available_operators not in cache:
if not args.quiet:
print("Oh! looks like you're running this for the first time");
print("I'll have to build up my cache of simplifications");
print("This may take a while.");
print("I'll only have to do this once.");
if not args.yes:
print();
input("any input to start:");
print();
bundle = calculate_simplifications(args, available_operators);
if not args.quiet:
print();
print("done!");
print();
cache[available_operators] = bundle;
with open(pathname, "wb") as stream:
pickle.dump(cache, stream);
if not args.quiet:
print();
print("saved!");
print();
return cache[available_operators];
def create_parser():
import pyparsing as pp
from pyparsing import Forward, Suppress, Keyword, Group, ZeroOrMore, Optional, Literal
root = Forward()
literal = pp.Word('01');
variable = pp.Word('wxyz')
highest = literal | variable | (Suppress('(') + root + Suppress(')'));
prefix_expression = Group(Literal("!") + highest) | highest
relational_expression = \
Group(prefix_expression + Literal('<=') + prefix_expression) \
| Group(prefix_expression + Literal('>=') + prefix_expression) \
| Group(prefix_expression + Literal('>') + prefix_expression) \
| Group(prefix_expression + Literal('<') + prefix_expression) \
| Group(prefix_expression + Literal('==') + prefix_expression) \
| Group(prefix_expression + Literal('!=') + prefix_expression) \
| prefix_expression;
logical_and_expression = Forward();
logical_and_expression <<= \
Group(relational_expression + Literal('&&') + logical_and_expression) \
| Group(relational_expression + Literal('!&') + logical_and_expression) \
| Group(relational_expression + Literal('&!') + logical_and_expression) \
| relational_expression;
logical_or_expression = Forward()
logical_or_expression <<= \
Group(logical_and_expression + Literal('||') + logical_or_expression) \
| Group(logical_and_expression + Literal('!|') + logical_or_expression) \
| Group(logical_and_expression + Literal('|!') + logical_or_expression) \
| logical_and_expression;
root <<= logical_or_expression;
return root;
def parse(parser, text):
return parser.parseString(text, parseAll = True).asList()[0]
def evaluate(expr):
match expr:
case "0":
return 0;
case "1":
return M;
case "w":
return W;
case "x":
return X;
case "y":
return Y;
case "z":
return Z;
case ("!", subexp):
return ~evaluate(subexp) & M;
case (left, "||", right):
return evaluate(left) | evaluate(right);
case (left, "!|", right):
return ~(evaluate(left) | evaluate(right)) & M;
case (left, "|!", right):
return (~evaluate(left) | evaluate(right)) & M;
case (left, "&&", right):
return evaluate(left) & evaluate(right);
case (left, "!&", right):
return ~(evaluate(left) & evaluate(right)) & M;
case (left, "&!", right):
return (~evaluate(left) & evaluate(right)) & M;
case (left, "<", right):
return (~evaluate(left) & evaluate(right)) & M;
case (left, "<=", right):
return (~evaluate(left) | evaluate(right)) & M;
case (left, ">", right):
return (evaluate(left) & ~evaluate(right)) & M;
case (left, ">=", right):
return (evaluate(left) | ~evaluate(right)) & M;
case (left, "==", right):
return ~(evaluate(left) ^ evaluate(right)) & M;
case (left, "!=", right):
return evaluate(left) ^ evaluate(right);
case _:
print(exp);
assert(not "TODO");
def repl(args, cost, lookup):
import readline;
print("""
Give a boolean expression using the variables 'w', 'x', 'y' and 'z'.
Operations: not ('!'), or ('||'), and ('&&').
Extended operations: nor ('!|'), orn ('|!'), nand ('!&'), andn ('&!'), xor ('!='),
Extended operations: nxor ('==')
""");
# print(f'I can do anything in {max(cost.values())} operations.')
# print();
parser = create_parser();
while True:
try:
line = input(">>> ");
except EOFError:
return;
truthtable = evaluate(parse(parser, line));
if truthtable in lookup:
print(f'{cost[truthtable]}: {pretty(lookup[truthtable])}')
else:
print('unreachable.')
def main(args):
available_operators = determine_available_operators(args);
match args.color:
case 'off': args.color = False;
case 'on': args.color = True;
case 'auto': args.color = os.isatty(1);
cost, lookup = get_simplifications(args, available_operators);
if args.printout:
for truthtable, exp in sorted(lookup.items()):
print(f'{truthtable:016b}: {pretty(exp)}');
elif args.command:
truthtable = evaluate(parse(create_parser(), args.command));
if truthtable in lookup:
print(pretty(lookup[truthtable]));
else:
print('unreachable.')
else:
repl(args, cost, lookup);
return 0;
exit(main(parse_args(sys.argv)))