4-variable-simplifier/main.py

784 lines
24 KiB
Python
Executable file

#!/usr/bin/env python3
import os;
import readline
import argparse
import pickle;
import sys;
from colorsys import hsv_to_rgb;
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('-E', '--extra-extended-operators', action='store_true', help='''
In addition to the operators of '--extended-operators' introduce
the ternary ('?:') operator.
This operator is very powerful but dramatically increases
simplification time by an nth degree.
''')
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",
"?:": "ternary",
};
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;
elif args.extra_extended_operators:
return set(standard + extended + ("?:", ));
else:
return set(standard);
LITERAL_ESCAPE = "\033[38;2;200;200;100m";
VARIABLE_ESCAPE = "\033[38;2;100;100;200m";
operator_colors = [];
for i in range(10):
r, g, b = hsv_to_rgb(i / 10, 1.0, 0.8);
r, g, b = [int(255 * x) for x in (r, g, b)];
operator_colors.append(f"\033[38;2;{r};{g};{b}m");
RESET_ESCAPE = "\033[0m";
def pretty(exp, in_color = False, depth = 0):
start_color, end_color = "", "";
if in_color:
start_color = operator_colors[depth % len(operator_colors)];
end_color = RESET_ESCAPE;
retval = "";
match exp:
case ("literal", x):
retval = str(x);
if in_color:
retval = LITERAL_ESCAPE + retval + RESET_ESCAPE;
case ("variable", x):
retval = x
if in_color:
retval = VARIABLE_ESCAPE + retval + RESET_ESCAPE;
case (op, inner):
retval += start_color + "(" + end_color;
retval += start_color + op + end_color;
retval += pretty(inner, in_color, depth + 1);
retval += start_color + ")" + end_color;
case (op, left, right):
retval += start_color + "(" + end_color;
retval += pretty(left, in_color, depth + 1);
retval += " " + start_color + op + end_color + " ";
retval += pretty(right, in_color, depth + 1);
retval += start_color + ")" + end_color;
case ("?:", cond, left, right):
retval += start_color + "(" + end_color;
retval += pretty(cond, in_color, depth + 1);
retval += " " + start_color + "?" + end_color + " ";
retval += pretty(left, in_color, depth + 1);
retval += " " + start_color + ":" + end_color + " ";
retval += pretty(right, in_color, depth + 1);
retval += start_color + ")" + end_color;
case _:
print(exp);
assert(not "TODO");
return retval;
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):
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 (_, cond, true, false):
return extract_variables(cond) + extract_variables(true) + extract_variables(false);
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, # or
'&&': lambda x, y: x & y, # and
'!|': lambda x, y: ~(x | y), # nor
'!&': lambda x, y: ~(x & y), # nand
'&!': lambda x, y: ~x & y, # andn
'|!': lambda x, y: ~x | y, # orn
'!=': lambda x, y: x ^ y, # xor
'==': lambda x, y: ~(x ^ y), # nxor
}
ternary_operators = {
'?:': lambda x, y, z: (x & y) | (~x & z), # ternary
};
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);
print([len(x) for x in todo]);
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_cost = my_cost + 1;
unary_truthtable = function(my_truthtable) & M;
unary_expression = (name, my_expression);
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()):
binary_cost = my_cost + 1 + costs[other_truthtable];
# x + y
binary_truthtable = function(my_truthtable, other_truthtable);
binary_truthtable = binary_truthtable & M
binary_expression = (name, my_expression, other_expression);
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);
consider(binary_truthtable, binary_expression, binary_cost);
# consider ternary operators:
for name, function in sorted(ternary_operators.items()):
if name in available_operators:
s = sorted(lookup.items());
for a_truthtable, a_expression in s:
for b_truthtable, b_expression in s:
ternary_cost = 1 + my_cost + costs[a_truthtable] + costs[b_truthtable];
# x ? y : z
ternary_truthtable = function(my_truthtable, a_truthtable, b_truthtable) & M;
ternary_expression = (name, my_expression, a_expression, b_expression);
consider(ternary_truthtable, ternary_expression, ternary_cost);
# y ? x : z
ternary_truthtable = function(a_truthtable, my_truthtable, b_truthtable) & M;
ternary_expression = (name, a_expression, my_expression, b_expression);
consider(ternary_truthtable, ternary_expression, ternary_cost);
# y ? z : x
ternary_truthtable = function(a_truthtable, b_truthtable, my_truthtable) & M;
ternary_expression = (name, a_expression, b_expression, my_expression);
consider(ternary_truthtable, ternary_expression, ternary_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.");
if "?:" in available_operators:
print();
print("You have selected ternary operators, so this WILL "
"take time. This will probably take weeks to run.");
print();
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 parse(text):
class Tokenizer:
def __init__(self, text):
self.position = 0;
self.text = text;
self.tokenkind = None;
self.tokendata = None;
def next(self):
# skip whitespace:
while self.position < len(self.text) and self.text[self.position].isspace():
self.position += 1;
if self.position == len(self.text):
self.tokenkind = "EOF";
return;
match self.text[self.position]:
case "0" | "1":
self.tokenkind = "literal";
self.tokendata = self.text[self.position];
self.position += 1;
case "w" | "x" | "y" | "z":
self.tokenkind = "variable";
self.tokendata = self.text[self.position];
self.position += 1;
case "(" | ")" | "?" | ":":
self.tokenkind = self.text[self.position];
self.position += 1;
case ">":
# could be ">" or ">=":
self.position += 1;
match self.text[self.position]:
case "=":
self.tokenkind = ">=";
self.position += 1;
case _:
self.tokenkind = ">"
case "<":
# could be "<" or "<=":
self.position += 1;
match self.text[self.position]:
case "=":
self.tokenkind = "<=";
self.position += 1;
case _:
self.tokenkind = "<"
case "=":
# could only be "=="
self.position += 1;
match self.text[self.position]:
case "=":
self.tokenkind = "==";
self.position += 1;
case _:
raise BaseException("expecting '=' after '='");
case "!":
# could be "!" or "!|" or "&!" or "!="
self.position += 1;
match self.text[self.position]:
case "&":
self.tokenkind = "!&";
self.position += 1;
case "|":
self.tokenkind = "!|";
self.position += 1;
case "=":
self.tokenkind = "!=";
self.position += 1;
case _:
self.tokenkind = "!"
case "&":
# could be "&&" or "&!"
self.position += 1;
match self.text[self.position]:
case "&":
self.tokenkind = "&&";
self.position += 1;
case "!":
self.tokenkind = "&!";
self.position += 1;
case _:
raise BaseException("expecting '&' or '!' after '&'");
case "|":
# could be "||" or "|!"
self.position += 1;
match self.text[self.position]:
case "|":
self.tokenkind = "||";
self.position += 1;
case "!":
self.tokenkind = "|!";
self.position += 1;
case _:
raise BaseException("expecting '|' or '!' after '|'");
case x:
raise BaseException(f'unknown token "{x}"!');
def parse_primary(tokenizer):
match tokenizer.tokenkind:
case "literal":
ast = ("literal", int(tokenizer.tokendata));
tokenizer.next();
case "variable":
ast = ("variable", tokenizer.tokendata);
tokenizer.next();
case "(":
tokenizer.next();
ast = parse_root(tokenizer);
if tokenizer.tokenkind != ")":
raise BaseException(f'expected token ")", found: {tokenizer.tokenkind}');
tokenizer.next();
case x:
raise BaseException(f'unexpected token "{x}"!');
return ast;
def parse_prefix(tokenizer):
if tokenizer.tokenkind == "!":
tokenizer.next();
return ("!", parse_prefix(tokenizer));
else:
return parse_primary(tokenizer);
def parse_compares(tokenizer):
left = parse_prefix(tokenizer);
while tokenizer.tokenkind in ("<=", "<", ">", ">="):
op = tokenizer.tokenkind;
tokenizer.next();
left = (op, left, parse_prefix(tokenizer));
return left;
def parse_equals(tokenizer):
left = parse_compares(tokenizer);
while tokenizer.tokenkind in ("==", "!="):
op = tokenizer.tokenkind;
tokenizer.next();
left = (op, left, parse_compares(tokenizer));
return left;
def parse_ands(tokenizer):
left = parse_equals(tokenizer);
while tokenizer.tokenkind in ("&&", "&!", "!&"):
op = tokenizer.tokenkind;
tokenizer.next();
left = (op, left, parse_equals(tokenizer));
return left;
def parse_ors(tokenizer):
left = parse_ands(tokenizer);
while tokenizer.tokenkind in ("||", "|!", "!|"):
op = tokenizer.tokenkind;
tokenizer.next();
left = (op, left, parse_ands(tokenizer));
return left;
def parse_ternary(tokenizer):
exp = parse_ors(tokenizer);
if tokenizer.tokenkind in ("?", ):
cond = exp;
tokenizer.next();
true = parse_ors(tokenizer);
if tokenizer.tokenkind != ":":
raise BaseException("expecting ':' in ternary expression, "
"found: '{tokenizer.tokenkind}'!");
tokenizer.next();
false = parse_ternary(tokenizer);
return ("?:", cond, true, false);
else:
return exp;
def parse_root(tokenizer):
return parse_ternary(tokenizer);
tokenizer = Tokenizer(text);
tokenizer.next();
root = parse_root(tokenizer);
if tokenizer.tokenkind != "EOF":
raise BaseException(
f"reached end of expression. "
f"Expecting EOF. found: {tokenizer.tokenkind}!");
return root;
def evaluate(expr):
match expr:
case ("literal", 0): return 0;
case ("literal", 1): return M;
case ("variable", "w"): return W;
case ("variable", "x"): return X;
case ("variable", "y"): return Y;
case ("variable", "z"): return Z;
case ("!", inner):
return ~evaluate(inner) & 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 ("|!", 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 ("?:", cond, left, right):
cond = evaluate(cond);
return (cond & evaluate(left)) | (~cond & evaluate(right));
case _:
print(f'expr = {expr}');
assert(not "TODO");
def repl(args, cost, lookup):
import readline;
print("Give a boolean expression using the variables 'w', 'x', 'y' and 'z'.");
print("Operators: not ('!'), or ('||'), and ('&&').");
print();
print("The py-parser struggles with deeply-nested parentheses.");
print("I may have to just write my recursive-descent parser.");
print();
if args.extended_operators:
print("Extended operators: nor ('!|'), orn ('|!'), nand ('!&'), ");
print("andn ('&!'), xor ('!='), and nxor ('==')");
print();
print(f'I can simplify any tree down to {max(cost.values())} operators or less.')
print();
while True:
try:
line = input(">>> ");
except EOFError:
return;
line = line.strip();
if not line: continue;
try:
parsed = parse(line);
except BaseException as e:
print(f"syntax error: {e}");
continue;
truthtable = evaluate(parsed);
if truthtable in lookup:
text = "";
c = cost[truthtable];
LITERAL_ESCAPE = "\033[38;2;200;200;100m";
print(f'{cost[truthtable]}: {pretty(lookup[truthtable], args.color)}')
print(text);
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, args.color)}');
elif args.command:
try:
parsed = parse(args.command);
except BaseException as e:
print(f"syntax error: {e}");
return 1;
truthtable = evaluate(parsed);
if truthtable in lookup:
print(pretty(lookup[truthtable], args.color));
else:
print('unreachable.')
else:
repl(args, cost, lookup);
return 0;
exit(main(parse_args(sys.argv)))