ANTLR4 を JavaScript から使ってみる

ANTLR4 で生成したパーサーを Java から使った経験があったので、同じように JavaScript から使おうとしたのですが、ハマってしまいました。何とか動かせたので手順を紹介します。

ANTLR4 のバージョンは 4.9.2 です。ドキュメントが不親切だと感じたのですが、[ごく最近](https://github.com/antlr/antlr4/commit/11ede0fc7726df8bc9eb17eca47e0be020c83933?branch=11ede0fc7726df8bc9eb17eca47e0be020c83933&diff=split)になって、生成されるコードが ESM 形式のモジュールに変更されていました。一方でランタイムは CommonJS 形式のままなので、現状はとても中途半端な状態です。将来、完全に ESM 形式に移行するなどの修正が行われれば、この文書の記述は役に立たなくなってしまう可能性があります。


# 前提

以下のインストールは済ませておいてください。

* npm
* Java
* Visual Studio Code
    * Install 'code' command in PATH も。
* Chrome


# この手順で達成すること

https://github.com/antlr/antlr4/blob/master/doc/javascript-target.md の手順に従って、以下の処理をする JavaScript プログラムを作成します。

1. [ANTLR4 の文法](https://github.com/antlr/grammars-v4/tree/master/antlr/antlr4)をパース
1. ANTLR でリスナーを使って左辺の名前をリストアップ
1. ブラウザーに表示


# JavaScript 用 lexer/parser の生成

一時保管用ディレクトリを作成し、ANTLR4 のパースで使う LexerAdaptor と文法ファイルをダウンロードし、ANTLR Tool (jar) で lexer/parser を生成します。

```shell
$ mkdir depot
$ cd depot
$ curl -sSLO https://github.com/antlr/grammars-v4/raw/master/antlr/antlr4/JavaScript/LexerAdaptor.js
$ curl -sSLO https://github.com/antlr/grammars-v4/raw/master/antlr/antlr4/ANTLRv4Lexer.g4
$ curl -sSLO https://github.com/antlr/grammars-v4/raw/master/antlr/antlr4/ANTLRv4Parser.g4
$ curl -sSLO https://github.com/antlr/grammars-v4/raw/master/antlr/antlr4/LexBasic.g4
$ curl -sSLO https://www.antlr.org/download/antlr-4.9.2-complete.jar
$ java -jar antlr-4.9.2-complete.jar -Dlanguage=JavaScript ANTLRv4Lexer.g4 ANTLRv4Parser.g4
$ ls
ANTLRv4Lexer.g4          ANTLRv4Parser.g4         ANTLRv4ParserListener.js
ANTLRv4Lexer.interp      ANTLRv4Parser.interp     LexBasic.g4
ANTLRv4Lexer.js          ANTLRv4Parser.js         LexerAdaptor.js
ANTLRv4Lexer.tokens      ANTLRv4Parser.tokens     antlr-4.9.2-complete.jar
```


# Webpack 用の構成を作る

webpack 実行用のディレクトリを作成します。最後に VS Code でこのディレクトリを開きます。

Webpack には詳しくないため、[Getting Started](https://webpack.js.org/guides/getting-started/) ガイドを参考にしました。

```shell
$ cd ..
$ mkdir webpack-antlr
$ cd webpack-antlr
$ npm init -y
$ npm install webpack webpack-cli --save-dev
$ code .
```


# package.json の書き換え

前述のガイドに従い、うっかり公開を防止するために package.json を書き換えます。さらに、念のため生成されたパーサーで使っている codepointat.js と fromcodepoint.js に合わせて、ライセンスを MIT にします。

```diff:package.json
 {
   "name": "webpack-antlr",
   "version": "1.0.0",
   "description": "",
-  "main": "index.js",
+  "private": true,
   "scripts": {
     "test": "echo \"Error: no test specified\" && exit 1"
   },
   "keywords": [],
   "author": "",
-  "license": "ISC",
+  "license": "MIT",
   "devDependencies": {
     "webpack": "^5.51.1",
     "webpack-cli": "^4.8.0"
   }
 }
```


# webpack.config.js の作成

webpack-antlr ディレクトリの直下に webpack.config.js を作成します。内容は以下のようにします。

```webpack.config.js
const path = require('path');

module.exports = {
  mode: 'development',
  entry: './src/index.js',
  devtool: 'eval-source-map',
  output: {
    filename: 'main.js',
    path: path.resolve(__dirname, 'dist'),
  },
  resolve: { fallback: { fs: false } },
};
```

ガイドに記載のデフォルトから変更した箇所がいくつかあります。

`resolve: { fallback: { fs: false } }` を指定しています。これは [javascript-target.md](https://github.com/antlr/antlr4/blob/master/doc/javascript-target.md) に記載されている修正です。これをしないと、Node.js のライブラリを読もうとしてエラーになってしまいます。

`mode: 'development'` を指定しています。Minify とかしてほしくなかったので。

`devtool: 'eval-source-map'` を指定しています。Webpack で一つにまとめられたファイルをデバッグ実行する際に、元のファイルとの位置の対応が必要なためです。[Devtool](https://webpack.js.org/configuration/devtool/) で開発用に推奨されているものの中から選びました。


# index.html の作成

webpack-antlr ディレクトリの直下に dist ディレクトリを作成し、index.html を作成します。内容は以下のようにします。

```index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>ANTLR-generated ANTLR parser running on browser JavaScript</title>
  </head>
  <body>
    <script src="main.js"></script>
    <textarea id="grammar-area" rows="10" cols="80">
// define a grammar called Hello
grammar Hello;
r   : 'hello' ID;
ID  : [a-z]+ ;
WS  : [ \t\r\n]+ -> skip ;
    </textarea>
    <div>
      <button type="button" onclick="listUp()">List up rules</button>
    </div>
    <pre id="output-area"></pre>
  </body>
</html>
```

script タグは一つだけで、この後 webpack で作成するスクリプトを指定しています。

textarea には [Hello.g4](https://github.com/antlr/grammars-v4/blob/master/antlr/antlr4/examples/Hello.g4) の中身が入っています。

button がクリックされると、この後作成する `listUp` 関数を呼び出します。この関数は、`grammar-area` の中身をパースして、左辺をリストアップして、`output-area` に書き出します。


# リストアップ処理を書いた index.js の作成

まず、webpack-antlr ディレクトリで以下を実行し、antlr4 runtime のインストールと、生成した lexer/parser の配置を行います。

```shell
$ npm install antlr4
$ mkdir src
$ cp ../depot/*.js src/
```

次に、src ディレクトリに index.js を作成します。中身は [javascript-target.md](https://github.com/antlr/antlr4/blob/master/doc/javascript-target.md) を参考に、以下のようにします。

```index.js
import antlr4 from 'antlr4';
import ANTLRv4Lexer from './ANTLRv4Lexer.js';
import ANTLRv4Parser from './ANTLRv4Parser.js';
import ANTLRv4ParserListener from './ANTLRv4ParserListener.js';

function listUp() {
  const input = document.querySelector('#grammar-area').value;
  const chars = new antlr4.InputStream(input);
  const lexer = new ANTLRv4Lexer(chars);
  const tokens = new antlr4.CommonTokenStream(lexer);
  const parser = new ANTLRv4Parser(tokens);
  parser.buildParseTrees = true;
  const tree = parser.grammarSpec();

  const lhsList = [];
  class MyListener extends ANTLRv4ParserListener {

    enterParserRuleSpec(ctx) {
      const ruleRefText = ctx.RULE_REF().getText();
      lhsList.push(ruleRefText);
    }

    enterLexerRuleSpec(ctx) {
      const tokenRefText = ctx.TOKEN_REF().getText();
      lhsList.push(tokenRefText);
    }
  }
  antlr4.tree.ParseTreeWalker.DEFAULT.walk(new MyListener(), tree);
  document.querySelector('#output-area').innerText = lhsList.join('\r\n');
}

// make accessible from html
window.listUp = listUp;
```

VS Code の IntelliSense の効きは今ひとつです。執筆時点では、antlr4[]()@4.9.2 を使っているにも関わらず VS Code で参照される型定義は 4.7 のもので、JSDoc は参照できません。これはランタイムだけでなく生成されたコードでも似たような状況で、例えば `ctx` に `RULE_REF()` があるのかなどは Java だと IDE の補完ですぐにわかるのですが、JavaScript ではソースコードを追う必要がありました。

上で `new antlr4.InputStream(input);` とした箇所は `antlr4.CharStreams.fromString(input)` でも良いかもしれません。[CharStreams.js](https://github.com/antlr/antlr4/blob/master/runtime/JavaScript/src/antlr4/CharStreams.js) には、CharStreams の返す InputStream は Unicode の全範囲 (U+10FFFF まで) をサポートするのに対し、デフォルトの InputStream は U+FFFF までしかサポートしないとの記述があります。

最後の行は、HTML 側から作成した関数が見えるようにするためのものです。以下のような方法もあります。

* `listUp` 関数を `export` し、webpack.config.js の [libraryTarget: 'window'](https://webpack.js.org/configuration/output/#librarytarget-window) を使う。
* `listUp` 関数を `export` し、webpack で application ではなく library を作成することで、`onclick="MyLibrary.listUp()"` のように参照する。
* HTML には関数を書かず、`addEventListener` で click イベントに対応させる。この場合、script タグを button タグより後ろにする。

デバッグ実行できることの確認のために、`document.querySelector('#output-area').innerText = lhsList.join('\r\n');` の行で F9 を押してブレークポイントを設定します。


# webpack を実行して dist ディレクトリに必要な資材を揃える

webpack-antlr ディレクトリで以下を実行します。

```shell
$ npx webpack --config webpack.config.js
asset main.js 1.79 MiB [emitted] (name: main)
runtime modules 670 bytes 3 modules
modules by path ./node_modules/antlr4/src/antlr4/ 398 KiB
  modules by path ./node_modules/antlr4/src/antlr4/*.js 115 KiB 17 modules
  modules by path ./node_modules/antlr4/src/antlr4/atn/*.js 213 KiB 16 modules
  modules by path ./node_modules/antlr4/src/antlr4/error/*.js 43.7 KiB 5 modules
  modules by path ./node_modules/antlr4/src/antlr4/dfa/*.js 12.6 KiB 4 modules
  modules by path ./node_modules/antlr4/src/antlr4/tree/*.js 9.08 KiB 3 modules
  modules by path ./node_modules/antlr4/src/antlr4/polyfills/*.js 3.53 KiB 2 modules
modules by path ./src/*.js 243 KiB
  ./src/index.js 1.03 KiB [built] [code generated]
  ./src/ANTLRv4Lexer.js 43.1 KiB [built] [code generated]
  ./src/ANTLRv4Parser.js 184 KiB [built] [code generated]
  ./src/ANTLRv4ParserListener.js 11.6 KiB [built] [code generated]
  ./src/LexerAdaptor.js 3.84 KiB [built] [code generated]
fs (ignored) 15 bytes [built] [code generated]
webpack 5.51.1 compiled successfully in 648 ms
```

# VS Code でデバッグする

VS Code で index.html を開き、Activity Bar で Run view を開いて `create a launch.json file` のリンクをクリックし、Chrome を選択します。`.vscode/launch.json` にいい感じの設定ができると思います。

```jsonc:launch.json
{
    // Use IntelliSense to learn about possible attributes.
    // Hover to view descriptions of existing attributes.
    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "type": "pwa-chrome",
            "request": "launch",
            "name": "Open index.html",
            "file": "/path/to/webpack-antlr/dist/index.html"
        }
    ]
}
```

F5 を押してデバッグを開始すると Chrome が自動的に開きます。開いたら Chrome で F12 を押して DevTools を表示してください。

List up rules ボタンを押すと、VS Code にフォーカスが移り、設定したブレークポイントで停止しています。DevTools ではソースコードが読めないので、`devtool: 'eval-source-map'` の設定により非常にデバッグしやすくなっていることがわかります。`lhsList` にホバーすると今格納されている値が見えると思います。▶️ ボタンを押して実行を再開すると、ブラウザーにその内容が書き出されます。

# まとめ

[javascript-target.md](https://github.com/antlr/antlr4/blob/master/doc/javascript-target.md) の手順で詳細が省略されている部分を具体的に説明しました。Webpack を使えば、CommonJS 形式と ESM 形式のモジュールが混在したコードをブラウザーで実行できることがわかりました。