众所周知,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的解析逻辑之后,我们就可以处理程序的语法树了,语法树的解析如下

flat
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
public class Loader extends JinxBaseListener {

/**
* 变量表,以变量名为key,包括:变量索引idx、变量类型
*/
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);
// main方法
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); // 设置COMPUTE_FRAMES后会自动计算,但是此处设置不能省略
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);
// 栈顶double值存入本地局部变量,idx代表索引
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);
// 遍历语法树生成Java指令
List<Instruction> instructions = loader.getInstructions();
// 生成Java.class文件
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,运行这个字节码文件可以打印出变量的值

$ 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
}

参考

ANTLR4表达式
Java代码
Java ASM系列
Enkel-JVM-language