16 KiB
Nasal - Modern Interpreter
目录
如果有好的意见或建议,欢迎联系我们!
- lhk101lhk101@qq.com (ValKmjolnir)
- sidi.liang@gmail.com (Sidi)
简介
Nasal
是一款语法与 ECMAscript 相似的编程语言,并作为脚本语言被著名开源飞行模拟器 FlightGear 所使用。
该语言的设计者为 Andy Ross。
该解释器由 ValKmjolnir 使用 C++
(-std=c++17
)重新实现。非常感谢 Andy 为我们设计了这个神奇且简洁的编程语言: Andy Ross 的 nasal 解释器。
该项目旧版本使用 MIT 协议开源 (2019/7 ~ 2021/5/4 ~ 2023/5),从 2023/6 开始新版本使用 GPL v2 协议。
为什么重新写 Nasal 解释器?
2019 年夏天,FGPRC 的成员吐槽,在 Flightgear 中提供的 nasal 控制台中进行调试很不方便,仅仅是想检查语法错误,也要花时间打开软件等待加载进去后调试。所以我就写了一个新的解释器来帮助检查语法错误和运行时错误。
我编写了 nasal 的词法分析器和语法分析器,以及一个全新的字节码虚拟机,并用这个运行时来进行 nasal 程序的调试。我们发现使用这个解释器来检测语法和运行时错误极大的提高了效率。
你也可以使用这个语言来写一些与 Flightgear 运行环境无关的有趣的程序,并用这个解释器来执行。你也可以让解释器来调用你自己编写的模块,使它成为项目中一个非常有用的工具。
下载
现在支持下载预览版(Nightly Build)。 Windows 平台的预览版解释器现在还没配置相关流水线, 请耐心等候或者直接在本地编译。 我们提供了一份 Cmake 文件,可以很方便地在 Visual Studio 中编译:
编译
下载最新代码包编译,项目非常小巧, 没有使用任何第三方库,只需要这两样即可编译: C++ 编译器以及 make 程序。
Windows 平台 (MinGW-w64)
确保 thread model 是 posix thread model
, 否则没有 thread 库。
mingw32-make nasal.exe -j4
Windows 平台 (Vistual Studio)
项目提供了 CMakeLists.txt 用于在Visual Studio
中创建项目。
Linux / macOS / Unix 平台
make -j
你也可以通过如下的其中一行命令来指定你想要使用的编译器:
make nasal CXX=... -j
使用方法
如果你是 Windows
用户且想正常输出 unicode,可以这样开启 unicode 代码页:
if (os.platform()=="windows") {
system("chcp 65001");
}
或者使用 std.runtime.windows.set_utf8_output()
:
use std.runtime;
runtime.windows.set_utf8_output();
与andy解释器的不同之处
必须用 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"
堆栈追踪信息
当解释器崩溃时,它会反馈错误产生过程的堆栈追踪信息:
内置函数 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
或 --detail
后,trace 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 |
调试器
在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:
0x0003a8 07:00 00 00 00 00 00 00 00 pnil 0x0 (std/lib.nas:413)
0x0003a9 56:00 00 00 00 00 00 00 00 ret 0x0 (std/lib.nas:413)
0x0003aa 03:00 00 00 00 00 00 00 56 loadg 0x56 (std/lib.nas:413)
--> 0x0003ab 0b:00 00 00 00 00 00 03 af newf 0x3af (test/fib.nas:1)
0x0003ac 02:00 00 00 00 00 00 00 03 intl 0x3 (test/fib.nas:1)
0x0003ad 0d:00 00 00 00 00 00 00 22 para 0x22 (x) (test/fib.nas:1)
0x0003ae 3e:00 00 00 00 00 00 03 be jmp 0x3be (test/fib.nas:1)
0x0003af 45:00 00 00 00 00 00 00 01 calll 0x1 (test/fib.nas:2)
vm stack (0x7fca7e9f1010, limit 16, 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:
0x0003a8 07:00 00 00 00 00 00 00 00 pnil 0x0 (std/lib.nas:413)
0x0003a9 56:00 00 00 00 00 00 00 00 ret 0x0 (std/lib.nas:413)
0x0003aa 03:00 00 00 00 00 00 00 56 loadg 0x56 (std/lib.nas:413)
0x0003ab 0b:00 00 00 00 00 00 03 af newf 0x3af (test/fib.nas:1)
0x0003ac 02:00 00 00 00 00 00 00 03 intl 0x3 (test/fib.nas:1)
0x0003ad 0d:00 00 00 00 00 00 00 22 para 0x22 (x) (test/fib.nas:1)
0x0003ae 3e:00 00 00 00 00 00 03 be jmp 0x3be (test/fib.nas:1)
--> 0x0003af 45:00 00 00 00 00 00 00 01 calll 0x1 (test/fib.nas:2)
vm stack (0x7fca7e9f1010, limit 16, total 8)
0x000007 | pc | 0x3c7
0x000006 | addr | 0x0
0x000005 | nil |
0x000004 | nil |
0x000003 | num | 0
0x000002 | nil |
0x000001 | nil |
0x000000 | func | <0x5573f66ef5f0> func(elems...) {..}
>>
交互解释器
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(..) {..}}
>>>
Web 界面
现已提供基于 Web 的库以及示例界面,您可以直接在浏览器中编写和运行 Nasal 代码。该界面包括代码编辑器和交互式 REPL(未完成)。
Web 代码编辑器
- 语法高亮: 使用 CodeMirror 提供增强的编码体验。
- 错误高亮和格式化: 清晰显示语法和运行时错误。
- 示例程序: 预加载的示例,帮助您快速上手。
- 执行时间显示选项: 可选择查看代码执行所需时间。
- 可配置的执行时间限制: 设置时间限制以防止代码长时间运行。
- 提示: 在线解释器的安全性尚未得到广泛测试,建议配合沙盒机制等安全措施使用。
Web REPL
- 重要提示: REPL 中的代码执行时间限制尚未正确实现。此 REPL 库目前不稳定,请勿在生产环境中使用。
- 交互式命令行界面: 在浏览器中体验熟悉的 REPL 环境。
- 多行输入支持: 使用
>>>
和...
提示符无缝输入多行代码。 - 命令历史导航: 使用箭头键轻松浏览命令历史。
- 格式化的错误处理: 接收清晰且格式化的错误消息,助力调试。
- 快速测试的示例代码片段: 访问并运行示例代码片段,快速测试功能。
运行 Web 界面
-
构建 Nasal 共享库:
cmake -DBUILD_SHARED_LIBS=ON . make nasal-web
-
设置并运行 Web 应用:
代码编辑器:
cd nasal-web-app npm install node server.js
在浏览器中访问
http://127.0.0.1:3000/
以使用代码编辑器。REPL:
cd nasal-web-app npm install node server_repl.js
在浏览器中访问
http://127.0.0.1:3001/repl.html
以使用 REPL 界面。