2.Ugly Print using Listener

손지웅·2025년 3월 10일

1. 개요

앞 장에서 언급했던 Pretty Printer 와 달리, Ugly Print는 말 그대로 못생긴 출력이다. 이는 코드를 난독화하여 이해하기 어렵게 하는 방식이다.

이를 위해서 Listener pattern을 활용할 수 있다.

2. ANTLR

ANother Tool for Language Recognition : 언어 인식을 위한 또 다른 도구

  • 파서를 자동으로 생성해주는 강력한 도구
  • 프로그래밍 언어의 문법 문석을 자동화
  • 코드를 AST로 변환하여 처리

앞서 배웠던 LLVM과는 이러한 차이점이 존재한다.

컴파일러 단계역할사용 기술
프론트엔드 (Frontend)소스 코드 분석 → AST(구문 트리) 생성ANTLR
중간 표현 (IR, Intermediate Representation)AST → LLVM IR 변환자체 구현 or Clang
백엔드 (Backend)LLVM IR 최적화 및 코드 생성LLVM
기계 코드 생성 (Machine Code Generation)IR을 CPU용 실행 코드로 변환LLVM

즉, ANTLR을 사용한다면

  1. 입력된 코드를 Token으로 쪼개는 단계 ( Lexing )
  2. Token을 분석하여 Parse Tree 생성 단계 ( Parsing )
  3. AST 생성 -> 트리를 탐색하며 코드 변환, 컴파일, 최적화 등 수행이 가능하게 된다.

이러한 ANTLR에는 두가지 방식이 존재한다.

    1. Listener : 트리의 노드를 방문하면서 자동으로 콜백 메서드가 실행 ( event 기반 )
    1. Visitor : 수동으로 특정 노드를 방문하여 원하는 동작을 수행 ( 직접 탐색 기반 )

두가지 방식 중 Listener 를 활용한다면, 코드를 Parsing 하면서 자동으로 특정 동작을 수행할 수 있다.

3. ANTLR 설치 및 실습

    1. java runtime binaries jar 다운로드

https://www.antlr.org/download.html

    1. Intellij IDEA에 ANTLR 설치

    1. ANTLR Jar 파일 추가
      Dependencies에 jar 파일 추가, 이를 통하여 외부 라이브러리를 활용할 수 있게 된다.

    1. 프로젝트에 tinyR4.g4 추가
  • tinyR4.g4란 ? 
    	g4 확장자는 ANTLR 문법 파일 확장자다. 즉 tinyR4라는 특정한 언어 문법을 정의한 파일
    	tinyR4는 사칙연산, fn main, 2진 연산자 등의 문법이 포함된 미니 프로그래밍 언어이다. 

    1. Configure ANTLR
      g4 파일을 우클릭하여 Lexer, Parser 파일이 저장될 경로 지정, 생성한 파일이 특정 패키지에 포함되도록 설정, 설정 활성화

    1. Generate ANTLR Recognizer
      마찬가지로 우클릭 후 Generate ANTLR 실행. Generated Package에 Lexer와 Parser, AST 탐색을 위한 기본 클래스, Token 정보 저장 파일 등을 자동으로 생성한다.

    1. ANTLR Preview
      g4 파일에서 program 우클릭 후 ANTLR Preview 선택. tinyR4 문법에 맞추어 간단한 코드를 작성하면 아래 그림과 같이 Parse tree를 볼 수 있다. 틀린 문법 또한 볼 수 있다.

      코드는 u32 타입의 변수 a를 입력으로 받는 함수를 작성한 것이다.

MiniC.g4 문법 파일

grammar MiniC;


@header { 
package generated;
}
program	: decl+			;
decl		: var_decl		
		| fun_decl		;
var_decl	:  type_spec IDENT ';' 
		| type_spec IDENT '=' LITERAL ';'	
		| type_spec IDENT '[' LITERAL ']' ';'	;
type_spec	: VOID				
		| INT				;
fun_decl	: type_spec IDENT '(' params ')' compound_stmt ;
params		: param (',' param)*		
		| VOID				
		|			;
param		: type_spec IDENT		
		| type_spec IDENT '[' ']'	;
stmt		: expr_stmt			
		| compound_stmt			
		| if_stmt			
		| while_stmt			
		| return_stmt			;
expr_stmt	: expr ';'			;
while_stmt	: WHILE '(' expr ')' stmt	;
compound_stmt: '{' local_decl* stmt* '}'	;
local_decl	: type_spec IDENT ';'
		| type_spec IDENT '=' LITERAL ';'	
		| type_spec IDENT '[' LITERAL ']' ';'	;
