Polyglot

Copyright (c) 2016-, All Rights Reserved by Kwanghoon Choi
(아래 내용을 자유롭게 이용하되 다른 웹 사이트 등을 통한 배포를 금합니다.)

[참고] 이 메모는 2016년 12월~2월에 학부 인턴으로 참여한 이화중과 이승휘와 함께한 폴리글롯 스터디의 결과물입니다. 특히 이화중 학생이 주요 내용을 정리하였고 이를 참고하여 메모를 작성하였습니다.

지금까지 배운 폴리글롯 사용법을 활용하여 주어진 Java 프로그램에서 선언했으나 사용하지 않는 클래스, 멤버 변수, 멤버 메소드, 지역 변수를 찾는 분석을 구현해보자.

1. 서론

폴리글롯을 사용하여  Java  프로그램에서 사용하지 않는 심볼을 분석하는 프로그램을 작성한다. 검사 대상은 클래스, 멤버 변수, 멤버 메소드, 지역 변수, 로컬 변수, 클래스 임포트로 제한한다. 클래스, 멤버 변수, 멤버 메소드의 경우 private 접근 제어를 갖는 경우에만 사용되지 않을 가능성이 있다고 가정하였다. 즉 public 접근 제어를 갖는 경우 분석 대상 프로그램에서 사용 하지 않았더라도 다른 소스 프로그램에서 사용할 여지가 있기 때문이다.

이 분석 프로그램을 작성함으로써 폴리글롯에 대한 이해를 높이려는 목적을 가지고 있다.

2. 전체 구조

분석기는 크게 3가지 요소로 구성되어 있다. 분석 과정에서 선언된 심볼들이 무엇인지 유지하는 환경 관리, 전체 AST를 탐색하면서 각 노드에서 사용한 심볼을 마크하고, AST 탐색 후 마크되지 않은 심볼을 사용하지 않은 심볼로 판단한다.

이 분석기를 구현하기 위해 앞에서 설명했던 폴리글롯 구조를 응용한다. 앞에서 설명한 내용 중 주요한 내용을 반복해서 요약하자면 이렇다.

  • 분석기의 Visitor 클래스로 EquGenerator 클래스를 사용한다.
  • 폴리글롯을 확장하여 분석기를 만들때 역시 새로운 언어를 정의하는 것으로 간주되며,
  • EquGenLang 인터페이스에 이 비지터의 방문 시작과 끝에 해당하는 메소드 equGenEnter(Node n, EquGenerator v)와 EquGen(Node n, EquGenerator v)를 선언하고, EquGenLang_c 클래스에서 적절히 구현하여 나중에 추가할 확장 노드의 해당 메소드를 호출하도록 연결한다.
  • 비지터의 enterCall(Node n)과 leaveCall(Node n)에서 EquGenLang_c 객체를 통해서 equGenEnter(n, this)와 equGen(n, this)를 호출한다.
  • 분석을 위해 관심이 있는 각 AST 노드의 확장 노드를 훅킹하기 위해 EquGenExtFactory_c에서 관심있는 확장 노드를 생성하는 훅킹 메소드를 오버라이딩하여 해당 노드를 분석하는 기능을 갖는 새로운 확장 노드를 만들도록 한다. (예: EquGenClassDeclExt 클래스와 이 클래스의 equGenEnter(EquGenerator v)와 equGen(EquGenerator v) 메소드)

앞서 설명한대로 EquGenerator 비지터 클래스를 아래의 코드에서 부터 시작해서 구현한다.

public class EquGenerator extends ContextVisitor {
public EquGenerator(Job job, TypeSystem ts, NodeFactory nf) {
super(job, ts, nf);
}

@Override
public EquGenLang lang() {
return (EquGenLang) super.lang();
}

@Override
public NodeVisitor begin() {
Report.report(1, "EquGenerator: begin()");
NodeVisitor nv = super.begin();
return nv;
}

@Override
public void finish() {
Report.report(1, "EquGenerator: finish()");
super.finish();
}

@Override
protected NodeVisitor enterCall(Node n) throws SemanticException {
return lang().equGenEnter(n, this);
}

@Override
protected Node leaveCall(Node old, Node n, NodeVisitor v)
throws SemanticException {
return lang().equGen(n,  this);
}

@Override
public TypeSystem typeSystem() {
return super.typeSystem();
}

@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}

EquGenerator 클래스의 비지터는 각 AST 노드를 방문할 때 마다 해당 노드의 equGenEnter() 메소드를 호출한다. 따라서 이 노드의 확장 노드 클래스에서 이 메소드를 오버라이딩 함으로써 이 노드를 분석할 수 있다.

