Java/EclipseでDSLサポート (5) - パースエラーの表示

サクサク進む。

直前の「Java/EclipseでDSLサポート (4) - スキャナエラーの表示 - しげるメモ」から今度は構文レベルのエラーを表示してみます。

完成品。

抽象構文木の定義

構文木ってのはパースした結果をそのままツリー上にしたもので、すべてのトークンが必ずツリー上に出現しますが、抽象構文木予約語などいらないものがあったらどんどん削っていく感じです。

if (a == true) hoge();

だったら、抽象構文木だと "if", "(", ")" 辺りはツリー上に表現した時点でなくてもわかるようになるので消えてたりします。

ちなみに、簡単なフィルタ - しげるメモの言語は別に構文木でも抽象構文木でも同じです。ともあれ、こいつの抽象構文木の構造をJavaで表現してみます。

package com.example.ashigeru.dsl;

public class ListFilterDsl {
    
    /** 演算子の左側 (PROPERTY, NUMBER) */
    public final ListFilterToken left;
    /** 演算子(OPERATOR) */
    public final ListFilterToken operator;
    /** 演算子の右側 (PROPERTY, NUMBER) */
    public final ListFilterToken right;

    public ListFilterDsl(ListFilterToken left,
            ListFilterToken operator, ListFilterToken right) {
        super();
        this.left = left;
        this.operator = operator;
        this.right = right;
    }
}

実際はもっと複雑な木になるのがほとんどですが、今回は1ノード1トークンと簡単な言語を選んだので、これくらいで終わりです。

パーサの作成

前回作ったスキャナの結果を利用して、構文解析を行うパーサを作ります。

先に文法を確認しておくと、こんな感じでした。

    <PROGRAM> ::= <TERM> <OP> <TERM>

    <TERM> ::= <PROPERTY>
            |  <INTEGER>

      <OP> ::= "=="
            |  "!="
            |  ">"

<PROPERTY> ::= ["a"-"z", "A"-"Z"]["a"-"z", "A"-"Z", "0"-"9"]*

 <INTEGER> ::= "0"
            |  ["+", "-"]? ["1"-"9"]["0"-"9"]*
簡単なフィルタ - しげるメモ

スキャナの時点で OP, PROPERTY, INTEGER の部分(いわゆるLexer Ruleな部分)の解析は終わっているので、PROGRAM(DSL全体)、TERM(演算数)、OP(演算子)の部分をやります。

package com.example.ashigeru.dsl;

import java.util.Iterator;
import java.util.List;

import com.example.ashigeru.dsl.ListFilterToken.Kind;

public class ListFilterParser {
    
    public static ListFilterDsl parse(
            List<? extends ListFilterToken> tokens)
            throws ListFilterParserException {
        
        int offset = 0;
        Iterator<? extends ListFilterToken> iter = tokens.iterator();
        
        // 最初は PROPERTY か NUMBER
        checkNotEof(offset, iter);
        ListFilterToken left = iter.next();
        checkIsPropOrNum(left);
        offset = left.offset + left.image.length();
        
        // 次は OPERATOR
        checkNotEof(offset, iter);
        ListFilterToken operator = iter.next();
        checkIsOperator(operator);
        offset = operator.offset + operator.image.length();
        
        // 最後は PROPERTY か NUMBER
        checkNotEof(offset, iter);
        ListFilterToken right = iter.next();
        checkIsPropOrNum(right);
        offset = right.offset + right.image.length();
        
        // これ以降はいらない
        checkEof(offset, iter);
        
        // 抽象構文木にして返す
        return new ListFilterDsl(left, operator, right);
    }

    // トークンが足らないとエラー
    private static void checkNotEof(int offset,
            Iterator<? extends ListFilterToken> iter)
            throws ListFilterParserException {
        if (!iter.hasNext()) {
            throw new ListFilterParserException(
                "なんか足らない", offset, -1);
        }
    }

    // PROPERTYかNUMBERじゃないとエラー
    private static void checkIsPropOrNum(ListFilterToken token)
            throws ListFilterParserException {
        if (token.kind != Kind.PROPERTY && token.kind != Kind.NUMBER) {
            throw new ListFilterParserException(
                "プロパティ名か数値でないとだめ",
                token.offset, token.image.length());
        }
    }

    // OPERATORじゃないとエラー
    private static void checkIsOperator(ListFilterToken token)
            throws ListFilterParserException {
        if (token.kind != Kind.OPERATOR) {
            throw new ListFilterParserException(
                "演算子じゃないとダメ",
                token.offset, token.image.length());
        }
    }

    // トークンが多すぎてもエラー
    private static void checkEof(int offset,
            Iterator<? extends ListFilterToken> iter)
            throws ListFilterParserException {
        if (iter.hasNext()) {
            throw new ListFilterParserException(
                "多すぎ", offset, -1);
        }
    }
}

ここでのポイントは、次の4つのヘルパーメソッドです。

  • checkNotEof - トークンが足りなくなったらエラー
  • checkIsPropOrNum - 演算数がほしいところで、PROPERTYかNUMBERじゃなければエラー
  • checkIsOperator - 演算子がほしいところで、OPERATORじゃなければエラー
  • checkEof - トークンが余計にあったらエラー

これらのエラーは、いずれもListFilterParserExceptionというクラスで定義してます。

package com.example.ashigeru.dsl;

public class ListFilterParserException extends Exception {
    
    /** エラーメッセージ */
    public final String message;
    
    /** 開始位置 */
    public final int offset;
    
    /** 終了位置 (-1なら最後まで) */
    public final int length;

    public ListFilterParserException(String message, int offset,
            int length) {
        super();
        this.message = message;
        this.offset = offset;
        this.length = length;
    }
}

とりあえず、ListFilterScannerとListFilterParserを組み合わせると、ListFilterDslというDSLの構文を表現するオブジェクトを作るところまでできました。

ちなみに今回は手書きでやりましたが、普通はANTLRとか使ってください

IDEに組み込む

Java/EclipseでDSLサポート (4) - スキャナエラーの表示 - しげるメモをまた変更します。今回はcreateProblemsの字句解析をやる辺りだけの変更で十分です。

// 引数の検査
StringLiteral string = (StringLiteral) arg;
String literal = string.getLiteralValue();
try {
    List<ListFilterToken> tokens =
        ListFilterScanner.scan(literal);
    ListFilterParser.parse(tokens);
}
// スキャナーの失敗
catch (ListFilterScannerException e) {
    problem(
        IStatus.ERROR,
        MessageFormat.format(
            "不明なトークン\"{0}\"", e.image),
        string,
        e.offset + 1, // 文字列リテラル開始記号 " で + 1
        e.length);
    return true;
}
// パーサの失敗
catch (ListFilterParserException e) {
    int length;
    // length が -1 なら最後まで
    if (e.length == -1) {
        length = literal.length() - e.offset + 1;
    }
    else {
        length = e.length;
    }
    problem(IStatus.ERROR, e.message, string,
        e.offset + 1,
        length);
    return true;
}

前回と似た感じの流れで、パーサを走らせてエラーならIDEに表示、といった感じです。

実際に使ってみると↓。

次回は、意味解析までやってエラーを表示します。