前段时间搞了个替代语言用来生成 Javascript, 暂定名叫 Stekinscript. 它支持缩进语法, 类似 Python 跟 CoffeeScript, 不过稍有些不一样. 先给个小例子, 如下面一段代码是计算 fibonacci 数的函数
fib: (n):
if n < 2
return 1
return fib(n - 1) + fib(n - 2)
这段代码将被翻译成类似如下的 Javascript 代码
var fib = function(n) {
if (n < 2) {
return 1;
}
return fib(n - 1) + fib(n - 2);
};
其中 fib: xxxxxxx 这是个定义语句, 定义 fib 的值为冒号之后的表达式, 而冒号之后的表达式
(n):
if n < 2
return 1
return fib(n - 1) + fib(n - 2)
是个匿名函数, 函数体的缩进要相对于匿名函数头 (n): 所在的语句多出一级; 另外想必大家能看出来分支语句条件成立时的那个 return 1 因为多一级缩进的缘故是分支的从句.
另外下面是一个 setTimeout 的例子
setTimeout(():
console.log(0)
console.log(1)
, 2000) # 这半句缩进较上两句都少, 表示它不属于上面匿名函数的函数体, 而是更前一句
等价于如下 Js 代码
setTimeout(function() {
console.log(0);
console.log(1);
}, 2000)
去异步调用
弄这个的目的并不是山寨出个蓝山脚本或者摩卡脚本什么的编译原理大作业. Js 真正表达很弱的地方并不在于是否使用类 C 的分号-大括号语法 (虽然这语法我很讨厌), 而是在于异步这个大坑. 比如读个文件要这么来
fs.readFile('/etc/passwd', function(err, data) {
if (err) {
return console.error(err);
}
console.log(data.toString());
});
等价的 Stekinscript 代码传统上可以是这样
fs.readFile('/etc/passwd', (err, data):
if err
return console.error(err)
console.log(data.toString())
)
不过 Stekinscript 还提供了另外一种写法, 如下
fs.readFile('/etc/passwd', %(err, data))
if err
return console.error(err)
console.log(data.toString())
注意到上面 fs.readFile 调用原本接受一个回调函数参数的地方被填入了 %(err, data) 这样一个实参, 它叫做异步占位符, 可以作为函数调用的参数传入 (也必须当作实参, 而不能单独写出或者进行运算; 一次函数调用的实参中也只能有一个异步占位符). 有了这个占位符之后, 不需要继续写回调函数的函数体了 (这样也就不用进一步缩进了, 因此写代码自然不会写出个坑来), 然后代码编译的过程将发生一些变化, 从这个带有异步占位符的函数调用之后, 代码块中接下来的部分将全部归入异步占位符所代表的回调函数的函数体. 这对于那些最后一句是一个长长的异步调用的语句块来说是非常适用的, 它会让异步代码至少看起来像是同步代码一样. 再比如下面这个例子, 读取文件中前 32 个字节
fs.open('/etc/passwd', 'r', %(err0, fd)) # 第 1 个异步占位符
if err0
return console.error(err0)
fs.read(fd, *Buffer(32), 0, 32, 0, %(err1, bytes, buffer)) # 第 2 个异步占位符
if err1
fs.close(fd)
return console.error(err1)
console.log(buffer.toString())
fs.close(fd)
等价于如下 Js 代码
fs: require('fs');
fs.open('/etc/passwd', 'r', function(err0, fd) {
if (err0) {
return console.error(err0);
}
fs.read(fd, new Buffer(32), 0, 32, 0, function(err1, bytes, buffer) {
if (err1) {
fs.close(fd);
return console.error(err1);
}
console.log(buffer.toString());
fs.close(fd);
});
});
管道
循环替代
循环什么的跟异步调用实在是八字不合. 在 Stekinscript 里并不提供通常意义上的循环, 而是类似 Shell 那样使用类似管道的方式来处理循环, 如
list: [1, 1, 2, 3, 5, 8]
sqrList: list |: $ * $
不过因为 | 表示位或运算的缘故, 所以用了这样个运算符 |: 表示对逐个元素映射; $ 表示每个列表中的元素 (因为 $ 是特殊符号的缘故, 在 Stekinscript 中使用 jQuery 时要用 jQuery 这个函数名). list |: $ * $ 表示将列表中每个元素平方之后构成一个新列表, 即 sqrList 的值将是 [1, 1, 4, 9, 25, 64].
管道还有一种多行形式. 如果在管道操作符 |: 之后立即折行, 并且接下来的一组语句缩进都多于管道表达式所在行的缩进, 那么这些语句会被认为是管道操作符右侧的部分 (相当于传统意义上的循环体). 如
list: [1, 1, 2, 3, 5, 8]
list2: list |:
sqr: $ * $
if sqr % 7 = 1 # 一个等号表示相等比较
return sqr # 在管道中的 return 语句表示将 return 的结果添加到结果列表中
# 因此结果列表中将包含所有平方后模 7 余 1 的那些平方数
console.log(list2) # 即此句会输出 [1, 1, 64]
管道中的异步调用
如果管道中含有异步调用, 那么管道会被为翻译为异步形式. 如
fileList: ['fileA', 'fileB', 'fileC']
filesContent: fileList |:
fs.readFile($, %(err, data)) # 异步的文件读取
if err
console.error(err)
return null
return data.toString() # 如果没什么问题就把文件内容加到结果列表中
console.log(filesContent)
这表示从 fileA, fileB, fileC 三个文件中依次读取文件内容, 然后将这些内容组成一个新列表.「依次」的意思是下一个文件的读取会在前一个文件读取完毕后执行回调后再进行, 而最后的 console.log(filesContent) 则会在最后一次文件读取之后才执行.
求调教
目前给出的这个是个预览版, 近一两个月还会有进一步改动 (包括但不限于增减保留字, 语法语义修改). 今天发这里的目的是请大家给些建议和吐槽. 目前核心特色就是异步调用跟管道, 之后围绕这些会考虑异常处理 (比如根据是否是异步上下文来决定是抛出异常还是调用回调), 异步的构造函数, 以及引入类/继承 (虽说个人不太喜欢传统的面向类的设计方式).
关于项目本身, 项目地址开头已经给出了, 这里再略解释一下
- 这个项目用的 C++ (而且是 C++11, 原因是前端是我当年毕业设计改出来的, 当时抱着猎奇的心理用了很多新特性, 所以相对比较挑剔编译器, 下面会讲到)
- 编译器要求是 clang 3.2+ / gcc 4.7+, 目前似乎只有 ArchLinux 系统上编译器版本比较新, Ubuntu 则是 clang 3.0 / gcc 4.6; Fedora 18 版本的 gcc 是 4.7 版本的
- 如果要使用 gcc 4.6 编译, 请检出 compl 分支 (代码修改去除了对 decltype 的使用, g++-4.6 可以编过, 但是不提供单元测试支持, 可运行案例测试)
- 编译需要 flex/bison 工具, 还需要 python2.7 生成一部分 c++ 代码
- 需要 GMP 这个库, ArchLinux 默认有装, Ubuntu/Fedora 可通过安装 libgmp-dev/gmp-devel 支持
- 有需要的话请安装 valgrind 作内存泄漏检查
- 后端代码生成很烂, 若要对比生成的源代码请自备 js 源代码格式化工具
- 前端也很烂, 必须从标准输入读取源代码
- 编译时有强制名字定义检查, 要通过
-i参数预定义一些名字如jQuery,require,exports之类的, 如stekin -i jQuery -i document < in.stkn > out.js这样编译; 内置的预定义名字请参考 env.cpp
其它