第一行代码
一、提要
标题为 一、提要在本章节我们将在Qing
上添加一些功能
- 声明变量
- 变量的打印
- 类型推断
而在本章节结束后,我们要达成的目标则是让如下的代码可以在Qing
上运行
var name="Qing"print namevar year = 2023print year
二、词法分析
标题为 二、词法分析2.1、词法规则定义
标题为 2.1、词法规则定义上一章,我们阐述了一个编程语言的三个模块,自然先到我们面前的就是第一个模块词法分析器,想要重头实现一个词法分析器是很难的事,还好市面上这么多的编程语言也不是凭空出现的,前辈们的诞生也依靠了很多处理相关工作的工具,而这里我们将会用到一个叫做antlr的工具,它能够大大地减轻我们对于词法的分析工作
现在,我们先用antlr
描述一下我们的规则,这些规则基本都是很容易看懂的,不过我也大概写了一些注释,需要特别注意的是antlr
的规则编写和java
一样每一行后需要跟一个;
// 定义语言名称grammar Qing;
// 定义解析规则// 根规则:所有的代码文本只有`variable`和`print`两种类型,EOF表示文件结束compilationUnit : ( variable | print )* EOF;// 声明赋值语句,例: var a = 5variable : VARIABLE ID EQUALS value;// 打印语句,例: print aprint : 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 ; // 用来过滤空行的特殊符号
2.2、代码生成
标题为 2.2、代码生成规则就这样被我们定义好了,现在我们需要运行antlr
来帮我们生成Java代码,antlr
执行程序你可以在官网或者github发布页面下载
antlr Qing.g4
不过我们是用IDEA来编写这个程序,更加方便的方式是使用antlr
的插件来帮我们生成java代码
antlr idea plugin
用插件的方式需要额外进行一些插件的配置
配置好了,现在我们可以生成代码了,执行Generate ANTLR Recognizer
后antlr
会在指定目录生成很多类,这些代码可以放在那里暂时不要管,我们来检验一下我们定义的规则是否有效,可以依照下图操作
解析树成功展示出来了,表明了我们规则没有问题
三、解析、编译以及运行
标题为 三、解析、编译以及运行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 namevar year = 2023print 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)
3.2.1、ASM
标题为 3.2.1、ASMASM是一种通用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代码var name="Qing"print namevar year = 2023print year
我们在项目根目录成功生成了first.class
文件,再运行一下看看
$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