转译器-解释器-编译器

树形的 ast 转换为另一个 ast,然后再打印成目标代码的字符串,这是转译器,把 ast 解释执行或者专成线性的中间代码再解释执行,这是解释器,把 ast 转成线性中间代码,然后生成汇编代码,之后做汇编和链接,生成机器码,这是编译器。

ast 也可以算一种树形 IR,IR 是 immediate representation 中间表示的意思。要先把 AST 转成线性 IR,然后再生成汇编、字节码等。

按照语法结构递归 ast,进行每个节点的翻译,这叫做语法制导翻译,用线性 IR 中的指令来翻译 AST 节点的属性。

 

语义分析要检查出语义的错误,比如类型是否匹配、引用的变量是否存在、break 是否在 while 中等,主要要做作用域分析、引用消解、类型推导和检查、正确性检查等。

作用域中有各种声明,要把它们的类型、初始值、访问修饰符等信息记录下来,保存这个信息的结构叫符号表,这相当于是一个缓存,之后处理这个符号的时候直接去查符号表就行,不用再次从 ast 来找。

引用消解呢就是对每个符号检查下是否都能查找到定义,如果查找不到就报错。

js 的源码中肯定不可能都写类型,很多地方可以直接推导出来,根据 ast 可以得出类型的声明,记录到符号表中,之后遍历 ast,对各种节点取出声明时的类型来进行检查,不一致就报错。

语义分析之后就代表着程序已经没有语法和语义的错误了,可以放心进行各种后续转换,不会再有开发者的错误。之后先翻译成线性 IR,然后对线性 IR 进行优化,需要优化就是因为自动生成的代码难免有很多冗余,需要把各种没必要的处理去掉。但是要保证语义不变。比如死代码删除、公共子表达式删除、常量传播等等。

线性 IR 的分析要建立流图,就是控制流图,控制流就是根据 if、while、函数调用等导致的程序跳转,把顺序执行的代码和跳转到的代码之间连接起来就是一个图,顺序执行的代码看成一个整体,叫做基本快。之后根据这个流图做数据流分析,也就是分析一个变量流经了那些代码,然后基于这些做各种优化。

这个部分叫做程序分析,或者静态分析,是一个专门的方向,可以用于代码漏洞的静态检查,可以用于编译优化,这个是比较难的。研究这个的博士都比较少。国内只有北大和南大开设程序分析课程。

 

优化之后的线性 IR 就可以生成汇编代码了,然后通过汇编器转成机器码,再链接一些标准库,比如 v8 目录下可以看到 builtins 目录,这里就是各种编译好的机器码文件,可以静态链接成一个可执行文件。

 

因为 js 是解释型语言,直接从源码解释执行,不要说 js 了,java 的字节码也不需要静态链接。像 c、c++这些生成可执行文件的才需要通过汇编器把代码专成机器码然后链接成一个文件。而且如果目标平台有这些库,那么不需要静态链接到一起,可以动态链接。你可能听过.dll 和.so 这就分别是 windows 和 linux 的用于运行时动态加载的保存机器码的文件。前端领域基本不需要汇编和链接,就算是 wasm,也是生成 wasm 字节码,之后解释执行。前端主要还是转译器。

 

 

转译器的目标代码也是高级语言,也是嵌套的结构,所以从高级语言到高级语言是从树形结构到树形结构,不像翻译成低级的指令方式组织的语言,还得先翻译成线性 IR,高级到高级语言的转换,只需要 ast,对 ast 做各种转换之后,就可以做代码生成了。

 

不管是跨语言的转换,比如 ts 转 rust,还是同语言的转换 js 转 js 都不需要线性结构,两棵树的转换要啥线性中间代码啊。所以一般转译器都是 parse、transform、generate 这 3 个阶段。

 

parse 广义上来说包含词法、语法和语义的分析,狭义的 parse 单指语法分析。这个不必纠结。

transform 就是对 ast 的增删改,之后 generator 再把 ast 打印成字符串,我们解析 ast 的时候把[]{} () 等分隔符去掉了,generate 的时候再把细节加回来。

其实前端领域主要还是转译器,因为主流 js 引擎执行的是源代码,但是这个源代码和我们写的源代码还不太一样,所以前端很多源码到源码的转译器来做这种转换,比如 babel、typescript、terser、eslint、postcss、prettier 等。

babel 是把高版本 es 代码转成低版本的,并且注入 polyfill。typescript 是类型检查和转成 js 代码。eslint 是根据规范检查,但--fix 也可以生成修复后的代码。prettier 也是用于格式化代码的,比 eslint 处理的更多,不只限于 js。postcss 主要是处理 css 的,posthtml 用于处理 html。相信你也用过很多了。taro 这种小程序转译器就是基于 babel 封装的。

 

首先转译器也是编译器的一种,只不过比较特殊,叫做 transpiler,一般的编译器叫做 compiler。解释器和编译器的区别确实是是否生成代码。提前编译成机器代码的叫做 AOT 编译器,运行时编译成机器代码的叫做 JIT 编译器,

