Nasal-Interpreter/doc/README_zh.md

32 KiB
Raw Blame History

Nasal - Modern Interpreter

GitHub code size GitHub release(latest by date) in dev license

这篇文档包含多语言版本: 中文 | English

目录

如果有好的意见或建议,欢迎联系我们!

简介

Nasal 是一款语法与ECMAscript相似的编程语言并作为运行脚本被著名开源飞行模拟器 FlightGear 所使用。 该语言的设计者为 Andy Ross

该解释器项目由 ValKmjolnir 完全使用 C++(-std=c++17)重新实现,没有复用 Andy Ross的nasal解释器 中的任何一行代码。尽管没有参考任何代码我们依然非常感谢Andy为我们带来了这样一个神奇且简洁的编程语言。

该项目使用 MIT 协议开源 (2019/7 ~ 2021/5/4 ~ 2023/5),从 2023/6 开始使用 GPL v2 协议。

我们为什么想要重新写一个nasal解释器?

2019年暑假FGPRC 的成员告诉我在Flightgear中提供的nasal控制台窗口中进行调试很不方便仅仅是想检查语法错误也得花时间打开软件等待加载进去后进行调试。所以我就写了一个全新的解释器来帮助他们检查语法错误以及运行时错误。

我编写了nasal的词法分析器和语法分析器以及一个全新的字节码虚拟机并用这个运行时来进行nasal程序的调试。我们发现使用这个解释器来检测语法和运行时错误极大的提高了效率。

你也可以使用这个语言来写一些与Flightgear运行环境无关的有趣的程序并用这个解释器来执行。你也可以让解释器来调用你自己编写的模块使它成为项目中一个非常有用的工具。

编译

windows macOS linux g++ clang++ vs

我们推荐你下载最新代码包编译,这个项目非常小巧,没有使用任何第三方库,因此编译起来非常轻松, 你只需要这两样东西: C++ 编译器以及make程序。

注意: 如果你想直接下载发行版提供的zip/tar.gz压缩包来构建这个解释器在下载之前请阅读发行日志以保证这个发行版的文件中不包含非常严重的bug。

Windows 平台(MinGW-w64)

一定要确保您的 MinGW thread model 是 posix thread model, 否则可能存在没有 thread 库的问题。

mkdir build

mingw32-make nasal.exe -j4

Windows 平台(Vistual Studio)

项目提供了 CMakeLists.txt 用于在Visual Studio中用这种方式来创建项目。

Linux/macOS/Unix 平台

mkdir build

make -j4

你也可以通过如下的其中一行命令来指定你想要使用的编译器:

make nasal CXX=...

使用方法

usage

如果你是 Windows 用户且想正常输出unicode在nasal代码里写这个来开启unicode代码页:

if (os.platform()=="windows") {
    system("chcp 65001");
}

教程

Nasal是非常容易上手的你甚至可以在15分钟之内看完这里的基本教程并且直接开始编写你想要的程序。 如果你先前已经是C/C++, javascript选手那么几乎可以不用看这个教程…… 在看完该教程之后,基本上你就完全掌握了这个语言:

基本类型

none 是特殊的错误类型。这个类型用于终止虚拟机的执行,该类型只能由虚拟机在抛出错误时产生。

nil 是空类型。类似于null。

var spc = nil;

num 有三种形式:十进制十六进制以及八进制。并且该类型使用IEEE754标准的浮点数double格式来存储。

# 该语言用 '#' 来作为注释的开头
var n = 2.71828;    # dec 十进制
var n = 2.147e16;   # dec 十进制
var n = 1e-10;      # dec 十进制
var n = 0xAA55;     # hex 十六进制
var n = 0o170001;   # oct 八进制

# 注意: true  false 关键字在现在的 nasal 里也是可用的
var n = true;       # n 实际上是数字 1.0
var n = false;      # n 实际上是数字 0.0

str 也有三种不同的格式。第三种只允许包含一个的字符。

var s = 'str';
var s = "another string";
var s = `c`;
# 该语言也支持一些特别的转义字符:
'\a'; '\b'; '\e'; '\f';
'\n'; '\r'; '\t'; '\v';
'\0'; '\\'; '\?'; '\'';
'\"';

vec 有不受限制的长度并且可以存储所有类型的数据。(当然不能超过可分配内存空间的长度)

var vec = [];
var vec = [0, nil, {}, [], func(){return 0}];
append(vec, 0, 1, 2);

hash 使用哈希表 (类似于python中的dict)通过键值对来存储数据。key可以是一个字符串也可以是一个标识符。

var hash = {
    member1: nil,
    member2: "str",
    "member3": "member\'s name can also be a string constant",
    funct: func() {
        return me.member2~me.member3;
    }
};

func 函数类型。(实际上在这个语言里函数是一种lambda表达式)

var f = func(x, y, z) {
    return nil;
}
# 函数声明可以没有参数列表以及 `(`, `)`
var f = func {
    return 114514;
}
var f = func(x, y, z, deft = 1) {
    return x+y+z+deft;
}
var f = func(args...) {
    var sum = 0;
    foreach(var i; args) {
        sum += i;
    }
    return sum;
}

upval 是存储闭包数据的特殊类型, 在 vm 中使用,以确保闭包功能正常。

ghost 是用来存储C/C++的一些复杂数据结构。这种类型的数据由内置函数生成。如果想为nasal添加新的数据结构, 可以看下文如何通过修改本项目来添加内置函数。

运算符

Nasal拥有基本的四种数学运算符 + - * /以及一个特别的运算符 ~,用于拼接字符串。

1+2-(1+3)*(2+4)/(16-9);
"str1"~"str2";

对于条件语句,可以使用== != < > <= >=来比较数据。and or 与C/C++中 && ||运算符一致。

1+1 and (1<0 or 1>0);
1<=0 and 1>=0;
1==0 or 1!=0;

单目运算符- !与C/C++中的运算符功能类似。

-1;
!0;

位运算符~ | & ^与C/C++中的运算符功能类似。

# 运行过程:
# 1.  f64 强转为 i32 (static_cast<int32_t>)
# 2. 执行位运算符

~0x80000000; # 按位取反 2147483647
0x8|0x1;     # 按位或
0x1&0x2;     # 按位与
0x8^0x1;     # 按位异或

赋值运算符= += -= *= /= ~= ^= &= |=正如其名,用于进行赋值。

a = b = c = d = 1;
a += 1;
a -= 1;
a *= 1;
a /= 1;
a ~= "string";

a ^= 0xff;
a &= 0xca;
a |= 0xba;
定义变量

如下所示。

var a = 1;             # 定义单个变量
var (a, b, c) = [0, 1, 2]; # 从数组中初始化多个变量
var (a, b, c) = (0, 1, 2); # 从元组中初始化多个变量

Nasal 有很多特别的全局变量:

globals; # 包含所有全局声明变量名和对应数据的哈希表
arg;     # 在全局作用域arg 是包含命令行参数的数组
         # 在局部作用域arg 是函数调用时的动态参数数组

具体实例:

var a = 1;
println(globals); # 输出 {a:1}
# nasal a b c
println(arg); # 输出 ["a", "b", "c"]

func() {
    println(arg);
}(1, 2, 3);   # 输出 [1, 2, 3]
多变量赋值

最后这个语句通常用于交换两个变量的数据类似于Python中的操作。

(a, b[0], c.d) = [0, 1, 2];
(a, b[1], c.e) = (0, 1, 2);
(a, b) = (b, a);
条件语句

nasal在提供else if的同时还有另外一个关键字elsif。该关键字与else if有相同的功能。

if (1) {
    ;
} elsif (2) {
    ;
} else if (3) {
    ;
} else {
    ;
}
循环语句

while循环和for循环大体上与C/C++是一致的。

while(condition) {
    continue;
}
for(var i = 0; i<10; i += 1) {
    break;
}

同时nasal还有另外两种直接遍历列表的循环方式:

forindex 会获取列表的下标,依次递增. 下标会从0递增到size(elem)-1结束。

forindex(var i; elem) {
    print(elem[i]);
}

foreach会依次直接获取列表中的数据. 这些数据会从elem[0]依次获取到elem[size(elem)-1].

foreach(var i; elem) {
    print(i);
}
生成子列表(subvec)

nasal提供了下面第一句的类似语法来从列表中随机或者按照一个区间获取数据并且拼接生成一个新的列表。当然如果中括号内只有一个下标的话你会直接获得这个下标对应的数据而不是一个子列表。如果直接对string使用下标来获取内容的话会得到对应字符的 ascii值。如果你想进一步获得这个字符串,可以尝试使用内置函数chr()

a[0];
a[-1, 1, 0:2, 0:, :3, :, nil:8, 3:nil, nil:nil];
"hello world"[0];
特殊函数调用语法

这种调用方式不是很高效,因为哈希表会使用字符串比对来找到数据存放的位置。

然而如果它用起来非常舒适,那效率也显得不是非常重要了……

f(x:0, y:nil, z:[]);
lambda表达式

函数有这样一种直接编写函数体并且立即调用的方式:

func(x, y) {
    return x+y;
}(0, 1);
func(x) {
    return 1/(1+math.exp(-x));
}(0.5);

测试文件中有一个非常有趣的文件y-combinator.nas,可以试一试:

var fib = func(f) {
    return f(f);
}(
    func(f) {
        return func(x) {
            if(x<2) return x;
            return f(f)(x-1)+f(f)(x-2);
        }
    }
);
闭包

闭包是一种特别的作用域,你可以从这个作用域中获取其保存的所有变量, 而这些变量原本不是你当前运行的函数的局部作用域中的。 下面这个例子里,结果是1:

var f = func() {
    var a = 1;
    return func() {return a;};
}
print(f()());

如果善用闭包,你可以使用它来进行面向对象编程。

var student = func(n, a) {
    var (name, age) = (n, a);
    return {
        print_info: func()  {println(name, ' ', age);},
        set_age:    func(a) {age = a;},
        get_age:    func()  {return age;},
        set_name:   func(n) {name = n;},
        get_name:   func()  {return name;}
    };
}
特性与继承

当然,也有另外一种办法来面向对象编程,那就是利用trait

当一个hash类型中有一个成员的key是parents,并且该成员是一个数组的话, 那么当你试图从这个hash中寻找一个它自己没有的成员名时虚拟机会进一步搜索parents数组。 如果该数组中有一个hash类型有一个成员的key与当前你搜索的成员名一致 那么你会得到这个成员对应的值。

使用这个机制,我们可以进行面向对象编程,下面样例的结果是114514:

var trait = {
    get: func {return me.val;},
    set: func(x) {me.val = x;}
};

var class = {
    new: func() {
        return {
            val: nil,
            parents: [trait]
        };
    }
};
var a = class.new();
a.set(114514);
println(a.get());

首先虚拟机会发现在a中找不到成员set,但是在a.parents中有个hash类型trait存在该成员,所以返回了这个成员的值。 成员me指向的是a自身,类似于一些语言中的this,所以我们通过这个函数,实际上修改了a.valget函数的调用实际上也经过了相同的过程。

不过我们必须提醒你一点如果你在这个地方使用该优化来减少hash的搜索开销:

var trait = {
    get: func {return me.val;},
    set: func(x) {me.val = x;}
};

var class = {
    new: func() {
        return {
            val: nil,
            parents: [trait]
        };
    }
};
var a = class.new();
var b = class.new();
a.set(114);
b.set(514);
println(a.get());
println(b.get());

var c = a.get;
var d = b.get;

println(c());
println(c());
println(d());
println(d());

那么你会发现现在虚拟机会输出这个结果:

114
514
514
514
514
514

因为执行a.get时在trait.get函数的属性中进行了me=a的操作。而b.get则执行了me=b的操作。所以在运行var d=b.get后实际上c也变成b.get了。 如果你想要用这种小技巧来让程序运行更高效的话,最好是要知道这里存在这样一个机制。

原生内置函数以及模块导入(import)语法

这个部分对于纯粹的使用者来说是不需要了解的, 它将告诉你我们是如何为解释器添加新的内置函数的。 如果你对此很感兴趣,那么这个部分可能会帮到你,并且……

警告: 如果你 不想 通过直接修改解释器源码来添加你自定义的函数,那么你应该看下一个节 模块 的内容。

如果你确实是想修改源码来搞一个自己私人订制的解释器 ———— “我他妈就是想自己私人订制,你们他妈的管得着吗?”, 参考源码中关于内置函数的部分,以及lib.nas中是如何包装这些函数的,下面是其中一个样例:

定义新的内置函数:

// 你可以使用这个宏来直接定义一个新的内置函数
var builtin_print(context*, gc*);

然后用C++完成这个函数的函数体:

var builtin_print(context* ctx, gc* ngc) {
    // 局部变量的下标其实是从 1 开始的
    // 因为 local[0] 是保留给 'me' 的空间
    for(auto& i : ctx->localr[1].vec().elems) {
        std::cout << i;
    }
    std::cout << std::flush;
    // 最后生成返回值,返回值必须是一个内置的类型,
    // 可以使用ngc::alloc(type)来申请一个需要内存管理的复杂数据结构
    // 或者用我们已经定义好的nil/one/zero这些可以直接使用
    return nil;
}

当运行内置函数的时候内存分配器如果运行超过一次那么会有更大可能性多次触发垃圾收集器的mark-sweep。这个操作会在gc::alloc中触发。 如果先前获取的数值没有被正确存到可以被垃圾收集器索引到的地方,那么它会被错误地回收,这会导致严重的错误。

可以使用gc::temp来暂时存储一个会被返回的需要gc管理的变量这样可以防止内部所有的申请错误触发垃圾回收。如下所示

var builtin_keys(context* ctx, gc* ngc) {
    auto hash = ctx->localr[1];
    if (hash.type!=vm_hash && hash.type!=vm_map) {
        return nas_err("keys", "\"hash\" must be hash");
    }
    // 使用gc.temp来存储gc管理的变量防止错误的回收
    auto res = ngc->temp = ngc->alloc(vm_vec);
    auto& vec = res.vec().elems;
    if (hash.type==vm_hash) {
        for(const auto& iter : hash.hash().elems) {
            vec.push_back(ngc->newstr(iter.first));
        }
    } else {
        for(const auto& iter : hash.map().mapper) {
            vec.push_back(ngc->newstr(iter.first));
        }
    }
    ngc->temp = nil;
    return res;
}

这些工作都完成之后在内置函数注册表中填写它在nasal中的别名并且在表中填对这个函数的函数指针:

nasal_builtin_table builtin[] = {
    {"__print", builtin_print},
    {nullptr,  nullptr}
};

最后将其包装到nasal文件中:

var print = func(elems...) {
    return __print(elems);
};

事实上__print后面跟着的传参列表不是必须要写的。所以这样写也对:

var print = func(elems...) {
    return __print;
};

如果你不把内置函数包装到一个普通的nasal函数中那么直接调用这个内置函数会在参数传入阶段出现 segmentation fault(段错误)

在nasal文件中使用import("文件名.nas")可以导入该文件中你包装的所有内置函数,接下来你就可以使用他们了。 当然也有另外一种办法来导入这些nasal文件下面两种导入方式的效果是一样的

use dirname.dirname.filename;
import("./dirname/dirname/filename.nas");
模块(开发者教程)

如果只有上文中那种方式来添加你自定义的函数到nasal中这肯定是非常麻烦的。 因此,我们实现了一组实用的内置函数来帮助你添加你自己创建的模块。

用于加载动态库的函数在std/dylib.nas中:

var dlopen = func(libname) {
    ...
}

var dlclose = func(lib) {
    ...
}

var dlcall = func(ptr, args...) {
    ...
}

var limitcall = func(arg_size = 0) {
    ...
}

这些函数是用来加载动态库的这样nasal解释器可以根据用户需求灵活加载动态库来执行。让我们看看这些函数该如何使用。

首先用C++写个项目,并且编译成动态库。我们就拿fib.cpp作为例子来说明(样例代码可以在./module中找到):

// 这个头文件得加上因为我们需要拿到nasal的api
#include "nasal.h"
double fibonaci(double x) {
    if (x<=2) {
        return x;
    }
    return fibonaci(x-1)+fibonaci(x-2);
}

// 模块函数的参数列表一律以这个为准
var fib(var* args, usize size, gc* ngc) {
    if (!size) {
        return nas_err("fib", "lack arguments");
    }
    // 传参会给予一个var指针指向一个vm_vec的data()
    var num = args[0];
    // 如果你想让这个函数有更强的稳定性,那么一定要进行合法性检查
    // nas_err会输出错误信息并返回错误类型让虚拟机终止执行
    if(num.type!=vm_num) {
        return nas_err("extern_fib", "\"num\" must be number");
    }
    // vm_num作为普通的数字类型不是内存管理的对象所以无需申请
    // 如果需要返回内存管理的对象请使用ngc->alloc(type)
    return var::num(fibonaci(num.tonum()));
}

// 然后将函数名字和函数地址放到一个表里,一定要记住表尾是{nullptr,nullptr}
module_func_info func_tbl[] = {
    {"fib", fib},
    {nullptr, nullptr}
};

// 必须实现这个函数, 这样nasal可以通过字符串名字获得函数指针
// 之所以用这种方式来获取函数指针, 是因为`var`是有构造函数的
// 有构造函数的类型作为返回值, 和C是不兼容的, 这导致
// 类似 "extern "C" var fib" 的写法会得到编译错误
extern "C" module_func_info* get() {
    return func_tbl;
}

接着我们把fib.cpp编译成动态库。

Linux(.so):

clang++ -c -O3 fib.cpp -fPIC -o fib.o

clang++ -shared -o libfib.so fib.o

Mac(.so & .dylib): 和Linux下操作相同。

Windows(.dll):

g++ -c -O3 fib.cpp -fPIC -o fib.o

g++ -shared -o libfib.dll fib.o

好了那么我们可以写一个测试用的nasal代码来运行这个斐波那契函数了。 下面例子中os.platform()是用来检测当前运行的系统环境的,这样可以实现跨平台:

use std.dylib;
var dlhandle = dylib.dlopen("libfib."~(os.platform()=="windows"?"dll":"so"));
var fib = dlhandle.fib;
for(var i = 1; i<30; i += 1)
    println(dylib.dlcall(fib, i));
dylib.dlclose(dlhandle.lib);

dylib.dlopen用于加载动态库并从动态库中获得函数地址。

dylib.dlcall用于调用函数,第一个参数是动态库函数的地址,这是个特殊类型,一定要保证这个参数是vm_obj类型并且type=obj_extern

dylib.dlclose用于卸载动态库,当然,在这个函数调用之后,所有从该库中获取的函数都作废。

dylib.limitcall用于获取使用固定长度传参的 dlcall 函数,这种函数可以提高你的程序运行效率,因为它不需要用 vm_vec 来存储传入参数,而是使用局部作用域来直接存储,从而避免了频繁调用可能导致的频繁垃圾收集。所以上面展示的代码同样可以这样写:

use std.dylib;
var dlhandle = dylib.dlopen("libfib."~(os.platform()=="windows"?"dll":"so"));
var fib = dlhandle.fib;
var invoke = dylib.limitcall(1); # this means the called function has only one parameter
for(var i = 1; i<30; i += 1)
    println(invoke(fib, i));
dylib.dlclose(dlhandle.lib);

如果得到如下运行结果,恭喜你!

./nasal a.nas
1
2 
3 
5 
8 
13
21
34
55
89
144
233
377
610
987
1597
2584
4181
6765
10946
17711
28657
46368
75025
121393
196418
317811
514229
832040
自定义类型(开发者教程)

创建一个自定义类型很容易。下面是使用示例:

const auto ghost_for_test = "ghost_for_test";

// 声明自定义类型的析构函数
void ghost_for_test_destructor(void* ptr) {
    std::cout << "ghost_for_test::destructor (0x";
    std::cout << std::hex << reinterpret_cast<u64>(ptr) << std::dec << ") {\n";
    delete static_cast<u32*>(ptr);
    std::cout << "    delete 0x" << std::hex;
    std::cout << reinterpret_cast<u64>(ptr) << std::dec << ";\n";
    std::cout << "}\n";
}

var create_new_ghost(var* args, usize size, gc* ngc) {
    var res = ngc->alloc(vm_obj);
    // 创建自定义类型
    res.ghost().set(ghost_for_test, ghost_for_test_destructor, new u32);
    return res;
}

var set_new_ghost(var* args, usize size, gc* ngc) {
    var res = args[0];
    if (!res.object_check(ghost_for_test)) {
        std::cout << "set_new_ghost: not ghost for test type.\n";
        return nil;
    }
    f64 num = args[1].num();
    *(reinterpret_cast<u32*>(res.ghost().pointer)) = static_cast<u32>(num);
    std::cout << "set_new_ghost: successfully set ghost = " << num << "\n";
    return nil;
}

var print_new_ghost(var* args, usize size, gc* ngc) {
    var res = args[0];
    // 用自定义类型的名字来检查是否是正确的自定义类型
    if (!res.object_check(ghost_for_test)) {
        std::cout << "print_new_ghost: not ghost for test type.\n";
        return nil;
    }
    std::cout << "print_new_ghost: " << res.ghost() << " result = "
              << *((u32*)res.ghost().pointer) << "\n";
    return nil;
}

我们使用下面这个函数来创建一个自定义类型:

void nas_ghost::set(const std::string&, nasal::nas_ghost::destructor, void*);

const std::string& 是自定义类型的类型名。

nasal::nas_ghost::destructor 是自定义类型的析构函数指针。

void* 是指向自定义类型实例的指针。

我们使用下面的这个函数检测是否是正确的自定义类型:

bool var::object_check(const std::string&);

参数是自定义类型的类型名。

与andy解释器的不同之处

error

必须用 var 定义变量

这个解释器使用了更加严格的语法检查来保证你可以更轻松地debug。这是非常有必要的严格否则debug会非常痛苦。 同样的flightgear 内置的 nasal 解释器也采取了类似的措施,所以使用变量前务必用 var 先进行声明。

在Andy的解释器中:

foreach(i; [0, 1, 2, 3])
    print(i)

这个程序可以正常运行。然而这个i标识符实际上在这里是被第一次定义,而且没有使用var。我认为这样的设计很容易让使用者迷惑。他们可能都没有发现这里实际上是第一次定义i的地方。没有使用var的定义会让程序员认为这个i也许是在别的地方定义的。

所以在这个解释器中,我直接使用严格的语法检查方法来强行要求用户必须要使用var来定义新的变量或者迭代器。如果你忘了加这个关键字,那么你就会得到这个:

code: undefined symbol "i"
 --> test.nas:1:9
  | 
1 | foreach(i; [0, 1, 2, 3])
  |         ^ undefined symbol "i"

code: undefined symbol "i"
 --> test.nas:2:11
  | 
2 |     print(i)
  |           ^ undefined symbol "i"

堆栈追踪信息

stackoverflow

当解释器崩溃时,它会反馈错误产生过程的堆栈追踪信息:

内置函数 die

die函数用于直接抛出错误并终止执行。

func() {
    println("hello");
    die("error occurred this line");
    return;
}();
hello
[vm] error: error occurred this line
[vm] error: error occurred in native function

call trace (main)
  call func@0x557513935710() {entry: 0x850}

