좌재귀 없애기 문서의 검증 코드다. if __name__ == "__main__": 아래가 자체 테스트다.

"""좌재귀 없애기 문서의 예를 확인한다: 본문의 변환, 변환 전후 언어 비교, 간접 좌재귀, 좌재귀에서 재귀 하강이 끝나지 않는 것.

실행: python3 14_left-recursion-elimination_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 eliminate_immediate(g, S, new):
    """S → S α1 | … | S αm | β1 | … | βn 을 S → β1 new | …, new → α1 new | … | ε 으로 고친다."""
    alphas = [p[1:] for p in g[S] if p and p[0] == S]
    betas = [p for p in g[S] if not (p and p[0] == S)]
    if not alphas:
        return dict(g)
    out = {}
    for A, prods in g.items():
        if A == S:
            out[S] = [b + (new,) for b in betas]
            out[new] = [a + (new,) for a in alphas] + [()]
        else:
            out[A] = list(prods)
    return out


def nullable_set(g):
    nul = set()
    changed = True
    while changed:
        changed = False
        for A, prods in g.items():
            if A not in nul and any(all(s in nul for s in p) for p in prods):
                nul.add(A)
                changed = True
    return nul


def left_recursive(g):
    """A ⇒+ A α 인 비단말 A들. '맨 왼쪽에 올 수 있다' 관계를 끝까지 따라가 자기 자신으로 돌아오는지 본다."""
    nul = nullable_set(g)
    edge = {A: set() for A in g}
    for A, prods in g.items():
        for p in prods:
            for s in p:
                if s in g:
                    edge[A].add(s)
                if s not in nul:
                    break
    out = set()
    for A in g:
        seen, todo = set(), list(edge[A])
        while todo:
            x = todo.pop()
            if x not in seen:
                seen.add(x)
                todo.extend(edge[x])
        if A in seen:
            out.add(A)
    return out


def naive_descent_depth(g, start, limit=200):
    """맨 왼쪽 비단말을 첫 규칙으로만 바꿔 갈 때, 입력을 한 글자도 못 읽은 채 몇 번 내려가는가."""
    depth, sym = 0, start
    while sym in g and depth < limit:
        p = g[sym][0]
        if not p or p[0] not in g:
            break
        sym = p[0]
        depth += 1
    return depth


if __name__ == "__main__":
    # S → Sa | b  ⇒  S → bA, A → aA | ε
    g = G("S -> S a | b")
    h = eliminate_immediate(g, "S", "A")
    assert show(h) == {"S": ["b A"], "A": ["a A", "ε"]}
    assert language(g, "S", 9) == language(h, "S", 9) == {("b",) + ("a",) * k for k in range(9)}
    assert left_recursive(g) == {"S"} and left_recursive(h) == set()

    # 좌재귀 문법에서 첫 규칙만 따라 내려가면 입력을 읽지 못한 채 끝없이 내려간다.
    assert naive_descent_depth(g, "S") == 200
    assert naive_descent_depth(h, "S") == 0

    # 일반 꼴: α 둘, β 둘
    g2 = G("S -> S a | S b c | d | e f")
    h2 = eliminate_immediate(g2, "S", "A")
    assert show(h2) == {"S": ["d A", "e f A"], "A": ["a A", "b c A", "ε"]}
    assert language(g2, "S", 8) == language(h2, "S", 8)

    # 산술식: E → E + T | T
    g3 = G("E -> E + T | T\nT -> id")
    h3 = eliminate_immediate(g3, "E", "E'")
    assert show(h3) == {"E": ["T E'"], "E'": ["+ T E'", "ε"], "T": ["id"]}
    assert language(g3, "E", 9) == language(h3, "E", 9)

    # 대표 문제 2: S → Aa | b, A → Sb 는 좌재귀인가? S ⇒ Aa ⇒ Sba 라서 그렇다(간접 좌재귀).
    g4 = G("S -> A a | b\nA -> S b")
    assert left_recursive(g4) == {"S", "A"}
    # A를 S의 규칙에 넣어 직접 좌재귀로 만든 뒤 같은 방법으로 없앤다.
    g4b = G("S -> S b a | b")
    assert language(g4, "S", 9) == language(g4b, "S", 9)
    h4 = eliminate_immediate(g4b, "S", "S'")
    assert show(h4) == {"S": ["b S'"], "S'": ["b a S'", "ε"]}
    assert language(g4, "S", 9) == language(h4, "S", 9) and left_recursive(h4) == set()

    # 우재귀는 좌재귀가 아니다
    assert left_recursive(G("E -> T + E | T\nT -> F * T | F\nF -> ( E ) | id")) == set()
    # 빈 문자열이 될 수 있는 기호 뒤에 숨은 좌재귀도 잡는다: S → B S c | d, B → ε | b
    assert left_recursive(G("S -> B S c | d\nB -> ε | b")) == {"S"}

    # 카드: L → L , id | id  /  A → Bc | d, B → Ae  /  A → Ab | Ac | d
    c1 = eliminate_immediate(G("L -> L , id | id"), "L", "L'")
    assert show(c1) == {"L": ["id L'"], "L'": [", id L'", "ε"]}
    assert left_recursive(G("A -> B c | d\nB -> A e")) == {"A", "B"}
    assert left_recursive(G("S -> a S b | ε")) == set()
    c3 = eliminate_immediate(G("A -> A b | A c | d"), "A", "A'")
    assert show(c3) == {"A": ["d A'"], "A'": ["b A'", "c A'", "ε"]}

    print("ALL CHECKS PASSED")