본문 바로가기

Programing/JVM(Java, Kotlin)

[Java] 자바 컴파일러 - parse(파스)

이전에 자바 컴파일러의 컴파일 단계라는 글을 쓴 적이 있다.

사실은 그 컴파일 이전에 구문 분석 트리를 만드는 작업을 수행한다.

이 글은 그 과정에 대해 다룬다.

 

자바에서는 JSP등에서 Runtime 중 동적 컴파일링을 할 수있는 도구를 제공한다.

ToolProvider 라는 서비스 로더를 통해 시스템 자바 컴파일러를 가져올 수 있다.

아래 코드는 자바 컴파일러를 가져오는 문장이다.

import javax.tools.JavaCompiler;
import javax.tools.ToolProvider;

public class JavaCompilerTest {
    public static void main(String[] args) {
        final JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        // ..

이후 JavaCompiler 라는 인터페이스를 통해 컴파일 할 수 있는 작업(task)를 획득 후, call() 메서드를 호출하면 컴파일을 할 수 있다.

대략적인 모습은 아래와 같다.

import com.naver.cafe.javachobostudy.jujitsu.StringSource;

import javax.tools.JavaCompiler;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;

public class JavaCompilerTest {

    public static void main(String[] args) {
        final JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        Iterable<JavaFileObject> sources = // ...
        final JavaCompiler.CompilationTask task = compiler.getTask(
        	null, null, null, null, null, sources);
        Boolean success = task.call();
    }
}

call() 호출 결과는 Boolean 타입이며 성공, 실패를 나타낸다.

Boxed(wrapper) 타입이긴 하나 내부의 코드는 boolean이기에 null로 반환될 경우는 없다.

// JavacTaskImpl.java
package com.sun.tools.javac.api;

public class JavacTaskImpl extends BasicJavacTask {
    private Result result;
    // ..
    
    public Boolean call() {
        return this.doCall().isOK();
    }
    
// Main.java
package com.sun.tools.javac.main;

public class Main {
    // ..

    public static enum Result {
        OK(0),
        ERROR(1),
        CMDERR(2),
        SYSERR(3),
        ABNORMAL(4);

        public final int exitCode;

        private Result(int exitCode) {
            this.exitCode = exitCode;
        }

        public boolean isOK() {
            return this.exitCode == 0;
        }
    }
}

JavaCompiler는 compile 이름의 오버로딩되어 있는 메서드가 두 개 있는데 그중 인자가 많은 쪽에 구현 코드가 적혀있다.

public void compile(List<JavaFileObject> sourceFileObjects,
                    List<String> classnames,
                    Iterable<? extends Processor> processors)
{
    // ..

    try {
        initProcessAnnotations(processors);

        delegateCompiler =
            processAnnotations(
                enterTrees(stopIfError(CompileState.PARSE, parseFiles(sourceFileObjects))),
                classnames);

        delegateCompiler.compile2();
        // ..
    } catch (Abort ex) {
        // ..
    }
}

마치 PARSE 과정의 부분과 유사하게 코드가 적혀있는데, 괄호의 안쪽부터 수행된다.

1. parseFiles

2. stopIfError

3. enterTrees

4. complie2 : 이것이 지난번에 기록했던 글의 과정이다.

 

1. parseFiles

입력으로 받은 파일들을 parse하는 과정이다.

public List<JCCompilationUnit> parseFiles(Iterable<JavaFileObject> fileObjects) {
   if (shouldStop(CompileState.PARSE))
       return List.nil();

    //parse all files
    ListBuffer<JCCompilationUnit> trees = new ListBuffer<>();
    Set<JavaFileObject> filesSoFar = new HashSet<JavaFileObject>();
    for (JavaFileObject fileObject : fileObjects) {
        if (!filesSoFar.contains(fileObject)) {
            filesSoFar.add(fileObject);
            trees.append(parse(fileObject));
        }
    }
    return trees.toList();
}

2. stopIfError

파싱하는 과정에서 에러가 발생하면 컴파일을 해도 의미가 없기 때문에 중간에 컴파일을 종료할 수 있게 에러를 체크하는 과정이다.

protected final <T> Queue<T> stopIfError(CompileState cs, Queue<T> queue) {
    return shouldStop(cs) ? new ListBuffer<T>() : queue;
}

3. enterTrees

public List<JCCompilationUnit> enterTrees(List<JCCompilationUnit> roots) {
    //enter symbols for all files
    if (!taskListener.isEmpty()) {
        for (JCCompilationUnit unit: roots) {
            TaskEvent e = new TaskEvent(TaskEvent.Kind.ENTER, unit);
            taskListener.started(e);
        }
    }

    enter.main(roots);

    if (!taskListener.isEmpty()) {
        for (JCCompilationUnit unit: roots) {
            TaskEvent e = new TaskEvent(TaskEvent.Kind.ENTER, unit);
            taskListener.finished(e);
        }
    }

    // If generating source, or if tracking public apis,
    // then remember the classes declared in
    // the original compilation units listed on the command line.
    if (needRootClasses || sourceOutput || stubOutput) {
        ListBuffer<JCClassDecl> cdefs = new ListBuffer<>();
        for (JCCompilationUnit unit : roots) {
            for (List<JCTree> defs = unit.defs;
                 defs.nonEmpty();
                 defs = defs.tail) {
                if (defs.head instanceof JCClassDecl)
                    cdefs.append((JCClassDecl)defs.head);
            }
        }
        rootClasses = cdefs.toList();
    }

    // Ensure the input files have been recorded. Although this is normally
    // done by readSource, it may not have been done if the trees were read
    // in a prior round of annotation processing, and the trees have been
    // cleaned and are being reused.
    for (JCCompilationUnit unit : roots) {
        inputFiles.add(unit.sourcefile);
    }

    return roots;
}

 

크게 보면 간단해 보이지만 실제 간단하지 않다.

1-1. parse

위에서 parseFiles안의 작업은 실제 parse라는 메서드에 의해 수행된다.

public JCTree.JCCompilationUnit parse(JavaFileObject filename) {
    JavaFileObject prev = log.useSource(filename);
    try {
        JCTree.JCCompilationUnit t = parse(filename, readSource(filename));
        if (t.endPositions != null)
            log.setEndPosTable(filename, t.endPositions);
        return t;
    } finally {
        log.useSource(prev);
    }
}

protected JCCompilationUnit parse(JavaFileObject filename, CharSequence content) {
    long msec = now();
    JCCompilationUnit tree = make.TopLevel(List.<JCTree.JCAnnotation>nil(),
                                  null, List.<JCTree>nil());
    if (content != null) {
        if (verbose) {
            log.printVerbose("parsing.started", filename);
        }
        if (!taskListener.isEmpty()) {
            TaskEvent e = new TaskEvent(TaskEvent.Kind.PARSE, filename);
            taskListener.started(e);
            keepComments = true;
            genEndPos = true;
        }
        Parser parser = parserFactory.newParser(content, keepComments(), genEndPos, lineDebugInfo);
        tree = parser.parseCompilationUnit();
        if (verbose) {
            log.printVerbose("parsing.done", Long.toString(elapsed(msec)));
        }
    }

    tree.sourcefile = filename;

    if (content != null && !taskListener.isEmpty()) {
        TaskEvent e = new TaskEvent(TaskEvent.Kind.PARSE, tree);
        taskListener.finished(e);
    }

    return tree;
}