整数和浮点数
在Lua 5.3之前的版本中,只支持一种类型的数字,默认是浮点数,可以通过修改Lua解释器源码来使用整数。我理解这是因为Lua最初是被用作配置语言,面向的使用者大多不是程序员,是不区分整数和浮点数的,比如5
和5.0
就是两个完全一样的数字。后来随着Lua使用范围的扩大,同时支持整数的需求越发强烈(比如位运算),最终在Lua 5.3版本中区分了整数和浮点数。这也带来了一些复杂度,主要二元运算符对不同类型的处理规则,分为如下三类:
- 支持整数和浮点数,包括
+
、-
、*
、//
和%
。如果两个操作数都是整数,则结果也是整数;否则(两个操作数至少有一个浮点数)结果是浮点数。 - 只支持浮点数,包括
/
和^
。无论操作数是什么类型,结果都是浮点数。比如5/2
,两个操作数虽然都是整数,但会转换为浮点数,然后计算结果为2.5
。 - 只支持整数,包括5个位操作。要求操作数一定是整数,结果也是整数。
对上述三类的处理,在语法分析的常量折叠fold_const()
函数和虚拟机执行时,都会体现。代码很繁琐,这里省略。
类型转换
Lua也定义了上述类型转换的规则(主要是不能完整转换情况下的规则):
- 整型转浮点型:如果不能完整转换,则使用最接近的浮点数。即转换不会失败,只会丢失精度。
- 浮点型转整型:如果不能完整转换,则抛出异常。
而Rust语言中,整型转浮点型规则一样,但浮点型转整型就不同了,没有检查是否能完整转换。这被认为是个bug并会修复。在修复前,我们只能自己做这个完整性的检查,即如果转换失败,则抛出异常。为此我们实现ftoi()
函数:
pub fn ftoi(f: f64) -> Option<i64> {
let i = f as i64;
if i as f64 != f {
None
} else {
Some(i)
}
}
整型转浮点型时直接用as
即可,而浮点型转整型时就需要用这个函数。
在语法分析和虚拟机执行阶段,都会涉及到这个转换,所以新建utils.rs
文件用来放这些通用函数。
比较
Lua语言中,大部分情况下是尽量避免整数和浮点数的区别。最直接的例子就是,这个语句5 == 5.0
的结果是true,所以Value::Integer(5)
和Value::Float(5.0)
,在Lua语言中是相等的。另外一个地方是,用这两个value做table的key的话,也认为是同一个key。为此,我们就要修改之前对Value的两个trait实现。
首先是比较相等的PartialEq
trait:
impl PartialEq for Value {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Value::Integer(i), Value::Float(f)) |
(Value::Float(f), Value::Integer(i)) => *i as f64 == *f && *i == *f as i64,
然后是Hash
trait:
impl Hash for Value {
fn hash<H: Hasher>(&self, state: &mut H) {
match self {
Value::Float(f) =>
if let Some(i) = ftoi(*f) {
i.hash(state)
} else {
unsafe {
mem::transmute::<f64, i64>(*f).hash(state)
}
}
不过,还是有一个地方需要区分类型的,就是在语法分析时,向常量表中添加常量时,查询常量是否已经存在的时候。为此要实现一个区分类型的比较方法:
impl Value {
pub fn same(&self, other: &Self) -> bool {
// eliminate Integer and Float with same number value
mem::discriminant(self) == mem::discriminant(other) && self == other
}
}
测试
至此,二元运算语句的语法分析终于完成。虚拟机执行部分就很简单,这里略过。可以使用如下测试Lua代码:
g = 10
local a,b,c = 1.1, 2.0, 100
print(100+g) -- commutative, AddInt
print(a-1)
print(100/c) -- result is float
print(100>>b) -- 2.0 will be convert to int 2
print(100>>a) -- panic