Справочник по Whale IR

Описывает типы, идентификаторы, допустимость функции, порядок оценки и формат обмена.

Wave Foundation

Модули и идентификаторы

Модуль состоит из целевой информации, глобальных определений и функций. Значения имеют явные типы. Интерфейс обрабатывает имя, тип, перегрузку и дженерики исходного языка и генерирует typed IR.

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

Каждое определение значения имеет идентификатор. Определения не могут дублироваться, а метаданные типа должны соответствовать типу, указанному в определении. Само по себе имя не идентифицирует определения, даже если объявления с одинаковыми именами затеняют друг друга.

IR Конфигурация и чтение

Ниже приведен полный пример Rust, который создает и проверяет функцию с помощью ящика ir, а затем выводит typed IR.

use ir::{ModuleBuilder, Target, Type};

fn main() {
    let target = Target::lookup("x86_64-whale-linux").unwrap();
    let mut module = ModuleBuilder::new(target.name(), target.data_layout());
    let mut function = module.begin_function("answer", vec![], Type::I32);
    let left = function.const_i32(40);
    let right = function.const_i32(2);
    let answer = function.add(Type::I32, left, right);
    function.ret(Some(answer));
    function.finish();
    let module = module.finish();
    ir::verify_module(&module).unwrap();
    print!("{}", ir::print_module(&module));
}

Принтер выводит IR:

module {
  format_version 4
  semantics_version 1
  target "x86_64-whale-linux"
  datalayout { ptr=64, endian=little }

  declare @f0 "answer": whale () -> i32, linkage internal

  fn @f0 "answer"() -> i32, entry %b0 {
  %b0 "entry":
    %v0: i32 = const i32 40
    %v1: i32 = const i32 2
    %v2: i32 = add i32 %v0, %v1
    ret i32 %v2
  }

}

Сохраните вывод как answer.wir, чтобы прочитать и проверить текстовым парсером или CLI. Исполнение скалярных целых описано ниже.

Сохраните вывод в answer.wir, чтобы прочитать и проверить его текстовым парсером и CLI. Выполнение IR пока недоступно.

тип

тип смысл
bool Логическое значение false или true
i1, i8, i16, i32, i64, i128 целое число со знаком указанной разрядности
u1, u8, u16, u32, u64, u128 целое число без знака указанной разрядности
f16, f32, f64 Значение с плавающей запятой с указанной разрядностью
ptr<T> T Указатель на тип значения.
fnptr<signature> Вызываемый указатель с точными типами параметров/результатов и соглашением о вызовах
array<T, N> N элементы одного типа
struct{T, ...} Поле упорядоченной структуры
tuple<T, ...> упорядоченные элементы кортежа
void Нет результатов

bool, i1 и u1 — это разные типы. signed i1 представляет −1 и 0, а unsigned u1 представляет 0 и 1. Целое число 1 не является неявным логическим условием. Условный переход, условие Select, trap_if требует операнда Bool.

Размер хранилища не определяется исключительно количеством битов в значении и соответствует целевой макет. Например, значение i1 составляет 1 бит, но занимает в памяти не менее 1 байта.

Функции и вызовы

Функция определяет все параметры, типы результатов, соглашение о вызовах и linkage. Прямые и косвенные вызовы должны совпадать с подписью звонящего. Вызов void не дает результата ID. Вызов nonvoid сохраняет определение результата, даже если O0 не использует результат.

Возвращаемый результат должен соответствовать типу результата функции. Возврат void не несет значения, а возврат nonvoid содержит значение объявленного типа результата.

Заявления, личности и звонки

Module.declarations записывает FunctionId каждой функции, имя, полную подпись, ссылку и имя внешней ссылки. Определение относится к этой идентичности; типы параметров и возвращаемых значений должны соответствовать его объявлению. Идентичные повторяющиеся объявления разрешаются к одному и тому же идентификатору через declare_function; конфликты и повторяющиеся определения являются ошибками. Внутреннему объявлению необходимо тело в модуле. Внешнее объявление может быть неразрешенным до момента связывания или иметь экспортированное тело. Внутренние функции не имеют link_name; внешние функции требуют явного непустого имени без NUL. Два разных объявления функции не могут претендовать на одно и то же внешнее имя. Глобальные объекты и функции по-прежнему используют отдельные внутренние пространства имен.

Зарегистрируйте объявления перед созданием тел с помощью begin_declared_function для поддержки переадресации вызовов и рекурсии. begin_function остаётся удобным для новой внутренней функции Whale. Проверенные API declare_function, begin_declared_function, function_addr, null_function и call возвращают Result; отклоненный вызов не добавляет инструкцию и не выделяет ее идентификатор результата.

Следующая полная программа на Rust объявляет внешнюю функцию, принимает ее типизированный адрес и выполняет как прямые, так и косвенные вызовы:

use ir::{Callee, CallingConvention, DataLayout, FunctionSignature, Linkage, ModuleBuilder, Type};

fn main() {
    let mut module = ModuleBuilder::new("x86_64-whale-linux", DataLayout::default_64bit_le());
    let signature = FunctionSignature {
        params: vec![Type::I32], ret: Type::I32,
        convention: CallingConvention::SysV64, variadic: false,
    };
    let identity = module.declare_function(
        "identity", signature, Linkage::External, Some("identity_i32".into()),
    ).unwrap();
    let mut function = module.begin_function("answer", vec![], Type::I32);
    let input = function.const_i32(42);
    let callback = function.function_addr(identity).unwrap();
    // The direct call's result remains defined even though it is unused.
    function.call(Callee::Direct(identity), vec![input]).unwrap();
    let result = function.call(Callee::Indirect(callback), vec![input]).unwrap().unwrap();
    function.ret(Some(result));
    function.finish();
    let module = module.finish();
    ir::verify_module(&module).unwrap();
    print!("{}", ir::print_module(&module));
}
module {
  format_version 4
  semantics_version 1
  target "x86_64-whale-linux"
  datalayout { ptr=64, endian=little }

  declare @f0 "identity": sysv64 (i32) -> i32, linkage external, link_name "identity_i32"
  declare @f1 "answer": whale () -> i32, linkage internal

  fn @f1 "answer"() -> i32, entry %b0 {
  %b0 "entry":
    %v0: i32 = const i32 42
    %v1: fnptr<sysv64 (i32) -> i32> = function_addr @f0
    %v2: i32 = call sysv64 i32 @f0(%v0)
    %v3: i32 = call sysv64 i32 indirect %v1(%v0)
    ret i32 %v3
  }

}

Callee::Direct(FunctionId) разрешается через таблицу объявлений; Для Callee::Indirect(ValueId) требуется значение Type::FnPtr(FunctionSignature). Подпись включает все типы параметров, тип результата и CallingConvention::{Whale, SysV64}. Он сохраняется через копии, хранилище, параметры, возвраты, фи и выбор. Указатели данных и целочисленные значения не подлежат вызову. Приведения, включающие типы указателей на функции, отклоняются; изменение аннотации типа не может изменить вызываемую подпись. Указатель функции имеет 64-битное адресное хранилище на этой цели; это само по себе не реализует теневые метаданные во время выполнения.

Арность, точные типы аргументов/результатов, наличие идентификатора результата и соглашение о вызовах должны совпадать. Неявных преобразований нет. Косвенный вызываемый объект должен доминировать над вызовом, как и его аргументы. variadic: true, параметры типа void и SysV64 агрегированные подписи параметров/результатов отклоняются. Совокупные подписи Whale могут быть представлены в IR; Собственная классификация ABI и передача машинных вызовов пока недоступны ни для одного соглашения.

null_function(signature) представляет собой типизированный нулевой указатель на функцию. Вызов правильно напечатан IR с обязательной ловушкой времени выполнения перед входом в вызываемый объект. Ненулевая цель, которая недействительна, просрочена или несовместима с проверяемой подписью, также должна быть перехвачена. Эти проверки во время выполнения и управление временем существования внешнего обратного вызова ожидают уровня интерпретатора/собственного выполнения; Успех верификатора не означает, что произвольные внешние адреса безопасны.

Формы вызовов AST

Это фрагменты выражений внутри программы формата AST:

{"Call":{"callee":{"Direct":"increment"},"args":[{"Lit":{"Int":{"bits":32,"signed":true,"value":"41"}}}]}}
{"Call":{"callee":{"Indirect":{"FunctionRef":"increment"}},"args":[{"Lit":{"Int":{"bits":32,"signed":true,"value":"41"}}}]}}

Direct и FunctionRef используют пространство имен функции, даже если переменная имеет одно и то же имя. Indirect сначала оценивает свое выражение, затем оценивает аргументы слева направо. Вызов void действителен как ExprStmt, но не как инициализатор переменной, аргумент, операнд или возвращаемое значение. Вызовы и ссылки на функции не являются числовыми константными выражениями времени компиляции. NullFunction принимает объект подписи с полями params, ret, convention и variadic.

