发一个用来生成 Js 的语言, 目标是扫平异步编程障碍
发布于 2年前 作者 neuront 1622 次浏览

前段时间搞了个替代语言用来生成 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

其它

18 回复

膜拜, 赞一个

膜拜, 赞一个

想法很好,实现的也不错,不过说句实话,真心没看出来这东西好在哪

同楼上。很赞。但是价值不大。

谢谢各位吐槽. 我是被异步套异步虐得疼, 才想搞个这东西. Coffee 虽然在语法上改进很多但是异步这块还是很麻烦. 缩进太多 / 调整缩进也不利于 git diff

异步套异步套异步套异步套异步…

这明显需要从设计上入手,函数里边显式地套函数,效率是非常非常低的; 另外严格模式只允许嵌套一次

用async,windjs之类的库,或者消息机制,嵌套多了肯定不好

技术上很好,不过好像有重复发明轮子的意思

谢谢提供这些参考.

windjs 当然是主要参考对象之一, 其原理是将 js 函数转换成字符串, 然后再重新编译一次. 不过那些 eval 太多确实很难堪.

刚刚才看了下 async 这个库, 还不错的样子, 感想是在 js 异步泛滥之前肯定没人会这样子写程序吧. async 的设计思想应该是把批量的异步操作变成单个, 而我的目标是把异步操作还原成同步操作.

可能是吧, 不过长得完全一样的东西应该没有吧?

话说我是 py 党, 所以有些语法废 js 而立 py (以后会考虑出个 yield 也说不定).

主要还是难在循环和异常的同步化上,单纯的异步回调同步化ToffeeScript和Kaffeine都可以,都是在命令上加上!。

e, stats = fs.stat!('foo')
ok

等价于

fs.stat('foo', function(e, stats) {
    ok;
});

如果能实现循环和异常的同步化,还是相当不错的。

ToffeeScript的异步支持比较弱,Kaffeine的异步提前能完成不少功能,但也并不完美,

如if的a||b判断,a真则不执行b,object构建时

{
  a: modify_c(),
  b: async_use_c!
}

都会存在陷阱。

谢谢参考. 没想到有如此接近的实现, 相见恨晚. (我 Coffee 功底差, 难以一战)

逻辑短路这个确实有些难搞.

循环目前是采用管道这种替代方案实现的, 不过对一般 for / while 循环进行变换算法上已经想到怎么搞了 (跟现在管道变换的实现方案差不多).

@neuront 我认为很有价值,跟python的gevent有点像。

@neuront 我个人感觉思想转变更好,同步异步我都习惯,所以很喜欢async这种能把异步的思想很直观的在代码里表现出来的库(windjs我不太习惯)

看过icecoffeescript吗?在coffee上又套一层,它的异步风格也还行,不知道能不能给你思路

@neuront icecoffeescript语法上就有点同步的意思,用await和defer

parallelSearch = (keywords, cb) ->
  out = []
  await 
    for k,i in keywords
      search k, defer out[i]
  cb out

rankPopularity = (keywords, cb) ->
  await parallelSearch keywords, defer results
  times = for r,i in results
    last = r[r.length - 1]
    [(new Date last.created_at).getTime(), i]
  times = times.sort (a,b) -> b[0] - a[0]
  cb (keywords[tuple[1]] for tuple in times)

w_list = [ "sun", "rain", "snow", "sleet" ]
f_list = [ "tacos", "burritos", "pizza", "shrooms" ]
await
  rankPopularity w_list, defer weather
  rankPopularity f_list, defer food

if weather.length and food.length
  await search "#{weather[0]}+#{food[0]}", defer tweets
  msg = tweets[0]?.text

alert if msg? then msg else "<nothing found>"

如果只是自娱自乐,怎么实现都随意,但是如果考虑到推广应用和学习曲线,个人的意见是:与其重新发明语法,不如扩展已有的语法,例如coffeescript的各种变体扩展。

@danielking 作为一个 py 党很我不爽 coffee 的语法.

@neuront 我也用py,怎么觉得coffee挺好的啊,另外ruby和erlang我用的更多,coffee确实糅合了其它三者的语法,有些怪异,但是我还是很喜欢,嘿嘿。

回到顶部