if_stmt		: IF '(' expr ')' stmt		
		| IF '(' expr ')' stmt ELSE stmt 		;
return_stmt	: RETURN ';'			
		| RETURN expr ';'				;
expr	:  LITERAL				
	| '(' expr ')'				 
	| IDENT				 
	| IDENT '[' expr ']'			 
	| IDENT '(' args ')'			
	| '-' expr				 
	| '+' expr				 
	| '--' expr				 
	| '++' expr				 
	| expr '*' expr				 
	| expr '/' expr				 
	| expr '%' expr				 
	| expr '+' expr				 
	| expr '-' expr				 
	| expr EQ expr				
	| expr NE expr				 
	| expr LE expr				 
	| expr '<' expr				 
	| expr GE expr				 
	| expr '>' expr				 
	| '!' expr					 
	| expr AND expr				 
	| expr OR expr				
	| IDENT '=' expr			
	| IDENT '[' expr ']' '=' expr		;
args	: expr (',' expr)*			 
	|					 ;

VOID: 'void';
INT: 'int';

WHILE: 'while';
IF: 'if';
ELSE: 'else';
RETURN: 'return';
OR: 'or';
AND: 'and';
LE: '<=';
GE: '>=';
EQ: '==';
NE: '!=';

IDENT  : [a-zA-Z_]
        (   [a-zA-Z_]
        |  [0-9]
        )*;


LITERAL:   DecimalConstant     |   OctalConstant     |   HexadecimalConstant     ;


DecimalConstant
    :   '0'
	|   [1-9] [0-9]*
    ;

OctalConstant
    :   '0'[0-7]*
    ;

HexadecimalConstant
    :   '0' [xX] [0-9a-fA-F] +
    ;

WS  :   (   ' '
        |   '\t'
        |   '\r'
        |   '\n'
        )+
	-> channel(HIDDEN)	 
    ;

MiniC 문법 분석

1. ANTLR 문법 개요

ANTLR 문법 파일은 크게 두 부분으로 나뉩니다.

  • 파서 규칙(Parser Rules): 프로그램의 구문을 정의하는 규칙.
  • 어휘 규칙(Lexer Rules): 프로그램의 최소 단위(키워드, 식별자, 숫자 등)를 정의.

2. 프로그램 구조 정의

2.1. 프로그램의 시작

program : decl+ ;
  • program은 MiniC 프로그램의 시작점이며, 하나 이상의 decl(선언)로 구성됨.

2.2. 선언(Declaration)

decl : var_decl
     | fun_decl ;
  • 선언은 변수 선언(var_decl) 또는 함수 선언(fun_decl)이 될 수 있음.

3. 변수 선언 (Variable Declaration)

var_decl : type_spec IDENT ';' 
         | type_spec IDENT '=' LITERAL ';' 
         | type_spec IDENT '[' LITERAL ']' ';' ;
  • int a; → 기본적인 변수 선언.
  • int b = 5; → 변수를 선언하면서 초기값 설정.
  • int arr[10]; → 배열 선언.

4. 자료형 정의 (Type Specifier)

type_spec : VOID
          | INT ;
  • VOIDvoid 타입을 의미.
  • INTint 타입을 의미.

5. 함수 선언 (Function Declaration)

fun_decl : type_spec IDENT '(' params ')' compound_stmt ;
  • 함수 선언 예시:
    int add(int a, int b) {
        return a + b;
    }

6. 함수의 매개변수 (Function Parameters)

params : param (',' param)*
       | VOID
       | ;
  • param (',' param)* → 여러 개의 매개변수 가능.
  • VOID → 매개변수가 없는 경우.

6.1. 매개변수 정의

param : type_spec IDENT
      | type_spec IDENT '[' ']' ;
  • int a → 일반 변수 매개변수.
  • int arr[] → 배열 매개변수.

7. 문장(Statements)

stmt : expr_stmt
     | compound_stmt
     | if_stmt
     | while_stmt
     | return_stmt ;
  • 문장은 여러 가지 유형으로 나뉘며 각각 특정한 기능 수행.

7.1. 표현식 문 (Expression Statement)

expr_stmt : expr ';' ;
  • 예: a = 5;

7.2. 블록 문장 (Compound Statement)

compound_stmt : '{' local_decl* stmt* '}' ;
  • {} 내부에 여러 개의 지역 변수 선언(local_decl)과 문장(stmt) 포함.

