removed experimental stuff, since it was not any better than the classic 9 operators. switch from py-parsing to handrolled recursive descent

This commit is contained in:
Alex Thannhauser 2025-06-10 12:29:50 -05:00
parent e3b0af2f12
commit 2499db7c7d

500
main.py
View file

@ -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: