関数型のテンプレートエンジンを作ってみる(0.2.2) - パーサを書く

前回の続きで、前回作ったトークナイザからパーサを書く。パーサの作成には引き続きJParsecのライブラリを使用する。

おさらい&今日のミッション

前回作ったnewTokenizer(Terminals)で生成するトークナイザ(字句解析部)は、Stringを受け取りListを生成するParserという形をしている。
今回作るパーサ(構文解析部)はListから何らかの最終生成物(仮にその型をXとする)を生成するParserという形になる。その両者を合わせて文字列からXを生成できるようにする。

    Terminals tags; // タグ用のTerminalsが入っているとする。
    /*一般的な形*/
    public Parser<X> newParser() {
        Parser<List<Token>> tokenizer = Tokenizers.newTokenizer(tags);

        Parser<X> parser = ...//今回ここの処理を書く

        return parser.from(tokenizer); // 合わせて最終的なパーサ
    }

テンプレートエンジンのパーサとしては、テンプレート(仮にSimpleTemplate型)を生成するParserにすれば良いような気もするが、今回の目標の一つとしてテンプレートエンジンに「静的スコープ」を持たせるというものがあり、その為にはSimpleTemplateを直接生成するのではなく、「宣言部の実行時にContextを受け取ってSimpleTemplateを生成するもの」を生成する必要がある。(「クロージャ」と「ラムダ式」のような関係。) この「式」にあたるものとして、SimpleTemplatePrototypeという型を用意する。

public interface SimpleTemplatePrototype {
    public SimpleTemplate instantiate(Context<Object, SimpleTemplate> lexicalContext);
}
    Terminals tags; // タグ用のTerminalsが入っているとする。
    /* StringからSimpleTemplatePrototypeを生成するパーサを生成する */
    public Parser<SimpleTemplatePrototype> newParser() {
        Parser<List<Token>> tokenizer = Tokenizers.newTokenizer(tags);

        Parser<SimpleTemplatePrototype> parser = ...//今回ここの処理を書く

        return parser.from(tokenizer); // 合わせて最終的なパーサ
    }

以下、パーサを組み立てるのに必要な要素を実装して行く。要素1〜要素5はTemplateParsersというクラスに書く。

要素1 リテラル文字列をパースする部分

前回のliteralTokenizerが吐き出すトークンを受け取る部分になる。

    public static final String LITERAL_NAME = "TEXT";
    public static Parser<SimpleTemplatePrototype> makeLiteralParser() {
        return Terminals.fragment(LITERAL_NAME).map(
            new Map<String, SimpleTemplatePrototype>() {
                public SimpleTemplatePrototype map(String source) {
                     return new SimpleLiteralPrototype(source);
                }
            }
        );
    }

literalTokenizerはLITERAL_NAMEでタグ付けされたFragmentを戻すようにしているので、その内容を受け取るにはTerminals.fragment(LITERAL_NAME)でParserを作り文字列を受け取る。受け取った文字列を元にSimpleLiteralPrototypeを生成している。


SimpleLiteralPrototypeはSimpleTemplatePrototypeの実装クラスで、instantiate()するとSimpleLiteralというTemplateクラスを生成する(中身は後で実装する。) SimpleLiteralは前々回作ったものと同じで良いだろう。

要素2 タグの属性部分をパースする部分

次にタグをパースするParserを作りたいが、その前にタグ内の属性部分をパースするParserを書く。
まず、「name=label」のパターン。両方がTerminals.Identifierなので、Terminals.Identifier.Parserを使う。

    public static final Terminals operators = Terminals.operators("/", "=");
