一個編譯器最簡前端的python實現

一個編譯器的前端通常包括詞法分析器語法分析器。在分析過程中,文本輸入詞法分析器,根據詞法規則解析出詞法單元。詞法單元作為輸入,進入到語法分析器。語法分析器根據語法規則對詞法單元序列進行分析(自頂而下,自底向上),確認詞法單元序列符合語言的語法規則。在分析的同時形成翻譯方案,由源語言(輸入文本的語言)轉換成目標語言(通常是一種中間語言)的過程。

這篇文章是《編譯原理》(龍書)第二版第五章練習5.5的一個作業。

將 if (C) S1 else S2 實現為一個語法分析器。

這個作業基本涵蓋了編譯器前端的最主要的技術,又很好的簡化了詞法和語法種類帶來的復雜性。因此值得作為對編譯技術的研究案例。

本文逐步回顧編譯器前端的關鍵技術,并在過程中用python實現。這個句型是C語言的,翻譯的目標語言是一種接近匯編語言模式的偽代碼。

詞法分析器

詞法分析器的主要作用是從文本中分析并提取語素(lexeme)并生成詞法單元(token)。提供給語法分析器作為語法分析的基本單位。

詞法分析器一般以正則模式作為區分,來識別不同的詞法單元。

需要識別的詞法

在這里,詞法分析器需要識別的模式有:

  • 空白字符
    包括空格,制表符\t, 和換行\n。將文本分隔為不同的單元,它們在詞法分析過程的第一步被剔除,不進入語法分析,我們在這里以空格為基本模式。
  • 分隔符
    分隔符一般指語法中有語法作用的標點符號,它本身是一個詞法單元,它還將連在一起的字符分開成不同的詞法單元。這里指( )
  • 關鍵字
    if 和 else

在這個語句中,C,S1 和 S2實際上是一個文法單元的非終結符號(None terminal),分別是Condition(條件表達式)和Statement(語句)。
關于表達式的定義不在此贅述。一般可以求值的,或者說可以賦值給一個變量的就作為一個表達式(x = expr)。語句主要產生副作用。為求簡便,我們在這里先將其實現為一個詞法值,當詞法器識別 'C' ,就認為它是一個語法單元的條件表達式。'S1', 'S2' 分別作為兩個不同的語句。我們將在實現的過程中根據需要來決定是否擴展它。在編譯過程中,C,S1,S2作為語法單元,實際上是由語法分析器在語法分析的過程中展開或者規約得到的語法樹的節點,正常詞法分析器輸出的全部是終結符號,在這里暫時替代一下,這一點要注意不要混淆。

因此, 我們需要識別的詞法有:

  1. 空格
  2. 分隔符( ),返回一個delimiter類型的詞法單元,值為它的字符。
  3. 關鍵字 if 和 else,分別返回一個keyword類型的詞法單元,值為字符串本身。
  4. 暫時替換的語法符號(非終結符)C,S1,S2。返回一個非終結符,值為它的字符,我們會附加一些屬性給這個符號。

詞法分析器的作用

詞法分析器的重要作用是識別正則模式,一般命名變量,或者識別數字,或者字符串時,都通過正則引擎來識別。在這里我們需要識別的模式比較簡單,全部是確定字符。
正則引擎的實現原理是有限自動機(finite-automata),有兩種類型,NFA和DFA,非確定有限自動機確定型有限自動機。可以根據算法機械構造NFA,然后再根據算法機械轉換為DFA。
https://github.com/dannyvi/simple-regex 實現了一個簡單的NFA的正則引擎。我們將用這個引擎來識別詞法單元。它可以用正則模式生成一個有限自動機,并識別相應的字符串是否匹配這個模式。
python的re模塊文檔中給出了一個詞法器更完整的例子。Writing a Tokenizer

下面是詞法器代碼:

from regex import regex_compile

space = regex_compile(r" ")
delimiter = regex_compile(r"\(|\)")
keyword = regex_compile(r"(if)|(else)")
node = regex_compile(r"(S1)|(S2)|C")

class Token:
    def __init__(self, typ, value):
        self.typ = typ
        self.value = value

    def __repr__(self):
        return '<token: {}.{} >'.format(self.typ, self.value)

def tokenizer(input_stream):
    def split(input_stream):
        symbol = ''
        for i in input_stream:
            if space.match(i):
                if symbol:
                    yield symbol
                    symbol = ''
            elif delimiter.match(i):
                if symbol:
                    yield symbol
                    symbol = ''
                yield i
            else:
                symbol += i
        if symbol:
            yield symbol

    def token(value):
        if delimiter.match(value): return Token(value, value)
        elif keyword.match(value): return Token(value, value)
        elif node.match(value): return Token(value, value)
        else: raise RuntimeError('"{}" unexpected symbol'.format(value))

    l = split(input_stream)
    return map(token, l)

