java编译器源码分析之语法分析器

token流到抽象语法树的过程是语法分析。
前面认识到token流,这部分将介绍抽象语法树(AST)。
那么什么是抽象语法树(AST)?AST长啥样?我们的token流是如何转变成AST的?下面围绕这三个问题展开讨论。

针对什么是抽象语法树以及语法树长啥样两个问题。可以看看这篇博客,文章对于语法树的结构和原理阐述的很清楚。在这里我想说的是:①抽象语法树是源代码抽象树结构的另一种表示;②抽象语法树是一种独立于源语言的语言结构,它有自己的语言规范;③抽象语法树在不同编译器中实现的方法不一样,比如c的编译器和javac,因此抽象语法树适用于多种语言。

由于语法分析的源码过于繁杂,不可能穷尽源码中的各种情况,因此我将以解析下面实例代码为路径说明语法解析的过程。
实例代码:

package com.test.syx;
import java.util.ArrayList;
import java.util.List;

// test
public class ASTTest {
	// a
	private static int a = 3;
	
	public int getA() {
		return a;
	}
	
	public static void main() {
		List<Integer> list = new ArrayList<Integer>();
		list.add(a);
		System.out.println("hello world!");
	}
}

javac根据token来生成不同的树节点。比如token是PACKAGE时生成一个JCExpression树节点;token是IMPORT时生成一个JCTree节点;token为CLASS时生成一个JCTree节点;每个树节点带有不同的子节点。最后三个节点形成一个JCTree.JCCompilationUnit对象,此对象就是这个类的抽象语法树。下面可分别来看一下他们的主要步骤:

Token.PACKAGE
if (S.token() == PACKAGE) {
            if (mods != null) {
                checkNoMods(mods.flags);
                packageAnnotations = mods.annotations;
                mods = null;
            }
            S.nextToken();
            pid = qualident();
            accept(SEMI);
        }

步骤1:if代码块的意思是判断package前是否有@注解,如果有则保存注解到packageAnnotations列表中;
步骤2:通过com.sun.tools.javac.parser.Scanner.nextToken()获取下一个token;
步骤3:com.sun.tools.javac.parser.Parser.qualident()解析包名生成包的AST(JCExpression);
步骤4:以分号结尾;

生成语法树的步骤3是重点,可详细来看下:

public JCExpression qualident() {    	
        JCExpression t = toP(F.at(S.pos()).Ident(ident()));
        while (S.token() == DOT) {
            int pos = S.pos();
            S.nextToken(); 
            t = toP(F.at(pos).Select(t, ident()));
        }
        
        return t;
    }

步骤1:通过com.sun.tools.javac.tree.TreeMaker.Ident(Name)方法建立一个JCIdent类型的树节点;节点name为token.name,节点tag为IDENT(35),节点的symbol为null,节点的pos为当前token的pos;
步骤2:如果token为DOT(.),则进入步骤3;否则直接返回返回生成的树节点(JCExpress);
步骤3:记录当前词法解析器的token位置pos,并获取该token;
步骤4:在pos位置通过com.sun.tools.javac.tree.TreeMaker.Select(JCExpression, Name)方法生成JCFieldAccess类型的节点;节点name为token.name,节点tag为SELECT(34),节点symbol为null,节点pos为token的位置;节点JCExpression为上一个的树节点;获取下一个token
步骤5:执行步骤2;

此时,package这一行构建了一个抽象语法树(包含JCIdent和JCFieldAccess树节点)。
package的抽象语法树节点

Token.IMPORT
JCTree importDeclaration() {
        int pos = S.pos();
        S.nextToken();
        boolean importStatic = false;
        if (S.token() == STATIC) {
            checkStaticImports();
            importStatic = true;
            S.nextToken();
        }
        JCExpression pid = toP(F.at(S.pos()).Ident(ident()));
        do {
            int pos1 = S.pos();
            accept(DOT);
            if (S.token() == STAR) {
                pid = to(F.at(pos1).Select(pid, names.asterisk));//导入“.*"的情况
                S.nextToken();
                break;
            } else {
                pid = toP(F.at(pos1).Select(pid, ident()));
            }
        } while (S.token() == DOT);
        accept(SEMI);
        return toP(F.at(pos).Import(pid, importStatic));
    }

步骤1:处理静态导入的情况,如果jdk大于1.5则checkStaticImports()返回true;
步骤2:通过com.sun.tools.javac.tree.TreeMaker.Ident(Name)方法生成JCIdent类型的树节点;
步骤3:处理DOT后面的标识符,如果token为STAR(*),则生成JCFieldAccess树节点,并结束循环返回生成的树;如果是正常的标识符则正常生成树节点;
步骤4:分号结束;
步骤5:生成JCImport类型的树节点,节点tag为IMPORT(2),节点子节点为上步骤的各树节点;
import的树节点

Token.CLASS

针对类以及类的属性和方法构建抽象语法树的过程比较繁杂,源码中对约定位置出现约定的内容进行不同的处理,可以看出java语言是一种规范性较强的语言。
步骤1:通过com.sun.tools.javac.parser.Parser.modifiersOpt(JCModifiers)方法建立class关键字前面修饰符(public/static/final)的树节点JCModifiers;
步骤2:通过com.sun.tools.javac.parser.Parser.classOrInterfaceOrEnumDeclaration(JCModifiers, String)建立类或者接口或者枚举类的抽象语法树;

整个实例代码的抽象语法是如图

实例代码的抽象语法树

总结

通过对源码的分析可知:
1> 抽象语法树的每一个节点都是一个JCXXX类型的对象;这个JCXXX继承了JCTree类并实现XXXTree接口;
2> JCTree类中定义了各种各样的标签,用来鉴别树节点的类型。标签用int整形值表示,IMPORT= 2,CLASSDEF=3,METHODDEF=4等;针对每一种标签定义了对应标签的树节点,比如JCTree.INDETIFIER的节点类型是JCIden,JCTree.SEMI的节点类型是JCSkip,JCTree.PRIVATE或者JCTree.STATIC的节点类型是JCModifiers,JCTree.CLASSDEF原生数据类型int/Boolean/double等的节点类型是JCPrimitiveTypeTree。这里可以思考一下为什么要定义不同类型的节点?
3> 每一个树节点下除了tag标签外,还有不同的信息;特别对于树节点的嵌套,比如建立package的抽象语法树时,name为com,type为JCIdent的树节点作为一个JCExpression嵌套在name为test,type为JCFieldAccess树节点内,而它又作为JCExpression嵌套在name为syx,type为JCFieldAccess树节点内。
4> 最后把package的节点树,import的节点树和class节点树封装成一个JCCompilationUnit节点,此节点标签tag为TOPLEVEL,表明为顶层节点。

至此Token流到抽象语法树的过程已经结束,可以看出词法分析和语法分析是融为一体的,我在分析的时候是拆开分析的。即得到一个token就会形成树节点(这里表述不太准确,并非一个token是一个树节点,比如定义的成员变量前面的修饰符private static则为一个树节点)。

参考资料

《javaweb技术内幕分析》
针对package-info.java的知识点:https://www.cnblogs.com/pepcod/archive/2013/02/20/2918856.html
针对抽象语法树的知识点:https://blog.csdn.net/philosophyatmath/article/details/38170131
https://blog.csdn.net/Dear_Mr/article/details/72587908?locationNum=2&fps=1
针对语法解析的知识点:https://blog.csdn.net/wang_jiao_jiao/article/details/79715548

注:个人还在学习阶段,博客仅以记载学习过程,如有错误还望大佬批评指正。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值