用 JavaScript 写一个超小型编译器

前几天看到 Github 上一个非常好的编译器 Demo:

thejameskyle/the-super-tiny-compiler: Possibly the smallest compiler ever

虽然是一个很小很小的并没有什么卵用的编译器,但可以向我们展示编译器的很多东西。

昨天和今天有空, 把它翻译了出来 ,如果可以的话,建议直接去 这里 看代码,Github上的阅读体验更好:

点我!!!

当然也可以看下面,不过知乎编辑器对于代码的支持真是蛋疼。。。 用客户端APP的同学请使用『浏览器打开』。

 
  1. /**

  2. * 今天让我们来写一个编译器,一个超级无敌小的编译器!它小到如果把所有注释删去的话,大概只剩

  3. * 200行左右的代码。

  4. *

  5. * 我们将会用它将 lisp 风格的函数调用转换为 C 风格。

  6. *

  7. * 如果你对这两种风格不是很熟悉,下面是一个简单的介绍。

  8. *

  9. * 假设我们有两个函数,`add` 和 `subtract`,那么它们的写法将会是下面这样:

  10. *

  11. * LISP C

  12. *

  13. * 2 + 2 (add 2 2) add(2, 2)

  14. * 4 - 2 (subtract 4 2) subtract(4, 2)

  15. * 2 + (4 - 2) (add 2 (subtract 4 2)) add(2, subtract(4, 2))

  16. *

  17. * 很简单对吧?

  18. *

  19. * 这个转换就是我们将要做的事情。虽然这并不包含 LISP 或者 C 的全部语法,但它足以向我们

  20. * 展示现代编译器很多要点。

  21. *

  22. */

  23.  
  24. /**

  25. * 大多数编译器可以分成三个阶段:解析(Parsing),转换(Transformation)以及代码

  26. * 生成(Code Generation)

  27. *

  28. * 1. *解析*是将最初原始的代码转换为一种更加抽象的表示(译者注:即AST)。*

  29. *

  30. * 2. *转换*将对这个抽象的表示做一些处理,让它能做到编译器期望

  31. * 它做到的事情。

  32. *

  33. * 3. *代码生成*接收处理之后的代码表示,然后把它转换成新的代码。

  34. */

  35.  
  36. /**

  37. * 解析(Parsing)

  38. * -------

  39. *

  40. * 解析一般来说会分成两个阶段:词法分析(Lexical Analysis)和语法分析(Syntactic Analysis)。

  41. *

  42. * 1. *词法分析*接收原始代码,然后把它分割成一些被称为 Token 的东西,这个过程是在词法分析

  43. * 器(Tokenizer或者Lexer)中完成的。

  44. *

  45. * Token 是一个数组,由一些代码语句的碎片组成。它们可以是数字、标签、标点符号、运算符,

  46. * 或者其它任何东西。

  47. *

  48. * 2. *语法分析* 接收之前生成的 Token,把它们转换成一种抽象的表示,这种抽象的表示描述了代

  49. * 码语句中的每一个片段以及它们之间的关系。这被称为中间表示(intermediate representation)

  50. * 或抽象语法树(Abstract Syntax Tree, 缩写为AST)

  51. *

  52. * 抽象语法树是一个嵌套程度很深的对象,用一种更容易处理的方式代表了代码本身,也能给我们

  53. * 更多信息。

  54. *

  55. * 比如说对于下面这一行代码语句:

  56. *

  57. * (add 2 (subtract 4 2))

  58. *

  59. * 它产生的 Token 看起来或许是这样的:

  60. *

  61. * [

  62. * { type: 'paren', value: '(' },

  63. * { type: 'name', value: 'add' },

  64. * { type: 'number', value: '2' },

  65. * { type: 'paren', value: '(' },

  66. * { type: 'name', value: 'subtract' },

  67. * { type: 'number', value: '4' },

  68. * { type: 'number', value: '2' },

  69. * { type: 'paren', value: ')' },

  70. * { type: 'paren', value: ')' }

  71. * ]

  72. *

  73. * 它的抽象语法树(AST)看起来或许是这样的:

  74. *

  75. * {

  76. * type: 'Program',

  77. * body: [{

  78. * type: 'CallExpression',

  79. * name: 'add',

  80. * params: [{

  81. * type: 'NumberLiteral',

  82. * value: '2'

  83. * }, {

  84. * type: 'CallExpression',

  85. * name: 'subtract',

  86. * params: [{

  87. * type: 'NumberLiteral',

  88. * value: '4'

  89. * }, {

  90. * type: 'NumberLiteral',

  91. * value: '2'

  92. * }]

  93. * }]

  94. * }]

  95. * }

  96. */

  97.  
  98. /**

  99. * 转换(Transformation)

  100. * --------------

  101. *

  102. * 编译器的下一步就是转换。它只是把 AST 拿过来然后对它做一些修改。它可以在同种语言下操

  103. * 作 AST,也可以把 AST 翻译成全新的语言。

  104. *

  105. * 下面我们来看看该如何转换 AST。

  106. *

  107. * 你或许注意到了我们的 AST 中有很多相似的元素,这些元素都有 type 属性,它们被称为 AST

  108. * 结点。这些结点含有若干属性,可以用于描述 AST 的部分信息。

  109. *

  110. * 比如下面是一个“NumberLiteral”结点:

  111. *

  112. * {

  113. * type: 'NumberLiteral',

  114. * value: '2'

  115. * }

  116. *

  117. * 又比如下面是一个“CallExpression”结点:

  118. *

  119. * {

  120. * type: 'CallExpression',

  121. * name: 'subtract',

  122. * params: [...nested nodes go here...]

  123. * }

  124. *

  125. * 当转换 AST 的时候我们可以添加、移动、替代这些结点,也可以根据现有的 AST 生成一个全新

  126. * 的 AST

  127. *

  128. * 既然我们编译器的目标是把输入的代码转换为一种新的语言,所以我们将会着重于产生一个针对

  129. * 新语言的全新的 AST。

  130. *

  131. *

  132. * 遍历(Traversal)

  133. * ---------

  134. *

  135. * 为了能处理所有的结点,我们需要遍历它们,使用的是深度优先遍历。

  136. *

  137. * {

  138. * type: 'Program',

  139. * body: [{

  140. * type: 'CallExpression',

  141. * name: 'add',

  142. * params: [{

  143. * type: 'NumberLiteral',

  144. * value: '2'

  145. * }, {

  146. * type: 'CallExpression',

  147. * name: 'subtract',

  148. * params: [{

  149. * type: 'NumberLiteral',

  150. * value: '4'

  151. * }, {

  152. * type: 'NumberLiteral',

  153. * value: '2'

  154. * }]

  155. * }]

  156. * }]

  157. * }

  158. *

  159. * So for the above AST we would go:

  160. * 对于上面的 AST 的遍历流程是这样的:

  161. *

  162. * 1. Program - 从 AST 的顶部结点开始

  163. * 2. CallExpression (add) - Program 的第一个子元素

  164. * 3. NumberLiteral (2) - CallExpression (add) 的第一个子元素

  165. * 4. CallExpression (subtract) - CallExpression (add) 的第二个子元素

  166. * 5. NumberLiteral (4) - CallExpression (subtract) 的第一个子元素

  167. * 6. NumberLiteral (4) - CallExpression (subtract) 的第二个子元素

  168. *

  169. * 如果我们直接在 AST 内部操作,而不是产生一个新的 AST,那么就要在这里介绍所有种类的抽象,

  170. * 但是目前访问(visiting)所有结点的方法已经足够了。

  171. *

  172. * 使用“访问(visiting)”这个词的是因为这是一种模式,代表在对象结构内对元素进行操作。

  173. *

  174. * 访问者(Visitors)

  175. * --------

  176. *

  177. * 我们最基础的想法是创建一个“访问者(visitor)”对象,这个对象中包含一些方法,可以接收不

  178. * 同的结点。

  179. *

  180. * var visitor = {

  181. * NumberLiteral() {},

  182. * CallExpression() {}

  183. * };

  184. *

  185. * 当我们遍历 AST 的时候,如果遇到了匹配 type 的结点,我们可以调用 visitor 中的方法。

  186. *

  187. * 一般情况下为了让这些方法可用性更好,我们会把父结点也作为参数传入。

  188. */

  189.  
  190. /**

  191. * 代码生成(Code Generation)

  192. * ---------------

  193. *

  194. * 编译器的最后一个阶段是代码生成,这个阶段做的事情有时候会和转换(transformation)重叠,

  195. * 但是代码生成最主要的部分还是根据 AST 来输出代码。

  196. *

  197. * 代码生成有几种不同的工作方式,有些编译器将会重用之前生成的 token,有些会创建独立的代码

  198. * 表示,以便于线性地输出代码。但是接下来我们还是着重于使用之前生成好的 AST。

  199. *

  200. * 我们的代码生成器需要知道如何“打印”AST 中所有类型的结点,然后它会递归地调用自身,直到所

  201. * 有代码都被打印到一个很长的字符串中。

  202. *

  203. */

  204.  
  205. /**

  206. * 好了!这就是编译器中所有的部分了。

  207. *

  208. * 当然不是说所有的编译器都像我说的这样。不同的编译器有不同的目的,所以也可能需要不同的步骤。

  209. *

  210. * 但你现在应该对编译器到底是个什么东西有个大概的认识了。

  211. *

  212. * 既然我全都解释一遍了,你应该能写一个属于自己的编译器了吧?

  213. *

  214. * 哈哈开个玩笑,接下来才是重点 :P

  215. *

  216. * 所以我们开始吧...

  217. */

  218.  
  219. /**

  220. * =======================================================================

  221. * (/^▽^)/

  222. * 词法分析器(Tokenizer)!

  223. * =======================================================================

  224. */

  225.  
  226. /**

  227. * 我们从第一个阶段开始,即词法分析,使用的是词法分析器(Tokenizer)。

  228. *

  229. * 我们只是接收代码组成的字符串,然后把它们分割成 token 组成的数组。

  230. *

  231. * (add 2 (subtract 4 2)) => [{ type: 'paren', value: '(' }, ...]

  232. */

  233.  
  234. // 我们从接收一个字符串开始,首先设置两个变量。

  235. function tokenizer(input) {

  236.  
  237. // `current`变量类似指针,用于记录我们在代码字符串中的位置。

  238. var current = 0;

  239.  
  240. // `tokens`数组是我们放置 token 的地方

  241. var tokens = [];

  242.  
  243. // 首先我们创建一个 `while` 循环, `current` 变量会在循环中自增。

  244. //

  245. // 我们这么做的原因是,由于 token 数组的长度是任意的,所以可能要在单个循环中多次

  246. // 增加 `current`

  247. while (current < input.length) {

  248.  
  249. // 我们在这里储存了 `input` 中的当前字符

  250. var char = input[current];

  251.  
  252. // 要做的第一件事情就是检查是不是右圆括号。这在之后将会用在 `CallExpressions` 中,

  253. // 但是现在我们关心的只是字符本身。

  254. //

  255. // 检查一下是不是一个左圆括号。

  256. if (char === '(') {

  257.  
  258. // 如果是,那么我们 push 一个 type 为 `paren`,value 为左圆括号的对象。

  259. tokens.push({

  260. type: 'paren',

  261. value: '('

  262. });

  263.  
  264. // 自增 `current`

  265. current++;

  266.  
  267. // 结束本次循环,进入下一次循环

  268. continue;

  269. }

  270.  
  271. // 然后我们检查是不是一个右圆括号。这里做的时候和之前一样:检查右圆括号、加入新的 token、

  272. // 自增 `current`,然后进入下一次循环。

  273. if (char === ')') {

  274. tokens.push({

  275. type: 'paren',

  276. value: ')'

  277. });

  278. current++;

  279. continue;

  280. }

  281.  
  282. // 继续,我们现在检查是不是空格。有趣的是,我们想要空格的本意是分隔字符,但这现在

  283. // 对于我们储存 token 来说不那么重要。我们暂且搁置它。

  284. //

  285. // 所以我们只是简单地检查是不是空格,如果是,那么我们直接进入下一个循环。

  286. var WHITESPACE = /\s/;

  287. if (WHITESPACE.test(char)) {

  288. current++;

  289. continue;

  290. }

  291.  
  292. // 下一个 token 的类型是数字。它和之前的 token 不同,因为数字可以由多个数字字符组成,

  293. // 但是我们只能把它们识别为一个 token。

  294. //

  295. // (add 123 456)

  296. // ^^^ ^^^

  297. // Only two separate tokens

  298. // 这里只有两个 token

  299. //

  300. // 当我们遇到一个数字字符时,将会从这里开始。

  301. var NUMBERS = /[0-9]/;

  302. if (NUMBERS.test(char)) {

  303.  
  304. // 创建一个 `value` 字符串,用于 push 字符。

  305. var value = '';

  306.  
  307. // 然后我们循环遍历接下来的字符,直到我们遇到的字符不再是数字字符为止,把遇到的每

  308. // 一个数字字符 push 进 `value` 中,然后自增 `current`。

  309. while (NUMBERS.test(char)) {

  310. value += char;

  311. char = input[++current];

  312. }

  313.  
  314. // 然后我们把类型为 `number` 的 token 放入 `tokens` 数组中。

  315. tokens.push({

  316. type: 'number',

  317. value: value

  318. });

  319.  
  320. // 进入下一次循环。

  321. continue;

  322. }

  323.  
  324. // 最后一种类型的 token 是 `name`。它由一系列的字母组成,这在我们的 lisp 语法中

  325. // 代表了函数。

  326. //

  327. // (add 2 4)

  328. // ^^^

  329. // Name token

  330. //

  331. var LETTERS = /[a-z]/i;

  332. if (LETTERS.test(char)) {

  333. var value = '';

  334.  
  335. // 同样,我们用一个循环遍历所有的字母,把它们存入 value 中。

  336. while (LETTERS.test(char)) {

  337. value += char;

  338. char = input[++current];

  339. }

  340.  
  341. // 然后添加一个类型为 `name` 的 token,然后进入下一次循环。

  342. tokens.push({

  343. type: 'name',

  344. value: value

  345. });

  346.  
  347. continue;

  348. }

  349.  
  350. // 最后如果我们没有匹配上任何类型的 token,那么我们抛出一个错误。

  351. throw new TypeError('I dont know what this character is: ' + char);

  352. }

  353.  
  354. // 词法分析器的最后我们返回 tokens 数组。

  355. return tokens;

  356. }

  357.  
  358. /**

  359. * =======================================================================

  360. * ヽ/❀o ͜ o\ノ

  361. * 语法分析器(Parser)!!!

  362. * =======================================================================

  363. */

  364.  
  365. /**

  366. * 语法分析器接受 token 数组,然后把它转化为 AST

  367. *

  368. * [{ type: 'paren', value: '(' }, ...] => { type: 'Program', body: [...] }

  369. */

  370.  
  371. // 现在我们定义 parser 函数,接受 `tokens` 数组

  372. function parser(tokens) {

  373.  
  374. // 我们再次声明一个 `current` 变量作为指针。

  375. var current = 0;

  376.  
  377. // 但是这次我们使用递归而不是 `while` 循环,所以我们定义一个 `walk` 函数。

  378. function walk() {

  379.  
  380. // walk函数里,我们从当前token开始

  381. var token = tokens[current];

  382.  
  383. // 对于不同类型的结点,对应的处理方法也不同,我们从 `number` 类型的 token 开始。

  384. // 检查是不是 `number` 类型

  385. if (token.type === 'number') {

  386.  
  387. // 如果是,`current` 自增。

  388. current++;

  389.  
  390. // 然后我们会返回一个新的 AST 结点 `NumberLiteral`,并且把它的值设为 token 的值。

  391. return {

  392. type: 'NumberLiteral',

  393. value: token.value

  394. };

  395. }

  396.  
  397. // 接下来我们检查是不是 CallExpressions 类型,我们从左圆括号开始。

  398. if (

  399. token.type === 'paren' &&

  400. token.value === '('

  401. ) {

  402.  
  403. // 我们会自增 `current` 来跳过这个括号,因为括号在 AST 中是不重要的。

  404. token = tokens[++current];

  405.  
  406. // 我们创建一个类型为 `CallExpression` 的根节点,然后把它的 name 属性设置为当前

  407. // token 的值,因为紧跟在左圆括号后面的 token 一定是调用的函数的名字。

  408. var node = {

  409. type: 'CallExpression',

  410. name: token.value,

  411. params: []

  412. };

  413.  
  414. // 我们再次自增 `current` 变量,跳过当前的 token

  415. token = tokens[++current];

  416.  
  417. // 现在我们循环遍历接下来的每一个 token,直到我们遇到右圆括号,这些 token 将会

  418. // 是 `CallExpression` 的 `params`(参数)

  419. //

  420. // 这也是递归开始的地方,我们采用递归的方式来解决问题,而不是去尝试解析一个可能有无限

  421. // 层嵌套的结点。

  422. //

  423. // 为了更好地解释,我们来看看我们的 Lisp 代码。你会注意到 `add` 函数的参数有两个,

  424. // 一个是数字,另一个是一个嵌套的 `CallExpression`,这个 `CallExpression` 中

  425. // 包含了它自己的参数(两个数字)

  426. //

  427. // (add 2 (subtract 4 2))

  428. //

  429. // 你也会注意到我们的 token 数组中有多个右圆括号。

  430. //

  431. // [

  432. // { type: 'paren', value: '(' },

  433. // { type: 'name', value: 'add' },

  434. // { type: 'number', value: '2' },

  435. // { type: 'paren', value: '(' },

  436. // { type: 'name', value: 'subtract' },

  437. // { type: 'number', value: '4' },

  438. // { type: 'number', value: '2' },

  439. // { type: 'paren', value: ')' }, <<< 右圆括号

  440. // { type: 'paren', value: ')' } <<< 右圆括号

  441. // ]

  442. //

  443. // 遇到嵌套的 `CallExpressions` 时,我们将会依赖嵌套的 `walk` 函数来

  444. // 增加 `current` 变量

  445. //

  446. // 所以我们创建一个 `while` 循环,直到遇到类型为 `'paren'`,值为右圆括号的 token。

  447. while (

  448. (token.type !== 'paren') ||

  449. (token.type === 'paren' && token.value !== ')')

  450. ) {

  451. // 我们调用 `walk` 函数,它将会返回一个结点,然后我们把这个节点

  452. // 放入 `node.params` 中。

  453. node.params.push(walk());

  454. token = tokens[current];

  455. }

  456.  
  457. // 我们最后一次增加 `current`,跳过右圆括号。

  458. current++;

  459.  
  460. // 返回结点。

  461. return node;

  462. }

  463.  
  464. // 同样,如果我们遇到了一个类型未知的结点,就抛出一个错误。

  465. throw new TypeError(token.type);

  466. }

  467.  
  468. // 现在,我们创建 AST,根结点是一个类型为 `Program` 的结点。

  469. var ast = {

  470. type: 'Program',

  471. body: []

  472. };

  473.  
  474. // 现在我们开始 `walk` 函数,把结点放入 `ast.body` 中。

  475. //

  476. // 之所以在一个循环中处理,是因为我们的程序可能在 `CallExpressions` 后面包含连续的两个

  477. // 参数,而不是嵌套的。

  478. //

  479. // (add 2 2)

  480. // (subtract 4 2)

  481. //

  482. while (current < tokens.length) {

  483. ast.body.push(walk());

  484. }

  485.  
  486. // 最后我们的语法分析器返回 AST

  487. return ast;

  488. }

  489.  
  490. /**

  491. * =======================================================================

  492. * ⌒(❀>◞౪◟<❀)⌒

  493. * 遍历器!!!

  494. * =======================================================================

  495. */

  496.  
  497. /**

  498. * 现在我们有了 AST,我们需要一个 visitor 去遍历所有的结点。当遇到某个类型的结点时,我们

  499. * 需要调用 visitor 中对应类型的处理函数。

  500. *

  501. * traverse(ast, {

  502. * Program(node, parent) {

  503. * // ...

  504. * },

  505. *

  506. * CallExpression(node, parent) {

  507. * // ...

  508. * },

  509. *

  510. * NumberLiteral(node, parent) {

  511. * // ...

  512. * }

  513. * });

  514. */

  515.  
  516. // 所以我们定义一个遍历器,它有两个参数,AST 和 vistor。在它的里面我们又定义了两个函数...

  517. function traverser(ast, visitor) {

  518.  
  519. // `traverseArray` 函数允许我们对数组中的每一个元素调用 `traverseNode` 函数。

  520. function traverseArray(array, parent) {

  521. array.forEach(function(child) {

  522. traverseNode(child, parent);

  523. });

  524. }

  525.  
  526. // `traverseNode` 函数接受一个 `node` 和它的父结点 `parent` 作为参数,这个结点会被

  527. // 传入到 visitor 中相应的处理函数那里。

  528. function traverseNode(node, parent) {

  529.  
  530. // 首先我们看看 visitor 中有没有对应 `type` 的处理函数。

  531. var method = visitor[node.type];

  532.  
  533. // 如果有,那么我们把 `node` 和 `parent` 都传入其中。

  534. if (method) {

  535. method(node, parent);

  536. }

  537.  
  538. // 下面我们对每一个不同类型的结点分开处理。

  539. switch (node.type) {

  540.  
  541. // 我们从顶层的 `Program` 开始,Program 结点中有一个 body 属性,它是一个由若干

  542. // 个结点组成的数组,所以我们对这个数组调用 `traverseArray`。

  543. //

  544. // (记住 `traverseArray` 会调用 `traverseNode`,所以我们会递归地遍历这棵树。)

  545. case 'Program':

  546. traverseArray(node.body, node);

  547. break;

  548.  
  549. // 下面我们对 `CallExpressions` 做同样的事情,遍历它的 `params`。

  550. case 'CallExpression':

  551. traverseArray(node.params, node);

  552. break;

  553.  
  554. // 如果是 `NumberLiterals`,那么就没有任何子结点了,所以我们直接 break

  555. case 'NumberLiteral':

  556. break;

  557.  
  558. // 同样,如果我们不能识别当前的结点,那么就抛出一个错误。

  559. default:

  560. throw new TypeError(node.type);

  561. }

  562. }

  563.  
  564. // 最后我们对 AST 调用 `traverseNode`,开始遍历。注意 AST 并没有父结点。

  565. traverseNode(ast, null);

  566. }

  567.  
  568. /**

  569. * =======================================================================

  570. * ⁽(◍˃̵͈̑ᴗ˂̵͈̑)⁽

  571. * 转换器!!!

  572. * =======================================================================

  573. */

  574.  
  575. /**

  576. * 下面是转换器。转换器接收我们在之前构建好的 AST,然后把它和 visitor 传递进入我们的遍历

  577. * 器中 ,最后得到一个新的 AST。

  578. *

  579. * ----------------------------------------------------------------------------

  580. * 原始的 AST | 转换后的 AST

  581. * ----------------------------------------------------------------------------

  582. * { | {

  583. * type: 'Program', | type: 'Program',

  584. * body: [{ | body: [{

  585. * type: 'CallExpression', | type: 'ExpressionStatement',

  586. * name: 'add', | expression: {

  587. * params: [{ | type: 'CallExpression',

  588. * type: 'NumberLiteral', | callee: {

  589. * value: '2' | type: 'Identifier',

  590. * }, { | name: 'add'

  591. * type: 'CallExpression', | },

  592. * name: 'subtract', | arguments: [{

  593. * params: [{ | type: 'NumberLiteral',

  594. * type: 'NumberLiteral', | value: '2'

  595. * value: '4' | }, {

  596. * }, { | type: 'CallExpression',

  597. * type: 'NumberLiteral', | callee: {

  598. * value: '2' | type: 'Identifier',

  599. * }] | name: 'subtract'

  600. * }] | },

  601. * }] | arguments: [{

  602. * } | type: 'NumberLiteral',

  603. * | value: '4'

  604. * ---------------------------------- | }, {

  605. * | type: 'NumberLiteral',

  606. * | value: '2'

  607. * | }]

  608. * (那一边比较长/w\) | }]

  609. * | }

  610. * | }]

  611. * | }

  612. * ----------------------------------------------------------------------------

  613. */

  614.  
  615. // 定义我们的转换器函数,接收 AST 作为参数

  616. function transformer(ast) {

  617.  
  618. // 创建 `newAST`,它与我们之前的 AST 类似,有一个类型为 Program 的根节点。

  619. var newAst = {

  620. type: 'Program',

  621. body: []

  622. };

  623.  
  624. // 下面的代码会有些奇技淫巧,我们在父结点上使用一个属性 `context`(上下文),这样我们就

  625. // 可以把结点放入他们父结点的 context 中。当然可能会有更好的做法,但是为了简单我们姑且

  626. // 这么做吧。

  627. //

  628. // 注意 context 是一个*引用*,从旧的 AST 到新的 AST。

  629. ast._context = newAst.body;

  630.  
  631. // 我们把 AST 和 visitor 函数传入遍历器

  632. traverser(ast, {

  633.  
  634. // 第一个 visitor 方法接收 `NumberLiterals`。

  635. NumberLiteral: function(node, parent) {

  636.  
  637. // 我们创建一个新结点,名字叫 `NumberLiteral`,并把它放入父结点的 context 中。

  638. parent._context.push({

  639. type: 'NumberLiteral',

  640. value: node.value

  641. });

  642. },

  643.  
  644. // 下一个,`CallExpressions`。

  645. CallExpression: function(node, parent) {

  646.  
  647. // 我们创建一个 `CallExpression` 结点,里面有一个嵌套的 `Identifier`。

  648. var expression = {

  649. type: 'CallExpression',

  650. callee: {

  651. type: 'Identifier',

  652. name: node.name

  653. },

  654. arguments: []

  655. };

  656.  
  657. // 下面我们在原来的 `CallExpression` 结点上定义一个新的 context,它是 expression

  658. // 中 arguments 这个数组的引用,我们可以向其中放入参数。

  659. node._context = expression.arguments;

  660.  
  661. // 然后来看看父结点是不是一个 `CallExpression`,如果不是...

  662. if (parent.type !== 'CallExpression') {

  663.  
  664. // 我们把 `CallExpression` 结点包在一个 `ExpressionStatement` 中,这么做是因为

  665. // 单独存在(原文为top level)的 `CallExpressions` 在 JavaScript 中也可以被当做

  666. // 是声明语句。

  667. //

  668. // 译者注:比如 `var a = foo()` 与 `foo()`,后者既可以当作表达式给某个变量赋值,也

  669. // 可以作为一个独立的语句存在。

  670. expression = {

  671. type: 'ExpressionStatement',

  672. expression: expression

  673. };

  674. }

  675.  
  676. // 最后我们把 `CallExpression`(可能是被包起来的) 放入父结点的 context 中。

  677. parent._context.push(expression);

  678. }

  679. });

  680.  
  681. // 最后返回创建好的新 AST。

  682. return newAst;

  683. }

  684.  
  685. /**

  686. * =======================================================================

  687. * ヾ(〃^∇^)ノ♪

  688. * 代码生成器!!!!

  689. * =======================================================================

  690. */

  691.  
  692. /**

  693. * 现在只剩最后一步啦:代码生成器。

  694. *

  695. * 我们的代码生成器会递归地调用它自己,把 AST 中的每个结点打印到一个很大的字符串中。

  696. */

  697.  
  698. function codeGenerator(node) {

  699.  
  700. // 对于不同 `type` 的结点分开处理。

  701. switch (node.type) {

  702.  
  703. // 如果是 `Program` 结点,那么我们会遍历它的 `body` 属性中的每一个结点,并且递归地

  704. // 对这些结点再次调用 codeGenerator,再把结果打印进入新的一行中。

  705. case 'Program':

  706. return node.body.map(codeGenerator)

  707. .join('\n');

  708.  
  709. // 对于 `ExpressionStatements`,我们对它的 expression 属性递归调用,同时加入一个

  710. // 分号。

  711. case 'ExpressionStatement':

  712. return (

  713. codeGenerator(node.expression) +

  714. ';' // << (...因为我们喜欢用*正确*的方式写代码)

  715. );

  716.  
  717. // 对于 `CallExpressions`,我们会打印出 `callee`,接着是一个左圆括号,然后对

  718. // arguments 递归调用 codeGenerator,并且在它们之间加一个逗号,最后加上右圆括号。

  719. case 'CallExpression':

  720. return (

  721. codeGenerator(node.callee) +

  722. '(' +

  723. node.arguments.map(codeGenerator)

  724. .join(', ') +

  725. ')'

  726. );

  727.  
  728. // 对于 `Identifiers` 我们只是返回 `node` 的 name。

  729. case 'Identifier':

  730. return node.name;

  731.  
  732. // 对于 `NumberLiterals` 我们只是返回 `node` 的 value

  733. case 'NumberLiteral':

  734. return node.value;

  735.  
  736. // 如果我们不能识别这个结点,那么抛出一个错误。

  737. default:

  738. throw new TypeError(node.type);

  739. }

  740. }

  741.  
  742. /**

  743. * ============================================================================

  744. * ( * ‘ヮ’) ”

  745. * !!!!!!!!!!!!编译器!!!!!!!!!!!

  746. * ============================================================================

  747. */

  748.  
  749. /**

  750. * 最后!我们创建 `compiler` 函数,它只是把上面说到的那些函数连接到一起。

  751. *

  752. * 1. input => tokenizer => tokens

  753. * 2. tokens => parser => ast

  754. * 3. ast => transformer => newAst

  755. * 4. newAst => generator => output

  756. */

  757.  
  758. function compiler(input) {

  759. var tokens = tokenizer(input);

  760. var ast = parser(tokens);

  761. var newAst = transformer(ast);

  762. var output = codeGenerator(newAst);

  763.  
  764. // 然后返回输出!

  765. return output;

  766. }

  767.  
  768. /**

  769. * =======================================================================

  770. * (๑˃̵ᴗ˂̵)

  771. * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!你做到了!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

  772. * =======================================================================

  773. */

  774.  
  775. // 现在导出所有接口...

  776. module.exports = {

  777. tokenizer: tokenizer,

  778. parser: parser,

  779. transformer: transformer,

  780. codeGenerator: codeGenerator,

  781. compiler: compiler

  782. };

转自https://blog.csdn.net/hj7jay/article/details/52117138

猜你喜欢

转载自blog.csdn.net/qq_31967569/article/details/89510073