致親愛的讀者: 我的的文字組織和寫文章的功底屬實通常, 寫的也比較趕時間, 因此係列文章的文字可能比較粗糙, 不免有詞不達意或者寫的很迷惑抽象的地方 若是您看了有疑問或者以爲我寫的實在亂七八糟, 這個很抱歉, 確實是個人問題, 您若是有不懂的地方 的地方或者發現個人錯誤(文字錯誤, 邏輯錯誤或者知識點錯誤都有可能), 能夠直接留言, 我看到都會回覆您!
因爲時間緣由, 目前測試並不完善, 因此推薦以下方式根據您的目的進行閱讀 若是您是學習用, 建議您先將整個項目clone到本地, 而後把感興趣的章節刪除, 本身重寫對照着重寫 書寫完每一步測試一下可否正常運行(在指定的路徑去讀取源碼測試可否編譯成功並在命令行執行 java Application(類名) 嘗試可否輸出指望結果, 我沒有研究Junit對編譯器輸出class文件進行測試, 因此目前可能須要您手動測試) 按照以上步驟, 等您將全部模塊重寫一遍, 大概也對這個系列的脈絡有深入理解了! 若是您重頭開始重寫, 每每可能因爲出現某些低級錯誤致使長時間debug才找獲得錯誤, 因此對於初學者, 推薦採用本身補寫替換模塊的 方式 對於但願貢獻代碼的朋友或者對Cva感興趣的朋友, 歡迎貢獻您的源碼與看法, 或者對於該系列一些錯誤/ bug願意提出指正的朋友, 您能夠留言或者在github發issue, 我看到後必定及時處理!
引言html
存儲符號定義以及信息的類前端
詞法分析器實現過程走代碼詳解java
樣例輸出分析git
編譯源碼的第一個步驟是進行詞法分析, 詞法分析器讀入源代碼的字符流,
將他們識別成Token流, 如
num++;
通過詞法分析
=> identifier + increment + semi (標識符 * 1, 自增 * 1, 分號 * 1)
你們能夠看到, 語法分析器作的事情很是簡單, 就是將源碼與Token一一對應, 相似創建一個映射關係,
咱們這裏每個Token都new了一個對象, 這樣的作法並不高效
(我的認爲高效的方法是用枚舉取代一些字面量固定的值, 主要是符號如+
, -
等),
可是能夠保存行號信息, 以便以後向用戶提供足夠的報錯信息.
獲得詞法分析的結果後(在這裏是和語法分析器配合每次讀一個Token並前看,
而不是一次性讀完再轉交語法分析器處理),詞法分析器將其輸出交由語法分析器Parser處理github
+
-
*
/
%
&&
=
.
!
<
++
--
&
|
^
>>
<<
>>>
等其餘運算符{
}
(
)
;``,
+=
-=
*=
/=
&=
|=
^=
>>=
<<=
>>>=
等賦值運算符,?
:
, ==
||
!
~
++
--
是單目操做符, unary, 目前 i++
++i
還不區分壓棧幀和自增順序:
?
屬於三目操做符的一部分.
圓點運算符調用方法, 目前尚未作對象的屬性調用{
}
(
)
是定界符void
byte
short
char
int
long
float
double
boolean
string
class
new
if
else
while
true
false
this
return
等, 還有部分保留字, 是但願在將來實現的
void
byte
short
char
int
long
float
double
boolean
string
是類型聲明,class
聲明一個類型時使用的前導關鍵字extends
是繼承聲明, 目前的繼承很弱雞, 不少功能沒有實現new
申請內存分配給新對象, 或者數組, 目前數組還不支持if
else
while
for
分支/循環結構關鍵字, 目前還不支持switch casetrue
false
上文所述 boolean
類型的兩字面量this
當前對象實例指針return
返回語句的前導關鍵字, 目前只作了最後一句返回, 後期但願實現方法中的返回Identifier
ConstInt
comment
space
\n
\r
EOF
println/echo
Identifier
標識符, 將類型/變量名等處理成一個標識符類型的記號, 具體意義取決於所處的位置ConstInt
整數常數字面量, 其餘類型同space
\n
\r
分別是空格符 換行符 回車符, 處理源代碼文件時將忽略它們// comment
行註釋/* comment */
塊註釋EOF
是源代碼的文件結束符println
&& echo
輸出語句, printf
的支持打算在後期的版本更新按照咱們規定的程序文法, 能給出記號的定義
EnumCvaToken類後端
package cn.misection.cvac.lexer; import java.util.EnumSet; import java.util.HashMap; import java.util.Map; public enum EnumCvaToken { /** * + */ ADD, /** * - */ SUB, /** * * */ STAR, /** * / */ DIV, /** * 求餘; */ REM, /** * & */ BIT_AND, /** * | */ BIT_OR, /** * ...更多的在這裏略去, 可見源碼中 */ ; }
CvaToken類數組
package cn.misection.cvac.lexer; /** * @author MI6 root */ public final class CvaToken { /** * the kind of the token */ private final EnumCvaToken enumToken; /** * extra literal of the token */ private String literal; /** * the line number of the token */ private final int lineNum; public CvaToken(EnumCvaToken enumToken, int lineNum) { this.enumToken = enumToken; this.lineNum = lineNum; } public CvaToken(EnumCvaToken enumToken, int lineNum, String literal) { this.enumToken = enumToken; this.lineNum = lineNum; this.literal = literal; } @Override public String toString() { return String.format("Token {%s literal: %s : at line %d}", this.enumToken.toString(), literal == null ? "null" : this.literal, this.lineNum); } public EnumCvaToken toEnum() { return enumToken; } public String getLiteral() { return literal; } public int getLineNum() { return lineNum; } }
其中 enumToken
字段標記了該記號的類型(即以上的EnumCvaToken
枚舉類),
如有值, 例如數字或者字符串或者Identifier, 將在 literal
字段中給出當前記號的值
此外, 還會在 lineNum
字段給出當前記號的行號, 主要是報錯環節給出儘量定位準確的錯誤信息app
詞法分析部分採用了手動實現的方式, 大體步驟以下, 你們能夠參照lexer package中的Lexer類查看過程,
固然, 這一切還要藉助io包內的文件流將文件讀入成StringBuffer(姑且把它當作buffer吧)jvm
private CvaToken lex() { char ch = stream.poll(); // skip all kinds of blanks ch = handleWhiteSpace(ch); switch (ch) { case LexerCommon.EOF: return new CvaToken(EnumCvaToken.EOF, lineNum); case '+': return handlePlus(); case '-': return handleMinus(); case '*': return handleStar(); case '&': return handleAnd(); case '|': return handleOr(); case '=': return handleEqual(); case '<': return handleLessThan(); case '>': return handleMoreThan(); case '^': return handleXOr(); case '~': return handleBitNegate(); case '/': return handleSlash(); case '%': return handlePercent(); case '\'': return handleApostrophe(); case '"': return handleDoubleQuotes(); default: return handleNorPrefOrIdOrNum(ch); } }
每當外部調用lexer的nextToken()方法(主要是Parser調用), 都會進行以下流程:ide
初始化源文件的按字符輸入流, 並讀入一個字符, 初始化全局行號爲1,
而後循環進行後面的全部步驟
跳轉到handleWhiteSpace方法檢查是不是空白符(空格, 換行符, 回車符), 若是是, 直接嘗試讀下一個字符, 但若是發生換行, 行號++
讀取符號, 會switch之, 若是落在前綴字符的區間內, 前綴字符即某個運算符的前綴, 如 +
是 ++
, +=
的前綴,
這樣的字符在詞法分析器中都有其特殊處理方法, 在這裏面, 也包含了對註釋的處理,
會忽略Cva行註釋// ... \n 與塊註釋/*... */
編譯器後端的任務很重, 因此在前端, 咱們應儘可能肯定變量的類型, 刪除無關的註釋, 空符, 好給後端騰出
同時這裏要注意, 咱們在Lexer的lex()方法中, 查看當前的字符是否爲case塊中的字符, 例如 +
, -
, *
, 等等, 若是是, 那就跳轉處處理他們的方法中
,爲何要跳轉方法? 由於他們是前綴字符, 即分析器看到了+
, 並不能肯定這是一個加號, 要和其後面的字符一塊兒看
後面的字符多是+
, 那麼該符號應該是++
, 自增符號, 若是後一個連續的的符號是=
, 那麼這個符號應該是+=
不然, 咱們才返回+
, 其餘的同理, 每次lex()方法被調用, 詞法分析器能返回一個類型符合的, 攜帶行號信息的記號,
如, 遇到 +
號:
private CvaToken handlePlus() { if (stream.hasNext()) { switch (stream.peek()) { case '+': { // 截取兩個; stream.poll(); return new CvaToken(EnumCvaToken.INCREMENT, lineNum); } case '=': { stream.poll(); return new CvaToken(EnumCvaToken.ADD_ASSIGN, lineNum); } default: { break; } } } return new CvaToken(EnumCvaToken.ADD, lineNum); }
咱們這裏把stream當作隊列, 每次poll()會彈出隊列首的字符, 這個首字符就是咱們peek()到的字符.
若是遇到的不是這些前綴字符, 咱們會到進入default分支中, 執行handleNorPrefOrIdOrNum()方法
private CvaToken handleNorPrefOrIdOrNum(char ch) { // 先看c是不是非前綴字符, 這裏是 int, 必須先轉成char看在不在表中; if (EnumCvaToken.containsKind(String.valueOf(ch))) { return new CvaToken(EnumCvaToken.selectReverse(String.valueOf(ch)), lineNum); } StringBuilder builder = new StringBuilder(); builder.append(ch); while (true) { ch = stream.peek(); // Cva命名允許_和$符號; if (ch != LexerCommon.EOF && !Character.isWhitespace(ch) && !isSpecialCharacter(ch)) { builder.append(ch); this.stream.poll(); continue; } break; } String literal = builder.toString(); // 關鍵字; if (EnumCvaToken.containsKind(literal)) { return new CvaToken(EnumCvaToken.selectReverse(literal), lineNum); } else { if (isNumber(literal)) { // FIXME 自動機; if (isInt(literal)) { return new CvaToken(EnumCvaToken.CONST_INT, lineNum, builder.toString()); } } else if (isIdentifier(literal)) { return new CvaToken(EnumCvaToken.IDENTIFIER, lineNum, builder.toString()); } else { errorLog("identifier or number which can only include alphabet, number or _, $", "an illegal identifier with illegal char"); } } return null; }
// 請注意, 通常不要return null, 儘可能拋出異常或者返回空對象, 這裏是由於根本走不到這個分支, 才 return null
進入default分支的方法首先會查表, 這個Hash表是EnumCvaToken中的利用Enum的literal字面量屬性反查Enum類型建的表,
這個表的key是Token的字面量, value是該Token的枚舉, 在Enum 中構建以下
private static final Map<String, EnumCvaToken> lookup = new HashMap<>(); static { for (EnumCvaToken kind : EnumSet.allOf(EnumCvaToken.class)) { if (kind.kindLiteral != null) { lookup.put(kind.kindLiteral, kind); } } } public static boolean containsKind(String literal) { return lookup.containsKey(literal); }
// 以上是常見的創建枚舉反查表的方法, 若是有看不懂的地方歡迎留言, 我看到會回覆.
在這一步, 若是遇到{
, }
這類非前綴字符(即不會產生歧義, 單字符只能單獨出如今代碼中),
直接返回該Token, 不然會進入下一步
當前的字符不是咱們支持的特殊字符, 那就一直讀取下去, 直到空格符/換行符/回車符/文件結束符/註釋,
這樣是儘量找到了一個最長的序列.
首先檢查這個序列是不是咱們定義的關鍵字之一, 這個過程也是查咱們前述的哈希表,
若是是關鍵字, 那麼就返回正確類型和行號的Token;
若是不是關鍵字, 那麼查看這個序列是否是一串數字, 若是是數字, 那麼返回一個類型爲ConstInt
(目前尚未作自動機識別浮點數, 後期有空了會作正確的行號和數字串的記號;
若是也不是數字串, 那麼檢查是否符合程序對於標識符的規定, 若是符合,
那麼返回一個類型爲Identifier
(通常是用戶自定義的變量常量名/類名/方法名),
正確行號和標識符串的記號, Cva的變量名如同Java也須要字母開頭/下劃線或者不常見美圓符號開頭,
若是不符合, 那麼只能是一個錯誤, 給出提示並結束分析.
例如, 對於語句樣例
echo "hello, world!\n"; println new Increment().incre(); echo "2 * 3 = "; println 2 * 3; // 目前暫時不支持 printf;
詞法分析後的Token流是
{Token {WRITE literal: null : at line 93}} {Token {STRING literal: hello, world! : at line 93}} {Token {SEMI literal: null : at line 93}} {Token {WRITE_LINE literal: null : at line 94}} {Token {NEW literal: null : at line 94}} {Token {IDENTIFIER literal: Increment : at line 94}} {Token {OPEN_PAREN literal: null : at line 94}} {Token {CLOSE_PAREN literal: null : at line 94}} {Token {DOT literal: null : at line 94}} {Token {IDENTIFIER literal: incre : at line 94}} {Token {OPEN_PAREN literal: null : at line 94}} {Token {CLOSE_PAREN literal: null : at line 94}} {Token {SEMI literal: null : at line 94}} {Token {WRITE literal: null : at line 95}} {Token {STRING literal: 2 * 3 = : at line 95}} {Token {SEMI literal: null : at line 95}}
能夠看到對於空白符和註釋, 在詞法分析階段咱們就進行了跳過, 源代碼文件流就會轉換成這樣的Token流供語法分析器處理 以上結果應該不難理解, 若是朋友們有任何疑問, 歡迎留言