| 版本 | 日期 | 備注 |
|---|---|---|
| 1.0 | 2023.8.23 | 文章首發(fā) |
前陣子向大家分享了我寫的插件https://marketplace.visualstudio.com/items?itemName=CamileSing.flink-sql,最近梳理了我之前的學習相關知識時的筆記,希望能夠幫到對這一塊實現(xiàn)感興趣的同學。
1. TypeScirpt
開發(fā)VS Code,可以選擇使用了TypeScript or JavaScript。雖然我沒學過TypeScript,但是我還是選擇了它。我想起大學工作室的時候,身邊有小伙伴就特別喜歡JavaScript這種寫起來很快的語言,但是我卻更喜歡Java這種語言。因為有些時候我根本不知道JavaScript里的一些變量的值到底是什么。
TS在官網(wǎng)是用一句話描述了它TypeScript is a strongly typed programming language that builds on JavaScript, giving you better tooling at any scale。一段時間用下來,發(fā)現(xiàn)TS真香,我本身接觸的語言也不算少,所以上手很快。而且它的類型系統(tǒng)非常強大,讓我非常有好感。
這個語言讓我比較印象深刻的是,它不僅設置了類似Java中Object的Unknown,還有所有類型子類的Never類型,用來代表其永遠不會發(fā)生,比如:
function foo(x: string | number): boolean {
if (typeof x === 'string') {
return true;
} else if (typeof x === 'number') {
return false;
}
// 如果不是一個 never 類型,這會報錯:
// - 不是所有條件都有返回值 (嚴格模式下)
// - 或者檢查到無法訪問的代碼
// 但是由于 TypeScript 理解 `fail` 函數(shù)返回為 `never` 類型
// 它可以讓你調用它,因為你可能會在運行時用它來做安全或者詳細的檢查。
return fail('Unexhaustive');
}
function fail(message: string): never {
throw new Error(message);
}
另外就是對于范型的支持也很有意思,上面這個函數(shù)簽名可以寫出foo(x: string | number)這樣的寫法。對于范型支持的更好意味著可以讓程序員更好的去做抽象。
在學習TypeScript的時候還接觸到了一本書,叫做《編程與類型系統(tǒng)》,被一些網(wǎng)友戲稱“一周入門TypeScript”。整體內(nèi)容還是比較不錯的,講到了類型系統(tǒng)來自于數(shù)學中的范疇論,以及類型系統(tǒng)的優(yōu)點:類型的主要優(yōu)點在于正確性、不可變性、封裝、可組合性和可讀性。這5種優(yōu)點是優(yōu)秀的軟件設計和行為的根本特性。系統(tǒng)中總有出現(xiàn)混亂或者無序狀態(tài)的傾向,而上述特性則起到抗衡這種傾向的作用。以此展開聊TypeScript的一些語法,以及對比JavaScript,TS做了哪些有用的改進。
2. 錯誤檢測能力:詞法、語法分析
插件的錯誤檢測能力,其實是基于詞法、語法分析實現(xiàn)的。我們先來解釋一下名詞:
- 詞法分析:一個個詞去找,有些情況下需要多看一個乃至多幾個個單詞才能確定這個詞是哪個類型的token(這種行為在編譯器里面叫peek)。
- 語法分析:根據(jù)已有token序列,分析每一行代碼是什么屬于什么語句類型——也是一個個token進來分析,有些情況下需要peek下一個乃至下下個單詞才能確定。
這塊其實是編譯原理的一部分,屬于前端編譯部分,并未涉及后端編譯。見:https://github.com/camilesing/Flink-SQL-Helper-VSCode/blob/main/src/extension.ts中的
// 使用生成的詞法分析器和解析器進行語法檢查
const inputStream = new ANTLRInputStream(event.getText());
//詞法解析
const lexer = new FlinkSQLLexer(inputStream);
const tokenStream = new CommonTokenStream(lexer);
//語法解析
const parser = new FlinkSQLParser(tokenStream);
parser.removeErrorListeners();
parser.addErrorListener({
syntaxError: (recognizer: Recognizer<any, any>, offendingSymbol: any, line: number, charPositionInLine: number, msg: string, e: RecognitionException | undefined): void => {
vscode.window.showErrorMessage("Parser flink sql error. line: " + line + " position: " + charPositionInLine + " msg: " + msg);
},
})
parser.compileParseTreePattern
// 解析文件內(nèi)容并獲取語法樹
const parseTree = parser.program();
寫這塊代碼我用到了Antlr4-TS這個庫。我根據(jù)一些Antlr4的語法規(guī)則,生成了對應的代碼,并將輸入內(nèi)容丟進這些類,讓它們吐出結果。在了解Antlr相關的語法規(guī)則時,讓我特別震撼——類似于剛畢業(yè)一年時接觸到DSL時的震撼。通過一系列規(guī)則的描述,竟然可以生產(chǎn)如此復雜、繁多的代碼,巨幅解放生產(chǎn)力。這些規(guī)則是一種很美又具有實際價值的抽象。
那讓我們拋開Antlr這個框架的能力,如果去手寫一個詞法、語法分析的實現(xiàn),該怎么做呢?
在編程語言里,一般會有保留字和標識符的概念。保留字就是這個語言的關鍵字,比如SQL中的select,Java中的int等等,標識符就是你用于命名的文字。比如public class Person中的Person,select f1 as f1_v2 from t1中的f1,f1_v2,t1。
再擴展一下概念,我們以int a=1;這樣一段代碼為例子,int 是關鍵字,a是標識符,=是操作符,;是符號(結束符)。搞清楚哪些詞屬于什么類型,這就是詞法解析器要做的事。那怎么做呢?最簡單的方法其實就是按照一定規(guī)則(比如A-Za-z$)一個個去讀取,比如讀到i的時候,它要去看后面是不是結束符或者空格,也就上文提到的的peek,如果不為空,就要繼續(xù)往后讀,直到讀到空格或者結束符。那么讀取出來是個int,就知道這是個關鍵字。
偽代碼如下:
循環(huán)讀取字符
case 空白字符
處理,并繼續(xù)循環(huán)
case 行結束符
處理,并繼續(xù)循環(huán)
case A-Za-z$_
調用scanIden()識別標識符和關鍵字,并結束循環(huán)
case 0之后是X或x,或者1-9
調用scanNumber()識別數(shù)字,并結束循環(huán)
case , ; ( ) [ ]等字符
返回代表這些符號的Token,并結束循環(huán)
case isSpectial(),也就是% * + - | 等特殊字符
調用scanOperator()識別操作符
...
這下我們知道了int a=1;在詞法解析器看來其實就是關鍵字(類型) 標識符 操作符 數(shù)字 結束符。這樣的寫法其實是符合Java的語法規(guī)則的。反過來說:int int=1;是能夠通過詞法分析的,但是無法通過語法分析,因為關鍵字(類型) 關鍵字(類型) 操作符 數(shù)字 結束符是不符合Java的語法定義的。
這個時候可能會有人問,為啥要有詞法分析這一層?都放到語法分析這一層也是可以做的啊??梢宰?,但會很復雜。而且一般軟件工程中會都做分層,避免外面的變動影響到里面的核心邏輯。 舉個例子:后續(xù)Java新增了一個類型,如果詞法分析、語法分析是拆開的,那么只要改詞法分析層的一些代碼就行了,語法分析不用。但是如果沒有詞法分析這一層,語法分析的代碼會有很多,而且一點點改動就很容易影響到這一層。
在此之后就會生成語法樹。后續(xù)我打算做一些基于語法樹的分析,Antlr提供了兩種讀語法節(jié)點的方式,一種是Vistor,一種是Listeners。前者意味著你可以主動的去遍歷一些節(jié)點,而后者就像注冊了鉤子,Antlr遍歷到這里的時候會主動“喊”你。
// 創(chuàng)建訪問器實例并訪問語法樹,以獲取語法錯誤和警告
const visitor = new MyFlinkSQLVisitor();
visitor.visit(parseTree);
const errors = visitor.getErrors();
編譯器其實分前端編譯部分和后端編譯部分的。語義分析也是在前端,在語義分析階段,其實是可以定義一些規(guī)則去做優(yōu)化的。
編譯器的后端,主要是負責語法樹到目標代碼(平臺無關),到平臺有關代碼——比如,同一段源代碼生成的x86體系下的可執(zhí)行程序和MIPS體系下的可執(zhí)行程序,其運行時結構會有較大的區(qū)別,這種區(qū)別會體現(xiàn)在目標代碼上。如果一步到位由語法樹轉換為目標代碼,就需要為每種CPU去寫一套完全獨立的后端。為了避免這種情況以及便于優(yōu)化,于是在語法樹和包含機器特征的目標代碼之間建立了一個中間結構,這樣就能更加方便地將語法樹轉換為適合不同CPU的目標代碼,這是設計中間結構的最初目的。高端gimple、低端gimple、cfg、ssa、RTL(Register Transfer Language)就是這樣的中間結構。這塊沒有什么實際的業(yè)務場景可以接觸,所以就沒有深入去看了。
3.小結
業(yè)余開發(fā)這款插件,的確花了我很多時間。現(xiàn)在想來還是很值得的——在這里面學到了很多,而且還把自己想做的東西做出來了。后續(xù)迭代中,有新的學習筆記或感悟,我也會整理上來,分享給大家。