我对编译器的深刻了解起源于 一条推特中的问题:Angular是如何用 Angular预先编译器(AOT)对静态代码进行解析工做的。在进行一些debugging后,我发现AOT很是依赖TypeScript编译器,因此我开始对它进行反编译(reverse-engineer)。有趣的是,大部分编译器都使用同样的规则,这些规则被普遍的认为是编译器理论。在理解编译器的内部机制时,对这些理论一窥到底是很是有必要的。
接下来我将描述对每一个编译器的第一阶段都很是重要的词法分析
这篇文章尽可能少的参入理论和教条主义,不过大部分依然是理论性的。在最后一章,我将展现TypeScript scanner是如何工做的并提供相关的连接。
TypeScript 语法是基于ECMAScript 规范的,我但愿读者们可以保持足够的好奇心查看文章中的连接,而且熟练掌握这些规范。 若是你能作到这些,你就会知道这些语法,而且在JavaScript的新特新被写入MDN以前就学习到了。若是你读完了这篇文章,能够经过理解装饰器(decorator)规范里描述的装饰器的语法特性来测试本身。
这篇文章比较长,所以你不须要一次性所有读完。一点一点的读这篇文章,有足够的时间记住文章里的内容。若是你一直想知道ECMAScript 规范或者想弄清楚编译器是如何工做的,那就开始读这篇文章吧!前端
编译器就是把一个用一种编程语言写成的程序编译成另外一种语言的电脑程序。编译器首先须要理解原来的输入的编程语言 ,而后把它编译成目标语言。因为这两种不一样的特性,须要把编译器的功能分红两大块:前端(a front-end)和后端(a back-end.)。前段处理输入源程序,后端处理输出目标代码。git
编译器能够当作是一个由多个阶段构成的流水线结构,上一步的结果输入到下一步,而后下一步再优化代码而且转化成这一步的须要的代码,最后又传给下一步。前端包括三个主要的阶段就是词法分析,语法分析和语义分析。
这篇文章主要目的在于介绍词法分析。github
在咱们开始谈词法分析以前,咱们须要聊一点天然语言和形式语言(Formal language
是用精确的数学或机器可处理的公式定义的语言)和他们的语法。像英语和法语这样的天然语言一般用于平常交流,并且天然发展而来的。形式语言,一方面。是由人类设计用来特殊的用途的——好比编程语言用来表示计算机的语言,数学符号表示数字之间的关系等等。
不管是天然语言仍是形式语言均可以用语法来描述。语法指该语言中的句子、短语、词汇的逻辑、结构特征以及构成方式,而语法包括对语法规律进行的总结描述或对语言使用的规范或限定。天然语言的语法是很是复杂的,并经过经验主义的方式来研究的。另外一方面,形式语言一般都是简单的,并根据咱们的需求定义的。取决于咱们能够经过怎样的方式分辨几种语法来定义规则。
词法描述了一种语言的词汇结构,就是语言中每一个单词(符号)。好比,\
和 d
都是JavaScript 的字母,可是语法并无定义在正常语句中\
后面跟d
的规则,因此当你执行\d
的代码的时候,咱们会获得无效符号的语法错误:express
\d Uncaught SyntaxError: Invalid or unexpected token
语法定义了语句的结构,就是单词符号在一条语句中组合方式。例如,JavaScript词法定义的 var
和const
,在语法中没有var
后面跟着const
,全部当下面这样使用时就会出现语法错误:编程
var const Uncaught SyntaxError: Unexpected token const
上面的结构根据ECMAScript语法规范是无效的,因此编译器并不会识别var
后面跟着const
这样的语句。后端
词法分析是编译器在处理源代码时三个阶段中的第一个阶段。词法分析的做用就是把源代码分解成被称为是标记(token)的子字符串,而且对每一个标记进行分类,进行词法分析的程序或者函数叫做词法分析器(lexical
analyzer,简称lexer),也叫扫描器(scanner)。它们读取输入字符流,按照词法生成标记,这个过程叫作标记化(tokenization)。若是一组字符串没有匹配的规则扫描器就会报错。这就是咱们例子中\d
出现报错的缘由。
扫描器对每个被识别的标记都会按语法分配一个语句范畴(syntactic category)。这个范畴或者说ECMAScript的标记种类很是普遍,包括但不限于识别码(Identifier),数字文字(NumericLiteral),字符串文字(StringLiteral )和各类不一样的像const
、let
、if
这样的关键字。
因此词法分析阶段的输出一般是由带有对应类型的标记和带有词位的子字符串组成的队列:编程语言
{class: SyntaxKind.ConstKeyword, lexeme: ‘const’}
若是你对ECMAScript 定义的标记类型的感兴趣,能够查看 SyntaxKind的列举。
词法分析器能够扫描整个源代码而后输出完整的标记队列,或者缓慢的扫描一次输出一个标记。扫描器把在解析前将整个源代码转化成标记序列而消耗没必要要的内存是不常见的。因此扫描器只有在代码须要被解析时才工做,TypeScript 扫描器也同样。TS扫描器在另外一方面也很是有趣。JavaScript 语法只定义了一些语言结构,如经常使用表达和模板文字,这将致使解析的歧义,因此须要扫描器根据解析上下文来识别不一样的字符集。
因为解析上下文是由解析器定义的,当请求一个标记时,TS扫描器能够被称为解析驱动。我会在多个目标符号部分详解这个复杂的问题。ide
咱们用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。
ECMAScript 就是解析用Unicode 的符号做为标记的规则的正常语法。根据Chomsky对语法的分类,常规语法是最受约束的而且最缺少表达能力的语法。它仅适合于描述标记是如何被组合的,但不能描述句子的结构。然而,一个语法规则越不自由越容易描述和解析。由于咱们如此关心定义和解析标记,因此这是一个理想的语法。
这个系列的下一篇文章咱们将会了解上下文无关文法(context-free grammar)。这类语法容许递归的结构,而且用来定义程序的结构。
值得注意的是,不少教育资料在解释扫描器并不用常规语法,而是用常规表达定义定义常规规范。可是,因为ECMAScript 用了常规语法,我会在这篇文章中解释它。
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