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:
val = some_func()
if val != 42 {
io.println("FAIL: expected 42")
return
}Такой подход имеет три проблемы:
- Много шаблонного кода: каждое утверждение требует 4 строки, файлы тестов раздуваются
- Слабые сообщения об ошибках: ручная конкатенация строк, отсутствие позиции в исходном коде
- Некомпозируемость: невозможно пакетно регистрировать утверждения, невозможно передать их в качестве аргумента в тестовый фреймворк
Текущие проблемы
- Нет единого механизма утверждений
- Код тестов заполнен паттерном
if+ печать +return - На уровне байткода уже есть инструкция
Throw, но на уровне языка она не доступна - RFC-011 определяет компилируемый условный тип
Assert(C), но runtimeassert()пока не реализован
Принципы проектирования
assert — единственный пользовательский механизм panic в YaoXiang. assert(false, "msg") эквивалентно raise, отдельное ключевое слово throw/raise не требуется. Сама функция assert является лучшей инкапсуляцией для if raise.
Никаких новых ключевых слов, никакого нового синтаксиса. Всё — вызовы функций.
Вариант A: native-функция
Реализовать assert как native-функцию без введения нового ключевого слова.
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:
assert(1 + 1 == 2) // Без сообщения, panic с информацией по умолчанию
assert(1 + 1 == 2, "math is broken") // Строковое сообщение
assert(x > 0, my_error) // Прямой выброс значения Errorassert(false, "msg") — это эквивалент raise/throw в YaoXiang — отдельное ключевое слово не нужно.
Перегрузка 2: Result-утверждение (Result)
Один аргумент Result, автоматически проверяется, является ли он Err:
Достоинства
- Нулевые изменения синтаксиса: чистая функция, без новых ключевых слов
- Нулевые новые концепции: повторно используется существующий механизм регистрации native-функций
- Высокая расширяемость: перегрузка функций естественно поддерживает множественные сигнатуры
- Самодокументируемость: пространство имён
std.assertсамо по себе является документацией
Недостатки
- Отсутствуют. При корректной сигнатуре типа assert компилятор может выводить мёртвый код через анализ достижимости функций. Дополнительные проходы не нужны.
Поведение в runtime
- Вычислить первый аргумент
condition: Bool - Если
true, вернутьUnit - Если
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 → вставить check | Bool-проверка + инжекция уточняющего факта в flow-sensitive assumption set Γ |
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 check2026-07-12 унифицированное решение: предыдущий вывод о «полной независимости» отменён.
assert()— это value-eliminator дляAssert, автоматически распределяемый через dispatch.
Изменения в компиляторе
Изменения в parser, AST, typecheck, IR gen не требуются.
Достаточно добавить регистрацию native-функции в src/std/:
- Добавить
src/std/assert.rs - Зарегистрировать
std.assert.assertиstd.assert.Assert(последний — компилируемый условный тип) - Внутри вызвать уже существующую инструкцию
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. Не нужно выбирать между «функцией» и «ключевым словом». Ниже — историческая запись.
assert(1 + 1 == 2, "math is broken")Сигнатура типа
Нет отдельной сигнатуры типа — ключевое слово обрабатывается parser.
Поведение в runtime
Аналогично варианту A.
Изменения в компиляторе
Требуются изменения в parser, AST, typecheck, IR gen:
- parser: добавить вариант
Expr::Assert - AST: добавить узел
Expr::Assert - typecheck: валидация типов аргументов
- 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]
Нужны ли варианты→ Не нужны. YAGNI. Подождём, пока оформится тестовый фреймворк.assert_eq,assert_ne? - [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 в других языках. Поэтому он должен покрывать три сценария:
- Условие + простое сообщение:
assert(cond, "msg") - Условие + пользовательский Error:
assert(cond, my_error) - Проверка Result:
assert(result)— самый краткийif is_err { panic }
Обоснованность перегрузки Result в том, что это кратчайший путь распространения ошибки — «Result должен быть Ok, иначе смерть». Не нужно сначала .is_ok(), а затем отдельно обрабатывать ошибку.
Приложение B: запись проектных решений
| Решение | Определение | Дата | Автор |
|---|---|---|---|
| Выбор между вариантом A и B | Унифицированное решение: пайплайн dispatch снимает противоречие A/B, assert — value-eliminator для Assert | 2026-07-12 | Чэньсюй |
| Опциональность message | Да: assert(cond, ?msg), String или Error | 2026-07-11 | Чэньсюй |
| Нужны ли варианты вроде assert_eq | Не нужны. YAGNI, подождём тестовый фреймворк | 2026-07-11 | Чэньсюй |
| Нужно ли отдельное ключевое слово raise/throw | Не нужно. assert(false, msg) эквивалентно raise | 2026-07-11 | Чэньсюй |
| Связь между assert и Assert | Две стороны одной медали. assert: (Bool) -> Assert(IsTrue(cond)), автоматическое распределение через dispatch | 2026-07-12 | Чэньсюй |
Ссылки
- RFC-007: унифицированный синтаксис определения функций — модель
name: type = value - RFC-010: унифицированный синтаксис типов — основы type system
- RFC-011: проектирование системы generics §4.3 — компилируемая верификация и условный тип
Assert(C) - RFC-026: базовый механизм FFI — механизм регистрации native-функций
- RFC-027: компилируемые предикаты и унифицированная статическая верификация — система компилируемого вычисления
