diff --git a/main.py b/main.py index 7b80a1c..269d693 100755 --- a/main.py +++ b/main.py @@ -1,62 +1,138 @@ #!/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(): +def calculate_simplifications(args, available_operators): + print(f'available_operators = {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); @@ -66,9 +142,9 @@ def calculate_simplifications(): 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, 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,62 +163,66 @@ 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]; - - # 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)}') - + 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 - 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)); + + 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."); + print("I'll only have to do this once."); - print(); - input("any input to start:"); - print(); - - costs, lookup = calculate_simplifications(); - - print(); - print("done!"); - print(); - - with open(pathname, "wb") as stream: - pickle.dump((costs, lookup), stream); - - print(); - print("saved!"); - print(); - -# for truthtable, exp in lookup.items(): -# print(f'{truthtable:016b}: {pretty(exp)}') + 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 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)))