...
    private static Parser<Attribute> makeAttributeParser() {

        Parser<Attribute> withSymbol = 
            Parsers.sequence(
                Terminals.Identifier.PARSER,
                operators.token("="), 
                Terminals.Identifier.PARSER,

                new Map3<String, Token, String, Attribute>() {
                    public Attribute map(String name, Token op, String label) {
                        return new Attribute(name, new SymbolValue(label));
                    }
                });

"=" 」を受け取って、Attributeオブジェクトを返している。このAttributeオブジェクトはパース中にのみ使用する一時的なクラスで、属性名と属性値の定義を保持する。
なお、operatorsはtokenizerを作るのに使ったものと同一である必要がある。


次は「name="expression"」のパターン。ダブルクォートの部分にはトークナイザ側でTerminals.StringLiteral.DOUBLE_QUOTE_TOKENIZERを使ったので、パースするときはTerminals.StringLiteral.PARSERで受ける。

        Parser<Attribute> withExpression = 
            Parsers.sequence(
                Terminals.Identifier.PARSER,
                operators.token("="),
                Terminals.StringLiteral.PARSER,

                new Map3<String, Token, String, Attribute>() {
                    public Attribute map(String name, Token op, String expression) {
                        return new Attribute(name, new Expression(expression));
                    }
                });

二つまとめて属性部分のパーサを組み立てる。

        return Parsers.or(withExpression, withSymbol);
    }

要素3 シングルタグをパースする部分(defineの場合の例)

シングルタグは「{define name=hoge value="fuga"/}」の用に「{」+タグ名で始まり「/」+「}」で終わっている。
defineタグなら以下のようになる。

    public static final Terminals startBrace = Terminals.operators("{");
    public static final Terminals endBrace = Terminals.operators("}");
    public static final Terminals operators = Terminals.operators("/", "=");
    public static final Terminals tags; // "insert", "define"を含むTerminalsが入っているとする

...
        Parser<SimpleTemplatePrototype> defineSingleTagParser =
	    // 「{」で始まり
            startBrace.token("{").next(
                Parsers.sequence(
		    // defineの後、属性が0個以上
                    tags.token("define"), makeAttributeParser().many(),

                    new Map2<Token, List<Attribute>, SimpleTemplatePrototype>() {
			// SimpleTemplatePrototypeを生成する
                        public SimpleTemplatePrototype map(Token tagToken, List<Attribute> attributes) {
                            String tagName = tagToken.toString(); // insertのはず
                            // name属性を探す
                            Attribute nameAttribute = Attribute.findAttribute("name", attributes);
                            // value属性を探す
                            Attribute valueAttribute = Attribute.findAttribute("value", attributes);
                            // value属性の値がSymbolかExpressionかで
                            final SimpleSource valueSource;
                            if (valueAttribute.getValue().isSymbol()) {
                                Symbol symbol = valueAttribute.getValue().getSymbol();
                                valueSource = new SimpleReference(symbol);
                            } else {
                                final String expression = valueAttribute.getValue().getExpression();
                                valueSource = new SimpleExpression(expression);
                            }
                            // SimpleDefineProcessorPrototypeを生成して戻す。
                            return new SimpleDefineProcessorPrototype(
                                nameAttribute.getValue().getSymbol(), valueSource);
                        }
                    }
                )
	    // 「/」+「}」で終わる
            ).followedBy(operators.token("/").next(endBrace.token("}")));

SimpleDefineProcessorPrototypeはSimpleTemplatePrototypeの実装クラスで、instantiate()するとSimpleDefineProcessorというTemplateクラスを生成する(中身は後で実装する。)
startBrace、endBrace、operators、tagsは、やはりtokenizerを作るのに使ったものと同一である必要がある。
SimpleReference、SimpleExpressionはSimpleSourceの実装クラスで、テンプレート評価時に固定値を戻したり、コンテキストから変数を取り出したりする。(実装内容については後日エンジン部分実装時に説明)

public interface SimpleSource {
    SimpleTemplate getTemplate(Context<Object, SimpleTemplate> context);
}

要素4 コンテナタグをパースする部分(defineの場合の例)

