Skip to content

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

Резюме ​

Ввести в YaoXiang механизм assert-утверждений для тестирования, проверки предусловий и runtime panic. assert и компилируемый уточняющий тип Assert(C) (см. RFC-011 §4.3) — это две стороны одного и того же уточняющего примитива — dispatch автоматически распределяет их на компилируемое доказательство или runtime-проверку по принципу «достижимы ли свободные переменные предиката на этапе компиляции». 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), но runtime assert() пока не реализован

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

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

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

Вариант A: native-функция ​

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

yaoxiang
use std.assert.assert

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

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

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

// Основная сигнатура: assert — это value-universe eliminator для 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-свободные переменные → Runtime: вставляется проверка, в flow-sensitive assumption set Γ инжектируется уточняющий факт

Опциональное сообщение ?msg и перегрузка Result (см. ниже) сохраняются как payload для runtime 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 компилятор может выводить мёртвый код через анализ достижимости функций. Дополнительные проходы не нужны.

Поведение в runtime ​

  1. Вычислить первый аргумент condition: Bool
  2. Если true, вернуть Unit
  3. Если false, вызвать runtime 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-свободные переменныеRuntime → вставить checkBool-проверка + инжекция уточняющего факта в flow-sensitive assumption set Γ
yaoxiang
use std.assert

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

# Runtime-значение — идёт по Runtime, вставляется Bool-проверка
x = read_int()
assert.assert(x > 0, "expected positive")  # Runtime check

2026-07-12 унифицированное решение: предыдущий вывод о «полной независимости» отменён. assert() — это value-eliminator для Assert, автоматически распределяемый через dispatch.

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

Изменения в parser, AST, typecheck, IR gen не требуются.

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

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

Достоинства ​

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

Недостатки ​

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

Вариант B: встроенное ключевое слово (отменён в пользу унифицированного решения) ​

Отменён. Противоречие между вариантами A и B снимается пайплайном dispatch — assert является value-eliminator для Assert, при известном на этапе компиляции идёт через пайплайн доказательства (нулевые runtime-затраты), в runtime — через check. Не нужно выбирать между «функцией» и «ключевым словом». Ниже — историческая запись.

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

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

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

Поведение в runtime ​

Аналогично варианту A.

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

Требуются изменения в parser, AST, typecheck, IR gen:

  1. parser: добавить вариант Expr::Assert
  2. AST: добавить узел Expr::Assert
  3. typecheck: валидация типов аргументов
  4. IR gen: генерация BytecodeInstr::Throw

Достоинства ​

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

Недостатки ​

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

Сравнение ​

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

Реалистичные ограничения анализа на этапе компиляции ​

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

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

Следовательно, преимущества варианта B на этапе компиляции на текущей стадии теоретические, а не практические.


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

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

2026-07-05: выбран вариант A (отменён в пользу унифицированного решения) ​

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

2026-07-12: определено унифицированное решение (отменяет вывод от 2026-07-11 о «полной независимости») ​

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

  • Известно на этапе компиляции → идёт в пайплайн доказательства (Proved стирается / Disproved — ошибка / Unknown — требуется доказательство)
  • Runtime-вход → вставляется check + инжекция предположения в Γ

Структура модуля: std.assert единообразно несёт runtime-утверждения (assert) и компилируемые уточняющие типы (Assert, IsTrue). Больше не «раздельная реализация», а две стороны одного примитива.

2026-07-11: проектирование перегрузок assert ​

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

Ответ:

Runtime 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 — value-eliminator для 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)), автоматическое распределение через dispatch2026-07-12Чэньсюй

Ссылки ​