KEIS BLOGは株式会社ケイズ・ソフトウェアが運営しています。

KEIS BLOG

Introduction to Antlr! Pt.2


shimoda01
こんにちは。焼き肉ってなぜ牛タンから始めるんでしょうか(うちだけ?)。グーグルで調べたら答えが出そうですが調べてません。ちなみに焼き肉が好きすぎて、食べ過ぎているせいか健康診断で野菜中心にしなさいと怒られたので、少し私生活を見直そうとしている下田です。さて、今回も前回に続き Antlr で遊びたいと思います。

前回と同じように、お決まりの呪文を設定して、環境を作りましょう。

$ wget http://www.antlr.org/download/antlr-4.5.1-complete.jar
$ alias antlr4='java -cp "./*" org.antlr.v4.Tool'
$ alias grun='java -cp "./:./*" org.antlr.v4.gui.TestRig'

では、今回はもう少し高度な文法を処理していきたいと思います。身近にある複雑な文法といえば、あれです、あれ、 SQL。エンタープライズの世界では、 RDB は必須スキルとなっています(最近は抽象化されちゃっていますが)。今回は MySQL の Create table 文を処理していきたいと思います。こんな感じの構文ですね。

CREATE TABLE tk (col1 INT, col2 CHAR(5), col3 DATE);

また、MySQL を触ったことがある方なら、 Create table ってどう書くんだっけと、公式サイトを見たことがあると思います。すると、以下の様な呪文が書いてあることを思い出していただけると思います。よく見ると、 何となく Antlr の構文に似てると思いませんか?

CREATE [TEMPORARY] TABLE [IF NOT EXISTS] tbl_name
    (create_definition,...)
    [table_options]
    [partition_options]

そうなんです、実はこれとほとんど同じ構成で Antlr に落としこむことが出来ます。今回は、 上記のサンプルの Create table 文を対象として、 create_definition の実装を中心に話を進めていきたいと思います。また、 全ての文は case insensitive 前提として処理をしていきます。なので、 Create table 文を取り込んで処理する前に小文字化処理を施します。適当に決めてしまいましたが、今回の文法の名前は MyMySQL.g4 です。

まずは、前回を応用して、上位の階層のみ定義しました。おさらいですが、固定文字( CREATE, TABLE など)は、大文字で定義しています。これでコンパイルが通れば、どんどん下の階層を実装していきます。

grammar MyMySQL;
create_table:
  CREATE TABLE tbl_name LPAREN create_definition+ RPAREN SEMICOLON ;

tbl_name: ID ;
create_definition: col_name column_definition ;
col_name: ID ;
column_definition: ;

CREATE: 'create' ;
TABLE: 'table' ;
LPAREN: '(' ;
RPAREN: ')' ;
SEMICOLON: ';' ;
ID: [a-z_]+ ;
WS : [ \t\r\n]+ -> skip;

次に実装するのは、 create_definition の階層配下です。公式には以下のように記載されています。

create_definition:
    col_name column_definition
  | [CONSTRAINT [symbol]] PRIMARY KEY [index_type] (index_col_name,...)
      [index_option] ...
  | {INDEX|KEY} [index_name] [index_type] (index_col_name,...)
      [index_option] ...
  | [CONSTRAINT [symbol]] UNIQUE [INDEX|KEY]
      [index_name] [index_type] (index_col_name,...)
      [index_option] ...
  | {FULLTEXT|SPATIAL} [INDEX|KEY] [index_name] (index_col_name,...)
      [index_option] ...
  | [CONSTRAINT [symbol]] FOREIGN KEY
      [index_name] (index_col_name,...) reference_definition
  | CHECK (expr)

全部実装すると気が遠くなります・・・。通常の実務で見ることのない構文なんかもあって面白いですが、サンプル文が処理できるように基本ルートのみ実装していきましょう。また、今回は多数のデータタイプもサポートしません!その部分を省くとして、その配下も続けて記載すると以下の様な構成になります。

create_definition:
    col_name column_definition

column_definition:
    data_type [NOT NULL | NULL]
      [AUTO_INCREMENT] [UNIQUE [KEY] | [PRIMARY] KEY]
      [COLUMN_FORMAT {FIXED|DYNAMIC|DEFAULT}]
      [STORAGE {DISK|MEMORY|DEFAULT}]

data_type:
    INT[(length)] [UNSIGNED] [ZEROFILL]
  | DATE
  | CHAR[(length)] [BINARY]
      [CHARACTER SET charset_name]

かなりすっきりしました。これなら実装できそうです。そしてざっと書き写していくとこんな感じになります。固定文字が多いので行が長くなっていますが、ほとんど同じ記述になっていることがわかると思います。では、さっそくコンパイルして、文字を処理してみます。

