-
TIL 119일 - Antlr로 덧셈 구현하기공부/개발 전체적인 2022. 4. 28. 02:03
Antlr
Antlr은 구조화돈 텍스트를 프로세싱하는 툴이다.
구현
일단 gradle 예시가 없어서 maven으로 했다.
pom.xml을 아래처럼 한다.
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>org.example</groupId> <artifactId>Antlr2</artifactId> <version>1.0-SNAPSHOT</version> <properties> <maven.compiler.source>16</maven.compiler.source> <maven.compiler.target>16</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>org.antlr</groupId> <artifactId>antlr4-runtime</artifactId> <version>4.7.1</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.antlr</groupId> <artifactId>antlr4-maven-plugin</artifactId> <version>4.7.1</version> <executions> <execution> <goals> <goal>antlr4</goal> </goals> </execution> </executions> </plugin> </plugins> </build> </project>
Antlr이 동작하는 과정은
1. 문법 파일을 준비한다.
2. 소스를 생성한다.
3. 리스너를 생성한다.
아무튼 위처럼 pom.xml을 수정하고, 아래 링크에서 문법을 가져온다.(메서드의 이름이 소문자로 시작하는지 확인하는 문법이라는데...)
https://github.com/antlr/codebuff/blob/master/grammars/org/antlr/codebuff/Java8.g4
GitHub - antlr/codebuff: Language-agnostic pretty-printing through machine learning (uh, like, is this possible? YES, apparently
Language-agnostic pretty-printing through machine learning (uh, like, is this possible? YES, apparently). - GitHub - antlr/codebuff: Language-agnostic pretty-printing through machine learning (uh, ...
github.com
mvn package
를 입력하면 target폴더에 뭔가..뭔가 생긴다...
MethodUppercaseListener 만들기
일단 위에서 target/generated_sources/antlr4에 파일들이 생기는데 이놈들을 가져와서 코드 사용하는 쪽으로 옮겨왔따.
그리고 MethodUppercaseListener 클래스 만들어주고...
import org.antlr.v4.runtime.tree.TerminalNode; import java.util.ArrayList; import java.util.List; public class UppercaseMethodListener extends Java8BaseListener { public List<String> getErrors() { return errors; } private List<String> errors = new ArrayList<>(); @Override public void enterMethodDeclarator(Java8Parser.MethodDeclaratorContext ctx) { TerminalNode node = ctx.Identifier(); String methodName = node.getText(); if (Character.isUpperCase(methodName.charAt(0))) { String error = String.format("Method %s is uppercased!", methodName); errors.add(error); } } }
테스트 코드를 아래처럼 만들어주니까
import org.antlr.v4.runtime.CharStreams; import org.antlr.v4.runtime.CommonTokenStream; import org.antlr.v4.runtime.tree.ParseTree; import org.antlr.v4.runtime.tree.ParseTreeWalker; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; class MainTest { @Test void name() { String javaClassContent = "public class SampleClass { void DoSomething(){} }"; Java8Lexer java8Lexer = new Java8Lexer(CharStreams.fromString(javaClassContent)); CommonTokenStream tokens = new CommonTokenStream(java8Lexer); Java8Parser parser = new Java8Parser(tokens); ParseTree tree = parser.compilationUnit(); ParseTreeWalker walker = new ParseTreeWalker(); UppercaseMethodListener listener = new UppercaseMethodListener(); walker.walk(listener, tree); assertThat(listener.getErrors().size()).isEqualTo(1); assertThat(listener.getErrors().get(0)).isEqualTo("Method DoSomething is uppercased!"); } }
일단은 예시에서 하는대로 똑같이 구현이 되기는 했다.
내가 뭘 한걸까?
UppercaseMethodListener class
일단 이놈은 내가 antlr 문법에 의해 만들어진 리스너를 상속해서 구현한 놈이다.
ParseTreeWalker의 인자로 리스너와 tree를 넣어주면 이 리스너로 tree를 읽어서 처리하는 것 처럼 보인다.
테스트 코드를 다시 보자...
1. 뭔가 분석할 문자열을 입력 한다.
String javaClassContent = "public class SampleClass { void DoSomething(){} }";
2. 렉싱과 파싱을 통해 트리를 만든다.
Java8Lexer java8Lexer = new Java8Lexer(CharStreams.fromString(javaClassContent));
CommonTokenStream tokens = new CommonTokenStream(java8Lexer);
Java8Parser parser = new Java8Parser(tokens);
ParseTree tree = parser.compilationUnit();/* Lexer - 어휘 분석을 한다. 분석하고자 하는 텍스트에서 의미가 있는 단어를 찾으면 token으로 만들어 주는 역할을 하는 놈 */
/* Parser - 토큰화된 데이터를 가지고 구조적으로 나타내는 역할을 하는 놈 */
3. ParseTreeWalker에 리스너와 만들어진 tree를 넣어서 동작하게 한다.
ParseTreeWalker walker = new ParseTreeWalker();
UppercaseMethodListener listener = new UppercaseMethodListener();
walker.walk(listener, tree);기타 등등...
java8.g4 문법 파일을 살펴보니 아래 같은 부분이 있다. 뭔가... 리스너에서 오버라이딩한 메서드 이름이 enterMethodDeclarator였는데... 음.... 이거랑 관련이 있어 보인다.
일단 느낌적인 느낌은 그래도 좀 알 거 같다. 이제 antlr 문법을 공부해서 덧셈을 구현해보자.
덧셈 문법을 구현해 보자
일단 아래같이 렉싱하고,파싱을하면 트리가 만들어 질 것이다.
문법 파일을아래처럼 만들어 봤다.
파싱 룰의 이름은 소문자, 렉싱 룰의 이름은 대문자다.
grammar Plus; /* * Parser Rules */ operation : NUMBER '+' NUMBER ; /* * Lexer Rules */ NUMBER : [0-9]+ ; WHITESPACE : ' ' -> skip ;
The basic syntax of a rule is easy: there is a name, a colon, the definition of the rule and a terminating semicolon
skip은 말그대로 skip이다.근데 위에처럼 더하기를 여러 번 할 때는 동작하지 않았다.
아래처럼 바꿨더니, 더하기가 여러 번이어도 동작했다.
grammar Plus; /* * Parser Rules */ operation : NUMBER '+' NUMBER | operation '+' NUMBER; /* * Lexer Rules */ NUMBER : [0-9]+ ; WHITESPACE : ' ' -> skip ;
테스트 코드는 아래처럼 짜봤다.
parser.operation() 부분이 좀 이해가 안 됐는데, 일단 오늘까지 공부한 것으로 추측컨데 내가 정의해둔 파싱 방법으로 이 것들을 파싱한다 뭐 이런 느낌인 듯하다. 내일 사칙연산 하면서 다시 봐봐야겠다.
@Test void name3() { String javaClassContent = "1 + 2 + 3 + 4 + 5"; PlusLexer plusLexer = new PlusLexer(CharStreams.fromString(javaClassContent)); CommonTokenStream tokens = new CommonTokenStream(plusLexer); PlusParser parser = new PlusParser(tokens); ParseTree tree = parser.operation(); ParseTreeWalker walker = new ParseTreeWalker(); PlusOperationListener listener = new PlusOperationListener(); walker.walk(listener, tree); assertThat(listener.getResult()).isEqualTo(15); }
그리고 리스너는 아래처럼 구현을 했었다.
public class PlusOperationListener extends PlusBaseListener { private int result = 0; public int getResult() { return result; } @Override public void exitOperation(PlusParser.OperationContext ctx) { if (ctx.NUMBER().size() == 2) { TerminalNode asd = ctx.NUMBER(0); TerminalNode qwe = ctx.NUMBER(1); result = Integer.parseInt(asd.getText()) + Integer.parseInt(qwe.getText()); } else { result += Integer.parseInt(ctx.NUMBER(0).getText()); } } }
그런데 처음에는 enterOperation 메서드를 오버라이딩해서 구현을 했었는데, ctx.NUMBER가 [5]부터 나왔었다.
그리고 현재 문법을 통해 1+2+3+4+5를 파싱하면 아래처럼 트리 구조가 만들어지는데, 사실 덧셈의 순서는 앞쪽 부터 해야 하기 때문에(뭐 덧셈만 있으면 상관 없긴 하지만)
exitOperation으로 바꾸니까 가장 처음에 ctx.NUMBER가 [1,2]가 나왔고 그 뒤에는 [3],[4],[5] 하나씩나오기 시작했다.
근데 또 생각이 난게 operation이라는 애는 결국 NUMBER의 조합이니까... 가져올 수 있지 않을까? 생각이 들었고 정답이었다.
근데 또 해보려고 하니까... 하나의 operation은 NUMBER+NUMBER이고 하나는 operation + NUMBER여서 뭔가 가장처음에 시도한 방법으로는 일괄 처리가 안 됐다.
음... 일단 오늘은 늦었으니까 여기까찌.
'공부 > 개발 전체적인' 카테고리의 다른 글
TIL 121일 - Visitor 패턴과 Antlr (0) 2022.04.30 TIL 120일 - Antlr으로 환불 규정...만들기?? (0) 2022.04.28 TIL 116일 - TDD로 알고리즘 문제 풀기 (0) 2022.04.24 TIL 115일 - 메서드를 만들 때는 매개변수가 유효한지 검사하라 (0) 2022.04.23 TIL 113일 - TDD (0) 2022.04.21