コンテナタグは「{define name=hoge}Hogeです{/define}」の用に「{」+タグ名で始まり「}」で終わるタグ開始部と、タグの中身と、「{/」+タグ名で始まり「}」で終わるタグ終了部で構成される。
defineタグなら以下のようになる。

        Parser<List<SimpleTemplatePrototype>> contentParser; // タグの中身をパースするParser
...     
        Parser<SimpleTemplatePrototype> defineContainerTagParser = 
            Parsers.sequence(
            // 開始部:  "{" "define" <attribute>* "}"
            startBrace.token("{").next(
                    Parsers.sequence(
                        tags.token("define"), makeAttributeParser().many(), 
                        new Map2<Token, List<Attribute>, List<Attribute>>() {
                            public List<Attribute> map(Token tagToken, List<Attribute> attrs) {
                                return attrs;
                            }
                        }
                    )
                ).followedBy(endBrace.token("}")),
            // タグの中身
            contentParser,
            // タグ終了部 "{" "/" "define" "}"
            startBrace.token("{").next(operators.token("/")).next(
                    tags.token("define").map(new Map<Token, Void>() {
			// 終了部はマッチングにだけ使用(戻り値はVoid)
                        public Void map(Token token) {
                            return null;
                        }
                    })
                ).followedBy(endBrace.token("}")),

            new Map3<List<Attribute>, List<SimpleTemplatePrototype>, Void, SimpleTemplatePrototype>() {
                // タグ開始部から属性のリストを受け取り、contentParserからはタグの中身を受け取る。
                public SimpleTemplatePrototype map(
			List<Attribute> attributes, final List<SimpleTemplatePrototype> contents, Void d) {
                    // name属性を探す
                    Attribute nameAttribute = Attribute.findAttribute("name", attributes);
                    // SimpleDefineProcessorPrototypeを生成して戻す。
                    return new SimpleDefineProcessorPrototype(
                            nameAttribute.getValue().getSymbol(),
                            new SimpleContainerPrototype(contents));
                }
            });
 

SimpleContainerPrototypeはSimpleTemplatePrototypeおよびSimpleSourceの実装クラスで、instantiate()するとSimpleContainerというTemplateクラスを生成する(中身は後で実装する。)
contentParserはタグの中身をパースするParserだが、タグの中にタグを書く事も出来るようにする為には、結局テンプレート全体をパースできるParserが必要になる。Jparsecでは、Parser.newReference()で参照を作り、後で中身をセットする事で、再帰構造を実現する。

要素3+4 タグをパースする部分を汎用にする

上記の例はdefineタグ用だったけど、タグ名の部分以外は基本的にどのタグも同じようなコードになる。
そこで、TagHandlerというインタフェースを導入して、後付けでタグを増やせるようにする。
まず、TagHandlerの定義

package sample.hotplate.sample.parser;

import java.util.List;

import sample.hotplate.sample.prototype.SimpleTemplatePrototype;

/**
 * パース時にタグを取り扱う為のインタフェース
 */
public interface TagHandler {
    /** @return このハンドラが扱うタグ名の一覧を戻す。 */
    String[] tagNames();
    /**
     * @return シングルタグを受け取る場合true、受け取らない場合はfalseを戻す。
     * @param tagName タグ名
     */
    boolean requireSingleTag(String tagName);
    /**
     * @return コンテナタグを受け取る場合true、受け取らない場合はfalseを戻す。
     * @param tagName タグ名
     */
    boolean requireContainerTag(String tagName);
    /**
     * @return シングルタグの定義を受け取ってタグ用のSimpleTemplatePrototypeを戻す。
     * @param tagName タグ名
     * @param attributes 属性のリスト
     */
    SimpleTemplatePrototype handleSingleTag(String tagName, List<Attribute> attributes);
    /**
     * @return コンテナタグの定義を受け取ってタグ用のSimpleTemplatePrototypeを戻す。
     * @param tagName タグ名
     * @param attributes 属性のリスト
     * @param elements タグの中身を含むSimpleTemplatePrototypeのList
     */
    SimpleTemplatePrototype handleContainerTag(
	String tagName, List<Attribute> attributes, List<SimpleTemplatePrototype> elements);
}

