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

"""유도와 파스 트리 문서의 예를 확인한다: 본문의 좌측·우측 유도, 잎을 왼쪽부터 읽기, 파스 트리와 좌측 유도의 일대일 대응.

실행: python3 12_derivation-parse-tree_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 min_len(g):
    """비단말마다 만들 수 있는 가장 짧은 문자열의 길이."""
    INF = 10 ** 9
    m = {A: INF for A in g}
    changed = True
    while changed:
        changed = False
        for A, prods in g.items():
            for p in prods:
                t = sum(m[s] if s in g else 1 for s in p)
                if t < m[A]:
                    m[A] = t
                    changed = True
    return m


def parse_trees(g, start, tokens):
    """tokens의 파스 트리를 모두 돌려준다. 비단말 노드는 (이름, 자식...), 단말은 문자열."""
    tokens = tuple(tokens)
    m = min_len(g)
    memo = {}

    def sym(s, i, j):
        if s not in g:
            return [s] if j == i + 1 and tokens[i] == s else []
        key = (s, i, j)
        if key in memo:
            return memo[key]
        memo[key] = []
        out = []
        for p in g[s]:
            for kids in seq(p, i, j):
                out.append((s,) + kids)
        memo[key] = out
        return out

    def seq(p, i, j):
        if not p:
            return [()] if i == j else []
        first_min = m[p[0]] if p[0] in g else 1
        rest_min = sum(m[x] if x in g else 1 for x in p[1:])
        out = []
        for k in range(i + first_min, j - rest_min + 1):
            heads = sym(p[0], i, k)
            if heads:
                for tail in seq(p[1:], k, j):
                    for h in heads:
                        out.append((h,) + tail)
        return out

    return sym(start, 0, len(tokens))


def leaves(t):
    """트리의 잎을 왼쪽부터 읽는다."""
    if isinstance(t, str):
        return [t]
    return [x for c in t[1:] for x in leaves(c)]


def bracket(t):
    """트리를 한 줄 글로 적는다. 예: E[E[id] + E[id]]"""
    if isinstance(t, str):
        return t
    return t[0] + "[" + " ".join(bracket(c) for c in t[1:]) + "]" if len(t) > 1 else t[0] + "[ε]"


def derivation(tree, leftmost=True):
    """파스 트리에서 좌측(또는 우측) 유도의 문장 형식들을 차례로 만든다."""
    form = [tree]
    steps = [[n if isinstance(n, str) else n[0] for n in form]]
    while any(not isinstance(n, str) for n in form):
        idxs = [i for i, n in enumerate(form) if not isinstance(n, str)]
        i = idxs[0] if leftmost else idxs[-1]
        form[i:i + 1] = list(form[i][1:])
        steps.append([n if isinstance(n, str) else n[0] for n in form])
    return [" ".join(s) for s in steps]


def count_leftmost_derivations(g, start, tokens):
    """tokens에 이르는 좌측 유도가 몇 가지인지 직접 센다(ε 규칙이 없는 문법용)."""
    tokens = tuple(tokens)

    def go(form):
        i = next((k for k, s in enumerate(form) if s in g), None)
        if i is None:
            return 1 if form == tokens else 0
        if len(form) > len(tokens) or form[:i] != tokens[:i]:
            return 0
        return sum(go(form[:i] + p + form[i + 1:]) for p in g[form[i]])

    return go((start,))


if __name__ == "__main__":
    g = G("E -> E + E | E * E | ( E ) | id")
    s = "id * id + id".split()
    trees = parse_trees(g, "E", s)
    # 본문의 트리: 뿌리가 + 이고 왼쪽 자식이 id * id
    t = [x for x in trees if x[2] == "+"][0]
    assert bracket(t) == "E[E[E[id] * E[id]] + E[id]]"

    # 본문의 좌측 유도
    assert derivation(t, True) == ["E", "E + E", "E * E + E", "id * E + E", "id * id + E", "id * id + id"]
    # 본문의 우측 유도
    assert derivation(t, False) == ["E", "E + E", "E + id", "E * E + id", "E * id + id", "id * id + id"]
    # 두 유도는 같은 트리에서 나왔고, 걸음 수는 비단말 노드 수(5)와 같다
    assert len(derivation(t, True)) - 1 == len(derivation(t, False)) - 1 == 5

    # 잎을 왼쪽부터 읽으면 원래 문자열이다
    for x in trees:
        assert leaves(x) == s

    # E ⇒ (E) ⇒ (E + E) ⇒ (id + E) ⇒ (id + id)
    t2 = parse_trees(g, "E", "( id + id )".split())
    assert len(t2) == 1
    assert derivation(t2[0], True) == ["E", "( E )", "( E + E )", "( id + E )", "( id + id )"]

    # 파스 트리 하나에 좌측 유도 하나: 트리 수와 좌측 유도 수가 같다
    for text in ["id", "id + id", "id * id + id", "id + id + id + id", "( id + id ) * id"]:
        toks = text.split()
        ts = parse_trees(g, "E", toks)
        assert len(ts) == count_leftmost_derivations(g, "E", toks)
        assert len({tuple(derivation(x, True)) for x in ts}) == len(ts)
    assert len(trees) == 2 and len(parse_trees(g, "E", "id + id + id + id".split())) == 5

    # 문장 형식과 문장: E * E + E 는 문장 형식이지만 문장은 아니다(비단말이 남아 있다)
    forms = derivation(t, True)
    assert "E * E + E" in forms and all(tok in ("id", "+", "*") for tok in forms[-1].split())

    # 카드: id + id * id 를 뿌리가 + 인 트리로 읽을 때의 우측 유도
    t3 = [x for x in parse_trees(g, "E", "id + id * id".split()) if x[2] == "+"][0]
    assert derivation(t3, False) == ["E", "E + E", "E + E * E", "E + E * id", "E + id * id", "id + id * id"]
    assert derivation(t3, True) == ["E", "E + E", "id + E", "id + E * E", "id + id * E", "id + id * id"]

    print("ALL CHECKS PASSED")