第一行代码

在本章节我们将在Qing上添加一些功能

  • 声明变量
  • 变量的打印
  • 类型推断

而在本章节结束后,我们要达成的目标则是让如下的代码可以在Qing上运行

Terminal window
var name="Qing"
print name
var year = 2023
print year

2.1、词法规则定义

标题为 2.1、词法规则定义

上一章,我们阐述了一个编程语言的三个模块,自然先到我们面前的就是第一个模块词法分析器,想要重头实现一个词法分析器是很难的事,还好市面上这么多的编程语言也不是凭空出现的,前辈们的诞生也依靠了很多处理相关工作的工具,而这里我们将会用到一个叫做antlr的工具,它能够大大地减轻我们对于词法的分析工作 现在,我们先用antlr描述一下我们的规则,这些规则基本都是很容易看懂的,不过我也大概写了一些注释,需要特别注意的是antlr的规则编写和java一样每一行后需要跟一个;

// 定义语言名称
grammar Qing;
// 定义解析规则
// 根规则:所有的代码文本只有`variable`和`print`两种类型,EOF表示文件结束
compilationUnit : ( variable | print )* EOF;
// 声明赋值语句,例: var a = 5
variable : VARIABLE ID EQUALS value;
// 打印语句,例: print a
print : PRINT ID ;
value : NUMBER | STRING ; //must be NUMBER or STRING value (defined below)
// 符号规则,代码切割为符号的规则
VARIABLE : 'var' ;
PRINT : 'print' ;
EQUALS : '=' ;
NUMBER : [0-9]+ ; // 数字
STRING : '"'.*'"' ; // "任意值"
ID : [a-zA-Z0-9]+ ; // 需要是字母和数字值
WS: [ \t\n\r]+ -> skip ; // 用来过滤空行的特殊符号

规则就这样被我们定义好了,现在我们需要运行antlr来帮我们生成Java代码,antlr执行程序你可以在官网或者github发布页面下载

antlr Qing.g4

不过我们是用IDEA来编写这个程序,更加方便的方式是使用antlr的插件来帮我们生成java代码 antlr idea plugin 用插件的方式需要额外进行一些插件的配置

配置好了,现在我们可以生成代码了,执行Generate ANTLR Recognizerantlr会在指定目录生成很多类,这些代码可以放在那里暂时不要管,我们来检验一下我们定义的规则是否有效,可以依照下图操作

解析树成功展示出来了,表明了我们规则没有问题

三、解析、编译以及运行

标题为 三、解析、编译以及运行

3.1、遍历解析树

标题为 3.1、遍历解析树

我们通过antlr工具生成了很多类,这里简单介绍一下

  • QingLexer - 定义解析规则、符号
  • QingParser - 包含每个规则的解析器、标记信息和内部类。
  • QingListener - 解析访问节点监听器,用于添加节点访问的回调
  • QingBaseListener - QingListener的空实现
  • QingVisitor - 解析树访问器
  • QingBaseVisitor - QingBaseVisitor的空实现

我们主要是使用QingBaseListener这个类来帮助我们对解析树进行遍历

这里我写了一个简易的监听器来监听赋值语句以及打印语句的解析,这里还没有涉及编译的细节,不过我们已经可以通过这个监听器把输入代码里的各项符号(Token)以及数值都拿到了

public class QingTreeWalkListener extends QingBaseListener {
/**
* 定义上下文中的变量名
*/
HashMap<String,String> variableMap = new HashMap<>();
@Override
public void exitVariable(QingParser.VariableContext ctx) {
final TerminalNode varName = ctx.ID();
final QingParser.ValueContext varValue = ctx.value();
if(varName !=null){
if(variableMap.containsKey(varName.getText())){
log.error("你要定义变量{},这个变量已经定义过了",varName.getText());
return;
}
variableMap.put(varName.getText(), varValue.getText());
log.info("你定义了一个变量,变量名为{},它的值为{}",varName.getText(),varValue.getText());
}
}
@Override
public void exitPrint(QingParser.PrintContext ctx) {
final TerminalNode varName = ctx.ID();
boolean printedVarNotDeclared = !variableMap.containsKey(varName.getText());
if(printedVarNotDeclared){
log.error("你要打印的变量{}从未定义过",varName.getText());
return;
}
log.info("你要打印一个变量,变量名为{},实际值为{}",varName.getText(),variableMap.get(varName.getText()));
}
}

监听器实现了之后,就可以实际地使用它了

public class SyntaxTreeTraverser {
/**
* 解析代码,并执行回调函数
* @param code
*/
public void parse(String code){
CodePointCharStream codePointCharStream = CharStreams.fromString(code);
QingLexer qingLexer = new QingLexer(codePointCharStream);
CommonTokenStream tokenStream = new CommonTokenStream(qingLexer);
QingParser qingParser = new QingParser(tokenStream);
QingTreeWalkListener qingTreeWalkListener = new QingTreeWalkListener();
QingTreeWalkErrorListener qingTreeWalkErrorListener = new QingTreeWalkErrorListener();
qingParser.addParseListener(qingTreeWalkListener);
qingParser.addErrorListener(qingTreeWalkErrorListener);
qingParser.compilationUnit();
}
}