これを使って、シングルタグとコンテナタグのパーサの生成部分を書き直す。

    // シングルタグパーサを生成する。
    public static Parser<SimpleTemplatePrototype> makeSingleTagParser(
            Terminals tags, String tagName, final TagHandler handler) {
        return
            startBrace.token("{").next(
                Parsers.sequence(
                    tags.token(tagName), makeAttributeParser().many(), 
                    new Map2<Token, List<Attribute>, SimpleTemplatePrototype>() {
                        public SimpleTemplatePrototype map(Token tagToken, List<Attribute> attributes) {
                            String tagName = tagToken.toString();
                            return handler.handleSingleTag(tagName, attributes);
                        }
                    }
                )
            ).followedBy(operators.token("/").next(endBrace.token("}")));
    }
    // コンテナタグパーサを生成する。タグ開始部 + 中身 + タグ終了部
    public static Parser<SimpleTemplatePrototype> makeContainerTagParser(
            Terminals tags, Parser<List<SimpleTemplatePrototype>> contentParser,
            final String tagName, final TagHandler handler) {

        return Parsers.sequence(
            startTagParser(tags, tagName),
            contentParser,
            endTagParser(tags, tagName),

            new Map3<List<Attribute>, List<SimpleTemplatePrototype>, Void, SimpleTemplatePrototype>() {
                public SimpleTemplatePrototype map(
			List<Attribute> attributes, final List<SimpleTemplatePrototype> contents, Void d) {
                    return handler.handleContainerTag(tagName, attributes, contents);
                }
            });
    }
    private static Parser<List<Attribute>> startTagParser(Terminals tags, String tagName) {
        return
            startBrace.token("{").next(
                Parsers.sequence(
                    tags.token(tagName), makeAttributeParser().many(), 
                    new Map2<Token, List<Attribute>, List<Attribute>>() {
                        public List<Attribute> map(Token tagToken, List<Attribute> attrs) {
                            return attrs;
                        }
                    }
                )
            ).followedBy(endBrace.token("}"));
    }
    private static Parser<Void> endTagParser(Terminals tags, String tagName) {
        return 
            startBrace.token("{").next(operators.token("/")).next(
                tags.token(tagName).map(new Map<Token, Void>() {
                    public Void map(Token token) {
                        return null;
                    }
                })
            ).followedBy(endBrace.token("}"));
    }

define用のTagHandlerの実装はは次のようになる。

package sample.hotplate.sample.parser.handler;

import java.util.List;

import sample.hotplate.sample.parser.Attribute;
import sample.hotplate.sample.parser.TagHandler;
import sample.hotplate.sample.processor.prototype.SimpleDefineProcessorPrototype;
import sample.hotplate.sample.prototype.SimpleContainerPrototype;
import sample.hotplate.sample.prototype.SimpleTemplatePrototype;

public class DefineTagHandler implements TagHandler {
    @Override
    public String[] tagNames() {
        return new String[] {"define"};
    }
    @Override
    public boolean requireSingleTag(String tagName) {
        return true; // シングルタグも可能
    }
    @Override
    public boolean requireContainerTag(String tagName) {
        return true; // コンテナタグも可能
    }
    // シングルタグを生成
    @Override
    public SimpleTemplatePrototype handleSingleTag(String tagName,
            List<Attribute> attributes) {
        Attribute nameAttribute = Attribute.findAttribute("name", attributes);
        Attribute valueAttribute = Attribute.findAttribute("value", attributes);
        return new SimpleDefineProcessorPrototype(
                nameAttribute.getValue().getSymbol(), 
                TagHandlerUtils.valueSource(valueAttribute));
    }
    // コンテナタグを生成
    @Override
    public SimpleTemplatePrototype handleContainerTag(String tagName,
            List<Attribute> attributes, List<SimpleTemplatePrototype> elements) {
        Attribute nameAttribute = Attribute.findAttribute("name", attributes);
        return new SimpleDefineProcessorPrototype(
                nameAttribute.getValue().getSymbol(), new SimpleContainerPrototype(elements));
    }
}

