added argparse and operator selection

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

442
main.py
View file

@ -1,62 +1,138 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import os;
import readline import readline
import argparse
import pickle; 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 W = 0b0101_0101_0101_0101
X = 0b0011_0011_0011_0011 X = 0b0011_0011_0011_0011
Y = 0b0000_1111_0000_1111 Y = 0b0000_1111_0000_1111
Z = 0b0000_0000_1111_1111 Z = 0b0000_0000_1111_1111
MASK = 0b1111_1111_1111_1111 M = 0b1111_1111_1111_1111
def pretty(exp): def calculate_simplifications(args, available_operators):
match exp: print(f'available_operators = {available_operators}')
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():
lookup = dict() # truthtable -> expression lookup = dict() # truthtable -> expression
costs = dict() # truthtable -> cost costs = dict() # truthtable -> cost
todo = [set() for _ in range(100)] # indexed by cost, set of truthtables. 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): def prequeue(truthtable, expression, cost):
lookup[truthtable] = expression; lookup[truthtable] = expression;
costs[truthtable] = cost; costs[truthtable] = cost;
todo[cost].add(truthtable); todo[cost].add(truthtable);
todo_count[0] += 1;
# literals: # literals:
prequeue(0b1111_1111_1111_1111, ("literal", 1), cost = 1); prequeue(0b1111_1111_1111_1111, ("literal", 1), cost = 1);
prequeue(0b0000_0000_0000_0000, ("literal", 0), cost = 1); prequeue(0b0000_0000_0000_0000, ("literal", 0), cost = 1);
@ -66,9 +142,9 @@ def calculate_simplifications():
prequeue(X, ("variable", "x"), cost = 0); prequeue(X, ("variable", "x"), cost = 0);
prequeue(Y, ("variable", "y"), cost = 0); prequeue(Y, ("variable", "y"), cost = 0);
prequeue(Z, ("variable", "z"), cost = 0); prequeue(Z, ("variable", "z"), cost = 0);
# completely unnecessary critera, alphabetical variables is more # Completely unnecessary critera, alphabetical variables is more
# aesthetically pleasing, says Benson. # aesthetically pleasing. Thanks Benson.
def is_aesthetically_better(this, that): def is_aesthetically_better(this, that):
def extract_variables(exp): def extract_variables(exp):
match exp: match exp:
@ -76,7 +152,7 @@ def calculate_simplifications():
return (); return ();
case ("variable", x): case ("variable", x):
return (x, ) return (x, )
case ("not", inner): case (_, inner):
return extract_variables(inner); return extract_variables(inner);
case (_, left, right): case (_, left, right):
return extract_variables(left) + extract_variables(right); return extract_variables(left) + extract_variables(right);
@ -87,62 +163,66 @@ def calculate_simplifications():
return extract_variables(this) < extract_variables(that); return extract_variables(this) < extract_variables(that);
unary_operators = { unary_operators = {
'not': lambda x: ~x, '!': lambda x: ~x,
} }
binary_operators = { binary_operators = {
'or': lambda x, y: x | y, '||': lambda x, y: (x | y) & M,
'and': lambda x, y: x & y, '&&': lambda x, y: (x & y) & M,
'nor': lambda x, y: ~(x | y) & MASK, '!|': lambda x, y: ~(x | y) & M,
'nand': lambda x, y: ~(x & y) & MASK, '!&': lambda x, y: ~(x & y) & M,
'less-than': lambda x, y: ~x & y, '&!': lambda x, y: (~x & y) & M,
'less-than-equal': lambda x, y: ~x | y, '|!': lambda x, y: (~x | y) & M,
'greater-than': lambda x, y: x & ~y, '!=': lambda x, y: (x ^ y) & M,
'greater-than-equal': lambda x, y: x | ~y, '==': lambda x, y: (~(x ^ y)) & M,
'equal': lambda x, y: ~(x ^ y),
'not-equal': lambda x, y: (x ^ y),
} }
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; min_cost = 0;
while sum(len(x) for x in todo): while todo_count[0]:
truthtables = todo[min_cost]; truthtables = todo[min_cost];
if not truthtables: if not truthtables:
min_cost += 1; min_cost += 1;
continue; continue;
todo_count = sum(len(x) for x in todo); assert(todo_count[0] <= 65536);
assert(todo_count <= 65536);
my_truthtable = min(truthtables); my_truthtable = min(truthtables);
truthtables.discard(my_truthtable); truthtables.discard(my_truthtable);
my_cost = min_cost; my_cost = min_cost;
my_expression = lookup[my_truthtable]; my_expression = lookup[my_truthtable];
todo_count[0] -= 1;
# print(f'{65536 - todo_count} of 65536 ({(65536 - todo_count) / 65536 * 100:.2f}%)');
if not args.quiet:
print(f'{65536 - todo_count} of 65536 ({(65536 - todo_count) / 65536 * 100:.2f}%): [{my_cost}] {pretty(my_expression)}'); print_status();
# 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)}')
def consider(new_truthtable, new_expression, new_cost): def consider(new_truthtable, new_expression, new_cost):
if new_truthtable not in costs: if new_truthtable not in costs:
todo[new_cost].add(new_truthtable); todo[new_cost].add(new_truthtable);
costs[new_truthtable] = new_cost costs[new_truthtable] = new_cost
lookup[new_truthtable] = new_expression 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]; current_cost = costs[new_truthtable];
assert(new_cost >= min_cost); assert(new_cost >= min_cost);
@ -155,63 +235,76 @@ def calculate_simplifications():
# consider unary operators: # consider unary operators:
for name, function in sorted(unary_operators.items()): for name, function in sorted(unary_operators.items()):
unary_truthtable = function(my_truthtable) & MASK; if name in available_operators:
unary_expression = (name, my_expression); unary_truthtable = function(my_truthtable) & M;
unary_cost = my_cost + 1; 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: # consider binary operators:
for name, function in sorted(binary_operators.items()): for name, function in sorted(binary_operators.items()):
for other_truthtable, other_expression in sorted(lookup.items()): if name in available_operators:
# x + y for other_truthtable, other_expression in sorted(lookup.items()):
binary_truthtable = function(my_truthtable, other_truthtable); # x + y
binary_truthtable = binary_truthtable & MASK binary_truthtable = function(my_truthtable, other_truthtable);
binary_expression = (name, my_expression, other_expression); binary_truthtable = binary_truthtable & M
binary_cost = my_cost + 1 + costs[other_truthtable]; 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 # y + x
binary_truthtable = function(other_truthtable, my_truthtable); binary_truthtable = function(other_truthtable, my_truthtable);
binary_truthtable = binary_truthtable & MASK binary_truthtable = binary_truthtable & M
binary_expression = (name, other_expression, my_expression); binary_expression = (name, other_expression, my_expression);
binary_cost = my_cost + 1 + costs[other_truthtable]; 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 return costs, lookup
pathname = "simplifications.bin" pathname = "simplifications.bin"
try: def get_simplifications(args, available_operators):
with open(pathname, "rb") as stream: available_operators = tuple(sorted(available_operators));
costs, lookup = pickle.load(stream);
except FileNotFoundError: try:
print("Oh! looks like you're running this for the first time"); with open(pathname, "rb") as stream:
print("I'll have to build up my cache of simplifications"); cache = pickle.load(stream);
print("This may take a while."); except FileNotFoundError:
print("I'll only have to do this once."); 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.");
print(); if not args.yes:
input("any input to start:"); print();
print(); input("any input to start:");
print();
costs, lookup = calculate_simplifications();
bundle = calculate_simplifications(args, available_operators);
print();
print("done!"); if not args.quiet:
print(); print();
print("done!");
with open(pathname, "wb") as stream: print();
pickle.dump((costs, lookup), stream);
cache[available_operators] = bundle;
print();
print("saved!"); with open(pathname, "wb") as stream:
print(); pickle.dump(cache, stream);
# for truthtable, exp in lookup.items(): if not args.quiet:
# print(f'{truthtable:016b}: {pretty(exp)}') print();
print("saved!");
print();
return cache[available_operators];
def create_parser(): def create_parser():
import pyparsing as pp import pyparsing as pp
@ -241,36 +334,30 @@ def create_parser():
logical_and_expression <<= \ logical_and_expression <<= \
Group(relational_expression + Literal('&&') + logical_and_expression) \ Group(relational_expression + Literal('&&') + 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; | relational_expression;
logical_or_expression = Forward() logical_or_expression = Forward()
logical_or_expression <<= \ 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) \ | Group(logical_and_expression + Literal('!|') + logical_or_expression) \
| Group(logical_and_expression + Literal('|!') + logical_or_expression) \
| logical_and_expression; | logical_and_expression;
root <<= logical_or_expression; root <<= logical_or_expression;
return root; return root;
parser = create_parser();
print(""" def parse(parser, text):
Please give a C-style expression using only variables 'w', 'x', 'y' and 'z'. return parser.parseString(text, parseAll = True).asList()[0]
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 evaluate(exp): def evaluate(expr):
match exp: match expr:
case "0": case "0":
return 0; return 0;
case "1": case "1":
return MASK; return M;
case "w": case "w":
return W; return W;
case "x": case "x":
@ -280,56 +367,89 @@ def evaluate(exp):
case "z": case "z":
return Z; return Z;
case ("!", subexp): case ("!", subexp):
return ~evaluate(subexp) & MASK; return ~evaluate(subexp) & M;
case (left, "||", right): case (left, "||", right):
return evaluate(left) | evaluate(right); return evaluate(left) | evaluate(right);
case (left, "!|", 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): case (left, "&&", right):
return evaluate(left) & evaluate(right); return evaluate(left) & evaluate(right);
case (left, "!&", 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): case (left, "<", right):
return (~evaluate(left) & evaluate(right)) & MASK; return (~evaluate(left) & evaluate(right)) & M;
case (left, "<=", right): case (left, "<=", right):
return (~evaluate(left) | evaluate(right)) & MASK; return (~evaluate(left) | evaluate(right)) & M;
case (left, ">", right): case (left, ">", right):
return (evaluate(left) & ~evaluate(right)) & MASK; return (evaluate(left) & ~evaluate(right)) & M;
case (left, ">=", right): case (left, ">=", right):
return (evaluate(left) | ~evaluate(right)) & MASK; return (evaluate(left) | ~evaluate(right)) & M;
case (left, "==", right): case (left, "==", right):
return ~(evaluate(left) ^ evaluate(right)) & MASK; return ~(evaluate(left) ^ evaluate(right)) & M;
case (left, "!=", right): case (left, "!=", right):
return evaluate(left) ^ evaluate(right); return evaluate(left) ^ evaluate(right);
case _: case _:
print(exp); print(exp);
assert(not "TODO"); assert(not "TODO");
while True: def repl(args, cost, lookup):
try: import readline;
raw_exp = input(">>> ");
except EOFError: print("""
print("exit"); Give a boolean expression using the variables 'w', 'x', 'y' and 'z'.
break; Operators: not ('!'), or ('||'), and ('&&').
More operators: nor ('!|'), orn ('|!'), nand ('!&'), andn ('&!'), xor ('!='),
try: More operators: nxor ('==')
exp = parser.parseString(raw_exp, parseAll = True).asList()[0] """);
except: print(f'I can do anything in {max(cost.values())} operations.')
print("syntax error"); print();
continue;
parser = create_parser();
truthtable = evaluate(exp);
while True:
print(f"done in {costs[truthtable]} operations:"); try:
line = input(">>> ");
print(f"0b{truthtable:016b}: {pretty(lookup[truthtable])}"); except EOFError:
return;
# print(pretty(lookup[truthtable]));
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)))