在很长一段时间里,Lua5.1一直是Roblox的首选语言。随着我们的发展,对更好的工具支持和更高性能的VM的需求也在增长。为了回答这个问题,我们开始重新构建名为“luau”(发音为/lu-WOW/)的Lua堆栈,目标是包含程序员期望现代语言提供的功能-其中包括类型检查器、新的Linter框架和更快的解释器,仅举几例。
要使这一切成为可能,我们必须从头开始重写大部分堆栈。问题是Lua5.1解析器与字节码生成紧密耦合,这不足以满足我们的需要。我们希望能够遍历AST进行更深入的分析,因此需要一个解析器来生成该语法树。从那里,我们可以自由地在该AST上执行任何我们想要执行的操作。
幸运的是,Studio中有一个现有的Lua5.1解析器,它只用于基本的linting pass。这使得我们可以非常轻松地采用解析器并对其进行扩展,以识别特定于LAU语法,从而最大限度地降低了以某种微妙方式更改结果解析的可能风险。这是一个关键的细节,因为我们在Roblox的神圣价值观之一就是向后兼容。我们已经编写了数百万行的Lua代码,我们致力于确保它们永远继续工作。
首先,我们需要了解一些关于我们是如何陷入这种情况的背景。我们之所以选择这些语法,是因为大多数程序员已经非常熟悉它们,而且实际上它们是行业标准。你不需要学任何新东西。
Local foo:String函数add(x:Number,y:Number):Number…。结束类型foo=(number,number)->;number local foo=bar作为字符串。
添加语法来批注绑定对于类型推理引擎更好地理解预期的类型非常重要。Lua是一种非常强大的语言,它允许您重载该语言中的几乎所有操作符。如果没有某种方法来注释事物,我们甚至不能自信地说表达式x+y将产生一个数字!
我们真正喜欢的来自TypeScript的东西是他们所说的类型断言。它基本上是一种将额外的类型信息添加到程序中以供检查器验证的方法。在TypeScript中,语法为:
不幸的是,当我们尝试这一功能时,我们遇到了一个糟糕的惊喜:这会破坏现有代码!我们的一个用户游戏有一个名为的功能。因此,他们的脚本包括如下片段:
LOCAL x=y As(w,z)--解析函数类型时需要‘->;’,得到<;eof>;
如果不是因为一个额外的复杂性,我们很可能已经完成了这项工作:我们希望我们的解析器只使用单一的前瞻标记。性能对我们来说很重要,编写性能非常高的解析器的一部分就是最大限度地减少必须执行的回溯操作。我们的解析器必须任意向前和向后扫描很远才能确定表达式的真正含义,这将不是很有效。
事实证明,TypeScript也要感谢JavaScript的自动分号插入规则,因为它使这项工作成为免费的。当您用TypeScript/JavaScript编写此代码段时,它将在每行插入分号,导致它被解析为两个单独的语句。然而,如果它在一行上,那么它在JavaScript中的as标记处是一个语法错误,但在TypeScript中是一个有效的类型断言表达式。因为Lua不这样做,也不强制使用分号,所以它必须尝试解析每个可能最长的语句,即使它们跨越多行。
尽管拥有我们想要的性能,但Luau的原始类型转换表达式并不向后兼容。遗憾的是,这违背了我们将Luau作为Lua5.1超集的承诺,因此如果没有一些额外的约束(比如在某些上下文中需要括号),我们就无法做到这一点!
Lua语法中另一个不幸的细节使我们无法在不引入另一个歧义的情况下向函数调用添加类型参数:
调用并返回带有两个类型参数A和B以及参数x的某些函数。
这种歧义只出现在表达式列表的上下文中。这在TypeScript和C#中并不是什么大问题,因为它们都有提前编译的优势。因此,他们都负担得起花费一些周期来尝试消除这个表达式的歧义,直到两个选项中的一个。
虽然看起来我们可以做同样的事情,比如在解析或类型检查期间应用启发式方法,但实际上我们不能。Lua5.1具有将全局变量动态注入任何环境的能力,这可以打破这种启发式方法。我们也完全没有这个好处,因为我们必须能够尽可能快地生成字节码,以便所有客户端都能开始解释。
解析此类型别名语句并不是突破性的更改,因为它已经是无效的Lua语法:
我们要做的很简单。我们解析一个主表达式,该表达式最终只解析类型,然后根据该表达式的解析结果决定要做什么:
如果是函数调用,请停止尝试解析更多的表达式即语句。
上面所遗漏的是非常明显的。它没有可以由另一个标识符领导的分支。然后我们要做的就是对表达式进行模式匹配:
Type foo=number--type别名类型(X)--函数调用类型={x=1}--赋值类型.x=2--赋值。
作为额外的片断,它仍然以与Lua 5.1完全相同的方式进行解析,因为我们没有从语句的上下文中进行解析:
这里的结论似乎是,我们必须为Luau设计语法,使其与前向兼容,并且具有最少上下文敏感的解析路径。它消除了需要解析器从该失败点回溯并尝试其他事情的事后猜测的必要性。这不仅给了我们一个快速解析器的好处,可以快速地一直读到源代码的末尾,而且它还可以返回AST,而不需要其他类型的阶段来消除歧义。
这也意味着我们在添加新语法时通常需要小心,这不一定是一个糟糕的地方。深思熟虑的语言要求它的设计者要有长远的眼光。