SimpleSourceを作り出す所はUtils化しておく。

package sample.hotplate.sample.parser.handler;

import sample.hotplate.core.Symbol;
import sample.hotplate.sample.parser.Attribute;
import sample.hotplate.sample.source.SimpleExpression;
import sample.hotplate.sample.source.SimpleReference;
import sample.hotplate.sample.source.SimpleSource;

public final class TagHandlerUtils {
    public static SimpleSource makeSource(Attribute attribute) {
        final SimpleSource source;
        if (attribute.getValue().isSymbol()) {
            Symbol symbol = attribute.getValue().getSymbol();
            source = new SimpleReference(symbol);
        } else {
            final String expression = attribute.getValue().getExpression();
            source = new SimpleExpression(expression);
        }
        return source;
    }
}

ついでにinsert用のTagHandlerも実装。SimpleTemplatePrototypeの実装クラスであるSimpleInsertProcessorPrototypeを生成する。

package sample.hotplate.sample.parser.handler;

import java.util.Collections;
import java.util.List;

import sample.hotplate.sample.parser.Attribute;
import sample.hotplate.sample.parser.TagHandler;
import sample.hotplate.sample.processor.prototype.SimpleInsertProcessorPrototype;
import sample.hotplate.sample.prototype.SimpleContainerPrototype;
import sample.hotplate.sample.prototype.SimpleTemplatePrototype;

public class InsertTagHandler implements TagHandler {
    @Override
    public String[] tagNames() {
        return new String[] {"insert"};
    }
    @Override
    public boolean requireSingleTag(String tagName) {
        return true; // シングルタグも可能
    }
    @Override
    public boolean requireContainerTag(String tagName) {
        return true; // コンテナタグも可能
    }
    // シングルタグを生成
    @Override
    public SimpleTemplatePrototype handleSingleTag(String tagName,
            List<Attribute> attributes) {
        Attribute valueAttribute = Attribute.findAttribute("value", attributes);
        return new SimpleInsertProcessorPrototype(
                TagHandlerUtils.makeSource(valueAttribute),
                new SimpleContainerPrototype(Collections.<SimpleTemplatePrototype>emptyList()));
    }
    // コンテナタグを生成
    @Override
    public SimpleTemplatePrototype handleContainerTag(String tagName,
            List<Attribute> attributes, List<SimpleTemplatePrototype> elements) {
        Attribute valueAttribute = Attribute.findAttribute("value", attributes);
        return new SimpleInsertProcessorPrototype(
                TagHandlerUtils.makeSource(valueAttribute), 
                new SimpleContainerPrototype(elements));
    }
}

要素5 Parser>からParserを作る

端的にSimpleTemplatePrototypeのリストからSimpleContainerPrototypeを生成する。

    /**
     * @return listParserを元にSimpleTemplatePrototypeを生成するParserを戻す
     * @param listParser List<SimpleTemplatePrototype>を生成するParser
     */
    public static Parser<SimpleTemplatePrototype>
	     flatten(Parser<List<SimpleTemplatePrototype>> listParser) {
        return listParser.map(new Map<List<SimpleTemplatePrototype>, SimpleTemplatePrototype>() {
            public SimpleTemplatePrototype map(List<SimpleTemplatePrototype> elements) {
                return new SimpleContainerPrototype(elements);
            }
        });
    }

全体のパーサを書く

全体のパーサを生成するParserFactoryというクラスを作る。

