Research source · python

check_antimirov_independent.py

site/public/research-artifacts/left-quotient-regular-expressions/check_antimirov_independent.py

405 lines. Source is displayed for inspection; it is not executed by this page.

File fingerprint

SHA-256: 3b7a4f77ef2875bb4e37da858ee9249c7aea11397270a927ebea1baadd85b9fa

#!/usr/bin/env python3
"""Independent finite audit for the regular-expression left quotient.

The construction side uses Antimirov partial derivatives over a raw binary
syntax tree parsed from infix strings.  The comparison side uses Thompson
epsilon-NFAs and subset reachability.  It does not call the Brzozowski
derivative, normalization, or nullability code in check_left_quotient.py.
"""

from __future__ import annotations

import hashlib
import itertools
import json
from collections import defaultdict, deque
from functools import lru_cache
from pathlib import Path

ALPHABET = ("a", "b")
ZERO = ("zero",)
ONE = ("one",)

A_SOURCES = (
    "0", "1", "a", "b", "(a|b)", "(a.b)", "(b.a)", "(a)*", "(b)*",
    "((a|b))*", "((a)*.b)", "(a.(b)*)", "((a.b))*", "((b.a))*",
    "((a|b).a)", "(a.(a|b))", "((a.b)|(b.a))", "((a)*.(b)*)",
    "(((a)*.b))*", "(((a.b))*.a)", "((a.b)|1)", "((a|1).b)",
    "(((a.b)|1))*", "(((a|b))*.a.b)", "((a)*|(b)*)",
    "((a)*.b.(a)*)", "(((a|b).(a|b)))*",
)

B_SOURCES = (
    "0", "1", "a", "b", "(a|b)", "(a.b)", "(b.a)", "(a)*", "(b)*",
    "((a|b))*", "((a)*.b)", "(a.(b)*)", "((a.b))*", "((a.b)|1)",
    "((a)*.b.(a)*)",
)


class Parser:
    def __init__(self, source: str):
        self.source = source
        self.index = 0

    def parse(self):
        expression = self.parse_union()
        if self.index != len(self.source):
            raise ValueError(f"trailing input at {self.index}: {self.source!r}")
        return expression

    def peek(self):
        return self.source[self.index] if self.index < len(self.source) else None

    def take(self, expected=None):
        token = self.peek()
        if token is None or (expected is not None and token != expected):
            raise ValueError(f"expected {expected!r} at {self.index}: {self.source!r}")
        self.index += 1
        return token

    def parse_union(self):
        expression = self.parse_concat()
        while self.peek() == "|":
            self.take("|")
            expression = ("alt", expression, self.parse_concat())
        return expression

    def parse_concat(self):
        expression = self.parse_postfix()
        while self.peek() == ".":
            self.take(".")
            expression = ("cat", expression, self.parse_postfix())
        return expression

    def parse_postfix(self):
        expression = self.parse_atom()
        while self.peek() == "*":
            self.take("*")
            expression = ("star", expression)
        return expression

    def parse_atom(self):
        token = self.peek()
        if token == "(":
            self.take("(")
            expression = self.parse_union()
            self.take(")")
            return expression
        self.take()
        if token == "0":
            return ZERO
        if token == "1":
            return ONE
        if token in ALPHABET:
            return ("lit", token)
        raise ValueError(f"unexpected {token!r}")


def parse(source: str):
    return Parser(source).parse()


def residual_cat(left, right):
    if left == ZERO or right == ZERO:
        return ZERO
    if left == ONE:
        return right
    if right == ONE:
        return left
    return ("cat", left, right)


@lru_cache(maxsize=None)
def antimirov_nullable(expression) -> bool:
    tag = expression[0]
    if tag in ("zero", "lit"):
        return False
    if tag in ("one", "star"):
        return True
    if tag == "alt":
        return antimirov_nullable(expression[1]) or antimirov_nullable(expression[2])
    if tag == "cat":
        return antimirov_nullable(expression[1]) and antimirov_nullable(expression[2])
    raise ValueError(tag)


