diff --git a/main.py b/main.py index 7f6a543..0194570 100755 --- a/main.py +++ b/main.py @@ -6,6 +6,8 @@ import argparse import pickle; import sys; +from colorsys import hsv_to_rgb; + def parse_args(argv): parser = argparse.ArgumentParser(prog = '4-variable-simplifier'); @@ -24,24 +26,6 @@ def parse_args(argv): You can refer to them using their names or their symbols. ''') - parser.add_argument('-E', '--experimental-operators', action='store_true', \ - 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. @@ -90,29 +74,6 @@ def determine_available_operators(args): extended = ("!|", "!&", "!=", "==", "|!", "&!"); - experimental = ( - "<00>", - "<01>", - "<10>", - "<11>", - - "<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: @@ -129,29 +90,72 @@ def determine_available_operators(args): raise BaseException(f"not an operator: '{o}'"); return available_operators; - elif args.experimental_operators: - return set(experimental); else: return set(standard); -def pretty(exp): +LITERAL_ESCAPE = "\033[38;2;200;200;100m"; +VARIABLE_ESCAPE = "\033[38;2;100;100;200m"; + +operator_colors = []; + +for i in range(10): + r, g, b = hsv_to_rgb(i / 10, 1.0, 0.8); + r, g, b = [int(255 * x) for x in (r, g, b)]; + operator_colors.append(f"\033[38;2;{r};{g};{b}m"); + +RESET_ESCAPE = "\033[0m"; + +def pretty(exp, in_color = False, depth = 0): + + start_color, end_color = "", ""; + + if in_color: + start_color = operator_colors[depth % len(operator_colors)]; + + end_color = RESET_ESCAPE; + + retval = ""; + match exp: case ("literal", x): - return str(x); + retval = str(x); + + if in_color: + retval = LITERAL_ESCAPE + retval + RESET_ESCAPE; case ("variable", x): - return x + retval = x + + if in_color: + retval = VARIABLE_ESCAPE + retval + RESET_ESCAPE; case (op, inner): - return f"({op}{pretty(inner)})" + retval += start_color + "(" + end_color; + + retval += start_color + op + end_color; + + retval += pretty(inner, in_color, depth + 1); + + retval += start_color + ")" + end_color; case (op, left, right): - return f"({pretty(left)} {op} {pretty(right)})" + retval += start_color + "(" + end_color; + + retval += pretty(left, in_color, depth + 1); + + retval += " " + start_color + op + end_color + " "; + + retval += pretty(right, in_color, depth + 1); + + retval += start_color + ")" + end_color; case _: print(exp); assert(not "TODO"); + return retval; + + W = 0b0101_0101_0101_0101 X = 0b0011_0011_0011_0011 Y = 0b0000_1111_0000_1111 @@ -174,7 +178,7 @@ def calculate_simplifications(args, available_operators): 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); @@ -205,13 +209,6 @@ def calculate_simplifications(args, available_operators): unary_operators = { '!': lambda x: ~x, - - # these were found by running the simplifier with -o xor,and,or,not - # 10 - x - "<00>": lambda x: 0, - "<01>": lambda x: ~x, - "<10>": lambda x: x, - "<11>": lambda x: M, } binary_operators = { @@ -226,26 +223,6 @@ def calculate_simplifications(args, available_operators): '!=': lambda x, y: x ^ y, # xor '==': lambda x, y: ~(x ^ y), # nxor - - # these were found by running the simplifier with -o xor,and,or,not - # 0011 - x - # 0101 - y - "<0000>": lambda x, y: 0, - "<1000>": lambda x, y: ~(x | y), - "<0100>": lambda x, y: x ^ (x | y), - "<1100>": lambda x, y: ~x, - "<0010>": lambda x, y: x ^ (x & y), - "<1010>": lambda x, y: ~y, - "<0110>": lambda x, y: x ^ y, - "<1110>": lambda x, y: ~(x & y), - "<0001>": lambda x, y: x & y, - "<1001>": lambda x, y: x ^ ~y, - "<0101>": lambda x, y: y, - "<1101>": lambda x, y: ~x | y, - "<0011>": lambda x, y: x, - "<1011>": lambda x, y: x | ~y, - "<0111>": lambda x, y: x | y, - "<1111>": lambda x, y: M, } def print_status(): @@ -374,109 +351,288 @@ def get_simplifications(args, available_operators): return cache[available_operators]; -def create_parser(): - from pyparsing import infix_notation, opAssoc, Word, oneOf +# def create_parser(): +# from pyparsing import infixNotation, opAssoc, Word, oneOf +# +# # this can't handle parenthesis very well... +# # do I really have to write my own parser? +# root = infixNotation(Word("01wxyz"), [ +# ('!', 1, opAssoc.RIGHT), +# (oneOf('< <= > >='), 2, opAssoc.LEFT), +# (oneOf('== !='), 2, opAssoc.LEFT), +# (oneOf('&& !& &!'), 2, opAssoc.LEFT), +# (oneOf('|| !| |!'), 2, opAssoc.LEFT)]); +# +# return root; - # this can't handle parenthesis very well... - # do I really have to write my own parser? - root = infix_notation(Word("01wxyz"), [ - ('!', 1, opAssoc.RIGHT), - (oneOf('< <= > >='), 2, opAssoc.LEFT), - (oneOf('== !='), 2, opAssoc.LEFT), - (oneOf('&& !& &!'), 2, opAssoc.LEFT), - (oneOf('|| !| |!'), 2, opAssoc.LEFT)]); +def parse(text): + class Tokenizer: + def __init__(self, text): + self.position = 0; + self.text = text; + self.tokenkind = None; + self.tokendata = None; + + def next(self): + # skip whitespace: + while self.position < len(self.text) and self.text[self.position].isspace(): + self.position += 1; + + if self.position == len(self.text): + self.tokenkind = "EOF"; + return; + + match self.text[self.position]: + case "0" | "1": + self.tokenkind = "literal"; + self.tokendata = self.text[self.position]; + self.position += 1; + + case "w" | "x" | "y" | "z": + self.tokenkind = "variable"; + self.tokendata = self.text[self.position]; + self.position += 1; + + case "(" | ")": + self.tokenkind = self.text[self.position]; + self.position += 1; + + case ">": + # could be ">" or ">=": + self.position += 1; + + match self.text[self.position]: + case "=": + self.tokenkind = ">="; + self.position += 1; + case _: + self.tokenkind = ">" + + case "<": + # could be "<" or "<=": + self.position += 1; + + match self.text[self.position]: + case "=": + self.tokenkind = "<="; + self.position += 1; + case _: + self.tokenkind = "<" + + case "=": + # could only be "==" + self.position += 1; + + match self.text[self.position]: + case "=": + self.tokenkind = "=="; + self.position += 1; + + case _: + raise BaseException("expecting '=' after '='"); + + case "!": + # could be "!" or "!|" or "&!" or "!=" + self.position += 1; + + match self.text[self.position]: + case "&": + self.tokenkind = "!&"; + self.position += 1; + case "|": + self.tokenkind = "!|"; + self.position += 1; + case "=": + self.tokenkind = "!="; + self.position += 1; + case _: + self.tokenkind = "!" + + case "&": + # could be "&&" or "&!" + self.position += 1; + + match self.text[self.position]: + case "&": + self.tokenkind = "&&"; + self.position += 1; + case "!": + self.tokenkind = "&!"; + self.position += 1; + case _: + raise BaseException("expecting '&' or '!' after '&'"); + + case "|": + # could be "||" or "|!" + self.position += 1; + + match self.text[self.position]: + case "|": + self.tokenkind = "||"; + self.position += 1; + case "!": + self.tokenkind = "|!"; + self.position += 1; + case _: + raise BaseException("expecting '|' or '!' after '|'"); + + case x: + raise BaseException(f'unknown token "{x}"!'); + + def parse_primary(tokenizer): + match tokenizer.tokenkind: + case "literal": + ast = ("literal", int(tokenizer.tokendata)); + tokenizer.next(); + + case "variable": + ast = ("variable", tokenizer.tokendata); + tokenizer.next(); + + case "(": + tokenizer.next(); + + ast = parse_root(tokenizer); + + if tokenizer.tokenkind != ")": + raise BaseException(f'expected token ")", found: {tokenizer.tokenkind}'); + + tokenizer.next(); + + case x: + raise BaseException(f'unexpected token "{x}"!'); + + return ast; + + def parse_prefix(tokenizer): + if tokenizer.tokenkind == "!": + tokenizer.next(); + return ("!", parse_prefix(tokenizer)); + else: + return parse_primary(tokenizer); + + def parse_compares(tokenizer): + left = parse_prefix(tokenizer); + + while tokenizer.tokenkind in ("<=", "<", ">", ">="): + op = tokenizer.tokenkind; + tokenizer.next(); + left = (op, left, parse_prefix(tokenizer)); + + return left; + + def parse_equals(tokenizer): + left = parse_compares(tokenizer); + + while tokenizer.tokenkind in ("==", "!="): + op = tokenizer.tokenkind; + tokenizer.next(); + left = (op, left, parse_compares(tokenizer)); + + return left; + + def parse_ands(tokenizer): + left = parse_equals(tokenizer); + + while tokenizer.tokenkind in ("&&", "&!", "!&"): + op = tokenizer.tokenkind; + tokenizer.next(); + left = (op, left, parse_equals(tokenizer)); + + return left; + + def parse_ors(tokenizer): + left = parse_ands(tokenizer); + + while tokenizer.tokenkind in ("||", "|!", "!|"): + op = tokenizer.tokenkind; + tokenizer.next(); + left = (op, left, parse_ands(tokenizer)); + + return left; + + def parse_root(tokenizer): + return parse_ors(tokenizer); + + tokenizer = Tokenizer(text); + + tokenizer.next(); + + root = parse_root(tokenizer); + + if tokenizer.tokenkind != "EOF": + raise BaseException( + f"reached end of expression. " + f"Expecting EOF. found: {tokenizer.tokenkind}!"); return root; -def parse(parser, text): - return parser.parseString(text, parseAll = True).asList()[0] - def evaluate(expr): match expr: - case "0": - return 0; - case "1": - return M; - case "w": - return W; - case "x": - return X; - case "y": - return Y; - case "z": - return Z; - case ("!", subexp): - return ~evaluate(subexp) & M; - case (first, "||", *rest): - result = evaluate(first); - for subexpr in rest[::2]: result = result | evaluate(subexpr); - return result; - case (first, "!|", *rest): - result = evaluate(first); - for subexpr in rest[::2]: result = ~(result | evaluate(subexpr)) & M; - return result; - case (first, "|!", *rest): - result = evaluate(first); - for subexpr in rest[::2]: result = (~result | evaluate(subexpr)) & M; - return result; - case (first, "&&", *rest): - result = evaluate(first); - for subexpr in rest[::2]: result = result & evaluate(subexpr); - return result; - case (first, "!&", *rest): - result = evaluate(first); - for subexpr in rest[::2]: result = ~(result & evaluate(subexpr)) & M; - return result; - case (first, "&!", *rest): - result = evaluate(first); - for subexpr in rest[::2]: result = (~result & evaluate(subexpr)) & M; - return result; - case (first, "<", *rest): - result = evaluate(first); - for subexpr in rest[::2]: result = (~result & evaluate(subexpr)) & M; - return result; - case (first, "<=", *rest): - result = evaluate(first); - for subexpr in rest[::2]: result = (~result | evaluate(subexpr)) & M; - return result; - case (first, ">", *rest): - result = evaluate(first); - for subexpr in rest[::2]: result = (result & ~evaluate(subexpr)) & M; - return result; - case (first, ">=", *rest): - result = evaluate(first); - for subexpr in rest[::2]: result = (result | ~evaluate(subexpr)) & M; - return result; - case (first, "!=", *rest): - result = evaluate(first); - for subexpr in rest[::2]: result = (result ^ evaluate(subexpr)) & M - return result; - case (first, "==", *rest): - result = evaluate(first); - for subexpr in rest[::2]: result = ~(result ^ evaluate(subexpr)) & M - return result; + case ("literal", 0): return 0; + case ("literal", 1): return M; + + case ("variable", "w"): return W; + case ("variable", "x"): return X; + case ("variable", "y"): return Y; + case ("variable", "z"): return Z; + + case ("!", inner): + return ~evaluate(inner) & M; + + case ("<", left, right): + return (~evaluate(left) & evaluate(right)) & M; + case ("<=", left, right): + return (~evaluate(left) | evaluate(right)) & M; + case (">=", left, right): + return ( evaluate(left) | ~evaluate(right)) & M; + case (">", left, right): + return ( evaluate(left) & ~evaluate(right)) & M; + + case ("!=", left, right): + 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)) & 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)) & M; + case ("!&", left, right): + return ~(evaluate(left) & evaluate(right)) & M; + case _: - print(expr); + print(f'expr = {expr}'); assert(not "TODO"); 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("Operators: not ('!'), or ('||'), and ('&&')."); + + print(); + + print("The py-parser struggles with deeply-nested parentheses."); + print("I may have to just write my recursive-descent parser."); print(); if args.extended_operators: - print("Extended operations: nor ('!|'), orn ('|!'), nand ('!&'), "); + print("Extended operators: nor ('!|'), orn ('|!'), nand ('!&'), "); print("andn ('&!'), xor ('!='), and nxor ('==')"); print(); - print(f'I can do anything in {max(cost.values())} operations.') + print(f'I can simplify any tree down to {max(cost.values())} operators or less.') print(); - parser = create_parser(); - while True: try: line = input(">>> "); @@ -487,10 +643,24 @@ def repl(args, cost, lookup): if not line: continue; - truthtable = evaluate(parse(parser, line)); + try: + parsed = parse(line); + except BaseException as e: + print(f"syntax error: {e}"); + continue; + + truthtable = evaluate(parsed); if truthtable in lookup: - print(f'{cost[truthtable]}: {pretty(lookup[truthtable])}') + text = ""; + + c = cost[truthtable]; + + LITERAL_ESCAPE = "\033[38;2;200;200;100m"; + + print(f'{cost[truthtable]}: {pretty(lookup[truthtable], args.color)}') + + print(text); else: print('unreachable.') @@ -506,12 +676,18 @@ def main(args): if args.printout: for truthtable, exp in sorted(lookup.items()): - print(f'{truthtable:016b}: {pretty(exp)}'); + print(f'{truthtable:016b}: {pretty(exp, args.color)}'); elif args.command: - truthtable = evaluate(parse(create_parser(), args.command)); + try: + parsed = parse(args.command); + except BaseException as e: + print(f"syntax error: {e}"); + return 1; + + truthtable = evaluate(parsed); if truthtable in lookup: - print(pretty(lookup[truthtable])); + print(pretty(lookup[truthtable], args.color)); else: print('unreachable.') else: