脚本语言:Xmas(一)

2023-06-25,

  很偶然的一个想法,在从北京回成都的高铁上:我想要一个计算器。于是在火车上花了十来个小时,完成了一个模型:能够处理+-*/的优先级,以及“()”,比如:1+(3+2)*4。这已是一年前的故事了;之后支持了函数,便成为了一门语言(虽然是脚本)。先后经过数次根本性地修改,便成为了现在本文的主角:一个基于栈的,支持过程式、函数式、面向对象三种风格的,拥有垃圾收集器的编译型脚本语言——Xmas

  一、Xmas是什么?

  首先她是“圣诞节”的简称,用以纪念多年前,还在看凉宫春日的忧郁时的自己(也是通过其,我第一次知道Xmas,凉宫曾将其画在活动室的窗户上)。

  起先,她只是一颗可以执行的抽象语法树,并且没有自己的完整的运行时,很多功能都依赖于宿主系统(C++);后来,她有了自己的字节码,便得以从宿主系统独立出来,编译器、虚拟机、垃圾收集器是各自独立的系统;现在,加入了lambda、面向对象后,一堆语法糖接踵而至,也有了一个更加高效和健壮的GC。

  究其根本,其仅仅由以下单元命令组成(以后会大量用到):

push ->入栈
pop ->出栈(实际中并没有这个操作)
set ->修改值(参数、变量)
+-*/% ->操作符
call ->函数调用
ret ->函数返回
goto ->无条件跳转(如break)
jump ->有条件跳转(如if、while)

  其中【pop】并不实际存在;因为很多命令本身需要通过pop来获取所操作的内容(基于栈的语言),所以其是集成到其他操作本身的,如:

, $) #res

  其表示了完整的减法指令:栈第二个弹出的值($1)作为被减数,第一个弹出的值($0)作为减数,并将结果入栈(#res);其中$代表了出栈,#代表了入栈,res委婉地表示自己是结果(result)。

  也就是:1 - 2,是以下指令:

 :
 :
 : , $) #res

  首先将1入栈,再将2入栈;所以,在栈中的顺序自然是:2在栈顶、1在栈的第二个位置(相对栈顶)。

  有了以上知识,后面的内容展开才能够更加容易(我使用了生成的指令,来说明代码的语义)。

  二、基本类型

  Xmas中并没有多少类型;尤其是基本类型:Null、Boolean、String、Ref、Function、Integer、Float、Decimal、Array、List、Set、Map、Object,其实挺多的;作为脚本,一个动态类型的语言,通过库的方式提供复杂的数据结构,是不现实的(太慢);所以,我将所有可能用到的容器类型都作为语言本身的基本类型提供:

  1、Array,映射了C++STL的std::vector;当然,在Xmas中可以happy地作为数组使用,还是动态增长的(所以,Xmas中并没有【数组】这个东西)。

  2、List,同样映射了std::list;任意节点地删除、插入,是其最完美地使用方法。

  3、Set,为了避免出现Map<value, null>这种尴尬的情形(为了完成序列的唯一性),所引入的std::unordered_set。

  4、Map,这个是必不可少的std::unordered_map;其原则上可以模拟上面的Array(Map<Integer, value>)、Set。

  当然,那个Object是为了支持对象而存在的,后来也顺便支持了面向对象(对象和面向对象,是两个不同的概念)。

  Null类型只有一个实例:null;很自然地,其代表了“这里什么也没有”(严格的,所以并没有【undefined】这个多余的东西);Boolean和String,不需多说,唯一需要说明的是:Xmas并没字符类型,只有字符串。

  Ref,是一个极为特殊的类型;其是用来和宿主语言交互的,其本身并不代表任何Xmas的事物;它是宿主语言中的东西,专业点就是,其内部包含了一个【非托管对象】;任何需要的,不方便在Xmas中实现的东西(如宿主语言已有,或为了性能),通过Ref便能够加入到Xmas中,当然操作它,亦需要定义适配API,以在Xmas中调用操作Ref中的内容。

  Function,就是函数本身(或者说函数指针);为了支持函数式编程风格,函数成为第一值类的公民,是必须的。

  Integer和Float,仅仅是long long和double的映射(在C++中);为了支持动态类型,而付出的带价。

  Decimal,是本人所写的H.Numeric.BigDecimal.decimal高精度类型的映射版本;许多动态语言都支持高精度类型,Xmas没有理由不支持。

  这些类型的目的无碍乎只有一个:严重地拖慢Xmas的运行速度......(玩笑~~)在Xmas第一个简易版本中(就是前面的那个计算器),通过使用模板,其速度比动态类型版本,要快一个数量级。但是,那时Xmas运行时,只能有一种类型.....

  三、函数

  函数是Xmas第一个支持的模型,也是最早支持的,当然而是作为一门语言的标志和基石;到现在为止,许多特性都是通过原生的函数调用来实现的,而你看到的只是编译器所提供的语法糖(当然,语义本身是有改变的)。

  其通过使用和javascript同样的关键词来声明(话说,就语法结构而言,Xmas和javascript几乎一样):

function add(a)
{
    return function(b){
        return a + b;
    };
}

  上面便是一个最基本的函数式的函数:其传入被加数a,并返回一个函数,其期待加数b以完成加法操作。嗯,这也是一个闭包(我不喜欢这个错误的说法),或者说一个lambda;其中a便是被闭包所捕获的自由变量。

  这并没有什么特别的,特别的是其所产生的指令:

add(a) :

 : push Function : lambda
 :
 : push Function : lambda
 : , paramNum : , self : false) #res
 : 

  add函数在返回之前,居然产生了一次函数调用!!其对应的真实代码是:

function add(a)
{
    return lambda(lambda_0, a);
}

  其中【lambda_0】便是“function(b){ return a + b;}”的函数本体;而【lambda】本身是一个函数:其用以生成一个闭包(lambda)。到这里一切很明了:Xmas本身并没有传统意义上的闭包,其是通过绑定来模拟实现的(最初的闭包版本,是通过调用bind函数,来实现的)。

  那么什么是【传统意义上的闭包】?

  1、首先,每个函数都有一个运行时环境;函数运行时的所有参数和本地变量,保存在其中。

  2、其次,变量定位是通过这个环境链来查找的;如果当前函数环境没有该变量,便向调用链的上一层寻找。

  然而,Xmas并没有这样的环境;个人认为每次函数调用都创建一个环境消耗太大(其实是太懒,不想修改虚拟机),便在编译期创建对应的环境(呃....这个其实,更加复杂;需要类似静态分析的技术),当在当前函数环境(编译期的)没有找到变量定义时,便向上一层寻找。

  所以,这更加印证了Xmas其实是一门编译型的脚本:在运行时,一切都是在编译期固定的,不可改变。(当然,通过在运行时,调用一次编译器,便可以修改整个代码的运行时环境;也就是所谓的“动态”。)

  四、对象

  注意,并不是面向对象.....

  对象是有必要存在的;因为Xmas并没有可以自定义结构的方法(无论如何都不可以)。所以,为了支持以下,看上去很方便的方式:

function change(obj)
{
    obj.value = ‘hehe';
}

  那个对象的【.】是有必要支持的;否则你没有任何办法通过函数改变一个对象的值(因为Xmas也是传值调用);当然,你可以返回一个全新的对象,但这样不就是太函数式了不是吗?Xmas只是支持函数式风格,并不是说一定要用。

  所以,如果一旦这样使用:

function AClass()
{
    obj = new Object(expand());
    obj.value = ;
    obj.getValue = function(){ return this.value;};
    obj.setValue = function(value){ return this.value = value;};
    return obj;
}

  你也就可以创建一个对象了:其有字段、也有成员函数。就差继承了.....所以,这依旧只是对象而非面向对象。需要说一下的是,【expand】是一个系统函数,其用来展开参数:可以展开当前函数传入的所有/部分参数(即使是没有声明的)、也可以展开一个Array以作为当次函数调用的参数。这里可以顺便说一句——Xmas是默认支持可变参数的,注意是【默认】;也就是不需要任何额外的声明什么的(是不是很方便?)。

  当然获取匿名参数也很简单(直接传递给下一个函数,便是上面的方法):

function add()
{
    ) + );
}

  回到对象上的话题,那么,字段的访问,其实现如何?

obj.value = ;
obj.func('param');

  对应了如下指令:

 :
 :
 : .value, $)
 : push String : param
 :
 : , $.func)
 : , paramNum : , self : true)

  你可能注意到了那个【self】,其对应了面向对象的this(在Xmas中self和this是对等的,都可以使用);而且,注意到了吗?生成的指令已经知道了该次函数调用,是传入了【self】;这再次印证了那句话:Xmas是一个编译型脚本。

  最后说一句,Xmas是支持Object定义的(语法糖而已):

