自己动手写编程语言(2)

前言

把修改后的上传到了我的仓库

https://github.com/Wooyme/NMSL

顺便取了个意义不明的名字Naive Mind Specialize Language,简称nmsl。

这篇先写一点原理相关的东西,然后就是lambda。

编译期与运行期

开发语言和开发应用最大的区别就是语言开发中我们要预想的情况并不是程序的运行情况而是语言编译的情况。把运行和编译的逻辑理清楚后就能理解很多东西了。

英语比较好的话可以看一下

https://www.youtube.com/watch?v=FJY96_6Y3a4

《 One VM to Rule Them All, One VM to Bind Them》

是Oracle Labs的人做的演讲。比较尴尬的是没有英文字幕,只能完全靠听力了。

Node

首先我们看到parser包下的SLNodeFactory里有大量的createXXX的方法,而在nodes目录下都是继承了Node的类。了解过编译原理的朋友肯定知道编译器首先要解析语法然后生成相应的语法树,那么既然有树就肯定会有节点,而Node类代表的就是这些节点。

SimpleLanguage中有builtinexpressioncontrolflowlocal这4个包,分别包含了内置函数,表达式,控制流和局部读写的节点。

Expression

Expression是比较简单的一种节点,功能上和builtin类似,但是与语法解析过程的联系更加紧密一些。

首先以SLBigIntegerLiteralNode.java为例。

@NodeInfo(shortName = "const")
public final class SLBigIntegerLiteralNode extends SLExpressionNode {

    private final SLBigNumber value;

    public SLBigIntegerLiteralNode(BigDecimal value) {
        this.value = new SLBigNumber(value);
    }

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

这个是涉及code generate最少的一个类了,所以就以它为例。SLBigIntegerLiteralNode总共就一个构造函数和一个executeGeneric方法。这里就出现了一个生命周期的问题。SLBigIntegerLiteralNode在生成语法树的过程中被实例化executeGeneric是在代码运行的时候被调用

所以就有了SLBigIntegerLiteralNode中的操作,在生成语法树的过程中把常量value保存在类里,然后在executeGeneric中直接返回这个保存的值。

更复杂的Expression

SLWritePropertyNode为例

@NodeInfo(shortName = ".=")
@NodeChild("receiverNode")
@NodeChild("nameNode")
@NodeChild("valueNode")
public abstract class SLWritePropertyNode extends SLExpressionNode {

    static final int LIBRARY_LIMIT = 3;

    @Specialization(guards = "arrays.hasArrayElements(receiver)", limit = "LIBRARY_LIMIT")
    protected Object write(Object receiver, Object index, Object value,
                    @CachedLibrary("receiver") InteropLibrary arrays,
                    @CachedLibrary("index") InteropLibrary numbers) {
        try {
            arrays.writeArrayElement(receiver, numbers.asLong(index), value);
        } catch (UnsupportedMessageException | UnsupportedTypeException | InvalidArrayIndexException e) {
            throw SLUndefinedNameException.undefinedProperty(this, index);
        }
        return value;
    }

    @Specialization(limit = "LIBRARY_LIMIT")
    protected Object write(Object receiver, Object name, Object value,
                    @CachedLibrary("receiver") InteropLibrary objectLibrary,
                    @Cached SLToMemberNode asMember) {
        try {
            objectLibrary.writeMember(receiver, asMember.execute(name), value);
        } catch (UnsupportedMessageException | UnknownIdentifierException | UnsupportedTypeException e) {
            // write was not successful. In SL we only have basic support for errors.
            throw SLUndefinedNameException.undefinedProperty(this, name);
        }
        return value;
    }
}

首先看到的是多了几个NodeChild的类注解以及Specialization的方法注解。刚刚也提到了code generate,所以这两个注解很明显就是和代码生成相关的。比较一下和上面的区别。发现SLWritePropertyNode是abstract的也没有实现executeGeneric方法,这就说明生成的代码为我们做了不少事情。

代码生成器生成的代码就不贴了,转述一下《 One VM to Rule Them All, One VM to Bind Them》里的话:

@NodeChild("receiverNode")会生成private final Node receiverNode; 同时会生成create(Node receiverNode)方法,而三个NodeChild自然就会生成三个这样的属性和三个create的参数。

与这些node相对的是带有Specialization注解的方法的参数,这个对应关系只与顺序相关,与名称是无关的。在运行过程中,生成的代码会调用三个子Node的executeGeneric方法得到值,然后传入到带有Specialization注解的方法中。

Node总结

ExpressionNode相同,其他几类Node的生命周期也是这样。不过有些细节上的区别。比如Builtins会在SLContext里被提前install。

