深刻編譯器——第一部分:詞法解析和Scanner(介紹ECMAScript的詞法規範和TypeScript scanner)

1. 文章的內容和主題

我對編譯器的深刻了解起源於 一條推特中的問題:Angular是如何用 Angular預先編譯器(AOT)對靜態代碼進行解析工做的。在進行一些debugging後,我發現AOT很是依賴TypeScript編譯器,因此我開始對它進行反編譯(reverse-engineer)。有趣的是,大部分編譯器都使用同樣的規則,這些規則被普遍的認爲是編譯器理論。在理解編譯器的內部機制時,對這些理論一窺到底是很是有必要的。
接下來我將描述對每一個編譯器的第一階段都很是重要的詞法分析
這篇文章儘可能少的參入理論和教條主義,不過大部分依然是理論性的。在最後一章,我將展現TypeScript scanner是如何工做的並提供相關的連接。

TypeScript 語法是基於ECMAScript 規範的,我但願讀者們可以保持足夠的好奇心查看文章中的連接,而且熟練掌握這些規範。 若是你能作到這些,你就會知道這些語法,而且在JavaScript的新特新被寫入MDN以前就學習到了。若是你讀完了這篇文章,能夠經過理解裝飾器(decorator)規範裏描述的裝飾器的語法特性來測試本身。
這篇文章比較長,所以你不須要一次性所有讀完。一點一點的讀這篇文章,有足夠的時間記住文章裏的內容。若是你一直想知道ECMAScript 規範或者想弄清楚編譯器是如何工做的,那就開始讀這篇文章吧!前端

2.編譯器編譯過程當中的幾個階段

編譯器就是把一個用一種編程語言寫成的程序編譯成另外一種語言的電腦程序。編譯器首先須要理解原來的輸入的編程語言 ,而後把它編譯成目標語言。因爲這兩種不一樣的特性,須要把編譯器的功能分紅兩大塊:前端(a front-end)和後端(a back-end.)。前段處理輸入源程序,後端處理輸出目標代碼。git

編譯器能夠當作是一個由多個階段構成的流水線結構,上一步的結果輸入到下一步,而後下一步再優化代碼而且轉化成這一步的須要的代碼,最後又傳給下一步。前端包括三個主要的階段就是詞法分析,語法分析和語義分析。
  • 詞法分析對構成源程序的字符流進行掃描而後根據構詞規則識別單詞(也稱單詞符號或符號)。
  • 語法分析是編譯過程的一個邏輯階段。語法分析的任務是在詞法分析的基礎上將單詞序列組合成各種語法短語,並生成抽象語法書(AST).語法分析程序判斷源程序在結構上是否正確。
  • 語義分析是編譯過程的一個邏輯階段. 語義分析的任務是對結構上正確的源程序進行上下文有關性質的審查, 進行類型審查,審查抽象語法樹是否符合該編程語言的規則。

這篇文章主要目的在於介紹詞法分析。github

3. 形式語言的語法

在咱們開始談詞法分析以前,咱們須要聊一點天然語言和形式語言(Formal language
是用精確的數學或機器可處理的公式定義的語言)和他們的語法。像英語和法語這樣的天然語言一般用於平常交流,並且天然發展而來的。形式語言,一方面。是由人類設計用來特殊的用途的——好比編程語言用來表示計算機的語言,數學符號表示數字之間的關係等等。
不管是天然語言仍是形式語言均可以用語法來描述。語法指該語言中的句子、短語、詞彙的邏輯、結構特徵以及構成方式,而語法包括對語法規律進行的總結描述或對語言使用的規範或限定。天然語言的語法是很是複雜的,並經過經驗主義的方式來研究的。另外一方面,形式語言一般都是簡單的,並根據咱們的需求定義的。取決於咱們能夠經過怎樣的方式分辨幾種語法來定義規則。

詞法描述了一種語言的詞彙結構,就是語言中每一個單詞(符號)。好比,\d都是JavaScript 的字母,可是語法並無定義在正常語句中\後面跟d的規則,因此當你執行\d的代碼的時候,咱們會獲得無效符號的語法錯誤:express

\d
Uncaught SyntaxError: Invalid or unexpected token

語法定義了語句的結構,就是單詞符號在一條語句中組合方式。例如,JavaScript詞法定義的 varconst,在語法中沒有var後面跟着const,全部當下面這樣使用時就會出現語法錯誤:編程

var const
Uncaught SyntaxError: Unexpected token const

上面的結構根據ECMAScript語法規範是無效的,因此編譯器並不會識別var後面跟着const這樣的語句。後端

3. 詞法分析

詞法分析是編譯器在處理源代碼時三個階段中的第一個階段。詞法分析的做用就是把源代碼分解成被稱爲是標記(token)的子字符串,而且對每一個標記進行分類,進行詞法分析的程序或者函數叫做詞法分析器(lexical
analyzer,簡稱lexer),也叫掃描器(scanner)。它們讀取輸入字符流,按照詞法生成標記,這個過程叫作標記化(tokenization)。若是一組字符串沒有匹配的規則掃描器就會報錯。這就是咱們例子中 \d出現報錯的緣由。
掃描器對每個被識別的標記都會按語法分配一個語句範疇(syntactic category)。這個範疇或者說ECMAScript的標記種類很是普遍,包括但不限於識別碼(Identifier),數字文字(NumericLiteral),字符串文字(StringLiteral )和各類不一樣的像 constletif這樣的關鍵字。

因此詞法分析階段的輸出一般是由帶有對應類型的標記和帶有詞位的子字符串組成的隊列:編程語言