Полный пример JSON сохраняет обратный вызов и вызывает его перед внешним вызовом. Опустите его с помощью:

cargo run --locked --features socket-cli -- ir lower ir/tests/fixtures/ast-v2-calls.json

Его ожидаемый IR проверяется в тестах на понижение. Имена функций и ссылок представлены на границе IR; сохранение их посредством генерации собственных объектов и связывания по-прежнему является отдельной работой.

Наличие блоков и значений

Каждый блок имеет уникальный идентификатор и ровно один terminator. Цели ветвей должны принадлежать одной и той же функции. Входной блок должен существовать и не может иметь переднюю кромку и phi. При создании цикла перейдите от входного блока к отдельному заголовку цикла.

Определение значения в пути к исполняемому файлу должно определять его типичную точку использования. Это означает, что все пути от точки входа до точки использования должны проходить через это определение. В том же блоке определения должны предшествовать использованию. Порядок хранения блоков не определяет доминирование.

Предположим, что точка входа разветвляется на left или right, а затем присоединяется к join. Значения, определенные только в left, не могут использоваться в качестве общих значений в join. Это связано с тем, что путь, проходящий через right, не определен. Значения каждого предыдущего блока необходимо объединить в phi, который принимается на вход.

Недоступные блоки также сохраняются в модуле. Верификатор постоянно проверяет идентификатор, тип, операнд и структуру ветвления блока. Определение недостижимого блока не может обеспечить ценность нормального использования достижимого пути.

Phi команда

phi ставится перед всеми обычными командами в блоке. Для каждого предыдущего блока требуется ровно один вход. Входное значение должно иметь тип phi и должно быть доступно в конце соответствующего предыдущего блока.

Даже если в одном предыдущем блоке имеется несколько ребер, вход будет только один. Цикл phi может относиться к значению блока, который появляется позже в порядке хранения модуля, если это значение, рассчитанное на повторяющемся фронте. Отсутствующие, дублированные или нерелевантные предыдущие блоки, а также неправильно введенные входные данные являются ошибками проверки.

Оценка и отбор

Whale AST оценивает цели вызова и подвыражения слева, где они указаны. Интерфейсный интерфейс выражает краткую оценку как ветвь потока управления.

Select выбирает одно из уже рассчитанных значений. Он не пропускает вычисление ни одного из входных данных. Например, даже если вы выберете безопасное значение, вы не сможете избежать trap, встречающегося при вычислении других входных данных. Вычисления, которые необходимо выполнить только по определенному пути, следует размещать внутри условного блока.

Отдел проверки trap

Некорректная IR — ошибка проверки. Проверка отвергает legacy undef; создайте IR заново из AST. Объявление без инициализатора использует uninit format 4 и проверки фактических чтений без замены нулём или произвольным значением. Нарушения условий исполнения дают определённые traps с позицией IR.

InterpreterTrap сообщает причину, исполненные шаги и ExecutionSite: ID функции, ID блока, индекс инструкции с нуля и необязательный ID результата. Индекс terminator следует за инструкциями. CLI также указывает файл. Typed IR пока не содержит исходных spans: это позиции IR, не номера строк исходника. Trap возвращает ошибку и прекращает последующее исполнение; библиотека не завершает процесс-хозяин.

Данная гарантия распространяется на проверенную IR и память трассировки. Внешняя встроенная сборка C·необработанный адрес· имеет отдельный контракт и не всегда обнаруживает нарушения за ее пределами. Пожалуйста, обратитесь к Модель памяти.

Формат обмена и представление текста

AST и typed IR используют соответствующие format version и общие semantics version. Читатель должен отклонить неверсионную/неизвестную версию/поле/функцию/дубликат ключа JSON. Конструкторам не следует предполагать, что неподдерживаемые свойства будут молча игнорироваться.

Целые числа передаются как числа разрядности ·signedness·. Константы с плавающей запятой передаются как ширина и точная битовая строка. Текст round-trip в IR должен сохранять имя·ID·тип·константу·последовательность·свойство·метаданные. Пространства и размещение комментариев сохранению не подлежат.

Доступны приведённый ниже контракт AST JSON и чтение, проверка и повторная печать typed IR format 4.

Печатаемые идентификаторы и имена в кавычках

typed IR format 4 печатает функции как @fN, глобальные значения как @gN, значения как %vN, блоки как %bN. Идентификаторы функций и глобальных значений принадлежат модулю; идентификаторы значений и блоков — содержащей их функции. Переданные ID сохраняются, включая пропуски в нумерации. Имена в кавычках служат описанием и не используются для разрешения ссылок. Функция явно задаёт entry %bN независимо от порядка хранения блоков.