我还为它写了一个解析失败的监听器

public class QingTreeWalkErrorListener extends BaseErrorListener {
@Override
public void syntaxError(Recognizer<?, ?> recognizer, Object offendingSymbol, int line, int charPositionInLine, String msg, RecognitionException e) {
log.error("第{}行不符合语法规则,详情如下\n{}",line,msg);
}
}

现在可以把测试代码跑起来试试了,我写了一个QingCompiler作为编译器的入口程序,他后续会成为编译器的主体,当然它现在只是为我们来简单测试一下解析树的遍历效果

public class QingCompiler {
public static void main(String[] args) throws Exception {
new QingCompiler().compile(args);
}
public void compile(String[] args) throws Exception {
String code =
"var name=\"Qing\" " +
"var year = 2023 " +
"print name " +
"print year ";
new SyntaxTreeTraverser().parse(code);
}
}

我们来测试一下下面这段示例

var name="Qing"
print name
var year = 2023
print year
你定义了一个变量,变量名为name,它的值为"Qing"
你定义了一个变量,变量名为year,它的值为2023
你要打印一个变量,变量名为name,实际值为"Qing"
你要打印一个变量,变量名为year,实际值为2023

除此之外,它还可以输出解析异常的情况

第1行不符合语法规则,详情如下
mismatched input 'String' expecting {<EOF>, 'var', 'print'}

3.2、生成字节码

标题为 3.2、生成字节码

我们已经成功遍历了语法解析树,下一步就是要用这棵树来形成字节码文件了,Qing是一个JVM语言,换言之,我们要生成.class字节码文件

如果你对java虚拟机比较了解,你也许知道.class文件是由一组指令构成,而每一个指令都包含:

  • 一个字节长度、代表着某种特定操作含义的数字 - 操作码(Opcode)
  • 零至多个代表此操作所需参数 - 操作数(Operands)

ASM是一种通用Java字节码操作和分析框架。它可以用于修改现有的class文件或动态生成class文件,我们利用这个工具来生成我们想要的字节码文件

3.2.2、指令组定义

标题为 3.2.2、指令组定义

通过ASM生成字节码,我们需要先将代码转换为一组指令(Instruction)

/**
* 指令接口
*/
public interface Instruction {
void apply(MethodVisitor methodVisitor);
}
/**
* 变量定义指令
*/
public class VariableDeclarationInstruction implements Instruction, Opcodes {
Variable variable;
public VariableDeclarationInstruction(Variable variable) {
this.variable = variable;
}
@Override
public void apply(MethodVisitor mv) {
final int type = variable.getType();
if(type == QingLexer.NUMBER) {
int val = Integer.parseInt(variable.getValue());
mv.visitIntInsn(SIPUSH,val);
mv.visitVarInsn(ISTORE,variable.getId());
} else if(type == QingLexer.STRING) {
mv.visitLdcInsn(variable.getValue());
mv.visitVarInsn(ASTORE,variable.getId());
}
}
}
/**
* 打印变量指令
*/
public class PrintVariableInstruction implements Instruction, Opcodes {
Variable variable;
public PrintVariableInstruction(Variable variable) {
this.variable = variable;
}
@Override
public void apply(MethodVisitor mv) {
final int type = variable.getType();
final int id = variable.getId();
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
if (type == QingLexer.NUMBER) {
mv.visitVarInsn(ILOAD, id);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(I)V", false);
} else if (type == QingLexer.STRING) {
mv.visitVarInsn(ALOAD, id);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
}
}
}

#todo:加一些解释

3.2.3、指令组装

标题为 3.2.3、指令组装

我们目前实际是实现了,变量的声明以及变量的打印功能,为此我们先引入一个实体,来表明我们的变量信息

public class Variable {
private int id;
private int type;
private String value;
}

我们再将之前的监听器调整一下

public class QingTreeWalkListener extends QingBaseListener {
/**
* 定义上下文中的变量名
*/
Map<String, Variable> variableMap = new HashMap<>();
Queue<Instruction> instructionsQueue = new ArrayDeque<>();
@Override
public void exitVariable(QingParser.VariableContext ctx) {
final TerminalNode varName = ctx.ID();
final QingParser.ValueContext varValue = ctx.value();
final int varType = varValue.getStart().getType();
final int varIndex = variableMap.size();
final String varTextValue = varValue.getText();
Variable var = new Variable(varIndex, varType, varTextValue);
if (variableMap.containsKey(varName.getText())) {
log.error("你要定义变量{},这个变量已经定义过了", varName.getText());
return;
}
variableMap.put(varName.getText(), var);
instructionsQueue.add(new VariableDeclarationInstruction(var));
log.info("你定义了一个变量,变量名为{},它的值为{}", varName.getText(), varValue.getText());
}
@Override
public void exitPrint(QingParser.PrintContext ctx) {
final TerminalNode varName = ctx.ID();
boolean printedVarNotDeclared = !variableMap.containsKey(varName.getText());
if (printedVarNotDeclared) {
log.error("你要打印的变量{}从未定义过", varName.getText());
return;
}
final Variable variable = variableMap.get(varName.getText());
instructionsQueue.add(new PrintVariableInstruction(variable));
log.info("你要打印一个变量,变量名为{},实际值为{}", varName.getText(), variableMap.get(varName.getText()));
}
public Queue<Instruction> getInstructionsQueue() {
return instructionsQueue;
}
}