grammar MyMySQL;
create_table: CREATE TABLE tbl_name LPAREN create_definition (COMMA create_definition)* RPAREN SEMICOLON ;

create_definition: col_name column_definition ;
column_definition:
  data_type
  (NOT NULL | NULL)?
  (AUTO_INCREMENT)?
  (UNIQUE (KEY)? | (PRIMARY)? KEY)?
  (COLUMN_FORMAT (FIXED|DYNAMIC|DEFAULT))?
  (STORAGE (DISK|MEMORY|DEFAULT))?
  ;

data_type:
    INT (length)? (UNSIGNED)? (ZEROFILL)?
  | DATE
  | CHAR (length)? (BINARY)? (CHARACTER SET charset_name)?
  ;
length: DIGIT+ ;
tbl_name: ID ;
col_name: ID ;
charset_name: ID;

CREATE: 'create' ;
TABLE: 'table' ;
LPAREN: '(' ;
RPAREN: ')' ;
SEMICOLON: ';' ;
COMMA: ',' ;
NOT: 'not' ;
NULL: 'null' ;
DEFAULT: 'default' ;
AUTO_INCREMENT: 'auto_increment' ;
COLUMN_FORMAT: 'column_format' ;
COMMENT: 'comment' ;
DISK: 'disk' ;
DYNAMIC: 'dynamic' ;
FIXED: 'fixed' ;
KEY: 'key' ;
MEMORY: 'memory' ;
PRIMARY: 'primary' ;
STORAGE: 'storage' ;
UNIQUE: 'unique' ;
INT: 'int' ;
UNSIGNED: 'unsigned' ;
ZEROFILL: 'zerofill' ;
DATE: 'date' ;
CHAR: 'char' ;
BINARY: 'binary' ;
CHARACTER: 'character' ;
SET: 'set' ;
DIGIT: [0-9] ;
ID: [a-z0-9_]+ ;
WS : [ \t\r\n]+ -> skip;

テストする文は予め小文字化して渡します。

antlr4 MyMySQL.g4 && javac -cp antlr-4.5.1-complete.jar *.java
echo 'create table tk (col1 int, col2 char(5), col3 date);' | grun MyMySQL create_table -gui

shimoda02

うまく parse 出来たところで、今度は実際にこの Parser を Java から使ってみたいと思います。簡単な流れを説明すると、Lexer で、文字列を Token 化しそれを今回定義したルールに従って Parser で Parse すると、 AST(抽象構文木) 化された Context が取得できます。 AST と言っても、難しいことはなく先ほど GUI で表示したツリーのモデルが取得出来るだけです。

package a;

import java.util.stream.Collectors;
import java.util.stream.IntStream;

import org.antlr.v4.runtime.ANTLRInputStream;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.ParserRuleContext;
import org.antlr.v4.runtime.tree.ParseTree;

import a.antlr.MyMySQLLexer;
import a.antlr.MyMySQLParser;

public class Main {
    public static void main(String[] args) {
        String sql = "CREATE TABLE tk (col1 INT, col2 CHAR(5), col3 DATE);";

        MyMySQLLexer lexer = new MyMySQLLexer(new ANTLRInputStream(
                sql.toLowerCase()));
        CommonTokenStream tokenStream = new CommonTokenStream(lexer);
        MyMySQLParser parser = new MyMySQLParser(tokenStream);
        ParserRuleContext context = parser.create_table();
        for (ParseTree child : context.children) {
            print(child, 1);
        }
    }

    private static void print(ParseTree child, int depth) {
        String string = IntStream.range(0, depth * 2).mapToObj(d -> "-")
                .collect(Collectors.joining());
        System.out.println(string + ">" + child.getPayload().getClass().getSimpleName());
        for (int i = 0; i < child.getChildCount(); i++) {
            ParseTree child2 = child.getChild(i);
            print(child2, depth + 1);
        }
    }
}

これを実行すると以下の様な結果が階層構造で得られます。次回はこの構造をこねくり回して行きたいと思います。

-->CommonToken
-->CommonToken
-->Tbl_nameContext
---->CommonToken
-->CommonToken
-->Create_definitionContext
---->Col_nameContext
------>CommonToken
---->Column_definitionContext
------>Data_typeContext
-------->CommonToken
-->CommonToken
-->Create_definitionContext
---->Col_nameContext
------>CommonToken
---->Column_definitionContext
------>Data_typeContext
-------->CommonToken
-------->CommonToken
-------->LengthContext
---------->CommonToken
-------->CommonToken
-->CommonToken
-->Create_definitionContext
---->Col_nameContext
------>CommonToken
---->Column_definitionContext
------>Data_typeContext
-------->CommonToken
-->CommonToken
-->CommonToken

【関連記事】
Splunkに株価を取り込んでみた ft. Fujikawa
Introduction to Antlr!