Этот полный модуль проверен и напечатан через Rust API IR. Оба блока ветвления называются "branch", но ID различают определения и входы phi.

module {
  format_version 4
  semantics_version 1
  target "x86_64-whale-linux"
  datalayout { ptr=64, endian=little }

  declare @f0 "choose": whale (bool) -> i32, linkage internal

  fn @f0 "choose"(%v0 "condition": bool) -> i32, entry %b0 {
  %b0 "entry":
    cbr bool %v0, label %b1, label %b2
  %b1 "branch":
    %v1: i32 = const i32 1
    br label %b3
  %b2 "branch":
    %v2: i32 = const i32 2
    br label %b3
  %b3 "join":
    %v99: i32 = phi i32 [ %v1, %b1 ], [ %v2, %b2 ]
    ret i32 %v99
  }

}

%v0 определяется в списке параметров. %b1 и %b2 различны даже при одинаковых именах; phi указывает каждого предшественника по ID. Ветвления и цели switch используют тот же синтаксис ID блоков. Принтер не перенумеровывает явно заданный %v99.

Все поля имён и строк заключаются в двойные кавычки: цель, имена функций, глобальных значений, параметров и блоков, внешние имена связывания, имена объявлений констант и причины trap. Печатные символы Unicode сохраняются. Последовательности экранирования: \", \\, \n, \r, \t, \0, а для остальных управляющих символов и U+2028/U+2029 — \u{hex} со строчными шестнадцатеричными цифрами. Имя с переводом строки, табуляцией, кавычкой, обратной косой чертой и корейским текстом остаётся одной записью.

"line\ncolumn\tquote\"slash\\한글"

Читатель принимает typed IR formats 3 и 4 с semantics version 1 и всегда печатает format 4. uninit требует format 4. Прежние корректные инструкции format 3 читаются, но legacy undef в обоих форматах является ошибкой проверки и требует генерации из AST. Текст format 2 нужно вручную перевести на явные ID, имена в кавычках и ссылки на вход. AST JSON имеет отдельный format 2.

Чтение и проверка текстового IR

ir::parse_module читает typed IR, проверяет его и возвращает Module. Поддерживается текущий синтаксис печати скаляров, управления потоком, памяти, прямых/косвенных вызовов и константных выражений. Сохраняются ID, имена, типы, точные целые и биты float, деревья выражений и вычисленные результаты, порядок блоков, вход, выравнивание, сигнатуры и link_name. Пробелы и комментарии строк // приводятся к каноническому виду. Сохраните полный IR выше как answer.wir и выполните команды в стандартной сборке.

cargo run --locked -- ir verify answer.wir
cargo run --locked -- ir print answer.wir -o canonical.wir

Неизвестные версии, поля, инструкции и escape-последовательности, литералы вне диапазона, повторные ID, противоречивые типы и лишний ввод отклоняются. ParseError сообщает смещение в байтах и строку/столбец Unicode scalar начиная с 1. Ошибки проверки по возможности относятся к содержащей функции или глобальному объявлению. print тоже проверяет ввод и при ошибке сохраняет прежний файл. Новый синтаксис или семантика требуют изменения соответствующей format/semantics version; неизвестные версии — ошибки.

Ограничения ресурсов проверки

По умолчанию IrLimits допускает 8 MiB ввода, 1,000,000 токенов, 1,000,000 посещаемых узлов и глубину типов и выражений по 128. Глубина корня 0, каждый потомок добавляет 1. Глубину можно уменьшить или увеличить до MAX_IR_NESTING, 256. Превышение возвращает LimitError, VerifyError::ResourceLimit, ConstEvalError::ResourceLimit или CallError::ResourceLimit. Узлы считают обход на каждой входной границе, включая типы и выражения, а не время выполнения.

use ir::{parse_module_with_limits, print_module, IrLimits};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let source = std::fs::read_to_string("answer.wir")?;
    let limits = IrLimits {
        max_type_depth: 32,
        max_const_depth: 32,
        max_nodes: 10_000,
        ..IrLimits::default()
    };
    let module = parse_module_with_limits(&source, limits)?;
    let canonical = print_module(&module);
    let reread = parse_module_with_limits(&canonical, limits)?;
    assert_eq!(print_module(&reread), canonical);
    let invalid = source.replacen("format_version 4", "format_version 99", 1);
    assert!(parse_module_with_limits(&invalid, limits).is_err());
    Ok(())
}

