0
0

BASIC言語のインタープレタをJavaで作る話

Last updated at Posted at 2024-07-07

この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 ファイルがありません。

without_visitor

公式ドキュメント 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パーサを作りそれを活用する解析ツールを開発すればわたしの困惑をきっと解消できるから。さてどこまでできるやら。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0