這個詞法器有兩個pass,首先將代碼用空白符和分隔符分開成為一個列表,然后對這個列表分析返回一個Token的map迭代對象。

from tokenizer import tokenizer
s = tokenizer("if ( C ) S1  else   S2 ")
list(s)

Out[4]: 
[<token: if.if >,
 <token: (.( >,
 <token: C.C >,
 <token: ).) >,
 <token: S1.S1 >,
 <token: else.else >,
 <token: S2.S2 >]

語法分析和翻譯

一個語法分析器主要的作用是按照給定的文法將詞法單元序列規約為語法樹,并將這個語法樹翻譯為目標語言。一般在規約時就可以展開翻譯,所以語法分析和翻譯同時進行。

標準 LR(1)分析器 (Canonical LR(1))

其他的分析器還有LL(1),SLR(LR 0),LALR(LR 1)。不過我們在這里就實現 Canonical LR(1)。

LR分析器是通過移入規約技術來實現分析過程的,所以分成兩個階段,首先構建語法分析表,然后按分析表進行規約和翻譯。

實際上語法分析相當于構建一個有限狀態機,由開始符號進入,并對不同的輸入進行狀態轉化,當輸入完成后,如果狀態處于接受狀態,就表示分析成功了。

語法分析表

通過算法將上下文無關文法G轉化為語法分析動作。我們可以先將其轉化為一個分析表,然后再對照分析表,來編寫分析和翻譯動作。

分析表形狀大概如下:

ACTION

狀態\輸入 a b c
0 s2 e1 s1
1 r2 r1 acc
2 e3 s1 s2

GOTO

狀態\非終結符 S N
0 1 2
1 0 1
2 1 0

ACTION 負責對輸入的終結符號選擇合適的動作(移入,規約,報錯,接受), GOTO負責對規約后的產生式進行狀態轉化。
語法分析在一個分析狀態棧(或者分析狀態序列)上操作。如果文本屬于這個語法的話,移入的狀態最終會被規約為開始符號。

