본문 바로가기

Programing/JVM(Java, Kotlin)

[javac] 자바 컴파일러: 컴파일 단계

질문:

- 질문 제목 : 클래스의 순서는 어떤 기준으로 정하나요??
- 질문 내용 :  main메서드에 Time객체를 생성하고

참조변수 t를통해 시간을 호출하는데 Time 객체를 생성하는 클래스가 선언된곳보다 뒤에 있어도 상관 없는건가요??? 코드는 위에서 아래로 진행되는거 아닌가요???

public class Exe5 {
    public static void main(String[] args) {
        Time t = new Time();
        t.hour = 12;
        t.minute = 34;
        t.second = 56;

        System.out.println(t.hour);
        System.out.println(t.minute);
        System.out.println(t.second);
    }
}

class Time {
    int hour;
    int minute;
    int second;
}

일단 main 함수 기준으로 아래와 같이 짰다면 에러가 발생한다.

public class Exe5 {
    public static void main(String[] args) {
        t.hour = 12;
        t.minute = 34;
        t.second = 56;
        Time t = new Time();	// 여기가 내려왔다.
        
        System.out.println(t.hour);
        System.out.println(t.minute);
        System.out.println(t.second);
    }
}

class Time {
    int hour;
    int minute;
    int second;
}
error: cannot find symbol
t.hour = 12;
^
  symbol:   variable t
  location: class Exe5

일단 질문대로 코드는 위에서 아래로 흐름을 갖는다.

 

그렇다면 Time 클래스의 경우 Exe5보다 아래에 있는데 컴파일러는 어떻게 해당 심볼(class)를 찾을 수 있을까?

(C언어의 경우 이런 경우에 에러가 난다. 그래서 타입의 선언을 호출하는 쪽 앞으로 미리 당기는 구문이 존재한다.)

 

만약 Time이 없는 채로 컴파일을 하게된다면?

아래와 같은 타입을 찾을 수 없는 에러가 발생한다.

Exe5.java:5: error: cannot find symbol
        Time t = new Time();
        ^
  symbol:   class Time
  location: class Exe5
Exe5.java:5: error: cannot find symbol
        Time t = new Time();
                     ^
  symbol:   class Time
  location: class Exe5
2 errors

컴파일 하는 여러 단계 중 ATTR 단계에서 에러가 발생하게 된다. (아래에 나온다)

컴파일 상태에 따른 단계

javac 컴파일러의 경우 총 10가지의 상태를 가지는데 CompileStates 이라는 enum에 정의되어 있다.

package com.sun.tools.javac.comp;
public class CompileStates extends HashMap<Env<AttrContext>, CompileStates.CompileState> {
    // ...

    /** Ordered list of compiler phases for each compilation unit. */
    public enum CompileState {
        INIT(0),
        PARSE(1),
        ENTER(2),
        PROCESS(3),
        ATTR(4),
        FLOW(5),
        TRANSTYPES(6),
        UNLAMBDA(7),
        LOWER(8),
        GENERATE(9);
        // ...

이 상태는 실질적인 컴파일을 하는 JavaCompiler에 정의되어 현재 상태를 기록한다.

package com.sun.tools.javac.main;
public class JavaCompiler {
    // ...
    protected CompileStates compileStates;
    // ...

PARSE

또한 컴파일러에 대한 지식이 있다면 소스코드를 토큰화하여 파싱트리를 구성하는 것을 알고 있을 것이다.

CompileStates.PARSE 에 해당하고 JavaCompiler의compile 메서드에서 수행한다. (아래코드에서 parseFiles부분이다)

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

자바에서는 구문분석 트리를 JCTree (JavaCompilerTree)라는 타입으로 구성을 한다.

package com.sun.tools.javac.tree;
public abstract class JCTree implements Tree, Cloneable, DiagnosticPosition {
    // ..
    public static class JCClassDecl extends JCStatement implements ClassTree

PROCESS

파싱이 끝났으면 컴파일 정책에 따라 다음 단계로 수행된다. 정책은 CompilePolicy 으로 5가지를 가진다.

package com.sun.tools.javac.main;
public class JavaCompiler {
    // ...
    protected static enum CompilePolicy {
        ATTR_ONLY,
        CHECK_ONLY,
        SIMPLE,
        BY_FILE,
        BY_TODO;
    // ...

ATTR_ONLY는 파스 트리의 속성만

CHECK_ONLY는 파스트리의 속성과 흐름 분석만

SIMPLE는 파스트리의 속성과 흐름 분석, 그리고 설탕물빼기(desugar) 그리고 결과물 생성까지 수행한다. 어떤 에러가 발생하게 되면 결과물을 만들지 않는다.

등의 단계별로 하는 일이 많아진다. 보통은 BY_TODO 정책으로 아래와 같이 모든 프로세스를 다 수행한다.

generate(desugar(flow(attribute(todo.remove()))));

ATTR

이 단계는 파스 트리에 속성(attribute)를 추가하며 주요 문맥 의존을 파악하는 단계이다.

package com.sun.tools.javac.comp;
public class Attr extends JCTree.Visitor {
    // ..
    public void attrib(Env<AttrContext> env) {
        switch (env.tree.getTag()) {
            case MODULEDEF:
                attribModule(env.tree.pos(), ((JCModuleDecl)env.tree).sym);
                break;
            case TOPLEVEL:
                attribTopLevel(env);
                break;
            case PACKAGEDEF:
                attribPackage(env.tree.pos(), ((JCPackageDecl) env.tree).packge);
                break;
            default:
                attribClass(env.tree.pos(), env.enclClass.sym);
        }
    }

바로 위에서 이야기 했던 Time 클래스가 컴파일시 없을 경우 이 단계에서 에러가 발생하게 된다.

package com.sun.tools.javac.tree;
public abstract class JCTree implements Tree, Cloneable, DiagnosticPosition {
    // ..
    final Types types;
    private Type capture(Type type) {
        return types.capture(type);
    }
    // ...

package com.sun.tools.javac.code;
public class Types {
    protected static final Context.Key<Types> typesKey = new Context.Key<>();

    final Symtab syms;

FLOW

이 단계는 ATTR가 수행된 파스 트리의 데이터 흐름에 대한 체크를 수행한다. analyzeTree가 호출된다.

package com.sun.tools.javac.comp;
public class Flow {
    // ...
    public void analyzeTree(Env<AttrContext> env, TreeMaker make) {
        new AliveAnalyzer().analyzeTree(env, make);
        new AssignAnalyzer().analyzeTree(env);
        new FlowAnalyzer().analyzeTree(env, make);
        new CaptureAnalyzer().analyzeTree(env, make);
    }

UNLAMBDA, LOWER

단계 이름은 desugar이지만 상태는 UNLAMBDA나 LOWER로 설정될 수 있다.

GENERATE

마지막 단계로 코드를 생성하고 class파일로 기록하는 역할을 한다.

JNI 코드가 있다면 JNIWriter가 개입할 수 있으나 아니라면 ClassWriter가 사용된다.

package com.sun.tools.javac.jvm;
public class ClassWriter extends ClassFile {
    // ..
    public JavaFileObject writeClass(ClassSymbol c)
        throws IOException, PoolOverflow, StringOverflow
    // ..
    public void writeClassFile(OutputStream out, ClassSymbol c)
        throws IOException, PoolOverflow, StringOverflow {