Передавайте ограничения также в verify_module_with_limits, ConstExpr::evaluate_with_limits, validate_signature_with_limits и ModuleBuilder::declare_function_with_limits. Итеративный обход типов предшествует рекурсивным clone, сравнению и диагностике; выражения вычисляются рабочим стеком. Заимствованные деревья Rust и их Drop принадлежат вызывающему коду. Произвольные непроверенные деревья сохраняют рекурсивные clone/Drop; отклонённая собственная сигнатура checked API объявления освобождается итеративно. Некорректная IR — ошибка проверки. Проверка отвергает legacy undef; создайте IR заново из AST. Объявление без инициализатора использует uninit format 4 и проверки фактических чтений без замены нулём или произвольным значением. Нарушения условий исполнения дают определённые traps с позицией IR.

Указана версия AST JSON

Сохраните следующее как program.json. Все четыре поля конверта являются обязательными. program содержит обязательные массивы declarations, globals и functions, которые могут быть пустыми. Требуются имя функции, параметры, тип возвращаемого значения, тело, convention и linkage. link_name может отсутствовать или быть нулевым для внутренних функций и должен быть непустой строкой без NUL для внешних функций. Каждое перечисление использует либо свое имя модуля, либо один объект с ключом варианта. Варианты модуля также принимают объект с нулевым значением, например {"Void":null}; кодер выдает имя устройства "Void". VarDecl.init может отсутствовать или быть нулевым; другие обязательные поля должны присутствовать.

{
  "format_version": 2,
  "semantics_version": 1,
  "features": [],
  "program": {
    "globals": [],
    "functions": [
      {
        "name": "answer",
        "parameters": [],
        "return_type": {
          "Int": {
            "bits": 128,
            "signed": false
          }
        },
        "body": [
          {
            "Return": {
              "Lit": {
                "Int": {
                  "bits": 128,
                  "signed": false,
                  "value": "340282366920938463463374607431768211455"
                }
              }
            }
          }
        ],
        "convention": "Whale",
        "linkage": "Internal",
        "link_name": null
      }
    ],
    "declarations": []
  }
}
cargo run --locked --features socket-cli -- ir lower program.json
module {
  format_version 4
  semantics_version 1
  target "x86_64-whale-linux"
  datalayout { ptr=64, endian=little }

  declare @f0 "answer": whale () -> u128, linkage internal

  fn @f0 "answer"() -> u128, entry %b0 {
  %b0 "entry":
    %v0: u128 = const u128 340282366920938463463374607431768211455
    ret u128 %v0
  }

}

Целое число value представляет собой десятичную строку. Signed После необязательного минуса целого числа используется число. Пробелы, плюсы, показатели степени и разделители не допускаются. Допустимый диапазон определяется заявленной шириной и signedness. Вышеуказанный u128::MAX сохраняется в неизменном виде до JSON и lowering. Отрицательные значения unsigned или значения, выходящие за пределы диапазона, не являются wrap и являются ошибками. Значение Float использует шестнадцатеричную битовую строку точной ширины, описанной в Числовые операции.

format_version равен 2 для этого формата AST; semantics_version равен 1. features должен быть пустым массивом. Неизвестные поля, версии, функции, повторяющиеся необработанные ключи JSON (включая экранированные эквивалентные ключи) и конечные значения являются ошибками, даже с --no-verify. Точка входа в библиотеку — ir::lower_ast::interchange::decode; encode излучает конверт. decode по умолчанию имеет ограничение на исходный байт в 8 МБ; decode_with_limit принимает ограничение на количество вызывающих абонентов. Вложенность JSON ограничена. Используйте этот необработанный декодер вместо синтаксического анализа общей карты, которая уже может отбрасывать повторяющиеся ключи.

Полная схема JSON определяет формы, обязательные поля и варианты. Дополнительно применяются проверки диапазона/типа и обнаружение дубликатов ключей. Подмножество скалярного понижения включает в себя литералы, переменные/константы, добавление/под/муль, сравнения, присваивание, if/ while, возврат и разрыв/продолжение. Поддерживаются ссылки на функции, прямые и косвенные вызовы; агрегатные выражения не поддерживаются. Opaque представим на схеме, но не поддерживается понижением.

Для миграции старый Program без envelope оборачивается в envelope, а числовые литералы JSON заменяются десятичными строками целых или строками битов плавающей точки. Ввод без версии отклоняется. Формат 1 переводится в формат 2 с добавлением program.declarations (пустого массива, если не используется) и явных convention/linkage в определениях. Версии независимы: AST format 2, typed IR format 4 и semantics version 1.

