ANTLR를 이용한 JASMIN 컴파일러

ANTLR를 이용한 tinyPython to Jasmin 컴파일러 만들기. - 3

Beige00 2023. 12. 29. 12:10

* 본 내용은 충남대학교 조은선 교수님의 컴파일러 개론을 수강하고 작성한 글입니다.

 

저번 글에서는 ANTLR가 파스트리를 DFS로 접근하며 접근될 때는 enter, 그 노드를 벗어날 때는 exit 함수를 실행한다고 공부했다. 따라서 enter에 구현을 하던, exit에 구현을 하던 결국 우리는 최종적으로 특정 메소드에 해당 노드 방문시 처리 함수를 정의해주어야하는 것 이었다.

 

그럼 이제 g4 rule을 보고 해당 토큰의 함수에 가서 그에 맞게 구현을 해주면 된다.

개인적으로 사용하기 위해 만든 인스턴스들

우선 IfflowOrder는 If_stmt를 다룰 때 더 자세히 설명하겠지만, Jasmin 상에서 If, elif label 명을 지어주기 위해 만들어둔 인스턴스이다. 본 프로그램에서 If label은 IFLABEL_(IfflowOrder2)(IfflowOrder), IFEND(IfflowOrder2)등 과 같이 구성된다.

예시, 당장 이해가 안가더라도 일단 넘어가자

WhileFlowOrder의 경우도 똑같이 Loop label을 작성하기 위해 사용되는 order instance이다. (예시 사진의 Lloop0 참조)

 

temFuncFormat의 경우는 Def_stmt에서 함수의 이름을 임시로 저장해두어 최종적으로 함수 저장 테이블에 기록해두기 위한 저장 공간이다.

 

result의 경우 컴파일링이 된 최종 jasmin code를 저장하고 있는 string 공간이며 이를 Test.j 파일에 write하는 것으로 프로그램을 끝내게 된다.

 

functionTable은 선언된 함수를 저장해두는 Hashtable이다.

table은 할당된 변수를 저장해두는 ArrayList 이다.

 

visitedNodes는 Enter method에 정의를 하는 본 프로그램의 특성상, enter에서 처리가 끝난후 다시 노드를 재방문하게 되어 중복 처리가 되는 현상이 일어난다. 이를 막기 위해서 set에 추가시켜 모든 방문 함수가 실행될 때 이미 처리가 된 노드인지 검사를 하게 한다.

예시. 이렇게 동일 노드를 여러번 반복 방문하게 된다.

makeFile() 의 경우는 그냥 최종 result 문자열을 담은 Test.j 파일을 디렉토리 내에 만들기 위해 exitProgram() 함수에 사용되는 private 함수이다.

 


1. enter & exitProgram

@Override public void enterProgram(tinyPythonParser.ProgramContext ctx) {
    this.result+=(".class public Test\n");
    this.result+=(".super java/lang/Object\n");
    this.result+=("; standard initializer\n");
    this.result+=(".method public <init>()V\n");
    this.result+=("aload_0\n");
    this.result+=("invokenonvirtual java/lang/Object/<init>()V\n");
    this.result+=("return\n");
    this.result+=(".end method\n");
    /* 기본적으로 class 정의는 존재한다고 가정 */
}
@Override public void exitProgram(tinyPythonParser.ProgramContext ctx) {
    this.result +=("return\n");
    this.result += (".end method\n");
    try {
        this.makeFile();
    }catch(Exception e){
        this.result+=("Error occured");
    }
}//return main

 

enterProgram의 경우 '본 텀프로젝트의 제약 조건에  Class 정의는 기본적으로 존재.(이름은 Test로 가정).' 이라고 주어져 있으므로 따로 동작을 정의해주는 것이 아니라 기본적인 public class 생성 및 기본 생성자를 하드코딩 해줬다.

여기서 java/lang/Object는 java에서 모든 class를 선언할 때 자동적으로 상속하는 super class이다.

생성자는 기본 생성자로 선언해주었다. 

enterProgram에 해당하는 jasmin의 java class 변환후 모습.

exitProgram의 경우 최종적으로 public class Test를 끝마치고 makeFile()을 실행시켜 결과를 Test.j에 저장하는 과정을 정의하였다.

 

2. enter & exitDefs

