从零写一个编译器(一):输入系统和词法分析
項目的完整代碼在 C2j-Compiler
前言
從半抄半改的完成一個把C語言編譯到Java字節碼到現在也有些時間,一直想寫一個系列來回顧整理一下寫一個編譯器的過程,也算是學習筆記吧。就從今天開始動筆吧。
一開始會先寫一個C語言的解釋器,直接遍歷AST直接執行,再之后會加入生成代碼部分,也就是編譯成Java字節碼
支持C語言的大部分使用,具體可以到上面的鏈接去看,當然依舊是比玩具級還玩具級的編譯器。
正式開始
完成一個編譯器大抵上主要有這幾部分
- 詞法分析
一般用有限狀態自動機或者手工編寫來實現,這一步輸出的是token序列
- 語法分析
主要分為自頂向下和自底向上的語法分析,一般有遞歸下降,LL(1),LR(1),LALR(1)幾種方法實現。這一步輸出的是語法樹
- 語義分析
語義分析主要任務是生成符號表,并且發現不符合語義的語句,這一步輸出的還是AST
- 代碼生成
這里一般會生成一個與平臺無關的較為貼近底層的中間語言(IR),這一步輸入AST,輸出的是IR
- 代碼優化
這一步的工作和名字一樣,就是進行代碼的優化,提升性能等等
- 目標代碼生成
這一步的任務就是生成平臺相關的匯編語言了
以上差不多就是整個通用意義上來說的編譯器了,但是也可以把包括調用鏈接器匯編器來生成可執行文件
水平時間有限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/
轉載于:https://www.cnblogs.com/secoding/p/11367511.html
總結
以上是生活随笔為你收集整理的从零写一个编译器(一):输入系统和词法分析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Linux学习笔记(十五)用户和用户组
- 下一篇: 我是如何学习写一个操作系统(一):开篇