    //SLContext中安装内置函数
    private void installBuiltins() {
        installBuiltin(SLReadlnBuiltinFactory.getInstance());
        installBuiltin(SLPrintBuiltinFactory.getInstance());
        installBuiltin(SLNanoTimeBuiltinFactory.getInstance());
        installBuiltin(SLDefineFunctionBuiltinFactory.getInstance());
        installBuiltin(SLStackTraceBuiltinFactory.getInstance());
        installBuiltin(SLNewObjectBuiltinFactory.getInstance());
        installBuiltin(SLEvalBuiltinFactory.getInstance());
        installBuiltin(SLGetSizeBuiltinFactory.getInstance());
        installBuiltin(SLHasSizeBuiltinFactory.getInstance());
        installBuiltin(SLIsExecutableBuiltinFactory.getInstance());
        installBuiltin(SLIsNullBuiltinFactory.getInstance());
        installBuiltin(SLWrapPrimitiveBuiltinFactory.getInstance());
        installBuiltin(SLMembersBuiltinFactory.getInstance());
        installBuiltin(SLOpenBuiltinFactory.getInstance());
        installBuiltin(SLWriteBuiltinFactory.getInstance());
        installBuiltin(SLCloseBuiltinFactory.getInstance());
        installBuiltin(SLReadBuiltinFactory.getInstance());
        installBuiltin(SLSleepBuiltinFactory.getInstance());
        installBuiltin(SLInterfaceBuiltinFactory.getInstance());
        installBuiltin(SLToIntNodeFactory.getInstance());
        installBuiltin(SLFromProxyBuiltinFactory.getInstance());
        installBuiltin(SLUnInterfaceBuiltinFactory.getInstance());
    }

运行期

可以看到在SLWritePropertyNode中有这样一个方法。

    @Specialization(guards = "arrays.hasArrayElements(receiver)", limit = "LIBRARY_LIMIT")
    protected Object write(Object receiver, Object index, Object value,
                    @CachedLibrary("receiver") InteropLibrary arrays,
                    @CachedLibrary("index") InteropLibrary numbers) {
        try {
            arrays.writeArrayElement(receiver, numbers.asLong(index), value);
        } catch (UnsupportedMessageException | UnsupportedTypeException | InvalidArrayIndexException e) {
            throw SLUndefinedNameException.undefinedProperty(this, index);
        }
        return value;
    }

这里出现了InteropLibrary这个类型,根据注解可以看出来,InteropLibrary和前面的Object是有关的。这里具体是什么关系很难直接说明,但是可以看到在SLObjectTypeSLBigNumber这两个类的注解中均出现了InteropLibrary.class。这就是Truffle API让人用着很不爽的地方,它大量的使用了注解和代码生成,让代码结构非常混乱,而且还找不到文档。如果不看完那3个小时的视频很多地方就很难理解。

现在已知的是,可以从receiverindex中产生它们对应的InteropLibrary,也就是这里的arraysnumbers

然后在@Specialization的注解中可以看到guards属性,从字面意思上就可以看出这个属性会生成用于保证方法正确运行的代码。因为这个write方法是要向array写入,所以这里的receiver就必须是array,于是就有了guards里的代码,保证receiver是有数组元素的。

但是!事实上,我们想要的往往是List,而不是一个Array,所以为了实现往List里写入元素只要加入这样一个方法。

    @Specialization(limit = "LIBRARY_LIMIT")
    protected Object write(List receiver, Object index, Object value,
                           @CachedLibrary("index") InteropLibrary numbers){
        try {
            receiver.set(numbers.asInt(index),value); 
        } catch (UnsupportedMessageException | IndexOutOfBoundsException e) {
            throw SLUndefinedNameException.undefinedProperty(this, index); // 如果超出list长度,则报错
        }
        return value;
    }

这里问题就来了,List是一个已知的类型,而前面的Object传进来的很明显也应该是一个已知的类型。那么这个Object会是什么呢?

DynamicObject

事实上,这个Object对应的是DynamicObject,而DynamicObject的最终实现则是runtime包下的SLObjectType。能看出SLObjectType即是DynamicObject的最直接证据就是SLObjectType的类注解

@ExportLibrary(value = InteropLibrary.class, receiverType = DynamicObject.class)

这个连接过程有大量的代码生成来实现。

可以看到SLObjectType实现了hasArrayElementshasMembers等方法,还有GetMembers,ReadMember,WriteMember这些子类。从字面意思上都可以看出这些东西的功能,但是在代码上完全看不出它们与DynamicObject或是InteropLibrary的关系。

如果想要知道SLObjectType中这些方法与InteropLibrary的关系,可以看com.oracle.truffle.api.interop.InteropLibaray的源码。

小结

花了很大的篇幅写ExpressionNode,所以Lambda放到下一篇。