ANTLR를 이용한 JASMIN 컴파일러

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

Beige00 2024. 1. 9. 13:29

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

 

1. enterExpr

@Override public void enterExpr(tinyPythonParser.ExprContext ctx) {
    if(ctx==null || visitedNodes.contains(ctx)){
        return;
    }
    if(ctx.NUMBER()!=null){
        visitedNodes.add(ctx);
        this.result+=("bipush "+Integer.parseInt(ctx.getChild(0).getText()))+"\n";
    }//상수 load
    else if(ctx.NAME()!=null){
        visitedNodes.add(ctx);
        if(table.contains(ctx.getChild(0).getText())) {
            this.result+=("iload_"+table.indexOf(ctx.getText())+"\n");
        }//변수 호출
        else if(this.functionTable.containsKey(ctx.getChild(0).getText())){
            enterOpt_paren((tinyPythonParser.Opt_parenContext) ctx.getChild(1));
            this.result+=("invokestatic Test/"+functionTable.get(ctx.getChild(0).getText())+"\n");
        }//함수 호출
    }//Name을 기반으로 table에 매핑되어있는 순서 호출
    else if(ctx.OPEN_PAREN()!=null){
        visitedNodes.add(ctx);
        enterExpr((tinyPythonParser.ExprContext)ctx.getChild(1));
    }//(expr)
    else{
        visitedNodes.add(ctx);
        //Expr 긁어가며 load => 연산자 읽어서 연산
        enterExpr((tinyPythonParser.ExprContext)ctx.getChild(0));
        for(int i=2; i<ctx.getChildCount(); i+=2){
            enterExpr((tinyPythonParser.ExprContext)ctx.getChild(i));
            if(ctx.getChild(i-1).getText().equals("+")){
                this.result+=("iadd\n");
            }
            else{
                this.result+=("isub\n");
            }
        }
    }//사칙 연산, 계산해서 tempVal 출력. 매 expr의 끝은 결국 NAME 이거나 NUMBER이며 이 연산의 끝은 tempVal0이 setting되는 것이다.
}// Number일 시, 해당되는 값을 tempVal0에 set, Name일 시 해당 변수의 위치를 Set OPEN_PAREN일시 () 무시하고 expr 진입, op일 시,

 

대부분의 terminal 노드 이전에 파싱이 되는 노드 expr의 enter 함수이다. 

그만큼 표현하는 범위도 방대하기 때문에 문법을 첨부했다.

더보기
expr:
      NAME opt_paren
    | NUMBER
    | '(' expr ')'
    | expr (( '+' | '-' ) expr)+
 ;

 

문법을 보지않아도 이해가 잘된 사람들은 expr이 어떤 역할을 하는지 알 것이다.

expr의 마지막은 NUMBER나 NAME opt_paren으로 귀결되고, 이외에는 '('  expr ')', expr '+'|'-' expr.. 과 같이 expr의 표현하는 방식을 다시 expr로 포장한 것이다.

더보기

ex) 3+3+5의 파싱과정

enterExpr(3+3+5) - expr '+' expr '+' expr =>여기서 개별 expr로 enterExpr

enterExpr(3) -> NUMBER {bipush 3}

'+' -> + {iadd}

enterExpr(3) -> NUMBER {bipush 3}

'+' -> + {iadd}

enterExpr(5) -> NUMBER {bipush 5}

그렇다면 이제 구현을 해보자.

 

만약 파라미터 ctx가 NUMBER였다면, 단순히 스택에 해당 expr NUMBER 값을 load해주면 된다. (bipush)

 

받아온 ctx가 NAME일 시 함수를 가리키거나, 변수를 호출하는 expression이다.

따라서 해당 표현을 table에서 검사해 존재할 시, iload_(table index)를 통해 스택에 해당 변수 메모리의 값을 로드한다.

만약 functionTable에 존재할 시, enterOpt_paren으로 함수 선언에 사용된 파라미터들을 처리한다. { a '()' <- ()부분 }

