ANTLR를 이용한 JASMIN 컴파일러

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

Beige00 2024. 1. 3. 16:45

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

 

1. enterAssignment_stmt

@Override public void enterAssignment_stmt(tinyPythonParser.Assignment_stmtContext ctx) {
    if(visitedNodes.contains(ctx)){
        return;
    }
    visitedNodes.add(ctx);
    enterExpr((tinyPythonParser.ExprContext) ctx.getChild(2));//load
    if(table.contains(ctx.getChild(0).getText())){
        this.result+=("istore_"+table.indexOf(ctx.getChild(0).getText())+"\n");
    }  //만약 이미 존재하는 변수 명일 경우 해당 변수 공간에 저장
    else{
        table.add(ctx.getChild(0).getText());
        this.result+=("istore_"+table.indexOf(ctx.getChild(0).getText())+"\n");
    }
    // 존재하지 않을 경우 새로운 변수 공간에 저장.
}
/*
변수 선언문, 현재 변수 symbol table을 참조하여 마지막 table에 저장한다.
key : Name
val : Integer
따라서 exp 처리를 번저 해주어야 한다. expr 자리에는 NUMBER가 올 수도, 사칙 연산이 올 수도 있다.
=> 해야할 일, Assignment의 경우에는 ldc val, istore_n과 같은 형태로 이루어진다.
결과 ldc val
    istore_n
*/

 

enterAssignment의 정의는 다음과 같다.

tinyPython에서 assignment 정의

저번 글에 설명한 non-Terminal enter Function 들과 다르게 Teminal 표현이 등장한다. 따라서 enter 함수 내에서 파일에 ANTLR 문을 작성해주는 부분이 등장하게 된다.

더보기

* non-Terminal vs Terminal. Parse tree의 leaf node는 어떤 표현이 될까?

 

처음보는 표현이 등장한다.(NAME)

tinyPython에서 assignment 정의

앞선 글에 설명한 small_stmt의 정의를 보자.

smallStmt

단순하게 눈에 보이는 차이점을 말해보자. 무엇이 다를까?

한눈에 보면 노란색, 보라색, 초록색 글씨가 보인다.

정답부터 미리 말하자면 노란색은 non-Terminal, 보라색은 Terminal, 초록색은 문자열이다.

문자열은 컴파일러 개론까지 다루고 있는 사람들이라면 다들 알고 있을 것이고, non-Terminal과 Terminal의 차이점은 뭘까?

 

보라색(?)들

이 규칙들을 유심히 보자. 과연 ANTLR가 자동 생성한 tinyPythonBaseListener에 enterSTRING라던가 enterNUMBER 같은 함수가 존재할까?

당연히 존재하지 않는다. 이 보라색들은 전부 Terminal로 간주하기 때문이다. Terminal들은 결과를 잘 보면 알겠지만 컴파일러에서 처리를 해줄 메소드가 필요 없다.

fragment NON_ZERO_DIGIT:
    [1-9]
    ;

fragment DIGIT:
    [0-9]
    ;

위의 Terminal 정의에 사용된 fragment 중 DIGIT을 가져와봤다. 이들을 보면서 동시에 DECIMAL_INTEGER를 보자.

이 DECIMAL_INTEGER를 표현하는데 method가 필요하지는 않을 것 같다. 그저 [1-9] + [0-9]의 반복으로 표현이 가능한 것이다. 그리고 이 DECIMAL_INTEGER 표현을 이용하는 정의는 

INTEGER:
    DECIMAL_INTEGER
    ;

INTEGER이다. 다시 이 INTEGER는 NUMBER를 정의하는데 쓰이고 이렇게 Terminal들은 더 이상 enter_Stmt 라는 메소드를 필요로 하지 않는 것들이다. 따라서 파스트리를 보면 이 Terminal들은 더 이상 어떠한 stmt로 진입하지 않고 단순히 해당하는 문자열을 매칭하는 것으로 끝나게 된다.(따라서 leaf가 된다.)

예시

따라서 해당 사진에서 OPEN_PALEN, CLOSE_PALEN, NUMBER는 전부 따로 enter_Stmt 함수가 없는 Terminal들이다.

그러면 Terminal node가 스캔되면 어떠한 일을 해야할까?

더 이상 진입할 stmt가 없으므로 enter_Stmt를 따로 실행시켜줄 필요는 없다.

그리고 Terminal node가 스캔된다는 것은 해당 구문의 최종 표현이 파싱되었다는 뜻이다.

그러므로 해당 구문을 어떻게 JASMIN으로 컴파일링 할지 결정이 되었다는 뜻이고, 컴파일러가 파일에 Jasmin 구문을 쓸 수 있게 된 것이라는 의미다.

( ex) int a = 3; 에서 a는 NAME, 3은 INTEGER로 판명이 됨. 그러므로 bipush 3, istore_1 과 같은 구문을 작성해줄 수 있음. )

기계적으로 NAME '=' expr을 번역해주자.

우선 assignment는 항상 int로 일어나므로(가정 참조) a = 3 과 같은 구문일 것이다. 이를 ANTLR로 번역해주기 위해서는 우선 저장이 될 expr을 load 해야한다. 따라서 expr의 정체를 파악해야한다.

