众所周知,JVM 虚拟机被设计为可以执行栈式指令的机器。因此任何一个语言只要编译之后得到的字节码符合 JVM 的标准,就可以在 JVM 上执行,例如 Kotlin、Groovy、Scala、Clojure。
我们自己设计一款语言,并命名为 Jinx,它支持类定义、变量定义、变量打印。它的语法解析逻辑如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| grammar Jinx;
@header { package com.nosuchfield.jinx.code; }
jinx: CLASS ID LEFT_BR classBody RIGHT_BR EOF; classBody: (variable | print)*; variable: VARIABLE ID EQUALS value; print: PRINT ID; value: STRING | INT | DOUBLE;
LEFT_BR: '{'; RIGHT_BR: '}'; CLASS: 'class'; VARIABLE: 'var'; PRINT: 'print'; EQUALS: '='; STRING: '"' ('\\"' | ~'"')+ '"'; DOUBLE: [0-9]+ '.' [0-9]+; INT: [0-9]+; // 这个ID不能放在前面,不然会被提前解析,导致print等字符串被解析为ID ID: [a-zA-Z] [a-zA-Z0-9]*;
WS: [\n\r\t ]+ -> skip;
|
Jinx 的最外层是类 class,class 的内部可以包含变量的定义和打印,变量的值支持字符串、整数和小数。有了 ANTLR4 的解析逻辑之后,我们就可以处理程序的语法树了,语法树的解析如下
flat1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
| public class Loader extends JinxBaseListener {
private final Map<String, ImmutablePair<Integer, Integer>> variables = new HashMap<>();
private final List<Instruction> instructions = new ArrayList<>();
private String className;
@Override public void enterJinx(JinxParser.JinxContext ctx) { className = ctx.ID().getText(); }
@Override public void exitVariable(JinxParser.VariableContext ctx) { String name = ctx.ID().getText(); JinxParser.ValueContext variable = ctx.value(); String text = variable.getText(); int type = variable.getStart().getType(); int idx = variables.size();
variables.put(name, ImmutablePair.of(idx, type)); instructions.add(new VariableInstruction(idx, type, text)); }
@Override public void exitPrint(JinxParser.PrintContext ctx) { String name = ctx.ID().getText(); if (!variables.containsKey(name)) { System.err.printf("variable %s not exist\n", name); System.exit(1); } int idx = variables.get(name).getLeft(); int type = variables.get(name).getRight(); instructions.add(new PrintInstruction(idx, type)); }
public List<Instruction> getInstructions() { return instructions; }
public String getClassName() { return className; }
}
|
在上面的语法树解析中,我们会解析每一个变量的定义语法和打印语法。
变量定义
我们会在定义每个变量的时候记录下变量的类型和索引,并把记录的数据关联到这个变量的名字上。此外,我们还会针对这个变量的类型、索引和值生成 JVM 保存变量的指令。
变量打印
在打印程序的解析中,我们会先通过变量的名称从关联表中取出变量的类型和索引(如果不存在就报错),之后根据变量的类型和索引创建 JVM 打印的指令。
上面的语法树解析最终生成了一个指令列表 instructions,我们接下来根据这个指令列表生成 JVM 所需要的字节码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| private byte[] generateBytecode(List<Instruction> instructions, String className) { ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES); classWriter.visit(V1_8, ACC_PUBLIC + ACC_SUPER, className, null, "java/lang/Object", null); MethodVisitor methodVisitor = classWriter.visitMethod(ACC_PUBLIC + ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null); for (Instruction instruction : instructions) { instruction.apply(methodVisitor); } methodVisitor.visitInsn(RETURN); methodVisitor.visitMaxs(0, 0); methodVisitor.visitEnd(); classWriter.visitEnd(); return classWriter.toByteArray(); }
|
如上我们根据指令和类名使用 ASM 生成了字节码数据,它生成了一个包含 main 方法的类,并且把我们的指令放在 main 方法中。每个指令都调用了其 apply 方法,接下来我们具体看一下变量定义和变量打印的 apply 方法是如何实现的。
变量定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| public void apply(MethodVisitor mv) { switch (type) { case JinxLexer.DOUBLE -> { double val = Double.parseDouble(value); mv.visitLdcInsn(val); mv.visitVarInsn(DSTORE, idx); } case JinxLexer.INT -> { int val = Integer.parseInt(value); mv.visitLdcInsn(val); mv.visitVarInsn(ISTORE, idx); } case JinxLexer.STRING -> { mv.visitLdcInsn(Utils.removeFirstAndLastChar(value)); mv.visitVarInsn(ASTORE, idx); } }
|
变量的定义很简单,都是先把变量的值从常量池取出,然后推到操作数栈的顶部。之后从操作数栈顶取数据,根据变量的 idx 把变量保存到局部变量表的指定索引位置。区别在于浮点型的保存指令是 DSTORE,整型是 ISTORE,字符串是 ASTORE。
变量打印
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| public void apply(MethodVisitor mv) { mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); switch (type) { case JinxLexer.INT -> { mv.visitVarInsn(ILOAD, idx); mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(I)V", false); } case JinxLexer.DOUBLE -> { mv.visitVarInsn(DLOAD, idx); mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(D)V", false); } case JinxLexer.STRING -> { mv.visitVarInsn(ALOAD, idx); mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false); } } }
|
变量的打印会先使用 System.out 变量,之后从局部变量表中根据变量的 idx 取出变量的值,然后执行 println 方法,入参分别为整型、浮点型和字符串。
有了以上这些指令,我们就可以正常生成字节码了,我们进行语法分析生成 instructions,并使用 instructions 最终生成字节码文件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| public void compile0(String file) throws IOException { JinxLexer lexer = new JinxLexer(CharStreams.fromFileName(file)); CommonTokenStream tokens = new CommonTokenStream(lexer); JinxParser parser = new JinxParser(tokens); parser.removeErrorListeners(); parser.addErrorListener(new ErrorHandler()); ParseTree tree = parser.jinx(); ParseTreeWalker parseTreeWalker = new ParseTreeWalker(); Loader loader = new Loader(); parseTreeWalker.walk(loader, tree); List<Instruction> instructions = loader.getInstructions(); String className = loader.getClassName(); String classFile = Paths.get(new File(file).getParent(), className + ".class").toString(); writeByteArrayToFile(classFile, generateBytecode(instructions, className)); }
|
上面代码的最后一行就是根据指令列表和类名生成字节码,并把字节码保存到文件中。我们创建一个源代码
class Test {
var name = "Mike"
var salary = 2370
print name
print salary
var number = 1.1
print number
}
使用编译器解析如上代码并最终生成一个字节码文件 Test.class,运行这个字节码文件可以打印出变量的值
1 2 3 4
| $ java Test Mike 2370 1.1
|
我们也可以查看字节码的信息如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
| $ 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:
{ 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 2: astore_0 3: ldc 5: istore_1 6: getstatic 9: aload_0 10: invokevirtual 13: getstatic 16: iload_1 17: invokevirtual 20: ldc2_w 23: dstore_2 24: getstatic 27: dload_2 28: invokevirtual 31: return }
|
参考
ANTLR4 表达式
Java 代码
Java ASM 系列
Enkel-JVM-language
本文链接: https://www.nosuchfield.com/2023/09/06/Implementing-a-Programming-Language-Running-on-the-JVM/