解释「解释解析器」

avatar
@古茗科技

解释解释解析器

希望能用这篇文章解释解析器能把解析器解释清楚,先来段绕口令醒醒神 😬

前言

不知道大家有没有想过一个问题:"我们编写的JS代码只是一些字符串,它是怎么被机器执行的?",下面我们带着这个问题进入文章。

概念

首先,我们编写的JS代码对于机器来说只是一个个字符,机器并不是一开始就认识他们。

运行环境

我们知道,JS可以运行在浏览器环境和node环境,这些环境都内置了JS引擎,我们接触较多的是谷歌开源的V8引擎,除此之外其他常见的引擎比如有:

  • 由 Mozilla 为 Firefox 开发的 SpiderMonkey
  • 为 Safari 浏览器提供支持的 JavaScriptCore

JavaScript定义

JavaScriptJS)是一种具有函数优先特性的轻量级、解释型或者说即时编译型的编程语言

语言类型

从JS定义中看到,提到了解释型、即时编译型语言,其实还有一种类型叫编译型语言

那什么是解析型,什么是编译型,什么是即时编译型?

编译型语言

在程序执行之前,需要经过编译器的编译过程,并且编译之后会直接保留机器能读懂的二进制文件,这样每次运行程序时,都可以直接运行该二进制文件,而不需要再次重新编译了。比如 C/C++、GO 等都是编译型语言。

解释型语言

而由解释型语言编写的程序,在每次运行时都需要通过解释器对程序进行动态解释和执行。比如 Python、JavaScript 等都属于解释型语言。

https://cdn.nlark.com/yuque/0/2024/png/1661749/1723973680251-386cdb87-a3a2-4b76-92d1-5fc32b8371e0.png

即时编译型语言

即时编译(JIT),也称为动态翻译或运行时编译,是一种执行计算机代码的方法,这种方法设计在程序执行过程中(在执行期)而不是在执行之前进行编译;实现 JIT 编译器的系统通常会不断地分析正在执行的代码,并确定代码中可被即时编译加速的部分,在这些部分中,由编译或重新编译带来的性能提高将超过编译该代码的开销。

JIT编译是两种传统的机器代码翻译方法——提前编译和解释器的结合,它结合了两者的优点和缺点。

V8执行JS代码流程

我们可以看到V8 在执行过程中既有解释器Ignition,又有编译器TurboFan

https://cdn.nlark.com/yuque/0/2024/png/1661749/1723973945746-94d1b919-9136-43e6-a18b-03dc1be876c9.png

小结

我们可以发现,不管是什么类型语言的执行第一步都是源代码到AST,进入本篇文章的重点:怎么得到AST,看图上可以得知,首先是对源代码也就是代码字符进行词法分析语法分析

解析器

词法分析

词法分析是计算机科学中将字符序列转换为标记**(token)序列的过程。进行词法分析的程序或者函数叫作词法分析器(lexical analyzer,简称lexer),也叫扫描器(scanner)。

语法分析

在计算机科学和语言学中,语法分析(英语:syntactic analysis,也叫 parsing)是根据某种给定的形式文法对由单词序列(如英语单词序列)构成的输入文本进行分析并确定其语法结构的一种过程。

一个简单的词法解析器例子🌰

伪代码字符串

(add 2 (subtract 4 2))

第一步:词法分析生成token

生成tokens过程是一个字符一个字符的往后遍历分析,处理各种场景;

比如处理上面伪代码字符串中「add」 关键字的场景(代码59-71行):遍历到'a'的时候,会继续往后遍历'd',再遍历'd',直到后一个字符不满足 /[a-z]/i 条件,最后得到'add'为一个token