遍历器也补充一个方法,获取指令队列

/**
* 语法树遍历器
*/
public class SyntaxTreeTraverser {
/**
* 获取指令集
* @param code
* @return
*/
public Queue<Instruction> getInstructions(String code) {
CodePointCharStream codePointCharStream = CharStreams.fromString(code);
QingLexer qingLexer = new QingLexer(codePointCharStream);
CommonTokenStream tokenStream = new CommonTokenStream(qingLexer);
QingParser qingParser = new QingParser(tokenStream);
QingTreeWalkListener qingTreeWalkListener = new QingTreeWalkListener();
QingTreeWalkErrorListener qingTreeWalkErrorListener = new QingTreeWalkErrorListener();
qingParser.addParseListener(qingTreeWalkListener);
qingParser.addErrorListener(qingTreeWalkErrorListener);
qingParser.compilationUnit();
return qingTreeWalkListener.getInstructionsQueue();
}
}

3.2.4、生成字节码

标题为 3.2.4、生成字节码

最后再调整一下编译器的代码,生成class文件

public class QingCompiler {
public static void main(String[] args) throws Exception {
new QingCompiler().compile(args);
}
public void compile(String[] args) throws Exception {
String code =
"var name=\"Qing\" " +
"var year = 2023 " +
"print name " +
"print year ";
final Queue<Instruction> instructions = new SyntaxTreeTraverser().getInstructions(code);
final byte[] byteCode = new BytecodeGenerator().generateBytecode(instructions, "first");
writeBytecodeToClassFile("first.class", byteCode);
}
private static void writeBytecodeToClassFile(String fileName, byte[] byteCode) throws IOException {
OutputStream os = Files.newOutputStream(Paths.get(fileName));
os.write(byteCode);
os.close();
}
}

3.3、运行第一个Qing代码

标题为 3.3、运行第一个Qing代码
Terminal window
var name="Qing"
print name
var year = 2023
print year

我们在项目根目录成功生成了first.class文件,再运行一下看看

Terminal window
$java first
"Qing"
2023

现在我们的代码成功运行了,不过在实际写代码的时候可没办法把代码写在main方法里直接跑😂,当然,我们要把代码抽离出来,每个代码片段应该都是一个文件,文件后缀是什么呢,当然就是qing

我们的编译器代码也需要改一下,它需要调整为接收一个文件路径

  • 调整后的代码
public class QingCompiler {
public static void main(String[] args) throws Exception {
new QingCompiler().compile(args);
}
public void compile(String[] args) throws Exception {
File qingFile = new File(args[0]);
String fileName = qingFile.getName();
String fileAbsolutePath = qingFile.getAbsolutePath();
String className = StringUtils.remove(fileName, ".qing");
final Queue<Instruction> instructions = new SyntaxTreeTraverser().getInstructions(fileAbsolutePath);
final byte[] byteCode = new BytecodeGenerator().generateBytecode(instructions, className);
writeBytecodeToClassFile(fileAbsolutePath, byteCode);
}
private static void writeBytecodeToClassFile(String fileName, byte[] byteCode) throws IOException {
String classFile = StringUtils.replace(fileName, ".qing", ".class");
OutputStream os = Files.newOutputStream(Paths.get(classFile));
os.write(byteCode);
os.close();
}
}
/**
* 语法树遍历器
*/
public class SyntaxTreeTraverser {
/**
* 获取指令集
* @param fileAbsolutePath
* @return
*/
public Queue<Instruction> getInstructions(String fileAbsolutePath) throws IOException {
CharStream charStream = CharStreams.fromFileName(fileAbsolutePath);
QingLexer qingLexer = new QingLexer(charStream);
CommonTokenStream tokenStream = new CommonTokenStream(qingLexer);
QingParser qingParser = new QingParser(tokenStream);
QingTreeWalkListener qingTreeWalkListener = new QingTreeWalkListener();
QingTreeWalkErrorListener qingTreeWalkErrorListener = new QingTreeWalkErrorListener();
qingParser.addParseListener(qingTreeWalkListener);
qingParser.addErrorListener(qingTreeWalkErrorListener);
qingParser.compilationUnit();
return qingTreeWalkListener.getInstructionsQueue();
}

#todo:此处加一张图说明如何idea中配置变量启动main