欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Javac工作原理分析(1):词法分析器

程序员文章站 2022-05-23 10:14:06
...

Javac分四个模块:词法分析器,语法分析器,语义分析器,代码生成器。
整个流程如下图:


Javac工作原理分析(1):词法分析器

本文先记录词法分析器的工作过程,文中出现的源码出自Jdk9
首先来看一下词法分析器设计的类图:


Javac工作原理分析(1):词法分析器

Lexer和Parser:Javac的主要词法分析器的接口类
Scanner:Lexer的默认实现类,逐个读取java源文件的单个字符,对读取到的词法进行归类
JavacParser:规定哪些词是符合Java语言规范的
Token:规定了所有Java语言的合法关键词
Names:存储和表示解析后的词法

词法分析过程是在JavacParser的parseCompilationUnit方法中完成的:

public JCTree.JCCompilationUnit parseCompilationUnit() {
        Token firstToken = token;
        JCModifiers mods = null;
        boolean consumedToplevelDoc = false;
        boolean seenImport = false;
        boolean seenPackage = false;
        ListBuffer<JCTree> defs = new ListBuffer<>();
        if (token.kind == MONKEYS_AT)
            // 解析修饰符 "@"
            mods = modifiersOpt();

        if (token.kind == PACKAGE) {
            // 解析package声明
            int packagePos = token.pos;
            List<JCAnnotation> annotations = List.nil();
            seenPackage = true;
            if (mods != null) {
                checkNoMods(mods.flags);
                annotations = mods.annotations;
                mods = null;
            }
            nextToken();
            JCExpression pid = qualident(false);
            accept(SEMI);
            JCPackageDecl pd = F.at(packagePos).PackageDecl(annotations, pid);
            attach(pd, firstToken.comment(CommentStyle.JAVADOC));
            consumedToplevelDoc = true;
            storeEnd(pd, token.pos);
            defs.append(pd);
        }

        boolean checkForImports = true;
        boolean firstTypeDecl = true;
        while (token.kind != EOF) {
            if (token.pos <= endPosTable.errorEndPos) {
                // error recovery
                skip(checkForImports, false, false, false);
                if (token.kind == EOF)
                    break;
            }
            if (checkForImports && mods == null && token.kind == IMPORT) {
                // 解析import声明
                seenImport = true;
                defs.append(importDeclaration());
            } else {
                // 解析class主体
                Comment docComment = token.comment(CommentStyle.JAVADOC);
                if (firstTypeDecl && !seenImport && !seenPackage) {
                    docComment = firstToken.comment(CommentStyle.JAVADOC);
                    consumedToplevelDoc = true;
                }
                if (mods != null || token.kind != SEMI)
                    mods = modifiersOpt(mods);
                if (firstTypeDecl && token.kind == IDENTIFIER) {
                    ModuleKind kind = ModuleKind.STRONG;
                    if (token.name() == names.open) {
                        kind = ModuleKind.OPEN;
                        nextToken();
                    }
                    if (token.kind == IDENTIFIER && token.name() == names.module) {
                        if (mods != null) {
                            checkNoMods(mods.flags & ~Flags.DEPRECATED);
                        }
                        defs.append(moduleDecl(mods, kind, docComment));
                        consumedToplevelDoc = true;
                        break;
                    } else if (kind != ModuleKind.STRONG) {
                        reportSyntaxError(token.pos, "expected.module");
                    }
                }
                JCTree def = typeDeclaration(mods, docComment);
                if (def instanceof JCExpressionStatement)
                    def = ((JCExpressionStatement)def).expr;
                defs.append(def);
                if (def instanceof JCClassDecl)
                    checkForImports = false;
                mods = null;
                firstTypeDecl = false;
            }
        }
        JCTree.JCCompilationUnit toplevel = F.at(firstToken.pos).TopLevel(defs.toList());
        if (!consumedToplevelDoc)
            attach(toplevel, firstToken.comment(CommentStyle.JAVADOC));
        if (defs.isEmpty())
            storeEnd(toplevel, S.prevToken().endPos);
        if (keepDocComments)
            toplevel.docComments = docComments;
        if (keepLineMap)
            toplevel.lineMap = S.getLineMap();
        this.endPosTable.setParser(null); // remove reference to parser
        toplevel.endPositions = this.endPosTable;
        return toplevel;
    }

从上述代码中可看出Javac分析词法的原貌,从源文件的一个字符开始,按照Java语法规范一次找出package、import、类定义,以及属性和方法定义等,最后构建一个抽象语法树。
基于上面的原理,下面代码的Token流如图所示。

package test;
public class Cifa {
    int a;
    int b = a + 1;
}