function tokenizer(input) {
  let current = 0;
  let tokens = [];

  while (current < input.length) {
    let char = input[current];

    if (char === '(') {
      tokens.push({
        type: 'paren',
        value: '(',
      });
      current++;
      continue;
    }

    if (char === ')') {
      tokens.push({
        type: 'paren',
        value: ')',
      });
      current++;
      continue;
    }

    let WHITESPACE = /\s/;
    if (WHITESPACE.test(char)) {
      current++;
      continue;
    }

    let NUMBERS = /[0-9]/;
    if (NUMBERS.test(char)) {
      let value = '';
      while (NUMBERS.test(char)) {
        value += char;
        char = input[++current];
      }
      tokens.push({ type: 'number', value });
      continue;
    }

    if (char === '"') {
      let value = '';
      char = input[++current];

      while (char !== '"') {
        value += char;
        char = input[++current];
      }

      char = input[++current];

      tokens.push({ type: 'string', value });

      continue;
    }

    let LETTERS = /[a-z]/i;
    if (LETTERS.test(char)) {
      let value = '';

      while (LETTERS.test(char)) {
        value += char;
        char = input[++current];
      }

      tokens.push({ type: 'name', value });

      continue;
    }

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

  return tokens;
}
  • 得到tokens如下
[
  { type: 'paren',  value: '('        },
  { type: 'name',   value: 'add'      },
  { type: 'number', value: '2'        },
  { type: 'paren',  value: '('        },
  { type: 'name',   value: 'subtract' },
  { type: 'number', value: '4'        },
  { type: 'number', value: '2'        },
  { type: 'paren',  value: ')'        },
  { type: 'paren',  value: ')'        }
];

第二步:处理tokens,生成AST

function parser(tokens) {
  let current = 0;

  function walk() {
    let token = tokens[current];

    if (token.type === 'number') {
      current++;
      return {
        type: 'NumberLiteral',
        value: token.value,
      };
    }

    if (token.type === 'string') {
      current++;

      return {
        type: 'StringLiteral',
        value: token.value,
      };
    }

    if (
      token.type === 'paren' &&
      token.value === '('
    ) {
      token = tokens[++current];

      let node = {
        type: 'CallExpression',
        name: token.value,
        params: [],
      };

      token = tokens[++current];

      while (
        (token.type !== 'paren') ||
        (token.type === 'paren' && token.value !== ')')
      ) {
        node.params.push(walk());
        token = tokens[current];
      }

      current++;

      return node;
    }

    throw new TypeError(token.type);
  }

  let ast = {
    type: 'Program',
    body: [],
  };

  while (current < tokens.length) {
    ast.body.push(walk());
  }

  return ast;
}
  • 得到AST如下
{
  type: 'Program',
  body: [{
    type: 'CallExpression',
    name: 'add',
    params: [{
      type: 'NumberLiteral',
      value: '2'
    }, {
      type: 'CallExpression',
      name: 'subtract',
      params: [{
        type: 'NumberLiteral',
        value: '4'
      }, {
        type: 'NumberLiteral',
        value: '2'
      }]
    }]
  }]
};
  • AST类型节点

我们看到了ast中有一些type,这些就是ast的节点类型,是提前定义好的,包含了这种语言的所有语法类型,文章中例子只是简单定义了几种,AST还有很多其他类型的节点,如下图:

https://cdn.nlark.com/yuque/0/2024/png/1661749/1723985624605-c79e974d-2619-4dee-b2f5-c8adad62368d.png

小结

第二步完成,生成好AST之后解析器的工作就完成了,后续我们就可以对这个ast进行自己想要的处理了,比如写babel插件、eslint插件、webpack插件等等;

每种解析器生成的ast都有自己的规范,但是思路都是一样的,比如常用js的解析器有babel/parser,acorn等。

现在市面上有各种各样的语言,每种语言也有各种各样的解析器,感兴趣的同学可以到AST Explorer尝试下不同语言的不同解析器;

https://cdn.nlark.com/yuque/0/2024/png/1661749/1723986218481-3b661aef-d840-4386-a3e5-9a4fcabc5867.png

总结

回到前言中的问题:"我们编写的JS代码只是一些字符串,它是怎么被机器执行的?",本篇文章只讲了第一步解析器,后续还有很多流程很多概念,比如字节码、机器码、解释器等等,感兴趣的同学可以自己继续了解探索

https://cdn.nlark.com/yuque/0/2024/png/1661749/1723990931231-e7686b3a-699b-445d-80f4-234c6ecd45c2.png

最后

留个问题讨论,文中提到C/C++、GO 等都是编译型语言,那么这些语言的作者是用什么语言,去编译他的新语言的呢,能自己编译自己么?

参考