今年もD言語の構文を拡張する

  • 6
    いいね
  • 0
    コメント

D言語 Advent Calendar 2016の23日目の記事です.
更に,Aizu Advent Calendar 2016の23日目の記事でもあります.

前回

前回の記事でD言語の構文拡張ライブラリであるTcenalの話をしました.
この記事ではTcenalの変更点などを見ていきます.

Tcenalの変更点

まず大きな変更点として,構文解析を行う前に字句解析を行うようにしました.
これによって前回問題になっていたintintに解析されてしまう問題を回避できます.

入力が字句列になることによって,構文解析時に字句の種類を調べる必要が出てきます.
それ用の構文をDSLに追加しました.

Tcenalの使い方などは変わってません.

例:代数的データ型とそのパターンマッチ

今流行りの(?)パターンマッチをやってみました.

パーサを書く

source/pattern_matching.d
mixin(generateParsers(
q{
    AggregateDeclaration <- CaseClassDeclaration / super
    CaseClassDeclaration <- "case" "class" @identifier "{" CaseClassConstructor*<","> ","? "}"
    CaseClassConstructor <- @identifier "(" (Type @identifier)*<","> ","? ")"

    PrimaryExpression <- CaseExpression / super
    CaseExpression <- "case" "(" Expression ")" "{" Case* "}"
    Case <- Pattern ":" Expression ";"
    Pattern <- @identifier ("(" Pattern*<","> ","? ")")? / PrimaryExpression
}));

前回のSwap演算子よりは多少複雑になってます.
@identifierは,前述したような,字句の種類を調べ,識別子だったら成功するようなパーサです.
superの仕様は前回と変わりません.

コード生成器を書く

パースツリーを直接いじってるので,かなり複雑になっています.

source/pattern_matching.d
string generateDlangCode(ParseTreeNode node)
{
    switch (node.ruleName)
    {
        case "CaseClassDeclaration":
        {
            string code = "class " ~ node.children[0].children[2].token.value ~ "{}";

            foreach (caseClassConstructor; node.children[0].children[4].children)
            {
                code ~=
                    "class " ~ caseClassConstructor.children[0].children[0].token.value ~ ":" ~ node.children[0].children[2].token.value ~"{"
                    q{this(typeof(this.tupleof) args) { this.tupleof = args; }}
                    q{static typeof(this) opCall(typeof(this.tupleof) args) { return new typeof(this)(args); }}
                ;

                foreach (field; caseClassConstructor.children[0].children[2].children)
                {
                    code ~= field.children[0].generateDlangCode() ~ " " ~ field.children[1].token.value ~ ";";
                }

                code ~= "}";
            }

            return code;
        }

        case "CaseExpression":
        {
            string code = "(){auto FOR_PATTERN_MATCHING_DONT_REFER_ME = " ~ node.children[0].children[2].generateDlangCode() ~ ";" ;

            foreach (case_; node.children[0].children[5].children)
            {
                code ~= case_.generateDlangCode();
            }

            code ~= "assert(0);}()";

            return code;
        }

        case "Case":
            return node.children[0].children[0].generateDlangCodeFromPattern("FOR_PATTERN_MATCHING_DONT_REFER_ME", "return " ~ node.children[0].children[2].generateDlangCode() ~ ";");

        default:
        {
            if (0 < node.ruleName.length) {
                string code;

                foreach (child; node.children)
                {
                    code ~= child.generateDlangCode();
                }

                return code;
            } else {
                return node.token.value ~ " ";
            }
        }
    }
}

string generateDlangCodeFromPattern(ParseTreeNode node, string matchingValue, string continuation)
{
    if (node.children[0].ruleName == "PrimaryExpression")
    {
        return "if (" ~ matchingValue ~ " == " ~ node.children[0].generateDlangCode() ~ ") {" ~ continuation ~ "}";
    }
    else if (node.children[0].children[1].children.length == 0)
    {
        if (node.children[0].children[0].token.value == "_")
        {
            return continuation;
        }
        else
        {
            return "auto " ~ node.children[0].children[0].token.value ~ " = " ~ matchingValue ~ ";" ~ continuation;
        }
    }
    else
    {
        string code = continuation;

        foreach_reverse (i, pattern; node.children[0].children[1].children[0].children[1].children)
        {
            code = pattern.generateDlangCodeFromPattern("(cast(" ~ node.children[0].children[0].token.value ~ ") " ~ matchingValue ~ ").tupleof[" ~ i.to!string ~ "]", code);
        }

        code = "if (cast(" ~ node.children[0].children[0].token.value ~ ") " ~ matchingValue ~ ") {" ~ code ~ "}";

        return code;
    }
}

alias parse = parseWithoutMemo!(Module!(createRuleSelector!().RuleSelector));

string PATTERN_MATCHING(string source)
{
    return source.lex().parse().node.generateDlangCode();
}

試してみる

source/app.d
import pattern_matching : PATTERN_MATCHING;

mixin(PATTERN_MATCHING(
q{
    import std.stdio;

    case class Expr {
        Number(int number),
        UnOp(string op, Expr expr),
        BinOp(string op, Expr lhs, Expr rhs),
    }

    int eval(Expr e)
    {
        return case (e) {
            Number(n): n;
            UnOp("-", operand): -operand.eval();
            BinOp("+", lhs, rhs): lhs.eval() + rhs.eval();
            BinOp("-", lhs, rhs): lhs.eval() - rhs.eval();
        };
    }

    void main()
    {
        BinOp("-", Number(33), Number(4)).eval().writeln();
        BinOp("+", Number(50), UnOp("-", Number(-7))).eval().writeln();
    }
}));
$ dub
29
57

代数的データ型やパターンマッチの構文が非常にアレですが,なんとか動いてくれました.

動くコード

https://github.com/youxkei/pattern_matching

まとめ

字句解析を行うことで,それなりにちゃんと構文解析が出来るようになりました.
ただ,字句解析器が非常にtoy lexerなので(浮動小数点数リテラルに対応していないなど),その辺りをどうにかしたいと思っています.

この投稿は D言語 Advent Calendar 201623日目の記事です。