Skip to content

RFC-030: Механизм assert утверждений

Краткое описание

Введение механизма assert утверждений для YaoXiang, используемого для тестирования, проверки предусловий и panic во время выполнения. assert и тип-уточнение компиляции Assert(C) (см. RFC-011 §4.3) являются двумя сторонами одной уточняющей сущности — dispatch автоматически распределяет в зависимости от того, "свободны ли переменные-предиката в период компиляции", на доказательство компиляции или проверку во время выполнения. assert(false, "msg") эквивалентен raise, отдельное ключевое слово throw/raise не требуется.

Мотивация

Зачем нужна эта функциональность?

Текущие E2E тесты YaoXiang могут использовать только if + io.println + return для симуляции утверждений:

yaoxiang
val = some_func()
if val != 42 {
    io.println("FAIL: expected 42")
    return
}

Такой подход имеет три проблемы:

  1. Много шаблонного кода: для каждого утверждения требуется 4 строки, файлы тестов раздуваются
  2. Слабые сообщения об ошибках: ручное конструирование строк, отсутствие информации о позиции в исходном коде
  3. Некомбинируемость: невозможно массово регистрировать утверждения, передавать их как параметры в тестовый фреймворк

Текущие проблемы

  • Нет единого механизма утверждений
  • Тестовый код заполнен паттернами if + печать + return
  • Инструкция Throw уже есть на уровне байткода, но не экспортирована на уровне языка
  • RFC-011 определяет условный тип компиляции Assert(C), но assert() для времени выполнения ещё не реализован

Принципы проектирования

assert — единственный механизм пользовательского panic в YaoXiang. assert(false, "msg") эквивалентен raise, отдельное ключевое слово throw/raise не требуется. Функция assert сама по себе является наилучшей обёрткой для if raise.

Не вводить новые ключевые слова, не вводить новый синтаксис. Всё — вызовы функций.

Решение А: native функция

Реализация assert как native функции, без введения нового ключевого слова.

yaoxiang
use std.assert.assert

main = {
    assert(1 + 1 == 2, "math is broken")
    assert(get_name() == "YaoXiang", "name mismatch")
}

Перегруженные сигнатуры

assert имеет две перегрузки:

// Основная сигнатура: assert является введением значения в宇宙 Assert
assert: (cond: Bool, ?msg: String | Error) -> Assert(IsTrue(cond))
//                                       ^^^^^^^^^^^^^^^^^^^^^^^^
//                                       Возвращает уточнённый тип, не ()
//
// IsTrue: Bool -> Type — мост от истинности к типу:
//   IsTrue(true)  = Void   (⊤, программа продолжается)
//   IsTrue(false) = Never  (⊥, расхождение/ошибка компиляции)

Фактическое поведение assert определяется dispatch-распределением:

  • Все свободные переменные известны в период компиляции → CompileTime: компилятор вычисляет cond, true → стирается в Void, false → ошибка компиляции (Never необитаем)
  • Существуют свободные переменные времени выполнения → Runtime: вставляется проверка, в потоково-чувствительное множество допущений Γ впрыскиваются уточнённые факты

Необязательное сообщение ?msg и перегрузка Result (см. ниже) сохраняются как полезная нагрузка raise во время выполнения.

Перегрузка 1: Условное утверждение (Bool, ?String | Error)

Bool + необязательное сообщение. Сообщение может быть значением String или Error:

yaoxiang
assert(1 + 1 == 2)                    // Без сообщения, информация panic по умолчанию
assert(1 + 1 == 2, "math is broken")   // Строковое сообщение
assert(x > 0, my_error)                // Прямой выброс значения Error

assert(false, "msg") — эквивалент raise/throw в YaoXiang — отдельное ключевое слово не нужно.

Перегрузка 2: Утверждение Result (Result)

Один параметр Result, автоматическая проверка на Err:

Преимущества

  • Нулевые изменения синтаксиса: чистая функция, новые ключевые слова не нужны
  • Нулевые новые концепции: повторное использование существующего механизма регистрации native функций
  • Высокая расширяемость: перегрузка функций естественно поддерживает множественные сигнатуры
  • Самодокументирование: пространство имён std.assert само по себе является документацией

Недостатки

  • Отсутствуют. Когда сигнатура типа assert корректна, компилятор может вывести мёртвый код через анализ достижимости функций. Дополнительный pass не требуется.