{class: SyntaxKind.ConstKeyword, lexeme: ‘const’}
若是你對ECMAScript 定義的標記類型的感興趣,能夠查看 SyntaxKind的列舉。

詞法分析器能夠掃描整個源代碼而後輸出完整的標記隊列,或者緩慢的掃描一次輸出一個標記。掃描器把在解析前將整個源代碼轉化成標記序列而消耗沒必要要的內存是不常見的。因此掃描器只有在代碼須要被解析時才工做,TypeScript 掃描器也同樣。TS掃描器在另外一方面也很是有趣。JavaScript 語法只定義了一些語言結構,如經常使用表達和模板文字,這將致使解析的歧義,因此須要掃描器根據解析上下文來識別不一樣的字符集。
因爲解析上下文是由解析器定義的,當請求一個標記時,TS掃描器能夠被稱爲解析驅動。我會在多個目標符號部分詳解這個複雜的問題。ide

4.定義標記

咱們用JavaScript在定義一個變量這個例子來演示語法規則是如何工做的。在JavaScript中,咱們能夠像下面這樣用const來定義一個變量:函數

const v = 3

咱們簡單的假設初始值是一個數字。當你看這段代碼時,能夠清楚的看到const定義了一個變量v,用=給這個變量分配了一個數字3的初始值。
顯然。掃描器並非這樣工做的。因爲ECMAScript 用Unicode 符號定義了程序碼,因此編譯中的這段代碼看起來是這樣的:佈局

c   o    n    s    t        v        =       3
99, 111, 110, 115, 116, 32, 118, 32, 61, 32, 51

Now its job is to split the expression into tokens and categorize them so the following list of tokens is produced:

如今編譯器的工做就是對這段表達式分割成標記,而且對它們進行分類,而後就生成了下面的這組符號:

{class: SyntaxKind.ConstKeyword, lexeme: 'const'}
{class: SyntaxKind.Identifier, lexeme: 'v'}
{class: SyntaxKind.EqualsToken, lexeme: '='}
{class: SyntaxKind.NumericLiteral, lexeme: '3'}

若是用let替代const第一個標記應爲SyntaxKind.LetKeyword

5.常規語法

ECMAScript 就是解析用Unicode 的符號做爲標記的規則的正常語法。根據Chomsky對語法的分類,常規語法是最受約束的而且最缺少表達能力的語法。它僅適合於描述標記是如何被組合的,但不能描述句子的結構。然而,一個語法規則越不自由越容易描述和解析。由於咱們如此關心定義和解析標記,因此這是一個理想的語法。
這個系列的下一篇文章咱們將會了解上下文無關文法(context-free grammar)。這類語法容許遞歸的結構,而且用來定義程序的結構。
值得注意的是,不少教育資料在解釋掃描器並不用常規語法,而是用常規表達定義定義常規規範。可是,因爲ECMAScript 用了常規語法,我會在這篇文章中解釋它。

6.瞭解這個語法

Now, let’s try to see how we can construct the grammar and the rules that help TypeScript identify the list of tokens I showed above. Here it is again and we need to define rules for recognizing each token in the statement:
如今,讓咱們嘗試看看咱們怎樣構建語法和規則來幫助TypeScript我在上面列出的標記。下面又是咱們須要在表達式中識別的每個符號:

const v = 3
{class: SyntaxKind.ConstKeyword, lexeme: 'const'}
{class: SyntaxKind.Identifier, lexeme: 'v'}
{class: SyntaxKind.EqualsToken, lexeme: '='}
{class: SyntaxKind.NumericLiteral, lexeme: '3'}

語法中的每一項規則是用生產方式來定義的。生產方式是能夠遞歸生成新的符號序列的替代規則。在JavaScript 中咱們能夠用const或者let來聲明一個變量,因而咱們能夠用關鍵字符號定義下面的規則:

Keyword ::
    const
    let

這個關鍵字符號的規則有兩個結果,這兩個結果表示符號關鍵字能夠是let或者const字符串。合成變量的關鍵字被稱做非終結的,意味着他有結果而且能夠被替代。這個替代性一般被認爲能被分解成更小的單位。const和let所產生的結果被稱爲終結符,不能被分解成更小的單位。沒有結果的終端符號在源碼中找到。非終結符是能夠被取代的符號。一個形式文法中必須有一個起始符號;這個起始符號屬於非終結符的集合。ECMAScript定義了許多其餘的非終結符關鍵字例如:if, else, for, do, while, function, class等等。

能夠用下面的任意佈局來定義ECMAScript語法:
non_terminal_symbol ::
  symbol1 symbol2  (production rule 1, Symbol1 followed by Symbol2)
  symbol3 symbol4  (production rule 2, Symbol3 followed by Symbol4)

::左邊的稱做左邊部分,右邊的稱爲右邊部分。對於常規的和上下文無關語法非終結符只能在左邊,右邊能夠是終結符也能夠是非終結符
然而對於常規語法,只能是下面的一種:

  • 只有終結字符的
  • 或者有終結字符和單個非終結字符,而且終結字符在開始或者結尾。
non_terminal_symbol ::
  terminal_symbol
non_terminal_symbol ::
  terminal_symbol non_terminal_symbol   (right-linear)
non_terminal_symbol ::
  non_terminal_symbol terminal_symbol   (left-linear)

上下文無關語法更加寬鬆,容許任意數量的終結字符和非終結字符在右邊。常規語法和上下文無關語法均可以有任意數量的符號在左邊:

non_terminal_symbol ::
  production rule 1
  production rule 2
  ...
  production rule n
相關文章
相關標籤/搜索