自制编译器:后端代码生成(一)

后端早就已经弄的差不多了,因为学校论文的事情耽搁的比较久,一直到现在才发博客。 
 

所谓的编译器后端的作用就是将语法树翻译成目标机器码。所谓目标机器码,考虑到直接翻译成具体平台(如X86,ARM等)过于复杂,因此先设计一个虚拟机,并翻译成这个虚拟机的机器码。

对于虚拟机以及其指令格式可参考这篇文章http://blog.csdn.net/roger__wong/article/details/8947720,如何去尝试实现这个虚拟机是在我的另外一个系列的博客里进行论述。


本篇文章从以下是那个方面来论述:后端架构与关键数据结构、节点翻译方式。

1、后端架构和关键数据结构

后端接受前端的语法树作为输入,对于其每一个节点根据节点类型的不同产生不同的代码。但在实现过程中为了简单方便,我并没有把后端抽象出一个单独的模块,而是在语法树每一个节点的基础上增加了一个genCode方法,通过调用这个方法来生成该节点及其所有孩子节点(通过递归)的代码。

其次编译器后端直接生成Class文件(文件结构也在上文提到的博客中有说明),程序中后端首先构造一个ClassOfClass的实体,然后再调用此类的方法生成Class文件:

public class ClassOfClass {
	public static int isPublic=1;
	public static int isStatic=2;
	public ArrayList<field> fields;
	public ArrayList<function> functions;
	public ArrayList<String> constPool;
	public String name;
	public ClassOfClass()
	{
		constPool=new ArrayList<String>();
		fields=new ArrayList<field>();
		functions=new ArrayList<function>();
		
	}
	public void WriteClassFile(String path)
	{
		try {
			PrintWriter pw=new PrintWriter(new FileOutputStream(path));
			pw.println(name);
			pw.println(fields.size());
			for(field f:fields)
			{
				pw.println(f.toString());
			}
			pw.println(functions.size());
			for(function f:functions)
			{
				pw.println(f.toString());
			}
		} catch (FileNotFoundException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
}
其中field结构:

public class field {
	public int head;
	public String type;
	public String fieldname;
    @Override
    public String toString()
    {
    	StringBuffer sb=new StringBuffer();
    	sb.append(head);
    	sb.append(" ");
    	sb.append(type);
    	sb.append(" ");
    	sb.append(fieldname);
    	return sb.toString();
    }
}

其中Function结构

public class function {
	public int head;
	public String rettype;
	public int argnum;
	public ArrayList<String> args;
	public ArrayList<Code> codes;
	public function()
	{
		args=new ArrayList<String>();
		codes=new ArrayList<Code>();
	}
	 @Override
	    public String toString()
	    {
	    	StringBuffer sb=new StringBuffer();
	    	sb.append(head);
	    	sb.append(" ");
	    	sb.append(rettype);
	    	sb.append(" ");
	    	sb.append(args.size());
	    	sb.append(" ");
	    	for(String s:args)
	    	{
	    		sb.append(s+" ");
	    	}
	    	sb.append("\r\n");
	    	sb.append("{");
	    	for(int i=0;i<=codes.size()-1;i++)
	    	{
	    		sb.append(i+":"+codes.get(i).toString()+"\r\n");
	    	}
	    	sb.append("\r\n");
	    	sb.append("}");
	    	return sb.toString();
	    }
}
其中Code结构

public class Code {
	public int Opcode;//操作码
	public ArrayList<String> Operands;
	public Code(int op)
	{
		Operands=new ArrayList<String>();
		Opcode=op;
	}
	 @Override
	    public String toString()
	    {
	    	StringBuffer sb=new StringBuffer();
	    	sb.append(Opcode);
	    	sb.append(" ");
	    	for(String s:Operands)
	    	{
	    		sb.append(s+" ");
	    	}
	    	return sb.toString();
	    }
}

完成一个Class的后端代码的生成工作,只需要调用语法树根节点(classdef)的genCode即可完成,因为根节点会不断的递归调用其子树的genCode方法,因此在递归调用的时候需要某些机制进行各方法之间信息的传递,这里建立新类BackendClassManager来进行信息的传递,消除耦合。

public class BackendClassManager {
	public static ClassOfClass cc=new ClassOfClass();//正在生成的class对象
	public static function tFunc;//正在生成代码的函数
	public static memberfuncdeclare mfc;//正在生成代码的语法树中的memberfuncdeclare节点
	public static constructor ct;//正在生成代码的构造函数节点
	public static HashMap<String,Integer> nameSlot=new HashMap<String,Integer>();//局部变量和局部变量表槽的对应hash
	public static expr expr1,expr2;//正在翻译的 expr
	public static Stack<ArrayList<Code>> loopcontinue=new Stack<ArrayList<Code>>();//在循环语句中出现的continue语句,用于回填地址
	public static Stack<ArrayList<Code>> loopbreak=new Stack<ArrayList<Code>>();//在循环语句中出现的break语句,用于回填地址
	public static void WriteToClassFile()
	{
		String path="E:/test.class";
		cc.WriteClassFile(path);
	}
	public static void genCode(classdef cd)
	{
		cd.genCode();
	}
}
cc代表目前正在编译的class。

tFunc代表正在生成代码的函数,也就是所有genCode的方法都要把生成的代码填充到tFunc的codes域中。

memberfuncdeclare代表语法树中正在生成后端代码的memberfuncdeclare节点,该节点和其子树包含此函数的所有代码。

nameSlot对应源码中出现的局部变量与目标代码中的局部变量表槽的一个关系,因为目标代码将不再会出现局部变量名这个概念,所以需要一个hash在编译时进行对应。

expr1和expr2对应正在翻译的expr,在某些运算符需要进行类型转换时需要用到正在翻译的表达式的信息。

loopcontinue和loopbreak用于循环语句的地址回填,因为一个循环在翻译的过程中,其break需要跳转到的地址是还未确定的,需要整个循环翻译完之后对目标地址进行回填。continue虽然可以确定目标地址,但是在continue对应的语句stmt节点无法知道循环的开始地址,需要通过某些机制让stmt节点知道此循环的开始地址,因此也把continue语句进行回填处理。

除此之外还是用了一个类CodeGenHelper来封装一些常用的代码序列,比如i2d,jmp等,目的是为了简化之后的目标代码的生成。


2、节点代码生成

按照从顶至低的方式依次分析。

(1)classdef节点

public void genCode() {
		BackendClassManager.cc.name=cn.toString();
		cb.genCode();
	}
代码很简单,首先把正在编译的类名设置成classdef中出现的类名,然后调用classbody的genCode方法。


(2)classbody

public void genCode() {
		if(cb!=null)
		{
			cb.genCode();
		}
		
	}
依然很简单,如果classmembers不为空,则调用classmembers的genCode方法。

值得注意的是,这些方法本身并没有生成目标代码乃是因为一个类的定义本身并不包含任何逻辑,而代码本身是对逻辑的阐述,所以在类声明、函数声明、成员变量声明等没有生成任何有意义的代码也就不值得奇怪了。


(3)classmembers

public void genCode() {
		if(type==0)
		{
			((membervardeclare)declare).genCode();
		}
		else if(type==1)
		{
			((memberfuncdeclare)declare).genCode();
		}
		else if(type==2)
		{
			ct.genCode();
		}
		if(cm!=null)
		{
			cm.genCode();
		}
	}
根据此classmembers的类型,对membervardeclare、memberfuncdeclare、constructor调用genCode方法,最后对下一个classmemebers调用genCode方法,这和本系列第一篇博客中的递推式是对应的。


(4)membervardeclare

public void genCode() {
		// TODO Auto-generated method stub
		
		field fd=new field();
		
		ArrayList<field> fs=BackendClassManager.cc.fields;
		if(af.toString().equals("public"))
		{
		fd.head+=ClassOfClass.isPublic;
		}
		if(isstatic==true)
		{
			fd.head+=ClassOfClass.isStatic;
		}
		fd.type=tp.toString();
		fd.fieldname=ID.toString();
		fs.add(fd);
	}
依然没有任何代码生成,只是将成员变量的信息放到ClassOfClass对象的fields域中。

(5)memberfundeclare

public void genCode() {
		function func=new function();
		BackendClassManager.tFunc=func;
		BackendClassManager.cc.functions.add(func);
		BackendClassManager.ct=null;
		BackendClassManager.mfc=this;
		BackendClassManager.nameSlot=new HashMap<String,Integer>();
		if(af.toString().equals("public"))
		{
		func.head+=ClassOfClass.isPublic;
		}
		func.rettype=tp.toString();
		if(da!=null)
		{
		ArrayList<type> al=da.gettypelist();
		func.argnum=al.size();
		
		for(type tp:al)
		{
			func.args.add(tp.toString());
		}
		ArrayList<id> tal=da.getidlist();
		BackendClassManager.nameSlot.put("this", 0);
		for(int i=0;i<=tal.size()-1;i++)
		{
			BackendClassManager.nameSlot.put(tal.get(i).toString(), i+1);
		}
		}
		else
		{
			func.argnum=0;
		}
		fb.genCode();
	
	}
成员函数的稍微有点复杂。

首先建立一个新的function对象,并把该对象设置为BackendClassManager.tFun,说明之后所有genCode都为这个函数生成的代码,并把这个函数加到classofClass对象的functions域中;判断该函数的返回值类型、是否是public、是否是静态,并把相关信息记录到function对象中重置nameslot,将函数所有参数压入nameslot表中,并注意this也当做参数放入表中,然后调用functionbody的genCode,为该函数生成代码。

(6)constructor

public void genCode() {
		
		function func=new function();
		BackendClassManager.tFunc=func;
		BackendClassManager.ct=this;
		BackendClassManager.nameSlot=new HashMap<String,Integer>();
		if(af.toString().equals("public"))
		{
		func.head+=ClassOfClass.isPublic;
		}
		func.rettype="NULL";
		if(da!=null)
		{
		ArrayList<type> al=da.gettypelist();
		func.argnum=al.size();
		func.argnum=al.size();
		for(type tp:al)
		{
			func.args.add(tp.toString());
		}
		ArrayList<id> tal=da.getidlist();
		BackendClassManager.nameSlot.put("this", 0);
		for(int i=0;i<=tal.size()-1;i++)
		{
			BackendClassManager.nameSlot.put(tal.get(i).toString(), i+1);
		}
		}
		else
		{
			func.argnum=0;
		}
		ss.genCode();
		
	}

(7)funcbody

	public void genCode() {
		// TODO Auto-generated method stub
		ss.genCode();
		returnexpr.genCode();
		ArrayList<Code> al=BackendClassManager.tFunc.codes;
		Code code=new Code(0x19);
		al.add(code);
	}

首先stmts生成代码,然后为返回表达式生成代码,最后在codes中加入Code(0x19),也就是返回指令。

值得注意的是,这里做了一个约定,在expr的genCode中,总是把该expr的结果放在expr代码执行后的栈顶,因此函数返回时实际上返回的是栈顶元素的值。


(未完待续)



你可能感兴趣的:(自制编译器:后端代码生成(一))