用 c++来写 js 解释器,像 v8、spidermonkey 等都是。我们在有了 ast 并且做完语义分析之后就可以遍历 ast,然后用 c++来执行不同的节点了,这种叫做 tree walker 解释器,直接解释执行 ast,v8 引擎在 17 年之前都是这么干的。但是在 17 年之后引入了字节码,因为字节码可以缓存啊,这样下次再直接执行字节码就不需要 parse 了。字节码是种线性结构,也要做 ast 到线性 ir 的转换,之后在 vm 上执行字节码。

 

一般解释线性代码的比如汇编代码、字节码等这种的程序才叫做虚拟机,因为机器代码就是线性的,其实从 ast 开始就可以解释了,但是却不叫 vm,我觉得就是因为这个,和机器码比较像的线性代码的解释器才叫 vm。

 

编译是耗时的,所以也不是啥代码都 JIT,要做热度的统计,到达了阈值才会做 JIT。然后把机器码缓存下来,当然也可能是缓存的汇编代码,用到的时候再用汇编器转成机器码,因为机器代码占的空间比较大。

 

v8 有 parser、ignation 解释器、turbofan 编译器,还有 gc。ignation 解释器就是把 parse 出的 ast 转成字节码,然后解释执行字节码,热度到达阈值之后会交给 turbofan 编译为汇编代码之后生成机器代码,来加速。gc 是独立的做内存管理的。

 

 

turbofan 是涡轮增压器,这个名字就能体现出 JIT 的意义。但 JIT 提升了执行速度,也有缺点,比如会使得 js 引擎体积更大,占用内存更大,所以轻量级的 js 引擎不包含 jit,这就是运行速度和包大小、内存空间之间的权衡。架构设计也经常要做这种两边都可以,但是要做选择的 trade off,我们叫做方案勾兑。

 

wasm:llvm 可以生成 wasm 字节码,所以 c++、rust 等可以转为 llvm ir 的语言都可以做 wasm 开发

ide 的 lsp:编程语言的语法高亮、智能提示、错误检查等通过 language service protocol 协议来通信,而 lsp 服务端主要是基于 parser 对正在编辑的文本做分析。

 

 

其实编程语言主要还是设计,实现的话首先实现 parser 和语义分析,后面分为两条路,一种是解释执行的解释器配合 JIT 编译器的路,一种是编译成汇编代码码,然后生成机器码再链接成可执行文件的编译器的路。

parser 部分比较繁琐,可以用 antlr 这种 parser 生成器来生成,语义分析要自己写,这个不太难,主要是对 ast 的各种处理。之后如果想做成编译器,可以用 llvm 这种通用的优化器和代码生成器,clang、rust、swift 都是基于它,所以很靠谱,可以直接用。如果做解释器可以写 tree walker 解释器,或者再进一步生成线性字节码,然后写个 vm 来解释字节码。JIT 编译器也可以用 llvm 来做。要把 ast 转成 llvm ir,也是树形结构转线性结构,这个还是编译领域很常见的操作。

其实编译原理只是告诉你怎么去实现,语言设计不关心实现,一门语言可以实现为编译型也可以实现为解释型,也可以做成 java 那种先编译后解释,你看 hermes 不就是先把 js 编译为字节码然后解释执行字节码么。语言不分编译解释,这个概念要有,c 也有解释器,js 也有编译器,我们说一门语言是编译型还是解释型主要是主流的方式是编译还是解释来决定的。

 

编程语言可以分为 GPL 和 DSL 两种。

GPL 是通用编程语言,它是图灵完备的,也就是能够描述任何可计算问题,像 c++、java、python、go、rust 等这些语言都是图灵完备的,所以一门语言能实现的另一门语言都能实现,只不过实现难度不同。比如 go 语言内置协程实现,那么写高并发程序就简单,java 没有语言级别的协程,那么就要上层来实现。你可能听到过设计模式是对语言缺陷的补充就是这个意思,不同语言设计思路不同,内置的东西也不同,有的时候需要运行时来弥补。、

编程语言有不同的设计思路,大的方向是编程范式,比如命令式、声明式、函数式、逻辑式等,这些大的思路会导致语言的语法,内置的实现都不同,表达能力也不同。这基本确定了语言基调,后续再补也很难,就像 js 里面实现函数式,你又不能限制人家不能用命令式,就很难写出纯粹的函数式代码。

DSL 不是图灵完备的,却换取了某领域的更强的表达能力,比如 html、css、正则表达式,jq 的选择器语法等等,比较像一种伪代码,特定领域的表达能力很强,但是却不是图灵完备的不能描述所有可计算问题。

编译原理是实现编程语言的步骤要学习的,更上层的语言设计还要学很多东西,最好能熟悉多门编程语言的特性。

 

 

 

 

看下 wepack 的 wxml transpiler,lexer 部分写的还可以。这种 xml 的 parser 比较简单,适合入门

 

 

js 引擎可以尝试用 babel 做 parser,自己做语义分析,解释执行 ast 试试,之后进一步生成字节码或其他线性 ir,然后写个 vm 来解释字节码。

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值