これは何? †
- https://www.antlr.org/index.html
- テキストを構文解析して構文木を作って、構文木を使ってなにかをするフレームワーク
- 構文木を作るところまでは、ANTLR4 が、構文規則から自動的に作ってくれる
- 構文木を解析するクラスの基底クラス (Visitor) も ANTLR4 が作ってくれる
- つまり、ANTLR4 のフレームワークに則って、基底クラスを継承する Visitor を作るだけで、テキストを解析して何かをするプログラムを作ることができる Viva!
- 例
- 入力テキスト
1 + 1 // THIS IS COMMENT.
2 + 3 * 4 // EXECUTE THE MULTIPLE SENTENCE ON THE RIGHT FIRST.
( 5 + 6 ) / 7 // EXECUTE THE PARENTS ON THE LEFT FIRST, INSTEAD OF RIGHT DIV.
- 構文木
- 構文木を読み込んで計算してみる
-------------------------------------------------------
T E S T S
-------------------------------------------------------
Running com.example.antlr4exam.ExprCalcTest
2021-10-28 22:34:55,237 [main] INFO c.e.a.ExprCalcVisitor L001 : 2.0
2021-10-28 22:34:55,239 [main] INFO c.e.a.ExprCalcVisitor // THIS IS COMMENT.
2021-10-28 22:34:55,239 [main] INFO c.e.a.ExprCalcVisitor
2021-10-28 22:34:55,240 [main] INFO c.e.a.ExprCalcVisitor L002 : 14.0
2021-10-28 22:34:55,240 [main] INFO c.e.a.ExprCalcVisitor // EXECUTE THE MULTIPLE SENTENCE ON THE RIGHT FIRST.
2021-10-28 22:34:55,241 [main] INFO c.e.a.ExprCalcVisitor
2021-10-28 22:34:55,241 [main] INFO c.e.a.ExprCalcVisitor L003 : 1.5714285714285714
2021-10-28 22:34:55,242 [main] INFO c.e.a.ExprCalcVisitor // EXECUTE THE PARENTS ON THE LEFT FIRST, INSTEAD OF RIGHT DIV.
2021-10-28 22:34:55,242 [main] INFO c.e.a.ExprCalcVisitor
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.495 sec
ANTLR = Anti LR †
ツール †
ANTLR Tools †
- https://www.antlr.org/download.html から antlr-4.x.x-compliete.jar をダウンロード
- 次の場所に配置 /usr/local/lib/antlr-4.9.2-complete.jar
- 環境変数設定 (~/.bashrc に追加)
export CLASSPATH="/usr/local/lib/antlr-4.9.2-complete.jar:$CLASSPATH"
alias grun="java org.antlr.v4.gui.TestRig"
- 確認
$ source .bashrc
$ grun
java org.antlr.v4.gui.TestRig GrammarName startRuleName
[-tokens] [-tree] [-gui] [-ps file.ps] [-encoding encodingname]
[-trace] [-diagnostics] [-SLL]
[input-filename(s)]
Use startRuleName='tokens' if GrammarName is a lexer grammar.
Omitting input-filename makes rig read from stdin.
- 構文木のGUI表示をする (構文木のデバックをする)
VSCode Plugin †
- 構文規則を図示することができる
- Expr.g4
grammar Expr;
startRule: prog EOF;
prog: line+;
line: expr comment? NEWLINE;
expr:
parents_expr
| expr MUL_DEV expr
| expr PLUS_MINUS expr
| NUMBER;
parents_expr: '(' expr ')';
MUL_DEV: ('*' | '/');
PLUS_MINUS: ('+' | '-');
NUMBER: ('-' | '+')? [0-9]+ ('.' [0-9]+)?;
NEWLINE: '\r'? '\n';
WHITESPACE: [ \t]+ -> skip;
comment: '//' WORD+;
WORD: ALPHABET+;
ALPHABET: [a-zA-Z.,];
- 図示
プロジェクト構成 †
- antlr4-maven-plugin でビルドする。 → https://www.antlr.org/api/maven-plugin/latest/antlr4-mojo.html の構成に従う
- ほぼ、通常の Maven の構成だが、src/main/antlr4 以下に構文ファイル *.g4 を置く
- 自動生成されるコードにパッケージをつけたければ *.g4 を置くディレクトリをパッケージ構成と同じにする
- pom.xml はこんな感じ。特に関心のない構文に関する処理をサボるために AspectJ でデフォルト処理を埋め込むようにすると便利。
<?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>com.example</groupId>
<artifactId>ANTLR4Exam</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
<build>
<plugins>
<plugin>
<!-- We have to user dev.aspectj instead of org.aspectj because org.aspectj can't treat Java9+. -->
<groupId>dev.aspectj</groupId>
<artifactId>aspectj-maven-plugin</artifactId>
<version>1.13.M3</version>
<configuration>
<complianceLevel>11</complianceLevel>
<!-- IMPORTANT to use the ajpectj with the lombok.
Execute the aspectj after javac and lombok.
Thus, weave aspects to class-file instead of src-file. -->
<excludes>
<exclude>**/*.java</exclude>
</excludes>
<forceAjcCompile>true</forceAjcCompile>
<sources/>
<!-- IMPORTANT-->
</configuration>
<executions>
<execution>
<id>default-compile</id>
<phase>process-classes</phase>
<goals>
<!-- use this goal to weave all your main classes -->
<goal>compile</goal>
</goals>
<configuration>
<weaveDirectories>
<weaveDirectory>${project.build.directory}/classes</weaveDirectory>
</weaveDirectories>
</configuration>
</execution>
<execution>
<id>default-testCompile</id>
<phase>process-test-classes</phase>
<goals>
<!-- use this goal to weave all your test classes -->
<goal>test-compile</goal>
</goals>
<configuration>
<weaveDirectories>
<weaveDirectory>${project.build.directory}/test-classes</weaveDirectory>
</weaveDirectories>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<!-- https://www.antlr.org/api/maven-plugin/latest/antlr4-mojo.html -->
<groupId>org.antlr</groupId>
<artifactId>antlr4-maven-plugin</artifactId>
<version>4.9.2</version>
<executions>
<execution>
<id>antlr</id>
<goals>
<goal>antlr4</goal>
</goals>
<configuration>
<visitor>true</visitor>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.4</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<artifactSet>
<excludes>
<exclude>junit:junit</exclude>
<exclude>javax.servlet:servlet-api</exclude>
</excludes>
</artifactSet>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<dependencies>
<!-- ANTLR4 -->
<dependency>
<groupId>org.antlr</groupId>
<artifactId>antlr4-runtime</artifactId>
<version>4.9.2</version>
<type>jar</type>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.20</version>
<scope>provided</scope>
</dependency>
<!-- SLF4J and Logback-class and Logback-core -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
<!-- AspectJ -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.9.7.M3</version>
</dependency>
<dependency>
<groupId>com.nickwongdev</groupId>
<artifactId>aspectj-maven-plugin</artifactId>
<version>1.12.6</version>
</dependency>
<!-- apache cli : Command Line Interface -->
<dependency>
<groupId>commons-cli</groupId>
<artifactId>commons-cli</artifactId>
<version>1.3.1</version>
</dependency>
<!-- JUnit -->
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-all</artifactId>
<version>1.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
<type>jar</type>
</dependency>
</dependencies>
</project>
ユーザアプリ †
- 基本的な処理の構成
- 自動生成された ExprParser? によってテキストから構文木を作る
- Parser を呼び出すときに Listener を貼り付けて、Parser が構文解析をするときのイベントを拾うことができる。まぁ log 取り以外には役に立たないかな。
- できた構文木を ExprCalcVisitor? によって処理する。ExprCalcVisitor? は自動生成された ExprVisitor? を継承する。ノード XXXX に行き着いたときに visitXXXX() が呼ばれる仕組みになっている
- 全部の visitXXXX() を実装するのはめんどいので、ユーザアプリ側の ExprCalcVisitor? で visitXXXX() を実装しなかったとき、つまり自動生成された ExprVisitor?#visitXXXX() が呼ばれたときのデフォルト処理を AspectJ で書いておくと便利
package com.example.antlr4exam;
import com.example.antlr4exam.grammar.ExprLexer;
import com.example.antlr4exam.grammar.ExprParser;
import com.example.antlr4exam.grammar.ExprParser.ProgContext;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.charset.Charset;
import lombok.extern.slf4j.Slf4j;
import org.antlr.v4.runtime.CharStream;
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CommonTokenStream;
@Slf4j
public class ExprCalcGenerator {
public static final void main(String[] args) {
try {
String answers = (new ExprCalcGenerator()).calcAnser(new File(args[0]));
System.out.printf(answers);
} catch (IOException ex) {
log.error("ERROR", ex);
System.exit(-1);
}
}
public String calcAnser(File calcFiles) throws FileNotFoundException, IOException {
CharStream stream = CharStreams.fromFileName(calcFiles.getAbsolutePath(), Charset.forName("UTF-8"));
ExprLexer lexer = new ExprLexer(stream);
CommonTokenStream tokens = new CommonTokenStream(lexer);
ExprParser parser = new ExprParser(tokens);
// Attach listener. I think listener is only available for logging.
ExprCalcListener listener = new ExprCalcListener();
parser.addParseListener(listener);
// Create tree from the document.
ProgContext prog = parser.prog();
// Create visitor.
ExprCalcVisitor visitor = new ExprCalcVisitor();
// Process tree using visitor.
String result = prog.accept(visitor);
return result;
}
}
package com.example.antlr4exam;
import com.example.antlr4exam.grammar.ExprListener;
import com.example.antlr4exam.grammar.ExprParser;
import org.antlr.v4.runtime.ParserRuleContext;
import org.antlr.v4.runtime.tree.ErrorNode;
import org.antlr.v4.runtime.tree.TerminalNode;
public class ExprCalcListener extends BaseListener implements ExprListener {
@Override
public void enterStartRule(ExprParser.StartRuleContext ctx) {
dumpEnter("START RULE");
}
@Override
public void exitStartRule(ExprParser.StartRuleContext ctx) {
dumpExit("START RULE");
}
@Override
public void enterLine(ExprParser.LineContext ctx) {
dumpEnter("LINE");
}
@Override
public void exitLine(ExprParser.LineContext ctx) {
dumpExit("LINE");
}
@Override
public void enterExpr(ExprParser.ExprContext ctx) {
dumpEnter("EXPR");
}
@Override
public void exitExpr(ExprParser.ExprContext ctx) {
dumpExit("EXPR");
}
@Override
public void enterProg(ExprParser.ProgContext ctx) {
dumpEnter("PROG");
}
@Override
public void exitProg(ExprParser.ProgContext ctx) {
dumpExit("PROG");
}
@Override
public void visitTerminal(TerminalNode tn) {
dump(tn.getPayload().toString());
}
@Override
public void visitErrorNode(ErrorNode en) {
dump("Error!");
}
@Override
public void enterEveryRule(ParserRuleContext prc) {
}
@Override
public void exitEveryRule(ParserRuleContext prc) {
}
@Override
public void enterParents_expr(ExprParser.Parents_exprContext ctx) {
dumpEnter("PARENTS");
}
@Override
public void exitParents_expr(ExprParser.Parents_exprContext ctx) {
dumpExit("PARENTS");
}
@Override
public void enterComment(ExprParser.CommentContext ctx) {
dumpEnter("COMMENT");
}
@Override
public void exitComment(ExprParser.CommentContext ctx) {
dumpExit("COMMENT");
}
}
package com.example.antlr4exam;
import com.example.antlr4exam.grammar.ExprBaseVisitor;
import com.example.antlr4exam.grammar.ExprParser;
import lombok.extern.slf4j.Slf4j;
import org.antlr.v4.runtime.tree.ErrorNode;
import org.antlr.v4.runtime.tree.TerminalNode;
@Slf4j
public class ExprCalcVisitor extends ExprBaseVisitor<String> {
@Override
public String visitProg(ExprParser.ProgContext ctx) {
// "line+"
StringBuilder sb = new StringBuilder();
ctx.children.forEach(subtree -> sb.append(visit(subtree)).append("\n"));
return sb.toString();
}
@Override
public String visitLine(ExprParser.LineContext ctx) {
// "expr comment? NEWLINE"
String ans = String.format("L%03d : %s", ctx.getStart().getLine(), visit(ctx.getChild(0)));
log.info(ans);
// comment
for( int cnt = 1; cnt < ctx.getChildCount() ; cnt++) {
log.info(visit(ctx.getChild(cnt)));
}
return ans;
}
@Override
public String visitExpr(ExprParser.ExprContext ctx) {
// If there is one child, that's Number.
if (1 == ctx.getChildCount()) {
return visitChildren(ctx);
}
// process "expr op expr"
double left = Double.parseDouble(visit(ctx.getChild(0)));
String op = visit(ctx.getChild(1));
double right = Double.parseDouble(visit(ctx.getChild(2)));
double ans = 0.0;
switch (op) {
case "+":
ans = left + right;
break;
case "-":
ans = left - right;
break;
case "*":
ans = left * right;
break;
case "/":
ans = left / right;
break;
}
log.debug("{} {} {} = {}", left, op, right, ans);
return Double.toString(ans);
}
@Override
public String visitParents_expr(ExprParser.Parents_exprContext ctx) {
// "( expr )"
// process "expr"
String ans = visit(ctx.getChild(1));
log.debug("({})", ans);
return ans;
}
@Override
public String visitTerminal(TerminalNode node) {
return node.getText();
}
@Override
public String visitErrorNode(ErrorNode node) {
log.error("ERROR");
return null;
}
// EXAMINATION: Process the comment nodes by the AspectJ.
//@Override public String visitComment(ExprParser.CommentContext ctx) { log.info("COMMENT");return visitChildren(ctx); }
}
package com.example.antlr4exam;
import com.example.antlr4exam.grammar.ExprBaseVisitor;
import lombok.extern.slf4j.Slf4j;
import org.antlr.v4.runtime.ParserRuleContext;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
@Aspect
@Slf4j
public class VisitorAspect {
@Around("execution(public * com.example.antlr4exam.grammar.ExprBaseVisitor.visit*(..))")
public Object aroundExecute(ProceedingJoinPoint thisJoinPoint) throws Throwable {
Class targetClass = thisJoinPoint.getSignature().getDeclaringType();
String targetMethod = thisJoinPoint.getSignature().getName();
Object targetObject = thisJoinPoint.getTarget();
if (targetClass.equals(ExprBaseVisitor.class)) {
// The method in the ExperBaseVisitor class was invoked.
// On the other word, this method is not defined in user defined class such as the ExperCalcVisitor.
if (targetObject instanceof ExprCalcVisitor) {
// If ExprCalcVisitor (extends ExprBaseVisitor) was invoked and invoked method was not defined in ExprCalcVisitor,
// the node of the ANTLR4 will be processed following code.
log.debug("********** hijacked: {}#{} {}", targetClass.getName(), targetMethod, targetObject.getClass().getName());
ParserRuleContext ctx = (ParserRuleContext) (thisJoinPoint.getArgs()[0]);
// In this exmple, simply connect texts of child nodes.
StringBuilder sb = new StringBuilder();
for (int cnt = 0; cnt < ctx.getChildCount(); cnt++) {
sb.append(ctx.getChild(cnt).getText()).append(" ");
}
return sb.toString().trim();
}
//else if (targetObject instanceof ExprOTHERVisitor) {
// If ExprOTHERVisitor (extends ExprBaseVisitor) was invoked and ...
//}
}
return thisJoinPoint.proceed(thisJoinPoint.getArgs());
}
}
プログラム言語の構文規則を使って、プログラム解析ツールを作る †
- 公式サイトが、主要なプログラム言語の構文規則を提供してくれている
- これを使ってプログラム解析ツールを作ることができる
- 例えば、IF、FOR、WHILE などの処理制御命令を拾ってフローチャートを作るとか
- こういうことをやりたいときに AspectJ で、関心がない構文のデフォルト処理を横断的に定義できると便利
- フローチャートの例で言えば、IF、FOR、WHILE 以外は、単に□で囲んで前の命令・後ろの命令と線で結べばいいので、その処理を AspectJ で定義して CSHARPVisitor#visitXXXX() を共通的に上書きしてやればいい。分岐やループの命令に関する visitYYYY() のみを頑張って実装すればいい。
Java