@Override public void enterDefs(tinyPythonParser.DefsContext ctx) {
    if(visitedNodes.contains(ctx)){
        return;
    }
    visitedNodes.add(ctx);
    if(ctx.getChildCount()>0){
        for(int i=0; i<ctx.getChildCount(); i++){
            if(ctx.getChild(i).getClass().equals(tinyPythonParser.Def_stmtContext.class)){
                enterDef_stmt((tinyPythonParser.Def_stmtContext) ctx.getChild(i));
            }//def_stmt, 만약 이외의 경우 NEWLINE이므로 연산 X
        }
    }
}
/* program이 실행되면 시작하는 자동 method로, 사전에 함수들을 전부 정의하는 것을 의미한다. */
@Override public void exitDefs(tinyPythonParser.DefsContext ctx) {
    this.result+=(".method public static main([Ljava/lang/String;)V\n");
    this.result+=(".limit stack 32\n");
    this.result+=(".limit locals 32\n");
}//main 함수의 시작

enterDefs는 앞서 계속 설명했던 것처럼 class의 시작과 동시에 사용할 모든 함수를 선언하는 선언부를 정의하는 토큰이다. 따라서 def_stmt, NEWLINE(\n)의 반복이 될 것이고 sub parsing tree를 직접 순회하며 def_stmt를 마주칠 때마다 방문처리를 해주게 된다. NEWLINE의 경우 따로 처리를 해주지 않을 것이기 때문에 생략한다.

exitDefs는 main 함수의 시작을 의미하므로, main 함수의 시작을 직접 하드코딩 해줬다.

 

3. enterStmt & enterSimple_stmt

@Override public void enterStmt(tinyPythonParser.StmtContext ctx) {
    if(visitedNodes.contains(ctx)){
        return;
    }
    visitedNodes.add(ctx);
    if(ctx.getChild(0).getClass().equals(tinyPythonParser.Simple_stmtContext.class)){
        enterSimple_stmt((tinyPythonParser.Simple_stmtContext) ctx.getChild(0));
    }
    else{
        enterCompound_stmt((tinyPythonParser.Compound_stmtContext) ctx.getChild(0));
    }
}

 

g4 rule에 따르면 stmt가 파싱될 시, simple_stmt | compount_stmt 라고 선언이 되어있다. 따라서 스캔된 stmt sub parse tree의 첫번째 child가 Simple일시 Simple_stmt로 간주하고 방문처리를, Compound일 시 Compount_stmt로 간주하고 방문처리를 한다.

 

@Override public void enterSimple_stmt(tinyPythonParser.Simple_stmtContext ctx) {
    if(visitedNodes.contains(ctx)){
        return;
    }
    visitedNodes.add(ctx);
    enterSmall_stmt((tinyPythonParser.Small_stmtContext)ctx.getChild(0));
}

 

Simple_stmt의 경우도 똑같이 Small_stmt로 이어지는 경우밖에 g4 rule에 정의되어있지 않으므로, Small_stmt를 방문처리 해준다.

 

4. enterSmall_stmt

@Override public void enterSmall_stmt(tinyPythonParser.Small_stmtContext ctx) {
    if(visitedNodes.contains(ctx)){
        return;
    }
    visitedNodes.add(ctx);
    if(ctx.getChild(0).getClass().equals(tinyPythonParser.Assignment_stmtContext.class)){
        enterAssignment_stmt((tinyPythonParser.Assignment_stmtContext) ctx.getChild(0));
    }
    else if(ctx.getChild(0).getClass().equals(tinyPythonParser.Flow_stmtContext.class)){
        enterFlow_stmt((tinyPythonParser.Flow_stmtContext) ctx.getChild(0));
    }
    else if(ctx.getChild(0).getClass().equals(tinyPythonParser.Print_stmtContext.class)){
        enterPrint_stmt((tinyPythonParser.Print_stmtContext) ctx.getChild(0));
    }
    else{
        enterReturn_stmt((tinyPythonParser.Return_stmtContext)ctx.getChild(0));
    }
}

 

Small_stmt는 각각 assignment, flow(continue,break), print, return_stmt로 이어진다.

 


이번에는 구체적인 구현 시작과 구현의 접근법에 대해 알아보았다. 이렇듯, non-terminal간의 파싱은 단순히 방문처리를 반복하는 것이므로 아주 쉬운 구현이 가능하다. 하지만 parsing rule에 terminal이 등장하는 시점에서(leaf node에 도달)는 최종적으로 Jasmin 코드를 써주어야하는 action을 정의해야한다. 그 부분에서는 여러가지 아이디어가 필요하게 되는데, 다음부터는 그 부분을 중점으로 글을 작성해보겠다.