1. 程式人生 > 程式設計 >從零寫一個編譯器(四):語法分析之構造有限狀態自動機

從零寫一個編譯器(四):語法分析之構造有限狀態自動機

專案的完整程式碼在 C2j-Compiler

通過上一篇對幾個構造自動機的基礎資料結構的描述,現在就可以正式來構造有限狀態自動機

我們先用一個小一點的語法推導式來描述這個過程

s -> e
e -> e + t
e -> t
t -> t * f
t -> f
f -> ( e )
f -> NUM
複製程式碼

初始化

狀態0是狀態機的初始狀態,它包含著語法表示式中的起始表示式,也就是編號為0的表示式:

0: s -> . e

這裡的點也就是之前Production類中的dosPos

負責這個操作的方法在StateNodeManager類中,前面先判斷當前目錄下是不是已經構建好語法分析表了,如果有的話就不需要再次構建了。

productionManager.buildFirstSets();可以先略過,後面會講到。

ProductionsStateNode就是用來描述狀態節點的

public static int stateNumCount = 0;
/** Automaton state node number */
public int stateNum;
/** production of state node */
public ArrayList<Production> productions;
複製程式碼

接著就是放入開始符號作為第一個狀態節點,也就是這一步的初始化

public void buildTransitionStateMachine
()
{ File table = new File("lrStateTable.sb"); if (table.exists()) { return; } ProductionManager productionManager = ProductionManager.getInstance(); productionManager.buildFirstSets(); ProductionsStateNode state = getStateNode(productionManager.getProduction(Token.PROGRAM.ordinal())); state.buildTransition(); debugPrintStateMap(); } 複製程式碼

對起始推導式做閉包操作

注意之前的 .,也就是Production裡的dosPos,這一步就有用了,利用這個點來做閉包操作

對.右邊的符號做閉包操作,也就是說如果 . 右邊的符號是一個非終結符,那麼肯定有某個表示式,->左邊是該非終結符,把這些表示式新增進來

s -> . e
e -> . e + t
e -> . t
複製程式碼

對新新增進來的推導式反覆重複這個操作,直到所有推導式->右邊是非終結符的那個所在推導式都引入,這也就是ProductionsStateNode裡的makeClosure方法

主要邏輯就是先將這個節點中的所有產生式壓入堆疊中,再反覆的做閉包操作。closureSet是每個節點中儲存閉包後的產生式

private void makeClosure() {
    Stack<Production> productionStack = new Stack<Production>();
    for (Production production : productions) {
        productionStack.push(production);
    }

    if (Token.isTerminal(production.getDotSymbol())) {
        ConsoleDebugColor.outlnPurple("Symbol after dot is not non-terminal,ignore and process next item");
        continue;
    }
            
    while (!productionStack.empty()) {
        Production production = productionStack.pop();
        int symbol = production.getDotSymbol();
        ArrayList<Production> closures = productionManager.getProduction(symbol);
        for (int i = 0; closures != null && i < closures.size(); i++) {
            if (!closureSet.contains(closures.get(i))) {
                closureSet.add(closures.get(i));
                productionStack.push(closures.get(i));
            }
        }
    }
}
複製程式碼

對引入的產生式進行分割槽

把 . 右邊擁有相同非終結符的表示式劃入一個分割槽,比如

s -> . e
e -> . e + t
複製程式碼

就作為同一個分割槽。最後把每個分割槽中的表示式中的 . 右移動一位,形成新的狀態節點

s -> e .
e -> e . + t
複製程式碼

分割槽操作就在ProductionsStateNode類中的partition方法中

主要邏輯也很簡單,遍歷當前的closureSet,如果分割槽不存在,就以產生式點的右邊作為key,產生式列表作為value,並且如果當前產生式列表裡不包含這個產生式,就把這個產生式加入當前的產生式列表

private void partition() {
    ConsoleDebugColor.outlnPurple("==== state begin make partition ====");

    for (Production production : closureSet) {
        int symbol = production.getDotSymbol();
        if (symbol == Token.UNKNOWN_TOKEN.ordinal()) {
            continue;
        }

        ArrayList<Production> productionList = partition.get(symbol);
        if (productionList == null) {
            productionList = new ArrayList<>();
            partition.put(production.getDotSymbol(),productionList);
        }

        if (!productionList.contains(production)) {
            productionList.add(production);
        }
    }

    debugPrintPartition();
    ConsoleDebugColor.outlnPurple("==== make partition end ====");
}
複製程式碼

