This commit is contained in:
Alex Thannhauser 2025-06-05 11:58:17 -05:00
parent dc8e79a9fa
commit 3feedab95c

185
main.py
View file

@ -8,51 +8,68 @@ 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('-E', '--experimental-operators', help='''
For two boolean variables, there are four possible inputs into a
binary operator. For each of those four an operator could return true
or false. That would mean in theory there could exist up to 16
theoretical operators with different behaviours. Even with the
'extended operators' option, we're only introducing 9 possible
operators, that's less that 60% of the possibilities!
Some of these would of course be useless, like a binary operator that
always produces 'false' regardless of the input, for instance, but
other may be extremely useful for acting in place of much more complex
trees of operators. In theory, the simplifier would produce the
smallest most compact expression trees possible.
Of course, no one would implement something like this and dare
to anger the logic gods...
''')
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 = {
@ -69,14 +86,32 @@ symbol_to_name = {
def determine_available_operators(args):
standard = ("!", "||", "&&");
extended = ("!|", "!&", "!=", "==", "|!", "&!");
experimental = (
"<0000>",
"<0001>",
"<0010>",
"<0011>",
"<0100>",
"<0101>",
"<0110>",
"<0111>",
"<1000>",
"<1001>",
"<1010>",
"<1011>",
"<1100>",
"<1101>",
"<1110>",
"<1111>");
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);
@ -86,8 +121,10 @@ def determine_available_operators(args):
available_operators.add(k);
else:
raise BaseException(f"not an operator: '{o}'");
return available_operators;
elif args.experimental_operators:
return set(experimental);
else:
return set(standard);
@ -95,16 +132,16 @@ 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");
@ -122,16 +159,16 @@ def calculate_simplifications(args, available_operators):
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:
# literals:
prequeue(0b1111_1111_1111_1111, ("literal", 1), cost = 1);
prequeue(0b0000_0000_0000_0000, ("literal", 0), cost = 1);
@ -140,7 +177,7 @@ def calculate_simplifications(args, available_operators):
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):
@ -165,32 +202,52 @@ def calculate_simplifications(args, available_operators):
}
binary_operators = {
'||': lambda x, y: (x | y) & M,
'&&': lambda x, y: (x & y) & M,
'||': lambda x, y: (x | y) & M, # or
'&&': lambda x, y: (x & y) & M, # and
'!|': lambda x, y: ~(x | y) & M,
'!&': lambda x, y: ~(x & y) & M,
'!|': lambda x, y: ~(x | y) & M, # nor
'!&': lambda x, y: ~(x & y) & M, # nand
'&!': lambda x, y: (~x & y) & M,
'|!': lambda x, y: (~x | y) & M,
'&!': lambda x, y: (~x & y) & M, # andn
'|!': lambda x, y: (~x | y) & M, # orn
'!=': lambda x, y: (x ^ y) & M,
'==': lambda x, y: (~(x ^ y)) & M,
'!=': lambda x, y: (x ^ y) & M, # xor
'==': lambda x, y: (~(x ^ y)) & M, # nxor
# these were found by running the simplifier with -o xor,and,or,not
# 1100 - x
# 1010 - y
"<0000>": lambda x, y: 0, # 0
"<0001>": lambda x, y: 0, # ((x == 0) && (y == 0)) || 0
"<0010>": lambda x, y: 0, # ((x == 0) && (y == 1)) || 0
"<0011>": lambda x, y: 0, # ((x == 0) && (y == 1)) || ((x == 0) && (y == 0)) || 0
"<0100>": lambda x, y: 0, # ((x == 1) && (y == 0)) || 0
"<0101>": lambda x, y: 0, # ((x == 1) && (y == 0)) || ((x == 0) && (y == 0)) || 0
"<0110>": lambda x, y: 0, # ((x == 1) && (y == 0)) || ((x == 0) && (y == 1)) || 0
"<0111>": lambda x, y: 0, # ((x == 1) && (y == 0)) || ((x == 0) && (y == 1)) || ((x == 0) && (y == 0)) || 0
"<1000>": lambda x, y: 0, # ((x == 1) && (y == 1)) || 0
"<1001>": lambda x, y: 0, # ((x == 1) && (y == 1)) || ((x == 0) && (y == 0)) || 0
"<1010>": lambda x, y: 0, # ((x == 1) && (y == 1)) || ((x == 0) && (y == 1)) || 0
"<1011>": lambda x, y: 0, # ((x == 1) && (y == 1)) || ((x == 0) && (y == 1)) || ((x == 0) && (y == 0)) || 0
"<1100>": lambda x, y: 0, # ((x == 1) && (y == 1)) || ((x == 1) && (y == 0)) || 0
"<1101>": lambda x, y: 0, # ((x == 1) && (y == 1)) || ((x == 1) && (y == 0)) || ((x == 0) && (y == 0)) || 0
"<1110>": lambda x, y: 0, # ((x == 1) && (y == 1)) || ((x == 1) && (y == 0)) || ((x == 0) && (y == 1)) || 0
"<1111>": lambda x, y: M, # ((x == 1) && (y == 1)) || ((x == 1) && (y == 0)) || ((x == 0) && (y == 1)) || ((x == 0) && (y == 0)) || 0
}
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];
@ -205,17 +262,17 @@ def calculate_simplifications(args, available_operators):
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 \
@ -266,13 +323,13 @@ 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");
@ -284,24 +341,24 @@ def get_simplifications(args, available_operators):
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():
@ -343,7 +400,7 @@ def create_parser():
| logical_and_expression;
root <<= logical_or_expression;
return root;
@ -396,35 +453,34 @@ def evaluate(expr):
def repl(args, cost, lookup):
import readline;
print("Give a boolean expression using the variables 'w', 'x', 'y' and 'z'.");
print("Operations: not ('!'), or ('||'), and ('&&').""");
print();
if args.extended_operators:
print("Extended operations: nor ('!|'), orn ('|!'), nand ('!&'), ");
print("andn ('&!'), xor ('!='), and nxor ('==')");
print();
# print(f'I can do anything in {max(cost.values())} operations.')
# print();
parser = create_parser();
while True:
try:
line = input(">>> ");
except EOFError:
return;
line = line.strip();
if not line: continue;
truthtable = evaluate(parse(parser, line));
if truthtable in lookup:
print(f'{cost[truthtable]}: {pretty(lookup[truthtable])}')
else:
@ -432,29 +488,28 @@ def repl(args, cost, lookup):
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;
return 0;
exit(main(parse_args(sys.argv)))