added argparse and operator selection

This commit is contained in:
Zander Thannhauser 2025-06-04 09:09:12 -05:00
parent 9ff2d22e51
commit 43c25d701f

412
main.py
View file

@ -1,61 +1,137 @@
#!/usr/bin/env python3
import os;
import readline
import argparse
import pickle;
import sys;
from heapq import heappush, heappop
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
MASK = 0b1111_1111_1111_1111
M = 0b1111_1111_1111_1111
def pretty(exp):
match exp:
case ("literal", x):
return str(x);
case ("variable", x):
return x
case ("not", inner):
return f"(!{pretty(inner)})"
case ("or", left, right):
return f"({pretty(left)} || {pretty(right)})"
case ("nor", left, right):
return f"({pretty(left)} !| {pretty(right)})"
case ("and", left, right):
return f"({pretty(left)} && {pretty(right)})"
case ("nand", left, right):
return f"({pretty(left)} !& {pretty(right)})"
case ("less-than", left, right):
return f"({pretty(left)} < {pretty(right)})"
case ("less-than-equal", left, right):
return f"({pretty(left)} <= {pretty(right)})"
case ("greater-than", left, right):
return f"({pretty(left)} > {pretty(right)})"
case ("greater-than-equal", left, right):
return f"({pretty(left)} >= {pretty(right)})"
case ("equal", left, right):
return f"({pretty(left)} == {pretty(right)})"
case ("not-equal", left, right):
return f"({pretty(left)} != {pretty(right)})"
case _:
print(exp);
assert(not "TODO");
def calculate_simplifications(args, available_operators):
print(f'available_operators = {available_operators}')
def calculate_simplifications():
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);
@ -67,8 +143,8 @@ def calculate_simplifications():
prequeue(Y, ("variable", "y"), cost = 0);
prequeue(Z, ("variable", "z"), cost = 0);
# completely unnecessary critera, alphabetical variables is more
# aesthetically pleasing, says Benson.
# Completely unnecessary critera, alphabetical variables is more
# aesthetically pleasing. Thanks Benson.
def is_aesthetically_better(this, that):
def extract_variables(exp):
match exp:
@ -76,7 +152,7 @@ def calculate_simplifications():
return ();
case ("variable", x):
return (x, )
case ("not", inner):
case (_, inner):
return extract_variables(inner);
case (_, left, right):
return extract_variables(left) + extract_variables(right);
@ -87,54 +163,53 @@ def calculate_simplifications():
return extract_variables(this) < extract_variables(that);
unary_operators = {
'not': lambda x: ~x,
'!': lambda x: ~x,
}
binary_operators = {
'or': lambda x, y: x | y,
'and': lambda x, y: x & y,
'||': lambda x, y: (x | y) & M,
'&&': lambda x, y: (x & y) & M,
'nor': lambda x, y: ~(x | y) & MASK,
'nand': lambda x, y: ~(x & y) & MASK,
'!|': lambda x, y: ~(x | y) & M,
'!&': lambda x, y: ~(x & y) & M,
'less-than': lambda x, y: ~x & y,
'less-than-equal': lambda x, y: ~x | y,
'&!': lambda x, y: (~x & y) & M,
'|!': lambda x, y: (~x | y) & M,
'greater-than': lambda x, y: x & ~y,
'greater-than-equal': lambda x, y: x | ~y,
'equal': lambda x, y: ~(x ^ y),
'not-equal': lambda x, y: (x ^ y),
'!=': 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 sum(len(x) for x in todo):
while todo_count[0]:
truthtables = todo[min_cost];
if not truthtables:
min_cost += 1;
continue;
todo_count = sum(len(x) for x in todo);
assert(todo_count <= 65536);
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;
# print(f'{65536 - todo_count} of 65536 ({(65536 - todo_count) / 65536 * 100:.2f}%)');
print(f'{65536 - todo_count} of 65536 ({(65536 - todo_count) / 65536 * 100:.2f}%): [{my_cost}] {pretty(my_expression)}');
# print([len(x) for x in todo])
# print(f'{len(todo)}; {my_cost}; {len(lookup)}')
# print(f'{my_truthtable:016b}: {my_expression}')
# print(f'{my_truthtable:016b}: {pretty(my_expression)}')
if not args.quiet:
print_status();
def consider(new_truthtable, new_expression, new_cost):
if new_truthtable not in costs:
@ -142,7 +217,12 @@ def calculate_simplifications():
costs[new_truthtable] = new_cost
lookup[new_truthtable] = new_expression
elif new_cost < costs[new_truthtable] or (new_cost == costs[new_truthtable] and is_aesthetically_better(new_expression, lookup[new_truthtable])):
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);
@ -155,63 +235,76 @@ def calculate_simplifications():
# consider unary operators:
for name, function in sorted(unary_operators.items()):
unary_truthtable = function(my_truthtable) & MASK;
unary_expression = (name, my_expression);
unary_cost = my_cost + 1;
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(unary_truthtable, unary_expression, unary_cost);
# consider binary operators:
for name, function in sorted(binary_operators.items()):
for other_truthtable, other_expression in sorted(lookup.items()):
# x + y
binary_truthtable = function(my_truthtable, other_truthtable);
binary_truthtable = binary_truthtable & MASK
binary_expression = (name, my_expression, other_expression);
binary_cost = my_cost + 1 + costs[other_truthtable];
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);
consider(binary_truthtable, binary_expression, binary_cost);
# y + x
binary_truthtable = function(other_truthtable, my_truthtable);
binary_truthtable = binary_truthtable & MASK
binary_expression = (name, other_expression, my_expression);
binary_cost = my_cost + 1 + costs[other_truthtable];
# 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);
consider(binary_truthtable, binary_expression, binary_cost);
return costs, lookup
pathname = "simplifications.bin"
try:
with open(pathname, "rb") as stream:
costs, lookup = pickle.load(stream);
except FileNotFoundError:
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.");
def get_simplifications(args, available_operators):
available_operators = tuple(sorted(available_operators));
print();
input("any input to start:");
print();
try:
with open(pathname, "rb") as stream:
cache = pickle.load(stream);
except FileNotFoundError:
cache = dict();
costs, lookup = calculate_simplifications();
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.");
print();
print("done!");
print();
if not args.yes:
print();
input("any input to start:");
print();
with open(pathname, "wb") as stream:
pickle.dump((costs, lookup), stream);
bundle = calculate_simplifications(args, available_operators);
print();
print("saved!");
print();
if not args.quiet:
print();
print("done!");
print();
# for truthtable, exp in lookup.items():
# print(f'{truthtable:016b}: {pretty(exp)}')
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
@ -241,36 +334,30 @@ def create_parser():
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;
parser = create_parser();
print("""
Please give a C-style expression using only variables 'w', 'x', 'y' and 'z'.
You can use any of the following operators: '!' (not), '&&' (and), '||' (or),
'<' ('and' with left argument negated), '>' ('and' with right argument negated),
'<=' ('or' with left argument negated), '>=' ('or' with right argument negated),
'!=' (xor), '==' (negated xor), '!|' (nor), '!&' (nand).
The "simpliest" expression is the one that uses the fewest number of operators.
It should be noted that more than one expression could be considered the
"simpliest". This program chooses one arbitrarily (alphabetical order).
""");
def parse(parser, text):
return parser.parseString(text, parseAll = True).asList()[0]
def evaluate(exp):
match exp:
def evaluate(expr):
match expr:
case "0":
return 0;
case "1":
return MASK;
return M;
case "w":
return W;
case "x":
@ -280,56 +367,89 @@ def evaluate(exp):
case "z":
return Z;
case ("!", subexp):
return ~evaluate(subexp) & MASK;
return ~evaluate(subexp) & M;
case (left, "||", right):
return evaluate(left) | evaluate(right);
case (left, "!|", right):
return ~(evaluate(left) | evaluate(right)) & MASK;
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)) & MASK;
return ~(evaluate(left) & evaluate(right)) & M;
case (left, "&!", right):
return (~evaluate(left) & evaluate(right)) & M;
case (left, "<", right):
return (~evaluate(left) & evaluate(right)) & MASK;
return (~evaluate(left) & evaluate(right)) & M;
case (left, "<=", right):
return (~evaluate(left) | evaluate(right)) & MASK;
return (~evaluate(left) | evaluate(right)) & M;
case (left, ">", right):
return (evaluate(left) & ~evaluate(right)) & MASK;
return (evaluate(left) & ~evaluate(right)) & M;
case (left, ">=", right):
return (evaluate(left) | ~evaluate(right)) & MASK;
return (evaluate(left) | ~evaluate(right)) & M;
case (left, "==", right):
return ~(evaluate(left) ^ evaluate(right)) & MASK;
return ~(evaluate(left) ^ evaluate(right)) & M;
case (left, "!=", right):
return evaluate(left) ^ evaluate(right);
case _:
print(exp);
assert(not "TODO");
while True:
try:
raw_exp = input(">>> ");
except EOFError:
print("exit");
break;
try:
exp = parser.parseString(raw_exp, parseAll = True).asList()[0]
except:
print("syntax error");
continue;
truthtable = evaluate(exp);
print(f"done in {costs[truthtable]} operations:");
print(f"0b{truthtable:016b}: {pretty(lookup[truthtable])}");
# print(pretty(lookup[truthtable]));
def repl(args, cost, lookup):
import readline;
print("""
Give a boolean expression using the variables 'w', 'x', 'y' and 'z'.
Operators: not ('!'), or ('||'), and ('&&').
More operators: nor ('!|'), orn ('|!'), nand ('!&'), andn ('&!'), xor ('!='),
More operators: 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)))