對所有分割槽節點構建跳轉關係

根據每個節點 . 左邊的符號來判斷輸入什麼字元來跳入該節點

比如, . 左邊的符號是 t,所以當狀態機處於狀態0時,輸入時 t 時, 跳轉到狀態1。

. 左邊的符號是e,所以當狀態機處於狀態 0 ,且輸入時符號e時,跳轉到狀態2: 0 – e -> 2

這個操作的實現再ProductionsStateNode的makeTransition方法中

主要邏輯是遍歷所有分割槽,每個分割槽都是一個新的節點,所以拿到這個分割槽的跳轉關係,也就是partition的key,即之前產生式的點的右邊。然後構造一個新的節點和兩個節點之間的關係

private void makeTransition() {
    for (Map.Entry<Integer,ArrayList<Production>> entry : partition.entrySet()) {
        ProductionsStateNode nextState = makeNextStateNode(entry.getKey());

        transition.put(entry.getKey(),nextState);

        stateNodeManager.addTransition(this,nextState,entry.getKey());
    }

    debugPrintTransition();

    extendFollowingTransition();
}
複製程式碼

makeNextStateNode的邏輯也很簡單,就是拿到這個分割槽的產生式列表,然後返回一個新節點

private ProductionsStateNode makeNextStateNode(int left) {
    ArrayList<Production> productions = partition.get(left);
    ArrayList<Production> newProductions = new ArrayList<>();

    for (int i = 0; i < productions.size(); i++) {
        Production production = productions.get(i);
        newProductions.add(production.dotForward());
    }

    return stateNodeManager.getStateNode(newProductions);
}
複製程式碼

stateNodeManager已經出現很多次了,它是類StateNodeManager,它的作用是管理節點,分配節點,統一節點。之後對節點的壓縮和語法分析表的最終構建都在這裡完成,這是後話了。

上面用到的兩個方法:

transitionMap相當於一個跳轉表:key是起始節點,value是一個map,這個map的key是跳轉關係,也就是輸入一個終結符或者非終結符,value則是目標節點

public void addTransition(ProductionsStateNode from,ProductionsStateNode to,int on) {
        HashMap<Integer,ProductionsStateNode> map = transitionMap.get(from);
        if (map == null) {
            map = new HashMap<>();
        }

        map.put(on,to);
        transitionMap.put(from,map);
}
複製程式碼

getStateNode先從判斷如果這個節點沒有建立過,建立過的節點都會加入stateList中,就建立一個新節點。如果存在就會返回這個原節點

public ProductionsStateNode getStateNode(ArrayList<Production> productions) {
    ProductionsStateNode node = new ProductionsStateNode(productions);

    if (!stateList.contains(node)) {
        stateList.add(node);
        ProductionsStateNode.increaseStateNum();
        return node;
    }

    for (ProductionsStateNode sn : stateList) {
        if (sn.equals(node)) {
            node = sn;
        }
    }

    return node;
}
複製程式碼

對所有新生成的節點重複構建

這時候的第一輪新節點才剛剛完成,到等到所有節點都完成節點的構建才算是真正的完成,在makeTransition中呼叫的extendFollowingTransition正是這個作用

private void extendFollowingTransition() {
    for (Map.Entry<Integer,ProductionsStateNode> entry : transition.entrySet()) {
        ProductionsStateNode state = entry.getValue();
        if (!state.isTransitionDone()) {
            state.buildTransition();
        }
    }
}
複製程式碼

小結

建立有限狀態自動機的四個步驟

  • makeClosure
  • partition
  • makeTransition
  • 最後重複這些步驟直到所有的節點都構建完畢

至此我們對

public void buildTransition() {
    if (transitionDone) {
        return;
    }
    transitionDone = true;

    makeClosure();
    partition();
    makeTransition();
}
複製程式碼

的四個過程都已經完成,自動機的構建也算完成,應該進行語法分析表的建立了,但是這個自動機還有些問題,下一篇會來改善它。