Javac工作原理分析(1):词法分析器
在上图的Token流中,除了在Java语言规范中定义的保留关键字,还有一个特殊的词TokenKind.IDENTIFIER,这个Token用于表示用户定义的名称,如类名、包名、变量名、方法名等等。
接下来带着两个问题继续深入:
1:Javac怎么知道package就是一个TokenKind.PACKAGE,而不是用户自定义的TokenKind.IDENTIFIER的名称呢?
2:Javac是如何知道哪些字符组合在一起就是一个Token的呢?它是如何从一个字符流中划分出Token来的?

先回答第一个问题:
Javac在进行词法分析时会由JavacParser根据Java语言规范来控制什么顺序、什么地方应该出现什么Token。下面以package关键词为例来说明词法分析器是如何解析词法的。
看一下JavacParser的构造函数:

protected JavacParser(ParserFactory fac,
                     Lexer S,
                     boolean keepDocComments,
                     boolean keepLineMap,
                     boolean keepEndPositions,
                     boolean parseModuleInfo) {
    this.S = S;
    nextToken(); // prime the pump
    ...忽略无关代码...
}
public void nextToken() {
    S.nextToken();
    token = S.token();
}

在创建JavacParser对象的构造函数时,Scanner会读取第一个Token,而这个Token就是TokenKind.PACKAGE,至于这个Token是怎么分辨出来的,将在第二个问题中回答。前面说了词法分析的整个过程是在JavacParser的parserCompilationUnit方法中完成的,我们再接着看看这个方法的第9行代码。这里是判断当前的Token是不是PACKAGE,如果是的话就会读取整个package的定义。我们看这个if中的执行过程:会接着读取下一个Token,而这个Token就是第二个TokenKind.IDENTIFIER,然后调用qualident方法。

public JCExpression qualident(boolean allowAnnos) {
        JCExpression t = toP(F.at(token.pos).Ident(ident()));
        while (token.kind == DOT) {
            int pos = token.pos;
            nextToken();
            List<JCAnnotation> tyannos = null;
            ......
            t = toP(F.at(pos).Select(t, ident()));
            ......
        }
        return t;
    }

这段代码的第一行是根据TokenKind.IDENTIFIER的Token构建一个JCIdent的语法节点,然后去取下一个Token,判断这个Token是否是TokenKind.DOT。如果是的话,就进入while循环,读取整个package定义的类路径名称;最后调用accept(SEMI)收尾,该函数判断下一个Token是不是一个Token.SEMI(分号’;’),这样整个”package test;”代码就解析完成了。

接下来回答第二个问题:
package语法、import语法、类定义、field定义、method定义、变量定义、表达式定义等,主要也就这些规则,而这些规则除了一些Java语法规定的关键词就是用户自定义的变量名称了。自定义变量名称包含包名、类名、变量名、方法名,关键词和自定义变量名称之间用空格隔开,每个语法表达式用分号结束。如何判断哪些字符组合是一个Token的规则是在Scanner的nextToken方法中定义的,没调用一次这个方法就会构造一个Token,而这些Token必然是com.sun.tools.javac.parser.Tokens.Token中的任何元素之一。
实际上在读取每一个Token时都需要一个转换过程,如在package中的”test”包名要转换成TokenKind.IDENTIFIER类型,在Java源码中的所有字符集合都要找到在com.sun.tools.parser.Tokens.Token中定义的对应关系,这个任务是在com.sun.tools.javac.parser.Tokens类中完成,Tokens负责将所有字符集合对应到Token集合中(在JDK9中Token其实是Tokens的静态内部类,在更早的JDK版本中是有Keywords类来完成Tokens的工作的)。字符集合到Token转换相关的类关系图如下图所示


Javac工作原理分析(1):词法分析器

每个字符集合都会是一个Name对象,所有的Name对象都存储在Name.Table这个内部类中,这个类就是对应的这个类的符号表。Tokens会将在Token中所有的元素按照它们的Token.name先转化成Name对象,然后建立Name对象和Token的对应关系,这个关系保存在Tokens类中的key数组中,这个key数组只保存了在Token类中定义的所有Token到Name对象的关系,而其他所有字符集合Tokens都会将它对应到TokenKind.IDENTIFIER类型,如下述代码所示:

TokenKind lookupKind(Name name) {
    return (name.getIndex() > maxKey) ? TokenKind.IDENTIFIER : key[name.getIndex()];
}

其中maxKey的值是在这个Token中最后一个Token对应的Name在Name.Table表中的起始位置。而Name.Table表的前面几个显然都是在Token类中定义的符号。不在Token中的Name对象Index肯定大于keyMax,所以默认的都是IDENTIFiER。

相关标签: javac jvm