Truffleでの言語実装を最小手数ではじめる

あけましておめでとうございます。
ということで、Truffleで言語実装したい気分なので、まずはJyukutyoの数式処理から始めることにしました。
オレオレJVM言語を作ろう! How to create a new JVM language #Graal #Truffle - Fight the Future


けど、APIがだいぶ変わってるようでそのままではできず、あとAntlr使っているのでTruffleのみの部分を切り離して試してみました。
足し算だけの式言語をつくります。
https://github.com/kishida/simplest_truffle_expr


簡易Truffle言語に変数を実装する - きしだのHatena
Truffle言語で関数呼び出しを実装する - きしだのHatena
Truffle言語をGraalVMで動かす - きしだのHatena

依存関係

truffle-apiとtruffle-dsl-processorが必要です。truffle-dsl-processorアノテーション処理をするだけなので、実行時には不要なはず。
GraalVMで動かす場合はどちらも不要だと思うけど、OpenJDKなどで動かすにはtruffle-apiが必要です。

<dependency>
    <groupId>org.graalvm.truffle</groupId>
    <artifactId>truffle-api</artifactId>
    <version>1.0.0-rc9</version>
</dependency>
<dependency>
    <groupId>org.graalvm.truffle</groupId>
    <artifactId>truffle-dsl-processor</artifactId>
    <version>1.0.0-rc9</version>
    <scope>provided</scope>
</dependency>    


GraalVMで動かす場合、実行時にtruffle-api.jarを含めるとGraalVMが持ってるクラスと競合してClassCastExceptionになります。しかし含めないとClassNotFound...動かし方がわからない・・・
※2019/1/4 追記。やっとわかった。
Truffle言語をGraalVMで動かす - きしだのはてな

準備

まずは言語で使う型を@TypeSystemを使って登録します。空のクラスにアノテーションを指定します。

@TypeSystem(long.class)
static abstract class MathTypes {
}


言語のASTのベースとなるクラスを作ります。これはTruffleのNodeクラスを継承します。このとき@TypeSystemReferenceで先ほど型を登録したクラスを指定しておきます。
メソッドとしてVirtualFrameを受け取るexecuteGenericというメソッドを登録しておきます。executeXxxで型ごとの処理を書けるっぽい。すべての型を処理する場合はexecuteGeneric

@TypeSystemReference(MathTypes.class)
static abstract class MathNode extends Node {
    abstract Object executeGeneric(VirtualFrame frame);
}

式のASTを作る

今回はlongだけ使うので、longリテラル用のノードを作ります。longを処理するためのexecuteLongメソッドを用意しておきます。
@NodeInfoはノード情報を持たせるアノテーションです。

@NodeInfo(shortName = "value")
static class LongNode extends MathNode {
    private long value;

    private LongNode(long value) {
        this.value = value;
    }

    static LongNode of(String v) {
        return new LongNode(Long.parseLong(v.trim()));
    }

    long executeLong(VirtualFrame frame) {
        return value;
    }

    @Override
    Object executeGeneric(VirtualFrame frame) {
        return value;
    }
}


そして足し算ノード。左項と右項を保持するフィールドを、@NodeChildとして持たせてます。また、実際の足し算はaddメソッドで定義しますが、型を限定する処理には@Specializationをつけるっぽい。

@NodeInfo(shortName = "+")
@NodeChild("leftNode")
@NodeChild("rightNoode")
public static abstract class AddNode extends MathNode {
    @Specialization
    public long add(long left, long right) {
        return left + right;
    }

    public Object add(Object left, Object right) {
        return null;
    }
}


アノテーションプロセッサによって、AddNodeGenというクラスが生成されて、実際にaddを呼び出すexecuteGenericなどのメソッドや、createというファクトリメソッドが生成されます。

@GeneratedBy(AddNode.class)
public static final class AddNodeGen extends AddNode {

    @Child private MathNode leftNode_;
    @Child private MathNode rightNoode_;
    @CompilationFinal private int state_;

    private AddNodeGen(MathNode leftNode, MathNode rightNoode) {
        this.leftNode_ = leftNode;
        this.rightNoode_ = rightNoode;
    }