trace back (main)
  0x000547     4c 00 00 16     callb   0x16 <__die@0x557512441780>(std/lib.nas:150)
  0x000856     4a 00 00 01     callfv  0x1(a.nas:3)
  0x00085a     4a 00 00 00     callfv  0x0(a.nas:5)

stack (0x5575138e8c40, limit 10, total 14)
  0x00000d    | null |
  0x00000c    | pc   | 0x856
  0x00000b    | addr | 0x5575138e8c50
  0x00000a    | nil  |
  0x000009    | nil  |
  0x000008    | str  | <0x5575138d9190> error occurred t...
  0x000007    | nil  |
  0x000006    | func | <0x5575139356f0> entry:0x850
  0x000005    | pc   | 0x85a
  0x000004    | addr | 0x0
栈溢出

这是一个会导致栈溢出的例子:

func(f) {
    return f(f);
}(
    func(f) {
        f(f);
    }
)();
[vm] error: stack overflow

call trace (main)
  call func@0x564106058620(f) {entry: 0x859}
   --> 583 same call(s)
  call func@0x5641060586c0(f) {entry: 0x851}

trace back (main)
  0x000859     45 00 00 01     calll   0x1(a.nas:5)
  0x00085b     4a 00 00 01     callfv  0x1(a.nas:5)
  0x00085b     582 same call(s)
  0x000853     4a 00 00 01     callfv  0x1(a.nas:2)
  0x00085f     4a 00 00 01     callfv  0x1(a.nas:3)

stack (0x56410600be00, limit 10, total 4096)
  0x000fff    | func | <0x564106058600> entry:0x859
  0x000ffe    | pc   | 0x85b
  0x000ffd    | addr | 0x56410601bd20
  0x000ffc    | nil  |
  0x000ffb    | nil  |
  0x000ffa    | func | <0x564106058600> entry:0x859
  0x000ff9    | nil  |
  0x000ff8    | func | <0x564106058600> entry:0x859
  0x000ff7    | pc   | 0x85b
  0x000ff6    | addr | 0x56410601bcb0
运行时错误

如果在执行的时候出现错误,程序会直接终止执行:

func() {
    return 0;
}()[1];
[vm] error: must call a vector/hash/string but get number

trace back (main)
  0x000854     47 00 00 00     callv   0x0(a.nas:3)

stack (0x564993f462b0, limit 10, total 1)
  0x000000    | num  | 0
详细的崩溃信息

使用命令 -d--detailtrace back信息会包含更多的细节内容:

hello
[vm] error: error occurred this line
[vm] error: error occurred in native function

call trace (main)
  call func@0x55dcb5b8fbf0() {entry: 0x850}

trace back (main)
  0x000547     4c 00 00 16     callb   0x16 <__die@0x55dcb3c41780>(std/lib.nas:150)
  0x000856     4a 00 00 01     callfv  0x1(a.nas:3)
  0x00085a     4a 00 00 00     callfv  0x0(a.nas:5)

stack (0x55dcb5b43120, limit 10, total 14)
  0x00000d    | null |
  0x00000c    | pc   | 0x856
  0x00000b    | addr | 0x55dcb5b43130
  0x00000a    | nil  |
  0x000009    | nil  |
  0x000008    | str  | <0x55dcb5b33670> error occurred t...
  0x000007    | nil  |
  0x000006    | func | <0x55dcb5b8fbd0> entry:0x850
  0x000005    | pc   | 0x85a
  0x000004    | addr | 0x0

registers (main)
  [pc    ]    | pc   | 0x547
  [global]    | addr | 0x55dcb5b53130
  [local ]    | addr | 0x55dcb5b43190
  [memr  ]    | addr | 0x0
  [canary]    | addr | 0x55dcb5b53110
  [top   ]    | addr | 0x55dcb5b431f0
  [funcr ]    | func | <0x55dcb5b65620> entry:0x547
  [upval ]    | nil  |

global (0x55dcb5b53130)
  0x000000    | nmspc| <0x55dcb5b33780> namespace [95 val]
  0x000001    | vec  | <0x55dcb5b64c20> [0 val]
  ...
  0x00005e    | func | <0x55dcb5b8fc70> entry:0x846

