博客 / 詳情

返回

筆記:寫Flink SQL Helper時學到的一些姿勢


theme: channing-cyan

版本 日期 備註
1.0 2023.8.23 文章首發
1.1 2024.10.15 改進部分描述方式

前陣子向大家分享了我寫的插件https://marketplace.visualstudio.com/items?itemName=CamileSin...,最近梳理了我之前的學習相關知識時的筆記,希望能夠幫到對這一塊實現感興趣的同學。

1. TypeScirpt

開發VS Code,可以選擇使用TypeScript or JavaScript。雖然沒學過TypeScript,但是我還是選擇了它。我想起大學工作室的時候,身邊有小夥伴就特別喜歡JavaScript這種寫起來很快的語言,但是我卻更喜歡Java這種語言。因為有些時候我根本不知道JavaScript裏的一些變量的值到底是什麼。

TS在官網是用一句話描述了它TypeScript is a strongly typed programming language that builds on JavaScript, giving you better tooling at any scale。一段時間用下來,發現TS真香,我本身接觸的語言也不算少,所以上手很快。而且它的類型系統非常強大,讓我非常有好感。

這個語言讓我比較印象深刻的是,它不僅設置了類似Java中Object的Unknown,還有所有類型子類的Never類型,用來代表其永遠不會發生,比如:

function foo(x: string | number): boolean {
  if (typeof x === 'string') {
    return true;
  } else if (typeof x === 'number') {
    return false;
  }

  // 如果不是一個 never 類型,這會報錯:
  // - 不是所有條件都有返回值 (嚴格模式下)
  // - 或者檢查到無法訪問的代碼
  // 但是由於 TypeScript 理解 `fail` 函數返回為 `never` 類型
  // 它可以讓你調用它,因為你可能會在運行時用它來做安全或者詳細的檢查。
  return fail('Unexhaustive');
}

function fail(message: string): never {
  throw new Error(message);
}

另外就是對於範型的支持也很有意思,上面這個函數簽名可以寫出foo(x: string | number)這樣的寫法。對於範型支持的更好意味着可以讓程序員更好的去做抽象。

在學習TypeScript的時候還接觸到了一本書,叫做《編程與類型系統》,被一些網友戲稱“一週入門TypeScript”。整體內容還是比較不錯的,講到了類型系統來自於數學中的範疇論,以及類型系統的優點:類型的主要優點在於正確性、不可變性、封裝、可組合性和可讀性。這5種優點是優秀的軟件設計和行為的根本特性。系統中總有出現混亂或者無序狀態的傾向,而上述特性則起到抗衡這種傾向的作用。以此展開聊TypeScript的一些語法,以及對比JavaScript,TS做了哪些有用的改進。

2. 錯誤檢測能力:詞法、語法分析

插件的錯誤檢測能力,其實是基於詞法、語法分析實現的。我們先來解釋一下名詞:

  • 詞法分析:一個個字符去找,有些情況下需要多看一個乃至多個字符才能確定這個詞是哪個類型的token(這種行為在編譯器裏面叫peek)。
  • 語法分析:根據已有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
// 解析文件內容並獲取語法樹
const parseTree = parser.program();

寫這塊代碼我用到了Antlr4-TS這個庫。我根據一些Antlr4的語法規則,生成了對應的代碼,並將輸入內容丟進這些類,讓它們吐出結果。在瞭解Antlr相關的語法規則時,讓我特別震撼——類似於剛畢業一年時接觸到DSL時的震撼。通過一系列規則的描述,竟然可以生產如此複雜、繁多的代碼,巨幅解放生產力。這些規則是一種很美又具有實際價值的抽象

那讓我們拋開Antlr這個框架的能力,如果去手寫一個詞法、語法分析的實現,該怎麼做呢?

在編程語言裏,一般會有保留字和標識符的概念。保留字就是這個語言的關鍵字,比如SQL中的select,Java中的int等等,標識符就是你用於命名的文字。比如public class Person中的Person, select f1 as f1_v2 from t1 中的f1,f1_v2,t1。

