Числовые операции

Описывает целочисленные операции 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, должны использовать явный trap_if для результата overflow операции checked. Операция по умолчанию не применяет неявно политику 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 возвращает 0, младшие восемь бит числа 256. 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 и флаг overflow Bool по индексу 1. Явный 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. Он не генерирует trap только потому, что count находится вне диапазона от 0 до N-1.

В 8-битных значениях count 0·8·16 все представляют собой 0-битные сдвиги. Битовая строка 11111111 из 8 бит count сдвигается на 7 бит. Это то же самое, даже если эта битовая строка представляет собой signed -1. unsigned Анализ выполняется перед остальными расчетами.

Исходные языки, которые отвергают отрицательные или чрезмерные count, должны поставить явную проверку перед сдвигом.

преобразование типов

преобразование смысл
Zero extension Увеличьте ширину, заполнив старшие биты 0.
Sign extension Увеличьте ширину, дублируя знаковый бит.
резка бит Сохраняйте только младшие биты, соответствующие ширине назначения.
Ударная реинтерпретация Интерпретация одной и той же битовой строки как разных типов
Числовое преобразование без потерь Если его невозможно выразить, сохранив числовое значение, trap

Например, преобразование 8-битного 11111111 в 16-битное zero extension становится 0000000011111111, а sign extension становится 1111111111111111. Даже если входные биты одинаковы, это разные операции.

Преобразование float-to-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 — отдельный логический тип: знаковое расширение, усечение и bitcast в i1/u1 запрещены. Преобразования агрегатов и указателей функций отклоняются. Проверка категорий указателя и целого не создаёт корректного выделения памяти и не восстанавливает права доступа. Проверки преобразований во время исполнения и 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; недопустимая пара — InvalidCast с opcode и обоими типами. Ошибка проверки не вставляет преобразований и не изменяет модуль.

sadd_chk, ssub_chk, smul_chk требуют знаковые целые, а uadd_chk, usub_chk, umul_chk — беззнаковые. Оба операнда должны точно соответствовать типу операции T, результат — tuple<T, bool>. Извлечение требует кортеж, существующий индекс поля и точный тип этого поля. В частности, извлечение поля переполнения как i1 отклоняется. Описанные выше результаты wrap/переполнения остаются контрактом исполнения; эти тесты проверяют структуру, а не выполнение реализованным backend.

арифметика с плавающей запятой

Значения с плавающей запятой имеют точную битовую строку f16·f32·f64. Операция округляет до ближайшего значения в объявленной ширине и, если ровно посередине, использует ties-to-even, которое выбирает значение с четными младшими битами значащих цифр.

Операция по умолчанию — fast-math, неявная FMA, которая не позволяет принудительно обнулять небольшие значения. Умножение, за которым следует сложение, сохраняет соответствующие шаги округления, и серверная часть не должна неявно объединять их в одну операцию.

Результатом числовой операции может быть NaN или бесконечность. NaN от математических операций и изменений ширины нормализуются до одного фиксированного количества тихих NaN на ширину. Сохраните и скопируйте исходные биты NaN. Поэтому поведение различается при передаче полезных данных NaN в память без арифметических операций и при их вычислении.

Он не предоставляет флаги состояния с плавающей запятой. Несмотря на то, что базовая арифметика с плавающей запятой позволяет получить результат NaN·бесконечность, преобразование float→int применяет приведенные выше правила trap.

Храните точные константы

Используйте FloatBits variant или любую строку шестнадцатеричных бит точной ширины. Равенство сохраненных значений сравнивается с битовой строкой, содержащей отрицательные 0 и NaN payload. Верификатор отклоняет, если ширина типов 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 хоста, поэтому контракт полного округления для каждой объявленной ширины еще не реализован.