앞 장에서 언급했던 Pretty Printer 와 달리, Ugly Print는 말 그대로 못생긴 출력이다. 이는 코드를 난독화하여 이해하기 어렵게 하는 방식이다.
이를 위해서 Listener pattern을 활용할 수 있다.
ANother Tool for Language Recognition : 언어 인식을 위한 또 다른 도구
앞서 배웠던 LLVM과는 이러한 차이점이 존재한다.
| 컴파일러 단계 | 역할 | 사용 기술 |
|---|---|---|
| 프론트엔드 (Frontend) | 소스 코드 분석 → AST(구문 트리) 생성 | ANTLR |
| 중간 표현 (IR, Intermediate Representation) | AST → LLVM IR 변환 | 자체 구현 or Clang |
| 백엔드 (Backend) | LLVM IR 최적화 및 코드 생성 | LLVM |
| 기계 코드 생성 (Machine Code Generation) | IR을 CPU용 실행 코드로 변환 | LLVM |
즉, ANTLR을 사용한다면
이러한 ANTLR에는 두가지 방식이 존재한다.
두가지 방식 중 Listener 를 활용한다면, 코드를 Parsing 하면서 자동으로 특정 동작을 수행할 수 있다.
https://www.antlr.org/download.html



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



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)
;
ANTLR 문법 파일은 크게 두 부분으로 나뉩니다.
program : decl+ ;
program은 MiniC 프로그램의 시작점이며, 하나 이상의 decl(선언)로 구성됨.decl : var_decl
| fun_decl ;
var_decl) 또는 함수 선언(fun_decl)이 될 수 있음.var_decl : type_spec IDENT ';'
| type_spec IDENT '=' LITERAL ';'
| type_spec IDENT '[' LITERAL ']' ';' ;
int a; → 기본적인 변수 선언.int b = 5; → 변수를 선언하면서 초기값 설정.int arr[10]; → 배열 선언.type_spec : VOID
| INT ;
VOID → void 타입을 의미.INT → int 타입을 의미.fun_decl : type_spec IDENT '(' params ')' compound_stmt ;
int add(int a, int b) {
return a + b;
}params : param (',' param)*
| VOID
| ;
param (',' param)* → 여러 개의 매개변수 가능.VOID → 매개변수가 없는 경우.param : type_spec IDENT
| type_spec IDENT '[' ']' ;
int a → 일반 변수 매개변수.int arr[] → 배열 매개변수.stmt : expr_stmt
| compound_stmt
| if_stmt
| while_stmt
| return_stmt ;
expr_stmt : expr ';' ;
a = 5;compound_stmt : '{' local_decl* 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 ;
if (x > 0) {
x = x - 1;
} else {
x = 0;
}while_stmt : WHILE '(' expr ')' stmt ;
while (x > 0) {
x = x - 1;
}return_stmt : RETURN ';'
| RETURN expr ';' ;
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 ;
+, -, *, /, %) 사용 가능.==, !=, <, >, <=, >=) 포함.and, or, !) 가능.args : expr (',' expr)*
| ;
expr (',' expr)* → 여러 개의 인자 전달 가능.; → 인자가 없을 수도 있음.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);
- 입력 파일 ( 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;
}
실행 결과

디렉토리 구조

fn main 함수와 사칙연산 구현
2진 연산자와 피 연산자 사이에는 빈칸 1칸
X + Y;Listener 방식을 이용하여 코드 구성
MiniCPrintListener 클래스는 ANTLR4를 이용해 MiniC 언어의 구문을 파싱한 후, 이를 다시 코드 형태로 변환하여 출력하는 역할을 합니다. MiniCBaseListener를 상속받으며, ParseTreeListener 인터페이스를 구현하여 파싱 트리를 순회하면서 필요한 정보를 저장하고 변환합니다.
private static StringBuilder output = new StringBuilder();
private ParseTreeProperty<String> miniCTree = new ParseTreeProperty<>();
output: 변환된 MiniC 코드를 저장하는 StringBuilder 객체입니다.miniCTree: ANTLR의 ParseTreeProperty를 이용하여 특정 노드에 변환된 코드를 저장하는 역할을 합니다.getOutput()public static String getOutput() {
return output.toString();
}
output에 저장된 전체 변환 결과를 반환합니다.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");
}
}
}
decl() 리스트를 순회하면서 각 선언을 miniCTree에서 가져와 output에 추가합니다.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에 추가합니다.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에 저장합니다.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()를 포함할 경우, 해당 표현식 문을 변환된 코드로 저장합니다.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에 저장합니다.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());
}
}
이 클래스는 MiniC 언어의 파싱 트리를 순회하면서 변환된 코드를 재구성하는 역할을 합니다. ParseTreeProperty<String>을 활용하여 트리의 각 노드에서 변환된 코드를 저장하고, 이를 출력하는 방식으로 동작합니다. 향후 확장을 위해 다양한 stmt 및 expr 타입을 추가적으로 처리할 수 있도록 개선할 수 있습니다.
parse tree


출력 결과