語法分析表G算法如下:
輸入: 文法G
輸出:分析表
方法:

  1. 構造G的LR(1)項集族C={I0, I1,...In}。

  2. 語法分析器的狀態i由Ii構造得到,狀態i按照下面規則構建:

    • 如果[A -> α?aβ, b],并且GOTO(Ii, a) = ij, 那么將ACTION[i, a]設置為移入j("sj")。a必須是終結符。
    • 如果[A -> α?, a] 在 Ii中,且A不是開始符號,那么將ACTION[i, a]設置為規約 A -> α ( "rn",n是產生式的編號 )。
    • 如果[S' -> S, $] 在Ii中,那么將ACTION[i, $]設置為接受("acc")。
  3. 狀態i對于各個非終結符號A的goto按照下面規則構造:
    如果GOTO(Ii, A) = Ij, 那么 GOTO[i, A] = j

  4. 所有沒按照2, 3條規則定義的分析表條目都設置為錯誤。

  5. 語法分析表的初始狀態是由 [S' -> ?S, $] 的項集構造得到的狀態。

所以,項集族,狀態和輸入符號決定了這個表的構造。
輸入符號就是文法里面的終結符號(Terminal)和非終結符號(None Terminal)。
狀態就簡單理解為項集族里面的項集閉包(Item-sets Closure)的編號。

項集族(Item-sets Closure Collections)
項集族是由項集閉包組成的,它是構造出來的一個文法的狀態機的全部狀態。

項集閉包(Item-sets Closure)
一個項集閉包就是一個狀態,大概解釋就是在這個狀態上進行輸入轉換可能用到的全部產生式組成的

項(Item)
標準 LR(1)分析器的項是由產生式當前位置輸入字符構成的。

class Item(object):
    """The Canonical LR(1) Item definition.

    :param symbol: str, the left part of production.
    :param body: str, the right part of production.
    :param dot: int, current position in the item.
    :param follow: str, possible input for the current configuration.
    """

    def __init__(self, symbol, body, dot, follow):
        self.symbol = symbol
        self.body = body
        self.pos = dot
        self.follow = follow

    def __str__(self):
        p = list(self.body)
        p.insert(self.pos, '◆')
        pr = ' '.join(p)
        return "[{}]  {} -> {}".format( self.follow, self.symbol, pr)

    def __repr__(self):
        return "<Item:{} >\n".format(self.__str__())

    def __eq__(self, other):
        if isinstance(other, Item):
            return ((self.symbol == other.symbol) and
                    (self.body == other.body) and
                    (self.pos == other.pos) and
                    (self.follow == other.follow))
        else:
            return False

    def __ne__(self, other):
        return not self.__eq__(other)

    def __hash__(self):
        return hash(self.__str__())

實現了項,和相關的操作。因為它在閉包中作為集合元素,所以實現 __hash____eq____ne__ 方法,這樣集合就可以迭代。

from regex.parsing_table import Item
i1 = Item('S', 'b|c', 2, 'k')
i1
Out[4]: <Item: S -> b|.c      k >

項集閉包的實現

class定義:

class Closure(object):
    def __init__(self, sets: Set[Item], label: int = None):
        self.label = label
        self.sets = sets
        self.goto = dict()  # type: dict[str, int]

    def __len__(self):
        return len(self.sets)

    def __iter__(self):
        return self.sets.__iter__()

    def __str__(self):
        return "\n".join([i.__str__() for i in self.sets])

    def __repr__(self):
        return "<Closure>:{}\n{}\n</Closure>\n".format(self.label,
                                                       self.__str__())

    def __eq__(self, other):
        return self.sets == other.sets

    def __ne__(self, other):
        return not self.__eq__(other)

    def __hash__(self):
        return hash(self.__str__())

    def __contains__(self, item):
        return item in self.sets

sets包含所有的項,label是這個閉包的狀態名,這里規定為一個數字,goto是一個字典,用于記錄這個狀態在不同輸入的轉換目標狀態。
__contains__ 用于計算項(Item)是否包含在這個閉包中。
因為閉包還會包含在族(collection)中作為一個元素,所以也實現 __eq____hash__,這樣集合就可以作為一個迭代器 。

項集閉包的算法:
對于一個項集(Item-sets) I ,計算它的閉包 CLOSURE(I) 。

  1. I 中的各個項加入到閉包當中。
  2. 閉包中的每個項, 如 [A -> α?Bβ, a],對文法G中的每個非終結符B的產生式 B -> γ,對FIRST(βa)中的每個終結符 b, 將 [B -> ?γ, b] 加入閉包當中。

這里的?B是推理當前位置后面可能出現的規約(B的產生式),以及B規約結束之后,能夠在之后出現的終結符。

def get_closure(cl: Closure, label: int) -> Closure:
    """get all Item of a Closure from given Items, by adding implied Items.

    The implied Items are the productions of the None terminals after the
    current position, which put a dot on the head."""
    def get_nterm(item):
        pos, prod = (item.pos, item.body)
        if pos < len(prod):
            symbol = prod[pos]
            if isnterm(symbol):
                return symbol
        return None
    item_set = set()
    q = queue.Queue()
    for i in cl.sets:
        item_set.add(i)
        q.put(i)
    while not q.empty():
        item = q.get()
        symbol = get_nterm(item)
        if symbol:
            products = [i for i in grammar if i[0] == symbol]
            suffix = item.body[item.pos+1:] + item.follow
            termins = firsts(suffix)
            for product in products:
                for terminal in termins:
                    new_item = Item(symbol, product[1], 0, terminal)
                    if new_item not in item_set:
                        item_set.add(new_item)
                        q.put(new_item)
    c = Closure(item_set, label)
    return c

我們需要知道關于grammarfirsts的定義。

grammar = [ ("stmt", ("if", "(", "C", ")", "S", "else", "S")),
             ...
          ]

grammar 在這里定義為一個產生式列表。產生式是由一個頭部和體合成的元組,產生式的體本身也是一個元組。

firsts是為了計算一個產生式的體中可能出現的第一個終結符號,我們在規約時需要知道可能出現的下一個終結符號和應該使用哪一個式子來進行規約。這樣我們在推導的時候就能知道應該進入的狀態。

對于一個終結符號或者非終結符號, 它的first(X)算法如下:

  1. X是終結符號: first(X) = X
  2. X是非終結符號:查找產生式 X -> Y1Y2...Yk, 如果Y1是非終結符且含有空產生式,那么 first(X) = first(Y1) ∪ firsts(Y2...Yk), 否則就等于 first(Y1),意思就是空產生式會形成多種可能性,確定只有一個first的符號,后面的符號就排除掉了。如果Y是終結符號,就不用下推。如果Y是非終結符號,就考慮它是否產生空產生式,來決定考察下一個符號。
  3. X是EPSILON: firsts(X) += EPSILON

具體的實現是這樣的:

def isnterm(symbol):
    return symbol in n_terminals

def isterm(symbol):
    return symbol in terminals

def produce_epsilon(none_terminal):
    return 'EPSILON' in [i[1] for i in grammar if i[0] == none_terminal]

# def is_start_symbol(symbol):
#   return symbol == "startsup"

def first(symbol):
    """Return the first terminal sets that may occur in the Symbol."""
    first_sets = set()
    if isterm(symbol):
        return set(symbol)
    elif produce_epsilon(symbol):
        first_sets = first_sets.union('EPSILON')
    elif isnterm(symbol):
        for i in grammar:
            if i[0] == symbol:
                body = i[1]
                epsilons = True
                current = 0
                while epsilons is True and current < len(body):
                    if body[current] != symbol:
                        first_sets = first_sets.union(first(body[current]))
                    if not produce_epsilon(body[current]):
                        epsilons = False
                    current += 1
    return first_sets

def firsts(suffix):
    if len(suffix) == 1:
        return first(suffix[0])
    else:
        if not produce_epsilon(suffix[0]):
            return first(suffix[0])
        else:
            return first(suffix[0]).union(firsts(suffix[1:]))

isnterm, isterm 分別判斷是否終結符號。
produce_epsilon 產生式的特殊形式,判斷是否一個空產生式。這里約定空產生式 N -> EPSILON 的體由 EPSILON 定義。
is_start_symbol 判斷是否開始符號。
first 計算一個終結符號或者非終結符號可能出現的第一個終結符。
firsts 計算一個句型(多個符號連在一起)可能出現的第一個終結符。

為求簡單我們在這里將 grammar, terminals, n_terminals 定義為列表。以后再實現如何將文法規則解析為python對象,和判斷是否終結符號的算法。

grammar = [("startsup", ("start")), 
           ("start", ("stmt")),
           ("stmt", ("if", "(", "C", ")", "S1", "else", "S2")),
          ]
terminals = ("if", "(", "C", ")", "S", "else", "S2", "$")
n_terminals = ("startsup", "start", "stmt")

實際上 C, S1, S2 都應該是非終結符號。未來會替換為產生式,由編譯器規約它們的體來得到節點。

這個文法是對原文法進行增廣后的增廣文法,由原來的產生式

  • start -> stmt
  • stmt -> if ( c ) S1 else S2

增加一個開始符號start'和在尾部增加一個結束輸入$ , 對原來的詞法序列在尾部增加一個終結字符 $, 這樣當掃描到最后一個字符為 $, 就進入接受狀態。

  • startsup -> start

這樣我們就實現了項集閉包的算法。

推導項集族

項集族C的算法:

  1. 在C中,初始添加 [startsup -> ◆ start, $] 的項 【由Item('startsup', ('start',), 0, '$')構造】,并計算閉包【get_closure(item)】為開始狀態。
  2. 在C中,對于每一個項集閉包I, 對于每一個文法符號X,可以是終結符號或者非終結符號,如果 goto(I, X) 非空且不在C中, 就加入到C中。

通過將每一個輸入可能得到的狀態加入到項集族,來完成構建。
這里,goto(I,X)指的是狀態 I 在輸入X下轉換到的狀態。(一個項集閉包)它的算法:

I中所有存在的項 [A -> α?Xβ, a], 將 [A -> αX?β, a]加入到集合,并計算它的閉包。

實現如下:

def goto(clos: Closure, letter: str) -> Closure:
    """a closure that could get from the current closure by input a letter.

    :param clos: the current closure.
    :param letter: the input letter.
    :return: Closure.
    """
    item_set = set()
    for item in clos.sets:
        dot, prod = (item.pos, item.body)
        if dot < len(prod) and prod[dot] == letter:
            new_item = Item(item.symbol,
                            item.body,
                            item.pos + 1,
                            item.follow)
            item_set.add(new_item)
    c = Closure(item_set)
    return get_closure(c, label=None)


def closure_groups():
    def find_label(closure, group):
        for i in group:
            if closure == i:
                return i.label
        return None
    group = set()
    all_symbols = terminals + n_terminals
    label = 0
    start_item = Item('startsup', 'start', 0, '$')
    start = get_closure(Closure({start_item}), label)
    q = queue.Queue()
    q.put(start)
    group.add(start)
    while not q.empty():
        c = q.get()
        for literal in all_symbols: # terminals + n_terminals:
            go_clos = goto(c, literal)
            if go_clos:
                if go_clos not in group:
                    label += 1
                    go_clos.label = label
                    group.add(go_clos)
                    q.put(go_clos)
                    c.goto[literal] = label
                else:
                    go_label = find_label(go_clos, group)
                    if go_label:
                        c.goto[literal] = go_label
    return group

得到了整個文法的項集族之后,我們就可以根據之前給出的構建出語法分析表了。這里將終結符號的ACTION和非終結符號的GOTO拼接在一起。并且沒有???♂?錯誤處理,這里默認將錯誤動作全部初始化為'.'。

def get_states_map(closure_group):
    def get_state_map(closure):
        """ table row like all_symbols list state maps."""
        all_symbols = terminals + n_terminals
        row = ["." for i in all_symbols]
        # None terminals GOTO action and Terminals shift action.
        for input, goto_label in closure.goto.items():
            row_pos = all_symbols.index(input)
            for item in closure:
                if item.pos < len(item.body):      # shape like [A -> ?.aβ b]
                    if item.body[item.pos] == input:
                        # None terminals GOTO state
                        if input in n_terminals:
                            row[row_pos] = str(goto_label)
                        # Terminals action shift state
                        elif input in terminals:
                            row[row_pos] = "s" + str(goto_label)
        # Terminals reduce action. shape like  [A -> ?.  a]
        for row_pos, input in enumerate(all_symbols):
            for item in closure:
                if item.pos == len(item.body) and \
                        item.follow == input and \
                        item.symbol != 'startsup':
                        # 'R' should be replaced with start_symbol
                    #if item.follow != '*':
                    production_num = grammar.index([item.symbol, item.body])
                    row[row_pos] = 'r' + str(production_num)
                    #else:
                    #    pass
        # accept condition 'startsup -> start. , $'
        acc_item = Item('startsup', 'start', 1, '$')
        if acc_item in closure:
            input = '$'
            row_pos = all_symbols.index('$')
            row[row_pos] = '$'
        return row

    state_map = [None for i in range(len(closure_group))]
    for closure in closure_group:
        row = get_state_map(closure)
        state_map[closure.label] = row
    return state_map


def generate_syntax_table():
    g = closure_groups()
    state_map = get_states_map(g)
    return state_map

這樣就得到了語法分析表。

全部代碼在這里:

import queue
from typing import Set

grammar = [("startsup", ("start", )),
           ("start", ("stmt", )),
           ("stmt", ("if", "(", "C", ")", "S1", "else", "S2")),
          ]
terminals = ("if", "(", "C", ")", "S1", "else", "S2", '$')
n_terminals = ("startsup", "start", "stmt")
all_symbols = terminals + n_terminals


class Item(object):
    """The Canonical LR(1) Item definition.

    :param symbol: str, the left part of production.
    :param body: str, the right part of production.
    :param dot: int, current position in the item.
    :param follow: str, possible input for the current configuration.
    """

    def __init__(self, symbol, body, dot, follow):
        self.symbol = symbol
        self.body = body
        self.pos = dot
        self.follow = follow

    def __str__(self):
        p = list(self.body)
        p.insert(self.pos, '◆')
        pr = ' '.join(p)
        return "[{}]  {} -> {}".format( self.follow, self.symbol, pr)

    def __repr__(self):
        return "<Item:{} >\n".format(self.__str__())

    def __eq__(self, other):
        if isinstance(other, Item):
            return ((self.symbol == other.symbol) and
                    (self.body == other.body) and
                    (self.pos == other.pos) and
                    (self.follow == other.follow))
        else:
            return False

    def __ne__(self, other):
        return not self.__eq__(other)

    def __hash__(self):
        return hash(self.__str__())


class Closure(object):
    def __init__(self, sets: Set[Item], label: int = None):
        self.label = label
        self.sets = sets
        self.goto = dict()  # type: dict[str, int]

    def __len__(self):
        return len(self.sets)

    def __iter__(self):
        return self.sets.__iter__()

    def __str__(self):
        return "\n".join([i.__str__() for i in self.sets])

    def __repr__(self):
        return "<Closure>:{}\n{}\n</Closure>\n".format(self.label,
                                                       self.__str__())

    def __eq__(self, other):
        return self.sets == other.sets

    def __ne__(self, other):
        return not self.__eq__(other)

    def __hash__(self):
        return hash(self.__str__())

    def __contains__(self, item):
        return item in self.sets


def isnterm(symbol):
    return symbol in n_terminals


def isterm(symbol):
    return symbol in terminals


def produce_epsilon(none_terminal):
    return 'EPSILON' in [i[1] for i in grammar if i[0] == none_terminal]


def first(symbol):
    """Return the first terminal sets that may occur in the Symbol."""
    first_sets = set()
    if isterm(symbol):
        return set(symbol)
    elif produce_epsilon(symbol):
        first_sets = first_sets.union('EPSILON')
    elif isnterm(symbol):
        for i in grammar:
            if i[0] == symbol:
                body = i[1]
                epsilons = True
                current = 0
                while epsilons is True and current < len(body):
                    if body[current] != symbol:
                        first_sets = first_sets.union(first(body[current]))
                    if not produce_epsilon(body[current]):
                        epsilons = False
                    current += 1
    return first_sets


def firsts(suffix):
    if len(suffix) == 1:
        return first(suffix[0])
    else:
        if not produce_epsilon(suffix[0]):
            return first(suffix[0])
        else:
            return first(suffix[0]).union(firsts(suffix[1:]))


def get_closure(cl: Closure, label: int) -> Closure:
    """get all Item of a Closure from given Items, by adding implied Items.

    The implied Items are the productions of the None terminals after the
    current position, which put a dot on the head."""
    def get_nterm(item):
        pos, prod = (item.pos, item.body)
        if pos < len(prod):
            symbol = prod[pos]
            if isnterm(symbol):
                return symbol
        return None
    item_set = set()
    q = queue.Queue()
    for i in cl.sets:
        item_set.add(i)
        q.put(i)
    while not q.empty():
        item = q.get()
        symbol = get_nterm(item)
        if symbol:
            products = [i for i in grammar if i[0] == symbol]
            suffix = item.body[item.pos+1:] + tuple(item.follow)
            termins = firsts(suffix)
            for product in products:
                for terminal in termins:
                    new_item = Item(symbol, product[1], 0, terminal)
                    if new_item not in item_set:
                        item_set.add(new_item)
                        q.put(new_item)
    c = Closure(item_set, label)
    return c


def goto(clos: Closure, letter: str) -> Closure:
    """a closure that could get from the current closure by input a letter.

    :param clos: the current closure.
    :param letter: the input letter.
    :return: Closure.
    """
    item_set = set()
    for item in clos.sets:
        dot, prod = (item.pos, item.body)
        if dot < len(prod) and prod[dot] == letter:
            new_item = Item(item.symbol,
                            item.body,
                            item.pos + 1,
                            item.follow)
            item_set.add(new_item)
    c = Closure(item_set)
    return get_closure(c, label=None)


def closure_groups():
    def find_label(closure, group):
        for i in group:
            if closure == i:
                return i.label
        return None
    group = set()
    label = 0
    start_item = Item('startsup', ('start',), 0, '$')
    start = get_closure(Closure({start_item}), label)
    q = queue.Queue()
    q.put(start)
    group.add(start)
    while not q.empty():
        c = q.get()
        for literal in all_symbols: # terminals + n_terminals:
            go_clos = goto(c, literal)
            if go_clos:
                if go_clos not in group:
                    label += 1
                    go_clos.label = label
                    q.put(go_clos)
                    group.add(go_clos)
                    c.goto[literal] = label
                    # print('add closure', go_clos)
                else:
                    go_label = find_label(go_clos, group)
                    if go_label:
                        c.goto[literal] = go_label
    return group


def get_states_map(closure_group):
    def get_state_map(closure):
        """ table row like all_symbols list state maps."""
        row = ["." for i in all_symbols]
        # None terminals GOTO action and Terminals shift action.
        for input, goto_label in closure.goto.items():
            row_pos = all_symbols.index(input)
            for item in closure:
                if item.pos < len(item.body):      # shape like [A -> ?.aβ b]
                    if item.body[item.pos] == input:
                        # None terminals GOTO state
                        if input in n_terminals:
                            row[row_pos] = str(goto_label)
                        # Terminals action shift state
                        elif input in terminals:
                            row[row_pos] = "s" + str(goto_label)
        # Terminals reduce action. shape like  [A -> ?.  a]
        for row_pos, input in enumerate(all_symbols):
            for item in closure:
                if item.pos == len(item.body) and \
                        item.follow == input and \
                        item.symbol != 'startsup':
                        # 'R' should be replaced with start_symbol
                    #if item.follow != '*':
                    production_num = grammar.index((item.symbol, item.body))
                    row[row_pos] = 'r' + str(production_num)
                    #else:
                    #    pass
        # accept condition 'startsup -> start. , $'
        acc_item = Item('startsup', ('start',), 1, '$')
        if acc_item in closure:
            input = '$'
            row_pos = all_symbols.index('$')
            row[row_pos] = '$'
        return row

    state_map = [None for i in range(len(closure_group))]
    for closure in closure_group:
        row = get_state_map(closure)
        state_map[closure.label] = row
    return state_map


def generate_syntax_table():
    g = closure_groups()
    state_map = get_states_map(g)
    return state_map

看下結果:

from parser import *
n = generate_syntax_table()
n 
state     if   (    C    )    S1   else  S2   $    startsup  start  stmt  
0         s1   .    .    .    .    .     .    .    .         2      3     
1         .    s4   .    .    .    .     .    .    .         .      .     
2         .    .    .    .    .    .     .    $    .         .      .     
3         .    .    .    .    .    .     .    r1   .         .      .     
4         .    .    s5   .    .    .     .    .    .         .      .     
5         .    .    .    s6   .    .     .    .    .         .      .     
6         .    .    .    .    s7   .     .    .    .         .      .     
7         .    .    .    .    .    s8    .    .    .         .      .     
8         .    .    .    .    .    .     s9   .    .         .      .     
9         .    .    .    .    .    .     .    r2   .         .      .     

語法分析和翻譯

語法分析

語法分析器在一個狀態棧上工作,這個棧存儲了移入的狀態,它代表了已經輸入,尚未規約的詞法單元。語法分析器對token_stream(經過詞法器解析后的代碼)的詞法單元逐個進行4種操作。分析器在分析開始前移入狀態0。分析器以狀態棧上的最后一個狀態(棧頂)為當前狀態,并且根據輸入字符查分析表,來獲得當前操作。

四種分析操作:

移入,將目標狀態移入到狀態棧頂。進入下一個詞法單元。
規約,規約目標產生式,當前詞法單元不變,繼續查表進行下一個操作,直到當前詞法單狀態元被移入。
接受,在含有增廣文法開始符號產生式的項 [startsup -> start◆, '$'],如果當前輸入為 '$', 分析成功進入接受狀態,并結束。
錯誤, 目前我們忽略錯誤處理。

代碼如下:

class SDT:
    def __init__(self):
        self.syntax_table = generate_syntax_table()
        self.state_stack = [0]
        self.accept = False

    def get_action(self, state, literal):
        return self.syntax_table[state][all_symbols.index(literal)]

    def ahead(self, token):
        action = self.get_action(self.state_stack[-1], token.typ)
        # shift action push a current state into state_stack
        if action[0] == 's':
            current_state = int(action[1:])
            self.state_stack.append(current_state)
        elif action[0] == '$':
            self.accept = True   # success
        # reduce action reduct a production and push
        elif action[0] == 'r':
            # get the production in grammar
            number = int(action[1:])
            production = grammar[number]
            head, body = production
            # pop the states of production body
            for _ in body:
                self.state_stack.pop()
            # push the state of head GOTO(I,X)
            state = self.get_action(self.state_stack[-1], head)
            self.state_stack.append(int(state))

            # reduce actions does not consume a token,
            # only when shifting, a token was consume and passed
            self.ahead(token)
        else:
            raise SyntaxError(f"Not a correct token '{token.__str__()}'.")

    def parse(self, token_stream):
        while True:
            try:
                token = next(token_stream)
                self.ahead(token)
            except StopIteration:
                # patch "$" in the end of token stream
                # to match the augmented grammar
                self.ahead(Token("$", "$"))
                break

它接受一個詞法單元流,并且分析,如果分析成功,accept就設置為True

from tokenizer import tokenizer
token_stream = tokenizer("if (C) S1 else S2")
sdt = SDT()
sdt.parse(token_stream)
sdt.accept
Out[8]: True

翻譯方案

翻譯方案一般插入到分析過程當中。

每個非終結符號都會形成一個函數,我們這里暫時在代碼中預定義好非終結符號的翻譯函數。

因為LR分析器是從右到左規約,而在移入的時候并不判斷目前在哪個產生式的內部,因此翻譯方案用后綴翻譯來實現,就是在規約的時候翻譯。產生式頭部的名稱作為函數名,規約的內容作為參數來進行調用,向上返回函數的結果。

建立一個參數棧:

        self.arg_stack = []

token在移入的時候作為值移入到棧中。

        self.push_arg(token)

規約時,將值移出,作為規約函數的參數。返回的結果,就是非終結符號的值,移入到棧中。

        # translations
        args = []
        for _ in body:
            arg = self.arg_stack.pop()
            args.insert(0, arg)
        translation = globals().get(head).__call__(*args)
        self.arg_stack.append(translation)

然而后綴翻譯方案只適用于綜合屬性(S屬性),對于繼承屬性并不適用。比如 stmt -> if (C) S1 else S2 大致會形成如下翻譯方案:

    C.code
    S1.scode
    goto stmt.next
    label L1
    S2.code

其中,stmt.next 由外部傳入,是stmt作為產生式的體時的繼承屬性,LL分析器通過預測分析表已經獲取了頭部,所以可以預先分配一個值。這里由于分析器是規約方式的,因此尚不知道繼承屬性的值。一般采取用一個空產生式來替代翻譯內容并先生成繼承屬性的方法來解決,不過會帶來語法分析時的復雜性。

我們在這里采用延遲調用的方法,就是 stmt 規約完成后并不直接返回翻譯的字符串值(因為還有一些屬性不知道), 而是返回一個函數,通過將未知的內容包裝成參數向上返回,在進行規約 start -> stmt 時, 再將start 生成的必要值作為參數來調用 stmt 規約的返回值,就可以獲得正確的翻譯方案了。

def stmt(IF, LPAR, c, RPAR, s1, ELSE, s2):
    def call(next_label):
        L1 = get_label()
        C_code = c.code(f_cond=L1)
        S1_code = s1.code()
        S2_code = s2.code()
        inter_code = """
        {} 
        {}
        goto {}
        label {}
        {}""".format(C_code, S1_code, next_label, L1, S2_code)
        return inter_code
    return call

添加對結束狀態的處理,和一些其他必要動作。這樣,分析和翻譯方案就變成了:


class SDT:
    def __init__(self):
        self.syntax_table = generate_syntax_table()
        self.state_stack = [0]
        self.arg_stack = []
        self.accept = False
        self.translation = ''

    def get_action(self, state, literal):
        return self.syntax_table[state][all_symbols.index(literal)]

    def ahead(self, token):
        action = self.get_action(self.state_stack[-1], token.typ)
        # shift action push a current state into state_stack
        if action[0] == 's':
            current_state = int(action[1:])
            self.state_stack.append(current_state)
            self.push_arg(token)
        elif action[0] == '$':
            self.translation = startsup(self.arg_stack[-1])
            self.accept = True   # success
            print('SUCCESS')
            print(self.translation)
        # reduce action reduct a production and push
        elif action[0] == 'r':
            # get the production in grammar
            number = int(action[1:])
            production = grammar[number]
            head, body = production
            # pop the states of production body
            for _ in body:
                self.state_stack.pop()
            # push the state of head GOTO(I,X)
            state = self.get_action(self.state_stack[-1], head)
            self.state_stack.append(int(state))

            # translations
            args = []
            for _ in body:
                arg = self.arg_stack.pop()
                args.insert(0, arg)
            translation = globals().get(head).__call__(*args)
            self.arg_stack.append(translation)

            # reduce actions does not consume a token,
            # only when shifting, a token was consume and passed
            self.ahead(token)
        else:
            raise SyntaxError(f"Not a correct token '{token.__str__()}'.")

    def parse(self, token_stream):
        while True:
            try:
                token = next(token_stream)
                self.ahead(token)
            except StopIteration:
                # patch "$" in the end of token stream
                # to match the augmented grammar
                self.ahead(Token("$", "$"))
                break

    def push_arg(self, token):
        if token.typ == 'C':
            token.code = lambda f_cond: 'Ccode Cfalse = {}'.format(f_cond)
        elif token.typ == 'S1':
            token.code = lambda : 'S1code'
        elif token.typ == 'S2':
            token.code = lambda : 'S2code'
        self.arg_stack.append(token)


all_labels = []


def get_label():
    n = 'L' + str(len(all_labels))
    all_labels.append(n)
    return n


def stmt(IF, LPAR, c, RPAR, s1, ELSE, s2):
    def call(next_label):
        L1 = get_label()
        C_code = c.code(f_cond=L1)
        S1_code = s1.code()
        S2_code = s2.code()
        inter_code = """
        {} 
        {}
        goto {}
        label {}
        {}""".format(C_code, S1_code, next_label, L1, S2_code)
        return inter_code
    return call


def start(stmt):
    def call():
        L = get_label()
        return stmt(L)
    return call


def startsup(f):
    return f()

運行一下,

from parser import SDT
from tokenizer import tokenizer
token_stream = tokenizer('if (C) S1 else S2')
sdt = SDT()
sdt.parse(token_stream)

成功翻譯:

        Ccode Cfalse = L1 
        S1code
        goto L0
        label L1
        S2code

這是個簡陋的過程,但是核心功能完整,我們可以在之后的過程中,逐步完善它。
通常,詞法規則和語法規則是由單獨的文件定義的。所以需要對詞法規則和語法規則進行解析的構件,來完成從源文本到python對象的轉換。翻譯方案通常嵌入到語法規則中。
錯誤處理可以在適當的情況引入到編譯過程當中。
另外,二義性文法,空產生式等情況的轉換在語法添加的過程當中會浮現。
當然還有為語法規則添加基本的語句,使之逐漸成為一個完善的編譯前端。

不論如何,我們已經完成了編譯前端從源語言到目標語言的全部流程,是一個成功的開始。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,401評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,011評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,263評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,543評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,323評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,874評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,968評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,095評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,605評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,551評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,720評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,242評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,961評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,358評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,612評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,330評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,690評論 2 370

推薦閱讀更多精彩內容