众所周知,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 TestMike23701.1
我们也可以查看字节码的信息如下
$ javap -verbose TestClassfile /src/main/resources/jinx/Test.classLast modified Jan 3, 2023; size 342 bytesMD5 checksum fff7d9ac9c044299ffd5a6194c452502public class Testminor version: 0major version: 52flags: ACC_PUBLIC, ACC_SUPERConstant 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}