문법 고치기 예제 사다리 문서의 문제 4 풀이 코드다. if __name__ == "__main__": 아래가 자체 테스트다.
"""문법 고치기 예제 사다리의 답을 확인한다: 고친 문법이 원래 문법과 같은 언어를 만들고 LL(1) 표에 충돌이 없는지.
실행: python3 15_grammar-rewriting-ladder_p4.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]
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 same(g1, s1, g2, s2, n):
return language(g1, s1, n) == language(g2, s2, n)
def is_ll1(g, start):
return not conflicts(ll1_table(g, start))
if __name__ == "__main__":
# 문제 1: DECL → DECL type id ; | DECL type id = id ; | ε
o1 = G("DECL -> DECL type id ; | DECL type id = id ; | ε")
a1 = G("DECL -> A\nA -> type id ; A | type id = id ; A | ε") # 좌재귀를 없앤 직후
b1 = G("DECL -> type id ; DECL | type id = id ; DECL | ε") # 줄인 꼴
c1 = G("DECL -> type id DECL' | ε\nDECL' -> ; DECL | = id ; DECL") # 좌인수분해
assert same(o1, "DECL", a1, "DECL", 12) and same(o1, "DECL", b1, "DECL", 12) and same(o1, "DECL", c1, "DECL", 12)
assert not is_ll1(o1, "DECL") and not is_ll1(b1, "DECL") and is_ll1(c1, "DECL")
assert ("type", "id", ";", "type", "id", "=", "id", ";") in language(c1, "DECL", 8)
# 문제 2: E → E + T | T, T → id | id ( )
o2 = G("E -> E + T | T\nT -> id | id ( )")
a2 = G("E -> T E'\nE' -> + T E' | ε\nT -> id | id ( )")
b2 = G("E -> T E'\nE' -> + T E' | ε\nT -> id T'\nT' -> ( ) | ε")
assert same(o2, "E", a2, "E", 9) and same(o2, "E", b2, "E", 9)
assert not is_ll1(o2, "E") and not is_ll1(a2, "E") and is_ll1(b2, "E")
# 문제 3: S → S ; a | a b | a c
o3 = G("S -> S ; a | a b | a c")
a3 = G("S -> a b S' | a c S'\nS' -> ; a S' | ε")
b3 = G("S -> a S''\nS'' -> b S' | c S'\nS' -> ; a S' | ε")
assert same(o3, "S", a3, "S", 10) and same(o3, "S", b3, "S", 10)
assert not is_ll1(a3, "S") and is_ll1(b3, "S")
# 문제 4: B → B or C | C, C → C and D | D, D → not D | id | id ( )
o4 = G("B -> B or C | C\nC -> C and D | D\nD -> not D | id | id ( )")
b4 = G("""B -> C B'
B' -> or C B' | ε
C -> D C'
C' -> and D C' | ε
D -> not D | id D'
D' -> ( ) | ε""")
assert same(o4, "B", b4, "B", 8)
assert not is_ll1(o4, "B") and is_ll1(b4, "B")
# 순서를 바꿔 좌인수분해만 먼저 하면 좌재귀가 남아 여전히 LL(1)이 아니다
half = G("B -> B or C | C\nC -> C and D | D\nD -> not D | id D'\nD' -> ( ) | ε")
assert not is_ll1(half, "B")
print("ALL CHECKS PASSED")