문맥 자유 문법 문서의 검증 코드다. if __name__ == "__main__": 아래가 자체 테스트다.

"""문맥 자유 문법 문서의 예를 확인한다: 예제 문법들이 만드는 문자열, 균형 괄호 언어, 괄호 맞추기를 못 하는 작은 DFA.

실행: python3 11_context-free-grammar_verify.py  (표준 라이브러리만 쓴다. 다른 파일을 불러오지 않는다.)
"""


def G(text):
    """'A -> x B | ε' 꼴의 줄들을 {비단말: [생성 규칙(기호 튜플), ...]}로 바꾼다. ε은 빈 튜플."""
    g = {}
    for line in text.strip().splitlines():
        head, body = line.split("->")
        alts = g.setdefault(head.strip(), [])
        for alt in body.split("|"):
            syms = tuple(s for s in alt.split() if s != "ε")
            alts.append(syms)
    return g


def show(g):
    return {A: [" ".join(p) or "ε" for p in ps] for A, ps in g.items()}


def language(g, start, maxlen):
    """문법 g가 start에서 만들 수 있는, 길이 maxlen 이하인 단말 문자열 전부."""
    L = {A: set() for A in g}
    changed = True
    while changed:
        changed = False
        for A, prods in g.items():
            for p in prods:
                parts = {()}
                for s in p:
                    choices = L[s] if s in g else {(s,)}
                    parts = {x + y for x in parts for y in choices
                             if len(x) + len(y) <= maxlen}
                    if not parts:
                        break
                if not parts <= L[A]:
                    L[A] |= parts
                    changed = True
    return L[start]


def accepts_dfa(delta, start, finals, s):
    q = start
    for ch in s:
        q = delta[(q, ch)]
    return q in finals


def small_dfa_exists(k, maxlen):
    """상태 k개짜리 DFA 가운데 길이 maxlen 이하 문자열에서 '( n개 뒤 ) n개'만 받아들이는 것이 있는가."""
    from itertools import product
    strings = [s for n in range(maxlen + 1) for s in product("()", repeat=n)]
    want = [s == tuple("(" * (len(s) // 2) + ")" * (len(s) // 2)) and len(s) % 2 == 0 for s in strings]
    keys = [(q, ch) for q in range(k) for ch in "()"]
    for targets in product(range(k), repeat=len(keys)):
        delta = dict(zip(keys, targets))
        for bits in range(2 ** k):
            finals = {q for q in range(k) if bits >> q & 1}
            if all(accepts_dfa(delta, 0, finals, s) == w for s, w in zip(strings, want)):
                return True
    return False


if __name__ == "__main__":
    T = lambda s: tuple(s.split())

    # S → E + E, E → id. 만들 수 있는 문자열은 id + id 하나뿐이다.
    g = G("S -> E + E\nE -> id")
    assert language(g, "S", 8) == {T("id + id")}

    # BALANCED → ( BALANCED ) | ε 은 정확히 '( n개 뒤 ) n개'를 만든다.
    bal = G("B -> ( B ) | ε")
    assert language(bal, "B", 12) == {tuple("(" * n + ")" * n) for n in range(7)}

    # 상태 1~3개짜리 DFA를 모두 시험해도 길이 8 이하에서 이 언어를 맞게 가르는 것이 없다.
    for k in (1, 2, 3):
        assert not small_dfa_exists(k, 8)

    # STMT 규칙 두 개만으로는 끝나는 문장이 하나도 없다(재귀를 멈출 규칙이 없다).
    stmt = G("STMT -> if ( EXPR ) STMT else STMT | if ( EXPR ) STMT\nEXPR -> expr")
    assert language(stmt, "STMT", 15) == set()
    stmt2 = G("STMT -> if ( EXPR ) STMT else STMT | if ( EXPR ) STMT | other\nEXPR -> expr")
    assert T("if ( expr ) if ( expr ) other else other") in language(stmt2, "STMT", 11)

    # 산술식 문법. 문서의 다섯 예가 모두 나온다.
    arith = G("E -> E + E | E * E | ( E ) | id")
    L = language(arith, "E", 7)
    for s in ["id + id", "id * id", "( id )", "id + id * id", "( id + id ) * id"]:
        assert T(s) in L
    for s in ["id +", "( id", "id id", "+ id", "( )"]:
        assert T(s) not in L
    assert len(L) == 60

    # 함수 호출 문법.
    call = G("S -> id ( E ) | id ( )\nE -> id , E | id")
    L = language(call, "S", 8)
    for s in ["id ( id , id )", "id ( id )", "id ( )"]:
        assert T(s) in L
    for s in ["id ( id , )", "id ( , id )", "id id ( )"]:
        assert T(s) not in L
    # 인자가 k개면 길이가 2k + 2 (k ≥ 1), 인자가 없으면 3
    assert sorted(len(s) for s in L) == [3, 4, 6, 8]

    # while 문 문법. 조건은 '비교', '값 하나', '빈 것' 세 꼴이라 문자열이 4 + 2 + 1 = 7개다.
    wh = G("S -> while ( E )\nE -> T comparison T | T | ε\nT -> id | number")
    L = language(wh, "S", 10)
    assert len(L) == 7
    for s in ["while ( id comparison id )", "while ( id )", "while ( number )",
              "while ( number comparison number )", "while ( id comparison number )", "while ( )"]:
        assert T(s) in L
    assert T("while ( comparison id )") not in L

    # 카드 C3: 대괄호 목록 [ ], [ id ], [ id , id ], …
    lst = G("L -> [ ] | [ I ]\nI -> id , I | id")
    assert language(lst, "L", 7) == {T("[ ]"), T("[ id ]"), T("[ id , id ]"), T("[ id , id , id ]")}

    # 이산수학의 균형 괄호 세 규칙(ε, (s), st)을 그대로 옮긴 문법은 모든 균형 괄호를 만든다
    def balanced(s):
        depth = 0
        for ch in s:
            depth += 1 if ch == "(" else -1
            if depth < 0:
                return False
        return depth == 0
    from itertools import product
    dyck = {s for n in range(11) for s in product("()", repeat=n) if balanced(s)}
    full = G("B -> ( B ) | B B | ε")
    assert language(full, "B", 10) == dyck
    assert sorted(len([s for s in dyck if len(s) == 2 * k]) for k in range(6)) == [1, 1, 2, 5, 14, 42]
    # BALANCED → ( BALANCED ) | ε 은 그 가운데 겹치기만 한 것만 만든다: ()() 는 못 만든다
    assert tuple("()()") in dyck and tuple("()()") not in language(bal, "B", 10)
    assert language(bal, "B", 10) < dyck

    print("ALL CHECKS PASSED")