2.1 현재 심볼 목록 환경 관리

사용되지 않은 심볼을 찾기 위해서 AST를 방문한 현재 위치에서 선언된 모든 클래스, 멤버 변수, 멤버 메소드, 지역 변수를 유지할 필요가 있다. 이 변수 심볼들을 저장하기 위해 환경을 유지하는데 Java의 HashMap을 사용한다.

private HashMap<JL5ClassType, Boolean> classEnv;
private HashMap<JL5ProcedureInstance, Boolean> methodEnv;
private HashMap<JL5FieldInstance, Boolean> fieldEnv;
private HashMap<JL5ProcedureInstance, HashMap<JL5LocalInstance, Boolean>> localEnv;

각 HashMap은 해당 심볼의 정보를 가지고 있는 클래스와 이 심볼이 사용되었는지 여부를 나타내는 Boolean 타입 변수의 쌍으로 이루어져 있다. 지역 변수에 대한 환경은 예외적으로 이 변수가 선언된 메소드를 구분하여 동일한 이름의 로컬 변수가 여러 메소드에 나타나는 경우를 구분한다. 나중에 조금 더 자세히 설명한다.

AST를 탐색 중에 확인된 사용한 심볼을 따로 목록을 관리한다. [이 목록을 별도로 관리하지 않는 간단한 버전을 생각해볼 수 있다.]

private HashSet usedClasses;
private HashSet usedMethods;
private HashSet usedFields;
private HashMap<JL5ProcedureInstance, HashSet> usedLocals;

현재 분석기의 비지터가 방문 중인 메소드를 저장할 변수가 필요하다.

private static JL5ProcedureInstance currentMethodEnv;

각 Java 소스 파일 마다 환경을 별도로 만들어 관리한다. EquGenerator 클래스의 begin() 메소드에서 환경을 저정할 HashMap 객체를 생성하고 초기화하는 것이 적절하다.

public NodeVisitor begin() {
Report.report(1, "EquGenerator: begin()");
NodeVisitor nv = super.begin();

classEnv = new HashMap<>();
methodEnv = new HashMap<>();
fieldEnv = new HashMap<>();
localEnv = new HashMap<>();

usedClasses = new HashSet<>();
usedMethods = new HashSet<>();
usedFields = new HashSet<>();
usedLocals = new HashMap<>();

return nv;
}

분석기의 비지터가 AST 각 노드를 방문하면서 새로 선언한 심볼을 만날 때마다 이 심볼을 환경에 추가한다. 클래스, 멤버 변수, 멤버 메소드, 지역 변수를 환경에 추가하는 메소드를 다음과 같이 만든다.

private static final boolean defaultUse = false;

public void addToClassEnv(JL5ClassType classType) {
if(classType.flags().isPrivate()) {
classEnv.put(classType, defaultUse);
} 
}

public void addToMethodEnv(JL5ProcedureInstance methodIns) {
if(methodIns.flags().isPrivate()) {
methodEnv.put(methodIns, defaultUse);
} 
}

public void addToFieldEnv(JL5FieldInstance fieldIns) {
if(fieldIns.flags().isPrivate()) {
fieldEnv.put(fieldIns, defaultUse);
}
}

public void addToLocalEnv(JL5LocalInstance localIns) {
try {
if(!localEnv.containsKey(currentMethodEnv)) {
localEnv.put(currentMethodEnv, new HashMap<>());
}
localEnv.get(currentMethodEnv).put(localIns, defaultUse);
} catch (NullPointerException e) {
if(currentMethodEnv == null) {
throw new CurrentMethodEnvNotSetException();
} else {
throw e;
}
}
}

현재 방문하고 있는 메소드를 지정하고 확인하는 메소드를 다음과 같이 만든다.

public void setCurrentMethodEnv(JL5ProcedureInstance methodIns) {
currentMethodEnv = methodIns;
}

public JL5ProcedureInstance getCurrentMethodEnv() {
return currentMethodEnv;
}

특정 심볼이 사용됨을 확인할 때 마다 다음과 같이 마크해둔다.

public void markOnClassEnv(JL5ClassType classType) {
/* Substitution Class Type */
if (classType instanceof JL5SubstClassType) { // 제네릭 클래스인 경우
addToUsedClasses(((JL5SubstClassType) classType).base()); // Base
for (Entry<TypeVariable, ReferenceType> substType 
: ((JL5SubstClassType) classType).subst().substitutions().entrySet()) {
addToUsedClasses((JL5ClassType) substType.getValue()); // Substitutions
}
}

/* Class Type */
else { // (제네릭이 아닌) 일반 클래스인 경우
addToUsedClasses(classType);
}
}

