ANTLR入門(TypeScript版)|環境構築から構文木出力までの最短ステップ

はじめに

簡単なDSLやプログラミング言語のフォーマッターを作るためのツールはANTLRやBison、Tree-sitterなどありますが、本記事では様々な言語向けにコード生成ができるANTLRを採用し、TypeScriptで動作するよう環境構築する手順を紹介します。

この記事のゴール

  • Lexer / Parserが正常に動作し、簡単なコードを構文木に変換できるまで

前提条件:

  • ANTLR本体はJavaで実装されているため、PCにjava(JDK)がインストールされている必要があります。
  • 本記事ではMacを想定しています。

1. プロジェクトの準備

まずは以下のコマンドをローカルPCのターミナルで実行し、ANTLRを動かすためのTypeScript環境を作成します。

mkdir antlr-typescript-project #プロジェクト用フォルダ作成
cd antlr-typescript-project #作成したフォルダに移動
npm init --yes #Nodeプロジェクトを初期化
npm i -D typescript ts-node #TypeScript環境を追加

npx tsc --init #TypeScript環境を初期化

作成したフォルダ内のpackage.jsonファイル内のscriptsに、以下の通りstart設定を追加します。

"scripts": {
   "test": "echo \"Error: no test specified\" && exit 1",
   "start": "ts-node index.ts"
 },

※ ts-node index.tsは「index.tsに書いた処理を実行する」という意味のコマンドで、これからindex.tsにパース処理を書いていきます。

以下のコマンドをローカルPCで実行します。

echo 'console.log("Hi")' > index.ts #index.tsファイルを作成し動作確認用の処理を記載
npm run start #package.jsonのscript>startに定義したコマンドを実行するコマンド

このコマンドにより、ターミナルに「Hi」が出力されたらindex.tsファイルが正常に実行できています。

VSCodeに拡張機能「ANTLR4 grammar syntax support」をインストールします。

2. 文法を作成する

TypeScript環境の作成ができたので、実際に文法を定義し、想定通りに動作するかを確認します。

以下のコマンドで、コードを読み取る際の文法を定義する文法ファイルを作成します。

touch ECMAScript.g4

作成されたECMAScript.g4ファイルに以下の動作確認用のコードを貼り付けます。
ここでは「var a = 1;」のような変数宣言の文法を定義しています。

grammar ECMAScript;

program : statement+ ;
statement : 'var' ID '=' NUMBER ';' ;

ID : [a-zA-Z]+ ;
NUMBER : [0-9]+ ;
WS : [ \t\r\n]+ -> skip ;

vscodeのデバッグタブを開き、左メニューのcreate a launch json fileをクリック。

vscode-create-launch-json.png

表示されるメニューからnode.jsを選択。

select-nodejs-debugger.png

.vscode/launch.jsonファイルが作成されるので以下のコードに更新し保存します。

{
    "version": "2.0.0",
    "configurations": [
        {
            "name": "Debug ANTLR4 grammar",
            "type": "antlr-debug",
            "request": "launch",
            "input": "input.txt",
            "grammar": "ECMAScript.g4",
            "printParseTree": true,
            "visualParseTree": true
        }
    ]
}

vscodeの設定タブで「Antlr4:Generation」の「edit in settings.json」をクリック。

edit-settings-json-link.pngsettings.jsonファイルが開くので、ファイル内のantlr4.generation > modeを"external"に変更し保存します。

パース対象のコードを保存するファイルinput.txtを

touch input.txt

このコマンドで作成し、作成された/input.txtファイルに

var a = 1;

と記載します。

ECMAScript.g4ファイルを開いて保存(Cmd + S)をすると以下の2つのファイルが自動生成されます。

  • .antlr/ECMAScriptLexer.interp
  • .antlr/ECMAScriptLexer.tokens

※ .interp / .tokensはVSCode拡張機能が文法をデバッグするために生成する中間ファイルです。

.vscode/launch.jsonファイルを開いた状態でデバッグタブの左メニューのドロップダウンで「Debug ANTLR4 grammar」を選択し、再生ボタンをクリックすると「parse tree: ECMAScript.g4」タブが表示されます。

debug-antlr4-parse-tree.pngこれでECMAScript.g4上で定義した文法が想定通りかを確認できるようになりました。

parse-tree-diagram-detail.png

3. 作成した文法でコードを解析する

次に先ほど作成した文法を使って実際にコードをパース(読み取り)します。

