このweb記事を読んだ。
この記事はBASIC言語のインタープレターをJava言語で実装した事例である。BASIC言語の文法を記述したファイルをパーサー・ジェネレータANTLRに与える。ANTLRがパーサを構成するJavaコードを生成する。自動生成されたコードを利用する形でインタープレタをJava言語で実装している。ビルドツールMavenを用いている。
わたしはこの記事が紹介するJavaコードを写経して動かしてみた。そのコードはよくできていてほとんど修正不要だった。それでもいくつか課題にでくわした。
わたしが見つけた解決方法を記述し公開しようと思う。
前提条件
わたしは下記の環境で作業した。
- macOS 14.5
- openjdk version 17.0.11
- Gradle 8.5
- IntelliJ IDEA 2023.3.2
あなたがJava開発環境を持っていて、Javaプログラミングに習熟しており、Gradleによるビルドを実行できると前提する。入門的な解説はしない。
BASICインタープレターの動かし方
わたしが今回作ったコード一式を下記URLで公開している。
下記のURLからレポジトリのコード一式をまとめたzipファイルがダウンロードできる。
zipをダウンロードして ~/tmp/littlebasic
ディレクトリに解凍したと仮定する。
次のコマンドによってビルドが実行される。
$ cd ~/tmp/littlebasic
$ gradle generateGrammarSource
...
$ gradle jar
...
jarファイルができたらBASICインタープレターを実行する用意が完了する。
サンプルとしてのBASICプログラムがひとつzipの中に含まれている。
REM Greatest common divisor
INPUT "A=" ain
INPUT "B=" bin
a = VAL(ain)
b = VAL(bin)
WHILE b > 0
t = a MOD b
a = b
b = t
END
PRINT "GCD=" + a
このBASICプログラムを起動すると、変数Aに数値を指定せよと要求してくる。Aに数値を指定すると、続いて変数Bにも数値を指定せよと要求してくる。Bに数値を指定すると演算が実行され、Greatest Common Divisor すなわちAとBの最大公約数が表示される。
ターミナルのウインドウで実際にどういう操作をすれば、どういう結果が返ってくるか、いくつか実例を示そう。
:~/tmp/littlebasic
$ java -jar app/build/libs/app.jar app/src/test/fixtures/runGCD.bas
A= 128
B= 32
GCD=32
:~/tmp/littlebasic
$ java -jar app/build/libs/app.jar app/src/test/fixtures/runGCD.bas
A= 78
B= 49
GCD=1
:~/tmp/littlebasic
$ java -jar app/build/libs/app.jar app/src/test/fixtures/runGCD.bas
A= 3120
B= 45
GCD=15
BASIC、どうですか? 40年ぐらい前、NEC PC-8000シリーズのパソコンを初めて触った時のことをわたしは思い出しました。まさにこんなBASICを打ち込んだら、動いた。おおお!とか、あああ?とか、呟きながら没頭したっけなあ。
ANTLR Grammarファイル
BASIC言語の文法が下記3つの .g4
ファイルに記述されています。本家 https://github.com/mateiw/littlebasic が公開しているファイルをコピーしました。1文字も書き換えていません。
ANTLRに関してここでは説明しません。他の書籍やweb記事を参照してください。
BASICインタープレターのJavaコード
BASICインタープレターの入り口となるJavaクラスのソースは下記のようなものです。
package demo;
import org.littlebasic.Interpreter;
import org.littlebasic.Value;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import static java.nio.file.Files.newInputStream;
public class App {
public static void main(String[] args) throws IOException {
if (args.length == 0) {
throw new IllegalArgumentException(
"Usage: java -jar app/build/libs/app.jar " +
"app/src/test/fixtures/runGCD.bas");
}
Path bas = Paths.get(args[0]);
assert Files.exists(bas);
InputStream basicCode = newInputStream(bas);
// create the Interpreter instance
Interpreter interpreter =
new Interpreter(System.in, System.out, System.err);
//
Value value = interpreter.run(basicCode);
}
}
見ての通り、org.littlebasic.Interpreter
クラスの中にすべての秘密が隠されています。このコードも本家 https://github.com/mateiw/littlebasic が公開したものをそのまま拝借しました。
疑問点と解消法
今回、わたしが遭遇した疑問点とその解消法を説明しましょう。それはGradleビルドファイル app/build.gradle のなかの次の記述に集約されています。
plugins {
id 'antlr'
}
...
generateGrammarSource {
arguments += [
"-lib", "src/main/antlr/basic",
"-package", "basic",
"-visitor",
"-long-messages"]
maxHeapSize = "64m"
}
Gradleプラグイン antlr を使いました。
このプラグインを適用すると generateGrammarSource
タスクが追加されます。generateGrammarSource
タスクが間接的にANTLRを実行して、文法記述ファイル .g4
からJavaソース群を生成してくれます。
-lib
オプションを指定する必要があった
ものは試し、build.gradle
ファイルをちょっと変更してみましょう。
generateGrammarSource {
arguments += [
/* "-lib", "src/main/antlr/basic", */
"-package", "basic",
...
つまり-lib
オプションを指定しなかったらどうなるか?を試してみよう。
$ cd ~/tmp/littlebasic
$ gradle clean generateGrammarSource
> Task :app:generateGrammarSource
error(110): basic/LBExpression.g4:2:7: can't find or load grammar LBTokens
...
エラーが発生しました。basic/LBExpression.g4
ファイルの2行目で、LBTokens
を探したが見つからなかった、と。ANTLRのドキュメント ANTLR Tool Commandline Optionsの
-lib
オプションに関する説明を参照しましょう。
When looking for tokens files and imported grammars, ANTLR normally looks in the current directory. This option specifies which directory to look in instead.
ANTLRが base/LBExpression.g4
ファイルを処理しようとしたところ2行目に import LBTokens;
と書いてあった。
だからANTLRはLBTokensを探し出す必要があるのだが、LBTokensがどこのディレクトリにあるのか?もしも-lib
オプションの指定が無ければANTLRはカレントディレクトリだけを探す。見つからなければエラーになる。このエラーを回避するには -lib
オプションで LBTokens.g4
が配置されたディレクトリのパスを指定する必要があった。
-package
オプションを指定する必要があった
ものは試し、build.gradle
ファイルをちょっと変更してみましょう。
generateGrammarSource {
arguments += [
"-lib", "src/main/antlr/basic",
/* "-package", "basic", */
...
つまり-package
オプションを指定しなかったらどうなるか?を試してみよう。
$ gradle clean generateGrammarSource compileJava
> Task :app:compileJava
/Users/kazuakiurayama/github/littlebasic/app/src/main/java/org/littlebasic/LittleBasicVisitor.java:3: エラー: パッケージbasicは存在しません
import basic.LBExpressionParser;
^
/Users/kazuakiurayama/github/littlebasic/app/src/main/java/org/littlebasic/LittleBasicVisitor.java:4: エラー: パッケージbasicは存在しません
import basic.LittleBasicBaseVisitor;
^
/Users/kazuakiurayama/github/littlebasic/app/src/main/java/org/littlebasic/LittleBasicVisitor.java:5: エラー: パッケージbasicは存在しません
import basic.LittleBasicParser;
^
...
ANTLRが文法からJavaコードを生成する処理は静かに完了したが、Javaコードをコンパイルするところで「パッケージbasicは存在しません」というエラーが大量に吐き出されました。なぜか?
ANTLRが app/build/generated-src/antlr/main/basic/LBExpressionParser.java
ファイルを生成していたので、そのソースコードの冒頭を見てみました。
// Generated from basic/LBExpression.g4 by ANTLR 4.5
import org.antlr.v4.runtime.atn.*;
import org.antlr.v4.runtime.dfa.DFA;
import org.antl1.v4.runtime.*;
import org.antlr.v4.runtime.misc.*;
import org.antlr.v4.runtime.tree.*;
import java.util.List;
import java.util.Iterator;
import java.util.ArrayList;
@SuppressWarnings({"all", "warnings", "unchecked", "unused", "cast"})
public class LBExpressionParser extends Parser {
...
本来ならば冒頭にパッケージ宣言文 package basic;
があるべきだが、無い。ANTLRが生成した
全ての *.java
ファイルに同じ問題が発生してしまいました。
ANTLRコマンドのオプション -package ____
を指定することによってこの問題を解消することができました。
-visitor
オプションを指定する必要があった
ものは試し、build.gradle
ファイルをちょっと変更してみましょう。
generateGrammarSource {
arguments += [
"-lib", "src/main/antlr/basic",
"-package", "basic",
/* "-visitor", */
...
つまり-visitor
オプションを指定しなかったらどうなるか?を試してみよう。
$ gradle clean generateGrammarSource compileJava
> Task :app:compileJava FAILED
/Users/kazuakiurayama/github/littlebasic/app/src/main/java/org/littlebasic/LittleBasicVisitor.java:4: エラー: シンボルを見つけられません
import basic.LittleBasicBaseVisitor;
^
シンボル: クラス LittleBasicBaseVisitor
場所: パッケージ basic
...
ANTLRが文法からJavaコードを生成する処理は静かに完了したが、Javaコードをコンパイルするところで「basic.LittleBasicBaseVisitorクラスが見つかりません」というエラーが吐き出されました。なぜか?
ANTLRがJavaコードを生成したはずのディレクトリを覗いてみると確かに basic/LittleBasicBaseVisitor.java
ファイルがありません。
公式ドキュメント ANTLR Tool Command Line Options にこう書いてありました。
-visitor
ANTLR does not generate parse tree visitors by default. This option turns that feature on. ANTLR can generate both parse tree listeners and visitors; this option and -listener aren’t mutually exclusive.
ANTLRにbasic/LittleBasicBaseVisitor.java
ファイルを生成させたければ -visitor
オプションを明示的に指定する必要がある、のでした。
なぜわたしは問題に遭遇したのか
わたしのlittlebasicプロジェクトはGradleでビルドを記述しました。やってみるとgenerateGrammarSouce
タスクで問題に遭遇した。ANTLRコマンドに対して適切なオプションを指定してやる必要があった。
一方的、本家のlittlebasicプロジェクトはMavenでビルドしていた。ANTLR 4 Maven plugin
を使っていた。
Mavenでビルドするのと同じ結果を得るために、Gradleではほんの少し、手間を加える必要があった、ということなんだろうと思う。
おわりに
ANTLRを使ってBASIC言語の処理系をJavaで作ることができました。わたしは次にVBAすなわちMicrosoft ExcelのVisual Basic for Applicationのソースコードを解析するプログラムをJavaで作ってみようと思う。Excel VBAで仕事していて困り果てたことが多々ある。VBAパーサを作りそれを活用する解析ツールを開発すればわたしの困惑をきっと解消できるから。さてどこまでできるやら。