local (0x55dcb5b43190 <+7>)
  0x000000    | nil  |
  0x000001    | str  | <0x55dcb5b33670> error occurred t...
  0x000002    | nil  |

调试器

dbg

v8.0版本中我们添加了调试器。 使用这个命令./nasal -dbg xxx.nas来启用调试器,接下来调试器会打开文件并输出以下内容:

展开
source code:
--> var fib=func(x)
    {
        if(x<2) return x;
        return fib(x-1)+fib(x-2);
    }
    for(var i=0;i<31;i+=1)
        print(fib(i),'\n');


next bytecode:
    0x000848     4a 00 00 01     callfv  0x1(std/lib.nas:427)
    0x000849     3d 00 00 00     pop     0x0(std/lib.nas:427)
    0x00084a     07 00 00 00     pnil    0x0(std/lib.nas:423)
    0x00084b     56 00 00 00     ret     0x0(std/lib.nas:423)
    0x00084c     03 00 00 5e     loadg   0x5e(std/lib.nas:423)
--> 0x00084d     0b 00 08 51     newf    0x851(test/fib.nas:1)
    0x00084e     02 00 00 03     intl    0x3(test/fib.nas:1)
    0x00084f     0d 00 00 08     para    0x8 (x)(test/fib.nas:1)

stack (0x55ccd0a1b9d0, limit 10, total 0)
>>

如果需要查看命令的使用方法,可以输入h获取帮助信息。

当运行调试器的时候,你可以看到现在的操作数栈上到底有些什么数据。 这些信息可以帮助你调试,同时也可以帮助你理解这个虚拟机是如何工作的:

展开
source code:
    var fib=func(x)
    {
-->     if(x<2) return x;
        return fib(x-1)+fib(x-2);
    }
    for(var i=0;i<31;i+=1)
        print(fib(i),'\n');


next bytecode:
    0x000850     3e 00 08 60     jmp     0x860(test/fib.nas:1)
--> 0x000851     45 00 00 01     calll   0x1(test/fib.nas:3)
    0x000852     39 00 00 07     lessc   0x7 (2)(test/fib.nas:3)
    0x000853     40 00 08 56     jf      0x856(test/fib.nas:3)
    0x000854     45 00 00 01     calll   0x1(test/fib.nas:3)
    0x000855     56 00 00 00     ret     0x0(test/fib.nas:3)
    0x000856     44 00 00 5f     callg   0x5f(test/fib.nas:4)
    0x000857     45 00 00 01     calll   0x1(test/fib.nas:4)

stack (0x55ccd0a1b9d0, limit 10, total 8)
  0x000007    | pc   | 0x869
  0x000006    | addr | 0x0
  0x000005    | nil  |
  0x000004    | nil  |
  0x000003    | num  | 0
  0x000002    | nil  |
  0x000001    | nil  |
  0x000000    | func | <0x55ccd0a58fa0> entry:0x487
>>

交互解释器

v11.0 版本新增了交互式解释器 (REPL),使用如下命令开启:

nasal -r

接下来就可以随便玩了~

[nasal-repl] Initializating enviroment...
[nasal-repl] Initialization complete.

Nasal REPL interpreter version 11.0 (Oct  7 2023 17:28:31)
.h, .help   | show help
.e, .exit   | quit the REPL
.q, .quit   | quit the REPL
.c, .clear  | clear the screen
.s, .source | show source code

>>>

试试引入 std/json.nas 模块 ~

[nasal-repl] Initializating enviroment...
[nasal-repl] Initialization complete.

Nasal REPL interpreter version 11.1 (Nov  1 2023 23:37:30)
.h, .help   | show help
.e, .exit   | quit the REPL
.q, .quit   | quit the REPL
.c, .clear  | clear the screen
.s, .source | show source code

>>> use std.json;
{stringify:func(..) {..},parse:func(..) {..}}

>>>