https://www.antlr.org/download.html
こちらのページにアクセスし、「Complete ANTLR 4.13.2 Java binaries jar」
をダウンロードし、ローカルPCの任意の場所に置きます。

先ほどと同じくvscodeの設定を開き、「Antlr4: Generation」項目のedit in settings.jsonをクリックしてsettings.jsonファイルを開きます。
settings.jsonファイルのantlr4.generation > alternativeJarに先ほど配置したファイルの絶対パスを記載します。以下は一例です。

...
    "antlr4.generation": 
      "alternativeJar": "/Applications/antlr/antlr-4.13.2-complete.jar",
      ...
    }
...

以下のコマンドにより、文法ファイル「ECMAScript.g4」 から TypeScript の Lexer / Parser クラスを自動生成します。

npm install -D antlr4ts-cli #TypeScript用のLexer/Parserクラスを生成するCLIツールのインストール
npx antlr4ts -visitor -listener -o generated ECMAScript.g4 #Lexer,Parserを作成
npm install antlr4ts #Parserが利用するランタイムをインストール

※ Lexerは文字のかたまりを意味のある単位(トークン)に分解する処理をします。
例えば「var a = 1;」という文字列を
[var] [a] [=] [1] [;]
このように単語に分解して解釈します。
ParserはLexerから渡された単語の並びが文法的に正しいかをチェックし、文の構造を作る役割を果たします。

上記コマンドにより

  • generated/ECMAScriptLexer.ts
  • generated/ECMAScriptListener.ts
  • generated/ECMAScriptParser.ts
  • generated/ECMAScriptVisitor.ts

が作成されました。

antlr4tsの仕様との整合性を取るため、プロジェクトルート直下のtsconfig.jsonファイルから

"verbatimModuleSyntax": true,

の行を削除します。
※ antlr4tsが生成するコードはCommonJSスタイルですが、上記の設定があるとESM形式が強制されてしまい、antlr4ts生成コードと衝突してしまいます。そのため上記の通りverbatimModuleSyntax設定を削除します。

次にinput.txtのコードを構文木に変換する(パースする)処理を作成します。
具体的な処理の流れは以下の通りです。

文字列
  ↓
InputStream
  ↓
Lexer(トークン化)
  ↓
TokenStream
  ↓
Parser(構文解析)
  ↓
ParseTree(構文木)

まずはinput.txtファイルを読み込むためにNode.jsの型定義をインストールします。

npm install -D @types/node

また、TypeScriptにインストールしたNode.jsの型定義を読み込ませるため、tsconfig.jsonファイルでtypesに"node"を指定します。

  "compilerOptions": {
    "types": ["node"],

パース処理を実装するため、index.tsファイルの中身を以下のコードに置き換えます。

import { ECMAScriptLexer } from "./generated/ECMAScriptLexer";
import { ECMAScriptParser } from "./generated/ECMAScriptParser";
import { ANTLRInputStream, CommonTokenStream } from "antlr4ts";
import * as fs from "fs";

// input.txt を読み込む
const input = fs.readFileSync("input.txt", "utf-8");

// 読み込んだ文字列をANTLRに渡す
const inputStream = new ANTLRInputStream(input);
const lexer = new ECMAScriptLexer(inputStream);
const tokenStream = new CommonTokenStream(lexer);
const parser = new ECMAScriptParser(tokenStream);
console.log("Parser↓==========================================")
console.log(parser)
console.log("Parser↑==========================================")

// パース実行
const tree = parser.program();
console.log("構文木↓==========================================");
console.log(tree.toStringTree(parser));
console.log("構文木↑==========================================")

ファイル修正後、

npm run start

を実行すると構文木が出力されます。パーサーのログに「_syntaxErrors: 0,」と書かれていたらパース成功です。

4. まとめ

この記事ではANTLRの環境構築手順をご紹介しました。
ここまでで

  • 文法を定義
  • VSCodeで文法をデバッグ
  • TypeScriptからパースを実行

ができる状態になりました。
現状では「var a = 1;」のような簡単なコードしかパースできませんが、文法ファイルにルールを追加していくことでより複雑な処理を実現することができます。

株式会社SPで一緒に働いてみませんか?

SPはエンジニアの成長を大切にする会社です。

ご興味ある方は一度気軽な雰囲気で、カジュアル面談はいかがでしょうか?

どのような課題を
解決したいですか?

株式会社SPでは、お客様の取り組みに寄り添いながら、
課題解決を伴走支援していきます。

まずはお気軽にこちらからお問い合わせください。

お問い合わせ・相談する(無料)