/**
 * usedClasses에 추가할 때, 가시성 검사 코드 중복을 피하기 위해 추가한 메서드
 * @param classType
 */
private void addToUsedClasses(JL5ClassType classType) {
if(classType.flags().isPrivate()) {// 가시성 검사
usedClasses.add(classType);
}
}


public void markOnMethodEnv(JL5ProcedureInstance methodIns) {
if(methodIns.flags().isPrivate()) {// 가시성 검사
usedMethods.add(methodIns);
}
}

public void markOnFieldEnv(JL5FieldInstance fieldIns) {
if(fieldIns.flags().isPrivate()) {// 가시성 검사
usedFields.add(fieldIns);
}
}

public void markOnLocalEnv(JL5LocalInstance localIns) {
// localIns은 가시성을 검사하지 않음.
try {
if(!usedLocals.containsKey(currentMethodEnv)) {
usedLocals.put(currentMethodEnv, new HashSet<>());
}
usedLocals.get(currentMethodEnv).add(localIns);
} catch (NullPointerException e) {
if(currentMethodEnv == null) {
throw new CurrentMethodEnvNotSetException();
} else {
throw e;
}
}
}

나중에 환경에 포함된 클래스, 멤버 변수, 멤버 메소드, 지역 변수 심볼들 중에서 Java 프로그램에서 사용했는지 여부를 검사하는 방법은 다음과 같다.

private void checkEnv(HashMap env, HashSet usedEnv) {
for(Entry<?, Boolean> curEnv: env.entrySet()) {
for(Object used: usedEnv) {
if(used.equals(curEnv.getKey())) {
curEnv.setValue(true);
break;
}
}
}
}

EquGenerator의 finish() 메소드에서 각 선언된 심볼이 사용되었는지 여부를 확인한다.

public void finish() {
Report.report(1, "EquGenerator: finish()");

checkClassEnv();
checkMethodEnv();
checkFieldEnv();
checkLocalEnv();

Report.report(1, "Class Env (" + classEnv.size()+"):\t\t" + classEnv);
Report.report(1, "Method Env (" + methodEnv.size()+"):\t\t" + methodEnv);
Report.report(1, "Field Env (" + fieldEnv.size()+"):\t\t" + fieldEnv);
Report.report(1, "Local Env (" + localEnv.size() + "):\t\t" + localEnv);

super.finish();
}

2.2 AST 노드 별 분석

2.2.1 클래스

분석기의 비지터는 Java 프로그램에서 클래스를 선언할 때 마다 EquGenClassDeclExt의 equGenEnter()를 호출한다. 이 메소드에서 클래스 심볼을 환경에 추가한다.

 

ClassDecl clzDecl = (ClassDecl)this.node();
v.addToClassEnv((JL5ClassType) clzDecl.type());

이와 유사하게, 비지터가 멤버 변수 선언을 만날 때마다 환경에 추가하기 위해 EquGenFieldDeclExt 클래스를 작성한다. 멤버 변수를 선언할 때 클래스 타입을 언급함으로써 이 클래스를 사용한다. 지역 변수 선언도 유사하게 EquGenLocalDeclExt 클래스를 작성함으로써 클래스 사용을 분석한다.

FieldDecl fldDecl = (FieldDecl) this.node();
Type type = fldDecl.type().type();
if(type instanceof JL5ClassType) {  // 타입이 클래스 타입인 경우
v.markOnClassEnv((JL5ClassType) type);
}

매개 변수에 언급된 클래스를 사용하는 것을 분석한다. EquGenProcedureDeclExt 클래스로 구현한다.

ProcedureDecl pcdDecl = (ProcedureDecl) this.node();
for(Formal arg : pcdDecl.formals()) {
Type type = arg.declType();
if(type instanceof JL5ClassType) {// 타입이 클래스 타입인 경우
v.markOnClassEnv((JL5ClassType) type);
}
}

클래스 선언에서 상속 받을 부모 클래스로 지정하거나 구현할 인터페이스를 나열함으로 해서 이 클래스나 인터페이스를 사용할 수 있다. 앞에서와 같은 EquGenClassDeclExt 클래스로 구현한다.

ClassDecl clzDecl = (ClassDecl)this.node();
/* Class 사용: Type as Superclass (Extends) */
if(clzDecl.superClass() != null) {// Object 이외의 부모 클래스가 존재하는 경우
v.markOnClassEnv((JL5ClassType) clzDecl.superClass().type());
}


/* Class 사용: Type as Interface (Implements) */
for(TypeNode arg : clzDecl.interfaces()) {
v.markOnClassEnv((JL5ClassType) arg.type());
}