function AClass()
{
    return new Object{
        value : ;
        func  : function(){ return "I'm a func";};
    };
}

  那么,其真实的代码呢?

function AClass()
{
    , 'func', function(){ return "I'm a func";});//Xmas同时支持两种字符串的声明【’/“】
}

  【Object】本身依然是一个函数,而已。

  五、面向对象

  篇幅不多的,先上一段代码:

class Xmas{
    function Xmas(){}
    function toString(){
        return this.name();
    }
    function finalize(){}
    function clone(){
        return clone(this);
    }
    function name(){
        return this.__name;
    }
    function has(name){
        return has_value(this, name);
    }
    function has_func(name){
        return typeof(get_value(this, name)) == Function;
    }
}

  这是Xmas面向对象时的基类(暂定);其有着几个令人着迷的地方:finalize、clone、__name,还有两个没有显示出的【__type、__final】。

  其中所有的函数,以及那三个带“__”的字段;其实都位于this.class.XXX;是的每个class都有一个【class】成员,其是所有实例所共享的;类似于C++的虚函数表。

  六、异常

  Xmas并不支持异常处理;但她实实在在地回在任何可能的地方抛异常:虚拟机发现函数调用所传入的函数并不是真是函数时、传入参数太少、错误的类型操作、调用宿主函数等等。

  原因嘛,是因为在意识到这个问题时;Xmas并不支持面向对象,也就是并没有Exception这个定义;所以抛出的异常,并非Xmas本身所能够识别的类型(是宿主语言的异常)。后来,有了面向对象,但我懒。

  但是,Xmas是支持基本的异常处理的:

function throw()
{
    obj = new Object{
        finalize : function(){ print("I'm dead");};
    };
    obj:{
        a =  + 'hh';//这里会抛异常,Xmas是不允许Integer和String进行“+”
    }
}

  其中【:{....}】语法,便是指:无论如何,obj的finalize函数都将被调用。这样对于稀缺的资源(如file、socket)在Xmas中有了可靠的释放保证。

  下面便是其对应的指令:

 : push String : finalize
 : push Function : lambda
 : push Function : Object
 : , paramNum : , self : false) #res
 : , $)
 : finalizer begin//finalize block 开始
 :
 : push String : hh
 : , $) #res
 : , $)
 : finalizer entry//finalizer 入口
 :
 : , $.finalize)
 : , paramNum : , self : true)
 : finalizer end//finalize block 结束
 : ret

  所以,还是字节码来的清晰;finalizer是语法糖和虚拟机同时支持的特性,也是费了自己一些些时间。

  当然,finalize函数自己再度抛出异常,并不会影响上一层的finalizer;只是原本的异常信息会被新的异常所覆盖;所打印出的调用堆栈,也将随之改变。

  七、还有很多

  Xmas是一门有完整的虚拟机支持的脚本;所以她也有一个垃圾收集器:一个基于标记的分代式GC。最初的是一个基于引用计数的简单的收集方案;但无赖,有一天我发现了【环引用】,所以被迫放弃的这个简单直接的方案;接着经过一个星期的努力,第一个基于节点复制算法的分代式GC,完成了。

  可惜的是,节点复制算法,需要迁移内存;其对于C++这样的语言,需要太多的控制策略才能够无障碍地使用(主要是函数调用栈和寄存器上的指针地址,需要修改);Xmas只能够在独立的线程中才能够安全的运行,线程间的数据交互需要通过第三个堆(全局堆)。

  所以,第三个基于标记的GC,并不需要迁移内存;所以,其并不需要那么多的系统级支持;依然能够狠开心在多线程间运行。

  其次,Xmas是默认支持尾递归调用优化;意味着,即使你不希望被优化,也毫无办法。所以,任何尾调用,都将变成迭代;值得一提的是,这个操作是在运行时,由虚拟机完成的,而非编译期;但其性能竟然和普通的循环一样(我表示很惊讶)。

  最后,本文,并非结束;后面的系列将针对上面的每个话题;额外地展开,所以,就这样了.......

脚本语言:Xmas(一)的相关教程结束。

《脚本语言:Xmas(一).doc》

下载本文的Word格式文档,以方便收藏与打印。