LL(1) 파싱 표 문서의 구현 코드다. if __name__ == "__main__": 아래가 자체 테스트다.
"""LL(1) 파싱 표를 만들고 문서의 표를 재현한다. LL(1)이 아닌 문법에서 충돌이 나는 칸도 확인한다.
실행: python3 20_ll1-parsing-table_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()}
EPS = "ε"
END = "$"
def first_of(seq, first, g):
"""기호열 seq의 FIRST. first는 비단말별 FIRST 집합."""
out = set()
for s in seq:
if s not in g:
out.add(s)
return out
out |= first[s] - {EPS}
if EPS not in first[s]:
return out
out.add(EPS)
return out
def first_sets(g):
"""더 늘어나지 않을 때까지 규칙을 되풀이해 모든 비단말의 FIRST를 구한다."""
first = {A: set() for A in g}
changed = True
while changed:
changed = False
for A, prods in g.items():
for p in prods:
f = first_of(p, first, g)
if not f <= first[A]:
first[A] |= f
changed = True
return first
def follow_sets(g, start, first):
"""더 늘어나지 않을 때까지 세 규칙을 되풀이해 모든 비단말의 FOLLOW를 구한다."""
follow = {A: set() for A in g}
follow[start].add(END)
changed = True
while changed:
changed = False
for B, prods in g.items():
for p in prods:
for i, A in enumerate(p):
if A not in g:
continue
fb = first_of(p[i + 1:], first, g)
add = fb - {EPS}
if EPS in fb:
add |= follow[B]
if not add <= follow[A]:
follow[A] |= add
changed = True
return follow
def ll1_table(g, start):
"""표[(비단말, 단말)] = 그 칸에 들어가는 생성 규칙들의 목록. 두 개 이상이면 충돌이다."""
first = first_sets(g)
follow = follow_sets(g, start, first)
table = {}
for A, prods in g.items():
for p in prods:
f = first_of(p, first, g)
cols = f - {EPS}
if EPS in f:
cols |= follow[A]
for x in cols:
table.setdefault((A, x), []).append(p)
return table
def conflicts(table):
return {k: v for k, v in table.items() if len(v) > 1}
def cells(table):
return {k: [" ".join(p) or EPS for p in v] for k, v in table.items()}
if __name__ == "__main__":
# 산술식 예: E → TE', E' → +E | ε, T → FT', T' → *T | ε, F → (E) | id
g = G("E -> T E'\nE' -> + E | ε\nT -> F T'\nT' -> * T | ε\nF -> ( E ) | id")
table = ll1_table(g, "E")
assert cells(table) == {
("E", "("): ["T E'"], ("E", "id"): ["T E'"],
("E'", "+"): ["+ E"], ("E'", ")"): [EPS], ("E'", END): [EPS],
("T", "("): ["F T'"], ("T", "id"): ["F T'"],
("T'", "+"): [EPS], ("T'", "*"): ["* T"], ("T'", ")"): [EPS], ("T'", END): [EPS],
("F", "("): ["( E )"], ("F", "id"): ["id"],
}
assert conflicts(table) == {}
# 30칸 가운데 13칸만 차 있다. 나머지 17칸은 문법 오류다.
assert len(table) == 13 and 5 * 6 - len(table) == 17
# FIRST만으로 채우는 칸은 8개, ε 규칙을 FOLLOW로 채우는 칸은 5개
first = first_sets(g)
by_first = sum(len(first_of(p, first, g) - {EPS}) for ps in g.values() for p in ps)
assert by_first == 8 and len(table) - by_first == 5
# S → aA | b | A, A → (S)
g2 = G("S -> a A | b | A\nA -> ( S )")
assert cells(ll1_table(g2, "S")) == {("S", "a"): ["a A"], ("S", "b"): ["b"], ("S", "("): ["A"],
("A", "("): ["( S )"]}
# 좌인수분해를 안 한 문법: 같은 칸에 규칙이 둘 들어간다
g3 = G("E -> T + E | T\nT -> F * T | F\nF -> ( E ) | id")
assert set(conflicts(ll1_table(g3, "E"))) == {("E", "("), ("E", "id"), ("T", "("), ("T", "id")}
# 좌재귀 문법: S → Sa | b
g4 = G("S -> S a | b")
assert cells(conflicts(ll1_table(g4, "S"))) == {("S", "b"): ["S a", "b"]}
# 모호한 문법(매달린 else)은 좌인수분해를 해도 충돌이 남는다
g5 = G("S -> i E t S S' | a\nS' -> e S | ε\nE -> b")
assert cells(conflicts(ll1_table(g5, "S"))) == {("S'", "e"): ["e S", EPS]}
# ε 규칙을 FIRST 칸에만 넣고 FOLLOW를 안 쓰면 E'와 T'의 ε 칸 5개가 비어 id $ 조차 못 읽는다
only_first = {}
for A, ps in g.items():
for p in ps:
for x in first_of(p, first, g) - {EPS}:
only_first[(A, x)] = [p]
assert ("T'", END) not in only_first and ("E'", END) not in only_first
# 카드: S → a S b | ε 의 표, S → a A a | ε, A → a b S | ε 의 충돌
assert cells(ll1_table(G("S -> a S b | ε"), "S")) == {("S", "a"): ["a S b"], ("S", "b"): [EPS], ("S", END): [EPS]}
bad = cells(conflicts(ll1_table(G("S -> a A a | ε\nA -> a b S | ε"), "S")))
assert bad == {("S", "a"): ["a A a", EPS], ("A", "a"): ["a b S", EPS]}
# FIRST끼리 겹치지 않아도 FIRST와 FOLLOW가 겹치면 충돌한다
assert cells(ll1_table(G("S -> A b | c\nA -> a A | ε"), "S")) == {
("S", "a"): ["A b"], ("S", "b"): ["A b"], ("S", "c"): ["c"], ("A", "a"): ["a A"], ("A", "b"): [EPS]}
print("ALL CHECKS PASSED")