    @Override
    Object executeGeneric(VirtualFrame frameValue) {
        int state = state_;
        Object leftNodeValue_ = this.leftNode_.executeGeneric(frameValue);
        Object rightNoodeValue_ = this.rightNoode_.executeGeneric(frameValue);
        if (state != 0 /* is-active add(long, long) */ && leftNodeValue_ instanceof Long) {
            long leftNodeValue__ = (long) leftNodeValue_;
            if (rightNoodeValue_ instanceof Long) {
                long rightNoodeValue__ = (long) rightNoodeValue_;
                return add(leftNodeValue__, rightNoodeValue__);
            }
        }
        CompilerDirectives.transferToInterpreterAndInvalidate();
        return executeAndSpecialize(leftNodeValue_, rightNoodeValue_);
    }

    private long executeAndSpecialize(Object leftNodeValue, Object rightNoodeValue) {
        ....
    }
    ...
    public static AddNode create(MathNode leftNode, MathNode rightNoode) {
        return new AddNodeGen(leftNode, rightNoode);
    }

}

実行準備

実行にはRootNodeを継承したクラスも必要っぽい。このexecuteメソッドが呼び出されます。

static class MathRootNode extends RootNode {
    private MathNode body;

    public MathRootNode(
            TruffleLanguage<?> language, FrameDescriptor frameDescriptor, 
            MathNode body) {
        super(language, frameDescriptor);
        this.body = body;
    }

    @Override
    public Object execute(VirtualFrame frame) {
        return body.executeGeneric(frame);
    }
}


コンテキストを保持することになってるクラスも作ります。今回は空のクラス

public static class MathLangContext {
}


そして言語を登録するクラスを作ります。
@TruffleLanguage.Registrationアノテーションを指定しますが、Jyukutyoのときに比べるとidが必要になってmimeTypeがなくなり、代わりにdefaultMimeTypeとcharacterMimeTypesを指定しています。
parseメソッドで足し算のパースを行ってNodeツリーを作りつつ、RootNodeに持たせて、そこからCallTargetを返すという感じ。

@TruffleLanguage.Registration(name = "MathLang", id = "mathlang",
        defaultMimeType = MathLang.MIME_TYPE, characterMimeTypes = MathLang.MIME_TYPE)
@ProvidedTags({StandardTags.CallTag.class, StandardTags.StatementTag.class, 
    StandardTags.RootTag.class, DebuggerTags.AlwaysHalt.class})
public class MathLang extends TruffleLanguage<MathLangContext>{
    public static final String MIME_TYPE = "application/x-mathlang";

    @Override
    protected CallTarget parse(ParsingRequest request) throws Exception {
        String source = request.getSource().getCharacters().toString();
        String[] nums = source.split("\\+");
        MathNode node = LongNode.of(nums[nums.length - 1]);
        for (int i = nums.length - 2; i >= 0; --i) {
            node = MathNodesFactory.AddNodeGen.create(LongNode.of(nums[i]), node);
        }
        MathRootNode root = new MathRootNode(this, new FrameDescriptor(), node);
        return Truffle.getRuntime().createCallTarget(root);
    }
    
    @Override
    protected MathLangContext createContext(Env env) {
        return new MathLangContext();
    }

    @Override
    protected boolean isObjectOfLanguage(Object object) {
        return false;
    }
    
}

実行

そして実行です。
一番苦労したところで、Jyukutyoの書いてるPolyglotEngineが見当たらず。
@TruffleLanguage.Registrationに指定したidを渡してContextを作り、evalすると計算してくれます。

public class MathMain {
    public static void main(String[] args) {
        String exp = "12+34+56";
        Context cont = Context.create("mathlang");
        System.out.println(cont.eval("mathlang", exp));
    }
}


いやー、なにがなんだかわかりませんね。

ネイティブコンパイル

GraalVMをJVMとして実行することには失敗してますが、ネイティブコンパイルはできました。--tool:truffleをつけるとよさげ。

$ native-image --tool:truffle -cp Mathexpr-1.0-SNAPSHOT.jar mathexpr.MathMain me
[me:53002]    classlist:     263.17 ms
[me:53002]        (cap):   1,308.09 ms
[me:53002]        setup:   2,527.17 ms
[me:53002]   (typeflow):   9,000.68 ms
[me:53002]    (objects):  13,375.22 ms
[me:53002]   (features):     681.50 ms
[me:53002]     analysis:  23,762.15 ms
629 method(s) included for runtime compilation
[me:53002]     universe:     642.53 ms
[me:53002]      (parse):   1,174.13 ms
[me:53002]     (inline):   2,004.82 ms
[me:53002]    (compile):  13,530.32 ms
[me:53002]      compile:  17,943.66 ms
[me:53002]        image:   1,967.82 ms
[me:53002]        write:     701.59 ms
[me:53002]      [total]:  47,888.13 ms
$ ./me
102