为了账号安全,请及时绑定邮箱和手机立即绑定

Slisp:编译到JVM平台上的lisp方言

标签:
JVM

一、前言

之前经常变更学习方向没有收到很好的学习效果浪费了不少时间。最近痛定思痛把方向定为JVM和编译原理这次真的不改了。本文是学习该方向的阶段性总结。

之前写过几个解释器但还没写过编译器。刚好看到知乎Belleve给出的一幅学习路线图于是决定实现一个lisp方言的编译器。

之所以选择JVM而不是X86作为目标平台一是JVM平常用的多一些可以互相印证、互相补充二是文档和社区资源丰富友好开发体验较好。

项目地址https://github.com/tdkihrr/Slisp

截止最新的commit77f126d4实现的功能有

  • 定义变量

  • 支持字符串、整数和布尔类型

  • 打印以上三种预置类型的值

  • 四则运算

  • 条件判断

二、编译和运行方法

来一段具体的Slisp程序

(define a (+ 1 2 3 4))

(println a)

(define b (+ a a))

(println b)

(define a (+ b b))

(println a)

(println (+ (+ 1 1)
            (- 6 4)
            (* 2 2)
            (/ 4 2)))

(println "Hello Slisp!")

(define c "Hello world!")

(println c)

(println true)

(println false)

(define d true)

(println d)

(if true (println true) (println false))

(if (== 1 1) (println "1 == 1") (println "1 != 1"))

以上程序出自本项目/Slisp/Hello.slisp。

想要运行必须先打包编译器

./gradlew clean build

得到了build/libs/slisp-0.1.0.jar之后在命令行编译源代码

java -jar build/libs/slisp-0.1.0.jar Slisp/Hello.slisp

即可生成Hello.class文件java Hello运行该文件输出为

10
20
40
10
Hello Slisp!
Hello world!
true
false
true
true
1 == 1

三、编译器组成部分

这个编译器由三部分组成一是前端部分二是构建抽象语法树三是递归下降生成字节码。

前端部分使用了Antlr来构建。Antlr是一个流行的parser generator可以根据给定的文法生成相应的parser。因为Slisp本身采用了lisp系的语法并不复杂所以很容易写出文法供Antlr使用。

构建抽象语法树使用了visitor模式。由于Antlr本身返回的结果已经是一棵树所以这部分的工作是根据每个节点不同的形态创建相应的类和实例。

这里有一些实现上的细节可以优化比如针对四则运算可以将这些运算全部用一个类来表示只更改其中的一个字段以示区别。还有一点是如果打算只使用一个visitor那么每个节点类都需要继承同一个接口或父类。

还有实现了一点简单的类型推导。传统的lisp方言大多是动态语言不过Slisp是静态的而且可以在定义变量时推导出变量的类型不需要开发者手动声明变量的类型。(define a 123)(define b "Hello")(define c true)可以由字面值推导出类型而(define d (+ 1 (- 2 3))也可以推导出表达式(+ 1 (- 2 3))的类型并以此确定d的类型。

生成字节码部分采用了递归下降来生成。比如对(+ (+ 1 1) (- 6 4) (* 2 2) (/ 4 2))生成了

      44: bipush        1
      46: bipush        1
      48: iadd
      49: bipush        6
      51: bipush        4
      53: isub
      54: iadd
      55: bipush        2
      57: bipush        2
      59: imul
      60: iadd
      61: bipush        4
      63: bipush        2
      65: idiv
      66: iadd

这段代码是Hello.class文件中的一部分使用OpenJDK中的javap反汇编器生成。

(+ 1 1)对应44、46和48先将两个1压入栈中然后相加将之前的两个人从栈中弹出然后将结果压入栈顶继续执行(- 6 4)

这里需要注意的是并不是说执行完这四个运算(+ 1 1) (- 6 4) (* 2 2) (/ 4 2)然后再计算它们的和。而是在计算完(+ 1 1)(- 6 4)之后结果为2和2立即计算了(+ 2 2)得到4然后计算(* 2 2)得到4再计算(+ 4 4)以此类推。过程如下所示

(+ (+ 1 1) (- 6 4) (* 2 2) (/ 4 2))
(+ 2 (- 6 4) (* 2 2) (/ 4 2))
(+ 2 2 (* 2 2) (/ 4 2))
(+ 4 (* 2 2) (/ 4 2))
(+ 4 4 (/ 4 2))
(+ 8 (/ 4 2))
(+ 8 2)
(10)

为了契合这样的字节码运算方式后端在创建抽象语法树的时候需要注意“左结合与右结合”的问题。这里采用了右结合的方式大致结构如下所示

(+ (/ 4 2)
   (+ (* 2 2)
      (+ (- 6 4)
         (+ 1 1))))

这样从底层开始生成字节码每生成一层就向上传递继续生成上层节点的字节码。

实际开发中使用了ASM库来辅助生成字节码只需要手动拼接好类似于bipush 1这样的文本传给ASM中合适的类和方法最后调用generateBytecode这样的方法即可。

虽然ASM库很方便但想要生成符合语义的字节码开发者仍需要阅读JVM规范。JVM规范中定义了各字节码的名称与语义对照着网络上的众多示例还是很容易理解的。

四、字节码简介

bipush是指将一个类型为byte扩充为int然后压到栈上。

iadd是将栈最上面的两个int弹出然后计算它们的和将结果压入栈顶。imulisubidiv都类似于iadd不同之处在于将运算符变为了*-/

istoreint保存在局部变量中。

iload从局部变量中取出保存在其中的值。

astore是将对一个Ojbect的引用保存在局部变量中。

alocal是将保存在局部变量中的引用压入栈顶。

ifeq是将栈顶的值与0进行比较如果相等进入true branch否则进行false branch。该指令还会指定一个数字作为false branch入口的地址。

if_icmpne是比较栈上的两个类型为int的值如果不相等进入true branch否则进入false branch。

值得注意的是诸如if这样的指令并不是单个存在它们更多的像是一个家庭比如比较两个int会有许多相似的指令从JVM规范中抄录一段

• if_icmpeq succeeds if and only if value1 = value2
• if_icmpne succeeds if and only if value1 ≠ value2
• if_icmplt succeeds if and only if value1 < value2
• if_icmple succeeds if and only if value1 ≤ value2
• if_icmpgt succeeds if and only if value1 > value2
• if_icmpge succeeds if and only if value1 ≥ value2

可以看到if_icmpne只是用来比较两个数相等时的情况还有其它指令用于比较不等、大于、小于、相等时的情况。像这样相似而略有区别的指令JVM规范大多将它们的文档合并在一起并起名为if_icmp<cond>这里的cond代表每个指令独特的部分。

原文出处https://www.cnblogs.com/tdkihrr/p/10206206.html  

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消