TokenizerとParserの両方で使うフィールドをまとめる。あとTagHandlerを登録できるようにする。

public final class ParserFactory {
    public static final Terminals startBrace = Terminals.operators("{");
    public static final Terminals endBrace = Terminals.operators("}");
    public static final Terminals operators = Terminals.operators("/", "=");
    public static final String LITERAL_NAME = "TEXT";

    public static final ParserFactory instance = new ParserFactory();
    
    /** タグ名をキー、TagHandlerを値とするMap */
    private Map<String, TagHandler> handlers = new LinkedHashMap<String, TagHandler>();

    public ParserFactory() {
        registerHandlers();
    }

    private void registerHandlers() {
        registerHandler(new DefineTagHandler());
        registerHandler(new InsertTagHandler());
    }
    public synchronized void registerHandler(TagHandler handler) {
        for (String tagName : handler.tagNames()) {
            handlers.put(tagName, handler);            
        }
    }

まず、TagHandlerのリストからタグ用のParserを作る部分。

    private List<Parser<SimpleTemplatePrototype>> makeTagParses(
            Map<String, TagHandler> handlers,
            Terminals tags, Reference<List<SimpleTemplatePrototype>> listParserRef) {

        List<Parser<SimpleTemplatePrototype>> tagParsers = new ArrayList<Parser<SimpleTemplatePrototype>>();
        for (String tagName: handlers.keySet()) {
            TagHandler handler = handlers.get(tagName);
            // コンテナタグありならコンテナタグのパーサを作って追加
            if (handler.requireContainerTag(tagName)) {
                tagParsers.add(
                    TemplateParsers.makeContainerTagParser(tags, listParserRef.lazy(), tagName, handler));
            }
            // シングルタグありならシングルタグのパーサを作って追加
            if (handler.requireSingleTag(tagName)) {
                tagParsers.add(
                    TemplateParsers.makeSingleTagParser(tags, tagName, handler));
            }
        }
        return tagParsers;
    }

リテラル用のParserとタグ用のParserから全体用のParserを作る部分。

    private Parser<SimpleTemplatePrototype> makeParser(Map<String, TagHandler> handlers, Terminals tags) {
	// タグの中身を再帰的にパースする為のReference
        Reference<List<SimpleTemplatePrototype>> contentParserRef = Parser.newReference();

	// 全タグのパーサを取得
        List<Parser<SimpleTemplatePrototype>> parsers = makeTagParses(handlers, tags, contentParserRef);
	// リテラル用のパーサを追加
        parsers.add(TemplateParsers.makeLiteralParser());

	// パーサーの全体を作成
        Parser<List<SimpleTemplatePrototype>> listParser = Parsers.or(parsers).many();
	// タグの内容のパーサとしてセット
        contentParserRef.set(listParser);

	// List<SimpleTemplatePrototype>ではなくSimpleTemplatePrototypeを吐くように調整
        return TemplateParsers.flatten(listParser);
    }


最後にトークナイザと合わせて完成。タグ名のTerminalsはhandlersのキーから生成。

    public synchronized Parser<SimpleTemplatePrototype> newParser() {
        // Tag用のTerminalsを作成
        Terminals tags = makeTags(this.handlers);
        // Tokenizer(字句解析部)を作成
        Parser<List<Token>> tokenizer = Tokenizers.newTokenizer(tags);
        // Parser(構文解析部)を作成
        Parser<SimpleTemplatePrototype> parser = makeParser(this.handlers, tags);
        // 両方を合わせてパーサを作成
        return parser.from(tokenizer);
    }

    private Terminals makeTags(Map<String, TagHandler> handlers) {
        String[] processorNames = handlers.keySet().toArray(new String[0]);
        return Terminals.caseSensitive(new String[0], processorNames);
    }


これで文字列からSimpleTemplatePrototypeを生成するパーサが出来たので、次回はテンプレートエンジン本体の実装に入る。