enterOpt_paren이 끝났을 시, 함수 파라미터들의 load가 끝났다고 가정했으므로, 바로 invokestatic Test/(함수이름)을 통해

함수를 실행시킨다. (런타임 스택 개념도 같이 생각해주면 좋다.)

 

받아온 ctx가 OPEN_PAREN일 시, (3+5)와 같은 형태의 괄호 표현일 것이다. 과제를 제출했을 때는 단순히 괄호는 처리해주지 않을 것이라는 것만 생각하여 OPEN_PAREN을 걷어내고 안에 있는 expr만 접근했다. 그러나 지금와서 생각하니 조금 수정이 필요할 것 같다.

괄호 수식내에 괄호가 또 존재할 수 있으며  만약 존재시, 해당 부분을 먼저 enterExpr을 해줘야할 것 같다.

따라서, 입력받은 expr이 OPEN_PAREN으로 시작하며, ChildCount가 2보다 클 시 (즉, 수식일 시.) 받아온 ctx를 getText로 문자열로 바꿔주고, 변환된 수식 문자열의 처음부터 수식을 읽어나가며 괄호 표현이 사용된 expr들을 찾고, 해당 expr부터 enter를 해줘 load를 해주는 것이 필요할 것 같다.

 

만약 expr로 시작할 시, 무조건 +, -로 연결된 수식의 구조이므로, 앞부터 읽어나가며 {'+'|'-' enterExpr} 세트를 처리해나간다. 여기서 +가 파싱될 시 iadd, -가 파싱될 시 isub를 해주면 된다.

 

사실 이 수식처리 부분을 학기가 끝난 지금 돌아보니 너무 대충 구현해준 것 같다.

사칙연산에 괄호가 들어갈 수 있으므로, 먼저 파싱된 expr {('+'|'-') expr} + 표현의 expr들을 전부 들어가서 OPEN_PAREN이 존재하는 지 확인하고, OPEN_PAREN 구조의 expr일 시 해당 expr load Jasmin code를 먼저 작성해줬어야하는데, 지나치게 단순하게 처리해 오류가 발생할 것 같다.

보다 완벽하게 구현하기 위해서는 Postfic Notation을 사용하고, 연산자 우선순위 또한 정해주면 될 것이다.

(해당 방법을 사용 시, +, - 가 아닌 연산자 우선순위가 정의된 모든 연산자를 사용 가능하다.)

더보기

* 해당 사칙연산 파싱 과정에는 Postfix Notation을 활용하면 좋을 것이다.

여기서 해당 내용까지 다루기에는 너무 지저분해지기에 잘 요약해둔 다른 블로그의 링크를 첨부한다.

https://jamanbbo.tistory.com/53

 

2. enterOpt_paren

@Override public void enterOpt_paren(tinyPythonParser.Opt_parenContext ctx) {
    if(visitedNodes.contains(ctx)){
        return;
    }
    visitedNodes.add(ctx);
    if(ctx.getChildCount()>2){
        for(int i=1; i<ctx.getChildCount(); i+=2){
            enterExpr((tinyPythonParser.ExprContext) ctx.getChild((i)));
        }
    }
}

 

괄호가 파싱되었을 때, 특히 a() 와 같이 함수에서 파라미터 입력 혹은 선언에 사용되는 괄호가 파싱될 시 사용되는 함수이다.

따라서 괄호 안에 있는 모든 expr을 enter 해줘 load를 하든, bipush를 하든 처리를 해준다.

 

 

3. 결과