@lru_cache(maxsize=None)
def partial(expression, letter: str):
    tag = expression[0]
    if tag in ("zero", "one"):
        return frozenset()
    if tag == "lit":
        return frozenset((ONE,)) if expression[1] == letter else frozenset()
    if tag == "alt":
        return partial(expression[1], letter) | partial(expression[2], letter)
    if tag == "cat":
        left, right = expression[1], expression[2]
        result = {residual_cat(item, right) for item in partial(left, letter)}
        result.discard(ZERO)
        if antimirov_nullable(left):
            result.update(partial(right, letter))
        return frozenset(result)
    if tag == "star":
        loop = expression
        return frozenset(
            item
            for item in (residual_cat(step, loop) for step in partial(expression[1], letter))
            if item != ZERO
        )
    raise ValueError(tag)


def partial_carrier(start):
    states = {start}
    edges = {}
    queue = deque((start,))
    while queue:
        state = queue.popleft()
        for letter in ALPHABET:
            targets = partial(state, letter)
            edges[(state, letter)] = targets
            for target in targets:
                if target not in states:
                    states.add(target)
                    queue.append(target)
    return states, edges


def compose(left, right):
    by_middle = defaultdict(set)
    for middle, target in right:
        by_middle[middle].add(target)
    return {
        (source, target)
        for source, middle in left
        for target in by_middle.get(middle, ())
    }


def closure(relation, states):
    result = set(relation) | {(state, state) for state in states}
    changed = True
    while changed:
        expanded = result | compose(result, result)
        changed = expanded != result
        result = expanded
    return result


def relation_semantics(expression, states, edges):
    identity = {(state, state) for state in states}

    @lru_cache(maxsize=None)
    def interpret(node):
        tag = node[0]
        if tag == "zero":
            return frozenset()
        if tag == "one":
            return frozenset(identity)
        if tag == "lit":
            return frozenset(
                (state, target)
                for state in states
                for target in edges[(state, node[1])]
            )
        if tag == "alt":
            return interpret(node[1]) | interpret(node[2])
        if tag == "cat":
            return frozenset(compose(interpret(node[1]), interpret(node[2])))
        if tag == "star":
            return frozenset(closure(interpret(node[1]), states))
        raise ValueError(tag)

    return interpret(expression)


def partial_run(states, word: str):
    current = frozenset(states)
    for letter in word:
        current = frozenset(
            target for state in current for target in partial(state, letter)
        )
    return current


def prepare_antimirov(a_expression, b_expression):
    states, edges = partial_carrier(b_expression)
    relation = relation_semantics(a_expression, states, edges)
    residuals = frozenset(
        target for source, target in relation if source == b_expression
    )
    return residuals, len(states)


def antimirov_accepts(residuals, suffix: str):
    return any(antimirov_nullable(state) for state in partial_run(residuals, suffix))


class Thompson:
    def __init__(self):
        self.next_state = 0
        self.epsilon = defaultdict(set)
        self.transitions = defaultdict(set)

    def fresh(self):
        state = self.next_state
        self.next_state += 1
        return state

    def compile(self, expression):
        tag = expression[0]
        start, final = self.fresh(), self.fresh()
        if tag == "zero":
            return start, final
        if tag == "one":
            self.epsilon[start].add(final)
            return start, final
        if tag == "lit":
            self.transitions[(start, expression[1])].add(final)
            return start, final
        if tag == "alt":
            left = self.compile(expression[1])
            right = self.compile(expression[2])
            self.epsilon[start].update((left[0], right[0]))
            self.epsilon[left[1]].add(final)
            self.epsilon[right[1]].add(final)
            return start, final
        if tag == "cat":
            left = self.compile(expression[1])
            right = self.compile(expression[2])
            self.epsilon[start].add(left[0])
            self.epsilon[left[1]].add(right[0])
            self.epsilon[right[1]].add(final)
            return start, final
        if tag == "star":
            body = self.compile(expression[1])
            self.epsilon[start].update((final, body[0]))
            self.epsilon[body[1]].update((final, body[0]))
            return start, final
        raise ValueError(tag)

    def epsilon_closure(self, states):
        result = set(states)
        queue = deque(result)
        while queue:
            state = queue.popleft()
            for target in self.epsilon[state]:
                if target not in result:
                    result.add(target)
                    queue.append(target)
        return frozenset(result)

    def step(self, states, letter: str):
        targets = {
            target
            for state in states
            for target in self.transitions[(state, letter)]
        }
        return self.epsilon_closure(targets)


