diff --git a/main.py b/main.py index 8857af0..2f3e49d 100755 --- a/main.py +++ b/main.py @@ -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)))