antlr 라이브러리는 .g4파일을 통해 파싱 규칙을 정의한다.
미리 정의한 .g4파일을 통해 gradle에서 자바코드로 문법클래스를 자동으로 생성한다.
이렇게 문법파일을 자바 클래스로 변환하여 자바 코드 레벨에서 String형태의 자료형에 문법을 적용하여 파싱을 진행할 수 있다.
먼저, gradle에서 antlr플러그인 + runtime을 추가한다.
plugins {
id 'antlr'
}
dependencies {
antlr "org.antlr:antlr4:4.13.1" // g4 파일을 java 코드로 변환
implementation "org.antlr:antlr4-runtime:4.13.1" // 실행할 때 사용
}
antlr {
arguments += ['-visitor', '-long-messages'] // Visitor & Listener 자동 생성
}
.g4파일 위치는 src/main/antlr/Arithmetic.g4로 두면 된다.grammar Arithmetic;
// Parser rules
expr : addExpr ;
addExpr : mulExpr ('+' mulExpr)* ;
mulExpr : atom ('*' atom)* ;
atom : INT | '(' expr ')' ;
// Lexer rules
INT : [0-9]+ ;
WS : [ \t\r\n]+ -> skip ;
ANTLR문법은 크게 3가지 파트로 나눌 수 잇다.
Grammar 선언
grammar {규칙이름};으로 작성한다.{규칙이름}.g4가 되어야 한다.Arithmetic.g4이 될 것이다.Parser 규칙
expr라는 의미 단위를 곱셈, 덧셈, 괄호처리를 작은 단위로 분해할 수 있도록 정의하였다. expr → addExpr → mulExpr → atom으로 진행되는 흐름을 확인할 수 있다.
[0-9]+로 정의되어 하나 이상의 숫자를 의미한다.skip을 붙여 무시하도록 설정하였다.build.gradle를 작성하는 것과 비슷하다고 생각했다../gradlew generateGrammarSource
.g4파일을 자바 코드로 변환한다.Lexer, Parser, BaseVisitor, Visitor, Listener 클래스가 build/generated-src/antlr/main/경로에 생성된다.public class Main {
public static void main(String[] args) {
String input = "2+3*4";
// 1. 문자열을 CharStream으로 변환
CodePointCharStream stream = CharStreams.fromString(input);
// 2. Lexer 생성
ArithmeticLexer lexer = new ArithmeticLexer(stream);
// 3. TokenStream 생성
CommonTokenStream tokens = new CommonTokenStream(lexer);
// 4. Parser 생성
ArithmeticParser parser = new ArithmeticParser(tokens);
// 5. 진입점(expr) 파싱 → ParseTree 얻기
ParseTree tree = parser.expr();
System.out.println(tree.toStringTree(parser));
}
}
parser.expr();를 통해 ParseTree라는 것을 생성한다.ParseTree는 각 노드로 이루어지며, 각 파서를 담당하는 노드 클래스는 ParserTree.{파서이름Context}로 구현되어있다.(regex (expr (element (atom (charClass [ (classAtom a) (classAtom b) (classAtom c) ])))))
BaseVisitor.class로 문제를 해결할 수 있다.public class ArithmeticBaseVisitor<T> extends AbstractParseTreeVisitor<T>
implements ArithmeticVisitor<T> {
@Override public T visitMulExpr(ArithmeticParser.MulExprContext ctx) {
return visitChildren(ctx);
}
@Override public T visitAddExpr(ArithmeticParser.AddExprContext ctx) {
return visitChildren(ctx);
}
...
}
visitChildren(ctx);를 반환한다.MulExpr 라는 파서를 .g4에서 선언했기에 자동생성되었다.public class ArithmeticVisitorImpl extends ArithmeticBaseVisitor<Integer> {
@Override
public Integer visitIntExpr(ArithmeticParser.IntExprContext ctx) {
return Integer.valueOf(ctx.INT().getText());
}
@Override
public Integer visitAddExpr(ArithmeticParser.AddExprContext ctx) {
int left = visit(ctx.expr(0));
int right = visit(ctx.expr(1));
return left + right;
}
@Override
public Integer visitMulExpr(ArithmeticParser.MulExprContext ctx) {
int left = visit(ctx.expr(0));
int right = visit(ctx.expr(1));
return left * right;
}
@Override
public Integer visitParenExpr(ArithmeticParser.ParenExprContext ctx) {
return visit(ctx.expr());
}
}
visitAddExpr 메서드에서, AddxprContext 라는 노드의 첫 번째 expr에 방문하면, 다시 visitIntExpr메서드가 invoke되어, 해당 expr노드의 값을 리턴한다.expr의 값을 더해 덧셈이 구현된다.BaseVisitor는, 사용자가 트리를 탐색하는 시점(visit(ctx.expr()))을 직접 제어한다.Listener클래스는 파서가 ParseTree를 생성할 때, 자동으로 콜백메서드를 호출한다.public class RegexBaseListener implements RegexListener {
/**
* {@inheritDoc}
*
* <p>The default implementation does nothing.</p>
*/
@Override public void enterRegex(RegexParser.RegexContext ctx) { }
/**
* {@inheritDoc}
*
* <p>The default implementation does nothing.</p>
*/
@Override public void exitRegex(RegexParser.RegexContext ctx) { }
/**
* {@inheritDoc}
*
* <p>The default implementation does nothing.</p>
*/
@Override public void enterExpr(RegexParser.ExprContext ctx) { }
/**
* {@inheritDoc}
*
* <p>The default implementation does nothing.</p>
*/
@Override public void exitExpr(RegexParser.ExprContext ctx) { }
/**
* {@inheritDoc}
*
* <p>The default implementation does nothing.</p>
*/
@Override public void enterElement(RegexParser.ElementContext ctx) { }
/**
* {@inheritDoc}
*
* <p>The default implementation does nothing.</p>
*/
@Override public void exitElement(RegexParser.ElementContext ctx) { }