Skip to content
Neuron Teckid edited this page Mar 8, 2016 · 1 revision

编译时语法树更改

Flatscript 并不依赖于 promise 体系, 而是直接修改语法树. 比如下面的 Flatscript 代码

console.log(fs.readFile('a.txt', %%))

的编译结果等价于以下 JS

fs.readFile('a.txt', function(err, result) {
    if (err) throw err;
    console.log(result);
})

源代码中 fs.readFile 的最后一个参数是两个百分号, 这是一个标记参数, 指明函数调用的此参数是一个回调, 那么编译器在处理这一段代码时, 会将语法树中该调用表达式所在的语句中其他成分生成到回调函数的函数体中去.

简单示例解释所谓 "语法树中的其他成分", 譬如如下的语句

console.log(fs.readFile('a.txt', %%) + fs.readFile('b.txt', %%))

此语句中的表达式有 4 个主要表达式, 递归地从叶到根 (编译顺序亦是如此) 是

  1. 异步函数调用 fs.readFile, 参数 'a.txt' (另一参数是异步标记就不列出了)
  2. 异步函数调用 fs.readFile, 参数 'b.txt'
  3. 双目运算 +, 作用于表达式 1 和表达式 2
  4. 普通函数调用 console.log, 参数为表达式 3

对于表达式 1 这个异步调用而言, 表达式 2/3/4 便是语法树中的其他成分; 类似地对于表达式 2, 表达式 1/3/4 是语法树中的其他成分. 也就是说, 编译器处理完表达式 1 后, 会生成一个回调函数, 函数体中包含表达式 2/3/4 而不含有表达式 1, 但这样一来, 表达式就不完整了, 类似

console.log(??? + fs.readFile('b.txt', %%))

所以需要用某个东西去替换上面的 ???. 这个东西便是编译器生成的回调函数的第二个形式参数 (因为 NodeJS API 中许多函数要求的回调都是 function (error, result) 形式, 函数执行结果在第二参数).

综上, Flatscript 编译器在处理上述包含异步函数调用的语句时, 会进行如下转换步骤

  1. 将表达式 1 以单独语句的形式置于当前上下文中, 除了异步标记其他参数均不变
  2. 生成一个匿名函数, 其形参是 err, result, 用这个函数替换表达式 1 中异步标记
  3. 将上述匿名函数体替换当前上下文, 并在这个函数体中先生成一个分支语句 if (err) 抛出此错误;
  4. 将上述匿名函数的形参 result 替代表达式 1 放入原语句的语法树中, 而表达式 2/3/4 均不变

经过以上 4 步的变换后, 这条语句

console.log(fs.readFile('a.txt', %%) + fs.readFile('b.txt', %%))

就会变为如下的 JS

fs.readFile('a.txt',         // 步骤 1
    function (err, result) { // 步骤 2
        if (err) throw err;  // 步骤 3
        console.log(
            result           // 步骤 4
            + fs.readFile('b.txt', %%)
    });

然后继续处理表达式 2, 以及后续的部分, 并为每个生成的回调 result 参数编号, 于是上面的语句最终会被编译为如下的 JS

fs.readFile('a.txt',
    function (err, resultA) {
        if (err) throw err;
        fs.readFile('b.txt', function (err, resultB) {
            if (err) throw err;
            console.log(resultA + resultB); 
        });
    });

但这样也有一个小问题, 因为语法树的处理在编译时, 以下的语句结构

console.log(syncFunc() + fs.readFile('a.txt', %%))

没有经过特殊处理, 虽然书写顺序上 syncFunc() 调用应该先于 fs.readFile, 但实际生成的结果会是

fs.readFile('a.txt',
    function (err, resultA) {
        if (err) throw err;
        console.log(syncFunc() + resultA);
    });

方法选择

在上述代码变换中有一个缺陷就是当一个 %% 标记所对应的回调函数生成之后, 对 err 参数的处理都是直接 throw, 这当然不对. 在 Flatscript 中引入方法选择在不同的上下文中选择不同的 throw, return 甚至 break, continue 语句实际生成的代码.

默认地, throw 语句都直接编译成 JS 的 throw 语句, 但在含有异步调用的 try 语句块内则不然, 此时 catch 对应的语句块会被编译为一个函数, 然后抛出异常的行为就被直接编译为对该函数的调用.

比如, 将前面的例子写入 try-catch 中去

try
    console.log(fs.readFile('a.txt', %%) + fs.readFile('b.txt', %%))
catch
    console.error($e) # $e 是一个特殊名字, 表示 catch 的异常对象

那么生成的 JS 会类似

function catch0($exception) {
    console.error($exception);
}
fs.readFile("a.txt", (function(err, result0) {
    if (err) return catch0(err);                    // CALL catch
    fs.readFile("b.txt", (function(err, result1) {
        if (err) return catch0(err);                // CALL catch
        console.log(result0 + result1);
    }));
}));

可以进一步查看 方法挑选 页面.

非正规异步函数调用

以上所有示例中, 都要求定义者或调用者的回调函数形式为 callback(error, result) (在 Flatscript 中这种形式称之为正规回调). 并且会针对 error 参数生成错误处理语句. 但这样不严谨. 如 JS 内置函数 setTimeout 它并不需要回调参数, 那么生成错误处理函数实属多余. Flatscript 提供了手段类似但生成代码稍有不同的版本. 如

console.log(0)
setTimeout(%, 1000) # 使用单个 % 表示这个回调函数不含有 err 参数因此不需要生成错误处理语句
console.log(1)
setTimeout(%, 1000)
console.log(2)

生成的 JS 代码将会类似

console.log(0);
setTimeout((function() {
    console.log(1);
    setTimeout((function() {
        console.log(2);
    }), 1000);
}), 1000);

又如 mocha 测试库中, 在异步测试结束后需要调用 done 函数, 这个 done 函数 it 函数的唯一回调参数, 如果将其当作正规回调处理, 那么 done 会被当作错误, 这显然是不对的. 在 Flatscript 中则提供了如下的方式来使用这些不规则的回调参数

describe('test', ():
    it('async', %done) # 使用 % 加上回调参数名, 若有多个参数, 则如 %(x, y, z)
    setTimeout(%, 10)
    assert.ok(true)
    done()
)

生成的 JS 代码将会类似

describe('test', function() {
    it('async', function(done) {
        setTimeout(function() {
            assert.ok(true);
            done();
        }, 10);
    });
});

注: 以上示例实际生成的代码都会有一些 name mangling, 以及额外的 try-catch, 为了便于理解和阅读, 在不更改生成机制且不影响执行结果的前提下我手动编辑简化了生成的代码.

Clone this wiki locally