自己动手实现一个可以运行在JVM上的编程语言
众所周知,JVM虚拟机被设计为可以执行栈式指令的机器。因此任何一个语言只要编译之后得到的字节码符合JVM的标准,就可以在JVM上执行,例如Kotlin、Groovy、Scala、Clojure。
我们自己设计一款语言,并命名为Jinx,它支持类定义、变量定义、变量打印。它的语法解析逻辑如下
1 | grammar Jinx; |
Jinx的最外层是类class,class的内部可以包含变量的定义和打印,变量的值支持字符串、整数和小数。有了ANTLR4的解析逻辑之后,我们就可以处理程序的语法树了,语法树的解析如下
1 | public class Loader extends JinxBaseListener { |
在上面的语法树解析中,我们会解析每一个变量的定义语法和打印语法。
变量定义
我们会在定义每个变量的时候记录下变量的类型和索引,并把记录的数据关联到这个变量的名字上。此外,我们还会针对这个变量的类型、索引和值生成JVM保存变量的指令。
变量打印
在打印程序的解析中,我们会先通过变量的名称从关联表中取出变量的类型和索引(如果不存在就报错),之后根据变量的类型和索引创建JVM打印的指令。
上面的语法树解析最终生成了一个指令列表instructions,我们接下来根据这个指令列表生成JVM所需要的字节码:
1 | private byte[] generateBytecode(List<Instruction> instructions, String className) { |
如上我们根据指令和类名使用ASM生成了字节码数据,它生成了一个包含main方法的类,并且把我们的指令放在main方法中。每个指令都调用了其apply
方法,接下来我们具体看一下变量定义和变量打印的apply
方法是如何实现的。
变量定义
1 | public void apply(MethodVisitor mv) { |
变量的定义很简单,都是先把变量的值从常量池取出,然后推到操作数栈的顶部。之后从操作数栈顶取数据,根据变量的idx把变量保存到局部变量表的指定索引位置。区别在于浮点型的保存指令是DSTORE
,整型是ISTORE
,字符串是ASTORE
。
变量打印
1 | public void apply(MethodVisitor mv) { |
变量的打印会先使用System.out
变量,之后从局部变量表中根据变量的idx取出变量的值,然后执行println
方法,入参分别为整型、浮点型和字符串。
有了以上这些指令,我们就可以正常生成字节码了,我们进行语法分析生成instructions,并使用instructions最终生成字节码文件。
1 | public void compile0(String file) throws IOException { |
上面代码的最后一行就是根据指令列表和类名生成字节码,并把字节码保存到文件中。我们创建一个源代码
class Test {
var name = "Mike"
var salary = 2370
print name
print salary
var number = 1.1
print number
}
使用编译器解析如上代码并最终生成一个字节码文件Test.class,运行这个字节码文件可以打印出变量的值
$ java Test
Mike
2370
1.1
我们也可以查看字节码的信息如下
$ javap -verbose Test
Classfile /src/main/resources/jinx/Test.class
Last modified Jan 3, 2023; size 342 bytes
MD5 checksum fff7d9ac9c044299ffd5a6194c452502
public class Test
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Utf8 Test
#2 = Class #1 // Test
#3 = Utf8 java/lang/Object
#4 = Class #3 // java/lang/Object
#5 = Utf8 main
#6 = Utf8 ([Ljava/lang/String;)V
#7 = Utf8 Mike
#8 = String #7 // Mike
#9 = Integer 2370
#10 = Utf8 java/lang/System
#11 = Class #10 // java/lang/System
#12 = Utf8 out
#13 = Utf8 Ljava/io/PrintStream;
#14 = NameAndType #12:#13 // out:Ljava/io/PrintStream;
#15 = Fieldref #11.#14 // java/lang/System.out:Ljava/io/PrintStream;
#16 = Utf8 java/io/PrintStream
#17 = Class #16 // java/io/PrintStream
#18 = Utf8 println
#19 = Utf8 (Ljava/lang/String;)V
#20 = NameAndType #18:#19 // println:(Ljava/lang/String;)V
#21 = Methodref #17.#20 // java/io/PrintStream.println:(Ljava/lang/String;)V
#22 = Utf8 (I)V
#23 = NameAndType #18:#22 // println:(I)V
#24 = Methodref #17.#23 // java/io/PrintStream.println:(I)V
#25 = Double 1.1d
#27 = Utf8 (D)V
#28 = NameAndType #18:#27 // println:(D)V
#29 = Methodref #17.#28 // java/io/PrintStream.println:(D)V
#30 = Utf8 Code
{
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=4, args_size=1
0: ldc #8 // String Mike
2: astore_0
3: ldc #9 // int 2370
5: istore_1
6: getstatic #15 // Field java/lang/System.out:Ljava/io/PrintStream;
9: aload_0
10: invokevirtual #21 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
13: getstatic #15 // Field java/lang/System.out:Ljava/io/PrintStream;
16: iload_1
17: invokevirtual #24 // Method java/io/PrintStream.println:(I)V
20: ldc2_w #25 // double 1.1d
23: dstore_2
24: getstatic #15 // Field java/lang/System.out:Ljava/io/PrintStream;
27: dload_2
28: invokevirtual #29 // Method java/io/PrintStream.println:(D)V
31: return
}