(expr은 non-Terminal 이므로 enterExpr을 기계적으로 실행한다. 이 때, 파스트리상 expr이 3번째 자식이므로 ctx.getChild(2)를 가지고 enter해줘야 한다.)

enterExpr이 return되면 expr의 정체를 판정했다는 뜻이다. 그러므로 판정된 expr을 스캔된 NAME에 저장해주어야한다.

여기서 2가지 경우를 생각할 수 있다.

 

(상황은 a = 2+3 이 파싱되어서 NAME:a '=' expr:2+3 으로 인식되었고, enterExpr을 통해 5라는 값이 스택에 load된 상태를 가정)

i) 파싱된 'a'라는 NAME을 가진 변수가 이미 존재. => 변수 a의 메모리를 expr로 초기화하는 것으로 간주

ii) 파싱된 'a'라는 NAME을 가진 변수가 선언된 적이 없음. => 변수 a에 해당하는 메모리 공간을 만들어주고, expr로 초기화.

 

이를 이해했다면, if문의 이해가 쉬울 것이다.

첫번째 케이스는 이미 NAME에 해당하는 변수가 table 내에 존재해서 해당 table 내의 NAME이 가지는 index 번호 공간에 파싱된 expr을 저장하는 것이다.

{ istore_(NAME의 table상 index) }

 

두번째 케이스는 NAME에 해당하는 변수가 table 내에 없으므로 (선언된 적이 없으므로) table에 해당 변수 NAME을 저장해주고 저장된 NAME의 table index로 istore_(index) 해준다.

 

2. enterFlow_stmt

@Override public void enterFlow_stmt(tinyPythonParser.Flow_stmtContext ctx) {
    if(visitedNodes.contains(ctx)){
        return;
    }
    visitedNodes.add(ctx);
    if(ctx.getChild(0).getClass().equals(tinyPythonParser.Break_stmtContext.class)){
        enterBreak_stmt((tinyPythonParser.Break_stmtContext) ctx.getChild(0));
    }
    else{
        enterContinue_stmt((tinyPythonParser.Continue_stmtContext)ctx.getChild(0));
    }
}

 

Flow_stmt는 정의에 non-Terminal만 포함하므로 단순히 파싱된 ctx가 어떤 type인지를 판별해 enter function을 실행해준다. Flow_stmt는 반복문에서 break, continue문을 담당한다.

 

3. enterBreak_stmt

@Override public void enterBreak_stmt(tinyPythonParser.Break_stmtContext ctx) {
    if(visitedNodes.contains(ctx)){
        return;
    }
    visitedNodes.add(ctx);
    this.result+=("goto Lend"+this.WhileFlowOrder+"\n");
}

파싱된 결과가 break 일 시, 반복을 끝낸다. 반복을 끝내기 위해서는 goto Lend로 jump 시켜버리면 된다.

이 때, WhileFlowOrder는 한 프로그램 내에서 여러 개의 반복문이 사용되었을 때, 구분을 해주기 위해 부여한 index number이다. 나중에 살펴보겠지만, enterWhile이 실행될 때마다 WhileFlowOrder를 1 씩 inc 연산 해준다.

 

4. enterContinue_stmt

@Override public void enterContinue_stmt(tinyPythonParser.Continue_stmtContext ctx) {
    if(visitedNodes.contains(ctx)){
        return;
    }
    visitedNodes.add(ctx);
    this.result+=("goto Lloop"+this.WhileFlowOrder+"\n");
}

파싱된 결과가 continue일 시, 바로 반복문의 시작으로 jump한다. 

 

5. enterCompound_stmt

@Override public void enterCompound_stmt(tinyPythonParser.Compound_stmtContext ctx) {
    if(visitedNodes.contains(ctx)){
        return;
    }
    visitedNodes.add(ctx);
    if(ctx.getChild(0).getClass().equals(tinyPythonParser.If_stmtContext.class)){
        enterIf_stmt((tinyPythonParser.If_stmtContext) ctx.getChild(0));
    }
    else{
        enterWhile_stmt((tinyPythonParser.While_stmtContext) ctx.getChild(0));
    }
}

Compound_stmt의 경우 nest 되는( 소스 코드 안에서 추가로 label을 만드는 ) 표현들의 정의이다. 해당 구문은 line의 처음을 파싱해내는 stmt에서 유도되는 문법이다.

더보기

파싱은 결국 프로그램 실행 직후 defs 를 제외한 모든 구문은 stmt에서 출발하여 stmt가 어떠한 stmt인지 구체화 해나가는 방향으로 진행된다.

( ex: stmt -> compound_stmt -> if_stmt )

해당 정의에는 non_terminal만 존재하므로 단순히 매칭되는 stmt에 enter해주면 된다.

 


이번 포스팅에서는 Terminal이 파싱되었을 때 작성법과 Jasmin 상에서 변수를 어떻게 처리해주는지를 살펴보았다.

다음 포스팅에서는 JASMIN 문법 몇가지와 IF, WHILE 구현에 대해 알아보겠다.

(개인적으로 가장 중요하다고 생각한다.)