7.3. 지역 변수 선언 (Local Variable Declaration)

local_decl : type_spec IDENT ';'
           | type_spec IDENT '=' LITERAL ';'
           | type_spec IDENT '[' LITERAL ']' ';' ;

7.4. 조건문 (If Statement)

if_stmt : IF '(' expr ')' stmt
        | IF '(' expr ')' stmt ELSE stmt ;
  • 예:
    if (x > 0) {
        x = x - 1;
    } else {
        x = 0;
    }

7.5. 반복문 (While Statement)

while_stmt : WHILE '(' expr ')' stmt ;
  • 예:
    while (x > 0) {
        x = x - 1;
    }

7.6. 반환문 (Return Statement)

return_stmt : RETURN ';'
            | RETURN expr ';' ;
  • return; → 값 없이 반환.
  • return expr; → 값을 반환.

8. 표현식 (Expressions)

expr : LITERAL
     | '(' expr ')'
     | IDENT
     | IDENT '[' expr ']'
     | IDENT '(' args ')'
     | '-' expr
     | '+' expr
     | '--' expr
     | '++' expr
     | expr '*' expr
     | expr '/' expr
     | expr '%' expr
     | expr '+' expr
     | expr '-' expr
     | expr EQ expr
     | expr NE expr
     | expr LE expr
     | expr '<' expr
     | expr GE expr
     | expr '>' expr
     | '!' expr
     | expr AND expr
     | expr OR expr
     | IDENT '=' expr
     | IDENT '[' expr ']' '=' expr ;
  • 산술 연산자(+, -, *, /, %) 사용 가능.
  • 관계 연산자(==, !=, <, >, <=, >=) 포함.
  • 논리 연산(and, or, !) 가능.

9. 인자 목록 (Arguments)

args : expr (',' expr)*
     | ;
  • expr (',' expr)* → 여러 개의 인자 전달 가능.
  • ; → 인자가 없을 수도 있음.

10. 어휘 규칙 (Lexer Rules)

MiniC에서 사용하는 기본적인 토큰 정의.

키워드

VOID: 'void';
INT: 'int';

WHILE: 'while';
IF: 'if';
ELSE: 'else';
RETURN: 'return';

연산자 및 비교 연산자

OR: 'or';
AND: 'and';
LE: '<=';
GE: '>=';
EQ: '==';
NE: '!=';

식별자 및 리터럴

IDENT : [a-zA-Z_] ([a-zA-Z_] | [0-9])* ;
LITERAL : DecimalConstant | OctalConstant | HexadecimalConstant ;

공백 및 무시할 문자

WS : (' ' | '\t' | '\r' | '\n')+ -> channel(HIDDEN);

ANTLR4를 사용하여 MiniC 언어의 소스 코드를 파싱해보기

- 입력 파일 ( test.c ) 을 Token으로 변환하고 Parsing 해보기 

// Main.java

import org.antlr.v4.runtime.*;
import org.antlr.v4.runtime.tree.*;
import generated.tinyR4Lexer;
import generated.tinyR4Parser;

public class Main {
    public static void main(String[] args) throws Exception {
        tinyR4Lexer lexer = new tinyR4Lexer(CharStreams.fromFileName("test.tr"));
        CommonTokenStream tokens = new CommonTokenStream(lexer);
        tinyR4Parser parser = new tinyR4Parser(tokens);
        ParseTree tree = parser.program();
    }
}
//test.c

#include <stdio.h>

int main() {
    printf("hello, world!\n");
    return 0;
}

실행 결과

디렉토리 구조

4. Java, ANTLR 를 이용하여 MiniC 문법으로 된 코드에 대한 ugly print 코드 작성하기

  1. fn main 함수와 사칙연산 구현

    • 그 외 나머지 문법은 구현 X
  2. 2진 연산자와 피 연산자 사이에는 빈칸 1칸

    • X + Y;
  3. Listener 방식을 이용하여 코드 구성

MiniCPrintListener 분석

개요

MiniCPrintListener 클래스는 ANTLR4를 이용해 MiniC 언어의 구문을 파싱한 후, 이를 다시 코드 형태로 변환하여 출력하는 역할을 합니다. MiniCBaseListener를 상속받으며, ParseTreeListener 인터페이스를 구현하여 파싱 트리를 순회하면서 필요한 정보를 저장하고 변환합니다.

주요 필드

