commit 258ddee9055fbe9bf3716740201d672e0eda040b Author: Sam Hadow Date: Mon Apr 13 10:38:00 2026 +0200 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c546bbd --- /dev/null +++ b/LICENSE @@ -0,0 +1,12 @@ +Copyright (c) 2026 sam.hadow. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + 4. Redistributions of any form whatsoever must retain the following acknowledgment: 'This product includes software developed by "Sam Hadow" (http://hadow.fr/).' + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/anf.py b/anf.py new file mode 100644 index 0000000..3057a83 --- /dev/null +++ b/anf.py @@ -0,0 +1,218 @@ +#!/bin/python3 +from __future__ import annotations + +import re +from dataclasses import dataclass +from typing import FrozenSet, Iterable, List, Union + +import sys + +##################### +# ANF representation over GF(2) +##################### +# +# polynomial: set of monomials. +# monomial: frozenset of variable names. +# +# Examples: +# 0 -> set() +# 1 -> {frozenset()} +# X3 -> {frozenset({"X3"})} +# X3 XOR X4 -> {{"X3"}, {"X4"}} +# X3 AND X4 -> {{"X3","X4"}} + + +Monomial = FrozenSet[str] +Poly = set[Monomial] + + +def zero() -> Poly: + return set() + + +def one() -> Poly: + return {frozenset()} + + +def var(name: str) -> Poly: + return {frozenset([name])} + + +def poly_xor(a: Poly, b: Poly) -> Poly: + # symmetric difference = XOR over GF(2) + return a ^ b + + +def poly_and(a: Poly, b: Poly) -> Poly: + out: Poly = set() + for ma in a: + for mb in b: + m = frozenset(set(ma) | set(mb)) + if m in out: + out.remove(m) + else: + out.add(m) + return out + + +def poly_not(a: Poly) -> Poly: + # NOT a = a XOR 1 + return poly_xor(a, one()) + + +##################### +# Tokenizer +##################### + +TOKEN_RE = re.compile( + r""" + \s*( + \(|\)| + !| + AND|OR| + 0|1| + X\d+| + [A-Za-z_][A-Za-z0-9_]* + ) + """, + re.VERBOSE, +) + +def tokenize(s: str) -> List[str]: + tokens = TOKEN_RE.findall(s) + stripped = re.sub(r"\s+", "", s) + joined = "".join(tokens).replace("AND", "AND").replace("OR", "OR") + if re.sub(r"\s+", "", joined) != stripped: + raise ValueError(f"Invalid token near: {s!r}") + return tokens + + +##################### +# Parser +##################### +# priotity order: +# ! +# AND +# OR +# +# Grammar: +# expr := or_expr +# or_expr := and_expr ("OR" and_expr)* +# and_expr := not_expr ("AND" not_expr)* +# not_expr := "!" not_expr | atom +# atom := VAR | 0 | 1 | "(" expr ")" + + +class Parser: + def __init__(self, tokens: List[str]): + self.tokens = tokens + self.i = 0 + + def peek(self) -> str | None: + return self.tokens[self.i] if self.i < len(self.tokens) else None + + def eat(self, expected: str | None = None) -> str: + tok = self.peek() + if tok is None: + raise ValueError("Unexpected end of input") + if expected is not None and tok != expected: + raise ValueError(f"Expected {expected!r}, got {tok!r}") + self.i += 1 + return tok + + def parse(self) -> Poly: + p = self.parse_or() + if self.peek() is not None: + raise ValueError(f"Unexpected token: {self.peek()!r}") + return p + + def parse_or(self) -> Poly: + left = self.parse_and() + while self.peek() == "OR": + self.eat("OR") + right = self.parse_and() + # A OR B = A XOR B XOR (A AND B) + left = poly_xor(poly_xor(left, right), poly_and(left, right)) + return left + + def parse_and(self) -> Poly: + left = self.parse_not() + while self.peek() == "AND": + self.eat("AND") + right = self.parse_not() + left = poly_and(left, right) + return left + + def parse_not(self) -> Poly: + if self.peek() == "!": + self.eat("!") + return poly_not(self.parse_not()) + return self.parse_atom() + + def parse_atom(self) -> Poly: + tok = self.peek() + if tok is None: + raise ValueError("Unexpected end of input") + + if tok == "(": + self.eat("(") + inside = self.parse_or() + self.eat(")") + return inside + + if tok == "0": + self.eat("0") + return zero() + + if tok == "1": + self.eat("1") + return one() + + if re.fullmatch(r"X\d+|[A-Za-z_][A-Za-z0-9_]*", tok): + self.eat() + return var(tok) + + raise ValueError(f"Unexpected token: {tok!r}") + +##################### +# CLI PRETTY PRINT +##################### + +def monomial_key(m: Monomial): + return (len(m), tuple(sorted(m))) + + +def format_monomial(m: Monomial) -> str: + if len(m) == 0: + return "1" + if len(m) == 1: + return next(iter(m)) + parts = sorted(m, key=lambda x: (len(x), x)) + return "(" + "⸱".join(parts) + ")" + + +def format_poly(p: Poly) -> str: + if not p: + return "0" + + monos = sorted(p, key=monomial_key) + return "⊕".join(format_monomial(m) for m in monos) + + +def convert(expr: str) -> str: + tokens = tokenize(expr) + poly = Parser(tokens).parse() + return format_poly(poly) + +##################### +# CLI +##################### + +if __name__ == "__main__": + + if len(sys.argv) > 1: + expr = " ".join(sys.argv[1:]) + else: + expr = input("Enter formula: ").strip() + + print(convert(expr))