def prepare_thompson(a_expression, b_expression):
    a_machine, b_machine = Thompson(), Thompson()
    a_start, a_final = a_machine.compile(a_expression)
    b_start, b_final = b_machine.compile(b_expression)
    start = (
        a_machine.epsilon_closure((a_start,)),
        b_machine.epsilon_closure((b_start,)),
    )
    reachable = {start}
    queue = deque((start,))
    while queue:
        a_states, b_states = queue.popleft()
        for letter in ALPHABET:
            target = (
                a_machine.step(a_states, letter),
                b_machine.step(b_states, letter),
            )
            if target not in reachable:
                reachable.add(target)
                queue.append(target)
    accepted_b_states = frozenset(
        b_states for a_states, b_states in reachable if a_final in a_states
    )
    return b_machine, b_final, accepted_b_states, len(reachable)


def thompson_accepts(prepared, suffix: str):
    machine, final, starts, _ = prepared
    for start in starts:
        states = start
        for letter in suffix:
            states = machine.step(states, letter)
        if final in states:
            return True
    return False


def words_up_to(maximum_length: int):
    return [
        "".join(letters)
        for length in range(maximum_length + 1)
        for letters in itertools.product(ALPHABET, repeat=length)
    ]


def main() -> int:
    a_expressions = tuple(parse(source) for source in A_SOURCES)
    b_expressions = tuple(parse(source) for source in B_SOURCES)
    suffixes = words_up_to(4)
    transcript = hashlib.sha256()
    mismatches = []
    max_carrier = 0
    max_product = 0

    for a_index, a_expression in enumerate(a_expressions):
        for b_index, b_expression in enumerate(b_expressions):
            residuals, carrier_size = prepare_antimirov(a_expression, b_expression)
            thompson = prepare_thompson(a_expression, b_expression)
            max_carrier = max(max_carrier, carrier_size)
            max_product = max(max_product, thompson[3])
            for suffix in suffixes:
                construction = antimirov_accepts(residuals, suffix)
                oracle = thompson_accepts(thompson, suffix)
                transcript.update(
                    f"{a_index}:{b_index}:{suffix}:{int(construction)}:{int(oracle)}\n".encode()
                )
                if construction != oracle:
                    mismatches.append({
                        "a_index": a_index,
                        "b_index": b_index,
                        "suffix": suffix,
                        "antimirov": construction,
                        "thompson": oracle,
                    })

    prior_path = Path(__file__).with_name("certificate.json")
    prior = json.loads(prior_path.read_text()) if prior_path.exists() else {}
    digest = transcript.hexdigest()
    certificate = {
        "certificate_type": "fprd-left-quotient-independent-audit.v1",
        "alphabet": list(ALPHABET),
        "quotienting_expressions": len(a_expressions),
        "target_expressions": len(b_expressions),
        "expression_pairs": len(a_expressions) * len(b_expressions),
        "suffixes": len(suffixes),
        "maximum_suffix_length": 4,
        "membership_comparisons": len(a_expressions) * len(b_expressions) * len(suffixes),
        "mismatch_count": len(mismatches),
        "maximum_antimirov_carrier_states": max_carrier,
        "maximum_thompson_product_states": max_product,
        "transcript_sha256": digest,
        "matches_v2_semantic_transcript": digest == prior.get("transcript_sha256"),
        "checker_sha256": hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
        "construction": "Antimirov partial derivatives on an independently parsed raw binary AST",
        "oracle": "Thompson epsilon-NFAs with subset-product reachability",
        "shared_mechanics": ["fixed infix expression corpus", "alphabet and case ordering"],
        "not_shared": ["regular-expression normalization", "nullability", "derivative code", "acceptance algorithm"],
        "mismatches": mismatches,
    }
    print(json.dumps(certificate, indent=2, sort_keys=True))
    return 1 if mismatches or not certificate["matches_v2_semantic_transcript"] else 0


if __name__ == "__main__":
    raise SystemExit(main())