数值运算
描述整数 wrap、checked 运算、移位、类型转换错误和浮点结果。
Wave Foundation
整数表示
N 位整数具有 N 值位。无符号整数的范围是 0 到 2^N − 1,有符号整数的范围是 −2^(N−1) 到 2^(N−1) − 1。运算的 signedness 决定了位串的解释。
下表解释了计算结果。 IR 代码示例使用当前打印机的表示形式。
加法、减法、乘法
基本整数 add·sub·mul 保存结果的低 N 位。 overflow 不会导致 trap。 checked运算返回相同的wrap结果,以及Bool,指示数学结果是否超出相应的signed或unsigned的范围。
| 操作 | Wrap 结果 | Checked overflow |
|---|---|---|
| u8: 255 + 1 | 0 | true |
| i8: 127 + 1 | −128 | true |
| u8: 0 − 1 | 255 | true |
| i8: 12 × 3 | 36 | false |
在 overflow 处中断执行的语言的前端必须对 checked 操作的 overflow 结果使用显式 trap_if。默认操作不会隐式应用源语言的overflow策略。
Wrap 和 IR 表示显式检查
将此完整格式 3 模块保存为 integer-operations.wir。文本读取器接受它,标量解释器可以执行它。
module {
format_version 4
semantics_version 1
target "x86_64-whale-linux"
datalayout { ptr=64, endian=little }
declare @f0 "add_u8": whale () -> u8, linkage internal
declare @f1 "require_no_overflow": whale () -> u8, linkage internal
declare @f2 "minimum_division": whale () -> i8, linkage internal
declare @f3 "minimum_remainder": whale () -> i8, linkage internal
declare @f4 "negative_shift": whale () -> i8, linkage internal
fn @f0 "add_u8"() -> u8, entry %b0 {
%b0 "entry":
%v0: u8 = const u8 255
%v1: u8 = const u8 1
%v2: u8 = add u8 %v0, %v1
ret u8 %v2
}
fn @f1 "require_no_overflow"() -> u8, entry %b1 {
%b1 "entry":
%v3: u8 = const u8 255
%v4: u8 = const u8 1
%v5: tuple<u8, bool> = uadd_chk u8 %v3, %v4
%v6: u8 = extract %v5, 0
%v7: bool = extract %v5, 1
trap_if bool %v7, reason="integer overflow"
ret u8 %v6
}
fn @f2 "minimum_division"() -> i8, entry %b0 {
%b0 "entry":
%v0: i8 = const i8 -128
%v1: i8 = const i8 -1
%v2: i8 = sdiv i8 %v0, %v1
ret i8 %v2
}
fn @f3 "minimum_remainder"() -> i8, entry %b0 {
%b0 "entry":
%v0: i8 = const i8 -128
%v1: i8 = const i8 -1
%v2: i8 = srem i8 %v0, %v1
ret i8 %v2
}
fn @f4 "negative_shift"() -> i8, entry %b0 {
%b0 "entry":
%v0: i8 = const i8 -1
%v1: i8 = const i8 -1
%v2: i8 = shl i8 %v0, %v1
ret i8 %v2
}
}
add_u8 返回 256 的低 8 位,即 0。minimum_division 返回 −128,minimum_remainder 返回 0。negative_shift 将 −1 解释为 unsigned count 255,再取 255 mod 8 = 7。用显式函数 ID 执行:
whale ir run integer-operations.wir --function @f0
whale ir run integer-operations.wir --function @f2
whale ir run integer-operations.wir --function @f3
whale ir run integer-operations.wir --function @f4
u8 0
i8 -128
i8 0
i8 -128
require_no_overflow 从索引 0 提取回绕结果,从索引 1 提取 Bool overflow 标志。显式 trap_if 在返回前停止。以下命令以状态 1 退出,并将诊断写到 stderr:
whale ir run integer-operations.wir --function @f1
Error: integer-operations.wir: trap at @f1 %b1 instruction 5: "integer overflow" (after 6 steps)
除法和余数
整数除以零或余数会导致陷阱。将最小有符号值除以 -1 即可得到最小值。在这种情况下余数为零。
| 操作 | 结果 |
|---|---|
| i8: −128 / −1 | −128 |
| i8: −128 % −1 | 0 |
| 除以整数 0 | trap |
| 整数 0 的余数 | trap |
转变
在移位N位值时,count的位串被解释为unsigned,并且使用除以N的余数。它不会仅仅因为 count 超出 0 到 N−1 的范围而生成 trap。
在 8 位值中,count 0·8·16 均为 0 位移位。 8 位count 的位串11111111 移位了 7 位。即使该位串表示signed-1,这也是相同的。 unsigned 分析在其余计算之前进行。
拒绝负数或过多count的源语言必须在转换前进行显式检查。
类型转换
| 转换 | 意义 |
|---|---|
| Zero extension | 通过用 0 填充高位来增加宽度 |
| Sign extension | 通过复制符号位来增加宽度 |
| 钻头切削 | 仅保留目标宽度对应的低位 |
| 节拍重新诠释 | 将相同的位串解释为不同的类型 |
| 无损数值转换 | 如果无法在保留数值的情况下表达,trap |
例如,将 8 位 11111111 转换为 16 位 zero extension 变为 0000000011111111,sign extension 变为 1111111111111111。即使输入位相同,也是不同的操作。
float 到 int 的转换会截断为零,然后检查整数范围。 NaN 和无穷大会导致陷阱。当转换为i8时,127.9变成127,而128.0陷入困境。 Bool 转换为整数 0 或 1。有符号 i1 不能表示 1,因此它不能作为此转换的目标。
将地址转换为整数不会恢复对该整数的指针访问。请参阅指针有效性。
cast 与 checked 运算的验证形式
验证先检查实际操作数类型是否与 src_ty 一致,再检查 opcode 允许的源类型、目标类型和位宽。结果的类型注解也必须与定义一致。不存在隐式转换。
| Opcode | 允许的类型 |
|---|---|
zext, sext |
非 Bool 整数;目标位宽必须更大 |
zext (Bool) |
Bool 到整数 0/1;允许除有符号 i1 外的所有整数目标,包括 u1 |
trunc |
非 Bool 整数;目标位宽必须更小 |
fext, ftrunc |
浮点数;目标位宽必须更大 / 更小 |
itof_s, itof_u |
有符号 / 无符号整数到浮点数 |
ftoi_s, ftoi_u |
浮点数到有符号 / 无符号整数 |
bitcast |
相同位宽的整数或浮点标量,或数据指针到数据指针 |
ptrtoint, inttoptr |
数据指针到整数 / 整数到数据指针 |
整数扩展和截断是位运算,因此整数操作数的有符号性可以不同。Bool 是独立的逻辑类型,不允许符号扩展、截断或到 i1/u1 的 bitcast。聚合类型和函数指针的 cast 会被拒绝。检查指针和整数的类别不会建立有效分配,也不会恢复访问权限。运行时转换检查和机器码 lowering 仍是单独的未完成工作。
下面的完整 Rust 程序输出两个有效转换,并确认将 zext 替换为 fext 后会被拒绝。
use ir::*;
fn main() {
let mut builder = ModuleBuilder::new("x86_64-whale-linux", DataLayout::default_64bit_le());
let mut function = builder.begin_function("widen", vec![("byte".into(), Type::U8)], Type::Void);
function.ret(None);
function.finish();
let mut module = builder.finish();
let function = &mut module.functions[0];
function.value_types.extend([(ValueId(1), Type::U32), (ValueId(2), Type::I32)]);
function.blocks[0].instructions.extend([
Instruction::Cast {
dst: ValueId(1), op: CastOp::ZExt,
src_ty: Type::U8, src: ValueId(0), dst_ty: Type::U32,
},
Instruction::Cast {
dst: ValueId(2), op: CastOp::Bitcast,
src_ty: Type::U32, src: ValueId(1), dst_ty: Type::I32,
},
]);
verify_module(&module).unwrap();
print!("{}", print_module(&module));
// An integer source cannot be annotated as a floating widening operation.
if let Instruction::Cast { op, .. } = &mut module.functions[0].blocks[0].instructions[0] {
*op = CastOp::FExt;
}
assert!(matches!(verify_module(&module), Err(VerifyError::InvalidCast { .. })));
}
module {
format_version 4
semantics_version 1
target "x86_64-whale-linux"
datalayout { ptr=64, endian=little }
declare @f0 "widen": whale (u8) -> void, linkage internal
fn @f0 "widen"(%v0 "byte": u8) -> void, entry %b0 {
%b0 "entry":
%v1: u32 = zext u8 %v0 to u32
%v2: i32 = bitcast u32 %v1 to i32
ret void
}
}
以下指令片段是故意无效的 IR。
%v1: i32 = ftoi_s f64 %v0 to i32 // Invalid when %v0 is actually Bool.
%v2: i16 = zext i32 %v3 to i16 // zext cannot narrow.
%v4: i1 = zext bool %v5 to i1 // Signed i1 cannot represent true as 1.
实际源类型不匹配会产生 OperandTypeMismatch;非法类型组合会产生包含 opcode 和两种类型的 InvalidCast。验证错误不会插入转换,也不会修改模块。
sadd_chk、ssub_chk、smul_chk 要求有符号整数;uadd_chk、usub_chk、umul_chk 要求无符号整数。两个操作数都必须与运算类型 T 完全相同,结果必须是 tuple<T, bool>。提取要求 tuple、存在的字段索引以及该字段的精确类型。尤其不能将溢出字段提取为 i1。上文的回绕和溢出结果是执行契约;这些测试验证结构正确性,并不表示已有后端执行这些运算。
浮点运算
浮点值具有精确的位串f16·f32·f64。该操作四舍五入到声明宽度中最接近的值,如果正好在中间,则使用ties-to-even,它选择具有有效数字的最低有效位的值。
默认操作是fast-math,隐式FMA,不允许对小值进行强制清零处理。乘法后加法保留它们各自的舍入步骤,并且后端不应隐式地将它们组合成一个运算。
数值运算的结果可以是 NaN 或无穷大。来自数学运算和宽度变化的 NaN 被标准化为每个宽度一个固定数量的安静 NaN。保存并复制保留原始 NaN 位。因此,在不进行算术运算的情况下将 NaN 有效负载传递到内存时和计算它时,行为是不同的。
它不公开浮点状态标志。尽管基本浮点运算允许 NaN·无穷大结果,但 float→int 转换应用上述 trap 规则。
存储精确的常量
使用 FloatBits variant 或任何精确宽度的十六进制位字符串。存储值的相等性与包含负0和NaNpayload的位串进行比较。如果类型IR和payload的宽度不同,验证者会拒绝。
use ir::{FloatBits, ModuleBuilder, Target, Type};
fn main() {
let bits = FloatBits::parse(32, "0xffc01234").unwrap();
assert_eq!(bits, FloatBits::F32(0xffc01234));
let target = Target::X86_64WhaleLinux;
let mut module = ModuleBuilder::new(target.name(), target.data_layout());
let mut function = module.begin_function("payload", vec![], Type::F32);
let value = function.const_float_bits(Type::F32, bits);
function.ret(Some(value));
function.finish();
let module = module.finish();
ir::verify_module(&module).unwrap();
assert!(ir::print_module(&module).contains("const f32 0xffc01234"));
println!("{}", bits);
}
0xffc01234
f16、f32 或f64 位串以 0x 开头,后面分别紧跟 4、8 或 16 个十六进制数字。 0x80000000代表f32负零; 0x7f800000 代表正无穷大。 const_float 将主机f64值转换为数字;使用const_float_bits保留原始位。精确的存储表示并不意味着完整的浮点执行后端。编译时算术仍然使用主机f64中间体,因此每个声明宽度的完整舍入契约尚未实现。