Поведение во время выполнения

  1. Вычислить первый аргумент condition: Bool
  2. Если true, вернуть Unit
  3. Если false, вызвать panic во время выполнения:
    • Вывести содержимое message (если есть)
    • Вывести стек вызовов (в режиме debug)
    • Завершить текущее выполнение

Поведение при сбое для разных перегрузок

СигнатураПоведение при сбое
assert(false)Информация panic по умолчанию
assert(false, "msg")Вывести строковое сообщение затем panic
assert(false, error_val)Выбросить значение Error
assert(Err(x))Извлечь содержимое Err и panic

Связь с Assert компиляции

assert и Assertдве стороны одной уточняющей сущности — dispatch-конвейер автоматически выбирает в зависимости от того, "доступны ли свободные переменные предиката в период компиляции":

УсловиеРаспределениеПоведение
Все свободные переменные известны в период компиляцииCompileTime → конвейер доказательстваProved → стирание, Disproved → ошибка компиляции, Unknown → требуется доказательство
Существуют свободные переменные времени выполненияRuntime → вставка проверкиПроверка Bool + впрыскивание уточнённых фактов в множество допущений Γ
yaoxiang
use std.assert

# Известно в период компиляции (параметры generics) — CompileTime, нулевые накладные расходы времени выполнения
Array: (T: Type, N: Int) -> Type = {
    data: Array(T, N),
    length: assert.Assert(N > 0),   # N — параметр generics, вычисляется в период компиляции
}

# Значение времени выполнения —— Runtime, вставка проверки Bool
x = read_int()
assert.assert(x > 0, "expected positive")  # проверка во время выполнения

2026-07-12 Единое решение: предыдущий вывод "полностью независимые" заменён. assert() является введением значения в Assert, dispatch автоматически распределяет.

Изменения в компиляторе

Изменения в парсере, AST, проверке типов, генерации IR не требуются.

