Runtime — режимы запуска, panic, prelude, статическое состояние
Решения этой группы определяют, как программа Nova исполняется: поддерживаемые режимы компиляции, что считается panic’ом и как он обрабатывается, что предоставляет prelude и почему в языке нет static-состояния.
| # | Решение |
|---|---|
| D7 | Один язык — три режима компиляции |
| D13 | Panic vs эффекты: что НЕ является эффектом |
| D26 | Базовая stdlib и prelude |
| D41 | Static-функции есть, static-состояния нет |
| D70 | ⚠️ REPLACED → D73 (migration map only) |
| D73 | From / Into protocol-пара с авто-выводом |
| D74 | Математические операции на числовых типах — instance-методы |
| D77 | TryFrom / TryInto — расширение D73 для fallible-конверсий |
| D76 | Mem эффект — runtime introspection для leak/growth тестов |
| D81 | assert(cond) vs debug_assert(cond) — build-mode семантика |
| D141 | Примитивы доступа к памяти — byte_at / bulk slice-операции |
| D177 | str Nova-body dispatch — Plan 54 Ф.2 extension |
| D178 | str API cleanup и расширения — Plan 91 Ф.2.6 |
| D179 | StringBuilder — pure Nova consume type — Plan 91 Ф.2.6 |
D7. Один язык — три режима компиляции
Что
Один и тот же исходник Nova поддерживает три режима исполнения: AOT (бинарь, как Go), JIT (как .NET) и интерпретатор (как Python). Скрипт за 1 строку и сервер на 100k строк — это разные режимы запуска одного языка, а не разные языки.
Правило
nova run script.nv # интерпретатор / JIT (быстрый старт)
nova build app.nv # AOT-бинарь, как `go build`
nova jit-server # долгоиграющий процесс с JIT-компиляцией
Один и тот же script.nv без модификации работает во всех трёх
режимах. Эффекты, типы, контракты, handler’ы — везде ведут себя
одинаково.
Почему
- Скрипт vs сервер — это режимы запуска. Не разные языки. Программисту не нужно «переписывать» под другой режим.
- Прецедент Julia — тот же подход (JIT по умолчанию + AOT через
PackageCompiler.jl) работает на масштабе data-science. - AI-first — LLM может генерировать код и запускать через интерпретатор для быстрой проверки, а тот же код собирать в бинарь для production.
- Эффекты ортогональны runtime’у — handler’ы перехватываются и в JIT, и в AOT, и в интерпретаторе одинаково.
Что отвергнуто
- Только AOT (Rust/Go-стиль) — медленный feedback loop, плохо для скриптов и REPL.
- Только интерпретатор (Python) — производительность недостаточна для backend.
- Транспиляция в чужой язык (TypeScript → JS) — теряется возможность контроля runtime, привязка к чужой экосистеме.
Связь
- 01-philosophy.md → D9 — «три режима компиляции в строго типизированном языке» — одна из двух потенциальных уникальных заявок Nova.
- 01-philosophy.md → D10 — три режима следуют из «всё — эффект»: handler’ы абстрагируют runtime.
Открытые вопросы
- Конкретные технологии: LLVM для AOT? Cranelift для JIT? Tree-walking для интерпретатора? — выбор реализации.
- Совместимость артефактов между режимами — пока считаем, что один исходник, разные бинарные форматы.
D13. Panic vs эффекты: что НЕ является эффектом
Что
Не каждое прерывание вычисления — эффект. Аппаратные/математические
сбои (деление на ноль, выход за границы массива, переполнение, OOM,
переполнение стека) не указываются в сигнатуре функции. Они
образуют общую категорию Panic — runtime-сбоев, перехватываемых
runtime’ом на границе fiber’а, не программистом в коде.
Правило
Граница
| Видимое (в сигнатуре) | Универсальное (не в сигнатуре) | |
|---|---|---|
| Что | эффекты, описывающие намерение | сбои, описывающие невозможность вычисления |
| Примеры | Net, Db, Time, Log, Fail[BusinessError] | деление на ноль, переполнение, выход за границы, OOM, переполнение стека |
| Где ловится | handler’ом в коде | runtime’ом на границе fiber’а |
| Как создаётся | throw | panic(msg) или сам runtime |
Перехват — на границе fiber’а runtime’ом
panic означает смерть текущего fiber’а, не процесса. Что это
значит для процесса в целом — зависит от runtime-окружения
(06-concurrency.md → D14):
- HTTP-handler — fiber на запрос. Panic = смерть fiber’а, runtime возвращает 500, остальные запросы продолжают.
- Worker очереди — fiber. Panic = задача упала, scheduler берёт следующую.
- Supervised group — supervisor видит «fiber завершился panic’ом», рестартует по своей стратегии.
- Синхронная программа без fiber-runtime (CLI-скрипт): fiber один
и совпадает с процессом, panic эффективно гасит процесс — но это
следствие топологии, не семантика panic’а. Если нужно гарантированно
убить процесс независимо от окружения — отдельная функция
exit.
fn handle_request(r Request) Db Log -> Response =>
process(r) // если panic — fiber умирает, runtime вернёт 500
// если throw — handler выше ловит обычно
fn server() Net Fail -> () {
supervised {
spawn handle_requests()
spawn periodic_cleanup()
} strategy = one_for_one, max_restarts = 3
// supervisor рестартует упавшие fiber'ы
}
Никакого try_panic/catch в коде. Программист не ловит
panic в обычной функции — это работа runtime’а на границе fiber’а.
Если программист хочет управляемую ошибку — пишет throw +
Fail[E], ловит обычным handler’ом.
Три уровня катастрофы
| Уровень | Конструкция | Что убивает | Перехват |
|---|---|---|---|
| Управляемая ошибка | throw err + Fail[E] | ничего, передаётся handler’у | handler’ом в коде (04-effects.md → D25) |
| Сбой fiber’а | panic(msg) | текущий fiber | runtime’ом на границе fiber’а; supervisor может рестартовать |
| Смерть процесса | exit(code, msg) | весь процесс | не перехватывается — процесс гасится с указанным exit code |
Никаких try_panic { ... } catch p { ... } или
panic_boundary { ... } recover (p) => { ... } в языке. exit
тем более не перехватывается — это финальная точка.
Когда какой использовать
throw err— контролируемая ошибка с информацией о причине. Всё, что вызывающий может осмысленно обработать. Дефолт.panic(msg)— поломан локальный инвариант, текущему вычислению дальше не жить, но процесс/сервер продолжают. Пример: «не должно случиться» в коде, который часть большого приложения.exit(code, msg)— поломан глобальный инвариант стартапа или операционной среды, продолжать процесс бессмысленно. Пример: битый конфиг при загрузке, нет доступа к критическим ресурсам, CLI завершает работу с конкретным exit code для скриптов.
// throw — обычная управляемая ошибка
fn parse(s str) Fail[ParseError] -> int =>
if !valid(s) { throw ParseError.BadFormat } else { ... }
// panic — поломан локальный инвариант
fn pop_nonempty(mut stack []int) -> int {
if stack.is_empty() { panic("pop_nonempty called on empty stack") }
stack.pop()
}
// exit — нечего продолжать
fn main() Io -> () {
let cfg = load_config("/etc/app.toml")
?? exit(1, "config not found at /etc/app.toml")
run(cfg)
}
exit — детали
- Сигнатура:
fn exit(code int, msg str) -> never.code— exit code для процесса (по конвенции 0 = успех, ≥1 = ошибка).msgвыводится в stderr перед завершением; пустая строка — без сообщения. - Не вызывает defer’ы / handler’ы. Процесс гасится, стек не
разворачивается. Если нужен cleanup — программист пишет его до
exit. - В тестах runtime тестов перехватывает
exitи превращает в fail теста (иначе один тест убил бы всю прогонку). Это деталь test-runner’а, не часть языкового контракта. - Прецеденты: C
exit(code), Goos.Exit(code), Ruststd::process::exit(code), Pythonsys.exit(code)— везде отдельная функция от panic-аналога, везде не вызывает destructor’ы / defer’ы.
Опция: строгий режим #strict_total
Для критичного кода (медицина, финансы, авионика):
#strict_total
fn critical(...) -> Result =>
// деление на ноль здесь — compile error
// обязаны checked-операции: safe_div(a, b)?, arr.get(i)?
Превращает функцию в тотальную (всегда завершается). Цена — больше кода, но для 1% случаев это окупается.
Почему
Если бы Fail[DivByZero] был обязателен, он бы появился в каждой
второй сигнатуре (любая функция со средним арифметическим,
дисперсией, делением). К нему присоединились бы Fail[IntegerOverflow],
Fail[ArrayBounds]. Это синдром Java checked exceptions —
информативность сигнатуры исчезает, потому что эффекты везде.
Сознательный компромисс: строгая теория эффектов уступает читабельности в зоне аппаратных сбоев.
Что НЕ Panic, а обычный эффект
- Бизнес-ошибки парсинга, валидации, аутентификации →
Fail[E]. - Network failure, DB connection refused →
Fail[NetError],Fail[DbError]внутри эффектаNet/Db. - Любая ошибка, которую программа намерена обрабатывать, — это не Panic.
Принцип: «обработать никак нельзя, надо умереть» → Panic; «обработать можно и нужно» → Fail.
Что отвергнуто
Fail[DivByZero]для каждой функции — спам в сигнатурах.try_panic/catchв обычном коде — путает сFail, усложняет reasoning о потоке управления.- Panic как обычное Throwable (Java RuntimeException) — приводит
к ловле «всего» через
catch (Exception e), антипаттерн.
Связь
- 04-effects.md → D25 —
throwиFail[E]. - 06-concurrency.md → D14 — supervisor, fiber’ы.
- 01-philosophy.md → D10 — «всё — эффект» с оговоркой про runtime panics.
D26. Базовая stdlib и prelude
Что
Базовые типы (Option[T], Result[T, E], Error, never,
Ordering) и их конструкторы (Some, None, Ok, Err) живут в
prelude — автоматически в скоупе любого модуля, без import.
Список prelude явно зафиксирован в одном месте, не «магия».
Bootstrap-расширение (Plan 35 sub-plan 35.A R27, 2026-05-12): большая часть prelude (
Option/Result/Some/None/Ok/Err/Error/never/println/panic) реализована hardcoded в type-checker’е и codegen’е. Параллельноcompiler-codegen::importsauto-импортируетstd/prelude.nvесли файл существует — это opt-in mechanism для расширения prelude из пользовательского кода (или для миграции hardcoded items в file-based form). Bootstrap MVP:std/prelude.nvсодержит placeholderPRELUDE_VERSION = 1.Plan 62 (закрыт 2026-05-18,
PRELUDE_VERSION = 3): большая часть prelude мигрирована в file-based декларацииstd/prelude/*.nv:
std/prelude/core.nv—Option/Result/Some/None/Ok/Err/Error/Ordering. Bottom-типnever— строчный встроенный примитив (Plan 76), в prelude не объявляется (какint/bool).std/prelude/runtime.nv—panic/exit/assert/debug_assert(printlnmigrated в Plan 62.B.bis —PRELUDE_VERSION = 7, 2026-05-18).std/prelude/errors.nv—RuntimeError(6 variants) +ReadBufferError(RuntimeNoneErrordeferred — bootstrap parser не поддерживает empty-body sum syntax).std/prelude/collections.nv—Iter[T]formal protocol declaration.std/prelude/protocols.nv—From/Into/Hashable/Equatable/Comparable/Display(6 formal protocols;TryFrom/TryIntodeferred — Plan 56 Ф.2.7 effect-row enforcement).std/prelude/effects.nv—Fail[E]formal effect declaration.Plan 62.D bis-1 (закрыт 2026-05-18,
PRELUDE_VERSION = 4):Range/RangeIterre-export через prelude facade изstd.collections.range. Раньше эта строка триггерила 4 latent codegen bugs (закрыты в bis-1).Plan 62.F.bis (закрыт 2026-05-18,
PRELUDE_VERSION = 5):
- Edition versioning (D124):
[package].edition = "2026.05"вnova.toml→ resolver auto-импортируетstd/prelude/e2026_05.nvвместо rolling facade. Mirror Rust’sedition = "2021". См. D124.- Structured W_PRELUDE_SHADOW lint (D125): user-declaration shadowing prelude-imported имени → structured lint warning через
lints::lint_prelude_shadow. Suppress:module X allow_prelude_shadowclause. См. D125.Time/Memformal effect declarations добавлены вstd/prelude/effects.nv(codegen dispatch неизменен через pre-registeredeffect_schemas).Plan 62.D.bis (закрыт 2026-05-18,
PRELUDE_VERSION = 6): StringBuilder/WriteBuffer/ReadBuffer formally declared черезexternal type(D126) вstd/prelude/collections.nv. Закрывает последний known-by-name hole в D26 visible prelude. Methods остаются вstd/runtime/<name>.nvчерезexternal fn(D82) — связь по receiver-type name. См. D126.Plan 62.B.bis (закрыт 2026-05-18,
PRELUDE_VERSION = 7):printlnformally declared вstd/prelude/runtime.nvчерез D69 variadic +[]any(canonical D26 signaturefn print(...items []any) Io -> ()). Plan 67 hotfix (silent-wrong-output bug вinfer_print_helperдляprintln(str.from(int))паттерна) absorbed как Ф.0 — refactor через unifiedinfer_expr_c_typedispatch. Codegen special-case (emit_c.rs:11270) fires ДО variadic routing (Ф.1 reorder) — preserves per-arg type info, synthesized[]anyarray никогда не строится; per-argnova_print_<type>dispatch сохраняется черезinfer_print_helper→ unified inference. Builtins HashSet shrink:"print","println"removed (Ф.5). Cross-file resolve через R26+R27 находит declarations. См. Plan 62.B.bis.Plan 62.A.bis (закрыт 2026-05-20): введён layered schema registry для sum-types в codegen (
SumSchemaRegistry—compiler-codegen/src/codegen/sum_schema_registry.rs). Registry работает в трёх слоях с убывающим приоритетом:DeclaredFromPrelude > DeclaredFromUser > HardcodedBaseline. Hardcoded entries (Option/Result/Error/RuntimeError) остаются в качестве ABI-compat fallback для runtime-хелперов вnova_rt/array.h. File-based декларации вstd/prelude/core.nv(черезexternal fn Option[T] @method) получают приоритет и маршрутизируют вызовы черезMethodRoutingregistry (HardcodedRuntimeFn / ExternalFn / DeclaredBody). Unblocked: 7 из 8 методов Option (is_some, is_none, unwrap, unwrap_or, unwrap_or_else, map, ok_or) + 4 из 9 методов Result (is_ok, is_err, ok, err) — задекларированы вstd/prelude/core.nv. Deferred в core: 5 Result-методов возвращающихT(unwrap_or и др.) — blocker: type-checker выводит genericT, codegen возвращаетnova_int,==после вызова ломается (Plan 62.B+).Option.or— trampoline вnova_rt/array.hотсутствует (Plan 62.B+). Phase 4 (удаление legacysum_schemas) deferred до Plan 59 sum-mono.Remaining deferred:
RuntimeNoneError(bootstrap parser empty-sum syntax),TryFrom/TryInto(Plan 62.E.bis — требует Plan 56 Ф.2.7 effect-row enforcement). Bottom-типnever— закрыт Plan 76 (строчный встроенный примитив, не требует prelude-декларации).Plan 99 (закрыт 2026-05-23): последние 6 closure-applying Option/Result-методов перенесены на Nova-body в
std/prelude/core.nv:Option.map[U],Option.unwrap_or_else,Option.ok_or[E],Result.map[U],Result.map_err[F],Result.unwrap_or_else. 15 / 17 Option/Result методов на Nova-body (7 Option + 8 Result), C-routed остаются толькоOption.unwrapиResult.unwrap(Plan 61 lineage — typedFail[E]effect). Декомпозирован на 4 sub-plan’а: Plan 99.1 (foundation — method-level generic в DeclaredBody: extractresolve_method_level_substhelper, mono_name с method-level suffix,register_novaopt_decl(U)lazy-emit,infer_method_level_return_for_sumдляinfer_expr_c_type); Plan 99.2 (contextual variant constructors — bareNoneиспользуетcurrent_fn_return_ty;Ok(v)/Err(e)берут (T,E) из rt; bareSome(v)использует ARG-type черезinfer_expr_c_type(arg)чтобы sub-expr контексты —s.char_at(i) == Some('/')вOption[int]-fn — не строилиNovaOpt_<rt's_X>для arg иного типа); Plan 99.3 (atomic per-method migration — 6 commits с regression-gate); Plan 99.4 (comprehensive tests + spec + close). Closure invoke черезNovaClosBase+ explicit cast — паритет RustFnOnce-mono. Param-naming: closure-параметрыdefault_fn/map_fn/err_fn(неf) — избегаем shadowing user-функций (см.contracts/trivial_congruence_positiveрегрессию). Полный nova test: 1141 PASS / 0 FAIL / 56 SKIP.Plan 95.bis (закрыт 2026-05-23): расширение Plan 95 — ещё 5 «чистых» Option/Result-методов перенесены на Nova-body в
std/prelude/core.nv:Option.unwrap_or,Option.or,Result.unwrap_or,Result.ok,Result.err. Удалены все соответствующие C-трамплины изnova_rt/array.h(включаяNOVA_ARRAY_IMPL-macro entryNova_Option_method_or_<T>+ explicit_nova_strспециализация,Nova_Result_method_unwrap_or_<n>,Nova_Result_method_ok_<n>+ back-compat#define-алиасы) и lazy-emit вregister_novaopt_decl/register_novares_decl. Также удалён inline emitResult.err()в codegen (Plan 59 Ф.7.5 D3 — теперь Nova-body эмитит boxed payload сам через mono’dregister_novaopt_declpath). ResultDeclaredBody-dispatch доработан: mono-имя всегда суффиксированный (Nova_Result_method_<m>_<n>), даже для legacyNova_Result*obj_ty, чтобы избежать C-redefinition. Граница не изменилась:unwrap(Fail-handler, Plan 61),unwrap_or_else/map/map_err/ok_or(closure-applying + method-level generic + Plan 98 inference) — остаются C-routed.
Plan 95 (закрыт 2026-05-23): builtin sum-типы
Option/Resultучаствуют в method-monomorphization через канал «method-only mono» — без регистрации вgeneric_type_templates(представлениеNovaOpt_<T>/NovaRes_<ok>_<err>*не трогается). Pre-existingMethodRouting::DeclaredBody(scaffold-only до Plan 95) теперь реально конструируется вinit_prelude_decls_from_itemsдля non-external методов наOption/Result, потребляется в перехватах вызоваNovaOpt_(#6 в emit_c.rs:14160) иis_result_like(#7).receiver_c_typeспец-кейситOption/Result→ value-тип черезcurrent_type_subst+ сохранённыеbuiltin_sum_type_params. Mono-имя совпадает с формой бывшего C-трамплина (Nova_Option_method_<m>_<T_sani>/Nova_Result_method_<m>_<n>) → call-site mangling не меняется. Перенесены на Nova-body:Option.is_some/is_none,Result.is_ok/is_err(=> match @ { ... }вstd/prelude/core.nv); C-трамплины удалены изnova_rt/array.h, lazy-emit вregister_novaopt_decl/register_novares_decl, и baseline-entries вinit_hardcoded_baseline. Граница:unwrap(Fail-dispatch),unwrap_or/unwrap_or_else/map/ok_or/map_err(closure-applying) — остаются C-routed. Закрыт маркер[M-option-methods-not-mono-able]. Plan 93 (узкий вариант «is_some-Nova-body») superseded by Plan 95 — целиком поглощён Ф.4. Plan 78 (prelude-codegen single-source) — узкий санкционированный пересмотр Ф.1 только для чистых тег-предикатов; реестр C-routing в силе.
Правило
Что в prelude (v1.0)
Типы:
type Option[T] | Some(T) | None
type Result[T, E] | Ok(T) | Err(E)
type Ordering | Less | Equal | Greater
// `never` — bottom-тип (uninhabited): строчный встроенный примитив,
// НЕ объявляется (как `int`/`bool`). См. «`never` — bottom-тип» ниже.
type any protocol { } // top-type через пустой protocol (D53)
Базовые методы Option[T]:
fn Option[T] @is_some() -> bool
fn Option[T] @is_none() -> bool
fn Option[T] @unwrap() Fail[Error] -> T // throw "called unwrap on None"
fn Option[T] @unwrap_or(default T) -> T // None → default
fn Option[T] @unwrap_or_else(f fn() -> T) -> T // None → f() (lazy default)
fn Option[T] @map[U](f fn(T) -> U) -> Option[U]
fn Option[T] @ok_or[E](err E) -> Result[T, E] // None → Err(err)
fn Option[T] @or(other Option[T]) -> Option[T]
Базовые методы Result[T, E]:
fn Result[T, E] @is_ok() -> bool
fn Result[T, E] @is_err() -> bool
fn Result[T, E] @ok() -> Option[T] // Ok(v) → Some(v); Err → None
fn Result[T, E] @err() -> Option[E] // Err(e) → Some(e); Ok → None
fn Result[T, E] @unwrap() Fail[E] -> T // Err(e) → throw e
fn Result[T, E] @unwrap_or(default T) -> T // Err → default
fn Result[T, E] @unwrap_or_else(f fn(E) -> T) -> T // Err → f(e) (lazy)
fn Result[T, E] @map[U](f fn(T) -> U) -> Result[U, E]
fn Result[T, E] @map_err[F](f fn(E) -> F) -> Result[T, F]
unwrap_or / unwrap_or_else — основной идиоматический путь
безопасного доступа к значению с fallback. Прецеденты — Rust
Option::unwrap_or, Swift ?? оператор, TypeScript ??.
let n int = parse_int(s).unwrap_or(0) // на ошибке — 0
let cfg = config.unwrap_or_else(|| default_config()) // lazy default
// Идиома: цепочка через map / unwrap_or:
let port int = env.get("PORT").map(parse_int).unwrap_or(8080)
@unwrap() — assertion-style: throw’ает Fail если None/Err. Идиома
для случаев когда программист гарантирует что значение есть
(prove’ил выше через if let / match). Caller-side либо ловит
через with Fail = ..., либо позволяет распространиться (паника
на границе fiber’а — D13).
Bootstrap status (2026-05-08)
| Метод | Codegen | Тесты |
|---|---|---|
Option.is_some / is_none | ✅ | ✅ |
Option.unwrap (Fail на None) | ✅ inline | ✅ runtime/unwrap_or.nv |
Option.unwrap_or(default) | ✅ runtime helper | ✅ |
Option.unwrap_or_else(f) | ✅ inline (closure call) | ✅ runtime/result_methods.nv |
Option.map(f) | ✅ inline | ✅ |
Option.ok_or(e) | ✅ inline | ✅ |
Option.or(other) | ✅ per-T trampoline Nova_Option_method_or_<T> | ✅ plan62/option_or_from_prelude.nv |
Result.is_ok / is_err | ✅ | ✅ |
Result.ok() → Option[T] | ✅ runtime helper | ✅ |
Result.err() → Option[E] | ✅ inline (boxed nova_str) | ✅ |
Result.unwrap (Fail на Err) | ✅ inline | ✅ |
Result.unwrap_or(default) | ✅ runtime helper | ✅ |
Result.unwrap_or_else(f) | ✅ inline (closure call) | ✅ |
Result.map(f) | ✅ inline | ✅ |
Result.map_err(f) | ✅ inline | ✅ |
Error.new(msg) | ✅ runtime helper | ✅ runtime/error_runtime_error.nv |
Error.msg (field) | ✅ direct field access | ✅ |
RuntimeError.DivByZero | ✅ unit-variant constructor | ✅ |
RuntimeError.Overflow | ✅ unit-variant constructor | ✅ |
RuntimeError.IndexOutOfBounds {i, n} | ✅ record-variant constructor | ✅ |
RuntimeError.TypeMismatch(s) | ✅ tuple-variant constructor | ✅ |
RuntimeError.AssertFailed(s) | ✅ tuple-variant constructor | ✅ |
RuntimeError.NoHandler(s) | ✅ tuple-variant constructor | ✅ |
Plan 62.B (2026-05-20):
Option.orреализован — per-T trampolineNova_Option_method_or_<T>. Все 17 Option/Result методов из §283-306 теперь задекларированы вstd/prelude/core.nvчерезexternal fn(раньше 5 Result-методов —unwrap/unwrap_or/unwrap_or_else/map/map_err— оставались hardcoded-only из-за generic-стаб блокера в type inference, см. plan-doc 62 §«Status update 2026-05-20»). Починен pre-existing багResult.mapдляbool/char-typed closure (хардкодNOVA_CLOS_CALL_iiint-layout → calling-convention mismatch).
Bootstrap-ограничения:
✅ ЗАКРЫТО (Plan 59 Ф.7.5 increment 2, 2026-05-21):Result[T, E]зашит на(nova_int Ok, nova_str Err). Generic monomorphization для произвольных T/E — отдельная задача (Q-result-monomorphization).Result[T, E]полностью мономорфизирован — per-(T,E) C-типNovaRes_<ok>_<err>*(аналогNovaOpt_<T>), реальные типы в Ok/Err payload’е. Legacy единыйNova_Resultустранён.- Lambda-параметры с не-
intтипом (напримерfn(e str) -> str => ...дляmap_err) требуют явной аннотации через closure-full (fn(...)). Closure-light (|x|) полагается на context-inference; если method-sig недостаточен — переключайся на closure-full. Codegen в bootstrap не делает inference closure-параметра по сигнатуре method’а (Q-closure-param-inference). - Zero-arg closure для
unwrap_or_else—|| expr(closure-light) илиfn() -> T => expr(closure-full). Парсер различает||-closure-start от||-binary OR по позиции. Errorимеет полеmsg. По D26 spec’у должно бытьreadonly msg, но bootstrap не enforce’ит readonly — поле модифицируется как обычное (bootstrap-grade compromise).RuntimeErrorварианты создаются и matchаются user-кодом, но встроенные операции (a/bна 0,arr[i]out-of-bounds, unhandled effects) пока бросаютnova_strчерезNova_Fail_fail, не структурированныйNova_RuntimeError*. Конверсия throw-points в RuntimeError-payload — отдельная задача (требует расширения fail-frame mechanism сnova_strнаvoid*payload).
Прочие prelude-типы:
// Error — record для quick-and-dirty ошибок с сообщением (D65)
type Error {
readonly msg str
}
fn Error.new(msg str) -> Error => { msg }
// RuntimeError — sum-тип встроенных runtime-сбоев (D65)
// Бросается встроенными операциями: a/b на 0, arr[i] на out-of-bounds, etc.
// StackOverflow и OutOfMemory не входят — они panic, не Fail (D13).
type RuntimeError
| DivByZero
| Overflow
| IndexOutOfBounds { index int, length int }
| TypeMismatch(str)
| AssertFailed(str)
| NoHandler(str)
// RuntimeNoneError — unit-тип, бросается через `expr!!` на Option (D85).
// Отдельный от RuntimeError — это категория «отсутствие значения», не
// аппаратный сбой.
type RuntimeNoneError
// Iterator protocol (D58)
type Iter[T] protocol {
mut next() -> Option[T]
}
// Range — литерал `a..b` / `a..=b` (D58)
type Range {
readonly start int
readonly end int
readonly inclusive bool
}
type RangeIter {
end int
inclusive bool
mut cur int
}
// Built-in opaque accumulator/buffer типы (Plan 04, D82, D126).
// Formal declarations — std/prelude/collections.nv через `external type`
// (D126, Plan 62.D.bis, 2026-05-18). Methods — std/runtime/string_builder.nv,
// std/runtime/write_buffer.nv, std/runtime/read_buffer.nv через `external fn`
// (D82, Plan 13 Ф.8; раньше были в едином std/runtime/builtins.nv —
// REMOVED 2026-05-08). До 62.D.bis типы существовали как «known-by-name»
// (без formal Nova-side declaration) — теперь canonical source в prelude.
// `[]u8` — canonical byte-slice (Plan 69, byte→u8 migration).
external type StringBuilder // UTF-8 string accumulator, @into() -> str (infallible)
external type WriteBuffer // binary write buffer, @into() -> []u8
external type ReadBuffer // cursor-style binary reader, view над []u8
// Ошибка ReadBuffer — недостаточно байт для read-операции.
type ReadBufferError
| UnexpectedEnd { wanted int, available int }
Базовые числовые и строковые типы (int, i8-i64, u8-u64,
f32, f64, str, bool, char, ()) — встроены в язык,
не stdlib, но упомянуты для полноты.
Size-accessor методы для built-in []T и str (Plan 60 / D117):
fn []T @len() -> int // O(1), zero-cost lowering arr->len
fn []T @capacity() -> int // O(1), zero-cost lowering arr->cap
fn []T @is_empty() -> bool // O(1), len() == 0
fn str @len() -> int // O(1) — байты (Plan 108 D26 rev)
fn str @char_len() -> int // O(n) — codepoints (UTF-8 walk)
fn str @byte_len() -> int // O(1) — deprecated alias для @len()
fn str @is_empty() -> bool // O(1) — len() == 0
Field-access form (arr.len, s.byte_len, etc.) запрещён в
user-language — D117 enforce’ит method-only. Internal C-поля
arr->len / arr->cap сохраняются как implementation detail.
Built-in opaque-типы для аккумуляции (StringBuilder,
WriteBuffer, ReadBuffer) — расширяют примитивы D26. Type
declarations — в std/prelude/collections.nv через external type
(D126,
Plan 62.D.bis, 2026-05-18). Methods — в std/runtime/string_builder.nv,
std/runtime/write_buffer.nv, std/runtime/read_buffer.nv (auto-generated
через Plan 13 Ф.8) — external fn декларации (D82).
Программист не пишет type StringBuilder { ... } body — external type — это opaque marker, реализация в runtime (nova_rt/).
| Тип | Глагол | Финализация | Use-case |
|---|---|---|---|
StringBuilder | @append | @into() -> str infallible | string concat в hot loop |
WriteBuffer | @write_* | @into() -> []u8 | binary serialize |
ReadBuffer | @read_* / @try_read_* | view, no into | binary parse |
Эти три типа заменяют старый унифицированный Buffer (Q-buffer
закрыт REPLACED 2026-05-08). Причина split: text+binary mixed
ломает @into() -> str infallible-семантику. См. Plan 04.
@clone() — shallow по умолчанию (Plan 17 Ф.1)
Конвенция в Nova:
@clone() -> Self— shallow copy. Возвращает новый экземпляр с тем же набором полей; managed-references (другие record’ы, массивы, вложенные коллекции) после clone разделяются между оригиналом и копией. Для глубокой копии —@deep_clone()(не в prelude, определяется по необходимости вручную).
Что значит «shallow» для разных категорий:
- Примитивы (
int,f64,bool,char,u8) — value semantics, clone = тривиальная копия. str— immutable,s.clone()возвращает тот же ptr (равноценно присваиванию). Семантически независимая копия не нужна.- Record — копируются поля; managed-поля (вложенные record’ы, массивы) — по ссылке.
[]T— копируется внутренний(ptr, len, cap)-storage в свежий buffer (O(n) поверхностно), но элементыT— managed-references share’аются еслиTсам не примитив.- HashMap / Vec / Set / Queue (stdlib) — копируется внутренний storage, элементы и ключи — по ссылке.
StringBuilder,WriteBuffer—@clone()тут deep для внутреннего byte-buffer’а, потому что сам тип определён как mutable accumulator с уникальным storage’ом — shared buffer между clone’ами = data race по семантике D26. Это исключение из общего shallow-правила, обоснованное mutability-семантикой типа.
Когда писать @deep_clone() — когда нужно гарантировать, что
после clone никакая мутация одной копии не видна другой. Stdlib не
вводит общий @deep_clone()-protocol; программист реализует на
конкретном типе:
fn HashMap[K, V] @deep_clone() -> HashMap[str, []int] {
let mut out = HashMap[str, []int].new()
for (k, v) in @ {
out.insert(k, v.clone()) // элементы клонируются shallow
}
out
}
Прецедент: Rust Clone shallow по умолчанию, deep — руками. Java
Object.clone() shallow, override для deep. Go — value semantics на
структурах + reference semantics на slice/map (=shallow на assign).
Bootstrap status (2026-05-08): только StringBuilder.@clone() и
WriteBuffer.@clone() зарегистрированы как built-in (deep, через
Nova_*_clone C-функции). Для record/коллекций программист пишет
clone вручную.
Подробно — Plan 17 Ф.1, Q-clone-semantics (closed).
StringBuilder.@into() -> str — infallible (UTF-8 invariant
поддерживается каждым @append, который принимает только str или
char). WriteBuffer.@into() -> []u8 — infallible (произвольные
байты валидны как []u8). ReadBuffer — view, @into()
не определён (явный throw блокирует D73 auto-derive).
ReadBuffer пара @read_* (Fail-form) / @try_read_* (Result-form)
— обе формы явно в runtime_registry.rs и в std/runtime/read_buffer.nv.
Каждая Fail-форма имеет независимую C-функцию Nova_ReadBuffer_method_read_X,
а Result-форма — Nova_ReadBuffer_method_try_read_X. Автоматический
синтез одной из другой отменён (Plan 13 Ф.9.5; ранее Plan 12 Ф.4.5
предлагал такое правило, но было отменено для соблюдения D82 single-source-
of-truth — всё что компилятор знает, должно быть в registry явно).
char — Unicode codepoint, НЕ UTF-8 byte sequence. char хранит
одно скалярное значение Unicode (диапазон 0..0x10FFFF, исключая
surrogate pairs 0xD800..0xDFFF). Размер в памяти — 4 байта (как Rust
char, Go rune, Swift Unicode.Scalar).
str хранит UTF-8 байты, char — codepoint. Конверсии:
char → strилиchar → []u8— UTF-8 encode (1-4 байта в зависимости от значения; см.Buffer.add_charв Q-buffer).str.chars() -> Iter[char]— UTF-8 decode по ходу итерации.
Это разделение типичное для современных языков (Rust, Swift). Go
использует rune = int32 по тому же принципу. C char это byte —
не аналог Nova char.
Bootstrap-status: char зарезервирован как тип, но синтаксис
char-литералов ('a') — ещё открытый вопрос (Q-char-literals).
В коде сейчас используется nova_int напрямую (передаём codepoint
как число) — это будет заменено на нормальный char при закрытии
Q-char-literals.
str — Unicode-string. Внутреннее представление — UTF-8 байты
(ptr, byte_len), но все public operations работают на уровне
codepoint’ов (Unicode scalar values). Содержимое — валидный UTF-8
по конвенции: литералы, конкатенация и str.from(...) гарантируют
валидность; FFI-код должен сам проверять при создании str из
чужого буфера.
Длина и индексация (codepoint-indexed, школа Python/Swift):
s.len— длина в codepoint’ах, O(n) (требует обхода UTF-8). Это базовая «длина строки» с точки зрения программиста.s.byte_len()— длина в байтах, O(1). Для FFI и буферных операций.s[a..b](slice, bracket-form) — принимает codepoint-индексы, O(b) (нужен обход до byte-offset’ов). Boundary всегда корректные — невозможно попасть в середину multi-byte sequence. Panic при OOB (consistent сarr[a..b], D144). Также 5 форм Range:s[a..b]/s[a..=b]/s[a..]/s[..b]/s[..].s[i](codepoint indexing) —Option[char], O(i).Noneеслиi >= s.len. См. также Q-string-indexing.s.chars() -> Iter[char]— ленивый обход codepoint за codepoint.
Plan 96.1 (2026-05-23): метод
s.slice(a, b)удалён в пользу bracket-формыs[a..b](D9 «один очевидный путь»; convergence Rust/Go/ Swift/Python — bracket-only). Старая clamp-семантика метода (OOB → обрезка до длины) удалена; bracket-form всегда panic’ит на OOB — симметрично сarr[a..b](D144). Closes[P-str-slice-clamp-vs-panic].
Поиск, сравнение, конверсия (все индексы — codepoint-offset):
fn str @find(needle str) -> Option[int] // codepoint-offset
fn str @rfind(needle str) -> Option[int] // последний codepoint-offset
fn str @contains(needle str) -> bool
fn str @starts_with(prefix str) -> bool
fn str @ends_with(suffix str) -> bool
fn str @split(sep str) -> Iter[str]
fn str @trim() -> str
fn str @to_lower() -> str
fn str @to_upper() -> str
s.find(":") -> Option[int] возвращает codepoint-индекс ”:”.
Это передаётся напрямую в bracket-slice s[0..i]:
let s = "Привет:мир" // 10 codepoints, 19 bytes
let i = s.find(":").unwrap_or(0) // i == 6 (codepoints)
let key = s[0..i] // "Привет"
let val = s[i + 1..] // "мир" (open-end)
assert(s.len() == 10) // codepoints
assert(key.len() == 6)
Почему codepoint-indexing (школа B) выбрана для Nova:
- AI-friendly. LLM генерирует код где
s.lenинтуитивно «количество символов». Byte-уровень (Rust/Go) — источник bug’ов у новичков и AI:"Привет".len == 12нелогично. - Безопасность boundary. Невозможно попасть в середину UTF-8 sequence — все индексы codepoint-выровнены.
- Consistency.
find/s[a..b]/s[i]— все codepoint-уровень, не нужно мысленно переключаться между byte и codepoint. - Прецеденты: Python (codepoints), Swift (graphemes — ещё выше), Java (UTF-16 code units, близко к codepoint для BMP). Все современные языки кроме system-low-level (Rust, Go, C) выбирают codepoint-or-grapheme уровень.
Цена:
- O(n) для
s.len, O(b) дляs[a..b]— обходы UTF-8. Внутреннее byte-хранилище неизбежно: альтернатива (UTF-32 4-byte per char) утроит память для ASCII-heavy кода. - Hot-path работа с byte-уровнем — через explicit
s.bytes()→[]u8или черезBuffer(Q-buffer). - В Nova принципе AI-генерация важнее микро-perf для primitive ops; программист может явно перейти на byte-уровень там где надо.
FFI / byte-уровень доступен через:
fn str @byte_len() -> int // O(1) — для C-interop размеров
fn str @bytes() -> []u8 // copy (D73 []u8.from(s))
Конверсия в []u8 через D73:
[]u8.from(s str) -> []u8— infallible (всегда работает,strгарантированно валидный UTF-8). Копируетs.ptr..s.ptr+s.lenв свежий[]u8. D73 авто-синтезируетs.into()дляlet b []u8 = s.into().- Копирует, не view: Nova не имеет readonly-меток (D6 — managed
heap без borrow-checker), а
[]u8mutable — без копии mutate испортил бы immutabilitystr. Стоимость O(n) — приемлемо для границы str↔bytes; для in-place аккумуляции использоватьBuffer(Q-buffer). str.from(b []u8) Fail[Utf8Error] -> str— fallible-форма (D73 + Fail-effect). Валидирует UTF-8; на ошибке throw’ает. Auto-derived:b.into()тоже декларируетFail[Utf8Error]. Result-форма (str.try_from(b)→Result[str, Utf8Error]) доступна через D77 как convenience sugar.
Nul-termination (C-interop): nova_str_concat сейчас аллоцирует
len + 1 байт и кладёт \0 после данных, чтобы s.ptr можно было
передать в C-функции. Литералы тоже nul-terminated (.rodata C-string).
Slice — НЕ добавляет \0 (просто view). Это значит
nova_str.ptr — не гарантированно cstring; зависит от того как
строка построена. Открытый вопрос (Q-cstring): либо унифицировать
(“все nova_str всегда nul-terminated, slice копирует”) ценой
аллокаций, либо отказаться от частичной гарантии и ввести явный
s.as_cstr() -> *const char (с копированием при необходимости).
В bootstrap’е действует текущее inconsistent поведение.
Дедупликация / interning: str не интернируется автоматически.
Одинаковые runtime-строки — разные инстансы. == сравнивает контент
(memcmp), O(min). Compile-time литералы deduplicate-аются C-компилятором
через стандартное string-literal pooling в .rodata. Для opt-in
interning — открытый вопрос (Q-string-interning): Atom-тип или
Sym[T] (Erlang-style); прецеденты — Rust не интернирует, Java/C#
имеют пул для литералов + opt-in intern().
Конкатенация: s1 + s2 — O(a+b), новая аллокация каждый раз.
В hot loop s = s + x × N → O(N²). Для аккумуляции использовать
Buffer (Q-buffer; финализация через @try_into() -> Result[str, Utf8Error] для UTF-8 или @into() -> []u8 для сырых данных).
Nova унифицирует string-builder и byte-buffer в один тип — отличается
от Go (bytes.Buffer + strings.Builder) и Rust (Vec<u8> +
String).
См. также Q-char-literals (синтаксис
char-литералов) и D54 (as/is для конверсий).
Математические операции на числовых типах объявлены как
instance-методы через @ (D74):
x.sqrt(), theta.cos(), y.atan2(x), a.hypot(b), n.abs(),
x.is_finite(), etc. Static-функции — только для констант
(f64.PI, f64.NAN) и парсинга (f64.try_parse(s)).
any — пустой protocol-тип (D53). Любой тип удовлетворяет
пустому контракту, поэтому any — top-type (универсальный супертип).
Имя lowercase — исключение в 03-syntax.md → D30
naming convention, по аналогии с примитивами. Использование:
fn dump(x any) Io -> (), Logger.log_event(level, fields []any)
для гетерогенных структурных логов.
Iter[T] — структурный protocol для итераторов (D58). Любой
тип с методом mut next() -> Option[T] автоматически удовлетворяет.
for x in collection-синтаксис вызывает collection.iter().next() в
цикле; коллекции реализуют iter() возвращая собственный iterator-тип.
Range — runtime-представление range-литерала a..b (exclusive)
и a..=b (inclusive) (D58). Range — обычное значение, можно
передавать как аргумент, хранить в переменной, использовать в for.
Стандартные эффекты в prelude — после D62 делятся на две категории по влиянию на семантику программы:
Semantic effects — влияют на результат
Программист обязан объявить в сигнатуре, если функция их использует. Caller получает информацию что зависит от resource’а.
| Эффект | Resource | Тестовый handler |
|---|---|---|
Fail[E] | error reporter | with Fail[E] = |e| ... |
Io | stdout/stderr | mock-stdout |
Net | сеть (HTTP/socket) | recorded responses |
Db | соединение к БД | in-memory db |
Fs | файловая система | virtual-fs |
Time | clock | fixed_ms(ms u64) / mut_clock(start_ms u64) |
Random | RNG | seeded(seed u64) |
Log | logger | capture-log |
Ask[T] | контекстный read (Reader) | fixed value |
Alloc[R] | region аллокация | (для real-time, D6) |
Detach | background scheduler | SyncDetach |
Blocking | OS-thread pool | mock |
Instrumental effects — observability, ambient
Mem (D76) и Trace — не влияют на результат программы,
только на наблюдаемость. Программист не декларирует их в
сигнатуре; компилятор не лифтит через D28-inference.
// Программист пишет:
fn parse_data(s str) -> Data { ... }
// Внутри может быть Trace.span("parse"), Mem.alloc_count() — это
// implementation detail, в сигнатуру НЕ лифтится.
Ambient capability — прецедент Async (D14/D62). Если в скоупе
нет active handler для instrumental эффекта — runtime-panic
(RuntimeError.NoHandler("Mem") через D65),
не compile error.
| Эффект | Категория |
|---|---|
Mem | instrumental, ambient |
Trace | instrumental, ambient |
Зачем разделять:
- Сигнатуры остаются чистыми. Если бы
Traceбыл semantic, то почти каждая функция бы содержала его — observability обычно pervasive. Шум в типах. - AI-friendly. LLM не должна писать
Memв сигнатуре — instrumental detail имплементации. - Интуитивно.
Timeв сигнатуре говорит “функция зависит от времени, тестируй с fixed clock”.Traceв сигнатуре ничего полезного не говорит.
Не существуют как эффекты
| Имя | Почему |
|---|---|
Async | runtime mechanic (suspension, D14 (REVISED)) |
Par | runtime mechanic (parallelism через parallel for) |
Mut | удалён (D62) — mut поля/параметры |
Базовые функции:
fn print(...items []any) Io -> () // variadic, см. D69
fn println(...items []any) Io -> () // variadic + newline
fn panic(msg str) -> never // смерть текущего fiber'а (D13)
fn exit(code int, msg str) -> never // смерть всего процесса (D13)
// Assertions — обычные fn-call, обязательно со скобками
fn assert(cond bool) -> () // always runtime; failure → panic (D13)
fn debug_assert(cond bool) -> () // debug-only; no-op в release (D81)
print/println — variadic (D69),
принимают любое число аргументов любого типа (any —
D54). Каждый аргумент конвертируется в строку
через str.from(v) (D73).
Spread разрешён: print(...parts).
assert/debug_assert — обычные функции, не keyword’ы. Вызываются
со скобками как любой fn-call: assert(x > 0). Build-mode семантика —
D81. Failure любого assert’а — panic (D13), не Fail.
never — bottom-тип (uninhabited)
never — bottom-тип языка: строчный встроенный примитив, в одном
ряду с int/bool/f64. Не объявляется ни в prelude, ни через
type — компилятор знает его напрямую (как и остальные примитивы).
Имя строчное по конвенции примитивов (Plan 76).
Свойства:
- Uninhabited — значений типа
neverне существует (0 значений). never— подтип любого типа (bottom type ⊥). Любой контекст, ожидающийT, может принятьnever-выражение.- Используется в типах не-возвращающих выражений —
throw expr,return expr,panic(...),exit(...), бесконечныйloop. Все имеют типnever, поэтому совместимы с любым контекстом.
Аналоги: Rust ! (never-RFC), Haskell Void, Kotlin/Scala
Nothing, TypeScript never. Не уникальная фича Nova.
Эффекты как обычные типы — Fail[E] не магия
Fail[E] объявляется в prelude как любой другой эффект — через
kind-токен effect (04-effects.md → D18 (REVISED),
D61):
type Fail[E] effect {
fail(value E) -> never
}
throw expr — сахар для Fail[E].fail(expr) (вызов операции
активного handler’а), как Db.query(...). Никакой специальной
обработки. См. 04-effects.md → D25,
04-effects.md → D61.
Что НЕ в prelude
Коллекции (String, HashMap, HashSet, LinkedList), I/O API (File, Http),
JSON, SQL, время как библиотека — обычные модули, требующие
явного импорта:
import std.io.{File, read_all}
import std.collections.HashMap
Почему
Зачем нужен prelude
Без prelude каждый файл начинается с:
import std.option.{Option, Some, None}
import std.result.{Result, Ok, Err}
Это шум на 90% файлов. Прецедент — Rust, Haskell, Swift, Kotlin: все имеют prelude. AI-first: LLM не должен генерировать boilerplate-импорты базовых типов.
Не противоречит «локальности контекста»
Prelude документирован, его содержимое — фиксированный список, не магия. LLM знает, что доступно везде. Всё остальное — явный импорт (07-modules.md → D29).
Что отвергнуто
- Никакого prelude, всё через явный import — шум, не выигрыш.
- Prelude определяется компилятором, без документации — магия, ломает AI-first тезис.
- Prelude настраивается per-project — усложнение без выгоды; LLM должен знать фиксированный набор.
Void— отвергнут, тип «без значения» это()(unit). См. 03-syntax.md → D20.
Связь
- 01-philosophy.md → D10 — AI-first, локальность через документированный prelude.
- 04-effects.md → D25 —
throwиFail[Error]. - 04-effects.md → D18 — эффекты как обычные типы.
- 02-types.md → D17 — sum-type,
neverкак пустой. - 03-syntax.md → D20 —
()вместоvoid. - 07-modules.md → D29 — prelude и явные импорты.
Открытые вопросы
Полный API— частично закрыт (2026-05-07): базовые методы (Option/Resultis_some/is_none/unwrap/unwrap_or/unwrap_or_else/map/ok_or/orдля Option;is_ok/is_err/ok/err/unwrap/unwrap_or/unwrap_or_else/map/map_errдля Result) описаны в prelude выше. Расширенный API (and_then,flatten, etc.) — отдельная задача (Q-monadic-api).Семантика— закрыто D67: ранний?дляOptionreturn Noneиз текущей функции.Errorкак универсальный тип — что в нём (поддержкаstr.from(e), цепочка причин)? Похоже на Ruststd::error::Error.
Цена
- Список prelude нужно поддерживать. Любое добавление в prelude — breaking change после v1.0 (имя становится «зарезервированным» в модулях). Поэтому prelude минимален.
- Импорт-конфликты. Если программист объявит свой
type Option, будет конфликт с prelude — компилятор предупредит.
Runtime stdlib проекция (Plan 13)
Все методы str / f64 / f32 которые знает компилятор объявлены в
std/runtime/string.nv и
std/runtime/math.nv — auto-generated
из compiler-codegen/src/codegen/runtime_registry.rs через команду
nova-codegen emit-runtime-stubs.
Эти модули НЕ требуют import — методы доступны через обычный
method-call синтаксис (s.find, x.sin), потому что str / f64 /
f32 — built-in типы из prelude. std/runtime/*.nv — read-only artefact
для:
- Code-review: разработчик видит формальные сигнатуры всех runtime-функций в одном месте.
- Type-check без полной компиляции:
nova-codegen checkзагружает декларации и валидирует user-код против них. - Single source of truth: runtime_registry.rs (Rust) — driver,
.nv-файлы — проекция. Изменение реестра → регенерация → diff видно в.nv.
Manual edits запрещены — pre-commit/CI guard через
emit-runtime-stubs --check (Plan 13 Ф.6).
См. docs/plans/13-runtime-stdlib-and-autogen.md.
GC introspection — std.runtime.gc (Plan 32)
Namespace gc.* доступен для runtime-инспекции и явного управления GC:
let h = gc.heap_size() // bytes; 0 если backend без introspection
let n = gc.live_count() // приблизительное число live-объектов
let a = gc.alloc_count() // монотонный счётчик с старта
gc.collect() // принудительный сбор (no-op под malloc)
gc.reset_stats() // сброс счётчиков
Без import — gc — встроенный namespace (как panic / exit).
Документация в std/runtime/gc.nv; фактический
dispatch — hard-coded в compiler-codegen/src/codegen/emit_c.rs (special-
case для gc.<method>() member-call’ов).
Semantics per backend:
| API | malloc | boehm |
|---|---|---|
heap_size() | 0 (honest «не поддерживается») | GC_get_heap_size() |
live_count() | alloc - free | alloc_count (upper bound) |
alloc_count() | counter | counter |
collect() | no-op | GC_gcollect() |
reset_stats() | zero counters | zero counters |
heap_size() == 0 — honest sentinel; differential-тесты могут
использовать if gc.heap_size() == 0 { ... skip ... }.
Прецеденты: Go runtime.GC() / runtime.ReadMemStats, Java
System.gc() / Runtime.totalMemory(), Python gc.collect() /
gc.get_stats(), .NET GC.Collect() / GC.GetTotalMemory(). Nova
следует convention.
См. docs/plans/32-gc-introspection.md.
D41. Static-функции есть, static-состояния нет
Что
У типа есть static-функции (fn Type.name(...)), но нет
static-полей, нет static-переменных, нет static initializer’ов.
Если нужны константы, ассоциированные с типом, — это const в том же
модуле. Если нужно «глобальное» изменяемое состояние — это handler
(эффект-capability), не static.
Правило
Static-функции — обычные функции в namespace типа
Внутри одной static-функции другие static-функции того же типа вызываются через полное имя, без сокращений:
fn Account.new(owner str) -> Account =>
Account { _balance: 0, owner }
fn Account.from_balance(owner str, initial money) -> Account {
let acc = Account.new(owner) // явное Account.new, не self.new
Account.deposit_static(acc, initial) // тоже явно
acc
}
Никакого Self::new (Rust) или просто new (Java/C#). Один способ
вызова static-функции — через имя типа, что внутри типа, что снаружи.
Константы рядом с типом — const в модуле
const ACCOUNT_MIN_BALANCE money = 0
const ACCOUNT_MAX_OVERDRAFT money = 1000
fn Account.new(owner str) -> Account =>
Account { _balance: ACCOUNT_MIN_BALANCE, owner }
Если нужна группировка — отдельный модуль:
module account_limits
export const MIN_BALANCE money = 0
export const MAX_OVERDRAFT money = 1000
// использование:
import account_limits
let acc = Account.new_with(account_limits.MIN_BALANCE)
Глобальное изменяемое — через handler
Вместо static counter / static config — handler, передаваемый через
with-блок:
// Эффект ([04-effects.md → D61](/spec/decisions/effects/#d61))
type IdGen effect {
fresh() -> u64
}
// Handler — обычная функция, возвращающая handler-литерал
fn counter_id_gen(c mut Counter) -> Effect[IdGen] =>
effect IdGen {
fresh() {
c.count += 1
c.count
}
}
// в main:
fn main() {
let mut counter = Counter { count: 0 }
with IdGen = counter_id_gen(counter) {
run_app()
}
}
Это пример closure-capture паттерна по D68. Альтернатива —
@as_handlerметод на record’еCounter— рассмотрена в D68 для случаев, когда state нужно проинспектировать снаружи. Выбор между паттернами детерминирован сценарием (нужен ли state наружу), не вкусом.
Тестируется тривиально — другой handler в with-блоке.
Почему
- Static state — главный источник скрытых багов. Глобальный изменяемый стейт не виден в сигнатурах, ломает параллельность, невозможно тестировать без хаков.
- Тесты. Static-поле = разделяемое состояние между тестами.
Каждый тест должен либо ресетить его (хрупко), либо запускаться
изолированно (медленно). Handler —
with-блок изолирует автоматически. - Параллелизм. Несколько fiber’ов на одном static-поле = data race по умолчанию. Handler-state живёт в scope и не делится случайно.
- DI is the language. Передача зависимостей — это handler. Не нужен отдельный фреймворк для DI, не нужны static-singleton’ы как замена.
- Единственный путь. Нет «иногда static, иногда handler» — всегда handler. Меньше способов сделать неправильно.
Что отвергнуто
- Static mutable поля (Java
static int counter, Python class variable) — мешают тестам и параллелизму. - Static immutable поля как
constна типе (const Account.MIN) — технически безопасно, но добавляет второй способ объявить константу. Один способ —constв модуле. - Companion-object (Kotlin) — то же что и static, просто в обёртке. Не нужен.
- Lazy static (Rust
lazy_static!) — скрытое глобальное состояние с инициализацией. Если нужна ленивость — handler с lazy полем.
Связь
- 05-memory.md → D6 — глобального mutable state не предусмотрено в модели памяти; всё живёт в fiber-scope или handler-scope.
- 04-effects.md → D11, 04-effects.md → D31 — handler-механизм для «глобальных» состояний.
- 04-effects.md → D18 — эффекты это обычные
type, не keywordeffect. - 03-syntax.md → D33 —
const— единственный способ объявить immutable «глобальную» константу.
Цена
- Привычка из Java/C#/Python ломается. Нет
Account.MAX_BALANCEкак поля, естьMAX_BALANCEкакconstв модуле. Чуть длиннее, но единообразнее. - Singleton’ы переписываются как handler. Это не цена, а фича — но мигрирующий код придётся переделать.
- Counter / cache / pool требуют явного создания и проброса в
with-блок. Не «само работает», а явный жизненный цикл.
Эволюция
В исходной формулировке D41 пример использовал устаревшие keyword’ы
effect IdGen { ... } и handler counter_id_gen(...) IdGen { ... } —
оба отменены (04-effects.md → D18 — эффект это
обычный type; слово handler не зарезервировано).
В текущем тексте пример переписан как type IdGen { ... } +
обычная функция, возвращающая handler-литерал.
D70. ToStr protocol — REPLACED → D73
⚠️ REPLACED → D73 (2026-05-06). Полное содержание D70 (ToStr protocol, @to_str() метод, free function to_str(v), auto-derive по структуре) удалено для устранения дублирования. Историческая запись об эволюции — в decisions/history/evolution.md → «
ToStrprotocol: D70 формализует to_str()».
Migration map (D70 → D73)
| Старая форма (D70) | Новая форма (D73) |
|---|---|
type ToStr protocol { to_str() -> str } | удалено — protocol больше не нужен |
fn UserId @to_str() -> str => ... | fn str.from(u UserId) -> Self => ... |
to_str(user) | str.from(user) |
user.@to_str() | user.into() (Into[str] авто-выведен из From) |
"${user}" (через to_str) | "${user}" (через str.from, без изменения синтаксиса) |
fn f[T: ToStr](v T) | fn f[T Into[str]](v T) (если bound нужен) |
Auto-derive для встроенных типов и record/sum перенесён из D70 на
str.from: stdlib pre-registers str.from(int), str.from(bool),
str.from(f64), str.from(<any record>), str.from(<any sum>). Newtype
без override делегирует к underlying-типу.
Почему замена: D70 + D73 решали одну задачу разными способами.
Конверсия в str — частный случай конверсии в любой тип. Принцип
«один очевидный путь» (D9) требует единого механизма. См. также D40
(philosophy «один способ»).
D73. From / Into protocol-пара с авто-выводом
Уточнение (2026-05-07):
from/intoмогут декларироватьFail[E]если конверсия fallible. Это унифицирует infallible и fallible конверсии под одной формойfrom/into— нет нужды в отдельномtry_from/try_into(D77 теперь convenience-sugar, см. там).
Что
Универсальный механизм нетривиальной конверсии значения между типами:
From[T]— protocol со static-методомfrom(v T) -> Self. «Целевой тип знает, как сделать себя из источника».Into[T]— protocol с instance-методом@into() -> T. «Источник знает, как превратиться в целевой».- Авто-вывод одного из другого — компилятор знает про симметрию.
Если задан только
From[X]для типаT, компилятор автоматически удовлетворяетInto[T]дляX(и наоборот). Программист пишет одну реализацию из пары. - Fallible конверсии объявляются эффектом
Fail[E]в сигнатуре — та жеfrom/intoформа; effect-aware auto-derive переносит эффект на парную форму.
Программисту доступны две формы вызова из одной реализации:
T.from(v X) // static, на целевом типе
v.into() // instance, на источнике (тип цели — из контекста)
Для fallible (с Fail[E]) семантика та же; ошибка распространяется
через стандартный effect-механизм — with Fail = handler { ... } /
? оператор / propagation наружу.
В отличие от as (D54) — compile-time numeric/newtype/sum cast без
runtime-кода, — From/Into для семантически нетривиальных
конверсий (парсинг, единицы измерения, формат-обмен, представление
в строку — последнее заменяет old D70 ToStr).
Правило
Декларация protocol’ов в prelude
type From[T] protocol {
from(v T) -> Self // static, на целевом типе
}
type Into[T] protocol {
@into() -> T // instance, на источнике
}
Self (D66) — тип, реализующий protocol. From.from — static-метод,
вызывается через точку (D35): Fahrenheit.from(celsius). Into.@into
— instance-метод, через @-нотацию: c.into().
Программист пишет одну сторону пары — компилятор автоматически
выводит другую. Подробности — секция «Into[T] protocol и
автоматический вывод» ниже.
Реализация на пользовательском типе
Программист пишет обычный static-метод (D35):
type Celsius f64
type Fahrenheit f64
fn Fahrenheit.from(c Celsius) -> Self =>
Self((c as f64) * 9.0 / 5.0 + 32.0)
let f = Fahrenheit.from(Celsius(100.0)) // Fahrenheit(212.0)
Структурно Fahrenheit теперь удовлетворяет From[Celsius] (D53 +
D72) — никаких явных impl блоков.
Несколько From[X] на одном типе через overloading по
параметру (D84):
fn Fahrenheit.from(c Celsius) -> Self => ...
fn Fahrenheit.from(k Kelvin) -> Self => ...
let f1 = Fahrenheit.from(Celsius(100.0))
let f2 = Fahrenheit.from(Kelvin(373.15))
Generic-функции с From-bound
fn parse_typed[U From[str]](s str) -> U => U.from(s)
let n int = parse_typed("42") // если int реализует From[str]
Bound [U From[X]] в generic-сигнатуре требует чтобы конкретный
тип U реализовывал From[X] — структурно, через D72 bound check.
Fallible конверсии через Fail[E]
Если конверсия может не получиться (валидация, парсинг, проверка
диапазона), from/into декларируют Fail[E] в сигнатуре:
type Utf8Error | InvalidByte | UnexpectedEnd
fn str.from(b []u8) Fail[Utf8Error] -> Self {
if !is_valid_utf8(b) {
throw Utf8Error.InvalidByte
}
// ...
}
// Caller-side — три варианта:
// (1) Propagate via Fail в сигнатуре caller'а:
fn parse_message(b []u8) Fail[Utf8Error] -> Message {
let s = str.from(b) // ошибка пробрасывается
parse_inner(s)
}
// (2) Catch handler'ом — Result-стиль через with-handler:
let r Result[str, Utf8Error] =
with Fail[Utf8Error] = |e| interrupt Err(e) {
Ok(str.from(b))
}
// (3) Default-fallback через with-handler:
let s str = with Fail[Utf8Error] = |_| interrupt "[invalid utf-8]" {
str.from(b)
}
Effect-aware auto-derive: если T.from(v V) Fail[E] -> Self,
компилятор авто-синтезирует v.into() Fail[E] -> T. Эффект
наследуется, видим в сигнатуре auto-derived формы.
Auto-derive 4-way (D73 + D77 unified)
Программист пишет ОДНУ форму из четырёх; компилятор синтезирует
остальные. Это объединяет D73 (from/into) и D77 (try_from/try_into)
в один механизм.
Разделение «реализовать» vs «использовать»:
| Природа конверсии | Программисту реализовать | Программисту использовать |
|---|---|---|
| Fallible | T.try_from(v) -> Result[T, E] | T.from(v) или v.into() (короче, throws Fail) |
| Infallible | T.from(v) -> T | T.from(v) или v.into() |
То есть писать богатую форму (try_from для fallible — Result-стиль
явный, error type first-class), а использовать в обычном коде
короткую (from / into).
Compiler синтезирует все 4 формы из одной:
| Программист написал | Compiler даёт |
|---|---|
try_from(v) -> Result[T, E] (fallible) | from() Fail[E], into() Fail[E], try_into() -> Result[T, E] |
from(v) -> T (infallible) | into() -> T. (try-формы НЕ синтезируются — не имеют смысла без error type.) |
Почему try_from — самое богатое для имплементации:
- Result в типе явный.
Result[T, E]показывает error type как first-class signature element — IDE / AI читают это сразу. ЧерезFail[E]нужен ещё шаг effect-rezolution. - Compiler легко синтезирует throwing-форму из Result — простое
match { Ok(v) => v, Err(e) => throw e }. Обратное (Result из throwing) требует with-handler инфраструктуры. - Boilerplate Ok(…) — это feature имплементации.
Ok(value)явно говорит «вот success-path»,Err(...)— «вот failure-path». Программист читает контракт без неявных throw’ов в теле функции.
Почему from/into — для использования в коде:
- Короче —
T.from(v)противT.try_from(v)?илиT.try_from(v).unwrap(). - Идиоматичнее —
v.into()через context-driven dispatch читается как «преобразовать v к ожидаемому типу». - Throws пропагируются естественно — caller или handle через
with Fail, или эффект уходит наружу. Программист не пишет?-цепочки руками.
Когда использовать try_from/try_into в коде:
- Когда нужен explicit branching на error type через
match. - Когда нужно map error в другой тип (
r.map_err(|e| MyError::Wrap(e))). - Когда нужен default fallback через
unwrap_orбез handler-блока.
В остальных случаях — from/into через эффекты.
Прецедент Rust: TryFrom каноническая форма для fallible
конверсий; сообщество выработало этот стиль.
Алгоритм синтеза (программист пишет try_from):
// Программист написал:
fn u64.try_from(s str) -> Result[Self, ParseIntError] => ...
// Компилятор синтезирует автоматически:
// (1) throwing-from через D73:
fn u64.from(s str) Fail[ParseIntError] -> Self =>
match try_from(s) { Ok(n) => n, Err(e) => throw e }
// (2) instance try_into через D77:
fn str @try_into() -> Result[u64, ParseIntError] =>
u64.try_from(@)
// (3) instance into через D73:
fn str @into() Fail[ParseIntError] -> u64 =>
u64.from(@)
// Программист может вызвать любую из 4-х форм:
let n = u64.try_from(s)? // → Result, propagate с ?
let n = u64.from(s) // → throws Fail (caller handles)
let n: u64 = s.try_into()? // → instance Result
let n: u64 = s.into() // → instance throws
let n = u64.try_from(s).unwrap_or(0) // → fallback default
Когда писать from вместо try_from:
- Конверсия математически не может failure’ить: numeric upcast
(
f64.from(int)), unit ↔ unit (Fahrenheit.from(Celsius)), newtype unwrap (int.from(UserId)). - Программист может сам убедиться что параметр валиден prerequisite’ом
(например
from(s str)гдеsуже валидирован выше) — но это опасно, лучше fallible форма.
Тонкости:
- Если программист пишет ОБЕ формы (
fromбез Fail иtry_fromсResult[T, !]) — compile-error: ambiguity, какая основная. Программист выбирает одну. - Compiler не синтезирует try-формы из infallible
from()— нет error-type для Result. Если нужно (например, generic-bound требуетTryFrom), программист пишет explicitT.try_from(v) -> Result[T, never](never = uninhabited error). Result[T, never]automatically converts toTчерез unwrap — never-type не имеет значений,Errветка unreachable.
Когда писать Fail, когда нет:
Fahrenheit.from(c Celsius)— без Fail (всегда успех).int.from(s str) Fail[ParseIntError]— с Fail (может не парситься).Buffer.into() Fail[Utf8Error] -> str— с Fail (валидация UTF-8).
Это унифицирует API: одна форма from/into для всех конверсий.
Не нужно решать «infallible или try_»; effect-аннотация в сигнатуре
сама описывает контракт. Согласовано с D2/D10/D25/D62/D65 («всё —
эффект», throw — операция Fail).
Соотношение с as (D54)
as — compile-time, без runtime-кода:
let n = 100 as u32 // numeric cast
let u = 42 as UserId // newtype ↔ underlying
let code = NotFound as int // sum → int
From — нетривиальная конверсия с runtime-логикой:
let f = Fahrenheit.from(c) // арифметика
let u = User.from(json_value) // парсинг
let m = Money.from(("USD", 100)) // конструирование с валидацией
Граница чёткая: если конверсия выражается одним bit-level/tag-уровнем —
as. Если требует логики или может бросить — from.
Соотношение с D55 record-coercion
D55 — automatic coercion в позиции с известным целевым типом для record-литералов и sum-конструкторов:
let u User = { id: 2, name: "Bob" } // D55: anonymous record → User
let m Maybe[int] = 42 // D55: 42 → Just(42)
D73 — explicit конверсия через method call для произвольных типов.
D55 срабатывает раньше на синтаксическом уровне; From.from — обычный
вызов. Не конфликтуют:
let f Fahrenheit = Celsius(100.0) // ОШИБКА: D55 не работает —
// Fahrenheit не sum с unary Celsius
let f = Fahrenheit.from(Celsius(100.0)) // ok: D73
let f = into[Fahrenheit](Celsius(100.0)) // ok: через free function
Into[T] protocol и автоматический вывод
Into[T] — protocol с instance-методом, симметричный к From[T]:
type From[T] protocol {
from(v T) -> Self // static — на целевом типе
}
type Into[T] protocol {
@into() -> T // instance — на источнике
}
Компилятор знает про симметрию From/Into и выводит одно из
другого автоматически. Программист пишет одну реализацию из
пары, вторая выводится без бланket-impl и orphan-rule:
// Программист пишет From — Into выводится автоматически.
type Celsius f64
type Fahrenheit f64
fn Fahrenheit.from(c Celsius) -> Self =>
Self((c as f64) * 9.0 / 5.0 + 32.0)
// Компилятор автоматически синтезирует:
// fn Celsius @into() -> Fahrenheit => Fahrenheit.from(@)
// → Celsius структурно удовлетворяет Into[Fahrenheit].
let f1 = Fahrenheit.from(Celsius(100.0)) // явная from-форма
let f2 = Celsius(100.0).into() // авто-выведенная into-форма
let f3 = into[Fahrenheit](Celsius(100.0)) // free function
let f4 Fahrenheit = into(Celsius(100.0)) // через context (D55)
Симметрично, если программист пишет @into, компилятор синтезирует
from:
// Программист пишет Into — From выводится автоматически.
type Json record { ... }
type User { id u64, name str }
fn Json @into() -> User =>
User { id: @get_u64("id"), name: @get_str("name") }
// Компилятор автоматически синтезирует:
// fn User.from(v Json) -> Self => v.into()
// → User структурно удовлетворяет From[Json].
let u1 = json.into() // явная into-форма
let u2 = User.from(json) // авто-выведенная from-форма
Если написаны обе — обе используются как написаны, авто-вывод
не применяется. Несовпадение результатов между руками
написанными from и into — ответственность программиста (типичный
лит-чек предупреждает, но не запрещает: бывают legitimate случаи
типа explicit-from-bytes vs implicit-into-bytes).
Запрет циклов авто-вывода. Авто-вывод одноуровневый: из From[X]
для T синтезируется Into[T] для X. Не наоборот в той же
итерации (это создало бы цикл). Это значит:
- Программист пишет
From[X]илиInto[X]— оба триггерят авто-вывод парного. - Компилятор не пытается «найти transitively From[Y] через From[X] и From[X→Y]».
Если нужна транзитивность (A → B → C через две промежуточные
конверсии) — программист пишет explicit:
fn C.from(a A) -> Self =>
let b = B.from(a)
Self.from(b)
Две формы вызова
Конверсия доступна в двух формах, обе из одной реализации:
Fahrenheit.from(Celsius(100.0)) // 1. static method (From[T] protocol)
Celsius(100.0).into() // 2. instance method (Into[T] protocol)
Обе формы эквивалентны. Выбирай по читаемости:
T.from(v)— целевой тип выделен в начале, читается как «build a Fahrenheit from this Celsius». Хорош в выражениях, где тип цели — главная информация.v.into()— короче в method-chains:c.into().log(). Тип цели берётся из контекста (let s str = v.into(), параметр функции, return-type). Без context — компилятор попросит указать тип цели через аннотацию.
Free function into[T, U From[T]](v T) -> U не вводится —
третья форма создавала бы лишний выбор для программиста и LLM
(нарушение D9 «один очевидный путь»). Static T.from уже
покрывает explicit-type case, instance .into() — context-driven.
Throwing-варианты
From.from может throw’ить через Fail[E]:
type ParseError | InvalidFormat | OutOfRange
fn UserId.from(s str) Fail[ParseError] -> Self =>
match parse_int(s) {
Some(n) if n >= 0 => Self(n as u64)
Some(_) => throw OutOfRange
None => throw InvalidFormat
}
let id UserId = UserId.from("42") // throws Fail[ParseError]
Это обычная сигнатура с эффектом, никаких специальных правил.
? после такого вызова — нарушение D67 (from возвращает T через
Fail, не Result/Option):
let id = UserId.from(s)? // ОШИБКА D67
let id = UserId.from(s) // ok, throw сам пробрасывается
Почему
-
Нетривиальные конверсии — частая нужда. Единицы измерения (
Celsius↔Fahrenheit), парсинг (str→UserId), формат-обмен (Json→User). БезFromкаждый тип придумывает своё имя (Celsius.to_fahrenheit,User.parse_json). Единый protocol даёт общий контракт. -
Замещает старый
ToStr(D70 REPLACED → D73). D70 использовал ту же форму (protocol с одним методом + free function в prelude), но только для конверсии вstr. D73 обобщает паттерн на любые конверсии:From+into. Конверсия вstr— частный случай D73, не отдельный механизм. -
Selfуниверсален (D66).Selfв protocol-методе делает объявление коротким — не нужно повторять имя типа. До D66From[T]потребовал бы typeclass-механизм; с D66 это обычный protocol. -
Bounds (D72) разблокируют generic-функции.
fn parse[U From[str]]до D72 было невозможно. Теперь — естественно. -
Прецедент Rust.
From/Into— самый используемый паттерн в Rust ecosystem. Nova берёт идею (явные конверсии через protocol), адаптирует под свою систему (структурная типизация, без orphan rule, free function вместо blanket-impl). -
AI-friendly. LLM генерирует
Fahrenheit.from(celsius)без обдумывания имени метода. Структурный bound[U From[T]]проверяется compile-time с понятной ошибкой («Barне реализуетFrom[Foo]: missing static methodfrom(v Foo)»).
Что отвергнуто
- Free function
into[T, U From[T]](v T) -> U. Раньше была предложена как третья форма вызова (into[Target](value)). Отвергнута: дублируетT.from(v)(ровно та же ширина и информация), создаёт три формы для одной операции — нарушение D9.T.fromдля explicit-type,v.into()для context-driven — этих двух достаточно. - Только
From[T]безInto[T](как было в первой редакции D73). БезIntomethod-formc.into()была недоступна. ТеперьInto[T]— first-class protocol; method-form работает; компилятор выводит парность изFrom[T]автоматически. - Blanket-impl типа Rust
T: From<U> ⇒ U: Into<T>. В Nova нет orphan rule и нетimplблоков (D42/D53), классический blanket-impl негде. Решение Nova — компилятор синтезирует парный protocol на уровне type-checker’а: если у типа естьfrom, считается что есть и@into(и наоборот). Это сохраняет преимущество Rust (одна реализация → две формы вызова) без orphan-механики. Fromкак trait с default-методами. Безimplблоков и orphan rule концептуально неприменимо. Авто-синтез symmetric’а заменяет.- Implicit conversion в позиции аргумента (Scala 3
Conversion, C++ implicit constructors). Nova: все конверсии явные (as,from, D55 — но D55 only для sum/record-литералов, без method call). @from(v T) -> Selfinstance-метод вместо static.fromэто фабрика — у неё нет существующего инстанса для@. По D35fn Type.methodдля конструкторов / static, что соответствует семантике.asдля нетривиальных конверсий (celsius as Fahrenheit). D54 явно ограничиваетas— compile-time numeric/newtype/sum. Расширять — теряется граница между cheap-cast и expensive-conversion.- Отдельный
ToStrprotocol для конверсии в строку (старая D70). Конверсия вstr— частный случайFrom[X]-механизма. Иметь два механизма для одной задачи нарушает D9. См. D70 v3 «REPLACED → D73» про переход.
Цена
- Без context требуется явный целевой тип.
v.into()на bare-line-position не компилируется — нужно либоlet x T = v.into(), либоT.from(v)с явным типом-prefix’ом. - Multiple
From[X]через overloading по типу параметра (D84) — четыре оси перегрузки и правила ambiguity описаны в D84. Fromот типа из чужого модуля. Без orphan rule — добавляешьfn MyType.from(v ForeignType)где угодно, но реализация живёт в модуле, владеющемMyType(по D47 visibility). Если ни один из типов не «твой» — добавитьFromнельзя без обёртки (newtype). Это сознательное ограничение: предотвращает duplicate conflicting implementations.
Связь
- 02-types.md → D53 — protocol = тип, основа.
- 02-types.md → D66 —
Selfв protocol. - 02-types.md → D72 — bounds для
[U From[T]]. - 03-syntax.md → D35 — static / instance методы;
receiver — любой тип, включая примитивы (
fn str.from(...)). - 03-syntax.md → D54 —
asдля тривиальных cast’ов; D73 покрывает остальное. - 02-types.md → D55 — record/sum coercion; D73 для остальных типов.
- 04-effects.md → D67 —
fromс throw черезFailследует общим правилам?. - 08-runtime.md → D70
— REPLACED → D73; конверсия в
strэто частный случай D73. - D26 —
From,Intoв prelude.
Открытые вопросы
Fromдля базовых типов. Stdlib pre-registersstr.from(int),str.from(bool),str.from(f64)(D70-replacement). Должны лиint.from(bool),f64.from(int)etc. — сейчас open вопрос Q-from-builtins.TryFrom— отдельный protocol для fallible конверсий с явнымResult/Failв сигнатуре? Сейчас обычныйfromсFail[E]достаточен. Q-tryfrom.- Auto-derive
From— для newtype можно автоматически (type UserId u64⇒UserId.from(n u64) -> Self)? Сейчас программист пишет вручную. Q-auto-from. From-цепочки. ЕслиB: From[A]иC: From[B], можно ли одно вызовом перейтиA → C? В Rust — нет (single-step). Nova — пока тоже нет, программист пишетC.from(B.from(a)). Q-from-chain.
Эволюция
v1 (первая редакция D73): только From[T] protocol + free function
into[T, U From[T]](v T) -> U. Into отвергнут как «Rust-style
blanket-impl нет, не нужен отдельный protocol». Method-form
value.into() не работала.
v2: добавлен Into[T] protocol с instance-методом @into() -> T.
Компилятор автоматически синтезирует парный protocol — T.from(v X)
written → X.into() -> T synthesized (и наоборот). Три эквивалентные
формы вызова из одной реализации: into[T](v), v.into(),
T.from(v).
v3 (текущая, 2026-05-06): убрана free function into[T, U](v).
Три формы — это нарушение D9. Остались две: T.from(v) (static,
explicit-type) и v.into() (instance, context-driven). Также:
- D70
ToStrпомечен как REPLACED → D73 — конверсия в строку выражается черезstr.from(v)/v.into()(с context = str). - D35 явно расширен: receiver-тип может быть примитивом
(
fn str.from(int),fn int @to_hex() -> strи т.п.).
Что было невозможно до этого: D73 как механизм требует bound’ы
(D72). До D72 (Q-bounds открыт) From/Into пара была заблокирована.
С D72 разблокирована.
D74. Математические операции на числовых типах — instance-методы
Что
Стандартные математические функции (sin, cos, sqrt, atan2,
hypot, abs, pow, floor, is_finite, и др.) объявляются как
instance-методы через @ на числовых типах (f64, f32, int,
i8-i64, u8-u64), а не как static Math.fn(...) или free function
sin(x). Static-функции остаются только для констант
(f64.PI, f64.NAN) и парсинга (f64.try_parse(s)).
let r = (x * x + y * y).sqrt()
let phi = im.atan2(re)
let dist = a.hypot(b)
let s = (theta + offset).sin()
let n = magnitude.abs()
Правило
Полный набор на f64 (prelude)
| Категория | Методы |
|---|---|
| Корни и степени | @sqrt(), @cbrt(), @sqr(), @pow(exp f64), @powi(n int) |
| Тригонометрия | @sin(), @cos(), @tan(), @asin(), @acos(), @atan() |
atan2 (двух-арг) | @atan2(other f64) -> f64 (y.atan2(x)) |
| Гиперболические | @sinh(), @cosh(), @tanh() |
| Экспонента / лог | @exp(), @ln(), @log10(), @log2(), @log(base f64) |
| Норма / расстояние | @abs(), @hypot(other f64) |
| Округление | @floor(), @ceil(), @round(), @trunc(), @fract() |
| Знак / минимум | @signum(), @min(other f64), @max(other f64) |
| Предикаты | @is_finite(), @is_nan(), @is_infinite() |
Аналогичный набор на int (где математически осмысленно):
@abs(), @pow(n int), @signum(), @min(other), @max(other),
@is_negative(), @is_positive(). Тригонометрия и логарифмы — только
на float-типах.
Static-функции на типе (не методы)
Для констант и операций без естественного receiver’а — обычные static через точку (D35):
f64.PI // константа π
f64.E // константа e
f64.NAN // тихий NaN
f64.INFINITY // +∞
f64.NEG_INFINITY // -∞
f64.MAX // максимальное конечное
f64.MIN_POSITIVE // минимальное положительное
f64.EPSILON // машинная точность
f64.try_parse(s str) -> Option[f64] // парсинг с возможной ошибкой
Парсинг через f64.try_parse(s) дополнен From[str] через D73 —
доступна обе формы:
let x = f64.try_parse("3.14") // Option[f64]
let y f64 = f64.from("3.14") // throws Fail[ParseError]
let z f64 = "2.71".into() // через D73 авто-Into
Двух-аргументные функции
atan2, hypot, min, max, pow, log принимают два аргумента.
Receiver — первый по математической / физической конвенции:
y.atan2(x) // arctangent of y/x — y первый
a.hypot(b) // √(a² + b²) — симметрично, но a первый
base.log(other) // log_base(other)
x.pow(n) // x^n
Это даёт chain-style: dy.atan2(dx).abs() < tolerance.
Соответствующее имя @sqr()
@sqr() — квадрат (x*x). Имя из Pascal (Sqr(x)), короче
squared, согласовано с одноимённым методом на других типах
(например, Complex @sqr()). Для нецелых степеней — @pow(2.0)
или @powi(2).
Почему
-
Согласовано с D35 (03-syntax.md → D35).
@-методы — основной механизм для type-bound функций. Числовые операции — type-bound по определению (зависят от типа:i32.abs()≠f64.abs()в реализации). Использовать static-стиль для одних операций и@для других — нарушение D40 «один способ». -
Chain-friendly формулы. Длинные математические выражения читаются слева направо в «pipeline»-стиле:
let result = (a*a + b*b).sqrt().abs().min(MAX_VALUE)В static-стиле было бы:
let result = f64.min(f64.abs(f64.sqrt(a*a + b*b)), MAX_VALUE)Вложенность растёт справа налево, читать тяжелее.
-
Прецедент Rust / Kotlin / Swift. Все три используют instance- методы для математики (
(2.0_f64).sqrt(),theta.cos()). Java/JS/Python со static-стилем (Math.sin(x)) — наследие старой эпохи без object-методов на примитивах. -
Free functions конфликтуют с user-кодом.
sin(x)как глобальная функция занимает имяsin— пользователь не может назвать так свою функцию без shadowing prelude.@sin()живёт в namespace типа, не глобально. -
AI-friendly. LLM пишет
theta.cos()без раздумий «math.cos или Math.cos или просто cos». Один паттерн — один способ вызова.
Что отвергнуто
- Static
Math.sin(x)(Java, JavaScript). Менее читаемо для длинных формул, не chain-friendly, и в Nova нет объекта-namespaceMath(нет static-namespace объектов как в Java). - Free function
sin(x)(C, Python). Захватывает короткие имена в глобальном scope, конфликтует с пользовательскими функциями. - Trait-style
Floatprotocol сsin/cos/...(HaskellFloating, Rustnum_traits::Float). Лишняя indirection, generics с bounds для каждой математической функции усложняют сигнатуры. В Novaf64/f32— отдельные типы, дублирование методов на оба допустимо (как в Rust). - Разные имена для разных размеров (
sinfдля f32,sinдля f64 как в C). Перегрузка по типу receiver’а (D84) даёт одно имя, разные реализации — естественно для языка с типами. @squared()вместо@sqr(). Длиннее без выгоды;sqrимеет Pascal-прецедент и согласовано со стилем коротких имён в Nova (@neg,@inv,@conj,@arg,@rem,@shl).- Только static-функции для констант + instance для операций
через
@(mixed). Принято: константы — static (f64.PI— у значения нет receiver’а), операции —@. Это два разных рода имён (decleration site), не конфликт.
Цена
-
Дублирование методов между f32/f64, потенциально int. Реализация — обычно одна (через builtin / FFI к libm), но объявления повторяются. Это цена отсутствия Float-protocol; терпимо для prelude, который пишется один раз.
-
x.sqrt()дляx < 0возвращаетNaN(IEEE 754) — runtime- surprise. Strict-режим (Fail[NaN]) — отдельная функция@try_sqrt()если понадобится; в base — IEEE без проверок. -
Нет namespace
math. Если пользователь хочетimport math; math.sin(x)— придётся писатьx.sin(). Часть программистов из Python/Java будут удивлены поначалу.
Связь
- D26 — prelude содержит математику как часть числовых типов; D74 уточняет форму объявления.
- 03-syntax.md → D35 —
@-методы как механизм. - 03-syntax.md → D46 — operator overloading
(
@plus,@times, …) дополняет D74 для арифметики. std/runtime/math.nv— auto-generated external-fn декларации всех f64/f32 math методов (Plan 13).- 03-syntax.md → D40 — «один способ» — выбор между static и instance не остаётся на усмотрение программиста.
- D73 — парсинг
чисел через
f64.from(s)/s.into(), согласовано с from/into. - std/math/complex.nv —
использует instance-стиль (
theta.cos(),im.atan2(re),a.hypot(b)) как канонический пример.
Эволюция
Изначально черновик complex.nv (2026-05) использовал static-стиль
f64.cos(theta), f64.atan2(im, re) по аналогии с Java Math.sin.
При обсуждении выявлено что это противоречит D35 (методы — основной
механизм) и плохо читается для математических формул. Все вызовы
переписаны в instance-стиль, и паттерн зафиксирован формальным
D-решением D74.
Math namespace отвергнут (нет static-namespace в Nova, имя Math
конфликтовало бы с пользовательскими типами Math для предметных
областей).
D77. TryFrom / TryInto — protocol-пара, расширение D73 для fallible-конверсий
Уточнение (2026-05-07): D73 теперь сам поддерживает fallible через
Fail[E]в сигнатуреfrom/into— единый механизм. Программист пишет одну из 4-х форм (from/into/try_from/try_into), компилятор синтезирует остальные. Рекомендуется писатьtry_fromдля fallible (Result-стиль явный, error type first-class в signature) иfromдля infallible (без boilerplateOk(...)). Подробности в D73 «Auto-derive 4-way».Этот документ (D77) описывает Result-форму (
try_from/try_into) как рекомендуемую implementation form для fallible конверсий (вопреки названию «convenience sugar» в раннем описании).
Что
Парный механизм к D73 для fallible-конверсий: когда конверсия может не получиться, программист может выбрать одну из двух эквивалентных форм:
- Throwing-форма через
Fail[E]—T.from(v) Fail[E] -> Self(D73, основная форма). - Result-форма —
T.try_from(v) -> Result[Self, E](D77, convenience sugar).
Семантически эквивалентны (одна задача — конверсия с возможной ошибкой), различаются формой возврата ошибки. D73 forma — Nova- канонический путь («всё — эффект», D2/D10), D77 — для error-aware веток с explicit Result.
Компилятор синтезирует одну из другой. Программист пишет одну
сторону, другая выводится — точно так же как From ↔ Into в D73.
// Программист пишет — одну форму:
fn u64.try_from(s str) -> Result[Self, ParseIntError] => ...
// Компилятор автоматически даёт обе формы вызова:
let n = u64.from("42") // throws Fail[ParseIntError]
let r = u64.try_from("42") // Result[u64, ParseIntError]
let opt = u64.try_from("42").ok() // Option[u64] через Result.ok()
Option-вариант не требует отдельного метода — Result.ok()
из prelude превращает Result в Option. Один универсальный путь.
Правило
Декларация protocol’ов в prelude
type TryFrom[T, E] protocol {
try_from(v T) -> Result[Self, E]
}
type TryInto[T, E] protocol {
@try_into() -> Result[T, E]
}
Self (D66) — реализующий тип. try_from — static-метод (как
обычный from), try_into — instance-метод.
Авто-синтез четырёхугольника
Если программист пишет любую одну форму из четырёх, компилятор выводит остальные три:
T.from(v X) ← throws Fail[E]
T.try_from(v X) ← Result[Self, E]
v.into() -> T ← throws Fail[E]
v.try_into() -> T ← Result[T, E]
Правила синтеза:
-
from→try_from: оборачивает throw в Result.// Если написано: fn u64.from(s str) Fail[ParseIntError] -> Self => ... // Синтезируется: fn u64.try_from(s str) -> Result[Self, ParseIntError] => with Fail[ParseIntError] = |e| interrupt Err(e) { Ok(Self.from(s)) } -
try_from→from: разворачивает Result в throw.// Если написано: fn u64.try_from(s str) -> Result[Self, ParseIntError] => ... // Синтезируется: fn u64.from(s str) Fail[ParseIntError] -> Self => match Self.try_from(s) { Ok(v) => v Err(e) => throw e } -
from↔into/try_from↔try_into: через D73-механизм на каждой из форм отдельно. То есть если написаноu64.from(s), синтезируются:
Если написаны обе (например, from и try_from обе вручную) —
обе используются как написаны, авто-синтез не применяется. Как в D73,
программист отвечает за consistency.
Какую форму писать?
Рекомендация — писать try_from, для парсинга / валидации:
fn u64.try_from(s str) -> Result[Self, ParseIntError] =>
if !is_all_digits(s) {
Err(InvalidDigit { position: 0 })
} else {
// ... основная логика
Ok(parsed_value)
}
Причины:
- Result-возврат явный — программисту не нужно держать в голове
активный handler
Fail[E]. - Тип ошибки виден в сигнатуре (
Result[Self, ParseIntError]), а не пробрасывается через эффект-row (где может теряться). - Pattern matching на Result удобен внутри парсера для composition.
from остаётся для случаев когда программист уверен в успехе и
не хочет писать match:
fn UserId.from(n u64) -> Self => Self(n) // infallible
fn Greeting.from(name str) -> Self =>
Self("Hello, ${name}!") // тоже infallible
Если конверсия infallible — from достаточно, try_from не
синтезируется (нет E).
Семантика равенства
from(s) и try_from(s).unwrap() — поведенческое равенство (с
учётом разной формы ошибки). Компилятор гарантирует:
try_from(v) == Ok(x)⇒from(v) == xtry_from(v) == Err(e)⇒from(v)бросаетthrow e
D67 ?-оператор
let v = u64.try_from(s)?— валидно, Result оборачивается через D67?на Result.let v = u64.from(s)?— ошибка (D67),fromвозвращает T черезFail, не Result. Throw сам пробрасывается без?.
// Функция возвращает Fail[ParseIntError]:
fn parse_pair(s str) Fail[ParseIntError] -> (u64, u64) {
let parts = s.split(",")
let a = u64.from(parts[0]) // throws через Fail (без ?)
let b = u64.from(parts[1]) // throws через Fail (без ?)
(a, b)
}
// Функция возвращает Result, использует try_from + ?:
fn parse_pair_r(s str) -> Result[(u64, u64), ParseIntError] {
let parts = s.split(",")
let a = u64.try_from(parts[0])? // ? на Result ([D85](/spec/decisions/effects/#d85))
let b = u64.try_from(parts[1])?
Ok((a, b))
}
Option через Result.ok()
Отдельный try_parse / from_str_or_null / similar не вводится.
Если нужен Option — Result.ok() в prelude:
fn Result[T, E] @ok() -> Option[T] => match @ {
Ok(v) => Some(v)
Err(_) => None
}
// Использование:
let opt = u64.try_from(s).ok() // Option[u64]
match u64.try_from(s).ok() {
Some(n) => n
None => default_value
}
Прецедент Rust: s.parse::<u64>().ok() → Option<u64>. Один
универсальный путь, не требует отдельного именования.
Почему
-
Согласовано с D73. Тот же auto-pair-механизм. Программист видит ровно один паттерн «пишу одну сторону — компилятор даёт все формы вызова». Не нужно помнить «for fallible — другая система».
-
Закрывает три формы вызова через одну реализацию. Парсинг — частый use case. Без D77 программисту нужно либо:
- Писать
try_Xотдельно (Kotlin-styletoIntOrNull, размножение имён), или - Всегда
match { Some => ... None => throw }обёртку.
- Писать
-
Стандартизованное имя
try_from. До D77 разные библиотеки могли использоватьtry_parse,parse_or_err,validate, и т.д. — каждая со своим именем. С D77 — единое имя какfromстандартно для конверсии. -
Прецедент Rust:
From/TryFrom— стандартstd. Auto-blanket реализация (Into ↔ From) делается компилятором. Nova повторяет паттерн. -
Option получается бесплатно через
Result.ok(). Не нужны_or_null-suffix имена (Kotlin),init?(Swift),*OrNull(Java fluent helpers). Один Result — три формы (from,try_from,try_from(...).ok()). -
AI-friendly. LLM пишет
Version.from(s)и работает; пишетVersion.try_from(s)?для propagation через Result — тоже работает. Не нужно помнить какая форма реализована — всегда обе доступны.
Что отвергнуто
u64.try_parse(s) -> Option[u64]— отдельный Option-вариант как метод. Конфликтует с принципом «один способ» (D9):try_parsevstry_from(...).ok()делают одно и то же. Result.ok() универсальнее.u64.parse(s)— отдельное имя для парсинга. Парсинг — это частный случай конверсии (str → u64), общий механизм черезfrom/try_fromлучше.OrNull-suffix имена (Kotlin):toIntOrNull. Размножение имён, не масштабируется (fromOrNull,intoOrNull,parseOrNull).- Java-style overloading throwing/non-throwing с одинаковым именем
(
int.parse(s) -> intvsint.parse(s) -> intчерез флаг). Тип-ambiguity, нечитаемо. - Failable initializer как в Swift (
init?). Специальный синтаксис конструктора — лишняя категория. У Novafrom/try_fromобычные функции.
Цена
-
Расширение compiler-логики. D73 уже синтезирует пару From/Into, D77 удваивает: from/try_from + into/try_into = 4 формы из одной написанной. Компилятор должен:
- Распознать одну из четырёх форм
- Сгенерировать остальные три
- Применять одни и те же правила structural-conformance. Цена — реализация в type-checker’е, не run-time.
-
Semantic equivalence требует доверия. Компилятор гарантирует что
from(v)иtry_from(v).unwrap()поведенчески одинаковы. Если программист пишет обе вручную и они расходятся — ответственность программиста (как в D73). -
Ambiguity при нескольких
try_from. Если уu64естьtry_from(str)иtry_from(f64)(через overloading D84) —u64.try_from(x)резолвится по типу аргумента. Стандартный overloading. -
Selfв Result.Result[Self, E]корректно по D66 (Self валиден в method-контексте). Generic-параметрEсвободен — не привязан к Self.
Связь
- D73 — базовая пара From/Into, D77 расширяет на fallible-форму.
- D67 —
?-оператор; работает на Result (try_from(s)?), не работает на throwingfrom. - D72 — bounds:
[U TryFrom[T, E]]для generic-функций fallible-конверсии. - D26 —
TryFrom,TryInto,Result,Optionв prelude.Result.ok() -> Option[T]— стандартный метод для перевода. - D30 — конвенция имён ошибок
(
Parse<TypeName>Error); не меняется. - std/data/semver.nv —
использует
u64.try_parse(legacy имя) — должно мигрировать наu64.try_fromпосле принятия D77.
Открытые вопросы
- Auto-derive для newtype?
type UserId u64— должны ли автоматически бытьUserId.from(n u64)иUserId.try_from(s str)? Сейчас — программист пишет вручную. Q-auto-from осталось открытым из D73, расширяется на D77. fromцепочки (A → B → C) — ни D73, ни D77 не вводят транзитивность. Программист пишетC.from(B.from(a)). Q-from-chain.TryFromдля одного и того жеTс разнымиE? Пример:u64.try_from(s str) -> Result[Self, ParseIntError]иu64.try_from(s str) -> Result[Self, ValidateError]— отличаются толькоE. По D84 ось 3 (overloading по типу результата) формально это поддерживает, но требует context для дисамбигуации (let r Result[u64, ParseIntError] = u64.try_from(s)). Если контекста нет — compile error «cannot resolve overload». Альтернатива на call-site без контекста —enum-объединение ошибок (type AnyError | A | B) или разные имена. Q-tryfrom-multi-error.
Эволюция
До D77 в первой реализации std/data/semver.nv использовался
u64.try_parse(s) -> Option[u64] — отдельное имя для Option-варианта
парсинга. При обсуждении выявилось три проблемы:
- Ad-hoc имя — каждая stdlib-либа могла использовать своё
(
try_parse,parse_opt,from_str_or_null). - Дублирование с
from—try_parseэто «fromминус throw, плюс Option». Семантически избыточно. - Прецедент Rust —
TryFromпарный кFromрешает ту же задачу унифицированно.
D77 формализует: одно имя try_from для Result-варианта, авто-
синтез четырёх форм вызова из одной реализации. Option получается
через Result.ok(). try_parse отвергается как избыточное.
Backward-compat: try_parse в существующих файлах (semver.nv) —
переименовывается на try_from. Общая семантика не меняется.
D76. Mem эффект — runtime introspection для leak/growth тестов
Status: active. Реализовано в bootstrap’е (2026-05-06). Тесты:
nova_tests/runtime/memory_growth.nv.
Что
Built-in эффект Mem даёт Nova-коду доступ к runtime-счётчикам
аллокаций. Цель — regression detection: тест запоминает
Mem.alloc_count() до и после горячего кода и assert’ит, что прирост
остался в разумном бюджете. Если codegen начнёт генерировать в N раз
больше аллокаций (баг типа “alloc-per-iter увеличился на порядок”),
тест поймает это сразу.
Операции
Mem.alloc_count() -> int // total nova_alloc since gc_init/reset
Mem.free_count() -> int // total frees (plain malloc backend → 0)
Mem.live() -> int // alloc_count - free_count
Mem.reset() -> () // zero stats counters (for per-test isolation)
Числа — это счётчики вызовов, не байты. Этого достаточно для поимки регрессий “1 alloc на итерацию стало 10”.
Семантика
Mempre-registered как built-in эффект (какTime,Fail). Compiler не требуетMemв сигнатуре функции — это ambient capability (D11 / D62-style).- Нет user-handler’а: в отличие от
TimeиFail, операцииMemне имеют vtable; они эмитируются прямо вNova_Mem_*inline-функции, которые ходят к runtime-counters. Причина: эти операции должны быть наблюдаемыми с очень низкими накладными расходами — vtable добавляет лишний indirect call который сам бы изменил alloc-pattern. И смысла переопределять их нет (это не business effect — это runtime-факт).
Реализация
compiler-codegen/nova_rt/alloc.h— runtime-функцииnova_gc_alloc_count,nova_gc_free_count,nova_gc_live_count,nova_gc_reset_stats. Доступны во всех allocator-backend’ах.compiler-codegen/nova_rt/alloc.c(Phase-0 plain malloc) — считаетnova_alloccalls;free_countвсегда 0 (releaseno-op). Достаточно для growth-rate тестов.compiler-codegen/nova_rt/effects.h—Nova_Mem_*inline- обёртки.compiler-codegen/src/codegen/emit_c.rs—effect_schemaspre-populated сMemschema; standard effect-call dispatch работает (Mem.live()→Nova_Mem_live()).
Bootstrap-ограничения
- Plain-malloc backend (default):
free_countвсегда 0,live==alloc_count. Это значит leak-тесты могут только измерять growth rate, не “осталось ли что-то живое”. Когда подключим Boehm GC (alloc_boehm.c) или RC (alloc_rc.c) — free_count станет осмысленным, тесты можно расширить. - Нет per-allocation type info.
alloc_count— счётчик всехnova_alloccalls без разбивки по типам. Production-runtime возможно даст breakdown (records, arrays, fiber stacks). - Не thread-safe в multi-threaded backend’е (счётчики не atomic). На bootstrap single-threaded fiber-runtime это OK.
Связь
- D7 — runtime modes;
Memдоступен во всех режимах. - D11 — pre-registered effects pattern.
- 05-memory.md → D6 — managed-heap design;
Mem— observability над ним.
Что отвергнуто
- Free function
mem_alloc_count()— нарушает D9 («одна идиома для одной задачи»). Effect-форма даёт ровно столько же выразительности и согласована с Time. - Bytes-tracking в bootstrap — требует instrumentированного allocator (overhead). Counts достаточно для regression-detection.
D81. assert(cond) vs debug_assert(cond) — build-mode семантика
Что
Два уровня assertion’ов в prelude:
assert(cond)— always runtime, проверяется во всех режимах сборки (debug/release/JIT/AOT). Failure → panic (D13).debug_assert(cond)— debug-only, в release-сборке полностью отбрасывается компилятором (zero cost).
Третий уровень — формальные контракты requires/ensures
(D24) — отдельный механизм, не путать.
Правило
Декларация в prelude
// always runtime — production invariants
fn assert(cond bool) -> ()
// debug-only — hot-path / sanity checks
fn debug_assert(cond bool) -> ()
Сигнатуры идентичны на уровне типов; разница — в семантике релиза. Обе — обычные prelude-функции (не keyword’ы), вызываются со скобками как любой fn-call (см. также syntax.md секция «Тестирование без моков»).
Семантика по build-mode
| Form | Compile-time check | Debug runtime | Release runtime | Use-case |
|---|---|---|---|---|
assert(cond) | нет | check | check | production invariants |
debug_assert(cond) | нет | check | no-op | hot-path / sanity |
requires/ensures (D24) | SMT где возможно | check rest | no-op | formal contracts |
Примеры использования
// Production invariant — всегда проверяется
fn divide(a int, b int) -> int {
assert(b != 0) // ВСЕГДА runtime, даже в release
a / b
}
// Hot-path — release не платит за проверку
fn fast_lookup(arr []int, idx int) -> int {
debug_assert(idx >= 0 && idx < arr.len()) // только в debug
arr[idx] // unchecked в release
}
// Формальный контракт — compile-time где возможно, runtime fallback
fn sqrt(x f64) -> f64
requires x >= 0.0
ensures result >= 0.0
=> ...
Build-mode mechanics в bootstrap
Bootstrap (D71) не различает debug/release — все три режима
(D7) одинаковы, всегда
checked. debug_assert в bootstrap’е — синоним assert (тот же
runtime check, готовность к production-семантике).
Production-runtime добавит:
- preprocessor-style
#ifdef NOVA_DEBUGдля C-backend, или - codegen-флаг для no-op generation в release-сборке.
Build-mode влияет на performance, не на семантику программы:
assert всегда работает; debug_assert — только performance в release.
Это согласовано с D7 принципом «один язык — три режима».
Почему assert = always runtime (не Java/C-style no-op)
-
AI-friendly: одна семантика. LLM генерирует
assert(...)ожидая, что invariant держится. Если в release он silent — это тихий bug class (Java pre-1.4 classic). -
Безопасность. «Production runs without your invariants» — известная проблема C/Java/Python: программист в курсе своих asserts только в debug, в release они исчезают без следа.
-
Прецедент Rust/Swift.
assert!в Rust always runtime;debug_assert!для debug-only. Swift аналогично:assertdebug-only,preconditionalways runtime — но Nova инвертирует defaults (более безопасный — короткое имя). -
Согласовано с D24. Если программист хочет zero-cost проверку с compile-time гарантией — пишет
requires(D24 contract). Если просто debug-time hint —debug_assert.assert— strong invariant, всегда работает. -
D13 (panic vs effects).
assertfailure = panic = fiber dies. Это «hardware/math сбой» класс, не business error. По D13 такое не должно зависеть от build-mode.
Что отвергнуто
assertno-op в release (C/Java/Python style). Тихие bug’и в production — главная причина отказа.assertкак keyword без скобок (Rust macro / Javaassertexpression). Закрыто в spec sweep 2026-05-07: assert — обычная fn-call, со скобками. Один способ для одной задачи (D40).- Только один уровень (
assertalways runtime). Hot-path use-case реален; безdebug_assertпрограммисты пишутif (DEBUG) { ... }ручками. Лучше дать canonical-форму. - Только один уровень (
assertdebug-only). Невозможно выразить production invariant. Java pre-1.4 опыт показывает что это anti-pattern.
Связь
- D7 — три режима компиляции; D81 уточняет, как build-mode влияет на assert-семантику.
- D13 — assert failure = panic, не Fail-эффект.
- D24 —
requires/ensuresконтракты; D81 определяет три уровня safety:assert<debug_assert<contracts. - D26 — prelude содержит обе функции (
assert,debug_assert). - spec/syntax.md — секция «Тестирование без моков» уточняет, что
assert(cond)обязательно со скобками (fn-call).
Эволюция
До 2026-05-07 spec упоминал assert неявно — в syntax.md как
«встроенный оператор» (без скобок), в D26 prelude как функцию (со
скобками). Bootstrap-парсер принимал только со скобками.
spec-assert-syntax sweep 2026-05-07 канонизировал форму
assert(cond) — функция из prelude, обязательно со скобками.
D81 закрывает оставшийся вопрос — семантика в release.
Принята модель Rust (assert! always runtime + debug_assert!
debug-only). До D81 spec не различал assert/debug_assert,
bootstrap имел только always-runtime nova_assert без build-mode
разделения. После D81: prelude содержит обе функции; production-
runtime реализует zero-cost debug_assert в release; bootstrap
оставляет debug_assert как alias assert до production.
D82. external fn — функции с runtime-implementation
Что
external fn — модификатор функции-декларации, означающий что тело
функции реализовано в runtime (C-коде nova_rt/), а не на Nova.
Декларация даёт сигнатуру и имя; codegen lookup’ит C-функцию по
имени в hard-coded таблице.
external применяется к функциям (этот D-block) и к типам
(D126, Plan 62.D.bis, 2026-05-18). Один и тот же keyword, два валидных
позиционирования. Built-in opaque-типы (StringBuilder, WriteBuffer,
ReadBuffer) теперь имеют formal Nova-side declaration через
external type в std/prelude/collections.nv — раньше (до 62.D.bis)
существовали как «known-by-name» (без formal declaration).
Правило
Грамматика
fn-decl = ['export'] ['external'] 'fn' [receiver] name [generic-params]
[params] [effects] ['->' return-type] [body | ';']
Порядок modifiers строгий: export первым, external вторым. Body
у external fn должен отсутствовать (никакого => или { ... }),
иначе compile error «external function cannot have a body».
Примеры
// Public external static
export external fn StringBuilder.new() -> Self
// Public external instance, mutating
export external fn StringBuilder mut @append(s str) -> ()
// Private external (используется внутри runtime/builtins.nv module'а)
external fn Nova_intrinsic_unreachable() -> never
Связь с D26 prelude
Built-in opaque-типы из D26 (StringBuilder, WriteBuffer,
ReadBuffer) имеют type declaration через external type
(D126,
std/prelude/collections.nv) + methods через external fn
(этот D-block, std/runtime/<name>.nv). Связь декларация ↔ methods
— по receiver-type name.
// std/prelude/collections.nv (Plan 62.D.bis, 2026-05-18)
module std.prelude.collections
export external type StringBuilder // D126
export external type WriteBuffer // D126
export external type ReadBuffer // D126
// std/runtime/string_builder.nv (auto-generated, Plan 13 Ф.8)
module std.runtime.string_builder
export external fn StringBuilder.new() -> Self
export external fn StringBuilder.with_capacity(n int) -> Self
export external fn StringBuilder mut @append(s str) -> Self
// ... остальные методы
Self в receiver-context для external — StringBuilder (имя
содержащего receiver-type’а). Те же правила, что для обычных
fn-декл.
Связь с D5/D47 видимостью
export external fn — публичная: имя видно из других модулей.
external fn без export — модуль-private. Те же правила, что для
обычных fn-декл. external ортогонален export.
Связь с будущим FFI
external fn — для функций, реализованных в Nova-runtime
(nova_rt/*.h/.c). Для функций, импортируемых из сторонних
C-библиотек (libc, OS-libs), будет отдельный keyword
extern("C") (Q-ffi, не реализуется сейчас). Семантика разная:
| Keyword | Реализация | C-name | Разрешён программисту |
|---|---|---|---|
external fn | Nova-runtime (nova_rt/) | Nova_<Type>_<...> mangled | нет (только в std.runtime.*) |
extern("C") fn (TBD) | сторонний C/lib | as-is | да (FFI) |
Программистский Nova-код не пишет external fn. Этот keyword —
экспозиционный: только модули в std.runtime.* имеют право его
использовать. Компилятор отклоняет external fn в любом другом
namespace’е.
Mangling и dispatch
Codegen не хранит список external-функций. Source of truth — это
std/runtime/builtins.nv. Codegen знает только правила mangling
и для каждой external fn декларации выводит C-name детерминированно:
| Nova-form | C-name |
|---|---|
T.method(...) static | Nova_T_static_method(...) |
t.method(...) instance | Nova_T_method_method(t, ...) |
t.method(...) mut instance | Nova_T_method_method(t, ...) (тот же mangling) |
Имена параметров в C-сигнатуре генерируются из позиций (arg0,
arg1, …); типы маппятся по canonical Nova→C таблице (int →
nova_int, str → nova_str, u8 → uint8_t, u32 →
uint32_t, &T → Nova_T*, mut T → Nova_T*, …).
Этот mapping архитектурно идентичен registry built-in conversions (D73 + Plan 08 Ф.2). Один механизм lookup’а.
Validation: builtins.nv — single source of truth
Подписи external-функций живут только в std/runtime/builtins.nv.
Никакой дублирующей таблицы в Rust-коде codegen’а быть не должно;
если есть — это bug, и расхождение между .nv-декларацией и Rust-
таблицей приведёт к runtime-крашу или silent UB.
Сигнатура в этом разделе понимается полно — это весь contract вызова, не только имя и типы параметров:
| Компонент | Используется для |
|---|---|
Имя метода (write_u32_be) | C-name через mangling |
Receiver-type + mut-флаг (WriteBuffer mut) | Первый параметр C-функции (Nova_WriteBuffer*), prefix mangling |
| Параметры (имена + типы, в порядке) | Остальные параметры C-функции; для overload — также часть mangling (Plan 11 Ф.3) |
| Return-type | C-return type; для auto-derive — целевой тип synthesized обёртки |
Effects (Fail[E], etc.) | Дополнительный *err-параметр в C-сигнатуре + control-flow эмиссии |
Любой из этих компонентов, если расходится между .nv-декларацией и
runtime-реализацией компилятора, отлавливается самим Nova-
компилятором при загрузке builtins.nv (раздел Diagnostics ниже),
не на стадии C-toolchain’а. В частности return-type входит в
проверку: если в builtins.nv ... -> u32, а компилятор знает
что runtime возвращает uint64_t — Nova-error «signature
mismatch».
Pipeline:
- Компилятор парсит
std/runtime/builtins.nvкак обычный Nova- модуль. Каждаяexport external fn ...-декларация даёт AST-узел с полной сигнатурой (имя, receiver, params, return, effects). - Codegen применяет mangling rules → C-name + C-prototype:
void Nova_WriteBuffer_method_write_u32_be(Nova_WriteBuffer*, uint32_t); - Codegen сверяет каждую декларацию со своим внутренним реестром реализованных runtime-функций (компилятор и runtime — один версионируемый артефакт, см. Diagnostics ниже).
- Если совпадает — codegen эмитит C-prototype в сгенерированный
header для линковки с
nova_rt/. - Если не совпадает (нет реализации, расходится сигнатура) → Nova compile error до запуска C-toolchain’а.
Что это даёт:
- Программист добавляет
export external fn WriteBuffer mut @write_u64_le(v u64) -> ()в builtins.nv → если компилятор уже поддерживаетNova_WriteBuffer_method_write_u64_le(в bundled runtime), декларация принимается; иначе — Nova-error с понятной диагностикой. - AI-генерируемый код для расширения runtime API — два места правки: builtins.nv (Nova-side) + nova_rt/*.c (C-side). Компилятор валидирует, что они согласованы.
Что это запрещает:
- Hard-coded списки методов конкретных opaque-типов в codegen’е
(сейчас
record_schemas.insert("StringBuilder", ...)+ method dispatch таблицы) — должны быть удалены или сведены к чтению AST builtins.nv. Q-codegen-builtins-cleanup, Plan 12 Ф.5. - «Скрытые» external-функции, известные только codegen’у, без
декларации в builtins.nv. Если codegen эмитит вызов
Nova_X_method_y— соответствующаяexternal fn X.@y(...)декларация обязана существовать в builtins.nv (или другом модуле вstd.runtime.*).
Diagnostics: компилятор сам валидирует, без C-toolchain
Nova компилируется в C, который потом обрабатывается C-toolchain
(cc/clang/MSVC). У C-toolchain есть свой линкер, но мы не
полагаемся на его ошибки для пользовательской диагностики:
mangled C-имя в undefined reference to Nova_WriteBuffer_method_X
не понятно тому, кто пишет на Nova.
Вместо этого Nova-компилятор сам знает, какие external-функции
реализованы в bundled runtime (nova_rt/). Runtime версионируется
вместе с компилятором; компилятор всегда знает свой runtime.
builtins.nv — проекция этого знания в Nova: декларации, которые
компилятор валидирует против собственного внутреннего реестра.
Расхождение выдаётся как Nova compile error до запуска cc.
Таксономия:
| Случай | Когда | Диагностика |
|---|---|---|
User вызывает несуществующий метод opaque-типа (sb.unknown()) | type-check | Nova: no method 'unknown' on StringBuilder. Available: append, len, capacity, ... |
external fn X.@y в builtins.nv ссылается на функцию, не реализованную в runtime | при загрузке builtins.nv в codegen | Nova: external fn 'StringBuilder.@y' not implemented in runtime. Either remove from std/runtime/builtins.nv or add Nova_StringBuilder_method_y to nova_rt/string_builder.c |
| Сигнатура в builtins.nv не совпадает с реализацией компилятора (тип параметра, return-type, effects) | при загрузке builtins.nv | Nova: signature mismatch for 'StringBuilder.@append': declared 'fn (s str) -> ()', runtime expects 'fn (s str) -> int' |
| Codegen эмитит вызов внешней функции, не объявленной в builtins.nv | bug в компиляторе | internal compile error: compiler bug: emitted call to undeclared external 'X.@y'. Не должно случаться у пользователя; если случилось — bug-report |
User объявил auto-derived форму (@try_read_X рядом с @read_X) | при загрузке builtins.nv | Nova: '@try_read_X' is auto-derived from '@read_X' (D77 Fail↔Result); remove from std/runtime/builtins.nv |
C-toolchain никогда не должен быть первым, кто заметит проблему.
Если он всё-таки выдаёт undefined reference — это bug в Nova-
компиляторе: либо реестр был неполным, либо валидация не сработала.
Что не валидируется на этом уровне:
- Семантика реализации (правильно ли
write_u32_beпишет big-endian байты) — runtime tests, не compile-time check. - Memory ownership / lifetime / aliasing — это контракт типа (mut, &T), линкер его не видит.
Почему
Зачем нужен external keyword
- Документация stdlib API. Программист (и AI) видя
external fn StringBuilder.new()понимает: тело реализовано runtime’ом, не Nova. Не нужно искать вnova_rt/где определён. - Compile-time validation. Без
externalкомпилятор не знает, что функция без тела должна искаться в C-runtime — попытается эмитить empty body и упадёт. Сexternal— явный contract. - AI-friendly. LLM-генерируемый код для stdlib имеет canonical
форму:
export external fn .... Шаблонная подстановка тривиальна. - Будущая совместимость с FFI. Когда появится
extern("C")для сторонних libs, два keyword’а различаются однозначно.
Почему не intrinsic или builtin
intrinsic— занят понятием compile-time intrinsic (Rust-styleintrinsics::transmute). Для Nova таких пока нет, но имя зарезервируем.builtin— слишком общее.int/strтоже builtin (D26), но они типы, не функции.external— точное слово: «реализация во внешнем (по отношению к Nova-source) контексте — runtime/C». Прецеденты: OCamlexternal, Dartexternal, Kotlinexternal.
Почему не extern
D30 фиксирует «полные слова, не сокращения». external — full word.
extern — сокращение (как в C/Rust). Мы выбираем full form.
Что отвергнуто
- Без keyword’а — компилятор сам решает по имени модуля. Магия:
программист не видит чего ожидать, AI генерирует boilerplate-
typeдекларации. builtin fn— конфликт с понятием built-in типа.@externalатрибут вместо keyword’а. Атрибуты в Nova зарезервированы для тестов / dev-tools (Q-attributes). Modifier-форма единообразна сexport/mut.external type— закрыто 2026-05-18 в D126. Изначально для три built-in (StringBuilder/WriteBuffer/ReadBuffer); future user-defined opaque типы (Channel, mmap’ed Region) — тот же D126 mechanism + relaxation whitelist’а. Plan 62.D.bis (Ф.1–Ф.6, 2026-05-18) — реализация в bootstrap.- Codegen — single source (вариант A). Сигнатуры жили бы в Rust-таблицах; builtins.nv был бы только документацией, а codegen cross-check’ал бы при чтении. Отвергнуто: дублирование (два места правки на каждую новую runtime-функцию), риск тихого расхождения если cross-check где-то пропущен, недружелюбно к AI (надо править Rust-код codegen’а).
- Hybrid: builtins.nv для типов + codegen хранит mangling. Тоже отвергнуто — оставляет Rust-таблицу как «второй источник», даже если меньшего объёма. Принят чистый вариант B: builtins.nv — единый источник; codegen знает только правила mangling.
Связь
- D5 / D47 —
exportmodifier;external— ортогональный второй modifier. - D26 — prelude содержит StringBuilder/WriteBuffer/ReadBuffer
как built-in opaque-типы; декларации API — через
external fn. - D30 — naming convention;
external— full word. - D52 — kind-tokens (
type/effect/protocol); D82 не добавляет нового kind-token’а. - D54 —
as/isдля конверсий; не пересекается. - D73 — From/Into registry; D82 использует тот же dispatch-механизм для external-функций.
- D126 —
type-аналог D82 (
external typeдля opaque-типов с runtime backing). Один keywordexternal, два валидных позиционирования.
Эволюция
До 2026-05-08 spec фиксировал Buffer как единый тип (Q-buffer) —
text+binary mixed. В разговоре про endianness-методы выявилось
семантическое смешение: add_str рядом с add_u32_le несогласовано.
Plan 04 (зафиксирован 2026-05-08) — split на три типа
(StringBuilder / WriteBuffer / ReadBuffer) + новый keyword
external для документирования stdlib runtime-функций. До D82 такие
функции декларировались как обычные fn без тела (компилятор
special-case’ил по имени receiver’а — fragile).
Bootstrap status (2026-05-08)
- ✅ Спека: D82 закрыт (этот блок). Validation rule (builtins.nv —
single source of truth) добавлен 2026-05-08 после обсуждения
signature mismatch для
WriteBuffer.@write_u32_be. - ⏳ Lexer:
KwExternaltoken — TBD (Plan 04 Этап 2). - ⏳ Parser:
externalmodifier вparse_fn_decl— TBD. - ⏳ AST:
is_external: boolflag — TBD. - ⏳ Codegen: чтение external-деклараций из AST builtins.nv, применение mangling rules, эмиссия C-prototype’ов в header — TBD (Plan 04 Этап 2).
- ⏳ Codegen cleanup: удалить hard-coded
record_schemas.insert(...)и method dispatch-таблицы для StringBuilder/WriteBuffer/ReadBuffer. Должны замениться чтением builtins.nv. Это ломает silent расхождения, которые сейчас существуют (Q-codegen-builtins-cleanup). - ⏳ Runtime:
nova_rt/string_builder.h/write_buffer.h/read_buffer.h— TBD. Реализации обязаны матчить builtins.nv по C-name + сигнатуре; иначе linker error.
Plan 13: расширение projection на str/math + декомпозиция (2026-05-08)
После Plan 13 Ф.8 в std/runtime/ нет ни одного handwritten файла.
builtins.nv ❌ REMOVED — декомпозирован на per-type auto-generated файлы:
| Что | Файл (auto-gen) |
|---|---|
| str API (UTF-8 операции) | std/runtime/string.nv |
| f64/f32 math (D74 instance-методы) | std/runtime/math.nv |
char/str interop (str.from(c char)) | std/runtime/char.nv |
| StringBuilder API | std/runtime/string_builder.nv |
| WriteBuffer API | std/runtime/write_buffer.nv |
| ReadBuffer API | std/runtime/read_buffer.nv |
Источник истины — compiler-codegen/src/codegen/runtime_registry.rs (Rust):
~157 entries (~17 str + ~50 math f64+f32 + ~50 ReadBuffer fail+try
форм + ~20 WriteBuffer numeric × LE/BE + StringBuilder + char).
Команда regen_runtime.bat (или .\regen_runtime.ps1, или прямой
nova-codegen emit-runtime-stubs) генерирует все 6 .nv файлов;
manual edit запрещён (CI guard через --check).
ExternalRegistry в codegen загружает 4 .nv файла через include_str!
(string_builder, write_buffer, read_buffer, char) — единый registry для
opaque-types dispatch (Plan 12). string.nv/math.nv пока загружаются
emit-runtime-stubs только; codegen-side dispatch для str/math остаётся
через legacy special-cases (Plan 13 Ф.4 deferred).
См. docs/plans/13-runtime-stdlib-and-autogen.md.
D109. Встроенные методы примитивных типов — hash, eq, ord
Что
Компилятор автоматически предоставляет следующие методы для стандартных
примитивных типов без явных деклараций в .nv файлах:
| Метод | Возврат | Применимо |
|---|---|---|
hash() -> u64 | беззнаковый 64-bit хеш | int, bool, f64, char, u8, str |
eq(Self) -> bool | равенство | int, bool, f64, char, u8, str |
lt(Self) -> bool | строго меньше | int, f64, char, u8, str |
le(Self) -> bool | меньше или равно | int, f64, char, u8, str |
gt(Self) -> bool | строго больше | int, f64, char, u8, str |
ge(Self) -> bool | больше или равно | int, f64, char, u8, str |
Эти методы нужны для использования примитивов как ключей в
HashMap[K, V Hashable] и других коллекциях с protocol bounds (D72).
Семантика
hash:
int/char/u8— FNV-1a по 8 байтам значения (nova_int_hash).bool— 0 или 1 (nova_bool_hash).f64— FNV-1a по битовому представлению (nova_f64_hash; -0.0 и 0.0 хешируются по-разному — bootstrap ограничение, production fix V2).str— FNV-1a по байтам контента (nova_str_hash; уже реализован, объявлен явно вstd/runtime/string.nv).
eq: сравнение по значению. f64.eq использует == (NaN != NaN по IEEE 754).
lt/le/gt/ge: лексикографически для str, по значению для остальных.
Для bool эти методы не предоставляются (нет естественного порядка).
Как реализовано
C-функции в nova_rt.h:
nova_int_hash(nova_int) -> nova_intnova_bool_hash(nova_bool) -> nova_intnova_f64_hash(nova_f64) -> nova_int(возвратnova_int= int64_t, хранит битовое значение u64)
eq/lt/le/gt/ge для nova_int/nova_bool/nova_f64 — inline C-операторы
==, <, <=, >, >= (без отдельных C-функций).
Codegen: prim_builtin_method(c_ty, method) в emit_c.rs перехватывает
метод-вызов до общего resolver’а и эмитит нужный код.
Что отвергнуто
- Явные декларации в prelude.nv — лишний boilerplate, нет спасения от расхождения между .nv и runtime impl. Codegen-уровень: единый источник правды.
Ordprotocol bound — структурный bound (lt/le/gt/geметоды) V2; для D109 достаточно auto-dispatch без формальногоOrdprotocol.- Хеш для пользовательских типов — авто-derive (аналог Rust
#[derive(Hash)]) V2; требует рекурсивного обхода полей.
Связь
- D72 — Generic bounds —
Hashableтребуетhash() -> u64. - D26 — stdlib — примитивные методы часть runtime stdlib.
- docs/plans/48-closures-in-generics.md → Ф.8 — реализация.
D124. Edition-versioned prelude resolver
Что
[package].edition = "<X.Y>" в nova.toml — pin prelude content на
конкретный snapshot. Resolver выбирает std/prelude/<sanitized(<X.Y>)>.nv
вместо rolling std/prelude.nv facade.
Sanitization rules (manifest::sanitize_edition):
- Не-alphanumeric ASCII →
_(e.g.2026.05→2026_05). - Digit-leading prefix →
e(e.g.2026_05→e2026_05), потому что Nova-identifier должен начинаться с буквы /_. - Empty input → empty output (caller-side responsibility).
Examples:
edition = "2026.05"→std/prelude/e2026_05.nvedition = "nightly"→std/prelude/nightly.nvedition = "v1-beta"→std/prelude/v1_beta.nv
Fallback chain (resolver-side):
- Edition pin:
std/prelude/<sanitized>.nv— если файл существует, import path =["std", "prelude", "<sanitized>"]. - Rolling facade:
std/prelude.nv— backward-compat default (нет edition в манифесте, или edition pin не найден).
Soft-fail: edition specified, но файла нет → silently fall back на rolling facade (не блокируем build, user может указать pin без файла для будущего расширения).
Правило
# nova.toml
[package]
name = "myapp"
edition = "2026.05"
→ Все модули в myapp auto-импортируют std/prelude/e2026_05.nv
вместо rolling std/prelude.nv. Будущие изменения rolling facade
(новые re-export’ы, signature drift) НЕ затрагивают packages с
pinned edition — они видят фиксированный snapshot.
Зачем
- Industry-standard pinning. Rust
edition = "2021", Gogo 1.21, Swift packageswift-tools-version— stability через explicit pin. - Migration safety. Maintainer’ы prelude могут add’ить re-export’ы в rolling facade без breaking changes для users с pinned edition.
- AI-friendly. LLM-генерируемый код с stable edition → reproducible.
Что отвергнуто
- Universal pin через one global rolling. Без edition future изменения prelude (например new re-export shadowing user-type) ломают существующие packages. Edition pin даёт opt-out из rolling.
- Multi-edition support в одном workspace. Каждый package имеет одну edition; transitive deps могут иметь свои edition’ы независимо.
- Auto-migrate workflow. Edition bump — explicit decision package
owner’а (как Rust
cargo fix --edition). Tooling может предложить, но не auto-apply.
Связь
- D26 — stdlib и prelude — base prelude content.
- D78 — package tooling —
nova.tomlschema. - Plan 62.F.bis Ф.1 — implementation.
D125. Prelude shadow warning lint
Что
W_PRELUDE_SHADOW — structured lint warning эмитимый когда
user-declaration top-level имени shadow’ит prelude-imported name
(D26, D29). User-declaration wins (silent shadow), warning сигнализирует
о потенциальной AI/training confusion.
Эмиттер: lints::lint_prelude_shadow (lints.rs::lint_module
включает его в общий проход). LintWarning имеет:
rule = "W_PRELUDE_SHADOW"(grep’абельно из CLI и дляEXPECT_COMPILE_WARNINGmatching вnova test).diag.messageначинается с[W_PRELUDE_SHADOW]tag (для rendering черезdiag.render—ruleполе не leak’ит в текст автоматически).- Actionable hint:
qualify as std.prelude.<sub>.<name>илиadd allow_prelude_shadow / no_prelude / partial_prelude(...).
Visibility detection: lints::collect_prelude_visibility — shared
helper между types::check_module (silent classify duplicates как
W_PRELUDE_SHADOW vs codegen-only merge) и lint_prelude_shadow
(structured warning emission). 2-pass:
- Names declared directly в
std/prelude/*.nvpeer files (включаяstd/prelude.nvfacade себя). - Names re-exported через
export import X.{A, B as C}selective list.
Suppress mechanisms:
- Module-level clause
module X allow_prelude_shadow— silences ALL W_PRELUDE_SHADOW warnings в модуле. См. 07-modules.md → Allow prelude shadow. - Prelude self-modules (
std.prelude.*,<pkg>.prelude.*) — automatically skipped (они LEGITIMATELY declare prelude names). - Item-level suppress (
#[allow(prelude_shadow)] type Foo) — DEFERRED (требует generic attribute parser; пока не приоритет).
Правило
module myapp.dsl
// Conflict: PRELUDE_VERSION auto-imported via std/prelude.nv;
// user-decl wins (codegen skips merged duplicate via Const-skip path),
// W_PRELUDE_SHADOW emitted.
const PRELUDE_VERSION int = 42 // → warning
module myapp.dsl allow_prelude_shadow
// Same conflict, suppress'нут (warning не эмитится).
const PRELUDE_VERSION int = 42 // → silent
Зачем
- AI/training clarity. LLM-generated code часто случайно shadow’ит
prelude names (e.g. local
type Result { ... }). Warning catches it early; explicit suppress сигнализирует intentional override. - Migration safety. Если будущий prelude bump добавит новое имя
(e.g.
From/Intoв Plan 62.E), existing user-decl с тем же именем получит warning — обнаружение early-stage. - Не error. Sometimes shadowing намеренно (DSL слой, embedded); warning + suppress даёт user-выбор vs hard block.
Что отвергнуто
- Hard error. Per D5 / D26: user wins на conflict — shadowing допустим как backward-compat механизм. Error блокировал бы legitimate DSL use-cases.
- Codegen-only merge как warning. Когда prelude impl-merge подтягивает
type не visible в user код (e.g. internal struct prelude’а), и user
re-declares то же имя — это НЕ shadow, потому что user не “видел”
prelude name. Lint фильтрует через
prelude_visible_namesvsmerged_from_imports_names. - Per-name allowlist.
allow_prelude_shadow = ["Option"]— слишком fine-grained, добавляет complexity без явного use-case. Module-level bool clause достаточен.
Связь
- D26 — stdlib и prelude — prelude scope rules.
- D29 — модули и импорты — name resolution.
- 07-modules.md → Allow prelude shadow — clause syntax.
- Plan 62.F.bis Ф.2 — implementation.
D141. Примитивы доступа к памяти — byte_at / bulk slice-операции
Plan 90. Принято 2026-05-22. Plan 90.1 amend. 2026-05-27 — extend-family API +
copy_fromhardening.
Что
Минимальный набор безопасных примитивов доступа к памяти, чтобы
алгоритмы рантайма и stdlib (str-методы, буферы, парсеры) выражались на
Nova без лишних аллокаций и без ухода в external fn. Сырые указатели и
unsafe-режим не вводятся — Nova остаётся языком без указателей
(D6).
Правило
str.byte_at — O(1) доступ к байту строки:
fn str @byte_at(i int) -> u8
Byte-indexed (не codepoint). Выход за границы (i < 0 || i >= byte_len)
— panic (D13).
Неустранимый примитив для data-dependent байтовых алгоритмов (лексер,
find, trim).
Bulk slice-операции []T:
fn []T mut @copy_from(src []T) // memmove (overlap-safe)
fn []T mut @copy_within(src_from int, dst_from int, len int) // memmove (overlap-safe)
fn []T mut @fill(v T) // заполнение
copy_from— строгое копирование:src.len != dst.len→panic«length mismatch». Всегда memmove (overlap-safe, паритет Go; см. «Overlap safety» ниже). Truncation use-case — через slicing:dst[..n].copy_from(src[..n])(D144). Breaking change (Plan 90.1): прежняя молчаливая truncation (srcкорочеdst→ хвост не тронут) заменена на panic. Migration:dst[..n].copy_from(src[..n]).copy_within— копирование внутри одного среза, корректно при перекрытии диапазонов (семантикаmemmove); диапазон вне границ →panic.fill— записываетvво все элементы.- Определены для любого
T(копирование element-storage корректно при non-moving GC, D6).
Extend-family API (Plan 90.1)
fn []T mut @extend_from(src []T) // append с ростом
fn []T mut @insert_from(i int, src []T) // вставка пачкой по позиции
fn []T mut @reserve(extra int) // preallocate hint
extend_from(src) — append элементов src в конец dst, с ростом:
- Рост: если
dst.len + src.len > dst.cap→ new_cap = max(2 × dst.cap, needed). Паритетpush(2x doubling, [D27]). - memmove: safe для self-extend (
dst.extend_from(dst)) —src.lenснапшотится до realloc; после realloc memmove работает со старым буфером (Boehm GC удерживает до сборки). Test:extend_from_self.nv. - View detach: при realloc существующие slice-view’ы от
dstстановятся dangling. LintW_VIEW_EXTEND_DETACHпредупреждает; suppress через#allow(view_extend_detach).
insert_from(i, src) — вставка src в позицию i (элемент, не байт):
- Диапазон
i:[0, dst.len]— включаяdst.len(append-at-end ≡extend_from).i < 0 || i > dst.len→ panic. - Рост: та же стратегия, что
extend_from. - In-place path (без realloc): memmove хвоста
[i, len)вправо наsrc.lenслотов; затем memmovesrcв образовавшуюся дыру (обрабатывает overlap). - Alloc path: prefix
[0, i)+ дыра + tail[i, len)— три memcpy без overlap.
reserve(extra) — hint на preallocate extra дополнительных слотов:
extra < 0→ panic.extra == 0→ no-op.dst.len + extra ≤ dst.cap→ no-op O(1). Иначе рост ≥dst.len + extra.dst.lenне изменяется.- View detach: при realloc — тот же lint.
Truncation idiom
// Новая строгая семантика copy_from:
dst.copy_from(src) // panic если src.len != dst.len
// Idiom для частичного копирования (была старая silent-truncation):
dst[..n].copy_from(src[..n]) // explicit prefix slice — Plan 96 D144
dst[..n] — slice NovaArray_T с len = cap = n (D-cap-len, D144);
copy_from на нём требует src[..n].len == n → panic-safe.
Overlap safety
Nova всегда использует memmove для array bulk-операций (не memcpy):
copy_from: memmove → safe если dst и src overlap (через view в тот же буфер).copy_within: явно memmove, документировано.extend_from/insert_from: memmove дляsrc-копирования → safe при view-аргументе.
Паритет Go (copy() + append() — memmove/safe). Отличие от Rust
copy_from_slice (UB при overlap, нет borrow-check): Nova overlap-safe
by default без lifetime annotations.
W_VIEW_EXTEND_DETACH lint (Plan 90.1)
let view = parent[1..4]
parent.extend_from([5, 6, 7]) // W_VIEW_EXTEND_DETACH: view may dangle after realloc
Lint срабатывает если в той же функции после let view = parent[a..b]
вызывается grow-метод на parent (extend_from / insert_from / reserve).
После realloc view.data указывает на стёртую память (Boehm GC удерживает
до сборки, но lifetime семантически опасен).
Suppress через #allow(view_extend_detach) перед module-декларацией.
Параллельный lint — W_VIEW_PUSH_DETACH (Plan 96.1, D144).
compare — один примитив сравнения []u8:
fn []u8 @compare(other []u8) -> int // <0 / 0 / >0, лексикографически
memcmp-класс (byte-wise, word/SIMD-скорость). Равенство — частный
случай: a == b ⇔ a.compare(b) == 0; оператор == и
lt/le/gt/ge выводятся из compare. Отдельного bytes_equal
нет. Определён только для []u8: для multi-byte T побайтовое
сравнение endianness-зависимо.
Почему
- Self-hosting и stdlib на Nova. Без примитивов доступа к памяти
str-методы и буферы вынужденно остаются C-кодом либо аллоцируют
(
slice/bytes). Примитивы переносят алгоритмы в Nova, оставляя в C лишь неустранимый минимум. - Безопасность сохранена. Все примитивы bounds-checked; нет сырых
указателей, нет
unsafe-keyword. Паритет с Go (copy()/bytes— safe, безunsafe), Rust (slice::copy_*/[u8]::cmp— safe), TS (typed arrays — указателей нет вовсе). FFI-граница закрытаexternal fn(D82) иexternal type(D126) — сырой указатель в систему типов Nova не попадает. compare— один примитив. memcmp возвращает порядок; равенство — его zero-case. Дублировать в два примитива (equal+compare) преждевременно (если профайл покажет — fast-path добавится позже, модель Gobytes.Equal).- Extend-family (Plan 90.1): паритет с Go
append(dst, src...), Rustextend_from_slice/Vec::reserve, TSpush(...arr)/splice, KotlinaddAll, JavaArrayList.addAll. Единственный grow-path до 90.1 —for x in src { dst.push(x) }(O(N) virtual calls);extend_from— bulk memmove, намного быстрее для primitive[]T. copy_fromhardening (Plan 90.1): молчаливая truncation — silent bug factory. Ни один из 5 эталонных языков не имеет такой гибрид «panic на длинный + silent на короткий». Strict equal-only + memmove — лучший баланс корректности и overlap-safety.
Связь
- D6 — память managed, без указателей.
- D13 — panic — семантика выхода за границы.
- D27 §1659 —
[]Tpush cap-growth — та же 2x стратегия, чтоextend_from/insert_from/reserve. - D82 —
external fn, D126 —external type: FFI-граница без сырых указателей. - D117 — size-accessors
[]T/str— соседняя группа методов built-in-типов. - D144 — slices
arr[a..b]— truncation idiom черезdst[..n].copy_from(...). - Plan 90 — baseline реализация.
- Plan 90.1 — extend-family + copy_from hardening.
- Plan 96.1 — W_VIEW_PUSH_DETACH (параллельный lint).
- Ориентиры: Go
copy()/bytes/append, Rustslice::copy_*/extend_from_slice/Vec::reserve, TS typed arrays/splice, KotlincopyInto/addAll, Javaarraycopy/ArrayList.addAll.
D173. std/net — Async TCP/UDP socket stdlib via libuv
Status: ✅ implemented (Plan 83.12, 2026-05-27). Merge
05f7e77592c.
Что
Nova предоставляет async-transparent сетевой stdlib std/net/ на базе
libuv (uv_tcp_t, uv_udp_t). Все операции блокируют fiber (не OS thread)
через park/wake D93, выглядят синхронно в коде пользователя.
Модуль состоит из четырёх файлов:
| Файл | Содержимое |
|---|---|
std/net/addr.nv | IpAddr, SocketAddr |
std/net/error.nv | NetError — типизированные сетевые ошибки |
std/net/tcp.nv | TcpListener, TcpStream |
std/net/udp.nv | UdpSocket |
Правила
1. Типы адресов
type IpAddr = | V4(u8, u8, u8, u8) | V6(str)
namespace SocketAddr {
fn new(ip IpAddr, port u16) -> SocketAddr
fn loopback(port u16) -> SocketAddr // 127.0.0.1:port
fn any(port u16) -> SocketAddr // 0.0.0.0:port
fn parse(s str) -> Result[SocketAddr, NetError]
}
str.from(SocketAddr) возвращает "ip:port" (human-readable).
2. TcpListener
namespace TcpListener {
fn bind(addr SocketAddr) -> Result[TcpListener, NetError]
}
type TcpListener {
fn accept(self) -> Result[TcpStream, NetError] // parks fiber until connection
fn local_port(self) -> u16
fn close(self)
}
bind(addr) — OS TCP bind + listen. local_port() корректен после bind
(для port=0 возвращает OS-assigned port).
accept() использует nova_sched_park_until(pred: pending_conns > 0) —
spurious wake безопасен, re-checks predicate (см. D93).
3. TcpStream — lifecycle state machine
IDLE ──connect──▶ CONNECTING ──cb──▶ CONNECTED
│
close()
▼
CLOSING ──close_cb──▶ CLOSED
Состояния: IDLE=0 / CONNECTING=1 / CONNECTED=2 / CLOSING=3 / CLOSED=4.
CAS-переходы атомарны. write() и read_bytes() проверяют stage ≥ CLOSING
перед операцией → возвращают Err("stream closing").
namespace TcpStream {
fn connect(addr SocketAddr) -> Result[TcpStream, NetError] // parks fiber
}
type TcpStream {
fn write(self, data str) -> Result[(), NetError]
fn read_bytes(self, max_len int) -> Result[str, NetError]
fn local_addr(self) -> SocketAddr
fn remote_addr(self) -> SocketAddr
fn close(self)
}
EOF semantics: uv_read_cb с nread == UV_EOF → read_bytes() возвращает
Ok("") (пустая строка). Чистое закрытие соединения = success, не error.
4. UdpSocket
namespace UdpSocket {
fn bind(addr SocketAddr) -> Result[UdpSocket, NetError]
}
type UdpSocket {
fn send_to(self, data str, addr SocketAddr) -> Result[(), NetError]
fn recv_from(self, max_len int) -> Result[(str, SocketAddr), NetError]
fn local_port(self) -> u16
fn close(self)
}
recv_from — parks fiber до получения датаграммы; возвращает (data, sender_addr).
5. NetError
type NetError =
| ConnectionRefused
| ConnectionReset
| TimedOut
| AddrInUse
| AddrNotAvailable
| Other(str)
Все Result[T, NetError] возвращаемые типы используют typed errors —
match-exhaustive на стороне пользователя.
6. Thread-affinity invariant
libuv handles (uv_tcp_t, uv_udp_t) должны закрываться на том же OS thread,
на котором они созданы. В M:N режиме fiber может мигрировать между workers.
Решение: nova_loop_defer_close(handle) — enqueue request в
NovaDeferredCloseQueue текущего loop; worker деqueue и вызывает uv_close
на своём thread. В AUTOARM=0 (single thread) — direct uv_close.
7. Park/wake контракт (D93-compliant)
- Caller fiber:
nova_sched_register_pending(scope, slot)→nova_sched_park(scope, slot) - libuv callback (
_tcp_connect_cb,_tcp_read_cb,_tcp_write_cb, …): устанавливает result поля →nova_sched_wake(scope, slot) - Fiber resume: читает result, возвращает
Ok(...)илиErr(...)
Stop callback для cancel: uv_read_stop + deferred uv_close → close_cb → wake.
Почему
- Fiber-transparent async — пользователь пишет последовательный код (как Go),
без
async/awaitключевых слов (в отличие от Rust/Tokio). - libuv — уже в runtime (Plan 22), cross-platform (Linux/Windows/macOS), production-grade event loop.
- D93 park/wake — единый контракт для всех блокирующих операций (Time.sleep, Channel, net). Не дублируется логика.
- Typed errors —
NetErrorsum type vs stringly-typed (Goerr.Error()) позволяет exhaustive match.
Связь
- D93 — park/wake contract — основа impl.
- D91 — Channel — аналогичный park/wake pattern.
- Plan 83.12 — реализация.
- Plan 83.3 — Blocking effect для DNS/sync IO.
- Plan 91 — std/net co-planned в 0.1.
- Ориентиры: Go
net.Listen/net.Dial, Rusttokio::net::TcpListener.
D177. str Nova-body dispatch — Plan 54 Ф.2 extension
Plan 91 Ф.2.5 — 2026-05-28
Что
Пять методов str (parse_int_radix, pad_left, pad_right, repeat,
replace) реализованы как Nova-body методы в std/runtime/string.nv и
диспатчатся через механизм Plan 54 Ф.2 (Nova method dispatch) вместо
C bootstrap shim’ов. Auto-available через std.prelude re-export — явный
import std.runtime.string.{pad_right} не требуется.
Правило
1. Nova-body декларации (std/runtime/string.nv)
// Parse int с указанной base (2..36). None при ошибке.
export fn str @parse_int_radix(radix int) -> Option[int] { ... }
// Pad до width codepoints слева символом fill.
export fn str @pad_left(width int, fill char) -> str { ... }
// Pad до width codepoints справа символом fill.
export fn str @pad_right(width int, fill char) -> str { ... }
// Повторить строку n раз (n ≤ 0 → "").
export fn str @repeat(n int) -> str { ... }
// Заменить все вхождения from на to.
export fn str @replace(from str, to str) -> str { ... }
Модуль std/runtime/string.nv использует #no_prelude для разрыва
циклического импорта prelude → string → prelude.
2. Prelude auto-availability
// std/prelude.nv
export import std.runtime.string.{parse_int_radix, pad_left, pad_right, repeat, replace}
Все пять методов доступны в любом пользовательском модуле без явного
import — аналогично остальным prelude items (D26).
3. Dispatch mechanism (Plan 54 Ф.2)
Codegen диспатчит obj.method(...) для obj: str через Plan 54 Ф.2:
obj_ty = "nova_str"→prim_nova_name = "str"- Look up
method_overloads[("str", method)] - Фильтр
!is_external— Nova-body методы получаютis_external = false - Генерируется вызов
Nova_str_method_<name>(obj, args...)
External fn методы (@len, @eq, @split, …) имеют is_external = true
и не перехватываются Plan 54 Ф.2 — они продолжают диспатчиться через
str_method_to_rt → прямые C функции (без изменения поведения).
4. Generated C names
| Nova method | C function |
|---|---|
str @parse_int_radix(radix int) | Nova_str_method_parse_int_radix |
str @pad_left(width int, fill char) | Nova_str_method_pad_left |
str @pad_right(width int, fill char) | Nova_str_method_pad_right |
str @repeat(n int) | Nova_str_method_repeat |
str @replace(from str, to str) | Nova_str_method_replace |
Функции генерируются при каждой компиляции — встроены в выходной .c файл
как static функции (аналогично всем Nova-body методам).
5. Removed C shims
nova_str_parse_int_radix удалён из nova_rt/array.h.
nova_str_pad_left, nova_str_pad_right, nova_str_repeat, nova_str_replace
оставлены в nova_rt/string_builder.h для внешних потребителей,
но codegen их больше не вызывает.
6. consume-method alias (nova_rt/string_builder.h)
Nova-body методы pad_left, pad_right, repeat вызывают
StringBuilder.into() — consume-метод (export external fn StringBuilder consume @into()).
Codegen генерирует Nova_StringBuilder_consume_into(sb) (D164 ABI, Plan 100.6).
Добавлен inline alias в string_builder.h:
static inline nova_str Nova_StringBuilder_consume_into(Nova_StringBuilder* b) {
return Nova_StringBuilder_method_into(b);
}
Почему
- Единый механизм — аналогично
fn int @seconds() -> Duration(Plan 91 Ф.1) Nova-body методы на примитивных типах позволяют писать стандартную библиотеку на Nova, а не на C. - Cycle-safe —
#no_preludeвstd/runtime/string.nv+ explicit importsstd.prelude.core.{Option, None, Some}иstd.prelude.collections.{StringBuilder}разрывают циклprelude → string → prelude. - Single source of truth — логика
replace(concat-loop вместо[]str.join) написана один раз на Nova; C bootstrap shim’ы удалены. - Backward compatible — external fn методы (
@len,@eq,@split, …) продолжают использоватьstr_method_to_rtбез изменений. Фильтр!is_externalв Plan 54 Ф.2 гарантирует, что только Nova-body методы перехватываются.
Связь
- D26 — prelude auto-availability.
- D82 — external fn декларации (str external методы).
- D176 —
str.as_bytes() -> readonly []u8используется вparse_int_radixbody. - Plan 91.4 — sub-plan Ф.2.5 D177.
- Plan 54 — Ф.2 dispatch mechanism.
D178. str API cleanup и расширения — Plan 91 Ф.2.6
Что
Комплекс из шести взаимосвязанных изменений str API, закрывающих Plan 91
Ф.2.6:
@bytes()→@to_bytes()— allocating copy;@as_bytes()(D176, zero-copyreadonly []u8) остаётся без изменений.@chars()→@to_chars()— allocating codepoint slice.@split(sep str) -> []str→-> readonly []str— возвращает zero-copy views в оригинальный буфер; тип сигнализирует об этом.@parse_int_radix(radix int)+@parse_int()→@parse_int(radix int = 10)— одна Nova-body функция с keyword-only default-параметром (D102). Вызов без аргументов:"42".parse_int()(radix=10). С явным radix:"ff".parse_int(radix: 16). Позиционная передача default-параметра запрещена D102.@compare(other str) -> int— новый C-примитив; возвращает отрицательное/ноль/положительное, как Cstrcmp. Реализован какnova_str_compareчерез__builtin_memcmp.readonly bytesparameter syntax — параметрfrom_bytes_lossyиfrom_bytes_uncheckedпереписан в формуreadonly bytes []u8(modifier перед именем параметра, а не перед типом). Оба варианта теперь поддерживаются парсером.
Правило
// D178 итоговый str API (bootstrap):
export external fn str @to_bytes() -> []u8 // allocating copy
export external fn str @as_bytes() -> readonly []u8 // D176: zero-copy
export external fn str @to_chars() -> []char // allocating codepoints
export external fn str @split(sep str) -> readonly []str
export external fn str @compare(other str) -> int // <0 / 0 / >0
// from_bytes: `readonly` перед именем параметра (новая форма, D178)
export external fn str.from_bytes_lossy(readonly bytes []u8) -> str
export external fn str.from_bytes_unchecked(readonly bytes []u8) -> str
// parse_int: единственный метод с keyword-only default (D102)
export fn str @parse_int(radix int = 10) -> Option[int] {
if radix < 2 || radix > 36 { return None }
// ... тело на Nova (Plan 54 Ф.2)
}
Prelude auto-import (std.prelude v11):
export import std.runtime.string.{
parse_int, pad_left, pad_right, repeat, replace,
compare, to_bytes, to_chars, as_bytes
}
Эквивалентность типов readonly []u8:
readonly []u8 ≡ readonly [] readonly u8
Оба варианта стриппируют recursive readonly до NovaArray_nova_byte* в
C codegen. Различие семантическое — первый «readonly array of u8», второй
«readonly array of readonly u8» — но в bootstrap-реализации оба ведут
себя идентично (нет изменяющих операций на байтах).
Default-параметры и keyword-only вызов (D102):
Параметр с дефолтным значением — всегда keyword-only (Nova D102). Попытка
передать позиционно вызывает ошибку компилятора. Для parse_int:
"ff".parse_int() // ✓ radix=10 (default)
"ff".parse_int(radix: 16) // ✓ явно radix=16
"ff".parse_int(16) // ✗ CODEGEN-FAIL: D102 keyword-only
Codegen: default-arg fill-in для Nova-body dispatch (Plan 54 Ф.2):
Когда вызов str.method(fewer_args_than_params) проходит через Plan 54
Ф.2 dispatch (method_overloads[("str", m)], !is_external filter),
codegen заполняет пропущенные trailing аргументы из MethodSig.param_defaults.
Поле param_defaults: Vec<Option<String>> добавлено в MethodSig; при
регистрации методов из FnDecl — populate через simple_literal_c (конвертирует
литеральные default-expressions в C-строку без вызова emit_expr).
Почему
- Консистентность
to_*prefix —to_bytes/to_charsсемантически аналогичны Rustto_vec()/to_string(): allocating copy. Безto_-prefix неясно, zero-copy или нет.as_bytes()остаётся как zero-copy аналог Rustas_bytes(). readonly []strизsplit— zero-copy views в оригинальный буфер; тип это выражает явно. Изменять элементы результата нельзя.- Единый
parse_int— вместо двух методов (parse_int()иparse_int_radix(r)) один с default-параметром. Упрощает API; radix=10 — наиболее частый случай. compareкак примитив — лексикографическое сравнение черезmemcmp; будущийPartialOrdauto-derive дляstrможет опираться на него.
C codegen mapping
| Nova method | C function |
|---|---|
str @to_bytes() | nova_str_to_bytes |
str @to_chars() | nova_str_to_chars |
str @compare(other) | nova_str_compare |
str @split(sep) | nova_str_split (unchanged) |
str @as_bytes() | nova_str_as_bytes (D176) |
Legacy C aliases сохранены для совместимости кода, написанного до D178:
nova_str_bytes → nova_str_to_bytes, nova_str_chars → nova_str_to_chars.
Связь
- D102 — keyword-only default params.
- D176 —
readonlytype modifier;as_bytes(). - D177 — Nova-body dispatch механизм.
- Plan 91.5 — sub-plan Ф.2.6 D178.
D179. StringBuilder — pure Nova consume type — Plan 91 Ф.2.6
Статус: закрыт (Plan 91 Ф.2.6 sub-phase, 2026-05-28).
Суть
StringBuilder перенесён из внешней реализации (C runtime / Rust String) в
чистый Nova-тип:
type StringBuilder consume {
mut buf []u8
}
Все методы реализованы на Nova; единственный внешний примитив —
buf.push(byte u8) (добавление байта в backing array), UTF-8 encoding
реализован через Nova bitwise ops.
API (финал D179)
// Конструкторы
StringBuilder.new() -> Self // pre-alloc 16 байт
StringBuilder.with_capacity(n) -> Self // pre-alloc n байт
StringBuilder.from(s str) -> Self // copy UTF-8 bytes
StringBuilder.from(c char) -> Self // UTF-8 encode одного codepoint
// Query
@len() -> int // байты O(1); аналог str.len (D26 school B)
@char_len() -> int // codepoints O(n) UTF-8 walk; новый метод
@capacity() -> int // allocated байты
@is_empty() -> bool
@clone() -> Self // deep copy buffer
// Prefix/suffix check
@starts_with(prefix str) -> bool
@ends_with(suffix str) -> bool
// Мутирующие (-> @, consume-тип — см. D131)
@append(s str) -> @ // append UTF-8 bytes из str
@append(c char) -> @ // append codepoint как UTF-8 (1-4 байта)
@append_bytes(readonly arr []u8) -> @ // raw bytes; caller обеспечивает UTF-8
@append_repeat(s str, n int) -> @ // append s ровно n раз
@truncate(len int) -> @ // обрезать буфер до len байт
// Операторы
@plus(s str) -> @ // sb + "text" → @append(s) (D46)
@plus(c char) -> @ // sb + c → @append(c) (D46)
// Consume (финализация)
@to_str() -> str // consume StringBuilder → str; infallible (UTF-8 invariant)
Изменения относительно pre-109
| Было (до D179) | Стало (D179) |
|---|---|
external type StringBuilder | type StringBuilder consume { mut buf []u8 } |
@byte_len() -> int | удалён (дублировал @len()) |
@peek() -> str | удалён (unsound: pointer aliasing с realloc) |
@into() -> str | @to_str() -> str (consume) |
@append_bytes(arr []u8) | @append_bytes(readonly arr []u8) |
| внешняя реализация C/Rust | чистый Nova-код |
Инфраструктура
std/runtime/string_builder.nv— Nova-реализация всех методов.compiler-codegen/nova_rt/string_builder.h— только UTF-8 helpers:nova_str_from_bytes_unchecked,nova_str_from_bytes_lossy,Nova_str_static_try_from_bytes,Nova_str_static_from_char,nova_str_replace. СтарыеNova_StringBuilder_*функции удалены.std/prelude/collections.nv—export import std.runtime.string_builder.{StringBuilder}(былоexternal type StringBuilder).compiler-codegen/src/codegen/runtime_registry.rs—RUNTIME_DEFINED_TYPESincludes"StringBuilder".emit_c.rs—lhs_is_nova_ptrguard:sb + "str"→@plusdispatch, неnova_str_concat.