private static StringBuilder output = new StringBuilder();
private ParseTreeProperty<String> miniCTree = new ParseTreeProperty<>();
  • output: 변환된 MiniC 코드를 저장하는 StringBuilder 객체입니다.
  • miniCTree: ANTLR의 ParseTreeProperty를 이용하여 특정 노드에 변환된 코드를 저장하는 역할을 합니다.

주요 메서드 분석

1. getOutput()

public static String getOutput() {
    return output.toString();
}
  • output에 저장된 전체 변환 결과를 반환합니다.

2. exitProgram(MiniCParser.ProgramContext ctx)

@Override
public void exitProgram(MiniCParser.ProgramContext ctx) {
    for (MiniCParser.DeclContext declCtx : ctx.decl()) {
        if (miniCTree.get(declCtx) != null) {
            output.append(miniCTree.get(declCtx)).append("\n");
        }
    }
}
  • MiniC 프로그램 전체를 처리하는 메서드입니다.
  • decl() 리스트를 순회하면서 각 선언을 miniCTree에서 가져와 output에 추가합니다.

3. exitFun_decl(MiniCParser.Fun_declContext ctx)

@Override
public void exitFun_decl(MiniCParser.Fun_declContext ctx) {
    String funcHeader = ctx.type_spec().getText() + " " + ctx.IDENT().getText() + "() {";
    output.append(funcHeader).append("\n");

    if (ctx.compound_stmt() != null && miniCTree.get(ctx.compound_stmt()) != null) {
        output.append(miniCTree.get(ctx.compound_stmt()));
    }

    output.append("}\n");
}
  • 함수 선언을 처리하는 메서드입니다.
  • 함수의 반환 타입과 이름을 가져와 output에 추가합니다.
  • 함수 본문이 존재하면 변환된 내용을 추가합니다.

4. exitCompound_stmt(MiniCParser.Compound_stmtContext ctx)

@Override
public void exitCompound_stmt(MiniCParser.Compound_stmtContext ctx) {
    StringBuilder block = new StringBuilder();
    for (MiniCParser.StmtContext stmtCtx : ctx.stmt()) {
        if (miniCTree.get(stmtCtx) != null) {
            block.append("    ").append(miniCTree.get(stmtCtx)).append(";\n");
        }
    }
    miniCTree.put(ctx, block.toString());
}
  • {}로 감싸진 블록을 처리합니다.
  • 내부의 stmt() 리스트를 순회하며 변환된 코드를 miniCTree에 저장합니다.

5. exitStmt(MiniCParser.StmtContext ctx)

@Override
public void exitStmt(MiniCParser.StmtContext ctx) {
    if (ctx.expr_stmt() != null) {
        miniCTree.put(ctx, miniCTree.get(ctx.expr_stmt()));
    }
}
  • stmt()expr_stmt()를 포함할 경우, 해당 표현식 문을 변환된 코드로 저장합니다.

6. exitExpr_stmt(MiniCParser.Expr_stmtContext ctx)

@Override
public void exitExpr_stmt(MiniCParser.Expr_stmtContext ctx) {
    if (ctx.expr() != null) {
        miniCTree.put(ctx, miniCTree.get(ctx.expr()));
    }
}
  • 표현식 문을 변환하여 miniCTree에 저장합니다.

7. exitExpr(MiniCParser.ExprContext ctx)

@Override
public void exitExpr(MiniCParser.ExprContext ctx) {
    if (ctx.getChildCount() == 3) {
        String left = miniCTree.get(ctx.getChild(0));
        String op = ctx.getChild(1).getText();
        String right = miniCTree.get(ctx.getChild(2));
        System.out.println("Parsed expression: " + left + " " + op + " " + right);
        miniCTree.put(ctx, left + " " + op + " " + right);
    } else {
        miniCTree.put(ctx, ctx.getText());
    }
}
  • 이항 연산자(Binary Operation) 표현식을 처리하는 메서드입니다.
  • 연산자와 피연산자를 변환하여 저장하며, 디버깅용으로 콘솔에 출력합니다.

결론

이 클래스는 MiniC 언어의 파싱 트리를 순회하면서 변환된 코드를 재구성하는 역할을 합니다. ParseTreeProperty<String>을 활용하여 트리의 각 노드에서 변환된 코드를 저장하고, 이를 출력하는 방식으로 동작합니다. 향후 확장을 위해 다양한 stmtexpr 타입을 추가적으로 처리할 수 있도록 개선할 수 있습니다.

parse tree


출력 결과

0개의 댓글