from abc import abstractmethod class U16: def __init__(self, value: int): self.value = value & 0xffff def __invert__(self): return U16(~self.value) def __and__(self, other): return U16(self.value & other.value) def __or__(self, other): return U16(self.value | other.value) def __rshift__(self, other): return U16(self.value >> other.value) def __lshift__(self, other): return U16(self.value << other.value) def __str__(self): return str(self.value) def __repr__(self): return f'{self.value}' class Node: _value: U16 | None = None @property def value(self) -> U16: if self._value is None: self._value = self.eval() return self._value @abstractmethod def eval(self) -> U16: pass @abstractmethod def __repr__(self) -> str: pass class Reference(Node): def __init__(self, net: dict[Node], name: str): self.net = net self.name = name def eval(self) -> U16: return self.net[self.name].value def __repr__(self) -> str: return f'Reference({self.name})' class Constant(Node): def __init__(self, value): self._value = value def __repr__(self) -> str: return f'Constant({self.value})' class BinaryOp(Node): def __init__(self, left, right): self.left = left self.right = right def __repr__(self) -> str: return f'{self.__class__.__name__}({self.left}, {self.right})' class And(BinaryOp): def eval(self) -> U16: return self.left.value & self.right.value class Or(BinaryOp): def eval(self) -> U16: return self.left.value | self.right.value class LShift(BinaryOp): def eval(self) -> U16: return self.left.value << self.right.value class RShift(BinaryOp): def eval(self) -> U16: return self.left.value >> self.right.value class Not(Node): def __init__(self, node): self.node = node def eval(self): return ~self.node.value def __repr__(self) -> str: return 'Not({self.node})' def parse_expr(net: dict[Node], expr: str) -> Node: parts = expr.split(' ') if len(parts) == 3: lhs = parse_expr(net, parts[0]) rhs = parse_expr(net, parts[2]) if parts[1] == 'AND': return And(lhs, rhs) elif parts[1] == 'OR': return Or(lhs, rhs) elif parts[1] == 'LSHIFT': return LShift(lhs, rhs) elif parts[1] == 'RSHIFT': return RShift(lhs, rhs) elif len(parts) == 2: if parts[0] == 'NOT': return Not(parse_expr(net, parts[1])) elif len(parts) == 1: if expr.isnumeric(): return Constant(U16(int(expr))) if expr.isalpha(): return Reference(net, expr) raise ValueError(expr) def parse_conn(net: dict[Node], conn: str) -> tuple[str, Node]: expr, target = conn.split(" -> ") return target, parse_expr(net, expr) def part1(inp: list[str]) -> int: net = dict() for conn in inp: target, node = parse_conn(net, conn) net[target] = node return net['a'].value def part2(inp: list[str]) -> int: net = dict() for conn in inp: target, node = parse_conn(net, conn) net[target] = node net['b'] = Constant(part1(inp)) return net['a'].value if __name__ == '__main__': with open('input') as f: inp = [line.strip() for line in f] print(part1(inp)) print(part2(inp))