再擴展一下概念,我們以int a=1;這樣一段代碼為例子,int 是關鍵字,a是標識符,=是操作符,;是符號(結束符)。搞清楚哪些詞屬於什麼類型,這就是詞法解析器要做的事。那怎麼做呢?最簡單的方法其實就是按照一定規則(比如A-Za-z$)一個個去讀取,比如讀到i的時候,它要去看後面是不是結束符或者空格,也就上文提到的的peek,如果不為空,就要繼續往後讀,直到讀到空格或者結束符。那麼讀取出來是個int,就知道這是個關鍵字。

偽代碼如下:

循環讀取字符
  case 空白字符
    處理,並繼續循環
  case 行結束符
    處理,並繼續循環
  case A-Za-z$_
    調用scanIden()識別標識符和關鍵字,並結束循環
  case 0之後是X或x,或者1-9
    調用scanNumber()識別數字,並結束循環   
  case , ; ( ) [ ]等字符
    返回代表這些符號的Token,並結束循環
  case isSpectial(),也就是% * + - | 等特殊字符
    調用scanOperator()識別操作符
  ...    

這下我們知道了int a=1;在詞法解析器看來其實就是關鍵字(類型) 標識符 操作符 數字 結束符。這樣的寫法其實是符合Java的語法規則的。反過來説:int int=1;是能夠通過詞法分析的,但是無法通過語法分析,因為關鍵字(類型) 關鍵字(類型) 操作符 數字 結束符是不符合Java的語法定義的。

這個時候可能會有人問,為啥要有詞法分析這一層?都放到語法分析這一層也是可以做的啊。可以做,但會很複雜。而且一般軟件工程中會都做分層,避免外面的變動影響到裏面的核心邏輯。 舉個例子:後續Java新增了一個類型,如果詞法分析、語法分析是拆開的,那麼只要改詞法分析層的一些代碼就行了,語法分析不用。但是如果沒有詞法分析這一層,語法分析的代碼會有很多,而且一點點改動就很容易影響到這一層。

在此之後就會生成語法樹。後續我打算做一些基於語法樹的分析,Antlr提供了兩種讀語法節點的方式,一種是Vistor,一種是Listeners。前者意味着你可以主動的去遍歷一些節點,而後者就像註冊了鈎子,Antlr遍歷到這裏的時候會主動“喊”你。

// 創建訪問器實例並訪問語法樹,以獲取語法錯誤和警告
const visitor = new MyFlinkSQLVisitor();
visitor.visit(parseTree);
const errors = visitor.getErrors();

編譯器其實分前端編譯部分和後端編譯部分的。語義分析也是在前端,在語義分析階段,其實是可以定義一些規則去做優化的。

編譯器的後端,主要是負責語法樹到目標代碼(平台無關),到平台有關代碼——比如,同一段源代碼生成的x86體系下的可執行程序和MIPS體系下的可執行程序,其運行時結構會有較大的區別,這種區別會體現在目標代碼上。如果一步到位由語法樹轉換為目標代碼,就需要為每種CPU去寫一套完全獨立的後端。為了避免這種情況以及便於優化,於是在語法樹和包含機器特徵的目標代碼之間建立了一箇中間結構,這樣就能更加方便地將語法樹轉換為適合不同CPU的目標代碼,這是設計中間結構的最初目的。高端gimple、低端gimple、cfg、ssa、RTL(Register Transfer Language)就是這樣的中間結構。這塊沒有什麼實際的業務場景可以接觸,所以就沒有深入去看了。

3.小結

業餘開發這款插件,的確花了我很多時間。現在想來還是很值得的——在這裏面學到了很多,而且還把自己想做的東西做出來了。後續迭代中,有新的學習筆記或感悟,我也會整理上來,分享給大家。

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.