Достаточно добавить регистрацию native функции в src/std/:

  1. Добавить src/std/assert.rs
  2. Зарегистрировать std.assert.assert и std.assert.Assert (последний — условный тип компиляции, см. #155)
  3. Внутренне вызвать существующую инструкцию BytecodeInstr::Throw

Преимущества

  • Нулевые изменения синтаксиса: чистая функция, новые ключевые слова не нужны
  • Нулевые новые концепции: повторное использование существующего механизма регистрации native функций
  • Высокая расширяемость: сигнатуры функций можно расширять для вариантов вроде assert_eq (в будущем)
  • Самодокументирование: пространство имён std.assert само по себе является документацией

Недостатки

  • Неизвестно в период компиляции: в отличие от решения B (ключевое слово), невозможно выполнить мёртвый код elimination в период компиляцииВ едином решении это неприменимо. assert в режиме CompileTime идёт через конвейер доказательства, cond, известная в период компиляции → стирание или ошибка компиляции (assert(false) → Never → мёртвый код).
  • Стек вызовов доступен только в режиме debug

Решение B: Встроенное ключевое слово (заменено единым решением)

Устарело. Противопоставление решения A и B разрешено dispatch-конвейером — assert является введением значения в Assert, компиляция известна через конвейер доказательства (нулевые накладные расходы), выполнение через проверку. Нет необходимости выбирать между "функцией" и "ключевым словом". Ниже — историческая запись.

yaoxiang
assert(1 + 1 == 2, "math is broken")

Сигнатура типа

Нет независимой сигнатуры типа — ключевое слово обрабатывается парсером.

Поведение во время выполнения

Аналогично решению A.

Изменения в компиляторе

Требуются изменения в парсере, AST, проверке типов, генерации IR:

  1. Парсер: добавить вариант Expr::Assert
  2. AST: новый узел Expr::Assert
  3. Проверка типов: валидация типов параметров
  4. Генерация IR: сгенерировать BytecodeInstr::Throw

Преимущества

  • Исходная позиция известна в период компиляции (не зависит от debug info)
  • Константная свёртка в период компиляции: assert(true) → пустая операция, assert(false) → ошибка компиляции

Недостатки

НедостатокВлияние
Требуются изменения парсераВведение нового синтаксического узла, увеличение стоимости поддержки
Ключевое слово нерасширяемоВарианты вроде assert_eq всё равно требуют функций
Преимущество компиляции непрактичноСм. анализ ниже

Сравнение

ИзмерениеРешение A (функция)Решение B (ключевое слово)
Стоимость реализации~20 строкparser + AST + typecheck + IR gen
Изменение синтаксисаНетНовое ключевое слово
РасширяемостьПерегрузка функцийТребуется配套 макросов
Позиция в исходникеdebug infoДоступна в период компиляции
Константная свёрткаТребуется pass поддержкиДоступна в период компиляции
Накладные расходы выполненияВызов функцииМинимальные

Практические ограничения анализа компиляции

Ключевое преимущество решения B — анализ в период компиляции — требует pass константной свёртки для реализации. То есть компилятор должен вычислить false в assert(false) в период компиляции, чтобы определить мёртвый код.

У YaoXiang сейчас нет pass константной свёртки. Даже с решением B, типичные конструкции вроде assert(x > 0) всё равно не могут быть проанализированы в период компиляции. Только литералы assert(true) / assert(false) могут быть проанализированы.

Таким образом, преимущество решения B в период компиляции является теоретическим, а не практическим на текущем этапе.


Открытые вопросы

  • [x] Выбрать решение A или решение B?Единое решение: assert является введением значения в Assert. Противопоставление A/B разрешено dispatch-конвейером — компиляция известна через конвейер доказательства, выполнение через проверку. "Бинарный выбор" не нужен.
  • [x] Требуется ли для assert поддержка упрощённой формы без message assert(cond)?Да. assert(cond, ?msg), message необязательно.
  • [x] Нужны ли варианты вроде assert_eq, assert_ne?Нет. YAGNI. Когда сформируется тестовый фреймворк — тогда и посмотрим.
  • [x] Должен ли вывод panic содержать исходную позицию? → Решение A зависит от debug info (стек вызовов).
  • [x] Проблема объединения assert / AssertРешено. Единое решение: assert: (Bool) -> Assert(IsTrue(cond)), две стороны одной медали, dispatch автоматически распределяет. Подробнее #156 (закрыт). Тип Never (⊥) как возвращаемый тип assert(false) встроен.

2026-07-05: Выбор решения A (заменено единым решением)

Решение A с реализацией в 20 строк побеждает по соотношению цена/качество. После определения единого решения 2026-07-12, противопоставление A/B разрешено dispatch-конвейером — assert является введением значения в Assert, больше нет "бинарного выбора" между "функцией" и "ключевым словом".

2026-07-12: Определено единое решение (заменяет заключение "полностью независимые" от 2026-07-11)

Вывод: assert и Assert — не два независимых механизма. assert: (Bool) -> Assert(IsTrue(cond)) — dispatch автоматически распределяет:

  • Известно в период компиляции → в конвейер доказательства (Proved стирается / Disproved ошибка / Unknown требуется доказательство)
  • Входные данные времени выполнения → вставка проверки + впрыскивание допущений Γ

Структура модуля: std.assert единообразно содержит утверждения времени выполнения (assert) и типы-уточнения компиляции (Assert, IsTrue). Не "реализовывать раздельно", а две стороны одной сущности.

2026-07-11: Дизайн перегрузки assert

Вопрос: Зачем assert нужны две перегрузки, а не единая (Bool, ?String)?

Ответ:

assert() во время выполнения — единственный механизм пользовательского panic в YaoXiang. assert(false, "msg") эквивалентен raise/throw в других языках. Поэтому он должен покрывать три сценария:

  1. Условие + простое сообщение: assert(cond, "msg")
  2. Условие + пользовательская Error: assert(cond, my_error)
  3. Проверка Result: assert(result) — наиболее лаконичный if is_err { panic }

Обоснованность перегрузки Result в том, что это кратчайший путь распространения ошибок — "Result должен быть Ok, иначе смерть". Не нужно сначала .is_ok() затем отдельно обрабатывать ошибку.

Приложение B: Запись решений по дизайну

РешениеРешениеДатаЗаписал
Выбрать решение A или BЕдиное решение: dispatch-конвейер разрешает A/B противопоставление, assert — введение значения в Assert2026-07-12晨煦
Является ли message необязательнымДа: assert(cond, ?msg), String или Error2026-07-11晨煦
Нужны ли варианты assert_eq и т.д.Нет. YAGNI, когда сформируется тестовый фреймворк2026-07-11晨煦
Нужно ли отдельное ключевое слово raise/throwНет. assert(false, msg) эквивалентен raise2026-07-11晨煦
Связь assert и AssertДве стороны одной медали. assert: (Bool) -> Assert(IsTrue(cond)), dispatch автоматически распределяет2026-07-12晨煦

Ссылки