정적 메소드를 호출할 때도 클래스를 사용할 수 있다. [정적 멤버 변수를 사용할 때?] EquGenCallExt 클래스로 구현한다.

Call call = (Call)this.node();
Type type = call.target().type();
if(type instanceof JL5ClassType) {// 타겟의 타입이 클래스 타입인 경우
v.markOnClassEnv((JL5ClassType) type);
}

클래스 타입 형변환을 할때도 역시 클래스를 사용한다. EquGenInstanceofExt 클래스로 구현한다.

Cast cast = (Cast) this.node();
if(cast.type() instanceof JL5ClassType) {// 클래스 타입인 경우
v.markOnClassEnv((JL5ClassType) cast.type());
} 

instanceof 연산자를 사용할 때도 클래스를 사용한다. EquGenInstanceofExt 클래스로 구현한다.

Instanceof insof = (Instanceof) this.node();
v.markOnClassEnv((JL5ClassType) insof.compareType().type());

객체를 생성하거나 이름없는 서브 클래스를 선언하는 경우에도 관련된 클래스를 사용한다. EquGenNewExt 클래스로 구현한다.

New nw = (New) this.node();
/* Class 사용: Type of New Object */
v.markOnClassEnv((JL5ClassType)nw.type());

/* Class 사용: Super class of Anonymous Subclass */
Type type = nw.type();
if(type instanceof JL5ParsedClassType) {
if(((JL5ParsedClassType)type).pclass() == null) {
// new 할 때 자식 클래스 정의하므로 이름이 없어 Parsed Class가 없음(null).
Type superType = ((JL5ClassType) type).superType();
v.markOnClassEnv((JL5ClassType) superType);
}
}

배열 객체를 생성할 때도 배열 원소 클래스를 지정한다. EquGenNewArrayExt 클래스로 구현한다.

NewArray nwArr = (NewArray) this.node();
if(nwArr.baseType().type() instanceof JL5ClassType) {// 베이스 타입이 클래스 타입인 경우
v.markOnClassEnv((JL5ClassType) nwArr.baseType().type());
}

요약하자면, 클래스를 사용하는 각 사례를 나열하고, 각 사례에 해당하는 확장 노드를 언급한 클래스로 다시 구현하여 클래스 심볼 사용을 분석하는 코드를 작성하였다. (한가지 더욱 간단한 구현 방법으로 CanonicalTypeNode를 탐색함으로써 위의 코드를 훨씬 줄일 수 있지 않을까 생각한다.)

2.2.2 메소드

메소드 선언을 찾기 위해서 EquGenProcedureDeclExt 클래스를 구현한다. 일반 메소드와 생성자를 모두 포함해서 분석할 수 있다. (cf. EquGenMethodDeclExt 클래스와 EquGenConstructorDeclExt 클래스)

v.addToMethodEnv((JL5ProcedureInstance)pcdDecl.procedureInstance());

메소드 호출을 찾는 EquGenCallExt 클래스를 구현하였다.

v.markOnMethodEnv((JL5ProcedureInstance) ((Call)this.node()).procedureInstance());

2.2.3 멤버 변수

멤버 변수를 선언하는 EquGenFieldDeclExt 클래스와 멤버 변수를 사용하는 EquGenFieldExt 클래스를 구현한다.

v.addToFieldEnv((JL5FieldInstance) ((FieldDecl) this.node()).fieldInstance());
v.markOnFieldEnv((JL5FieldInstance) ((Field)this.node()).fieldInstance());

2.2.4 지역 변수

지역 변수 심볼에 대한 분석을 할 때 여러 블록이나 메소드에서 선언된 동일한 이름의 다른 지역 변수들을 단순히 equals() 메소드로 구분할 수 없음을 유의해야 한다. [나중에 고려할 점: 새로운 블록을 들어갈 때마다 환경에 이름이 서로 다른 지역 변수들을 관리하여 구현할 수 있다.]

private HashMap<JL5ProcedureInstance, HashMap<JL5LocalInstance, Boolean>> localEnv;
private HashMap<JL5ProcedureInstance, HashSet> usedLocals;

지역 변수 선언을 만날 때 마다 EquGenLocalDeclExt 클래스로 아래와 같이 구현한다.

v.addToLocalEnv((JL5LocalInstance) ((LocalDecl) this.node()).localInstance());

메소드의 매개 변수도 지역 변수이므로 EquGenProcedureDeclExt 클래스에서 다음의 코드를 활용하여 구현한다.