.class public Test
.super java/lang/Object
; standard initializer
.method public <init>()V
aload_0
invokenonvirtual java/lang/Object/<init>()V
return
.end method
.method public static sum(II)I
.limit stack 32
.limit locals 32
iload_0
iload_1
iadd
ireturn
.end method
.method public static main([Ljava/lang/String;)V
.limit stack 32
.limit locals 32
bipush 3
istore_0
bipush 4
istore_1
iload_0
iload_1
invokestatic Test/sum(II)I
istore_2
getstatic java/lang/System/out Ljava/io/PrintStream;
iload_2
invokevirtual java/io/PrintStream/println(I)V
iload_0
bipush 3
if_icmplt IFLABEL_00
getstatic java/lang/System/out Ljava/io/PrintStream;
iload_0
invokevirtual java/io/PrintStream/println(I)V
goto IFEND0
IFLABEL_00:
iload_1
bipush 2
if_icmpge IFLABEL_01
getstatic java/lang/System/out Ljava/io/PrintStream;
iload_1
invokevirtual java/io/PrintStream/println(I)V
goto IFEND0
IFLABEL_01:
iload_1
bipush 3
if_icmple IFELSE0
getstatic java/lang/System/out Ljava/io/PrintStream;
bipush 3
invokevirtual java/io/PrintStream/println(I)V
goto IFEND0
IFELSE0:
getstatic java/lang/System/out Ljava/io/PrintStream;
iload_2
invokevirtual java/io/PrintStream/println(I)V
IFEND0:
Lloop0:
iload_2
bipush 6
bipush 10
iadd
if_icmpge Lend0
iload_2
bipush 7
if_icmpne IFLABEL_10
getstatic java/lang/System/out Ljava/io/PrintStream;
ldc "c=7"
invokevirtual java/io/PrintStream/println(Ljava/lang/String;)V
iload_2
bipush 1
iadd
istore_2
goto IFEND1
IFLABEL_10:
iload_2
bipush 9
if_icmpne IFELSE1
getstatic java/lang/System/out Ljava/io/PrintStream;
ldc "break point"
invokevirtual java/io/PrintStream/println(Ljava/lang/String;)V
goto Lend0
goto IFEND1
IFELSE1:
getstatic java/lang/System/out Ljava/io/PrintStream;
iload_2
invokevirtual java/io/PrintStream/println(I)V
iload_2
bipush 1
iadd
istore_2
IFEND1:
goto Lloop0
Lend0:
getstatic java/lang/System/out Ljava/io/PrintStream;
ldc "Hello world! This is final Term project."
invokevirtual java/io/PrintStream/println(Ljava/lang/String;)V
return
.end method
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

public class Test {
    public Test() {
    }

    public static int sum(int var0, int var1) {
        return var0 + var1;
    }

    public static void main(String[] var0) {
        byte var32 = 3;
        byte var1 = 4;
        int var2 = sum(var32, var1);
        System.out.println(var2);
        if (var32 >= 3) {
            System.out.println(var32);
        } else if (var1 < 2) {
            System.out.println(var1);
        } else if (var1 > 3) {
            System.out.println(3);
        } else {
            System.out.println(var2);
        }

        while(var2 < 6 + 10) {
            if (var2 == 7) {
                System.out.println("c=7");
                ++var2;
            } else {
                if (var2 == 9) {
                    System.out.println("break point");
                    break;
                }

                System.out.println(var2);
                ++var2;
            }
        }

        System.out.println("Hello world! This is final Term project.");
    }
}​

 

제작한 컴파일러가 생성한 Test.j jasmin 코드를 jasmin decompiler로 다시 java로 구성한 코드이다.

결과적으로 tinyPython -> (자체 제작 컴파일러) -> Jasmin -> (Jasmin decompiler) -> Java code 로 컴파일링을 완료하였다.


이번 포스팅을 끝으로 마치려고 했으나, 일정이 있어 다음 포스팅을 마지막으로 작성할 것 같다.

다음 포스팅에서는 Test.j Jasmin code를 Test.tpy 코드와 비교해가며 어떤 과정으로 나의 컴파일러가 동작했을지 간단히 따라가볼것이고, 터미널에 어떻게 명령어를 주어 컴파일링, 디컴파일링을 했는지 기록할 것이다.