從零寫一個編譯器(一):輸入系統和詞法分析

項目的完整代碼在 C2j-Compilerjava

前言

從半抄半改的完成一個把C語言編譯到Java字節碼到如今也有些時間,一直想寫一個系列來回顧整理一下寫一個編譯器的過程,也算是學習筆記吧。就從今天開始動筆吧。git

一開始會先寫一個C語言的解釋器,直接遍歷AST直接執行,再以後會加入生成代碼部分,也就是編譯成Java字節碼github

支持C語言的大部分使用,具體能夠到上面的連接去看,固然依舊是比玩具級還玩具級的編譯器。性能

正式開始

完成一個編譯器大抵上主要有這幾部分學習

  • 詞法分析

通常用有限狀態自動機或者手工編寫來實現,這一步輸出的是token序列優化

  • 語法分析

主要分爲自頂向下和自底向上的語法分析,通常有遞歸降低,LL(1),LR(1),LALR(1)幾種方法實現。這一步輸出的是語法樹指針

  • 語義分析

語義分析主要任務是生成符號表,而且發現不符合語義的語句,這一步輸出的仍是ASTcode

  • 代碼生成

這裏通常會生成一個與平臺無關的較爲貼近底層的中間語言(IR),這一步輸入AST,輸出的是IR遞歸

  • 代碼優化

這一步的工做和名字同樣,就是進行代碼的優化,提高性能等等token

  • 目標代碼生成

這一步的任務就是生成平臺相關的彙編語言了

以上差很少就是整個通用意義上來講的編譯器了,可是也能夠把包括調用連接器彙編器來生成可執行文件

水平時間有限C2j-Compiler裏對於後三步是直接遍歷AST生成目標Java字節碼的,沒有任何優化。詞法分析使用手工編寫,語法分析使用LALR(1)語法分析表

輸入系統

對於一個有千行計的源文件,構建一個輸入系統來提升輸入的效率是頗有必要的。

輸入系統主要有三個文件

  • FileHandler.java
  • DiskFileHandler.java
  • Input.java

FileHadnler

做爲一個輸入的接口,DiskFileHandler實現這個接口來實現從文件讀入。主要有三個方法

void open();
int close();
int read(byte[] buf, int begin, int end);

其中read就是把指定數據長度複製到指定的緩衝區裏而且指定了緩衝區的開始位置

完整的源代碼都在個人倉庫裏 dejavudwh

Input

Input是整個輸入系統實現的關鍵點,其中利用了一個緩衝區來提升輸入的效率,也就是先把一部分的文件內容放入緩衝區,當輸入指針即將越過危險區域時,就從新的對緩衝區進行輸入,這樣就能夠整塊整塊的來讀入文件內容,來避免屢次的IO。

inputAdvance是向前一個位置獲取輸入,在獲取輸入前,會先檢查是否是須要flush緩衝區

public byte inputAdvance() {
        char enter = '\n';

        if (isReadEnd()) {
            return 0;
        }

        if (!readEof && flush(false) < 0) {
            //緩衝區出錯
            return -1;
        }

        if (inputBuf[next] == enter) {
            curCharLineno++;
        }

        endCurCharPos++;

        return inputBuf[next++];
}

flush的主要邏輯就是判斷next指針是否是越過了危險位置,或者force爲true也就是要求強制flush,就調用fillbuf來填滿緩衝區

private int flush(boolean force) {
        int noMoreCharToRead = 0;
        int flushOk = 1;

        int shiftPart, copyPart, leftEdge;
        if (isReadEnd()) {
            return noMoreCharToRead;
        }

        if (readEof) {
            return flushOk;
        }

        if (next > DANGER || force) {
            leftEdge = next;
            copyPart = bufferEndFlag - leftEdge;
            System.arraycopy(inputBuf, leftEdge, inputBuf, 0, copyPart);
            if (fillBuf(copyPart) == 0) {
                System.err.println("Internal Error, flush: Buffer full, can't read");
            }

            startCurCharPos -= leftEdge;
            endCurCharPos -= leftEdge;
            next  -= leftEdge;
        }

        return flushOk;
}
private int fillBuf(int startPos) {
        int need;
        int got;
        need = END - startPos;
        if (need < 0) {
            System.err.println("Internal Error (fill buf): Bad read-request starting addr.");
        }

        if (need == 0) {
            return 0;
        }

        if ((got = fileHandler.read(inputBuf, startPos, need)) == -1) {
            System.err.println("Can't read input file");
        }

        bufferEndFlag = startPos + got;
        if (got < need) {
            //輸入流已經到末尾
            readEof = true;
        }

        return got;
}

詞法分析

詞法分析的工做在於把源文件的輸入流分割成一個一個token,Lexer的輸出可能就相似<if, keyword>。識別出標識符,數字,關鍵字就在這一部分。

Lexer裏一共有兩個文件:

  • Token.java
  • Lexer.java

Token

Token主要就是用來標識每一個Token,在Lexer裏用到主要是像NAME來表示標識符,NUMBER來表示數字,STRUCT來表示struct關鍵字。

//terminals
NAME, TYPE, STRUCT, CLASS, LP, RP, LB, RB, PLUS, LC, RC, NUMBER, STRING, QUEST, COLON,
RELOP, ANDAND, OR, AND, EQUOP, SHIFTOP, DIVOP, XOR, MINUS, INCOP, DECOP, STRUCTOP,
RETURN, IF, ELSE, SWITCH, CASE, DEFAULT, BREAK, WHILE, FOR, DO, CONTINUE, GOTO,

Lexer

而Lexer就是利用以前的Input讀入輸入流,來輸出Token流

public void advance() {
        lookAhead = lex();
}

Lexer的主要邏輯就是在lex(),每次利用inputAdvance從輸入流讀出,直到碰見空白符或者換行符就表明了至少一個Token的結束,(這裏若是碰見雙引號也就是字符串裏的空格不能看成空白符處理),以後就開始進行分析。

代碼太長只截出來一部分,邏輯都很簡單,另一開始寫的時候就沒有處理註釋,後來也就沒有加上去

for (int i = 0; i < current.length(); i++) {
                length = 0;
                text = current.substring(i, i + 1);
                switch (current.charAt(i)) {
                    case ';':
                        current = current.substring(1);
                        return Token.SEMI.ordinal();
                    case '+':
                        if (i + 1 < current.length() && current.charAt(i + 1) == '+') {
                            current = current.substring(2);
                            return Token.INCOP.ordinal();
                        }

                        current = current.substring(1);
                        return Token.PLUS.ordinal();

                    case '-':
                        if (i + 1 < current.length() && current.charAt(i + 1) == '>') {
                            current = current.substring(2);
                            return Token.STRUCTOP.ordinal();
                        } else if (i + 1 < current.length() && current.charAt(i + 1) == '-') {
                            current = current.substring(2);
                            return Token.DECOP.ordinal();
                        }

                        current = current.substring(1);
                        return Token.MINUS.ordinal();

                        ...
                        ...
}

到這裏輸入系統和詞法分析就結束了。

詞法分析階段的工做就是將輸入的字符流轉換爲特定的Token。這一步是識別組合字符的過程,主要是標識數字,標識符,關鍵字等過程。這一部分應該是整個編譯器中最簡單的部分

另外個人github博客:https://dejavudwh.cn/

相關文章
相關標籤/搜索