재귀 하강 파싱 문서의 구현 코드다. if __name__ == "__main__": 아래가 자체 테스트다.

"""재귀 하강 파싱(되돌아가기 있음)을 구현하고 문서의 cad 추적과 id + id 연습을 재현한다.

실행: python3 17_recursive-descent-parsing_impl.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]


import sys


def rd_parse(g, start, tokens):
    """되돌아가기가 있는 재귀 하강 파싱. (받아들였는가, 기록)을 돌려준다.
    기록의 한 줄: ("try", 비단말, 규칙, 위치) / ("match", 단말, 위치) / ("mismatch", 단말, 입력, 위치)"""
    tokens = list(tokens)
    log = []

    def sym(s, pos):
        if s not in g:
            if pos < len(tokens) and tokens[pos] == s:
                log.append(("match", s, pos))
                yield pos + 1
            else:
                log.append(("mismatch", s, tokens[pos] if pos < len(tokens) else "끝", pos))
            return
        for p in g[s]:                      # 규칙을 적힌 순서대로 시험한다
            log.append(("try", s, " ".join(p) or "ε", pos))
            yield from seq(p, pos)          # 실패하면 여기로 돌아와 다음 규칙으로 간다

    def seq(p, pos):
        if not p:
            yield pos
            return
        for mid in sym(p[0], pos):
            yield from seq(p[1:], mid)

    for end in sym(start, 0):
        if end == len(tokens):
            return True, log
        log.append(("mismatch", "끝", tokens[end], end))
    return False, log


def tries(log):
    return [(e[1], e[2]) for e in log if e[0] == "try"]


if __name__ == "__main__":
    # 문서의 추적: S → dAc | cAe | cAd, A → a, 입력 cad
    g = G("S -> d A c | c A e | c A d\nA -> a")
    ok, log = rd_parse(g, "S", "c a d".split())
    assert ok
    assert log == [
        ("try", "S", "d A c", 0), ("mismatch", "d", "c", 0),          # 걸음 1
        ("try", "S", "c A e", 0), ("match", "c", 0),                  # 걸음 2
        ("try", "A", "a", 1), ("match", "a", 1),                      # 걸음 3
        ("mismatch", "e", "d", 2),                                    # 걸음 4
        ("try", "S", "c A d", 0), ("match", "c", 0),                  # 걸음 5
        ("try", "A", "a", 1), ("match", "a", 1), ("match", "d", 2),   # 걸음 6~7
    ]
    assert rd_parse(g, "S", "c a".split())[0] is False
    assert rd_parse(g, "S", "c a d d".split())[0] is False

    # 연습: E → T + E | T, T → F * T | F, F → (E) | id, 입력 id + id
    e = G("E -> T + E | T\nT -> F * T | F\nF -> ( E ) | id")
    ok, log = rd_parse(e, "E", "id + id".split())
    assert ok
    assert tries(log) == [
        ("E", "T + E"), ("T", "F * T"), ("F", "( E )"), ("F", "id"),
        ("T", "F"), ("F", "( E )"), ("F", "id"),
        ("E", "T + E"), ("T", "F * T"), ("F", "( E )"), ("F", "id"),
        ("T", "F"), ("F", "( E )"), ("F", "id"),
        ("E", "T"), ("T", "F * T"), ("F", "( E )"), ("F", "id"),
        ("T", "F"), ("F", "( E )"), ("F", "id"),
    ]
    # 같은 id를 여러 번 다시 읽는다: 첫 id 2번, 둘째 id 4번
    matches = [x[2] for x in log if x[0] == "match" and x[1] == "id"]
    assert matches.count(0) == 2 and matches.count(2) == 4

    # 길이 6 이하의 모든 입력에서 문법의 언어와 같은 답을 낸다
    from itertools import product
    L = language(e, "E", 6)
    for n in range(7):
        for s in product(["+", "*", "(", ")", "id"], repeat=n):
            assert rd_parse(e, "E", s)[0] == (s in L)

    # 되돌아가기의 값: 괄호를 d겹 두른 ((…id…))에서 시험한 규칙 수가 한 겹마다 4배 넘게 는다
    nest = lambda d: ["("] * d + ["id"] + [")"] * d
    counts = [len(tries(rd_parse(e, "E", nest(d))[1])) for d in range(7)]
    assert counts == [14, 69, 292, 1187, 4770, 19105, 76448]
    assert all(counts[i + 1] > 4 * counts[i] for i in range(6))

    # 좌인수분해한 문법에서는 같은 입력에서 한 겹마다 7번씩만 는다
    f = G("E -> T E'\nE' -> + E | ε\nT -> F T'\nT' -> * T | ε\nF -> ( E ) | id")
    counts2 = [len(tries(rd_parse(f, "E", nest(d))[1])) for d in range(7)]
    assert counts2 == [7 * d + 8 for d in range(7)]
    for n in range(6):
        for s in product(["+", "*", "(", ")", "id"], repeat=n):
            assert rd_parse(f, "E", s)[0] == (s in L)

    # 좌재귀 문법에서는 입력을 읽지 못한 채 자기 자신을 끝없이 부른다
    sys.setrecursionlimit(500)
    try:
        rd_parse(G("S -> S a | b"), "S", "b a".split())
        raise AssertionError("끝났다")
    except RecursionError:
        pass

    # 카드 C1: S → aBc | aBd, B → b, 입력 abd
    ok, log = rd_parse(G("S -> a B c | a B d\nB -> b"), "S", "a b d".split())
    assert ok and tries(log) == [("S", "a B c"), ("B", "b"), ("S", "a B d"), ("B", "b")]

    print("ALL CHECKS PASSED")