Ввод отклонен и восстановление CLI

Сохраните следующий полный ввод как invalid.json.

{
  "format_version": 99,
  "semantics_version": 1,
  "features": [],
  "program": {
    "globals": [],
    "functions": [],
    "declarations": []
  }
}
cargo run --locked --features socket-cli -- ir lower invalid.json -o rejected.wir
Failed to parse socket JSON: unsupported AST format_version 99; expected 2

Команда завершает работу с ненулевым статусом и не создает новые выходные данные и не перезаписывает существующие файлы. Несоответствие типов также не будет выполнено перед публикацией вывода. Двоичные файлы, созданные без socket-cli, завершаются со статусом 2 и выводят команду восстановления, содержащую --features socket-cli.

Интерпретатор скалярных целых чисел

Интерпретатор по умолчанию исполняет целые/Bool, поток управления, стековые выделения, хранение и чтение указателей данных, typed GEP, memcpy и memset. Чтение checked-пар исключает padding. Адреса — синтетические 64-битные значения без разыменования памяти хоста. Аргументы и результаты ограничены целыми/Bool или void; float, вызовы, указатели функций, общие агрегатные значения, глобальные адреса и native-исполнение не поддержаны. Стековые выделения живут до возврата. Завершение лексического времени жизни, передача указателей при вызове/возврате, адаптеры внешней памяти и native shadow metadata требуют дальнейшей реализации.

По умолчанию InterpreterOptions::max_steps равен 1,000,000. Каждая исполненная инструкция, включая phi, и каждый terminator расходуют один шаг. Ноль останавливает перед первой операцией; бесконечный цикл возвращает InterpreterError::StepLimit. ir_limits отдельно ограничивает проверку. Неиспользуемая арифметика тоже исполняется и может вызвать trap. checked overflow возвращает Bool; только явный trap_if делает его trap.

Сохраните полный модуль как swap-loop.wir. Вход задан явно, хотя выходной блок хранится первым. При входе в блок все входы phi читаются из значений предшественника до записи любых результатов phi. Три итерации меняют местами 11 и 22 трижды и возвращают 22.

module {
  format_version 4
  semantics_version 1
  target "x86_64-whale-linux"
  datalayout { ptr=64, endian=little }

  declare @f7 "swap_loop": whale (u32) -> i32, linkage internal

  fn @f7 "swap_loop"(%v0 "iterations": u32) -> i32, entry %b11 {
  %b90 "exit":
    ret i32 %v5
  %b11 "entry":
    %v1: i32 = const i32 11
    %v2: i32 = const i32 22
    %v3: u32 = const u32 0
    %v4: u32 = const u32 1
    br label %b20
  %b20 "loop":
    %v5: i32 = phi i32 [ %v1, %b11 ], [ %v6, %b30 ]
    %v6: i32 = phi i32 [ %v2, %b11 ], [ %v5, %b30 ]
    %v7: u32 = phi u32 [ %v3, %b11 ], [ %v9, %b30 ]
    %v8: bool = icmp ult u32 %v7, %v0
    cbr bool %v8, label %b30, label %b90
  %b30 "next":
    %v9: u32 = add u32 %v7, %v4
    br label %b20
  }

}
whale ir run swap-loop.wir --function @f7 --arg 3 --max-steps 100
i32 22

Rust API возвращает значение и число шагов либо структурированную ошибку проверки, неподдерживаемой операции, аргументов, лимита или trap. Полная программа читает тот же файл swap-loop.wir:

use ir::{interpret_with_options, parse_module, ConstValue, FunctionId,
         InterpreterError, InterpreterOptions};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let source = std::fs::read_to_string("swap-loop.wir")?;
    let module = parse_module(&source)?;
    let options = InterpreterOptions {
        max_steps: 100,
        ..InterpreterOptions::default()
    };
    let result = interpret_with_options(
        &module, FunctionId(7), &[ConstValue::U(3)], options,
    )?;
    assert_eq!(result.value, Some(ConstValue::I(22)));
    assert_eq!(result.steps, 32);
    let stopped = interpret_with_options(
        &module, FunctionId(7), &[ConstValue::U(3)],
        InterpreterOptions { max_steps: 0, ..options },
    );
    assert!(matches!(stopped, Err(InterpreterError::StepLimit { .. })));
    println!("i32 22");
    Ok(())
}