ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 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여서 뭔가 가장처음에 시도한 방법으로는 일괄 처리가 안 됐다. 

    음... 일단 오늘은 늦었으니까 여기까찌.

     

Designed by Tistory.