for(Formal arg : pcdDecl.formals()) {// ProcedureDecl pcdDecl = (ProcedureDecl) this.node();
v.addToLocalEnv((JL5LocalInstance) arg.localInstance());
}

/* Local 환경: Current Method */
v.setCurrentMethodEnv((JL5ProcedureInstance) ((ProcedureDecl) this.node()).procedureInstance());

지역 변수를 사용하는 사례는 EquGenLocalExt 클래스로 구현한다.

v.markOnLocalEnv((JL5LocalInstance) ((Local)this.node()).localInstance());

2.1.5 import 문

폴리글롯에 import 문을 표현하는 노드가 있다. 하지만 이 노드를 통해 문자열로 표현된 "import" 정보만 얻을 수 있고 클래스 타입 정보를 얻을 수는 없다. 참고로, EquGenerator.context().importTable()을 호출하여 문자열 리스트로 표현된 정보를 얻을 수 있다.

3. 여러 Java 소스 프로그램을 포함하는 프로젝트를 입력받아 분석하기

보통 Java 프로젝트는 여러 Java 소스 프로그램을 포함한다. 따라서 프로젝트의 시작 디렉토리를 지정하면 그 안의 모든 소스 프로그램을 찾아서 분석하면 편리할 것이다. 폴리글롯의 시작 메소드 (polyglot.main.Main.start())가 분석기의 시작점이다. getPolyglotArgs 메소드를 새로 만들어서 옵션(명령행 인자)로 디렉토리를 지정한 경우 그 하위에 위치한 모든 Java 프로그램을 입력받아 처리하도록 구성하였다. 참고로 폴리글롯에서 이미 구현된 출력 디렉토리를 지정하는 -d 또는 -D 옵션을 함께 사용하는 경우 혼동을 방지하기 위해 내부적으로 이 옵션 지정 여부를 관리한다.

/**
 * 컴파일할 파일이 있는 디렉터리 명령행 인자를 반영하기 위한 명령행 인자 가공 메서드.
 * 입력된 인자가 컴파일할 디렉터리인 경우, 그 하위에 있는 컴파일할 파일을 찾아서 추가함.
 * 그 이외의 경우, 가공하지 않음.
 * 
 * @param clArgs입력 받은 명령행 인자
 * @return가공된 명령행 인자
 */
private static String[] getPolyglotArgs(String[] clArgs) {
// 이전 인자가 output directory (-D 또는 -d) 옵션이었는지를 저장.
// 첫 번째 인자의 이전 인자는 없으므로 false.
boolean prevArgOutputDirOption = false;

// 결과를 저장할 ArrayList 객체 생성.
ArrayList result = new ArrayList<>();

// 전체 명령행 인자에 대해...
for(String arg : clArgs) {
File argFile = new File(arg);

// 하위 파일을 컴파일할 디렉터리가 아닌 인자: 가공하지 않음.
if(!argFile.isDirectory() || prevArgOutputDirOption) {
result.add(arg);
}

// output 옵션이 아닌 디렉터리: 하위의 컴파일할 파일을 찾아서 추가함.
else {
result.addAll(getAllSubCompilableFilePaths(argFile));
}

// 현재 다루는 인자가 output directory (-D 또는 -d) 옵션인지 확인.
// 다음번 루프에 하위 파일을 검색할 디렉터리를 결정하는 데 영향을 미침.
if(arg.toLowerCase().equals("-d")) {
prevArgOutputDirOption = true;
} else {
prevArgOutputDirOption = false;
}
}

return result.toArray(new String[result.size()]);
}

private static ArrayList getSubCompilableFilePaths(File file) {

File[] packedFile = new File[1];
packedFile[0] = file;

return getSubCompilableFilePaths(packedFile, new ArrayList());
}

private static ArrayList getSubCompilableFilePaths(File[] files, ArrayList fileList) {

for(File file : files) {
if(file.isDirectory()) {// 디렉터리인 경우
getSubCompilableFilePaths(file.listFiles(), fileList);
} else if (file.isFile()) {// 파일인 경우
String fileName = file.getName().toLowerCase();
if(fileName.endsWith(".java")) {
fileList.add(file.getPath());
}
}
}

return fileList;
}

폴리글롯 시작 메소드를 다음과 같이 수정한다.

polyglotMain.start(getPolyglotArgs(args), new tool.compiler.java.ExtensionInfo());

4. 마무리

지금까지 폴리글롯을 활용하여 Java 프로그램의 AST를 탐색하며 사용되지 않는 심볼을 찾는 간단한(?) 분석기를 작성해보았다. 이 연습을 통해 폴리글롯에 더 익숙하게 되었을 것으로 기대한다.