Nova — открытые вопросы дизайна

Что обсуждали, но не зафиксировали как решение. Когда вернёшься к работе над языком — сначала закрой эти вопросы.


Q1. Унификация методов типов и эффектов

Контекст. Сейчас (после D35):

fn User @greet() -> str => ...       // метод инстанса: неявный self через @
fn list_users() Db -> []User => ...  // функция с эффектом: handler в скоупе

D35 ввёл @-методы (неявный self), но только для типов данных. Для эффектов остался отдельный синтаксис «обычная функция + эффект в сигнатуре». Это два разных способа объявить «функцию, ассоциированную с типом/эффектом».

Предложение. Распространить @ на эффекты:

fn User @greet() -> str => ...
fn Db @list_users() -> []User => ...   // self = активный handler в скоупе

Один синтаксис, два способа доступа к self:

  • Для data-типа — экземпляр через u.greet()
  • Для эффект-роли — активный handler через Db.list_users() (или db.list_users() если есть локальное имя)

Эффект в сигнатуре автоматически — через @ (не нужно дублировать).

Статус. D35 закрыл часть про data-типы. На эффекты не распространён — вопрос остаётся открытым.

Тонкие места:

  1. Неявный self для эффектов — это «магия» или унификация?
  2. Куда отнести функции с несколькими эффектами (Net Db Log Fail)? fn (Net, Db, Log) @method — некрасиво, и self не один.
  3. Переписывание stdlib — но stdlib ещё нет, дешёвое время.

Q2. Tuple-namespace (Db, Log).method

Контекст. Был вопрос: «можно ли (Db, Log).list_users() как сокращённый with

Решение в обсуждении. Отвергнуто (не даёт ничего сверх with-блока, дублирует информацию из сигнатуры).

Не зафиксировано в decisions/ явно. Если возникнет снова — сослаться на это обсуждение.


Q3. Реализация fiber stack для Async

Bootstrap-status (2026-05-06): временное решение — minicoro (mco) с fixed-size growable stacks. Размер по умолчанию — то, что minicoro даёт ас default’ом. Этого достаточно для bootstrap- сценариев (тесты до 64 fiber’ов в NOVA_SCOPE_CAP). Production-выбор остаётся открытым — список ниже.

D14 говорит «открытый вопрос»:

  • Segmented stacks (как старый Go)?
  • Cactus stacks (как Cilk)?
  • On-demand growable (как новый Go)?

Каждый имеет цену:

  • Segmented: дешёвый старт, hot-spot на границе сегмента
  • Cactus: хороший для work-stealing, сложнее реализация
  • Growable: выделяет много vsmem заранее, копирование при росте

Дефолтный размер fiber stack тоже не определён. Erlang начинает с 233 слов (~2KB), Go — с 8KB. Для Nova нужно решить, ориентируясь на основной use-case (серверы? embedded? AI-генерация?).


Q4. Семантика Alloc[Cycle] collector’а — ✅ ЗАКРЫТО

Закрыто D6. D21 (~T/~&T opt-in cycle collection) отменён в пользу tracing GC по умолчанию. Эффект Alloc[Cycle] снят, префиксы ~T/~&T удалены из языка. Управление collector’ом — runtime-параметр, не часть языка.


Q5. Точная граница Panic (частично закрыто)

D13 определил Panic как «аппаратные/математические сбои»:

  • Деление на ноль ✓
  • Целочисленное переполнение (int: panic в debug И release — Plan 33.8 D13, не wrapping; доказуемо-безопасные чеки элидируются Z3 — Plan 140.4 D272; sized-типы u8/…/i32: wrap по модулю 2^N — Plan 33.7) ✓ закрыто
  • Выход за границы массива ✓
  • Переполнение стека — открыт (Q5.2 ниже)
  • OOM ✓ закрыто D13 (Panic, fiber умирает; supervisor может рестартовать)

Остаются открытыми:

Q5.2. Stack overflow recoverable? Может ли быть не-panic, обрабатываемая ошибка? Или всегда смерть fiber’а? Erlang restart, Java StackOverflowError. Nova должна заявить позицию — скорее всего «fiber умирает, supervisor рестартует» по аналогии с OOM, но не зафиксировано явно.

Q5.4. Assertion failures в debug. Это Panic или обычные Fail? Если Panic — нельзя поймать в обычном коде (только supervisor видит). Если Fail[AssertionError] — нужно везде декларировать. Скорее всего Panic (это «сбой инварианта», не бизнес-ошибка), но не зафиксировано.


Q6. Effect polymorphism — синтаксис + rank-2 семантика

Текущий пример:

fn map_eff[T, U, E](xs [T], f (T) E -> U) E -> [U]

E — параметр-эффект. Не определён точный синтаксис:

  • Можно ли E1, E2? [E1, E2]?
  • Как ограничивать (bound) эффект-параметры?
  • Как стирать (erase) для разнородных задач — D12 коснулся, но не всё детализировано.

Rank-2 polymorphism в handler-method’ах

D42 модель B (через D53) допускает generic в методах protocol/effect:

type Db effect {
    in_transaction[T](body fn() Db Fail -> T) Fail -> T
}

Здесь T — generic метода (rank-2): один и тот же handler Db вызывает in_transaction с разными T для каждого вызова. После D61 (tail-only семантика, без resume) не специфицировано, как handler управляет произвольным T:

  • body возвращает T — handler-method получает T через вызов body(). Может ли handler решить «вернуть T или другое»?
  • Если handler делает interrupt v — типы v и T должны быть совместимы. Как это проверяется при rank-2?
  • SMT-проверка контрактов на body() — возможна ли она на rank-2-T?

Использовано в orm_demo.nv и stdlib_sql.nv — компилятор должен поддержать раньше чем self-hosted production.

Связь: D42 (REVISED), D53, D61, D12, Q-bounds.


Q7. Macros / comptime

В overview.md сказано: «typed compile-time функции (как Zig comptime)». Но конкретный синтаксис, мощь, ограничения — не описаны.

Большой частичный ответ (Plan 114.4.2 V1 + Plan 114.4.3 V2, 2026-06-01): comptime-функции через const fn (D199) — V2 production-grade subset.

V1 baseline (Plan 114.4.2):

  • fn calc(const a int, ...) -> const int { ... } — all const params + const return.
  • Whitelist: literals, arithmetic, as-casts, refs к const params/locals, local const decls, final expr, calls к другим const fn.
  • Blacklist: control flow, mut/consume, effects, allocations, generic, recursion.
  • Comptime evaluator (env-based interp + memoization) inline’ит result литералом на call site, const fn drop’нут из codegen.

V2 extensions (Plan 114.4.3):

  • Control flow: if/else/match allowed; loops → V2.1 followup.
  • Recursion: direct + mutual с depth-limit (256) + memoization. Fibonacci O(n).
  • Mixed-args: any combination const/runtime params + return. Mixed fn stays в codegen с const-param validation at call sites. Covers [M-114.4.2-runtime-return] (subset).
  • Generic const fn: fn[T] foo(const a int) -> const int T-independent body. T reflection (size_of[T]) → V3.
  • First-class alias: const ALIAS = const_fn allowed; calls через alias map. Real first-class через runtime trampoline → V3.

V3 extensions (Plan 114.4.4, 2026-06-01):

  • #fn_eval_max_depth(N) attribute (configurable recursion depth).
  • ✅ Friendly UX errors (runtime-let + HOF detection).
  • ✅ Loops в body (for/while/loop + mut + break/continue).

V4 extensions (Plan 114.4.4 finish, 2026-06-02):

  • ✅ Record/sum/tuple patterns в match (closes [M-114.4.3-pattern-record-sum]).
  • ✅ Type reflection size_of[T]/align_of[T] для primitives (closes [M-114.4.3-t-reflection] для primitive surface).

V4.1 extensions (Plan 114.4.4 V4.1 session, 2026-06-02):

  • ✅ Per-const-arg mono-specialization для mixed const fns (closes [M-114.4.3-mono-specialization]).

V4.2 extensions (Plan 114.4.4.3 V4.2 session, 2026-06-02):

  • ✅ Runtime trampoline для first-class const fn use (closes [M-114.4.3-runtime-hof]). apply(double, 5) / ro f = double теперь работают через автоматическую генерацию <f>__trampoline.

V4.3 extensions (Plan 114.4.4.4 V4.3 session, 2026-06-02):

  • ✅ Closure-returning const fn — comptime closure specialization (closes [M-114.4.3-closure-from-const-fn]). fn make_adder(const n) -> const fn(int) -> int => |x| x + n теперь работает: каждый вызов make_adder(LITERAL) генерирует specialized fn <host>__closure_<idx> с substituted const param + closure body, replaced в call site как fn pointer.

V4.4 extensions (Plan 114.4.4 V4.4 followups session, 2026-06-02):

  • ✅ Ф.1: size_of/align_of для composite types — tuples / FixedArray / Array / Unit / Readonly. Closes [M-114.4.4-trampoline-record-reflection].
  • ✅ Ф.2: Closure-returning const fn captures outer const locals (Block body с Stmt::Const + trailing closure). Closes [M-114.4.4-closure-captures-outer]. Parser note: ClosureLight |x| после const X = ... парсится как binary OR — workaround = explicit ClosureFull fn(x T) -> R => body syntax.
  • ✅ Ф.3 [M-114.4.4-trampoline-generics] — generic const fn first-class через HOF type inference (V4.5 Ф.3 session, 2026-06-02). Approach #2 chosen — recursive structural unification + in-place Ident rewrite.
  • ✅ Ф.4 [M-114.4.4-closure-generic] — generic closure-returning const fn через explicit TurboFish (V4.5 Ф.4 session, 2026-06-02). Reuses subst_type_ref helper.

🎯 Plan 114.4.4 family fully complete — все 11 [M-114.4.4-*] markers closed, A1-A40 acceptance criteria, 65/65 fixtures PASS на release nova-cli.

V4.6 extensions (Plan 114.4.4 V4.6 final-followups session, 2026-06-03):

  • ✅ M1 [M-114.4.4-trampoline-named-types] — size_of/align_of для user records + sums через TypeDecl registry (thread-local guard).
  • ✅ M2 [M-114.4.4-closure-light-after-const-stmt-parser] — parser lookahead disambiguation. ClosureLight |x| после Stmt::Const works correctly now.
  • ✅ M3 [M-114.4.4-closure-generic-hof-inference] — TurboFish optional для generic closure-returning fns в HOF context (pre-pass adds TurboFish via inference).
  • ✅ M4 [M-114.4.4-trampoline-complex-concrete] — composite TypeRef concrete types (Tuple/Array/FixedArray/Named-with-generics/Unit/Readonly/Func) via mangle_type_ref stable serialization.

🎯🎯 Plan 114.4.4 family ПОЛНОСТЬЮ ЗАКРЫТА V3-V4.6 — все 15 markers закрыты, A1-A44 acceptance criteria, 70/70 fixtures PASS на release nova-cli.

Что остаётся открытым (Q7 V4.3+):

  • Comptime-функции имеют доступ к типам как первый класс? → V4.0 size_of/align для primitives ✅; records/generics → Plan 114.4.4.2.
  • Можно ли генерировать код во время компиляции? → Нет (без macro-system; future Q-блок).
  • Reflection во время компиляции — да или нет? → V4 primitives size_of/align_of ✅; records/generics → V4.1.
  • Custom DSL через comptime — допускается? → Нет (out-of-scope).
  • ✅ Loops (for/while) в body — закрыто Plan 114.4.4 Ф.3 (2026-06-01).
  • ✅ Runtime first-class const fn (HOF) — закрыто Plan 114.4.4.3 V4.2 (2026-06-02) автоматическим trampoline’ингом.
  • ✅ Closure-returning const fn — закрыто Plan 114.4.4.4 V4.3 (2026-06-02) comptime closure specialization.
  • Record/sum patterns в match — V4 followup Plan 114.4.4.1.
  • True monomorphization (per-const-arg specialization) — V4 followup Plan 114.4.4.5.
  • Generic const fn first-class use — V4.2 followup [M-114.4.4-trampoline-generics] (trampoline body нужны concrete types для intrinsic substitution).
  • Generic closure-returning const fn — V4.3 followup [M-114.4.4-closure-generic].

🎯 Plan 114.4.4 family complete — все V3-V4.3 phases landed.

См. docs/plans/114.4.2-const-fn.md (V1) и docs/plans/114.4.3-const-fn-v2-extensions.md (V2).


Q8. Обновить paradigm.md и revolutionary.md

Технический долг. Эти файлы содержат устаревший синтаксис из ранних этапов:

  • trait / impl Trait for Type — отменено в D15
  • type X = { ... } с = для record — отменено в D17
  • type X = { методы } для интерфейсов — заменено на protocol X { методы } в D42
  • effect X { ... } — отменено в D18
  • match с -> — отменено в D19
  • mut self, : в аннотациях типа, throws без Fail[] — устарели

Что делать: обновить под текущие D-решения. Это просто переписать все примеры с новым синтаксисом, без изменения смысла.

syntax.md, effects.md, decisions/01-philosophy.md (D9) актуализированы. audit.nv тоже актуален. paradigm.md помечен как устаревший до полной переписи.


Q9. Стандартная библиотека

Не описана структура. Что есть в stdlib:

  • String, HashMap, HashSet, Option, Result — очевидно (Vec нет — []T встроенный, см. D58)
  • LinkedList, Tree, Graph — какие именно типы?
  • Json, Sql.builder — упоминаются в audit.nv, не описаны
  • Time, Random, Net, Db — стандартные эффекты, не определены их операции. Частично закрыто: TimeD316-D318 (Plan 175, std/time); NetD301/D302/D407 (Plan 91.x/183, std/net); Fs/Os/Io (не входили в исходный список — добавлены тем же классом эффектов) — D322-D324 (Plan 176, std/io/std/fs/std/os). Random/Db остаются открытыми.
  • HTTP, WebSocket, gRPC — что в core, что в external? Частично закрыто: HTTP — D357-D361+ (Plan 178, std/http). WebSocket/gRPC остаются открытыми.

Конкретные пробелы методов

String API не зафиксирован. В nova_tests/ и examples/ используются s.len, s.contains(sub), s.starts_with(p), s.ends_with(p), s.to_ascii_lower(), s.to_ascii_upper(), s.trim_ascii() — все работают в compiler-codegen runtime, но это implementation-факт, не часть спеки. Production-компилятор должен ориентироваться на формальный список. (Plan 91.18: bare to_lower/to_upper/trim — Unicode под import std.unicode; _ascii_-варианты — из prelude.)

Escape sequences в string literals ("\n", "\t", "\"", "\\") — в bootstrap’е поддержаны, в спеке упоминаются только для tagged templates (D48:1510), не для обычных "..."-литералов. Нужно явно зафиксировать список поддерживаемых escapes для обычных string-литералов.

Array APIxs.len, xs.push(v), xs.pop(), xs.get(i) работают в bootstrap’е, не описаны в спеке. xs.map(f), xs.filter(p), xs.reduce(...), xs.find(p) — НЕ работают (в bootstrap’е) и НЕ описаны.

Это большая работа на отдельный документ.


Q10. Tooling детали

Заявлены в overview.md:

  • nova run, nova build, nova fmt, nova lint, nova test
  • nova check --fragment
  • nova run --record / nova replay (time-travel)

Но не описаны:

  • Формат .nrec файлов трасс
  • Структура package manager (content-addressed как Deno)
  • Hot reload — как именно работает?
  • LSP протокол — расширения для эффектов? Для контрактов?

Q11. Имя языка

«Nova» — рабочее имя. Конфликтует с:

  • Battle.net Nova (игровой движок Activision)
  • Various JS/Python/Ruby библиотеки
  • Команда Linux nova (OpenStack compute)
  • Nova Networks (компания)

Если язык будет реально публиковаться — нужно другое имя.


Q12. Модель concurrency — ЗАКРЫТО (D50)

D50 закрыл основные пункты Q12:

  • Q12.1 (spawn-семантика)spawn только в structured-scope; fire-and-forget — через detach { ... } с эффектом Detach.
  • Q12.2 (Async vs Par) → один эффект Async, Par не вводится.
  • Q12.6 (C interop)blocking { ... } примитив + эффект Blocking (несовместим с Realtime)ОБНОВЛЕНО. И blocking { ... } expression-форма, и эффект Blocking отозваны: D172 (Plan 113) сделал blocking{} hard compile-error; Plan 91.15 P0 удалил эффект Blocking из компилятора (realtime_suspend_effect + builtin-registry) и из всех сигнатур std/net. CPU-bound offload теперь покрывается blocking{}→threadpool на codegen-уровне (Plan 83.3, D50 §4) без отдельного эффекта. #blocking fn attribute сохранён (см. D50).
  • await/маркер на call site → нет, эффект Async в сигнатуре единственная декларация (подтверждение D14).

Двухэтапный план реализации (Go-style v1.0, Erlang-style v2.0+) — там же.

Остаётся открытым в Q9 (stdlib):

  • Channel[T] API — ✅ закрыто D79.
  • Mutex / RwLock / AtomicSized — ✅ закрыто D169 / D169 / D168; доступны как runtime.sync stdlib, не prelude (D167-D173).
  • Atomic[T] generic — ⏳ отложено (M2 reject в Plan 103.2; future generic-codegen plan; не входит в Plan 103.x scope).
  • Размер blocking-pool по умолчанию (runtime-конфиг).
  • Q12.7 — Domain-примитив (явная граница ОС-потока) для real-time embedded use-case, отложено до user-feedback.

Исторический контекст подвопросов сохранён ниже на случай возврата.

Q12.1. Семантика spawn

В examples/audit.nv:256 используется spawn write_audit(...) как fire-and-forget вне supervised/parallel блока. Это противоречит structured concurrency: задача переживает родителя, отмена не прорастает. Варианты:

  1. spawn всегда внутри scope’а (supervised, parallel, nursery-блок). Unstructured нет вообще.
  2. spawn структурный, detach отдельный для долгоживущих задач. detach требует явного эффекта Detach или capability.
  3. Текущее поведение audit.nvspawn fire-and-forget, scope неявно «модуль/процесс». Это удобно, но ломает structured concurrency.

Нужно решение. Влияет на cancellation, на семантику ошибок, на supervision.

Q12.2. Граница Async vs Par

Сейчас оба эффекта в стандартном наборе, но границы не описаны:

  • Async = «функция может уступить fiber-scheduler’у»?
  • Par = «функция запускает несколько fiber’ов параллельно»?
  • Должны ли они комбинироваться (Net Async Par для parallel for с сетью)?
  • parallel for требует Par или достаточно Async?
  • Может ли быть Par без Async (например, чисто CPU-bound параллелизм)?

Текущие примеры противоречат: audit.nv:222 пишет Async без Par для middleware, который spawn’ит задачу — то есть эффект параллелизма не виден в сигнатуре. Это дыра в «сигнатура = полное описание».

Q12.3. Multithreading vs concurrency

Не зафиксировано: fiber’ы работают на одном ОС-потоке, на M-потоках с work-stealing (Go/Tokio), или на отдельных Domain’ах (OCaml 5)? Это влияет на:

  • Производительность CPU-bound кода (один поток — bottleneck)
  • Сложность реализации (M:N сложнее)
  • Семантику shared state (см. Q12.4)

OCaml 5 разделяет Domain (ОС-поток с собственной кучей) и Fiber (легковесная задача внутри Domain). Это явное двухуровневое разделение. Nova могла бы:

  1. Один scheduler на процесс, M-потоков (Go-style) — простая модель, но shared state требует синхронизации.
  2. Domain как явный примитив — изоляция кучи, передача данных через каналы. Сложнее, но безопаснее.
  3. Single-threaded по умолчанию, multi-thread opt-in — упрощает ~T и ~&T, но ограничивает параллелизм.

Q12.4. Shared state между fiber’ами

Статус: ✅ partial closed (Plan 103.x, 2026-05-26).

  • Channels → ✅ D79 (capability-split, send/recv, select).
  • Actor mailbox pattern → зафиксирован как предпочтительный idiom в D79 §«Что отвергнуто» + §1531 rewrite.
  • Mutex/RwLock → ✅ D169 (Mutex/RwLock/ReentrantMutex, runtime.sync stdlib, не prelude).
  • Atomic-операции → ✅ sized types D168 (12 типов × 13 ops); Atomic[T] generic — ⏳ отложено (M2 reject).
  • STM → отвергнуто (не в scope Nova v1.x).
  • Semaphore/Barrier/CountDownLatch/Condvar → ✅ D170.
  • Once/OnceCell/Lazy → ✅ D171.
  • Memory ordering → ✅ D167 (MemOrdering enum + fence()).
  • AI-first guidance → ✅ D173 (decision tree + 5 patterns).

Остаётся: Atomic[T] generic (future plan); Domain-примитив (Q12.7, отложено).

Исторический контекст (до Plan 103.x):

Если два fiber’а должны делить состояние, рассматривались варианты: Channels (D79 ✅), Actor mailbox (D79 ✅ idiom), Mutex/RwLock (D169 ✅), Atomic[T] (D168 ✅ sized; generic ⏳), STM (отвергнуто).

Q12.5. Thread-safety ~T и ~&T — СНЯТО

Статус: снято. D21 отменён в пользу managed memory (D6 пересмотрен). Префиксов ~T/~&T нет, atomic refcount не нужен. GC сам обеспечивает thread-safe управление памятью.

Заменяющий вопрос — thread-safety GC: как concurrent collector синхронизируется с приложением? Это runtime-implementation deal, не language design. Современные решения (ZGC, Shenandoah, MMTk) уже дают ответы — выбирается на этапе реализации.

Q12.6. C interop для блокирующих вызовов

D14:665 упоминает «механизм detach to OS thread», но не описывает. Если fiber вызывает синхронный C-код (например, read(2) без O_NONBLOCK), он блокирует весь scheduler. Нужен механизм:

  • Явный detach { c_call() } блок, runtime передаёт fiber на отдельный ОС-поток?
  • Эффект Blocking в сигнатуре C-обёртки?
  • Pool блокирующих потоков для всех detach?

Что блокируется без Q12

  • Q9 (stdlib). Нельзя описать Channel, Mutex, Atomic, Mailbox без модели shared state.
  • AI-first тезис. Без Par в сигнатуре audit_middleware LLM не видит, что функция параллельна. Сигнатура не полна.
  • Реализация GC (D6). Concurrent collector работает на отдельном потоке параллельно с приложением — выбор реализации зависит от Q12.3.
  • Эффекты на границах (D12). Очереди и планировщики типизированы, но «передача задачи между потоками» — это та же граница, что «передача через процесс»? Не описано.

Приоритет

Высокий. Это структурный вопрос уровня D6/D14, не «деталь runtime». Влияет на сигнатуры stdlib, AI-first тезис и память.

Прагматичный план: Go-style v1.0, Erlang-style v2.0

После обсуждения — выбран двухэтапный план:

v1.0 — Go-style fibers + shared memory:

  • Fiber’ы как goroutine: shared heap, передача данных по указателю
  • Каналы как stdlib (Channel[T] с send/recv)
  • Mutex, RwLock, Atomic[T] как stdlib-типы (Q12.4)
  • Один scheduler на процесс, M ОС-потоков work-stealing (Q12.3.1)
  • Atomic refcount для ~T/~&T всегда (Q12.5.1) — простая модель, цена ~10ns
  • Preemptive scheduling — runtime прерывает fiber’ы по таймеру или через compiler-вставленные точки (как Go 1.14+)
  • Supervisor как библиотечный паттерн поверх panic = exit fiber (D13)

v2.0+ — Erlang-style isolation (опционально, если докажет ценность):

  • Per-fiber heap (изолированная куча, как Erlang process)
  • Per-fiber GC (микросекундные паузы локально)
  • Передача между fiber’ами только через каналы с копированием
  • Полная изоляция падений
  • Hot reload, distributed-by-default

Erlang-модель сильнее (изоляция, supervision, distributed бесплатно), но сложнее реализовать (per-process heap + per-process GC). Для v1.0 Nova не берёт эту планку — Go-модель достаточна для backend-серверов и CLI-приложений, и хорошо сочетается с D6 (managed memory с concurrent GC).

Что зафиксировано из v1.0-плана

  • Preemptive fiber-runtime — обязательно (исключает «cycle in plugin останавливает весь сервер»)
  • Shared heap с concurrent GC — единая куча на процесс, GC снимает вопрос refcount/atomicity для shared ownership (D6 пересмотрен)
  • Channels как stdlib — описание в Q9 (stdlib)
  • C interop через detach — Q12.6, фиксируется как stdlib-функция с эффектом Blocking в сигнатуре
  • Supervisor — библиотечный паттерн — поверх runtime-границы fiber’а

Что остаётся открытым

  • Точные API Channel[T], Mutex, Atomic[T] — Q9
  • Граница Async vs Par (Q12.2) — Q9 stdlib опишет
  • Семантика spawn структурный/unstructured (Q12.1) — Q9 определит
  • Q12.7 (новый): следует ли вводить Domain-примитив (явная граница ОС-потока) для real-time embedded use-case — отложено до user-feedback

Q13. Версионирование типов данных как stdlib-паттерн

Идея. Эволюция типов через sum-type вариантов + методы преобразования

  • handler Db/Fs, знающий о версиях:
type Account
    | V1 { id u64, balance money }
    | V2 { id u64, balance money, frozen bool }
    | V3 { id u64, balance money, frozen bool, currency Currency }

fn Account.to_latest(self) -> Account => match self {
    V1 { id, balance }                 => V2 { id, balance, frozen: false }.to_latest()
    V2 { id, balance, frozen }         => V3 { id, balance, frozen, currency: USD }
    v3                                 => v3
}

Handler Db при чтении применяет миграцию прозрачно, возвращая latest версию.

Почему не D-решение. Реальная сложность миграций (DDL, блокировки, конкурентные writers, big-rollback) — снаружи кода. Любой язык с sum-types даёт ту же выразительность. Ввод evolution как ключевого слова противоречил бы D18 («не плодить специальные сущности») и не давал бы ничего сверх библиотечного решения.

Когда вернуться. При работе над Q9 (stdlib) — описать как рекомендуемый паттерн для типов, читаемых из persistent storage, вместе с операциями Db handler’а.

Связь. Q9, D10, D18.


Q14. Cost-types — resource bounds в сигнатуре

Идея. Опциональный контракт о сложности:

fn sort[T](xs [T]) -> [T]
    requires bounded(time: O(n log n), space: O(1))
=> ...

Проверяется статически где можно (RAML / AARA подходы).

Почему отложить. Research-уровень. Nova и так признаёт высокую планку реализации эффектов (decisions/01-philosophy.md → D10). Брать ещё одну рискованную ставку до v1.0 — превышение допустимого риска.

Когда вернуться. После стабилизации D10/D14/D21 и реализации R5 в проде.

Связь. R4 (контракты), R5 (AI-first), D10.


Q-may-gc-precision. Точность may-GC effect analysis (residual после Plan 144.0)

Контекст. D273 (Plan 144.0) зафиксировал may-GC решётку с дефолтом MayGC (top) и доказательством NoGC только над полностью разрешённым неаллоцирующим конусом вызовов. Соундность закрыта (любое сомнение → MayGC). Остаётся точность — где консервативный анализ помечает MayGC, хотя реально NoGC, теряя элизию тира O1:

  1. Cross-module callee-резолюция. Вызов в функцию из другого модуля, чей FnDecl не в текущей вселенной графа, → unresolved → MayGC. Точнее было бы тянуть may-GC-эффект через manifest/ exports (как exports_consume_types в D164), чтобы доказуемо-NoGC peer-функции не отравляли коллера.
  2. Точность str-literal-interning. Сейчас интернированный str-литерал (static const u8[]) — в allowlist как non-allocating, но граница «интернирован vs строится буфер» (интерполяция, from-конверсии) консервативна; тонкая классификация дала бы больше NoGC-листьев.
  3. Более тонкая классификация alloc-сайтов. .clone() на provably-POD/value-record (str — value-record после Plan 139), малые ArrayLit/RecordLit, которые codegen может разместить на стеке без nova_alloc, сейчас все → MayGC. Сверка с реальными emit-путями emit_c.rs уточнила бы allowlist.

Почему отложено. Все три — улучшения точности, не соундности: каждый residual-кейс уже покрыт консервативным MayGC (теряем элизию, остаёмся корректны). Браться имеет смысл только когда Plan 144 Ф.2 начнёт потреблять набор (тир O1) и измеримая доля элизий теряется на этих паттернах — иначе оптимизируем неиспользуемый артефакт.

Когда вернуться. Вместе с Plan 144 Ф.2 (потребление O1), если профиль покажет, что cross-module / clone / interpolation-сайты доминируют в упущенной элизии.

Связь. D273, D271 (sibling [Q-loop-opt-thresholds]), Plan 144 §7.5 (тиры O0–O3) / §7.6 (H4), [M-144.0-may-gc-effect-analysis].


Q-gc-layout-precision. Точность per-type GC layout bitmaps (residual после Plan 144.1)

Контекст. D375 (Plan 144.1) зафиксировал per-type pointer-offset bitmap’ы с дефолтом «неизвестно → указатель» (over-approximate) и пометкой unresolved=true для нерезолвящихся типов. Соундность закрыта (никогда не пропустить реальный GC-указатель). Остаётся точность — где консервативный анализ помечает слот как указатель (или весь тип unresolved), хотя реально слот скаляр / не-GC, раздувая будущую mark-работу Ф.5:

  1. Closures env-bitmap. Сейчас fn(...)-поле моделируется как ОДИН GC-указатель на closure-объект (env — heap), а анонимный protocol { } / dyn — void* + флаг unresolved (макс-консервативно). Точный per-capture env-bitmap (word0 = fn-ptr = НЕ-GC code-pointer; далее — по одному слоту на capture с их GC-ностью) дал бы точную пометку env’а вместо «сканировать как один указатель». Прецедент: Go gcdata на display-класс, JVM/.NET display-классы, MLton (Plan 144 §7.6 Q10). Гейтится на H5 (захват by-value boxing vs interior-in-frame) — Ф.0.
  2. Generic / erased инстанциации. Неразрешённый generic-слот / erased nova_int-boxed элемент / cross-module тип, чей TypeDecl не во вселенной графа, → unresolved → весь объект сканируется консервативно. Точнее было бы тянуть layout per-instantiation (как мономорфизация в codegen) или через manifest/exports (родственно exports_consume_types D164), чтобы доказуемо-скалярные поля не раздували mark.
  3. FFI-pointer edge-cases. Raw *T / *ro u8 классифицируется как НЕ-GC скаляр (указывает ВНЕ GC-heap — корректно для нынешнего FFI-контракта, residual-note [classify-raw-ptr] в коде). Но *T в boxed Nova-структуру (если такой паттерн появится через unsafe-мост) под non-moving object-start lookup был бы безвреден как указатель — текущий skip консервативен в обратную сторону и опирается на инвариант «raw-ptr не в nova_alloc-heap». Сверка с реальными FFI-emit-путями уточнила бы границу.

Почему отложено. Все три — улучшения точности, не соундности: каждый residual уже покрыт консервативно (over-approximate указатель / unresolved → лишняя mark-работа, остаёмся корректны). Браться имеет смысл только когда Plan 144.5 Ф.5 начнёт потреблять bitmap’ы (точный mark-sweep) и измеримая доля false-retention / лишнего скана придётся на эти паттерны — иначе оптимизируем неиспользуемый артефакт.

Когда вернуться. Вместе с Plan 144.5 (потребление bitmap’ов точным tracer’ом), если профиль покажет, что closures / generic-erased / FFI-границы доминируют в лишней mark-работе.

Связь. D375, D273 (sibling emit-nothing may-GC, [Q-may-gc-precision]), Plan 144 §7 (heap-сторона) / §7.6 (Q8/Q10/H1), [M-144.1-heap-bitmaps].


Q15. Enum с числовыми значениями ✅ ЗАКРЫТО (D52)

D52 ввёл sum-варианты с числовыми discriminants и auto-increment:

type ExitStatus | Ok | Failure | Critical                  // 0, 1, 2 (auto)
type FileMode | Read = 1 | Write | Execute                 // 1, 2, 3
type ErrorCode
    | NotFound       = 404
    | Unauthorized   = 401
    | InternalError  = 500
type Bit u8 | Off = 0 | On = 1                             // явный базовый тип

Явный базовый тип (type X u8 | …) пока не реализован — parser drift, см. Plan 105.

Cast Sum → int безопасный (c as int); cast int → Sum через pattern match с Fail[InvalidVariant]. Конфликт значений запрещён компилятором.

Закрывает все use-case’ы исходного Q15:

  • Привязка численного значения — через = n.
  • Автонумерация — через auto-increment от первого варианта.
  • Сериализация в wire-формат — через as int (стабильный discriminant).

Q16. Bitflags ✅ ЗАКРЫТО (D46)

С введением D46 (operator overloading) вопрос закрывается Вариантом C (newtype над int с методами @or, @and):

type Permission(int)
const READ    Permission = Permission(1)
const WRITE   Permission = Permission(2)
const EXECUTE Permission = Permission(4)

fn Permission @or(other Permission) -> Permission =>
    Permission(@0 | other.0)

fn Permission @and(other Permission) -> Permission =>
    Permission(@0 & other.0)

fn Permission @contains(flag Permission) =>
    (@ & flag).0 != 0

let p = READ | WRITE
if p.contains(READ) { ... }

Типобезопасность сохранена, оператор | работает через @or. Stdlib Bitflags[T] (Вариант A) не нужен.

Контекст. Permissions, capabilities, set-of-options — частый паттерн:

Read | Write | Execute
HTTP_GET | HTTP_POST
INTR_HOLD | INTR_LATCH

Это не sum-type (sum-type = один из вариантов, bitflags = комбинация). Sum-type для них не подходит. Нужен либо int с константами и битовыми операциями (как в C), либо специальный тип Bitflags[T].

В Nova никак — нет ни int-констант с битовыми операторами как идиомы, ни Bitflags-типа.

Варианты

A. Stdlib Bitflags[T] — generic-тип над enum-подобным sum-type:

type Permission | Read | Write | Execute
ro p Bitflags[Permission] = Permission.Read | Permission.Write
if p.contains(Permission.Read) { ... }

Требует перегрузки операторов |, & для конкретного типа. См. D1 — перегрузка операторов «только для стандартных traits» (намёк, что для custom-типов нельзя). Нужно явно расширить.

B. Goлевой стиль int + константы:

ro PERM_READ    = 1
ro PERM_WRITE   = 2
ro PERM_EXECUTE = 4
ro p = PERM_READ | PERM_WRITE

Работает уже сейчас (int + битовые операторы), но теряется типобезопасность — p имеет тип int, не Permission.

C. Newtype над inttype Permission(int), методы .has(...), .with(...). Безопасно, но многословно, и | не работает без operator overloading.

Приоритет

Средний. Нужно для backend (HTTP, БД, ОС-вызовы). Решение зависит от того, есть ли в Nova operator overloading для custom-типов.

Связь. Q15 (если оба решаются через derive-макросы), D1 (перегрузка операторов).


Q17. Bootstrap-язык компилятора — Rust

Решение из обсуждения: первый компилятор Nova (v0.1–v1.0) пишется на Rust. После self-hosting (v2.0+) переписывается на Nova.

Почему Rust:

  • Лучшая экосистема для компиляторов (LLVM через inkwell, парсеры через chumsky/logos, AST через native sum-types)
  • Pattern matching, sum-types, traits — естественны для PL-кода
  • LLM знает Rust очень хорошо — AI-codegen качество максимальное
  • Прецедент массовый: Roc, Gleam, Carbon, Mojo, Grain — все на Rust
  • Концептуальная близость к Nova (~T/&T/мономорфизация/sum-types идейно срисованы с Rust)

Почему не C:

  • AST на C через enum + union — в 3-5 раз больше кода + ручная память
  • Нет exhaustiveness check для switch
  • LLM хуже на C-компиляторных задачах
  • Memory bugs съедят 30% времени разработки

Почему не OCaml:

  • В 1.5x короче Rust для компилятора, но LLM знает хуже
  • LLVM bindings слабее
  • Меньше потенциальных контрибьюторов

Это не D-решение. Это выбор реализации, как SMT-движок в D24. В дизайн-документе языка не фиксируется — может измениться в зависимости от инструментов и команды.

Связь. D24 (по аналогии — выбор реализации, не дизайна).


Q19. ЗАКРЫТО (2026-05-10) — Trailing-block синтаксис expr { x => body }

Статус: закрыто. Решено ревизией Closure-rev (Plan 19, D43 rev):

  • Trailing-block (f(args) { block }) — разрешён только для callback’ов без параметров (DSL-форма: with_timeout, retry, transaction, region, supervised).
  • Trailing-fn (f(args) fn(p) body) — для callback’ов с params, идентично closure-full без имени.
  • Старая форма f(args) { x => body } (с параметрами через =>) отменена.
  • Ответ на исходный вопрос Q19: «общий механизм», но в двух чётких формах для разных случаев — { block } для no-params, fn(p) body для with-params.

Вопрос остаётся в файле как исторический контекст — показывает эволюцию от Kotlin-style { x => body } к Rust-style разделению.


Q19 (исторический контекст). Trailing-block синтаксис expr { x => body } — общий механизм или фиксированные примитивы?

Контекст. В коде Nova используется паттерн «функция/конструкция + блок в качестве последнего аргумента»:

race {
    body(),
    sleep(dur).then { throw Timeout }    // .then { ... } — trailing block
}

with_timeout(2.seconds) {                 // trailing block
    Db.exec(sql`UPDATE counters SET v = v + 1`)
}

supervised {                              // trailing block
    spawn handle_requests()
}

with Db = real_db {                       // блок после with
    transfer(alice, bob, 100)
}

region {                                  // блок region (D6)
    ro buf = []f32.with_capacity(1024)
    buf.to_owned()
}

Не зафиксировано: это специальный синтаксис языка для structured concurrency / scope primitives, или общий механизм «вызов функции с блоком как последним аргументом» (как Swift/Kotlin trailing closure)?

Варианты

A. Только зафиксированные примитивы языка. Список конструкций с блоком фиксирован: with, supervised, region, parallel for, race, select, with_timeout, try_panic (отменён) — каждая описана в D-решениях. Программист не может делать expr { x => body } для своих функций.

Плюсы:

  • Парсер однозначен — { после имени = блок только для известных конструкций
  • AI-first: LLM видит конкретные конструкции, не путает с лямбдами
  • Минимум синтаксической поверхности

Минусы:

  • Расширение языка требует D-решения
  • Меньше гибкости для библиотек

B. Общий trailing-closure механизм (Swift/Kotlin/Ruby стиль). Любая функция, последний параметр которой — функция, может быть вызвана с блоком вместо круглых скобок:

fn with_lock[T](lock Mutex, body fn() -> T) -> T => ...

with_lock(my_mutex) {                     // trailing block
    do_work()
}

Плюсы:

  • Унифицирует язык: parallel for, with_timeout, custom with_lock — всё одна форма
  • Библиотеки могут создавать DSL-подобные конструкции
  • Прецедент Swift, Kotlin, Ruby

Минусы:

  • Парсер сложнее — { после идентификатора может быть и блок-литерал, и trailing closure
  • AI-first: дублирование с обычной лямбдой f((x) => ...)
  • Скрытое поведение: что значит obj.method { x => ... }?

C. Гибрид — ключевые слова и whitelist’ed функции. Зафиксированные примитивы (вариант A) + явный список stdlib-функций, которые принимают trailing block (with_lock, with_resource, with_timeout). Пользовательские функции — только через обычный вызов с лямбдой.

Плюсы:

  • Гибкость для stdlib без обобщения
  • Парсер всё ещё знает, что является блоком

Минусы:

  • Список нужно поддерживать
  • Грамматика менее однородна

Моё предложение

Вариант A — только зафиксированные примитивы языка. Причины:

  1. AI-first: один способ передать closure — обычный аргумент f((x) => body). Не два способа делать одно.
  2. Парсер однозначен: { после with/supervised/region/race/ parallel/select/with_timeout/try — это блок, не record/ handler-литерал. Иначе грамматика становится контекстно-зависимой.
  3. Принцип «не плодить специальные сущности» (D17, D18, D22) — trailing-closure это ещё одна синтаксическая роль {, которая увеличивает поверхность.

.recover { err => ... } в examples/audit.nv был ошибкой — заменён на handler with Fail[E] = |err| ... { ... }.

Приоритет

Низкий. Сейчас все нужные конструкции (with, supervised, region, parallel for, race, select, with_timeout) зафиксированы как примитивы языка. Если возникнет реальный use-case для общего trailing-closure — пересмотреть. Пока — нет.

Связь. D14 (structured concurrency primitives), D22 (лямбды).


Q18. ЗАКРЫТО — Cycle-detection больше не актуален

Статус: закрыто. D21 отменён в пользу managed memory (D6 пересмотрен). Циклы освобождаются автоматически concurrent GC, никаких compile-time ошибок цикла, никаких suggestion’ов о weak-направлении не нужно.

Вопрос остаётся в файле как исторический контекст — показывает, почему мы отказались от opt-in cycle collection. Дальнейшее тело сохранено, но не актуально для текущего дизайна.

См. D6 (новая версия) и discussion-log этап 13.


Историческое тело (не актуально)

Контекст. D21 фиксирует: цикл через ~T без ~weak — compile error с suggestion. Это уже работает на уровне дизайна. Открытый вопрос — качество этих сообщений и наличие lint-режима для поиска циклов в большом проекте.

Зачем нужно

Программисты регулярно создают потенциальные циклы — особенно при рефакторинге. Сейчас в D21 (отменено) зафиксирован формат ошибки:

error: cycle possible in `~T` references between Node and Edge
   suggestion: use `~weak` for one direction, or `~&T` to enable cycle collection

Этого недостаточно для AI-first языка. LLM получает короткое сообщение, не видит варианты, не знает какую сторону цикла сделать слабой. Человек тоже теряется.

Что улучшить

1. Расширенный формат ошибки с тремя вариантами:

error: cycle in `~T` references: Tree → children → Tree → parent → Tree

  options:
    1. Make `parent` weak (typical for trees, leaves owned by root):
         parent ~weak Tree
    2. Make `children` weak (rare, used when leaves outlive parent):
         children []~weak Tree
    3. Use `~&T` for both (enables cycle collection, runtime cost):
         children []~&Tree, parent ~&Tree

  see: docs/memory/cycles.md#trees

LLM или человек видит все варианты с пояснением, делает осознанный выбор. Это AI-first (R5.3) — обучающий сигнал.

2. Lint-режим nova lint --memory-graph:

Анализирует граф типов всего проекта, находит возможные циклы и неоптимальные паттерны. Полезно при рефакторинге крупных систем.

3. Документация docs/memory/cycles.md:

Каталог типичных паттернов с готовыми решениями:

  • Деревья (parent → child) — parent ~weak
  • Doubly linked list — tail ~weak или prev ~weak
  • Observer / Subscription (publisher ↔ subscriber) — subscribers []~weak
  • DOM-like (parent ↔ children + listeners) — ~&T (подходит для cycle collector)
  • Graph (произвольные циклы) — ~&T

Каждый паттерн со ссылкой на ошибку компилятора, чтобы LLM могла переходить от ошибки к docs автоматически.

4. Stdlib-defaults с явным комментарием:

// в stdlib
type Tree[T] {
    value T
    children []~Tree[T]
    parent ~weak Tree[T]    // weak: tree owned top-down, leaves don't outlive root
}

Комментарий объясняет почему именно эта сторона weak — для обучения программиста, использующего stdlib.

Что отвергнуто

Авто-вставка ~weak. Компилятор не может выбрать сторону цикла без знания домена. Контрпример: дерево vs subscription/publisher — weak’ом помечается противоположная сторона. Авто-выбор приведёт к тихим багам с жизнью объектов.

Авто-конверсия ~T~&T при цикле. Скрывает performance-импликацию (cycle collector работает в этой зоне), нарушает real-time гарантии тихо. D21 опт-ин, не автомат.

Приоритет

Средний. Не блокирует язык, но критично для UX. Без хороших сообщений compile-time проверка циклов превратится в раздражитель, а не помощника. Делается одновременно с реализацией type checker’а (этап 2 roadmap).

Связь. D21, R5.3, roadmap этап 2.


Приоритет

Если возвращаться к работе:

Сначала (закрыть, чтобы продолжать):

  • Q1 (унификация методов) — структурный вопрос
  • Q8 (обновить документы) — технический долг
  • Q12 (concurrency model) — блокирует Q9 и ломает AI-first тезис

Потом (важно для v0.1):

  • Q5 (граница Panic)
  • Q6 (effect polymorphism)
  • Q9 (stdlib) — зависит от Q12, Q13
  • Q15 (enum с числами) — частая боль для wire-протоколов
  • Q16 (bitflags) — нужно для permissions/options

Можно отложить (детали реализации):

  • Q3, Q4 (runtime детали)
  • Q7 (macros) — но блокирует Q15 D-вариант
  • Q10 (tooling)
  • Q11 (имя)
  • Q13 (schema evolution как stdlib-паттерн) — описать вместе с Q9
  • Q14 (cost-types) — после v1.0

Q20. Нужен ли defer? ✅ ЗАКРЫТО (2026-05-10) D90

Закрыт D90 (2026-05-10): добавлены defer и errdefer как scope-level cleanup statement’ы. Семантика — Zig-style: scope-level (не function-level), LIFO, eager arguments, infallible body, no-suspend. errdefer запускается только на throw/panic-exit. Решение мотивировано отсутствием RAII в Nova (D6 managed heap) — без defer resource cleanup пишется через handler-блоки, что многословно.

Контекст. Слово defer присутствует в подсветке VSCode-расширения (editors/vscode/syntaxes/nova.tmLanguage.json, editors/vscode/README.md) как зарезервированное ключевое слово, но семантика в spec/decisions/ не определена. До формального решения defer использовать нельзя.

Семантика, обсуждавшаяся ранее (Zig/Swift-style):

  • Block-scoped (а не function-scoped как Go).
  • Срабатывает при throw, не при panic (D13/D25).
  • LIFO.
  • Две формы: defer expr и defer { ... }.
  • Эффекты внутри тела defer должны быть в сигнатуре enclosing-функции.

Главный вопрос — нужен ли defer в Nova вообще.

В Nova уже есть два механизма cleanup’а без отдельного defer:

  1. Handler-обёртки. Защита ресурса оформляется как функция, принимающая lambda-тело. Cleanup — на выходе из обёртки:

    fn with_file[T](path str, body fn(File) Fail[IoError] -> T)
        Fail[IoError] -> T {
        ro f = File.open(path)!!       // Result[File, IoError] → throw IoError
        ro result = body(f)            // если throw — handler ловит выше
        f.close()                        // обычное закрытие
        result
    }
    
    with_file("data.txt") { f =>
        f.write(data)
    }
    

    Это более общий механизм — handler видит throw, может откатить транзакцию, обработать как угодно. Согласовано с D10 «всё — handler».

  2. region { ... } (D6) — арена освобождается en-masse на выходе из блока, без defer.

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

  • Q20.1. Покрывают ли handler-обёртки реальные use-case’ы, или остаются практичные сценарии, где defer x.close() рядом с открытием — заметно эргономичнее? Нужны примеры из реальных программ (придёт с MVP-stdlib и первыми пользователями).
  • Q20.2. Если defer всё-таки вводится — взаимодействие с supervised(cancel:) (срабатывает ли defer при отмене fiber’а?), с region (порядок: defer’ы сначала, арена потом, или наоборот?), с «двойным throw» (defer в процессе throw сам делает throw — что выигрывает?).
  • Q20.3. Альтернатива — RAII через protocol-метод @drop() на типе ресурса. Тип File определяет fn File @drop() Io -> (), компилятор вставляет вызов на выходе из скоупа, как в Rust/C++. Это встроеннее в систему типов, чем defer, и не требует ручного вызова. Рассмотреть как третий вариант.

Статус. До решения — defer не используется в коде/документации языка. Подсветка VSCode оставлена как форвард-резерв (если решение в пользу defer’а — менять не придётся; если против — будет небольшое изменение в syntaxes/).

Связь: D10 (всё — handler), D13 (panic), D25 (throw), D6 (region).


Q21. Управление proliferation эффектов в публичных сигнатурах

Контекст. Типичная backend-функция после нескольких слоёв архитектуры (router → middleware → controller → service → repository) накапливает 5-8 эффектов в публичной сигнатуре:

export fn create_order(req CreateOrderReq)
    Db Net Log Trace Fail[OrderError] Time Random
    -> OrderResponse

Любое расширение в нижнем слое (например, добавили Cache в репозиторий) поднимается через все public-функции вверх. Без механизма группировки это работает как «N-вирусов одновременно» — хуже одиночного async-вируса в Rust. Автор языка считает это критическим вопросом: ожидается, что эффектов в реальных программах будут десятки или сотни.

С другой стороны — D28 («public — обязательно явно») и D10 (AI-first, «сигнатура = полное описание») явно требуют, чтобы ничего не пряталось от читателя. Любой механизм группировки балансирует на этом ребре.

Три альтернативы.

Вариант A. Effect aliases через alias-keyword

alias StandardWeb = Db Net Log Trace Fail Time

export fn create_order(req Req) StandardWeb Random -> Resp

StandardWeb — синтаксический сахар, раскрывается компилятором в union эффектов. Видимость через export alias/private как у других деклараций (D47). Может ссылаться на другие алиасы.

Плюсы:

  • Лаконично в сигнатуре.
  • Локально для проекта (каждый проект свои алиасы).
  • Расширяется обычным redeclaration алиаса.

Минусы / открытые вопросы:

  • Семантика «контракта»: если функция объявила StandardWeb, но реально использует только Db Fail — компилятор разрешает (алиас как ≤-подмножество, но это ослабляет D28 «гарантия чистоты — проверенный факт») или требует все эффекты алиаса использовать (тогда алиас бесполезен)?
  • Параметризованные эффекты в алиасе (Fail[E]): что значит alias A = Fail[OrderError], alias B = Fail[UserError], использование A B — конфликт, объединение, или ошибка?
  • AI-first риск: LLM, видя StandardWeb, не знает её состав без чтения определения — это «−1 файл к локальности контекста». Для пользовательского кода допустимо, для prelude-алиасов опасно.
  • Стандартные алиасы в prelude (Web, Cli, BatchJob) — привлекательно, но: устаревает (нужно Cache?), не угадать «правильный» набор (REST vs GraphQL vs WebSocket — разные), все программы Nova зависят от выбора. Лучше не делать prelude-алиасов; алиасы — проектный механизм.

Вариант B. Алиас как protocol с композицией

После D18 (эффект = protocol) естественно использовать тот же механизм:

export protocol StandardWeb : Db, Net, Log, Trace, Fail, Async, Time

export fn create_order(req Req) StandardWeb Random -> Resp

StandardWeb — обычный протокол, расширяющий 7 других. Composition (A : B, C) — единый механизм для эффектов и для структурных интерфейсов.

Плюсы:

  • Один механизм с D18, не отдельная фича. Меньше концепций.
  • Не вводит новой грамматики (только composition между протоколами).
  • Hover/doc показывают композицию естественно.

Минусы / открытые вопросы:

  • Требует решить семантику protocol composition — это отдельный D, не покрытый D18 в текущей форме. Inheritance vs flat union vs mixin — выбор не сделан.
  • Композиция протоколов влияет и на data-протоколы (структурные контракты для типов), не только на эффекты. Большая семантическая поверхность.
  • Те же AI-first и параметризационные вопросы, что в варианте A.

Вариант C. Effect rows / row polymorphism

export fn create_order[E](req Req) Db Net | E -> Resp

E — переменная для «остальных эффектов». Caller подставляет конкретные эффекты под E. Прецедент — Koka, Effekt (где это академически проверено).

Плюсы:

  • Не пакует имена в группы — каждый эффект остаётся явным в той функции, которая его реально использует.
  • Решает полиморфные случаи: map_eff[T, U, E](xs [T], f (T) E -> U) E -> [U] — функция-комбинатор работает с любым эффектом вызываемой лямбды.
  • AI-first сохраняется: E — это «остальное», не магическое имя группы.

Минусы / открытые вопросы:

  • Сложнее — это полноценный полиморфизм. Реализация дороже.
  • Уже частично есть в Q6 «Effect polymorphism — синтаксис». Нужно свести Q6 + Q21.C в одно решение.
  • Не решает именования в публичных API — там 7 эффектов всё равно пишутся явно. Подходит больше для библиотечных комбинаторов, чем для backend-сигнатур.

Что не делается (для всех вариантов)

  • Subtraction (Alias \ Effect) — сложная row-typing семантика, не для MVP.
  • Default-эффекты на модуль — нарушает «сигнатура = полное описание» (D10).
  • Effect categories в стиле Helium — сложнее, чем алиасы, без выигрыша.
  • Опт-ин на effect inference в publicD28 остаётся, public явный.

Когда решать

Откладываю до первой стадии stdlib (Q9) и первых реальных программ. Сейчас «proliferation» — это прогноз, не измеренная проблема. До MVP неясно:

  • Сколько эффектов реально в типичной сигнатуре?
  • Какие пакуются в стандартные группы?
  • Будут ли row-polymorphic комбинаторы доминировать в stdlib?

Принимать решение в текущем виде — риск ввести неправильную семантику (см. открытые вопросы каждого варианта) или избыточный механизм. Когда начнёт писаться реальный код на Nova, картина прояснится: либо proliferation действительно болит и алиас/protocol-композиция — очевидная победа, либо row-polymorphism в stdlib + 5-7 эффектов в public-сигнатуре оказываются нормой.

Действие сейчас: ничего в спеке не вводить, не использовать ни в примерах, ни в подсветке. Если автор хочет «лёгкое решение прямо сейчас» — рекомендуемый вариант B (protocol composition): дешевле по новой грамматике, опирается на уже принятый D18.

Связь: D18 (эффект = protocol), D28 (public явно), D10 (AI-first), D47 (видимость), Q6 (effect polymorphism — пересекается с вариантом C), Q9 (stdlib — проявит реальную картину proliferation).


Q22. Унификация type / protocol в один keyword? ✅ ЗАКРЫТО (D53, D61)

Финальное решение — гибрид:

  • Declaration syntax объединён под единым type-keyword’ом по D53. Все объявления идут через type, с kind-токеном для категории.
  • Семантика расщеплена по D61: effect и protocolразные kind-токены с разным поведением.
    • effect — поддерживает with-substitution (mock в тестах) и continuation-capture (interrupt, throw).
    • protocol — структурный контракт, без with-substitution.
type Hashable protocol { hash() -> u64 }    // структурный контракт
type Logger effect { log(msg str) -> () }   // эффект (with-substitution)
type Db effect { query(q Sql) -> []DbRow }  // эффект
type any protocol { }                        // top-type

Анонимный protocol-тип в позиции параметра — protocol { ... } (с обязательным префиксом, симметрично []T, (A, B), fn() -> T).

D42 помечен revised → D53. Выбор между effect и protocol — программистский, через два sniff-вопроса (см. D62 правило 4).

Контекст ниже сохранён как историческая справка.


Исходный контекст Q22

Контекст (до D53). После D18-revised и D42 в Nova два keyword’а:

  • type — данные (record, sum-type, alias).
  • protocol — поведение (эффекты + структурные контракты).

Различаются позицией в репо и формой литералов: у type литерал — Name { field: value }, у protocol — handler-литерал Name { op(args) => body }.

Гипотеза. Возможно, достаточно одного type, разрешающего либо поля, либо методы (но не одновременно). Парсер уже различает содержимое {...} для литералов (двоеточие vs стрелка) — то же самое правило могло бы работать и для объявлений.

// data — type с полями
type User { id u64, name str }

// behavior — type только с сигнатурами методов
type Logger { log(msg str) -> () }
type Db {
    query(q Sql) -> []DbRow
    exec(q Sql)  -> ()
}

Аргументы за единый type:

  • Один keyword вместо двух — проще грамматика.
  • Прецеденты: TypeScript (interface и для полей и для методов), Common Lisp CLOS (defclass).
  • Содержимое {...} уже различимо парсером, можно поднять это правило на уровень объявления.

Аргументы против (как сейчас в D18-revised):

  • Декларация категории заранее. Данные и поведение — разные категории (значение существует/нет, сериализуемо/нет, расширение = +поле или +метод, подтипирование = по форме или по контракту). Keyword фиксирует категорию явно.
  • Прецеденты статически типизированных языков идут в обратную сторону: Rust struct/trait, Swift struct/protocol, Go struct/interface. Единый keyword — у структурных/динамических языков (TypeScript runtime — duck typing).
  • Handler-литералы. Сейчас форма литерала однозначно связана с keyword объявления: у type XX { field: value }, у protocol XX { op(args) => body }. С единым type LLM может смешать формы в одном литерале (синтаксически корректно для разных имён, семантически бессмыслица).
  • D42 уже принят — концептуально разделил данные и поведение. Откат к единому keyword требует пересмотра D42.

Тонкие места при унификации:

  • Запретить ли смешивание полей и методов в одном {...}? (Иначе получится «класс» — нежелательно, противоречит «protocols + data».)
  • Что с пустым type X { } — данные без полей или поведение без операций? Сейчас type X = () для unit, protocol X { } теоретически пустой контракт.
  • Параметризация Fail[E] — она у data-типа или у protocol’а? (Сейчас protocol Fail[E], и это согласовано с другими параметрическими протоколами.)

Решение пока: не трогать. D18-revised только что прошёл по spec/ и примерам, аргументы в пользу унификации риторические («одного keyword’а хватит»), без измеренной болевой точки. Оставить как открытый вопрос — если в реальном коде Nova появится систематическое неудобство от двух keyword’ов, вернуться.

Связь: D18 (эффект через protocol), D42 (разделение данные/поведение), D17 (объявление типов без =).


Q23. Группировка методов: methods Type { ... }-блок (Rust impl-style)

Контекст. Сейчас методы типа объявляются через D35 — отдельные fn Type @method(...) декларации:

type Account { ro id u64, balance money, closed bool }

fn Account.new(owner str) -> Account => ...
fn Account @balance_pct(of money) -> f64 => @balance / of * 100.0
fn Account @is_solvent() -> bool => !@closed && @balance > 0
fn Account mut @deposit(amount money) => @balance += amount
fn Account mut @withdraw(amount money) Fail[Overdraft] => ...

Имя типа повторяется на каждом методе. Для типа с 15 методами — 16 повторений. Локальность теряется: тип и его поведение визуально разнесены, особенно когда методы рассеяны по файлу.

Предложение. Ввести methods Type { ... }-блок (как impl Type в Rust):

type Account { ro id u64, balance money, closed bool }

methods Account {
    fn new(owner str) -> Account =>
        Account { id: ids.next(), balance: 0, closed: false }

    fn @balance_pct(of money) -> f64 =>
        @balance / of * 100.0

    fn @is_solvent() -> bool =>
        !@closed && @balance > 0

    fn mut @deposit(amount money) =>
        @balance += amount

    fn mut @withdraw(amount money) Fail[Overdraft] =>
        if amount > @balance { throw Overdraft }
        @balance -= amount
}

Имя типа задаётся блоком. Внутри — fn name(...) для static-функций, fn @name(...) для методов инстанса, fn mut @name(...) для мутирующих. Это тот же @-синтаксис D35, просто сгруппирован.

Альтернативы (расширенный контекст).

  • (a) Оставить как сейчас — раздельные fn Type @method. Минимум концепций, но плохая локальность.
  • (b) methods Type { ... }-блок — это предложение.
  • (c) Методы внутри type блока (Java/Kotlin/Swift-стиль) — сильнее всего ломает текущую модель: type становится «данные + методы» (полу-класс), grow-pressure на наследование, путаница type/protocol для эффектов.

Вариант (c) явно отвергнут: возврат методов в type размывает D1 («protocols + data, без классов»), создаёт два механизма для поведения (type с методами и protocol), и порождает семантические дыры (что значит «эффект внутри метода type?»).

Вариант (b) — компромисс, дающий локальность без слома модели.

Что даёт (b):

  1. Локальность. Тип и методы в одном месте файла.

  2. Группировка по теме. Несколько methods-блоков для одного типа — конструкторы отдельно, базовые операции отдельно, conditional methods (если введут bounds) отдельно. Прецедент Rust.

  3. Extension methods из чужого модуля видны явно.

    import HashMap from std
    
    methods HashMap[K, V] where K: Json, V: Json {
        fn @to_json() -> str => ...
    }
    

    Сейчас в Nova расширение чужого типа делается через fn ForeignType @method где-нибудь в своём модуле — визуально неотличимо от «своих» методов. С блоком — заявка явная.

  4. Совместимость с protocol. type/protocol остаются раздельными. Структурная совместимость работает как сейчас: компилятор смотрит, какие методы определены у типа (через methods или старый стиль), и сравнивает с протоколом.

  5. D1, D17, D42 не меняются. type остаётся чистым (только данные), protocol — чистым контрактом. Меняется только D35: методы переезжают в methods-блок.

Тонкие места:

  1. Один способ или два? Текущий стиль fn Type @method остаётся валидным или нет?

    • Только блок — чище, но breaking change для существующих spec/examples (кода мало, миграция дешёвая).
    • Оба разрешены — совместимо, но «два способа одного» нарушает AI-first.

    Рекомендация: только methods-блок, миграция один раз.

  2. Static vs instance в блоке. Та же разметка: fn name(...) — static (конструктор/factory), fn @name(...) — instance, fn mut @name(...) — mutating instance. Без новых правил.

  3. Множественные блоки для одного типа. Разрешены (как Rust многократный impl). Программист сам группирует по теме.

  4. where-clauses для conditional methods — зависит от Q-bounds. Если bounds в MVP нет, where тоже нет, conditional откладывается.

  5. Visibility. Каждый метод сам декларирует export/private (D47). Блок methods свою видимость не имеет.

  6. Embed/delegation (D39). Прокси- методы при use Type генерируются на основе всех methods-блоков типа. Override метода — отдельный метод во внешнем methods-блоке обёртки, явный вызов @Inner.method() для делегации.

Прецеденты:

  • Rust impl Type { ... } — ровно эта идиома, 10+ лет, любят.
  • Swift extension Type { ... } — то же для своих и чужих типов.
  • Kotlin — методы внутри class + extension functions снаружи (два способа, что у нас неприемлемо).
  • Gofunc (r Type) method() отдельно, как Nova сейчас. Жалоба сообщества — нет визуальной группировки.

Цена:

  1. Новый keyword methods. Короткий, читаемый, семантически точен.
  2. Миграция всех существующих fn Type @method в methods-блоки — делается раз, кода пока мало.
  3. Семантика множественных блоков для одного типа должна быть зафиксирована.

Решение пока: не трогать. Текущий стиль (D35) рабочий, breaking change без измеренного болевого опыта рискован. Когда появится первый средний по размеру тип (например, Vec[T] или HashMap[K, V] в stdlib) с 20+ методами, локальность станет реальной проблемой — тогда вернуться к выбору (b) vs текущее.

Связь: D1 (protocols + data, без классов — Q23 это сохраняет), D17 (type для данных), D35 (методы через @ — Q23 переселяет их в блок), D39 (embed/delegation на основе методов), D42 (protocol остаётся раздельным), D47 (видимость), Q22 (унификация type/protocol — связанный, но ортогональный).


Q-bounds. Синтаксис bounds на дженериках (если будут)

CLOSED by D72. Bounds приняты, синтаксис [T Hashable] без двоеточия, единое правило «name type». Текст ниже — историческая справка.

Контекст. В MVP bounds на дженерики отвергнуты (02-types.md → D42, open-questions D42: «сейчас параметр без bound, компилятор полагается на структурное соответствие при использовании»; history/rejected.md: «[T: Bound] отвергнут в MVP»). Если/когда bounds будут вводиться, нужно зафиксировать синтаксис.

Главное правило, которое уже есть в языке. Двоеточие в Nova — только разделитель ключ-значение (record-литералы, dict-литералы, 02-types.md → D17). В аннотациях типов двоеточия нет: u User, не u: User. Параметр T с указанным контрактом — это аннотация типа, не key-value.

Рекомендуемый синтаксис, если bounds появятся.

fn all[T FromRow]() Db Fail -> []T
fn map[K Hashable, V](m HashMap[K, V]) -> ...

Без двоеточия, по правилу «имя тип» — единый стиль с параметрами функции (x int), полями record (id u64), let-bindings (let x int = 5), for-loops (for x int in xs), embed (use w HashMapIter[K, V]).

Что отвергается заранее:

  • [T: FromRow] (Rust/Scala/Kotlin) — конфликтует с D17.
  • [T where FromRow] (C#-style) — многословно.
  • [T impl FromRow] (Swift some-style) — нестандартно.

Тонкие места:

  1. Несколько bounds на один параметр — [T FromRow & Hashable]? [T (FromRow, Hashable)]? Лучше — анонимный structural type или композиция протоколов.
  2. Связь с эффектами в bounds — может ли protocol-bound включать эффект-операции? (Эффекты — это protocol, D18, так что технически да.)
  3. Что бывает с уже принятыми решениями про anonymous structural type в позиции параметра (D42): fn f(x { show() -> str }) — можно ли это перенести в bound: fn f[T { show() -> str }]()?

Статус. Открыт как «предзафиксированная форма» — если bounds будут, использовать [T Bound] без двоеточия. Целиком решение о вводе bounds откладывается до post-MVP.


Q-self-mandatory. Обязательное использование Self где валидно

Контекст. D66 разрешает Self в любом type-контексте (методы, static-функции, protocol, effect). Сейчас программист может писать либо Self, либо явное имя типа:

fn Box[T].of(v T) -> Self => ...           // Self
fn Box[T].of(v T) -> Box[T] => ...         // явное имя — тоже валидно

Предложение. Сделать Self обязательным там где он валиден. Использование явного имени типа — compile error или линт-warning.

Аргументы за:

  1. DRY жёстче. Имя типа никогда не повторяется → переименование Box → Container точечное.
  2. AI-консистентность. LLM не «забывает» дописать generic- параметры (Box[T] vs Box).
  3. Один способ (D40).

Аргументы против:

  1. Имя типа читается лучше для коротких типов. User, Box — нагляднее чем Self. Self экономит для generic’ов с параметрами.
  2. Прецедентов нет. Rust, Swift, Scala — Self всегда опционален. Strict-Self нет ни в одном языке.
  3. Конфликт с существующим стилем. Большая часть spec/examples написана с явными именами типов. Миграция значительная.
  4. AI-training data. LLM обучен на языках без обязательного Self — генерирует имена типов. Hard-rule вызовет постоянные compile errors при first generation.
  5. Гибче через линтер. Если хочется единообразия — это linter-warning, не language-rule. Программист отключает локально при необходимости.

Варианты решения:

A. Hard-rule в языке. fn Box.of(v T) -> Box[T] — compile error «use Self». Жёстко, но единообразно.

B. Линт-warning по умолчанию. Линтер предупреждает «здесь можно Self», программист игнорирует/исправляет. Гибко.

C. Style-guide рекомендация. Прописать в convention’ах: «для методов с явным receiver’ом и static-конструкторов используй Self», без linter enforcement.

D. Оставить как есть. Обе формы валидны без рекомендации.

Тонкие места:

  1. Свободные функции с generic-параметрами не имеют receiver’аSelf запрещён по D66. Hard-rule не применим в свободных функциях.
  2. Bound [T From[Self]] в свободной функцииSelf запрещён, нужно явное имя или другой generic. Это уже исключение.
  3. Sum-вариантыfn Tree @clone() -> Self валиден, но в теле match @ { Leaf => Leaf, ... } нельзя Leaf => Self.Leaf (конструктор). Self применим только в типовых позициях.
  4. Миграция существующих файлов — большая часть spec/decisions/, examples/, nova_tests/ написаны с явными именами. Hard-rule потребует масштабного sweep.

Статус. Не зафиксировано. Склонность — (B) линт-warning: сохраняет гибкость, даёт DRY-win опционально, не ломает существующий код. Решение откладывается до появления линтера в toolchain.

Связь: D66 (Self universal), D40 (один способ), D72 (bounds, где Self запрещён в свободных функциях).


Q-anon-effect. Анонимный эффект в позиции эффекта

Контекст. D42 разрешает анонимный структурный тип в позиции параметра:

fn log_one(x { show() -> str }) Log -> () =>
    Log.info(x.show())

После D18-revised эффект — это protocol. По симметрии:

fn log_one(s str) { log(msg str) -> () } -> () =>
    log("...")            // анонимный эффект между ) и ->

Допускать ли это? Удобно при «одноразовом» эффекте без отдельного объявления. Но границы между параметрами и анонимным эффектом могут читаться плохо: fn f(x { ... }) { ... } -> () — два {...} подряд, парсер должен различить «структурный параметр» и «анонимный эффект».

Статус. Решение отложено. На MVP — анонимные эффекты запрещены, эффект всегда именованный protocol.


Q-field-tags. Метаданные на полях типов (аналог Go struct tags)

Контекст. В Go теги вида `json:"id"` решают связку «имя поля в коде ↔ имя в wire-формате» для JSON, БД, валидации, и доступны через runtime reflection. В Nova сейчас такого механизма нет — маппинг делается ручными функциями (fn User.from_row(r) -> User => ..., fn User @to_json() -> Json => ...) или handler-литералами.

Для record с 5-10 полями ручной маппинг — приемлемая цена и AI-friendly прозрачность. Для record с 30+ полями (типичный backend domain-model) — заметный boilerplate, который накапливается на каждом формате (Json/Sql/Validate/Yaml/Protobuf).

Что в других языках:

ЯзыкМеханизмГде живёт
Go`json:"id"` после поляruntime reflection
Rust#[serde(rename = "id")]compile-time macros (derive)
Swiftenum CodingKeys: String, CodingKeycompile-time через protocol
Kotlin@SerialName("id") + serialization plugincompile-time
OCaml[@@deriving yojson]compile-time PPX
C# / Java[JsonPropertyName("id")] / @JsonPropertyruntime reflection

Современные типизированные языки в подавляющем большинстве идут по пути compile-time derive, не runtime reflection. Это согласовано с направлением Nova (всё видно в типах, никакой невидимой runtime-магии).

Варианты:

A. Не вводить, оставить ручной маппинг. Прозрачно, никакой магии, полный контроль. Цена — boilerplate для больших record’ов и дублирование при многоформатной сериализации.

B. Compile-time атрибуты + derive-макросы (Rust serde-style). Атрибут на поле и/или типе:

@derive(Json, FromRow)
type User {
    ro id u64       @json("id")          @sql("user_id")
    name str              @json("name")        @sql("full_name")
    email str             @json("email")       @sql("email")
    internal_token str    @json(skip)          @sql(skip)
}

Атрибуты — compile-time литералы, доступные только comptime-функциям. Сами по себе ничего не делают; @derive(Json) запускает comptime-функцию, которая читает атрибуты и генерирует fn User @to_json() -> Json, fn User.from_json(j Json) -> User.

Раскрытие генерации обязано быть видимым через тулинг (nova check --show-derive User) — это сохраняет AI-first прозрачность, LLM может посмотреть «что на самом деле вызывается».

Зависит от Q7 (macros/comptime) — без него вариант B нереализуем.

C. Schema-объект как first-class value. Программист руками объявляет схему рядом с типом:

type User {
    ro id u64
    name str
    email str
}

ro user_json_schema = JsonSchema[User] {
    field("id",    (u) => u.id,    (u, v) => User { ..u, id: v })
    field("name",  (u) => u.name,  (u, v) => User { ..u, name: v })
    field("email", (u) => u.email, (u, v) => User { ..u, email: v })
}

Никаких атрибутов на полях. Schema — обычный Nova-объект, читается функциями Json.encode_with(user, user_json_schema). Цена — multiline-объявление вместо одного-двух тегов на поле, дублирование имён полей в schema.

Не зависит от других open-questions, можно зафиксировать сейчас.

D. Runtime reflection (как Go, Java, C#). Отвергается заранее:

  • Противоречит AI-first: тег json:"id" ничего не делает «сам по себе», нужно знать какую-то библиотеку «снаружи кода». LLM, читая поле, не видит активную конструкцию.
  • Несовместимо с принципом «всё видно в типах» — теги это out-of-band метаданные, которые компилятор не валидирует.
  • Несовместимо с capability-режимом и effect-видимостью — reflection обходит эффект-систему.

Тонкие места:

  1. Несколько потребителей одного поля (json + sql + validate). В B — несколько атрибутов рядом с полем, шумно но локально. В C — несколько отдельных schema-объектов, поле повторяется по числу форматов. Что лучше — открыто.

  2. Skip-семантика. Поля, которые не сериализуем: отдельный atom @json(skip), отсутствие атрибута → не включать, или priv field modifier (D220) автоматически skip’нут? (priv fields — implementation detail, не serialize’ятся by default.)

  3. Default-имя. Если 95% полей сохраняют имя один-к-одному (snake_case в коде ↔ snake_case в wire), не нужно ли по умолчанию маппить, отмечать атрибутом только исключения? Это сильно срезает boilerplate.

  4. Имена derive’ов. @derive(Json) или @derive(Encoder[Json]) или @derive(SerializableTo[Json])? Упирается в дизайн stdlib (Q9).

  5. Composability с эффектами. Если Json.encode имеет эффекты (например, Fail[EncodeError]) — генерируемая функция должна правильно их пробрасывать. Это согласуется с D28 (вывод эффектов в private), но требует, чтобы comptime-генератор корректно вычислял эффект-сигнатуру.

  6. Совместимость с readonly/mut-полями (D36). При decode’е из wire-формата нужно создавать новый User (потому что idreadonly), не мутировать существующий. Генератор должен это учитывать.

Связь.

  • Q7 (macros/comptime) — блокирует вариант B.
  • Q9 (стандартная библиотека) — определяет Json, FromRow, Validate и т.п.
  • Q15 (representation tags для enum’ов) — смежная задача, тоже про сериализацию, может решаться тем же механизмом.

Статус. Открыт. Рекомендуемый путь:

  1. Сейчас — оставить вариант A (ручной маппинг). Это работает, у Nova нет реализации, спешить некуда.
  2. После Q7 — вернуться, рассмотреть B+C: атрибуты для частых случаев, schema для сложных. Это не взаимоисключающие варианты.
  3. Никогда — вариант D (runtime reflection).

Q-anonymous-union. Anonymous unions (TS-style string | number) без обёрток

Контекст. Сейчас sum-тип в Nova (D52) требует именованные конструкторы:

type StrOrInt | S(str) | I(int)
ro x StrOrInt = S("alice")

С D55 literal coercion программист пишет просто let x StrOrInt = "alice" (компилятор оборачивает в S). Но тип всё равно остаётся sum-обёрткой, а не «string или number».

TypeScript/Scala 3 имеют anonymous unions без обёрток:

type StrOrInt = string | number;     // tip = string ИЛИ number
let x: StrOrInt = "alice";            // тип x — string, не обёртка

Здесь stringподтип string | number. Это subtyping, которого в Nova сейчас нет (только структурная типизация для protocol’ов).

Альтернативы синтаксиса для Nova (если когда-то введём):

A. С маркером type для existing types:

type IntOrStr | type int | type str
type Maybe[T] | type T | None

Парсер однозначен — type X = «existing type», без type = «новый конструктор». Не ломает текущий синтаксис sum’ов.

B. Со скобками:

type IntOrStr | (int) | (str)

Двусмысленно с tuple-конструкторами одного поля.

C. Не вводить. Использовать D55 coercion + named sum’ы как сейчас (type StrOrInt | S(str) | I(int)). Громоздко при объявлении, но coercion убирает шум при использовании.

Главные минусы введения:

  1. Subtyping — серьёзное расширение системы типов. Нужно решить variance, type inference, exhaustiveness, dispatch. Сейчас Nova эти концепции не имеет.
  2. Runtime-cost. Каждое значение IntOrStr несёт runtime-tag (иначе is-проверка не работает). Boxing на границах. Для статически типизированного языка — реальная цена.
  3. Прецедентов мало. TS (бесплатно в JS-runtime), Scala 3 (с cost). Большинство строго типизированных языков (Rust, Swift, Kotlin, F#, OCaml, Haskell) не вводят anonymous unions — используют named variants.

Решение пока: не вводить. D55 coercion + named sum покрывают большинство use-case’ов. Если в реальном Nova-коде накопится измеренная боль от обёрток — вернуться.

Связь: D52, D55, D54 (is-pattern уже даёт runtime type-check для any).


Q-stdlib-data-types. SqlValue, JsonValue, Sql, теги sql/json в stdlib

SQL-часть — эталонная реализация в examples/stdlib_sql.nv и применение в examples/orm_demo.nv. Окончательная фиксация в prelude (D26) — отдельным D-блоком после v1.0-stdlib.

JSON-часть — открыта. Number representation и Object representation не зафиксированы (см. подвопросы ниже).

Контекст. D48 фиксирует tagged template literals и стандартные теги json, sql, regex, bytes. Но возвращаемые типы этих тегов и их структура — не определены.

С введением D55 (literal coercion) типизация SQL-аргументов через closed sum становится практичной:

// Кандидат для prelude:
type SqlValue
    | I(i64)
    | F(f64)
    | S(str)
    | B(bool)
    | Bytes([]byte)
    | Null

type Sql {
    template str           // "SELECT * FROM users WHERE id = ?"
    args []SqlValue        // [I(42)]
}

// Tag-функция:
fn sql(parts []str, args []SqlValue) -> Sql =>
    Sql {
        template: parts.join("?"),
        args
    }

// Использование (через D55 coercion):
ro q = sql`SELECT * FROM users WHERE id = ${user_id}`
ro users = Db.query(q)
Db.query(sql`... ${42} ... ${"alice"}`)   // безопасно, без injection

Аналогично для JSON:

type JsonValue
    | Null
    | Bool(bool)
    | Number(...)            // f64? i64+f64? opaque Number?
    | String(str)
    | Array([]JsonValue)
    | Object(HashMap[str, JsonValue])

ro j JsonValue = json`{"name": "${user}", "age": ${age}}`

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

  1. Number representation в JsonValue. Number(f64) теряет int-precision. Int(i64) | Float(f64) точнее, но coercion 42 ambiguous (i64 или f64?). Number(NumberKind) где NumberKind | I(i64) | F(f64) — двухуровневое, гибко но громоздко. Прецеденты: Rust serde_json — opaque Number с методами as_i64()/as_f64().
  2. Object representation. HashMap[str, JsonValue] теряет порядок ключей. Альтернатива — []Field. Большинство JSON-парсеров используют HashMap.
  3. Compile-time JSON-парсинг через json\…“. Нужен Q7 (macros/comptime), без него — runtime.
  4. Db.query сигнатура.Решено: через Sql-тег. fn Db.query(q Sql) Fail[DbError] -> []DbRow, fn Db.exec(q Sql) Fail[DbError] -> int. Это единая публичная сигнатура — все usage’и через sql\…`илиSql.builder().build()(динамические запросы). Прямого пути с raw-string'ом и отдельным[]SqlValue-массивом не осталось. Эталон — examples/stdlib_sql.nv`.
  5. Где разместить. В prelude (D26) или в stdlib-модулях (std.sql, std.json)? Гибрид: Option/Result — prelude, SqlValue/JsonValue — модули?

Решение пока: не вводить в prelude. Зафиксировать как часть Q9 (stdlib). При работе над Q9 решить все 5 пунктов.

Связь: D48 (tagged templates), D55 (coercion делает это эргономичным), D26 (prelude), Q9.


Q-numeric-coercion. Coercion числовых литералов через D55

DEFERRED — ждёт JsonValue (Plan 17 Ф.3, 2026-05-08). Rationale: сквозная coercion (D44 numeric + D55 sum) полезна для JsonValue | Number(f64) | ... и подобных, но без зафиксированного JsonValue-типа в stdlib главного use-case’а нет. Текущий exact-match строже, не ломает existing код. Trigger: добавление JsonValue или Configvalue в stdlib (Q-stdlib-data-types) — первый случай, где coerce даст реальное упрощение ({ "k": 42 } без Number(42.0)).

Контекст. D55 ввёл literal coercion для sum-конструкторов с exact match типа значения. Тонкость возникает с числовыми литералами:

type Wrapper | F(f64)

ro w Wrapper = 1.5         // ok — F(1.5), exact match (f64)
ro w Wrapper = 42           // ??? 42 это int, не f64

D44 разрешает literal coercion для числовых типов в позиции с явной аннотацией:

ro x u8 = 200               // 200 как u8 (D44)
ro y f64 = 42                // 42 как f64 (D44)

Вопрос: работает ли эта литеральная coercion сквозь D55?

ro w Wrapper = 42           // ⇒ w = F(42 as f64)? или ОШИБКА?

Альтернативы:

A. Сквозная coercion (D44 + D55 комбинируются). Литерал 42 подгоняется под f64 в позиции F(f64)-параметра. Эргономично, но добавляет цепочку конверсий — D55 говорит «exact match».

B. Только exact match. Программист пишет 42 as f64 или 42.0:

ro w Wrapper = 42 as f64    // явно
ro w Wrapper = 42.0          // float-литерал

Строже, без неожиданностей. Но громоздче.

C. Только для числовых литералов. Literal coercion (D44) работает для int↔int, int↔float; D55 видит уже «адаптированный» литерал. Это частный случай A, но ограниченный литералами (не переменными).

Проблема для JsonValue:

type JsonValue | ... | Number(f64) | ...

ro j JsonValue = 42         // что: Number(42 as f64) или ОШИБКА?

Без сквозной coercion JsonValue неэргономичен — каждое целое требует 42.0 или 42 as f64. С coercion — 42 работает.

Решение пока: отложено до решения по JsonValue (Q-stdlib-data-types). Сейчас D55 строго требует exact match. Если JSON/SQL покажет реальную боль — расширить D55 до варианта C (literal-only через D44).

Связь: D44, D55, Q-stdlib-data-types.


Q-style-coercion. Когда применять D55 coercion, когда писать явно?

CLOSED by D55 → «Style-guide» (Plan 17 Ф.1, 2026-05-08): permissive (вариант A) с формализованными рекомендациями (вариант B для линтера). nova fmt не переписывает между формами — выбор стилистический. Сводка-таблица «coerce / явный» добавлена в D55.

Контекст. D55 ввёл literal coercion в позиции с явным целевым типом. Это opt-in эргономика — старая форма (явные конструкторы и имена типов) тоже валидна и работает. Получаются два равнозначных написания одного и того же:

// Sum-coercion
ro m Maybe[int] = 42                    // ✓ coercion
ro m Maybe[int] = Just(42)               // ✓ явный, тоже валидно

// Record-coercion
ro u User = { id: 2, name: "Bob" }       // ✓ coercion
ro u User = User { id: 2, name: "Bob" }   // ✓ явный, тоже валидно

fn make() -> Duration => { nanos: 100 }    // ✓ coercion
fn make() -> Duration => Duration { nanos: 100 }    // ✓ явный

Это classical style-vs-mandate вопрос, не зафиксированный в D55.

Реальные case’ы из миграции examples/:

  1. Однозначно лучше с coercion:

    export fn Duration.from_secs(n i64) -> Duration => { nanos: n * 1e9 }
    //                                                ^^^ имя из аннотации, чище
    
  2. Явный конструктор лучше из-за визуальной симметрии в match:

    match @buckets[idx] {
        Occupied { value } => Some(value)        // лучше Some(value)
        _                  => None               // ← unit, не coerce'ится
    }
    // С coercion: `value` слева, `None` справа — асимметрично, читать сложнее.
    
  3. Явный конструктор лучше в let без аннотации:

    ro ip_value = if e.ip != "" { Some(e.ip) } else { None }
    // нет аннотации — coercion не работает; нужны явные Some/None.
    
  4. Спорно — { {...} } после else:

    else { Money { amount: a + b, currency: c } }    // явный
    else { { amount: a + b, currency: c } }           // coerce — `{ {...}}` шумно
    

Альтернативы политики:

A. Permissive (текущее). Программист сам выбирает — coercion или явно. D55 разрешает оба.

  • Плюс: гибкость, читаемость per-case.
  • Минус: inconsistency — в кодовой базе одно и то же пишется по-разному. Code review устаёт. LLM генерирует то так, то так.

B. Style guide (рекомендация). D55 разрешает оба, но стиль рекомендует одну форму:

  • expression-body return: предпочитать coercion (короче).
  • match-веточки с unit-альтернативой (Some/None): явные конструкторы (visual symmetry).
  • let без аннотации: явный конструктор (других вариантов нет).
  • Сложные nested-литералы ({ {...} }): явный для ясности.

Это не правило компилятора, а guideline для nova fmt/линтера и code review.

C. Mandatory coercion. Запретить явный конструктор там, где coercion применим — компилятор ругается «излишняя обёртка». Жёсткая форма, единая.

  • Плюс: zero ambiguity, единый стиль везде.
  • Минус: ломает практичность (case 3 выше — нет аннотации, нельзя без Some), требует разрешения для let без аннотации.

D. Mandatory explicit. Запретить coercion — программист всегда пишет имя.

  • Плюс: explicit always.
  • Минус: убивает D55 целиком, теряем эргономику prelude-типов.

Решение пока: A (permissive). При работе над nova fmt/style guide вернуться к B — формализовать рекомендации. C/D — слишком жёстко, ограничивает практический код.

Тонкости для guideline (если введём B):

  • expression-body с явным -> T: предпочитать coercion (короче).
  • let x T = ... с аннотацией: предпочитать coercion.
  • let x = ... без аннотации: явный конструктор обязателен.
  • match-arms: unit-варианты (None, Empty) не coerce’ятся, для визуальной симметрии писать все ветки с явным конструктором.
  • { {...} } (block + record-литерал): писать явный имя для ясности (избегать визуально шумного { {...}}).
  • call-site аргументы коллекций: [42, "alice"] для []SqlValue — coercion лучше (нет [I(42), S("alice")]-шума).
  • nested coercion: let r Result[User, str] = { id: 2, name: "Bob" } — двойная coercion (record + sum), явный был бы Ok(User { ... }). Coercion значительно короче.

Связь: D55, Q-anonymous-union, Q-numeric-coercion (связаны), Q9 (style guide как часть tooling в v1.0).


Q-array-api. Формальный API []T — что встроено, что расширяется

CLOSED by D38 → «Built-in API для []T» (Plan 17 Ф.1, 2026-05-08): зафиксирован минимальный built-in API (len/cap/is_empty, [i]/get, push/pop, iter/for, static-конструкторы new/with_capacity/filled) и список текущих stdlib extensions (map/filter/fold/any/all/ first/last). Slicing xs[a..b] остаётся отложенным (Q-array-slicing). Embed use []T — разрешён по D39.

Update 2026-05-28 (Plan 91.7, D181): Все mut-методы (push/reserve/truncate/fill/copy_from/extend_from/ insert_from/copy_within) теперь возвращают @ (fluent chain, D131). @slice(from, to) удалён (Plan 96.1 — arr[a..b] единственный путь). []T.new() / []T.with_capacity(n) подтверждены как canonical (D372). Generic [T Ord] @sort() / @min / @max / @binary_search — followup [M-91.7-sort-generic].

Update 2026-06-08 (Plan 91 Ф.4+Ф.6 CLOSED): time/math/sort conformance (nova_tests/plan91_fe4/ 10/0 — 8 pos + guard + neg) + getting-started (examples/getting_started.nv). Instant — НЕ отдельный тип: ships как Monotonic (D124). Handler-lit capture codegen-фикс (emit_c.rs) — sort+duration CC-FAIL устранён. Новых D/Q нет. 5 pre-existing codegen followup-маркеров — в plan-91 Ф.4/Ф.6 closure: [M-91.6-spawn-global-const-capture], [M-91.6-time-now-schema-mismatch], [M-91.6-duration-zero-cross-module-const], [M-91.6-parallel-for-value-typing], [M-91.6-sqlite-ffi-codegen].

Update 2026-06-08 (Plan 91 Ф.1 CLOSED — все MVP-фазы закрыты): HashMap/Set/vec-combinators работают; codegen-фикс fold/HOF с method-level-generic accumulator ≠ типу элемента (fold int→str/int→bool — ClosureLight-arg не получал substituted param-типы; emit_c.rs instance ~20675 + static ~21949). nova_tests/plan91_fe1/ 10/0. Новых D/Q нет. 4 followup-маркера в plan-91 Ф.1 closure: [M-91.1-composite-array-storage] (P1, map int→record/Option — []U хардкод NovaArray_nova_int), [M-91.1-method-turbofish-dispatch] (P2, xs.map[int] codegen drop), [M-91.1-set-from-iter-iterable-param] (P2), [M-91.1-dead-arrayext-mono-path] (P3).

Update 2026-06-08 (Ф.1 followups — 3 из 4 ЗАКРЫТЫ): Новых D/Q не требуется (всё — codegen-correctness).

  • [M-91.1-method-turbofish-dispatch]obj.method[U](...) (Call{TurboFish{base:Member}}) теперь перехватывается в начале emit_call: stash type_args → recurse на Member base → resolve_method_level_subst сидирует слоты. Turbofish и inferred сходятся на один mono. Поле current_method_turbofish.
  • [M-91.1-set-from-iter-iterable-param]Set.from_iter теперь принимает конкретный []T (зеркало HashMap.from([](K,V))); generic-протокол Iterable[T] стирался в void*, for-in не мог восстановить итератор.
  • [M-91.1-composite-array-storage]ВАЖНО: дизайн в плане был неверен. «typed-storage» (real NovaArray_<Composite> struct) ломает 47 тестов: весь stdlib (HashMap-бакеты, tuple, JSON) держится на int64-erasure + side-channel array_element_types (var→real elem C-type). Правильный фикс — завершить side-channel, а не менять storage: (1) name-alignment call-site↔body; (2) propagation реального элемент-типа через generic map/filter в array_element_types; (3) .get()/infer перепаковывают NovaOpt_nova_intNovaOpt_<elem> (NPO); (4) composite-receiver: closure-param re-type. []record/[]sum теперь полностью годны через index/for-in/get. 0 blast radius (доказано baseline diff). Обобщает [M-59.1-array-of-mono-tuple].
  • [M-91.1-dead-arrayext-mono-path] (WON’T FIX) — путь ЖИВОЙ. Probe-panic сработал на type_name="[]T" method="my_filter" (plan100_4_5): пользовательские fn[T] []T @method не регистрируются в external_registry → live sentinel не перехватывает. Удалять нельзя.
  • 🟡 NEW [M-91.1-value-struct-array-elem] (P2) — []Option[T]/[]tuple-by-value (value-struct элементы, не pointer): erasure не вмещает >8 байт, side-channel readback не покрывает. Требует typed-storage именно для value-struct (узкий случай) ИЛИ box-to-pointer. Pre-existing лимит (падал и на baseline), не регрессия.

Update 2026-06-10 (Plan 138.1 Ф.0-Ф.6 CLOSED, D239 partial):

  • [M-91.1-value-struct-array-elem]ЗАКРЫТ через Plan 138.1 typed-storage: в юнитах с import std.collections.vec_owned.{Vec} массив-литерал [...] и тип []Option[int]/[]tuple резолвятся в Vec[T] (typed *mut T storage, без int64-erasure). vec_nested_pos, vec_record_elem_pos, vec_tuple_elem_pos, vec_option_elem_pos, vec_mutate_pos — all PASS (plan131 27/1 vs baseline 23/5). Универсальный flip (Vec-free юниты) → Plan 138.2.
  • as_slice() удалён из Vec[T] API (Plan 138.1 Ф.0.1a): view → v[..] (через Index[Range, Vec[T]]), копия → clone(). to_array() также не добавлен (при []TVec[T] это clone()). API таблица []T ниже остаётся валидной для компилятор-встроенных методов (NovaArray путь); Vec[T] методы — в vec_owned.nv.
  • xs[a..b] slicing — ЗАКРЫТО: @index(Range) как Nova-метод (Index[Range, Vec[T]]), panic на OOB. xs.get(r Range) -> Option[Vec[T]] — safe вариант. Zero-copy view: interior pointer в GC-tracked буфере; push на view → silent detach от родителя.

Контекст. []T — встроенная конструкция языка (D27). По D32 runtime-представление — (ptr, len, cap)-структура. В примерах spec/ и examples/ используются: xs.len, xs.push(x), []T.with_capacity(n), xs.iter(), и т.д. Но формального D-решения про API []T нет — это используется «по умолчанию», без зафиксированного списка.

Вопросы:

Q-array-api.1. Что входит в API []T

Из практики и примеров видны следующие операции — нужно зафиксировать полный список:

Геттеры:

  • xs.len — количество элементов (поле или метод? сейчас как поле).
  • xs.cap — capacity (выделенная память).
  • xs.is_emptylen == 0 (для удобства).

Конструкторы (static-функции на типе []T):

  • []T.with_capacity(n int) -> []T — выделить с capacity n, len 0.
  • []T.alloc(n int) -> []T — выделить с len n (заполнено default-T). Не уверен, что зафиксировано. См. Q-array-api.4.
  • []T.from(other []T) -> []T — копия (shallow clone).

Мутирующие:

  • mut xs.push(item T) -> () — добавить в конец, grow при переполнении.
  • mut xs.pop() -> Option[T] — удалить с конца.
  • mut xs.clear() -> () — обнулить len, capacity сохранить.
  • mut xs.insert(i int, item T) -> () — вставить по индексу.
  • mut xs.remove(i int) -> Option[T] — удалить по индексу.

Итерация:

  • xs.iter() -> Iter[T] — итератор по элементам.
  • for x in xs { ... } — синтаксический сахар над iter().

Доступ:

  • xs[i] — индексирование, panic при out-of-bounds (D13).
  • xs.get(i int) -> Option[T] — безопасный доступ.

Slicing (если есть):

  • xs[a..b] — slice. Возвращает []T без копирования (zero-cost). Не зафиксировано.

Q-array-api.2. Можно ли расширять []T методами через fn []T @custom()

Да — программист может объявить собственный метод на []T, как на любом типе:

fn []T @sum_int() -> int where T = int =>     // bound пока нет, см. Q-bounds
    @fold(0) { (acc, x) => acc + x }

fn []f64 @average() -> f64 =>
    @fold(0.0) { (a, x) => a + x } / (@len as f64)

Это валидно по D35 — методы на типе через fn Type @method. []T — тип, расширение работает. Нужно зафиксировать формально, что встроенные конструкции (массивы, tuples) подлежат расширению так же, как именованные типы.

Q-array-api.3. use []T в record (D39 на встроенные типы)

Может ли record-тип использовать use []T для прокси-делегации?

type Holder[T] {
    use data []T
    extra str
}

ro h = Holder[int] { data: [1, 2, 3], extra: "info" }
ro n = h.len             // прокси к data.len через D39
h.push(42)                 // прокси к data.push

D39 написан под именованные типы (use Account). Распространение на встроенные конструкции ([]T, tuples) — естественное расширение, но не зафиксировано формально.

См. clarification в D39.

Q-array-api.4. []T.alloc(n) vs []T.with_capacity(n) — разница

Из текущих примеров используются оба:

Если оба существуют:

  • with_capacity(n) — len=0, cap=n. Push не реаллокирует, пока len < cap.
  • alloc(n) — len=n, cap=n. Все элементы инициализированы default-T.

Для default-T нужен механизм default-значения. Либо Default-protocol, либо требование bound’а на T. Не зафиксировано.

Возможно, alloc(n) вообще не нужен — пользовательские структуры (HashMap, Vec) делают with_capacity и заполняют сами.

Q-array-api.5. Slicing — есть ли xs[a..b] ✅ ЗАКРЫТО Plan 96 / D144

Range-индексирование xs[a..b] реализовано Plan 96 (2026-05-23) — sub-slice view, без копии backing. 5 форм Range (Rust RangeBounds parity): a..b/a..=b/a../..b/... Также str[a..b] (codepoint-indexed, panic при OOB). Полная семантика — D144.

Решение пока

Не вводить отдельный D — это часть Q9 (stdlib). При работе над stdlib уточнить полный API []T и зафиксировать одним D-блоком.

Сейчас: считать API []T де-факто включающим len/cap/push/pop/with_capacity/iter/get/[i] (по текущим примерам). Расширение через fn []T @method разрешено по D35. Embed use []T в record — clarification в D39.

Связь: D27 (синтаксис массивов), D32 (runtime-представление), D35 (методы на типе), D38 (turbofish для конструкторов), D39 (use-delegation), Q9 (stdlib), Q-bounds (где []T методы используют T-constraint’ы).


Q-embed-syntax. Embed-keyword — use vs альтернативы

Контекст. D39 (revised) фиксирует embed через use name Type — alias-имя поля обязательно:

type AuditedAccount {
    use account Account                // имя поля = "account"
    audit_log []AuditEntry
}

Этот вопрос — про выбор keyword’а (use), не про обязательность имени (она зафиксирована в D39).

use — multi-purpose: и для embed здесь, и потенциально для импортов/локальных алиасов в будущем (D29 использует import, но use тоже рассматривался — например, как Rust use std::io). Это создаёт перегрузку семантики keyword’а.

Альтернативы

A. Текущий D39 — use name Type

type AuditedAccount {
    use account Account
    audit_log []AuditEntry
}
type Wrapper { use w HashMapIter[K, V] }

За: проверенный, кода уже написано. Против: use многозначен (embed, потенциально импорты, scope- локальные aliases).

B. Go-style — голый тип без keyword (с обязательным alias)

После нового D39 имя поля обязательно везде, поэтому Go-style без keyword’а выглядел бы так:

type AuditedAccount {
    account Account                    // обычная запись поля!
    audit_log []AuditEntry
}

Но это превращает embed в обычное поле — синтаксически неотличимо. Для активации delegation нужен специальный токен. Голый тип (Go-style без keyword’а и без alias’а) несовместим с обязательным alias’ом из D39 — теряется единственный синтаксический маркер «это embed, а не обычное поле».

Чтобы спасти этот вариант, нужен какой-то маркер:

type AuditedAccount {
    account = Account                  // `=` как маркер embed?
    audit_log []AuditEntry
}

Но = уже занят (присваивание в let, alias в type X alias Y). Нет хорошего символа.

Против: обязательность alias’а из D39 сделала Go-style не применимым без явного keyword’а. Голый embed теряет различие с обычным полем.

C. embed name Type — отдельный keyword

type AuditedAccount {
    embed account Account
    audit_log []AuditEntry
}
type Wrapper { embed w HashMapIter[K, V] }

За:

  • Keyword точно описывает намерение. embed однозначен, use — общий.
  • Освобождает use для других целей (scope-aliases, импорты в блоке).
  • AI-locality высокая.

Против:

  • Ещё один keyword в языке.
  • Очень похоже на A синтаксически — выигрыш только в семантической точности слова (одна роль вместо потенциально нескольких).

Отвергнутые альтернативы

  • name : Type через : — конфликт с D17 (Nova явно отвергла : в type annotations).
  • Type + {...} (intersection) — конфликт с D46 operator overloading (+ = @plus).
  • extends Type — обещает наследование (Java/C# семантика), а D39 — delegation, не наследование. Вводит в заблуждение.
  • ~Type — конфликт с removed memory prefix (~T/~&T из отменённого D21), путает ветеранов.
  • @embed Type@ уже значит self-method/field в D35, перегрузка значения.
  • +Type — конфликт с унарным +, не принято в mainstream.

Сравнение топ-3

АспектA. use TypeB. голый TypeC. embed Type
Keyworduse (multi-purpose)(нет)embed (специфичный)
Длинасредняякороткаясредняя
ПрецедентD mixin, partial RustGoOCaml include (схожая идея)
AI-localityвысокаясредняявысокая
Парсерпрямолинейныйlookahead по caseпрямолинейный
use нужен для импортов?да, занятсвободенсвободен

Решение пока

Не менять. D39 принят, кода с use написано (примеры, decisions). Менять синтаксис без сильного триггера — лишний breaking change.

Update 2026-05-08: D39 расширен формой use _ Type (anonymous embed) для simple wrappers где явный alias бессмысленный. Это снимает часть давления на keyword use — программист не вынужден придумывать имя поля каждый раз. Q-embed-syntax по-прежнему открыт про выбор use vs embed keyword’а, но anonymous form закрыла главный pain-point обязательного alias’а в simple cases.

Реализация anonymous embed — Plan 11 Ф.9 (через override-precedence в общем overload-resolution, lazy mechanism).

Если возвращаться — мой собственный голос за C (embed Type):

  • Точная семантика keyword’а («embed» однозначно говорит «встроить», тогда как use это и многое другое).
  • Освобождает use для других целей (потенциально — local-aliases типов, импорты в скоупе функции).
  • Совместимо с alias-формой через as в Go-style — при желании программиста.

Триггеры для пересмотра:

  • Если в реальном Nova-коде накопится боль от перегрузки use (программист путает embed и импорт).
  • Если use потребуется для другой семантики (например, using-statement из C# для эффект-handler’ов в with-альтернативе).

Связь: D39, D29 (импорты — потенциальный второй пользователь use), D17 (record-форма), D52 (kind-токены — embed встал бы наряду с alias).


Q-positional-partial-pattern. .. для позиционных конструкторов sum ✅ ЗАКРЫТО (D59)

D59 формализовал partial-pattern .. для трёх контекстов одновременно — record ({ field, .. }, наследие D17/D52), позиционные конструкторы sum (Cons(..), Move(x, ..)) и массивы ([head, ..], [a, .., z]). Единый .. маркер «остальные элементы игнорируются».

Также формализованы array-patterns ([], [r], [a, b], slice- bind [head, ..rest]) и tuple-patterns ((a, b), (a, _, c), destructuring let).

Контекст ниже сохранён как историческая справка.


Исходный контекст Q-positional-partial-pattern

Контекст. D17 фиксирует partial pattern matching только для record-формы:

type Shape | Circle { radius f64 } | Square { side f64 }

match shape {
    Circle { radius, .. } => 3.14 * radius * radius      // .. — остальные поля
    Circle { radius }     => 3.14 * radius * radius      // эквивалент
}

Для позиционных конструкторов (Cons(T, LinkedList[T]), Click(int, int), etc.) текущий синтаксис требует placeholder для каждого поля:

type LinkedList[T] | Empty | Cons(T, LinkedList[T])

match list {
    Empty       => true
    Cons(_, _)  => false              // два `_` для двух полей
}

При большем числе полей растёт шум: Click(int, int) | Move(int, int, int) | Scroll(int)Click(_, _), Move(_, _, _), Scroll(_). Программист пишет «не интересуют поля» N раз.

Предложение

Расширить .. partial-pattern на позиционные конструкторы:

match list {
    Empty     => true
    Cons(..)  => false              // partial: все поля игнорируются
}

match event {
    Click(..)   => "click"           // не важны координаты
    Move(x, ..) => "move at ${x}"    // важна только первая
    _           => "other"
}

Правила (предлагаемые):

  1. Cons(..) — все поля игнорируются (как Cons(_, _) сейчас).
  2. Move(x, ..) — первое поле в bind, остальные игнорируются.
  3. Move(.., z) — последнее в bind, начальные игнорируются.
  4. Move(x, .., z) — первое и последнее, среднее игнорируется.

Прецеденты

  • Rust: .. работает в обеих формах (Variant(_, _) и Variant(..)), Variant(x, ..)/Variant(.., x) тоже.
  • Swift: case .variant(_, _, _) явно, .. нет — все поля прописываются.
  • OCaml: Cons (_, _) явный wildcard, нет .. для tuple.
  • Haskell: wildcard _ для каждого поля.

Rust — единственный mainstream-прецедент. Но Rust-сообщество любит .. — частая идиома.

Цена

  1. Новая форма pattern. Парсер должен различать Variant(..), Variant(x, ..), Variant(.., x), Variant(x, .., y).
  2. Конфликт с record-формой ... В { field, .. } .. стоит после ,. В позиционной (x, ..) тоже после ,. Парсер различает по виду внешних скобок ({} vs ()), что согласовано с D17.
  3. Тонкость с одним полем: Variant(..) для конструктора с одним полем эквивалентно Variant(_). Скорее всего разрешено.

Решение пока

Не вводить формально. Текущий код использует Cons(..) идиому неформально (examples/stdlib_linkedlist.nv) — ожидая, что D17/D52 будет расширен. До формализации Cons(..) фактически работает по интуитивному правилу «.. означает “остальное игнорируется”», но компилятор может потребовать строгую форму D17. При работе над парсером — зафиксировать в revision к D17 или D52.

Связь: D17 (partial pattern для record), D52 (sum-варианты), D19 (match-arms через =>).


Q-static-method-protocol. Static-методы в protocol через .name()-префикс

РЕШЕНО 2026-05-22 (Plan 97). Принято предложение из этого вопроса: static-методы в type X protocol { ... } маркируются точка-префиксом .name(), по симметрии с D35 (fn Type.name(...) в реализации). Формализовано в D143 (parse rule, matching rules, backwards-compat: bare имя — instance, как было). Реализовано в Plan 97 Ф.1 (parser + AST EffectMethod.is_static). Применено в prelude: From[T] { .from(t T) }, TryFrom[T, E] { .try_from(t T) -> Result[Self, E] } (см. std/prelude/protocols.nv).

Note on hard runtime-enforcement: парсер принимает .name(), AST хранит is_static: bool. Type-checker строгое сопоставление static↔instance при satisfaction-проверке — deferred (Plan 15 Ф.5+ или отдельный follow-up). См. docs/simplifications.md#m-protocol-static-enforcement-deferred.

Историческое DEFER (предыдущее) — снято: Plan 15 закрыт, Plan 59 мономорфизировал Result, что покрывает основные use-case’ы (From/Into/TryFrom/TryInto).

Контекст. D42/D53 описывают protocol с instance-методами (без префикса):

type Hashable protocol {
    hash() -> u64
    eq(other Self) -> bool
}

Реализация — через D35 @-методы.

Для static-функций (конструкторов, factory-функций) protocol сейчас не предусмотрен. Это блокирует, например, generic collect:

type FromIter[T] protocol {
    .from_iter(it Iter[T]) -> Self      // static-функция-конструктор
}

fn Iter[T] @collect[Out: FromIter[T]]() -> Out =>
    Out.from_iter(@)

Предложение

Расширить protocol-синтаксис: точка-префикс (.name()) маркирует static-функцию (по симметрии с D35 fn Type.name(...) — точка в реализации).

type FromIter[T] protocol {
    .from_iter(it Iter[T]) -> Self      // static — через точку
    @count() -> int                      // instance (если нужен @ symmetry,
                                          //  Q-protocol-method-prefix)
    method() -> bool                      // instance (текущее, без префикса)
}

Реализация (структурно):

type Vec[T] { data []T }
fn Vec[T].from_iter(it Iter[T]) -> Vec[T] => ...

// Vec[T] автоматически удовлетворяет FromIter[T]

Минусы

  • Тонкость грамматики: точка в protocol-блоке как маркер.
  • Связано с Q-collect-mechanism — без bound’ов на дженериках (Q-bounds) generic-collect не работает даже со static-protocol.
  • Self в protocol — концепция уже есть, но в static-контексте означает «конкретный реализующий тип» (как Swift Self в protocol).

Решение пока

Не вводить. Когда понадобится collect/from_iter-style generic- конструкторы — вернуться. Связано с Q-bounds, Q-collect-mechanism.

Связь: D35 (точка для static), D42 (protocol), D53, Q-bounds, Q-collect-mechanism, Q-protocol-method-prefix.


Q-protocol-method-prefix. @method() vs голое method() в protocol-объявлении

CLOSED by D53 → «Method-prefix в protocol-блоке» (Plan 17 Ф.1, 2026-05-08): обе формы валидны и эквивалентны. @method() для визуальной симметрии с реализацией; голое method() для краткости. mut @method() обязательно с @. Bootstrap парсит обе формы; std/testing/property.nv использует голую.

Update 2026-05-29 (Plan 91.8a, D183): Default body в protocol method — новая фича: метод с телом (=> expr или { ... }) — default impl, implementer может override. Метод без тела — abstract (обязательно реализовать). Также renames: Iter→Iterable, Display→Printable, Equatable.eq→equals, Comparable.cmp→compare -> int. Ordering sum-type удалён.

Контекст. Сейчас в protocol-блоке instance-методы пишутся без префикса:

type Hashable protocol {
    hash() -> u64                    // instance, без префикса
    equals(other Self) -> bool       // (D183: было `eq`)
}

В реализации — с @:

fn User @hash() -> u64 => ...

Асимметрия: declaration без @, definition с @. Программист мысленно сопоставляет.

Предложение

@ обязателен и в protocol-объявлении — для полной симметрии:

type Hashable protocol {
    @hash() -> u64                   // instance — @, как в реализации
    @eq(other Self) -> bool
    .new() -> Self                    // static — точка (Q-static-method-protocol)
    mut @push(item T) -> ()           // mut instance
}

За

  • Полная симметрия declaration ↔ definition.
  • Меньше неявности — программист не помнит, что без префикса в protocol = instance.
  • AI-friendly — точно как реализация.

Против

  • Breaking change — все 16+ protocol-объявлений переписать.
  • Шум@hash() чуть длиннее hash().
  • В существующих языках (Swift, Rust, Kotlin) protocol/trait не использует self-маркер в declaration — convention.

Решение пока

Не менять. Текущая асимметрия живёт. Программист привыкает. Возможен пересмотр вместе с Q-static-method-protocol — если вводим точку для static, можно добавить @ для instance ради консистентности.

Связь: D42, D35, Q-static-method-protocol.


Q-collect-mechanism. Generic collection construction

Контекст. В Rust:

let v: Vec<i32> = (0..5).collect();
let s: HashSet<i32> = (0..5).collect();

Один метод collect, целевой тип выводится из контекста или передаётся через turbofish (collect::<Vec<_>>()). Универсальный collection-builder.

В Nova через D58 Iter[T] есть, но универсальный collect не работает без:

  1. Bound’ов на дженерикахOut: FromIter[T] (Q-bounds, отвергнуты в MVP).
  2. Static-method в protocolFromIter[T] { .from_iter(...) -> Self } (Q-static-method-protocol).
  3. Type-as-value или turbofish для передачи целевого типа.

Альтернативы

A. Конкретные методы — без collect

fn Range @to_vec() -> []int
fn Range @to_set() -> Set[int]
fn Range @to_linked_list() -> LinkedList[int]

N методов для N целей. Простой, рабочий, без bound’ов.

B. Turbofish + bound (Rust-style)

fn Iter[T] @collect[Out]() -> Out where Out: FromIter[T] => ...
ro v = (0..5).collect[[]int]()

Требует Q-bounds + Q-static-method-protocol.

C. Type-as-value (Swift-style)

ro v = (0..5).collect([]int)        // []int как «type-callable»

Тип в позиции аргумента вызывает type’s from_iter. Требует Q-type-as-value.

D. Передача функции явно

ro v = (0..5).collect(([]int).from_iter)

Длинно, но без новых концепций.

Решение пока

A в MVP — конкретные to_vec, to_set, etc. на каждом типе- итераторе. B/C/D — после Q-bounds/Q-static-method-protocol/Q-type- as-value.

Связь: Q-bounds, Q-static-method-protocol, Q-type-as-value, D58.


Q-type-as-value. Передача типа как значения (xs.collect([]int))

Контекст. В Swift:

let v = Array(0..<5)              // Array — «type как callable»

Тип-имя в позиции функции — вызывает соответствующий init.

В Nova сейчас типы — compile-time сущности. Передавать []int как значение в ()-аргументе не работает:

fn collect[Out](ctor SomeProtocol) -> Out => ...
collect([]int)                    // []int это тип, не значение — ошибка

Turbofish работает (collect[[]int]()), но передача в ()-аргументе требует механизма «type as callable».

Предложение

Type в позиции выражения вызывает соответствующий конструктор по convention:

  • []int = type-callable, эквивалентно []int.from_iter или []int.new (выбор по сигнатуре).
  • User = type-callable, эквивалентно User.new или общему конструктору.

Минусы

  • Type-resolution полнее. Какой конструктор выбирается — from_iter? new? Зависит от target-типа в позиции? Сложно.
  • Прецеденты ограничены — Swift, Python (list(...)), но не Rust/ Kotlin/Go.
  • Type-as-value в runtime — требует runtime-tag типа (как Swift Mirror, Java reflection).

Решение пока

Не вводить. Если когда-то понадобится для эргономики collect — вернуться вместе с Q-collect-mechanism.

Связь: Q-collect-mechanism, D38 (turbofish — текущая альтернатива).


Q-range-extras. Reverse и step для Range

Контекст. D58 ввёл базовый Range (a..b, a..=b). Не зафиксировано:

  1. Reverse range5..0 (start > end). Что значит:
    • Пустой range (Rust-style — для прямого направления)?
    • Идущий назад (5, 4, 3, 2, 1, 0)?
  2. Step — итерация с шагом, (0..100).step(10) или 0..100..10?

Прецеденты

  • Rust: 5..0 — пустой; reverse через (0..5).rev(). Step через step_by(n).
  • Python: range(5, 0) — пустой. range(5, 0, -1) — обратный. Step через третий аргумент.
  • Kotlin: 5 downTo 0 — отдельный keyword. Step через step(n).
  • Swift: stride(from: 0, to: 100, by: 10) — отдельная функция.

Решение пока

Не зафиксировано. Реализуется в examples/stdlib_range.nv как методы:

fn Range @reverse() -> Range
fn Range @step(n int) -> StepIter

Конкретный синтаксис — после первой версии Range (см. examples).

Связь: D58.


Q-resume-semantics. Семантика resume в handler-method’е ✅ ЗАКРЫТО (D61)

Закрыт через D61 в варианте (II) tail-only: resume как keyword отвергнут, заменён на комбинацию return v (нормальное завершение, continuation возобновляется) + interrupt v (досрочное прерывание with-блока). Линейность — one-shot. Multi-shot отложен под Q-multishot-resume (если когда-нибудь потребуется backtracking-эффект). Для never-операций разрешён только interrupt. См. D61 «Алгоритм компиляции/интерпретации эффектов» — пошаговое тех-задание имплементатору.

Оригинальный текст ниже сохранён для истории.

Контекст. Все handler-литералы в spec’е и examples массово используют resume(value) для возобновления континуации операции (query(q) => resume(real.query(q)), log(msg) { ... ; resume(()) }). Но формального D-блока про resume не существует — только фрагментарные упоминания в D10 и D31. Программист, читающий spec, не знает:

  1. Что формально означает resume(v)? Возвращает ли он что-то?
  2. One-shot или multi-shot? Можно ли вызвать resume дважды? Что произойдёт?
  3. Тип resume. fn(R) -> () или fn(R) -> T_with_block?
  4. Что если resume не вызван? handler возвращает за весь with-блок? Какое значение?
  5. Запрещён ли resume для never-операций (Fail.throw)?
  6. resume() без аргумента для unit-операций — сахар или обязательная форма?

Ключевые design choices

One-shot vs multi-shot

МодельПрецедентСтоимостьUse-cases
One-shotKoka, OCaml 5, Effдёшево, stack-basedFail, Db, Log, Time, Random — 95% реальных эффектов
Multi-shotMulticore-OCaml лаб.дорого, копирование континуацииbacktracking, недетерминизм, choose-effect

Nova — backend-язык, фокус на надёжности и производительности. Склонность — one-shot: вызвал resume дважды → runtime panic (или compile-time error, если static-анализ позволит).

Тип resume

(I) fn(R) -> () — handler-method заканчивается, значение v становится результатом операции в бизнес-коде. (II) fn(R) -> T_with_blockresume возвращает финальное значение всего with-блока, позволяя писать «после resume» (например, log время выполнения).

(II) мощнее, (I) проще. Koka использует (II).

Без resume

Если handler-method не вызвал resume — handler возвращает за весь with-блок. Это типичный паттерн для Fail:

fn try_parse(s str) -> Option[int] =>
    with Fail[ParseError] = |_| interrupt None {
        Some(parse(s)!!)
    }

Здесь |_| interrupt None — handler-лямбда. Если бизнес-код бросает — handler возвращает None, и весь with-блок даёт None.

never-операции

Fail.throw имеет тип never (нет возвратного значения). resume для неё запрещён — нечего возобновлять. Линтер/тайпчекер должен запретить.

Unit-аргумент

Для операции () -> () (log(msg) -> ()) resume принимает ():

log(msg) { println(msg); resume(()) }

Можно сделать resume() без аргумента — синтаксический сахар. Решение зависит от того, насколько часто такое встречается (Log.log, Time.sleep — частые).

Что нужно зафиксировать в D-блоке

  • Линейность (one-shot recommended).
  • Точный тип resume.
  • Поведение если не вызван.
  • Запрет для never.
  • Поведение для unit-операций.
  • Что происходит при второй попытке вызова (panic vs compile-error).
  • Пример каждой формы.

Решение пока

Не зафиксировано. Все примеры используют resume(...) интуитивно по принципу «возвращает значение в место операции». До D-блока — это имплицитная семантика, нужная для понимания handler’ов. Обсудить и записать в виде D-блока (вероятно, D61 после D60).

Связь: D10, D31, 04-effects.md.


Q-handler-method-param-inference. Тип параметра handler-method’а ✅ ЗАКРЫТО (D61)

Закрыт через D61 в варианте (A) inference обязателен по умолчанию, явные типы разрешены опционально. Параметры handler-method’а биндятся по позиции к параметрам декларации операции; типы автоматически выводятся из effect-декларации. Можно писать query(q Sql) => ... для документации, но это избыточно.

Оригинальный текст ниже сохранён для истории.

Контекст. Сейчас в handler-литералах параметры пишутся без типа:

with Db = Db {
    query(q) => resume(real.query(q))     // q: Sql выводится из protocol
    exec(q)  { staged.push(q); resume(()) }
}

Тип q (а также sql, args в старой форме (sql, args)) выводится из сигнатуры protocol’а Db.query(q Sql) -> []DbRow. То же самое делает лямбда: (req) => handle(req) получает тип req из контекста-параметра.

Вопрос: должна ли спека разрешать инференс, или требовать явный тип в handler-method’е?

Аргументы

За инференс (текущая практика во всех ~20 примерах):

  • handler-method всегда вызывается через protocol — типы фиксированы. Дублировать в каждом литерале — шум.
  • Симметрия с лямбдой.
  • Все примеры в spec/examples написаны без типов; требование явных типов — большой sweep.

Против:

  • Локальное чтение хуже: query(q) { use(q) } — непонятно q : ?.
  • AI-first: LLM проще генерирует с явными типами (меньше неочевидного контекста).
  • D45 inferred return type — там inference только для return, параметры всегда явные. Handler-method был бы исключением.

Возможные варианты

(A) Инференс обязателен — типы из protocol-сигнатуры всегда выводятся, явные типы запрещены (избыточны). (B) Инференс опционаленquery(q) и query(q Sql) оба валидны. Линтер может предлагать опускать. (C) Явные типы обязательныquery(q Sql) всегда. Sweep всех примеров.

(B) самый гибкий, но создаёт «два пути»; (A) самый компактный; (C) самый локально-читаемый.

Решение пока

Не зафиксировано. Все примеры используют (A)-форму неявно. До формального решения работает «инференс из protocol-сигнатуры», но это нужно явно зафиксировать в D-блоке (вероятно, в составе D31 или отдельным расширением).

Связь: D31, D45 (inferred return type — прецедент, но только для return).


Q-fail-coercion. Auto-coercion типов ошибок при ?-операторе

DEFERRED — нужен дизайн (Plan 17 Ф.3, 2026-05-08). Rationale: проблема known (без auto-coerce каждое разнотипное ? требует ручной .map_err(AppError.Variant)?), но дизайн нетривиален: вариант через unary-конструктор «один подходящий wrap» — близкий аналог Rust From<E> через D73, но взаимодействие с D55 sum-coercion требует точной формализации (что приоритетнее: явный .map_err или auto-wrap?). Trigger: реальная боль в stdlib — когда ≥5 функций требуют .map_err(...)? boilerplate и pattern регулярен (один wrap-конструктор). Варианты решения перечислены ниже (auto-derive From через #[from]-маркер; явный ?.into(); sum-type AppError с Or<A, B>).

Контекст. D65 фиксирует семантику Fail[E]. При транзитивном пробросе через ? если callee бросает E', а caller декларировал Fail[E] (E ≠ E’) — программист обязан явно использовать .map_err(...) или multi-Fail в row.

type AppError
    | Parse(ParseError)
    | Db(DbError)

fn process(s str) Fail[AppError] -> int {
    ro n = parse(s).map_err(AppError.Parse)?      // явный wrap
    Db.query(...).map_err(AppError.Db)?            // явный wrap
}

В Rust есть From<E>-trait, через который ? автоматически конвертирует тип ошибки если есть имплементация. Для Nova аналогичное правило могло бы быть:

Если E (caller’s Fail) имеет ровно один sum-вариант с типом E' (callee’s Fail), ? автоматически вызывает этот вариант-конструктор:

parse(s)?     // вместо parse(s).map_err(AppError.Parse)?

compile-time проверка: вариантов с типом E’ должно быть ровно один. Если несколько — ambiguous, compile error.

За

  • Убирает boilerplate .map_err(...) для типичных wrap’ов.
  • Прецедент Rust — программисты знают.
  • Compile-time проверка остаётся (один вариант — однозначно, иначе ошибка).

Против

  • Магия. По месту вызова parse(s)? неочевидно что происходит wrap.
  • AI-friendly? LLM может не знать про auto-coercion и путаться.
  • D40-style «один способ». Auto-coercion + явный .map_err — два способа, неоднозначность.
  • Локальное reasoning. С явным .map_err(AppError.Parse) сразу видно как ошибка маппится. С ? — нужно смотреть на тип AppError.

Решение пока

Не зафиксировано. Оставляется как потенциальная будущая фича. В текущем D65 — всегда явный .map_err(...)? или multi-Fail в row.

Связь: D65, D4, D25.


Q-pipe-operator. Pipe-оператор |>

DEFERRED to v0.5+ (Plan 17 Ф.3, 2026-05-08). Rationale: trailing-block (D43) + method-chain через @-методы (D35) покрывают паттерн «data flow» без |>. Добавление оператора требует решения о приоритете и ассоциативности, конкуренции с bitwise OR, и effect-row inference на partial-application — complexity без ясного выигрыша. Trigger для пересмотра: ≥3 use-case’а в реальной кодовой базе, где method chain или free-function call дают объективно худший читаемость, и где pipe был бы естественнее.

Контекст. Во многих функциональных языках (Elixir, F#, Elm, Hack, OCaml) есть pipe-оператор x |> f |> g, эквивалент g(f(x)). Делает цепочки трансформаций линейными слева-направо.

ro result = users
    |> filter(active)
    |> map(format_name)
    |> join(", ")

vs. без pipe:

ro result = join(map(filter(users, active), format_name), ", ")

или через method chaining (если каждая функция — метод):

ro result = users.filter(active).map(format_name).join(", ")

Статус. В bootstrap-парсере токена |> нет. В спеке тоже не упомянут. Был ли намеренно отвергнут или просто не дошли руки — не зафиксировано.

За

  • Линейная читаемость для трансформаций («data flow»).
  • Не требует чтобы функция была методом типа.
  • Прецедент в FP-сообществе.

Против

  • D40 «один способ». Уже есть method chaining — pipe это альтернатива, дублирующая тот же data-flow паттерн.
  • AI-friendly? Method chaining x.f().g() гораздо более распространён в LLM-обучении (Java/Python/JS/Rust). |> редкий, повышает cognitive load.
  • Эффекты + pipe. users |> get_users() |> ... — где effect-row? Pipe скрывает callee, осложняет вывод эффектов.
  • Партикулярно для Nova. Все типы могут иметь @-методы (D35), поэтому method chaining — universal pattern. Pipe не добавляет выразительности.

Решение пока

Не зафиксировано. Скорее всего не добавлять — есть method chaining через @-методы, а D40 призывает не дублировать паттерны. Если решим зафиксировать как «no» — отдельный D-блок «No pipe».

Связь: D35 (@-методы), D40 (один способ).


Q-string-interpolation. Интерполяция строк "hello ${name}"

CLOSED by D44 → «Строковые литералы и интерполяция» (Plan 17 Ф.4, 2026-05-08): синтаксис ${expr} (JS-style), escape \${ для буквального ${. Codegen эмитит StringBuilder цепочку с pre-size estimate (одна аллокация, без O(N²) от +). Реализован весь стек: lexer (sentinel \x01$ для escape), parser (sub-lex/parse каждого ${expr}), AST (ExprKind::InterpolatedStr { parts }), codegen (StringBuilder.append + into), interp.

Тесты: nova_tests/types/string_interpolation.nv (13 тестов: int / negative / str / bool / f64 / char literal / multi / expression in ${} / escape / большие строки).

Const-инициализатор: интерполяция запрещена (требует runtime StringBuilder); compile error.

Контекст. Многие современные языки имеют интерполяцию строк:

Контекст. Многие современные языки имеют интерполяцию строк:

JS: `Hello ${name}, you are ${age}`
Python f"Hello {name}, you are {age}"
Kotlin "Hello $name, you are $age"
Swift "Hello \(name)"
Rust println!("{name}")

В bootstrap-парсере Nova — конкатенация через +:

ro s = "Hello " + name + ", you are " + str.from(age)

Статус. Не зафиксировано в спеке. Bootstrap не парсит интерполяцию.

За

  • Читаемость, особенно для длинных строк.
  • Меньше boilerplate (+ и str.from(...)).
  • Универсальная фича — все программисты ожидают.

Против

  • Сложность парсера/лексера. Интерполяция — это string-mode + embedded expression mode. Усложняет грамматику.
  • Tagged templates (D-?) — есть планы на tag... ` с runtime’ом. Интерполяция и tagged templates — пересекаются (sql tag = Sql.eval с интерполированными частями?).
  • AI-friendly формат. ${...} — популярно (JS), но \(...) (Swift), {...} (Python) — все разные. Нужно выбрать один синтаксис.
  • Type coercion. "${n}" для int — implicit str.from(n) (D73)? Или требовать явный? (Plan 17 закрыл это: ${expr} — sugar над str.from(expr).)

Альтернативы синтаксиса

  1. "${expr}" — JS-style (привычно большинству)
  2. "\(expr)" — Swift (без $-конфликта со shell)
  3. "{expr}" — Python f-string (без префикса) или f"{expr}"

Решение пока

Не зафиксировано. Скорее всего нужно — современный язык без интерполяции выглядит архаично. Вопрос — какой синтаксис и как взаимодействует с tagged templates.

Связь: Tagged templates (нет D-блока пока), D40.


Q-coercion-order. Порядок применения coercion: sum vs record vs spread vs punning

Контекст. В Nova есть несколько форм неявных трансформаций литералов в позиции с явным типом:

  • D55 literal coercion — sum-конструкторы (E.Variant(...)) и record-литералы.
  • D60 spread[...arr, x] для массивов и { ...rec, k: v } для записей.
  • D52 field punning{ x, y }{ x: x, y: y } когда идентификаторы совпадают с именами полей.

Композиция этих правил в одном литерале может давать неоднозначность порядка применения:

ro p = { ...base, x }        // что сделать первым:
                              // (a) spread → record { ...base.fields, x: x }?
                              // (b) punning x → x: x → record { ...base.fields, x: x }?
                              // обычно эквивалентно, но не всегда
ro r RecordType = { ...base, value: 5 + 10 }
// 1. coerce 5 + 10 в тип поля value (sum-coercion если value: SomeOption)?
// 2. spread base?
// 3. record-construct?

Открытые пункты

  1. Формальный порядок — нужно зафиксировать sequence: spread → punning → field-coercion → sum-coercion (или другой).
  2. Многошаговая coercion: let r SomeRecord = { x: 5 } где SomeRecord.x: SomeSumType. Двушаговая: int → SomeVariant → field. Зафиксировано ли «не более одного уровня coercion»?
  3. Type checker order: bottom-up vs top-down — на какой стадии применяется coercion?

За

  • Без формального порядка LLM может генерировать «работает в одном направлении, не в другом».
  • D55 в большом примере (audit.nv, orm_demo.nv) активно использует comlex coercion — нужно правило.

Против

  • Может оказаться что в реальном коде эти кейсы редки.
  • Решение требует expert-внимания к type-checker дизайну (production компилятор).

Решение пока

Не зафиксировано. Bootstrap применяет правила «по одному за раз» (сначала spread, потом field-resolution, потом coercion внутри полей) — это implementation-факт. Production должен дать явное правило.

Связь: D55, D60, D52, Q-numeric-coercion, Q-style-coercion.


Q-pure-view. Семантика pure_view для handler-state в контрактах

Контекст. D24 упоминает pure_view как механизм ссылки на handler-state в контрактах:

fn transfer(...) Db -> ()
    ensures Db.balance(to) == old(Db.balance(to)) + amount
=> ...

«В v1.0 поддержка частичная — только для эффектов с явным pure_view (чистая проекция состояния handler’а). Полная поддержка — research, отдельный D-пункт после v1.0.»

Но что такое pure_view формально нигде не зафиксировано:

  • Декларация: pure_view — это атрибут метода эффекта? Отдельная декларация? Свойство handler’а?
  • Семантика: какие операции можно использовать в pure_view? Только чтение — нельзя Db.exec(...)?
  • SMT-кодировка: как решатель переводит Db.balance(...) в uninterpreted function + axioms?
  • Проверка: handler обязан реализовать pure_view соответствующим методом?

Используется в

За

  • Без этого ключевая фича spec ↔ impl не работает на effect-методах.
  • Db/Net/Time/Random — типичные handler’ы, контракты на них — естественны.

Против

  • Большой scope: формализация SMT-кодировки + axioms + проверка pure’ности.
  • Связь с D62: handler — обычное значение, Db.balance(x) это вызов через handler-стек, который может меняться. Что значит «pure» для такого вызова?

Решение пока

Открыто. До формализации контракты с Db.X(...) принимаются грамматикой, но SMT их не доказывает → ошибка @must_verify или runtime check. Production-компилятор должен дать формальное определение.

Связь: D24, Q-contract-dsl, R5.7, D62.


Q-contract-dsl. Формальный contract-DSL: result, old(...), .is_ok, .is_err

Контекст. В D24 «Контракты как обычная часть языка» приведены примеры requires/ensures с использованием:

  • result — выражение «значение, которое функция возвращает»
  • old(expr) — значение expr до вызова функции
  • result.is_ok, result.is_err — для функций с Fail эффектом
  • result.value, result.error — для Result-типов?

Используется в revolutionary.md:175,365-366,386-388 и 09-tooling.md → D24, но формальная семантика этих ключевых слов не зафиксирована.

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

  1. Что такое result для функции с Fail? В сценарии fn withdraw() Fail[Overdraft] -> () функция формально возвращает (). result.is_ok означает «функция завершилась без throw». Но тогда result.is_ok для () -> () всегда true (ничего не бросает) — что отличает от Fail[E] -> () где результат может быть is_err?

  2. Семантика result для Result-типа. Если функция возвращает Result[T, E], result.is_ok это вызов method’а на возвращённом Result, а result.value/result.error — извлечение payload’а? Тогда два механизма (Fail и Result) дают разные значения result.is_ok.

  3. old(expr) — глубина копии. old(acc.balance) копирует поле, old(arr) копирует массив или ссылку?

  4. Композиция контрактов. Можно ли использовать другие функции в requires/ensures (requires is_valid(input))? Если да — что с эффектами этой функции (она должна быть pure)?

За

  • Контракты — ключевая фича spec ↔ impl (R5.7).
  • Без формализации SMT-checker и LLM-генератор не могут проверять.

Против

  • Большой scope: D24 + новый D-блок про contract DSL.
  • Связь с handler-state (Q-pure-view): Db.balance в ensures — как handler-зависимое значение проверяется?

Решение пока

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

Связь: D24, R5.7, Q-pure-view (handler-state в контрактах).


Q-alloc-region. Полная семантика Alloc[R] и связь с region { }

Контекст. В spec упомянут эффект Alloc[R] — аллокация в named- региона R (overview.md, effects.md, D26 prelude, 04-effects.md → D2). Параллельно 05-memory.md → D6 и 06-concurrency.md → D14 объявляют region { ... } как примитив языка (как parallel for / race). Связь между Alloc[R] (эффект) и region { } (блок-примитив) явно не зафиксирована.

Что есть сейчас:

fn alloc_in(buf []u8) Alloc[r] -> Buffer    // r — имя региона
                                              // (как параметр?)

fn map_audio(samples []f32, gain f32) -> []f32 =>
    realtime nogc {
        region {                             // примитив языка
            samples.map() { x => x * gain }
        }
    }

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

  1. Объявление Alloc[R]. Эффект параметризуется именем региона. Откуда берётся R? Это compile-time имя (как lifetime в Rust), тип-параметр функции, или identifier из enclosing region { }?
  2. Handler для Alloc[R]. Кто его ставит? Блок region { } автоматически? Или программист пишет with Alloc[r] = arena_handler { ... }?
  3. Сигнатура region { body }. Что body может делать с эффектом? Body — лямбда fn() Alloc[r] -> T? Тип T уезжает из региона как — копируется, references запрещены?
  4. Multi-region. Можно ли вложить region { region { ... } }? Получится Alloc[outer], Alloc[inner] — две арены. Как escape между ними? Сейчас D6 показывает sequential let scratch = region { ... }; region { finalize(scratch) }, но не вложенный случай.
  5. Связь с realtime nogc { }. D64 говорит «внутри realtime nogc — только region-allocations и стек». То есть Alloc[R] в сигнатуре функции = «эта функция совместима с realtime nogc { } контекстом». Нужна формальная связь.
  6. Coercion / inference. Если функция f() Alloc[r] -> T вызывается внутри region { }, должен ли компилятор автоматически связать r с регионом блока? Или программист пишет явно?
  7. Сравнение с прецедентами:
    • Rust — lifetimes 'a через borrow checker.
    • Koka<alloc<r>> эффект в effect row.
    • Encore — capabilities + parallel regions.
    • Cyclone — region-based memory с явными аннотациями.

Статус. Alloc[R] оставлен в prelude как зарезервированное имя, концептуально упомянут в декларациях, но полная семантика откладывается до v1.0+. Реализация regions — пост-MVP вместе с realtime nogc enforcement.

Что делаем сейчас (MVP):

  • Alloc[R] остаётся в prelude-списке как заявленный эффект.
  • region { ... } в spec упомянут, но в bootstrap/codegen не имплементирован (managed GC покрывает 99% backend-сценариев).
  • realtime { } блок (D64) парсится, без compile-time enforcement.

Решение об активации: когда появится первый real-time use case (audio-обработка, embedded), или когда будет реализован concurrent GC и понадобится strict no-GC escape hatch.

Связь: D6 (managed GC + region opt-in), D14 (region как примитив языка), D62 (эффекты прямые, ambient runtime), D64 (realtime { } / realtime nogc { }), D26 (prelude — Alloc[R] зарезервирован).


Q-record-spread-args. Spread record-литерала в позиции аргументов функции

Контекст. Сейчас named arguments в Nova нет. Опциональные параметры выражаются через паттерн «опции-record + spread» (syntax.md → «Опциональные параметры»). Но это требует отдельного record-типа для каждого набора опций, что иногда избыточно.

Предложение. Разрешить f(...{field1: v1, field2: v2}) — spread record-литерала в позиции аргументов. Компилятор раскладывает поля по именам параметров функции:

fn name(x int, s str) -> ()

name(...{x: 2, s: "test"})            // эквивалент name(2, "test")
name(...{s: "test", x: 2})            // порядок полей не важен (spread по именам)

ro opts = { x: 2, s: "test" }
name(...opts)                          // spread существующего record'а

name(2, ...{s: "test"})               // позиционный + spread остальных

Семантика:

  • ...record-expr в позиции аргумента раскладывается по именам параметров функции.
  • Несоответствие имён поля и параметра — compile error.
  • Непокрытые параметры — compile error («missing field»).
  • Можно комбинировать с обычным spread (D60) внутри: name(...{ ...base, s: "override" }).

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

  1. ... — явный маркер. Парсер однозначен без type-directed parsing (в отличие от name({...}) который мог бы быть либо одним record-аргументом, либо named-form).
  2. Согласовано с D60. Spread уже расширяет литералы — теперь расширяет и call-site.
  3. Закрывает named arguments без отдельной фичи.
  4. Refactoring безопасен — добавил параметр, spread-вызовы получают compile error «missing field».

Тонкости:

  1. Конфликт с variadic spread (D69). f(...xs) для variadic = «развернуть массив [T]». f(...rec) для record = «развернуть по именам». Различимы по типу spread-выражения (массив vs record), но требует type-check для resolution — мягкий type-directed step.
  2. Дублирует паттерн опций-record в простых случаях. Если уже есть fn connect(opts ConnArgs), то connect(...opts_record)connect(opts_record). Преимущество только когда не хочется заводить отдельный record-тип под опции (т.е. для разнородных параметров).
  3. Композиция с D60: name(...{ ...defaults, x: 9 }) — nested spread внутри record-литерала, потом spread record’а в аргументы. Двухуровневый spread.
  4. Парсер vs type-checker. Сейчас D60 spread — чисто синтаксический. Здесь — семантический: раскладка на этапе type-check’а. Шаг к усложнению, но не критичный.
  5. mut-параметры. fn deposit(mut acc Account, amount money) — spread ...{acc, amount} должен сохранять mut-семантику. Технически тот же self-resolution, что и при обычном вызове.
  6. Default-значения (если когда-то будут — отвергнуты сейчас, см. history/rejected.md). Spread мог бы пропускать поля без override — но без default’ов compile error.

Альтернативы рассмотрены:

  • f({x: 1, s: "test"}) без ... (named arguments через D55). Отвергнуто: требует type-directed parsing для различения «один record-аргумент vs named-form».
  • Python-style f(x=1, s="test") — отдельный синтаксис named-аргументов. Новая грамматика, конфликтует с lambda-параметрами.
  • Не вводить ничего — паттерн опций-record уже работает. Цена — отдельный record-тип под каждый набор опций.

Прецеденты:

  • JavaScriptf(...obj) spread для массивов, для объектов — только в литералах ({...obj}). Разворачивания в args нет.
  • Pythonf(**kwargs) разворачивает dict в named-args. Прямой прецедент семантики.
  • Rubyf(**hash) аналогично.
  • OCaml — labeled arguments f ~x:1 ~s:"test" — отдельный механизм.

Статус. Не зафиксировано. Склонность — принять (хорошо ложится на существующие D60/D69, явный синтаксис, закрывает named arguments без отдельной фичи). Решение откладывается до появления конкретного use case (когда D55+D60 паттерн опций-record окажется многословным в реальном коде).

Связь: D55 (record-coercion), D60 (spread в литералах), D69 (variadic spread на call-site, для массивов), history/rejected.md «Default-значения параметров».


Q-math-protocol. Float / Numeric protocol для generic числового кода

Контекст. D74 объявляет математические операции (@sqrt, @sin, @cos, @atan2, @hypot, …) как instance-методы на конкретных числовых типах (f64, f32, int). Generic-код, желающий работать с «любым числом» (например, Complex[T], Vector[T], Matrix[T]), упирается в отсутствие protocol-bound — без него theta.cos() на абстрактном T не скомпилируется.

В D74 было прямо отвергнуто:

Trait-style Float protocol с sin/cos/... (Haskell Floating, Rust num_traits::Float). Лишняя indirection, generics с bounds для каждой математической функции усложняют сигнатуры.

Решение принято для stdlib: математика на конкретных типах, не через protocol. Но это блокирует пользовательский generic-код.

Вопрос: ввести ли FloatNumeric) protocol для generic-кода?

Варианты:

A. Не вводить (текущее). Generic числовой код — невозможен без дублирования ComplexF32 / ComplexF64, VectorF32 / VectorF64. Цена — ~2x кода для каждого generic числового типа. Для stdlib терпимо (сделано один раз). Для прикладного — программист пишет type ComplexF32 { ... } отдельно.

// Текущий подход: дублирование
type Complex { re f64, im f64 }
type ComplexF32 { re f32, im f32 }      // отдельный тип
// Дублированная алгебра, дублированные методы

B. Ввести только Float protocol для generic-кода. Stdlib-реализация на f32/f64 остаётся как в D74 (instance-методы), но дополнительно объявляется protocol с теми же сигнатурами. Generic-код использует protocol-bound:

type Float protocol {
    @sqrt() -> Self
    @sin() -> Self
    @cos() -> Self
    @atan2(other Self) -> Self
    @hypot(other Self) -> Self
    @abs() -> Self
    @is_finite() -> bool
    @is_nan() -> bool
    // ...
}

// Теперь generic Complex работает:
type Complex[T Float] { re T, im T }

export fn Complex[T].from_polar(r T, theta T) -> Self =>
    { re: r * theta.cos(), im: r * theta.sin() }

Структурно f32 и f64 автоматически удовлетворяют Float — никаких impl не нужно (D53). Бесплатно для stdlib, разблокирует пользовательский generic-код.

C. Ввести иерархию NumericFloat ⊂ … (как Haskell Num / Floating / Real). Гранулярные bounds — функция использующая только +/* требует Numeric, использующая sin/cosFloat. Лучше типобезопасность, дороже сложность.

type Numeric protocol {
    @plus(other Self) -> Self
    @times(other Self) -> Self
    @neg() -> Self
    // ... только арифметика
}

type Float protocol {
    @sqrt() -> Self
    @sin() -> Self
    // ... тригонометрия
    // Float наследует Numeric? — открытый подвопрос protocol-наследования
}

Тонкости:

  1. Конфликт с D74 «отвергнут Float protocol». Формулировка D74 касалась stdlib-реализации. Можно уточнить: stdlib пишет instance-методы напрямую (никакой indirection), а дополнительно объявленный protocol существует только для типизации generic- bound’ов — без runtime-overhead через мономорфизацию.
  2. Какие методы включить в Float? Полный набор D74 (~25 методов) делает protocol тяжёлым. Минимум: @sqrt, @sin, @cos, @abs, @hypot, @atan2 — что нужно для Complex / Vector. Остальное — расширения (FloatExtra, Hyperbolic).
  3. Protocol-наследование (Q-protocol-inheritance) — если хочется Float : Numeric, нужно решение про composition protocol’ов (открытый вопрос D42).
  4. Numeric для int? int не имеет sqrt/sin, но имеет +/*/-. Generic-функция sum[T Numeric](xs []T) -> T хочет работать и с int, и с f64. Нужна отдельная иерархия.
  5. Прецеденты:
    • Rustnum_traits::Float, num_traits::Num, целая иерархия. Работает, но сложно для новичков.
    • HaskellNum, Floating, Real — каноническая иерархия. Известная сложность для обучения.
    • SwiftBinaryFloatingPoint, BinaryInteger, Numeric — протоколы в stdlib. Используется в обобщённой математике.
    • Go — нет (generics с bounds появились поздно). Дублирование.
    • JuliaAbstractFloat, через duck-typing. Просто, но слабо проверяемо.

Связанные открытые вопросы:

  • Q-bounds — закрыто D72, bounds разрешены.
  • Q-default-generic ниже — для Complex[T = f64], чтобы старый не-generic вызов Complex.from(2.0) остался работать.
  • Q-protocol-inheritance — для Float : Numeric.

Статус. Не зафиксировано. Текущее (A) работает для stdlib через дублирование. Если/когда понадобится generic числовой код в проде — ввести B (минимальный Float protocol с 6-8 ключевыми методами). C (иерархия) — отложено до накопления реальных use-case’ов.

Связь: D72 (bounds), D74 (math на числовых типах), D53 (protocol = тип), D42 (protocol-наследование — открытый подвопрос).


Q-default-generic. Default-значения generic-параметров ✅ ЗАКРЫТО (2026-05-10)

ЗАКРЫТО → D88 (2026-05-10). Триггер — D87 Effect[E, IRT = never]: тип handler’а должен сообщать о возможности interrupt, но обратная совместимость требует Effect[E] ≡ Effect[E, never] через default. Это и есть real consumer, которого ждали.

Принят синтаксис из текущего раздела as-is: [T = f64], [T Bound = Default], обязательные параметры до опциональных. Содержимое ниже — историческое описание перед закрытием.

DEFERRED — nice-to-have (Plan 17 Ф.3, 2026-05-08). Rationale: прецеденты есть (Rust, C++, TS), синтаксис чистый ([T = f64]), но в Nova сейчас нет generic Complex / Vector / Matrix — главного use-case’а. Без real consumer’а добавлять сложность вывода типов (default vs inference из аргументов) — overengineering. Trigger: появление generic math-типов (Q-math-protocol) или первый кейс «добавить параметр к существующему типу не ломая caller’ов» в stdlib.

Контекст. Сейчас generic-параметры объявляются без default-значений (D16: [T]). Если добавить generic к существующему типу (ComplexComplex[T]), все существующие вызовы ломаются — программист обязан указать [T] явно.

Предложение. Разрешить default-значение в generic-объявлении, чтобы можно было параметризовать тип/функцию без breaking change’а для существующих использований:

type Complex[T = f64] {
    re T
    im T
}

// Старые вызовы продолжают работать без [T]:
ro z = Complex.from(2.0)             // T выводится как f64 из default
ro z Complex = Complex.new(1.0, 2.0)  // тип Complex (без скобок) → Complex[f64]

// Новые — с явным параметром:
ro z32 Complex[f32] = Complex.new(1.0_f32, 2.0_f32)

Синтаксис: [T = f64] — без двоеточия (стиль Nova). Альтернатива [T default f64] — длиннее, без выгоды.

Семантика:

  • Complex без скобок ≡ Complex[f64] (default подставляется).
  • Complex[f32] — явная инстанциация.
  • Default — тип, должен быть уже определён (никаких forward references).

Использование с алиасами:

type Complex32 alias Complex[f32]
type Complex64 alias Complex[f64]      // эквивалент `Complex` без скобок

Тонкости:

  1. Парсер. [T = f64] — после имени параметра идёт = потом тип. Грамматически чисто (нет конфликтов с другими = в Nova, потому что generic-список окружён []).
  2. Несколько параметров с default. [K = str, V = int] — все опциональны, можно не указывать ни одного. Если только часть с default — обязательные должны идти до опциональных ([T, U = f64] ✅, [T = f64, U] ❌).
  3. Default через bound. [T Float = f64] — bound Float + default f64. Парсер: name bound = default.
  4. Inference vs default. Если компилятор может вывести T из аргументов — default не нужен:
    fn first[T = int](xs []T) -> Option[T]
    first([1, 2, 3])             // T = int (вывод из []int, не default)
    first[]([])                   // []? — пусто, default не помогает
                                   // (тип []T неизвестен)
    
    Default — это «когда не выводится и не указан».
  5. Прецеденты:
    • Ruststruct Vec<T, A: Allocator = Global>. Работает.
    • C++template<typename T = int>. Работает.
    • TypeScriptinterface Foo<T = string>. Работает.
    • Swift, Kotlin — нет default-параметров для generic’ов.
    • Java — нет.

Конфликт с D9 «один очевидный путь»:

Default делает Complex и Complex[f64] эквивалентами — это нарушает «один путь». Но это не выбор для программиста (как с default arguments — там программист может опустить или передать), это сокращённая запись. Аналогично D58 implicit iter в for-loop (for x in xsfor x in xs.iter()) — формально два пути, но семантически тот же.

Решает реальную проблему:

Без default — добавление generic к существующему типу = breaking change. С default — backward-compatible эволюция API. Это важно для долгоживущей stdlib.

Тонкость с D52 (newtype):

type Complex Complex[f64]      // ОШИБКА: парсер не знает, это newtype
                                // или alias-без-keyword
type Complex alias Complex[f64]  // ok: явный alias

Default-параметры дополняют alias-механику, не заменяют. Алиасы полезны для конкретных инстанций (Complex32, ResultStr), default — для самой частой инстанции «по умолчанию».

Статус. Не зафиксировано. Полезно для:

  • Backward-compat при добавлении generic к существующим типам.
  • Complex[T = f64], HashMap[K, V, S = DefaultHasher] (если будет hasher-параметр), Result[T, E = Error] (если хочется упростить частый случай).

Решение — отложено до появления конкретного use case (например, generic Complex / Vector / Matrix через Q-math-protocol).

Связь: D16 (generic [T]), D52 (newtype, alias), D72 (bounds — комбинация [T Bound = Default]), Q-math-protocol — главный use case.


Q-char-literals. Синтаксис char-литералов ✅ ЗАКРЫТО (2026-05-07)

Реализовано предложенное в Q-char-literals: lexer recognizes 'a' / '\n' / '\u{HEX}' → TokenKind::Char(u32) (codepoint). AST: ExprKind::CharLit + Literal::Char (для match-pattern’ов). Codegen: char как nova_int в bootstrap (codepoint напрямую). См. nova_tests/types/char_literals.nv (16 тестов).

Оригинальный текст ниже сохранён для истории.

Контекст. В prelude есть тип char (D26), он используется в сигнатурах: fn s @chars() -> Iter[char], fn s @char_at(i int) -> Option[char]. Однако синтаксис char-литерала ('a', '\n', 'é') в спеке не описан.

Это пробел: код вида match c { '0'..='9' => ... } или if c == '"' нужен в практических парсерах (std/encoding/json.nv, std/math/complex.nv и т.д.), но формально не определён.

Предложение. Добавить char-литералы стандартного вида:

ro c char = 'a'
ro nl = '\n'
ro backslash = '\\'
ro quote = '\''
ro unicode = 'é'                // é
ro emoji = '\u{1F600}'              // 😀, escape с {} для codepoint > 0xFFFF

Грамматика:

char-literal = "'" ( raw-char | escape ) "'"
escape       = '\\' ( "'" | '"' | '\\' | 'n' | 'r' | 't' | 'b' | 'f' | '0'
                    | 'u' hex4
                    | 'u{' hex+ '}' )
hex4         = hex hex hex hex

Тонкости:

  1. Конфликт с tuple-индексом? t.0, t.1 (D37) использует точку и цифру. Char-литерал начинается с ' — нет конфликта.
  2. Конфликт со str-литералом? "a" это str, 'a' это char. Чёткое разделение: одинарные = char, двойные = string.
  3. Single-quote string как в Python? Нет — Python разрешает оба варианта для строк. Nova использует одинарные только для char.
  4. char это codepoint или byte? Codepoint (как Rust, Swift). Размер 4 байта (Unicode scalar). Не байт-char как в C.
  5. Range patterns ('0'..='9' в match) — отдельный вопрос (Q-range-patterns), часто связан с char-литералами.

Прецеденты:

ЯзыкChar-литералТип
Rust'a'char (Unicode scalar, 4 байта)
Swift"a" as Character или Character("a")Character
Go'a'rune (int32)
C/C++'a'char (byte) или int
Java'a'char (UTF-16 code unit)
Pythonнет (всё — str)
OCaml'a'char (byte)

Большинство языков используют одинарные кавычки. Nova следует этому прецеденту.

Статус. Не зафиксировано. Текущая нужда — парсеры в stdlib (json, complex, sql) используют char-литералы в кодекс-стиле. Bootstrap parser char-литералы не поддерживает — это блокирует прогон таких файлов. Предложенный синтаксис согласован с прецедентами и Nova-style (escape через \\, \u{...} для extended codepoint’ов).

Связь: D44 (числовые литералы — другой класс литералов), D26 (char в prelude как тип), D48 (tagged template literals — соседняя категория), Q-range-patterns ('0'..='9').


Q-string-indexing. Семантика s[i] для str ✅ ПЕРЕЗАКРЫТО (2026-06-13, Plan 152.1)

РЕШЕНИЕ (2026-06-13, Plan 152.1 / D249): вариант «линзы / byte-coordinates» (переоткрывает прежнее закрытие «вариант B / codepoint-indexed», 2026-05-07). str НЕ индексируется целымs[i] (int) → E_STR_NO_INT_INDEX (fix-it: байт s.as_bytes()[i] O(1) / codepoint s.as_chars().nth(i) O(n)). Единственный str[..]byte-range slice s[a..b] (Index[Range,str], zero-copy view, bounds-as-requires-контракт + UTF-8-codepoint-boundary panic; slice.nv). Бэар s.len()E_STR_NO_LEN; длина — byte_len() (O(1)) / as_chars().count() (O(n)). Обоснование: на UTF-8 codepoint-index = O(n)-ложь под видом O(1); байт-offset композируется со slice за O(1) (find→s[k..]). Прецедент: Rust (нет int-index), Swift (нет int-subscript). Полная модель — D249/D250.

(Историческая запись прежнего закрытия — вариант B / codepoint — ниже; развёрнут D26 MAJOR AMEND 2026-06-13.)

Прежнее решение (2026-05-07, РАЗВЁРНУТО): вариант B (Codepoint). В соответствии со школой B codepoint-indexed API, s[i] — codepoint at index, Option[char].

Контекст. D26 фиксирует str как UTF-8 byte slice внутри, но все public operations работают на codepoint-уровне. Что означает s[i]? Три варианта были рассмотрены:

ВариантСемантикаCostПрецедент
A. Bytes[i]: byte (u8)O(1)Go
B. Codepoints[i]: Option[char]O(i)Python
C. ЗапрещеноТолько s.bytes()[i] / s.chars().nth(i)n/aRust

Принят вариант B — consistent со всем остальным D26 API (s.len, s.slice, s.find, etc — всё codepoint-indexed). См. D26 «Почему codepoint-indexing (школа B) выбрана для Nova».

Связь: D26, Q-char-literals, D27.


Q-string-len. Единица длины str ✅ ЗАКРЫТО (2026-06-13, Plan 152.1 / D249)

РЕШЕНИЕ: нет бэар len(). Длина — свойство представления (у str три расходящиеся длины: байты / codepoint’ы / graphemes). str.byte_len() -> int (O(1)) — единственный length-метод на самом str (шорткат для аллокаций); codepoint-длина — as_chars().count() (O(n)); grapheme-длина (Phase B) — as_graphemes().count(). Бэар s.len()E_STR_NO_LEN с fix-it. Расхождение с Vec.len() намеренное. См. D249, D26 AMEND.


Q-string-collation. Сравнение/упорядочивание строк ✅ ЗАКРЫТО (2026-06-13, Plan 152.5 / D254)

РЕШЕНИЕ: дефолт = byte-Ord; locale-collation — отдельный opt-in UCA-слой (Phase B). str.compare/Ord/Equal/Hash — byte-lexicographic (быстрый, детерминированный, locale-НЕзависимый, как Rust/Go). eq_ignore_ascii_case (ASCII-fold, без таблиц) — в ядре. Locale-aware — явный std/unicode/collate.Collator на UCA (DUCET) + опц. CLDR-tailoring (Phase B, [M-152-collation]); str НИКОГДА не делает collation молча. Unicode case-folding (eq_ignore_case) — 152.5b (делегат std/unicode). Прецедент: Rust (byte Ord

  • crate), Go (byte + x/text/collate). См. D254.

Q-unicode-data. Откуда и как берутся Unicode-таблицы ✅ ЗАКРЫТО (2026-06-14, Plan 152.4 / D253)

РЕШЕНИЕ: build-time codegen из UCD, версия-пин, ленивые таблицы, без ICU. Все таблицы std/unicode генерируются инструментом nova-codegen unicode --ucd-dir <UCD> из официального Unicode Character Database в компактные ;-кодированные Nova-таблицы. Пин к версии (const UNICODE_VERSION str = "16.0"), ленивая инициализация (парсятся в HashMap/range-таблицы при первом вызове через module-level ro lazy-static, D199). НЕ хардкодим вручную, НЕ зависим от ICU/ОС. --check — CI-guard (расхождение с UCD → fail). Прецедент: Rust unicode-* (codegen), Go maketables. UCD-файлы в репозиторий НЕ коммитятся (объём); путь — --ucd-dir.

Реализовано полностью (152.4.1–152.4.6):

ВыходUCD-источникиСодержимое
norm_data.nv (152.4.1/2)UnicodeData.txt, CompositionExclusions.txt, DerivedNormalizationProps.txtNFD/NFKD full decomp, CCC, canonical composition
grapheme_data.nv (152.4.3)GraphemeBreakProperty.txt, emoji-data.txt, DerivedCoreProperties.txt (InCB)GCB / Extended_Pictographic / Indic_Conjunct_Break ranges
case_data.nv (152.4.4/5)CaseFolding.txt, SpecialCasing.txt, UnicodeData.txt[12,13,14], DerivedCoreProperties.txt (Cased/Case_Ignorable)FOLD/LOWER/UPPER/TITLE maps + Cased/Case_Ignorable ranges
word_data.nv (152.4.5)WordBreakProperty.txt (+ Extended_Pictographic reused)Word_Break category ranges (WB1-WB16)
sentence_data.nv (152.4.6)SentenceBreakProperty.txtSentence_Break category ranges (SB1-SB11), 14 категорий

Conformance (--emit-conformance → фикстуры plan152_4): NormalizationTest.txt (UAX #15), GraphemeBreakTest.txt + WordBreakTest.txt + SentenceBreakTest.txt (UAX #29, independent oracles), case-mapping breadth (UCD-derived + independent hand-oracle для выборки). См. D253, Plan 152.4.


Q-char-case-return-type. Что возвращает char.@to_uppercase()str или итератор? ✅ ЗАКРЫТО (2026-06-15, Plan 152.3b / D252)

РЕШЕНИЕ: str (материализованный), НЕ итератор. Per-code-point case-mapping может давать НЕСКОЛЬКО code point’ов (ß→"SS", fi→"FI", İ→"i"+◌̇), значит возврат — последовательность, не один char. Развилка форм:

  1. str — материализованная строка из 1–3 code point’ов.
  2. Итератор / CharsView — как Rust char::to_uppercase() -> ToUppercase: Iterator<Item=char> (lazy, без аллокации для 1-cp случая).

Выбран str по трём причинам: (1) CharsView для char-case в Nova не существует и не нужен — расширение ограничено 1–3 cp (крошечное), lazy-выигрыш нулевой; (2) симметрия со string-level to_uppercase(s) -> str (case.nv, 152.4.4) — одна ментальная модель; (3) str напрямую конкатенируется/сравнивается ('ß'.to_uppercase() == "SS") без промежуточного collect. Прецедент противоположного выбора (Rust-итератор) обоснован у Rust отсутствием выделенного короткого-строкового типа в core; у Nova str — value-record, дешёвый. См. D252, Plan 152.3.


Q-cstring. Гарантия nul-termination для nova_str.ptr

ЗАКРЫТО 2026-06-03 — Plan 118.1 / D26 amend (spec/decisions/08-runtime.md §«Nul-termination» rule 3). Reversed 2026-05-07 «variant 2». Canonical: full str invariant + str.as_cstr() method (foundation shipped 2026-06-05 commit 15abfd7546b; method runtime followup [M-118.1-cstr-runtime-wiring]).

Контекст. Bootstrap-runtime сейчас:

  • nova_str_concat — аллоцирует len + 1, кладёт \0 после данных.
  • Литералы ((nova_str){.ptr="...", .len=N}) — nul-terminated (C .rodata).
  • nova_str_slice — НЕ добавляет \0, просто view.

Это полу-гарантия: пользователь не может надёжно передать s.ptr в C-функцию без копирования.

Варианты:

  1. Always nul-terminated. slice копирует с \0. Простой C-interop ценой O(n) на slice. Прецедент: Java Native Interface (через GetStringUTFChars).
  2. Никогда не гарантировано. Удалить \0 из concat, литералы — честный slice указатель. Любой C-interop — через явный s.as_cstr() -> *const c_char (копирует если нужно). Прецедент: Rust (&strCString — явная аллокация).
  3. Текущее inconsistent. Документировать что \0 есть после литералов и concat, но не slice. Программист сам знает контекст. Не рекомендуется — путает.

Решение (2026-05-07): вариант 2 — Rust-style. Согласуется с принципом «нет скрытых аллокаций» и упрощает nova_str_slice (zero- copy). C-interop через явный Buffer.from(s).add_byte(0).into() (или будущий s.as_cstring() -> []byte если станет частым use-case). Текущий bootstrap всё ещё inconsistent (concat/литералы — terminated, slice — нет); fix — отдельная задача рантайма.

Финальное решение (2026-06-03): вариант 1 (с уточнением)full str ALWAYS nul-terminated, инвариант ptr[len] == 0 держится для всех путей конструкции (литералы, concat, slice, runtime). C-interop через явный метод str.as_cstr() -> *const c_char с runtime-check инварианта (zero-copy если уже terminated, иначе defensive copy / panic в debug). Slice копирует с \0 — O(n) ценой гарантии. Reversed 2026-05-07 reasoning: «нет скрытых аллокаций» проигрывает simplicity + zero-friction FFI (Plan 115 ptr / Plan 118 typed pointers / Plan 91.12 std.ffi.cstr — все три зависят от этого инварианта). Прецедент: Zig ([*:0]u8 встроен в язык).

Связь: D26 §«Nul-termination» (amended 2026-06-03 rule 3), Plan 118.1 (project-plan118-status), D214 (Plan 115 ptr), D216 (Plan 118 typed pointers), Q-ffi §22 «CStr / nul-terminated views».


Q-string-interning. Опциональное interning через Atom-тип

Контекст. D26 фиксирует str как не интернируемый. Это даёт предсказуемый perf и совпадает с Rust/Go. Но кейсы AST identifiers, JSON keys, log fields — это много дубликатов одной строки, где interning экономит память и даёт O(1) ==.

Варианты:

  1. Auto-intern для коротких строк. Python-style — runtime автоматически интернирует строки до N байт. Скрытая cost.
  2. Явный Atom тип. Erlang-style — Atom это always-interned immutable identifier. Создаётся через Atom("foo"), сравнение O(1) pointer equality.
  3. Sym[T] newtype. Тоньше — пользователь сам объявляет какие строки интернируются (type UserId = Sym[str]). Управление pool’ом через runtime API.
  4. Просто Rc[str]/Arc[str]. Ручная дедупликация через reference counting + хеш-таблицу. Самый простой вариант — нет нового типа, но программист сам управляет pool.

Предложение: отложить, по умолчанию 2 (Atom-type) как наиболее expressive (Erlang/Elixir опыт показывает что Atoms полезны вне строк — для tag-types, error-codes). До решения — Nova-программист использует HashMap если нужна явная дедупликация.

Связь: D26, D6 (GC managed heap), Q-stdlib-data-types.


Q-buffer. Buffer — mutable byte accumulator ❌ REMOVED (2026-05-08)

Buffer удалён из языка полностью в Plan 04 Этап 6 (2026-05-08). Заменён split’ом на StringBuilder/WriteBuffer/ReadBuffer. Никакой backward compatibility — Nova не в production, революционный язык важнее обратной совместимости.

Удаление одним коммитом:

  • codegen dispatch удалён (record_schemas + 5 групп special-case’ов в emit_call/infer).
  • nova_rt/buffer.h удалён.
  • nova_tests/runtime/buffer.nv удалён.
  • nova_rt/nova_rt.h#include "buffer.h" удалён.
  • 14 std/ файлов мигрированы на StringBuilder (text-only sweep); url.nv decode_query — на WriteBuffer + str.from_bytes([]u8)? (Plan 176 Ф.0.5: было str.try_from([]byte)?, интринзик ретайрнут).

Замены:

  • text accumulation → StringBuilder (Q-string-builder)
  • binary accumulation → WriteBuffer (Q-write-buffer)
  • binary reading → ReadBuffer (Q-read-buffer)
  • mixed text+binary → WriteBuffer + str.from_bytes([]u8)? (D325 — UTF-8 validate + конверсия; Plan 176 Ф.0.5). WriteBuffer @write_char(c) / @write_str(s) добавлены для UTF-8 encode chars/strings в byte buffer (Plan 04 Этап 6.1).

Buffer — неудачное решение (попытка унифицировать text+binary в одном типе). Правильно заменено split’ом со специализированной семантикой. История unified Buffer (Q-buffer 2026-05-07 → ⚠️ REPLACED 2026-05-08 → ❌ REMOVED 2026-05-08) сохранена ниже для понимания эволюции; не использовать ни при каких условиях — компилятор Buffer не знает.

См. также: D82 (external keyword), D26 (prelude добавляет три новых типа), Plan 04 Этап 6 (docs/plans/04-buffer-split-and-external.md).

Контекст. D26 фиксирует что s1 + s2 — O(a+b) per call, новая аллокация. В hot loop s = s + x × N → O(N²). Это норма для immutable strings, но требует builder для production кода.

Также для бинарных протоколов (network, serialization) нужен аккумулятор []byte с capacity-grow. Это та же задача: растущий байт-буфер. Различие только в финализации: с UTF-8 валидацией → str, без проверки → []byte.

Решение (2026-05-07): один тип Buffer для обоих случаев. Это шаг вперёд относительно прецедентов (которые имеют два типа — bytes.Buffer + strings.Builder в Go, Vec<u8> + String в Rust). Унификация даёт меньше API surface и одну mental model для AI-генерации.

Реализовано (2026-05-07): runtime nova_rt/buffer.h, codegen special-case dispatch для Buffer.new() / Buffer.with_capacity(n) / Buffer.from(s/b) (Path-form static) и для buf.add_*() / buf.into() / buf.try_into() / buf.into_str_unchecked() / buf.len() / buf.capacity() / buf.clone() (Member-form instance методов на receiver-type Nova_Buffer*). Tests: nova_tests/runtime/buffer.nv (16 passing) — basic ops, capacity-grow, clone independence, UTF-8 add_char (1/2/4-byte), hot-loop 1000-add accumulation. UTF-8 валидация в try_into реализована вручную (overlong/surrogate detection). После consume mutating-method даёт nova_assert("buffer consumed: ...") panic.

API (финализирован, не реализован)

// Создание
Buffer.new() -> Buffer
Buffer.with_capacity(n int) -> Buffer
Buffer.from(s str) -> Buffer            // copy UTF-8 bytes
Buffer.from(b []byte) -> Buffer         // copy bytes

// Аккумуляция (mutating, @-методы; bootstrap-limit: разные имена,
// см. ниже про overload)
fn Buffer mut @add_str(s str) -> ()      // O(s.len) memcpy + grow
fn Buffer mut @add_bytes(b []byte) -> () // O(b.len) memcpy + grow
fn Buffer mut @add_byte(b byte) -> ()    // одна byte
fn Buffer mut @add_char(c char) -> ()    // UTF-8 encode 1-4 bytes

// Финализация (consume — после неё mutating-методы → runtime panic
// "buffer consumed")
fn Buffer @into() -> []byte                              // infallible — через D73
fn Buffer @into() Fail[Utf8Error] -> str                 // fallible — D73 + Fail (target str)
fn Buffer @try_into() -> Result[str, Utf8Error]          // D77 sugar — equivalent
fn Buffer @into_str_unchecked() -> str                   // escape hatch без проверки

// Note: D73 уточнение (2026-05-07) — fallible from/into через Fail-effect
// в сигнатуре. То есть `buf.into()` для target `str` декларирует
// Fail[Utf8Error] и throw'ает на невалидный UTF-8; для target `[]byte` —
// infallible. Compiler разрешает overload по target-type через D73 dispatch.
// `try_into()` остаётся как D77 convenience-sugar (Result-стиль).

// Read (без consume)
fn Buffer @len() -> int
fn Buffer @capacity() -> int
fn Buffer @clone() -> Buffer                      // для snapshot'ов

Семантика

  • @add_* копируют контент в internal []byte (heap-allocated, 2x capacity grow).
  • @into() -> []byte через D73 ([]byte.from(buf Buffer) авто- выводится из Buffer @into). Перевод ownership: после @into() любой mutating-метод → runtime panic.
  • @try_into() -> Result[str, Utf8Error] через D77. Walks buffer, валидирует UTF-8, на успехе — zero-copy переход в str. Тоже consume.
  • @into_str_unchecked() — escape hatch для случаев когда buffer доказуемо валидный UTF-8 (например, аккумуляция только через @add_str и @add_char без @add_byte/@add_bytes). Без runtime check, дешевле.
  • @clone() возвращает независимую копию buffer; для snapshot’а без consume.

Bootstrap-limitation: overload по типу аргумента

Текущий codegen использует method_receivers: HashMap<name, (recv_ty, is_instance)> — ключ это только имя метода. Это значит overload @add(s str) и @add(b []byte) на одном receiver Buffer дадут last-wins (второй перепишет первого). Поэтому в API разные имена: @add_str / @add_bytes / @add_byte / @add_char.

Когда Q-overloading закроется (overload by argument type) — можно будет уплотнить в @add(...). Текущий API forward-compatible: добавить overload-ed @add потом не сломает существующие @add_str etc.

Конверсии str ↔ []byte (через D73)

Без Buffer есть простая граница:

fn []byte.from(s str) -> []byte =>
    // copy s.ptr..s.ptr+s.len
fn str.try_from(b []byte) -> Result[str, Utf8Error] =>
    // validate UTF-8, return Ok(str) или Err(Utf8Error)

D73/D77 авто-синтезируют s.into() / b.try_into(). Это для одноразовой конверсии. Buffer нужен когда конкатенаций много.

Прецеденты

ЯзыкBuilder/BufferФинализацияОсобенности
Gobytes.Buffer + strings.Builderразные методыразделены: bytes vs UTF-8
RustVec<u8> + StringString::from_utf8Vec for raw, String для UTF-8
JavaByteArrayOutputStream + StringBuilder.toString()разделены
Pythonbytearray + ''.joinmanualразделены

Nova унифицирует в один Buffer — меньше API surface, единая mental model. Это design-выбор согласованный с principle «одна идиома для одной задачи» (D9).

Связь: D26, D73 (From/Into для финализации), D77 (TryFrom/TryInto для UTF-8 валидации), Q-overloading (overload по типу аргумента в @add), Q-array-api ([]T.from/@push general API), Q-clone-semantics (@clone() deep vs shallow), Q-readonly-types (TS-style Readonly<T> / DeepReadonly<T>).


Q-string-builder. StringBuilder — UTF-8 string accumulator ✅ ЗАКРЫТО (2026-05-08)

Обновление 2026-05-28 (D179, Plan 91.6): StringBuilder переведён с external type на чистый Nova consume-тип type StringBuilder consume { mut buf []u8 }. Все методы Nova-body (кроме buf.push(u8) — builtin array op). API изменения: @into() → @to_str(), @byte_len/@peek удалены, @char_len новый. Подробности — см. D179 в 08-runtime.md. Старые API ниже — историческая декларация на момент закрытия Q-string-builder в 2026-05-08.

Контекст. Replaces text-side унифицированного Buffer (Q-buffer). s1 + s2 — O(a+b) per call; в hot loop O(N²). Требуется builder. Split на StringBuilder (text-only) + WriteBuffer (binary-only) выявился при добавлении endianness-методов в Q-buffer — text+binary смешение ломает API.

Решение (2026-05-08): отдельный тип StringBuilder с append-only text-семантикой. @into() -> str infallible (UTF-8 invariant поддерживается каждым @append). Декларации API — через external fn (D82), реализация — nova_rt/string_builder.h.

API (Plan 04 Этап 3)

export external fn StringBuilder.new() -> Self
export external fn StringBuilder.with_capacity(n int) -> Self
export external fn StringBuilder.from(s str)  -> Self
export external fn StringBuilder.from(c char) -> Self

export external fn StringBuilder mut @append(s str)  -> ()
export external fn StringBuilder mut @append(c char) -> ()

export external fn StringBuilder @len()      -> int
export external fn StringBuilder @capacity() -> int
export external fn StringBuilder @clone()    -> Self
export external fn StringBuilder @into()     -> str    // infallible

Семантика

  • Append-only. @append(s) копирует UTF-8-байты; @append(c) — encode codepoint в 1-4 байта.
  • @into() infallible. UTF-8 валиден по построению (str/char входы) — invariant держится без runtime-check на финализации.
  • Consume. После @into() любой mutating-метод → runtime panic.
  • @clone() — deep копия internal byte storage.
  • 2x capacity grow — стандартное удвоение.

Что отвергнуто

  • Объединение с WriteBuffer обратно (унифицированный Buffer). Q-buffer закрылся как REPLACED — split лучше: type-safety + infallible @into() -> str.
  • @append(b []byte) — нарушит UTF-8 invariant. Сырые байты → WriteBuffer.
  • @into_str_unchecked() escape hatch (был в Q-buffer). Не нужен — построение через @append(s|c) уже гарантирует UTF-8.

Прецеденты

ЯзыкТипФинализация
JavaStringBuilder.toString() infallible
Gostrings.Builder.String() infallible
RustStringpush_str/push)identity (уже String)

Все three разделяют builder для строк и для байтов. Nova согласована с этим mainstream’ом.

Связь: D26 (prelude), D82 (external keyword), D73 (from(c char) через D73), Q-buffer (REPLACED), Q-write-buffer, Q-read-buffer.


Q-write-buffer. WriteBuffer — binary serialization buffer ✅ ЗАКРЫТО (2026-05-08; re-impl 2026-06-01 Plan 91.12 V1)

Контекст. Replaces binary-side унифицированного Buffer (Q-buffer). Бинарные протоколы (network, serialization) требуют endianness-методы (write_u32_le, write_i64_be, …) — 18 числовых типов × LE/BE. В унифицированном Buffer’е такие методы не вписывались рядом с text (add_str, add_char).

Решение V1 (2026-05-08): отдельный тип WriteBuffer с endianness-aware write-методами. @into() -> []byte infallible. Декларации API — через external fn (D82), реализация — nova_rt/write_buffer.h.

Re-implementation V2 (2026-06-01, Plan 91.12 V1): external type WriteBuffer (D126) мигрирован на pure Nova record { mut buf []u8 } (паттерн StringBuilder / Plan 109 D179). API surface неизменён — те же 27 методов, та же семантика. C runtime nova_rt/write_buffer.h удалён.

Migration motivation: D126 (external type) retracted (см. spec D126); WriteBuffer — тонкий обёртка над []u8 cursor — pure Nova реализация не требует C-side struct, использует существующие []u8 primitives (push/append/extend_from). Numeric LE/BE encoding через bit-shifts + as u8 truncating casts. f32/f64 — через f{32,64}.to_bits() (D74, Plan 74) reinterpret-cast.

API (Plan 04 Этап 3)

export external fn WriteBuffer.new() -> Self
export external fn WriteBuffer.with_capacity(n int) -> Self
export external fn WriteBuffer.from(b []byte) -> Self

// Bytes
export external fn WriteBuffer mut @write_byte(v byte)      -> ()
export external fn WriteBuffer mut @write_bytes(src []byte) -> ()

// 18 числовых × LE/BE (write_u8/i8 без endianness — 1 byte):
export external fn WriteBuffer mut @write_u8(v u8)           -> ()
export external fn WriteBuffer mut @write_i8(v i8)           -> ()
export external fn WriteBuffer mut @write_u16_le(v u16)      -> ()
export external fn WriteBuffer mut @write_u16_be(v u16)      -> ()
export external fn WriteBuffer mut @write_u32_le(v u32)      -> ()
export external fn WriteBuffer mut @write_u32_be(v u32)      -> ()
export external fn WriteBuffer mut @write_u64_le(v u64)      -> ()
export external fn WriteBuffer mut @write_u64_be(v u64)      -> ()
export external fn WriteBuffer mut @write_i16_le(v i16)      -> ()
export external fn WriteBuffer mut @write_i16_be(v i16)      -> ()
export external fn WriteBuffer mut @write_i32_le(v i32)      -> ()
export external fn WriteBuffer mut @write_i32_be(v i32)      -> ()
export external fn WriteBuffer mut @write_i64_le(v i64)      -> ()
export external fn WriteBuffer mut @write_i64_be(v i64)      -> ()
export external fn WriteBuffer mut @write_f32_le(v f32)      -> ()
export external fn WriteBuffer mut @write_f32_be(v f32)      -> ()
export external fn WriteBuffer mut @write_f64_le(v f64)      -> ()
export external fn WriteBuffer mut @write_f64_be(v f64)      -> ()

export external fn WriteBuffer @len()      -> int
export external fn WriteBuffer @capacity() -> int
export external fn WriteBuffer @clone()    -> Self
export external fn WriteBuffer @into()     -> []byte

Семантика

  • @write_uN_le/be — endianness-explicit. Программист обязан выбрать LE/BE; нет «default-endian». Это безопасно: bug-class «забыл endianness» исключён на API-уровне.
  • @write_u8/@write_i8 — 1 byte, endianness не нужен.
  • @into() -> []byte infallible (любые байты валидны как []byte).
  • Consume. После @into() mutating → runtime panic.
  • 2x capacity grow — как StringBuilder.

Что отвергнуто

  • WriteBuffer.from(s str) — не вводим в MVP. Программист пишет wb.write_bytes(s.bytes()) или WriteBuffer.from(s.bytes()). Future вопрос: добавить как convenience.
  • Default-endian (write_u32 без суффикса). Bug-class. Программист забывает что default может быть LE на одной системе и BE на другой; network-protocol code тогда ломается тихо.
  • Объединение с StringBuilder обратно — split победил, см. Q-string-builder.
  • write_str метод — нарушает binary-only семантику. Программист пишет wb.write_bytes(s.bytes()) — explicit конверсия str→bytes.

Прецеденты

ЯзыкТипEndianness
Rust byteorder crateWriteBytesExt traitLE/BE explicit
Go encoding/binarybinary.LittleEndian.PutUint32namespace per endian
Java ByteBuffer.order(ByteOrder.LITTLE_ENDIAN)mode-per-buffer

Nova выбирает Rust-style explicit per-method — самый безопасный (нет hidden state).

Связь: D26, D82, Q-buffer (REPLACED), Q-string-builder, Q-read-buffer, Q-overloading.


Q-read-buffer. ReadBuffer — cursor-style binary reader ✅ ЗАКРЫТО (2026-05-08; re-impl 2026-06-01 Plan 91.12 V1)

Контекст. Pair к WriteBuffer для читающей стороны бинарных протоколов. View над []byte с position-cursor; @read_* advance’ит position. Pair @read_* (Fail-form, throw на end-of-buffer) / @try_read_* (Result-form) — auto-derive на C-runtime уровне.

Решение V1 (2026-05-08): отдельный тип ReadBuffer. View, не value (нет @into() — явный throw блокирует D73 auto-derive). Декларации — через external fn (D82), реализация — nova_rt/read_buffer.h.

Re-implementation V2 (2026-06-01, Plan 91.12 V1): external type ReadBuffer (D126) мигрирован на pure Nova cursor record { ro data []u8, mut pos int }. API surface неизменён — 22 try_read_X/read_X pair, UTF-8 decode helper. C runtime nova_rt/read_buffer.h удалён.

try_read_X теперь — primary impl (Nova body с bit-extraction + sign- extension paths); read_X — thin Nova one-liners => @try_read_X()!! (паритет D30 §2 try_* convention). Sign-extension для i16/i32 explicit: if raw >= 2^(N-1) { raw - 2^N } else { raw } (Nova int = i64 native). i64 без sign-extension (already in native range). UTF-8 char/str через _decode_utf8_at helper (mirror C runtime _nova_rb_decode_utf8_one).

API (Plan 04 Этап 3)

export external fn ReadBuffer.from(b []byte) -> Self    // view, no copy

export external fn ReadBuffer @position()           -> int
export external fn ReadBuffer @remaining()          -> int
export external fn ReadBuffer @has_remaining(n int) -> bool
export external fn ReadBuffer @remaining_bytes()    -> []byte    // copy of remaining

// Throwing form (Fail[ReadBufferError])
export external fn ReadBuffer mut @read_byte()       Fail[ReadBufferError] -> byte
export external fn ReadBuffer mut @read_bytes(n int) Fail[ReadBufferError] -> []byte
export external fn ReadBuffer mut @read_u8()         Fail[ReadBufferError] -> u8
export external fn ReadBuffer mut @read_i8()         Fail[ReadBufferError] -> i8
export external fn ReadBuffer mut @read_u16_le()     Fail[ReadBufferError] -> u16
export external fn ReadBuffer mut @read_u16_be()     Fail[ReadBufferError] -> u16
// ... все 18 числовых × LE/BE

// Try form (Result[T, ReadBufferError]) — auto-derived на C-runtime уровне
export external fn ReadBuffer mut @try_read_byte()       -> Result[byte, ReadBufferError]
export external fn ReadBuffer mut @try_read_bytes(n int) -> Result[[]byte, ReadBufferError]
export external fn ReadBuffer mut @try_read_u8()         -> Result[u8, ReadBufferError]
// ... все 18 числовых × LE/BE

// Block D73 auto-derive of @into() — ReadBuffer is a view, not a value
fn ReadBuffer @into() Fail[Error] -> () =>
    throw Error.new("ReadBuffer.@into() is not supported; use @remaining_bytes()")

ReadBufferError

export type ReadBufferError
    | UnexpectedEnd { wanted int, available int }

Будущие варианты (InvalidFormat, InvalidUtf8) — добавлять по мере появления read-методов с этими failure modes.

Auto-derive read/try_read на C-runtime уровне

Программист stdlib не пишет @read_* и @try_read_* отдельно. Одна C-функция на каждый числовой × LE/BE возвращает result-структуру:

typedef struct ReadResult_u32 {
    nova_bool ok;       // 1 = success, 0 = UnexpectedEnd
    uint32_t  value;
    int64_t   wanted;   // для error
    int64_t   available;
} ReadResult_u32;

Codegen эмитит обе Nova-сигнатуры на одну C-функцию:

  • @read_u32_be (Fail-form): проверяет ok, throw’ит через Nova_Fail_fail с ReadBufferError.UnexpectedEnd { wanted, available }.
  • @try_read_u32_be (Result-form): wrapper упаковывает в Result.Ok(value) / Result.Err(UnexpectedEnd {wanted, available}).

Минимизирует C-код в 2x (~18 functions вместо 36) и поддерживает D77 «программист пишет одну форму, обе доступны».

Семантика

  • View, no copyReadBuffer.from(b) хранит указатель + len + pos, не копирует input. Срок жизни []byte должен пережить ReadBuffer (managed heap GC обеспечивает).
  • @position/@remaining/@has_remaining — read-only cursor metadata.
  • @remaining_bytes() — копирует оставшиеся байты в новый []byte. Это compromise: zero-copy view над slice потребовал бы Q-readonly-types.
  • @into() явный throw — блокирует D73 auto-derive @into() для ReadBuffer. ReadBuffer — view, не value-to-convert.

Что отвергнуто

  • Только @try_read_* (без Fail-формы). Программист часто хочет try — early-exit через ? operator. Но Fail-форма короче для «just read it, throw on error» паттерна. Обе нужны.
  • Только @read_* (Fail-only). Result-форма необходима для graceful-recovery в protocol parser’ах.
  • ReadBuffer.@into() -> []byte — нарушает view-семантику. Какой bytes возвращать — все? Только remaining? Двусмысленно. Лучше явный @remaining_bytes().
  • Default-endian — bug-class, как и в WriteBuffer.

Прецеденты

ЯзыкТипRead API
Rust byteorderReadBytesExt traitread_u32::<LE>(...) Result
Go binary.Readbinary.LittleEndian.Uint32(b)panic on short read
Java ByteBuffer.getInt()BufferUnderflowException

Nova auto-derive read/try_read — оригинальная фича (закрепляется плотно D77 pattern).

Связь: D26, D77 (TryFrom/TryInto параллель), D82, Q-buffer (REPLACED), Q-write-buffer.


Q-codegen-builtins-cleanup. Удаление hard-coded external-таблиц из codegen ✅ CLOSED (2026-05-08)

Plan 12 закрыт (2026-05-08). Ф.1-Ф.5 + Ф.7 acceptance. std/runtime/builtins.nv — single source of truth; codegen читает AST через ExternalRegistry (include_str!-embedded в binary). Hard-coded dispatch удалён в emit_call (Member-form instance + Member-form static + Path-form static). Acceptance: добавление WriteBuffer @write_zero(n int) в builtins.nv + runtime impl работает БЕЗ правки Rust-codegen’а.

Не сделано (отложено): Ф.6 type-checker gate для unknown methods на opaque types. Сейчас unknown method даёт linker error (late-stage); idealем early-stage type error. Отдельный refactor types/mod.rs, не блокер для main goal’а.

Контекст. D82 (расширен 2026-05-08) фиксирует: std/runtime/builtins.nv — единственный источник истины для сигнатур external-функций. Codegen знает только правила mangling и Nova→C type mapping, но не хранит список самих функций.

Сейчас codegen этому ещё не соответствует. В compiler-codegen/ есть:

  • record_schemas.insert("StringBuilder", ...) / "WriteBuffer" / "ReadBuffer" — hard-coded layout/method tables.
  • Method dispatch таблицы — special-case Nova_StringBuilder_method_* emit’ы в emit_c.rs.
  • Старый record_schemas.insert("Buffer", ...) (Plan 04 Этап 6 удалит).

Проблема. Любое расхождение между builtins.nv и Rust-таблицей — silent. Если в builtins.nv:

export external fn WriteBuffer mut @write_u32_be(v u32) -> ()

а в codegen Rust hard-coded Nova_WriteBuffer_method_write_u32_be(buf, v) где v имеет тип int (не uint32_t) — компилируется, но runtime UB: codegen эмитит call с nova_int (64-bit), runtime ждёт uint32_t, ABI ломается.

Что нужно сделать. Codegen читает AST builtins.nv (как обычный Nova-модуль) и для каждой external fn декларации:

  1. Применяет mangling rules → C-name.
  2. Применяет Nova→C type mapping → C-prototype.
  3. Эмитит prototype в сгенерированный header.
  4. При встрече вызова wb.@write_u32_be(v) — lookup’ит декларацию в builtins.nv AST, не в Rust-таблице.

После этой миграции hard-coded таблицы удаляются. Расхождение между .nv-декларацией и runtime-реализацией ловится линкером (undefined reference / type mismatch при включённом -Wstrict-prototypes).

Объём работы.

  • AST-walker для builtins.nv (можно переиспользовать существующий parser).
  • Mangling rules вынесены в один модуль (сейчас разбросано).
  • Type mapper Nova→C централизован.
  • Удаление record_schemas.insert(...) для StringBuilder/WriteBuffer/ ReadBuffer (после Plan 04 Этап 6 — и для Buffer, до — оставить).

Зависимости.

  • Plan 04 Этапы 1-5 (runtime типы реализованы) — закрыты.
  • Plan 04 Этап 6 (удаление Buffer) — pending.
  • Этот cleanup — после Этапа 6 или параллельно.

Связь: D82 (single source of truth правило), Plan 12 (этот cleanup — план), Plan 04 Этап 6 (предшествует), Q-overloading (overload-resolution тоже читает из AST builtins.nv).


Q-match-unit-arms-in-expr. Bootstrap-codegen: match в expression-position с unit-arms

Контекст. Когда match-выражение стоит в expression-position (let r = match expr { ... }), а тело какой-то arm’ы содержит только unit-возвращающие statements (например assert(...), println(...)), bootstrap-codegen эмитит C-код с void mismatch — функция nova_assert возвращает unit (void), но codegen ожидает nova_int или nova_str value.

Симптом. При компиляции:

error C2440: невозможно преобразование "void" в "nova_int"

Где встречается. Обнаружено реальной работой со stdlib:

  • nova_tests/runtime/error_runtime_error.nv — 4 места (IndexOutOfBounds / TypeMismatch / AssertFailed / NoHandler).
  • std/collections/hashmap.nv — 4 места (проверки Occupied/Some).

Workaround. Переписать на if let Pattern = expr { stmts; assert(...) }if let это statement-form, нет mismatch.

Что нужно для закрытия. Codegen должен распознавать unit-result match-arms и:

  • Либо завернуть в block-expr с явным () return.
  • Либо detect’ить void в C и эмитить как statement, а не expression.
  • Либо в type-checker’е требовать всем arm’ам совпадающий не-unit тип, если match в expr-position (более строгая семантика).

Status. Не закрыто (этот конкретный void-statement-arm симптом). Workaround достаточен для bootstrap, но ограничивает идиоматический Nova-код. После полного codegen rewrite (Plan 02) — закрыть.

Родственный кейс — ЗАКРЫТ (D275, 2026-06-14). Зеркальная проблема — value-ветка (fluent -> @-хвост, типа Vec*) рядом с unit-сиблингом в discard-позиции — была codegen-mismatch’ем (объявлялся tmp(Vec*) = NOVA_UNIT; → CC-FAIL). emit_match это коэрсил (unit-доминирование [M-91.13]), а emit_if_expr нет. D275 распространил unit-коэрс на if и выровнял infer_expr_c_type(Match) с emit ([M-codegen-fluent-tail-if-unify] закрыт; workaround в std/unicode/case.nv убран). Это другой симптом (value-vs-unit, не void-statement-arm), но та же семейная зона «unit в expr-position match/if».

Связь: Plan 02 (codegen-c-backend), D19 (match-arms =>), D275 (if↔match unit-коэрс паритет), Q-pattern-mut (ниже — связанное ограничение).


Q-pattern-mut. Bootstrap-codegen: mut в pattern не парсится

Контекст. В match/let-pattern’ах нельзя использовать mut-модификатор:

match result.get("section") {
    Some(mut section_map) => {       // ✗ parse error
        section_map.insert("k", "v")
    }
    None => ()
}

Парсер не распознаёт Some(mut x) — ожидает identifier, а не keyword.

Workaround. Переписать на if let с отдельным let mut:

if Some(section_map_immut) = result.get("section") {
    mut section_map = section_map_immut
    section_map.insert("k", "v")
}

Где встречается. Реально нужно при работе с Option/Result обёртками над mutable коллекциями. В std/encoding/ini.nv переписано workaround’ом при миграции.

Что нужно для закрытия. Расширить parser pattern-grammar:

pattern = ['mut'] (identifier | constructor-pattern | record-pattern
                  | tuple-pattern | wildcard | literal)

mut в pattern должен создавать mutable binding (как let mut x = expr) — это compile-time annotation, runtime-поведение не меняется.

Status. Не закрыто. Workaround (отдельный let mut) рабочий, но многословный. Низкий приоритет — после Plan 02 (codegen rewrite) или при type-checker rewrite.

Связь: Q-match-unit-arms-in-expr (родственное bootstrap- ограничение), D33 (let/const/mut/readonly), Plan 02.


Q-overloading. Перегрузка функций / методов по типу аргументов ✅ CLOSED by D84

Закрыт D84 (2026-05-10). Полная семантика: четыре оси (receiver-тип, типы аргументов, тип результата, арность); правила резолва — самый специфичный матч, concrete > generic, non-variadic > variadic, args-фильтр перед result-фильтром, ambiguity → compile error с hint’ом. Распространяется на свободные функции, методы и static-функции на типе.

Реализация в bootstrap-codegen:

  • Methods (с receiver’ом) — ✅ работает (Plan 11): multi-overload registry + strict resolution + C-name mangling.
  • Free-functions (без receiver’а) — ✅ разрешено по D84, codegen будет расширен через тот же mangling-механизм.
  • Method values + disambiguation через as fn(...) — ✅ Plan 11 Ф.4/Ф.5.

Variant 4 (protocol-based dispatch) — параллельный путь, не отменяющий D84: используется когда расширяемость через protocol предпочтительнее ad-hoc перегрузки. Описан как идиоматичный путь в D84 «Что отвергнуто».

Контекст. D46 фиксирует operator overloading через @plus/ @times etc — это перегрузка по operator-кейсу. Но ad-hoc overload обычных функций / методов по типу аргументов — не описан.

Текущее состояние bootstrap-codegen:

Ось перегрузкиBootstrapПрецеденты
По receiver-типу (fn int @m() vs fn str @m())✅ РаботаетRust impl блоки
По типу результата (через D73/D77 dispatch)✅ РаботаетHaskell type classes
По типу аргумента на одном receiver (fn T @m(s str) vs fn T @m(b []byte))❌ Last-winsJava, C++, Swift
По arity (разное число аргументов)❌ Last-winsC# optional params

Причина: method_receivers: HashMap<name, (recv_ty, is_instance)> — ключ это только имя метода. Insert по тому же имени переписывает.

Use-cases требующие arg-type overload:

  • Buffer.add(s str) / Buffer.add(b []byte) — Q-buffer.
  • Logger.log(msg str) / Logger.log(level int, msg str).
  • Coercive constructors: Money.from(int) / Money.from(f64) / Money.from(str) — частично решается D73 (несколько from с разными типами параметра).

Варианты:

  1. Полная ad-hoc overload (Java/Swift-style). Компилятор резолвит по статическим типам аргументов. Требует переделки method_receivers в HashMap<name, Vec<Sig>> + dispatch-логику.
  2. Только D73-style dispatch. Программист пишет несколько T.from(V1), T.from(V2) — это уже работает (D73 specifically для from). Расширить same-name multiple-defines на любые статические методы — но с явной семантикой “разные параметры значит разные dispatch-ключи”.
  3. Запретить overload, требовать разные имена. Текущее состояние bootstrap. add_str / add_bytes etc. Lower expressiveness, но очень предсказуемо.
  4. Generic functions (fn add[T](v T) с trait-bound). Если Buffer @add[T Encodable](v T) — один метод, dispatch через protocol. Требует Encodable protocol с методом encode_to_buffer. Сложнее объявить, но extensible (новые типы могут implement Encodable).

Предложение: на bootstrap-уровне 3 (разные имена — что и делается в Q-buffer). На production-уровне — 4 (protocol-based) как идиоматичный Nova-путь, с fallback на 1 для редких случаев где protocol не подходит (overload по числу аргументов).

Связь: D46 (operator overloading, specific case), D53 (protocols как основной механизм абстракции), D73 (From уже допускает multiple T.from(V1)/T.from(V2) — частный случай), Q-buffer (motivating use-case).


Q-overload-result-type. Result-type overload (ось 3 D84) — отложено

DEFERRED (2026-05-10). Производная от D84 — ось 3 (по типу результата) частично реализована: type-checker регистрирует overloads с разным return-type, но codegen на call-site не делает expected-type propagation. Trigger: реальный use-case в stdlib, где single-target Into[T] через D73 + ось 1 не покрывает (например T.@into() -> X vs T.@into() -> Y — multi-target конверсии для одного receiver’а).

Контекст. D84 заявляет четыре оси перегрузки. Оси 1 (receiver-type), 2 (arg-types), 4 (arity) — реализованы в bootstrap-codegen (Plan 11 + 2026-05-10 free-fn extension). Ось 3 (result-type) — частично:

fn Celsius @into() -> Fahrenheit => ...
fn Celsius @into() -> Kelvin     => ...

ro f Fahrenheit = c.into()       // должно резолвиться в первый
ro k Kelvin     = c.into()       // должно резолвиться во второй

Что работает:

  • Type-checker допускает обе декларации (overload по возврату — валидно по D84).
  • Mangling даёт уникальные C-имена.

Что не работает:

  • При c.into() codegen не смотрит на ожидаемый тип из контекста (let-аннотация, return-position, тип параметра, поле record-литерала).
  • Если кандидатов несколько с одинаковыми arg-types и разными return- type — ambiguity error, даже когда контекст однозначно задаёт тип.

Что нужно для реализации.

Codegen на каждом call-site должен:

  1. Вытащить expected type из контекста выражения (let-annotation, return-position, argument-type вызывающей функции, поле record-литерала).
  2. Применить как фильтр 3 в D84 resolve: отбросить кандидатов с несовместимым return-type.
  3. Если после фильтра остался один — выбрать его.
  4. Если несколько / ноль — fallback на текущую ambiguity error.

Это требует bidirectional type inference через выражения: типы текут не только bottom-up (из аргументов), но и top-down (из контекста).

Workaround сейчас. Вместо instance-method overload по возврату — static-функции с разными именами:

fn Fahrenheit.from(c Celsius) -> Self => ...
fn Kelvin.from(c Celsius) -> Self => ...

// Вместо `c.into()`:
ro f = Fahrenheit.from(c)
ro k = Kelvin.from(c)

Это работает потому что T.from(...) overload’ится по receiver-типу (ось 1), которая полностью реализована.

АльтернативноInto[T] (D73) работает в bootstrap’е через single-target конверсию + контекст из let-аннотации. Multi-target Into — пока не покрывается.

Когда разморозить. Реальный use-case в stdlib, где обходной путь (static-функции / single-target Into) не работает или требует много дублирования. Например:

  • Vec[T] @into() -> List[T] vs Vec[T] @into() -> Set[T].
  • Json @into() -> User vs Json @into() -> Order (но это уже из плохого дизайна — лучше User.from_json(j)).

Связь: D84 (основное определение четырёх осей), D73 (Into[T] — частный случай через single-target), Plan 11 (bootstrap для осей 1, 2, 4).

Родственный кейс — структурное == с variant-литералом (Plan 153.3, 2026-06-14) ✅ ЗАКРЫТ точечно. Тот же дефицит bidirectional inference всплыл вне overload-резолва: result == Ok(x), где result : Result[int, int] (non-default E) — литерал Ok(x) инферился bottom-up как Result[int, str] (E дефолтит str), не совпадал с LHS → структурный eq сравнивал два разных NovaRes_<…> → CC-FAIL. Решено codegen-точечно ([M-153-result-eq-literal-expected-type] RESOLVED): в ==-NovaRes_-ветке, если типы операндов Eq/Neq расходятся и одна сторона — голый Ok/Err-литерал, codegen переэмитит её под concrete NovaRes_<n> другой стороны (reemit_result_variant_as). Это частное решение для binop-==; общий top-down expected-type проход (overload-резолв @into, return-position, record-поле) — остаётся открытым в этом Q. Точечный codegen-фикс == усиливает мотивацию для общей реализации (та же механика, шире охват).


Q-clone-semantics. @clone() — shallow или deep / рекурсивно?

CLOSED by D26 → «@clone() — shallow по умолчанию» (Plan 17 Ф.1, 2026-05-08): @clone() — shallow для record и коллекций (поля копируются, managed-references share’ятся). Для deep-копии программист пишет вручную (@deep_clone() не в prelude). Исключение — opaque accumulator-типы (StringBuilder, WriteBuffer), для которых @clone() deep по семантике типа (mutable internal buffer не должен share’иться между копиями).

Прецедент Rust (Clone shallow, DeepClone руками), Java (Object.clone shallow), Go (slice/map share по assign).

Регрессия: nova_tests/runtime/clone_semantics.nv.

Контекст. В Nova нет & borrowing (D6 — managed heap), поэтому все ссылки value-shared через GC. Это значит let b = a копирует указатель, не контент. Когда нужна независимая копия, программист вызывает a.clone().

Вопрос: что именно делает @clone()?

Прецеденты:

ЯзыкDefaultРасширения
Rustper-type (Clone trait)derive(Clone) рекурсивно поля
Javashallow (Object.clone)override для deep
Govalue-types (assignment копирует поля)references — share
Pythoncopy.copy shallow, copy.deepcopy recursiveразные функции
JS/TSObject.assign({}, x) shallowstructuredClone(x) deep

Варианты для Nova:

  1. Auto-derived deep clone (Rust-style). Компилятор синтезирует @clone() для record/sum-типа: для каждого поля вызывает field.clone(). Программист может override для специальных случаев.
    • int @clone() → value copy (тривиально)
    • str @clone() → тот же ptr (immutable, нет смысла копировать)
    • Buffer @clone() → копия internal []byte (mutable, требует независимости)
    • Cache @clone() → пустой cache (override — зависит от business semantics)
    • Циклы: runtime-detection (set уже-клонированных) или запрет.
  2. Shallow по умолчанию, явный @deep_clone(). Дешевле default, но программист должен помнить про share-mut между clone’ами.
  3. Не вводить @clone() в prelude. Каждый тип сам определяет что клонировать значит. Минимум surprise, максимум ad-hoc work.

Предложение: 1 (auto-derived deep) — Rust-style. Это:

  • Безопасный default (после clone независимы).
  • Auto-derive снимает boilerplate для типичных типов.
  • Override доступен где нужна другая семантика.
  • Циклы — отдельная задача (Q-cycle-detection); пока Nova не имеет явных reference-циклов в data-types (D6: GC может collect cycles но user-code их не создаёт идиоматически).

Тонкости:

  • Buffer @clone() — deep копия []byte (vital для buffer’а; shared buffer между clone’ами = data races).
  • str @clone() — тот же ptr (str immutable, копия эквивалентна).
  • []T @clone() — auto-derived: новый array, для каждого элемента вызывается element.clone(). O(n).
  • Записи с handler-фунциями / closures — closure clone что значит? Открытый sub-вопрос.

Связь: D6 (managed heap), Q-buffer (Buffer @clone() — конкретный mutable use-case), Q-cycle-detection (когда это станет актуально).


Q-readonly-types. TypeScript-style Readonly<T> / DeepReadonly<T>

Контекст. TS позволяет помечать тип как иммутабельный на любой глубине через mapped types:

type Readonly<T> = { readonly [K in keyof T]: T[K] }
type DeepReadonly<T> = T extends object
  ? { readonly [K in keyof T]: DeepReadonly<T[K]> }
  : T

В Nova сейчас:

  • D36 даёт readonly modifier на отдельных полях record.
  • НЕТ readonly T как type-modifier для целого типа.
  • НЕТ keyof T / mapped types.

Use-cases:

  • s.bytes() хочет вернуть []byte без mutate-возможности — сейчас нет способа. Workaround: копировать (что и делает D26).
  • API возвращает «config», который не должен меняться вызывающим — сейчас только конвенция «не меняй».
  • Snapshot vs live view — сейчас выражается копированием.

Варианты:

  1. Полный TS-style. keyof T, mapped types, readonly T, DeepReadonly<T>. Большая type-system фича. Compile-time only (нет runtime enforcement в managed heap без borrow-checker).
  2. Read[T] effect-marker. Маркируем функции «читает только» через эффект — runtime может проверять (через GC marker?) или просто compile-time hint. Согласовано с Nova effect-system.
  3. const T newtype. Простой type-flag. const []byte — это отдельный тип, []byteconst []byte через as, mutate methods compile-time error.
  4. Не вводить. Полагаться на конвенцию + копирование там где надо независимость.

Тонкость: D6 (managed heap, без borrow-checker) ограничивает runtime enforcement. Любая readonly-проверка может быть только compile-time (как в TS). Это значит Nova-код может через as cast’ом обойти readonly — это soft guarantee, не hard. TS живёт с этим, но это тонкость.

Предложение: отложить до созревания. Сейчас — 4 (конвенция + копия) через Buffer.clone(), s.into() -> []byte (копия) и т.д. Когда будет 5+ конкретных use-cases где readonly нужен — выбрать вариант 2 (Read[T] как effect) или 3 (const T newtype) в зависимости от того, нужно ли это compile-time only или runtime.

Связь: D6, D36 (readonly поля), D62 (effects как runtime-marker pattern), Q-effect-polymorphism.


Q-keywords-as-fields. Можно ли использовать keyword как имя поля?

CLOSED by D83 (2026-05-08) вариантом 1 — keywords строго запрещены как identifier’ы. Без escape-механизма (Rust r#, Swift backticks отвергнуты как overkill для bootstrap’а; могут быть добавлены после v1.0 если накопится FFI-боль).

Sweep задача: std/collections/queue.nv — поле in []T переименовать в input или inputs.

Контекст. std/collections/queue.nv использует in как имя поля:

export type Queue[T] {
    mut in  []T            // ⛔ in — keyword (for x in iter)
    mut out []T            // ✅ out — обычный ident
}

Bootstrap-парсер падает на in field-declaration: expected identifier, got 'in'.

Варианты (на момент обсуждения):

  1. Запретить keywords как identifiers вообще. Все keywords — зарезервированы. Программист переименовывает (input, inq). Самый простой, согласован с большинством языков (Rust/Go/Java).
  2. Контекстно-чувствительные keywords. in keyword только в for x in iter-конструкции, везде ещё — обычный ident. Сложнее парсер, но эргономичнее. Прецедент: Swift, C# (contextual keywords).
  3. Raw-identifier escape. r#in — keyword как ident через префикс. Прецедент: Rust r#fn, r#move.

Принятое решение: Вариант 1, зафиксирован в D83.

Связь: D30 (naming convention), D83 (closing decision).


Q-effect-type-anonymous. Anonymous effect types в позиции type

CLOSED by Variant 3 (2026-05-08): использовать Iter[T] protocol из prelude. Нет нужды в anonymous effect types в позиции типа — D58 даёт Iter[T] явно, D53 protocol-as-type работает для структурного match’а.

Sweep done: std/collections/linkedlist.nv:from_iter и std/collections/set.nv:from_iter мигрированы с it effect { mut next() -> Option[T] } (некорректный синтаксис) → it Iter[T] (корректный, prelude).

Контекст. std/collections/linkedlist.nv использовал effect { ... } inline в параметре функции:

fn LinkedList[T].from_iter(it effect { mut next() -> Option[T] }) -> Self {
    while Some(x) = it.next() { ... }
}

Это anonymous effect type — структурный effect, объявленный в позиции type-аннотации. Bootstrap-парсер не поддерживал: expected type, got 'effect'. Также синтаксис некорректен по двум причинам:

  1. effect — kind-token при declaration of named type (D18/D61), не используется в позиции типа значения inline.
  2. mut на operation в effect-declaration не описан spec’ом — effects описывают operations без mut (D61).

Варианты (на момент обсуждения):

  1. Поддержать anonymous effect types. Парсер видит effect {...} в type-position, парсит method-block, создаёт anonymous effect. Согласовано с D53 (protocol как type).
  2. Только named effects. Программист объявляет effect Iter[T] { mut next() -> Option[T] } отдельно, потом использует имя. Простой, less expressive.
  3. Iter[T] protocol в prelude. Стандартный protocol для итераторов; пользователь принимает it Iter[T] без объявления. Прецедент: Rust IntoIterator, Swift IteratorProtocol.

Принятое решение: Variant 3Iter[T] protocol уже есть в D26 prelude (см. D58/08-runtime.md строки 332-336):

type Iter[T] protocol {
    mut next() -> Option[T]
}

Программист пишет it Iter[T] для drain-параметра. Структурно любой тип с mut @next() -> Option[T] удовлетворяет. Anonymous effects в позиции типа не нужныD53 protocol-as-type решает то же самое.

Связь: D58 (Iter[T] protocol), D53, D26 (prelude содержит Iter[T]).


Q-generic-receiver-method. fn []T @method[U](...) — generic methods на slice

ЧАСТИЧНО ЗАКРЫТО (2026-05-17) для user-defined generic типов через D119

Generic method с method-level type-param теперь работает на user generic types: Wrapper[T] @map[U](f fn(T) -> U) -> Wrapper[U] — compiler emit’ит mono’d instance per (T, U) pair, bidirectional inference из closure-typed args, return type корректно substituted.

Остаётся OPEN для built-in []T (slice receiver) — требует отдельной parser-side работы ([]T в receiver position). Q-array-api всё ещё open.

Контекст. std/collections/vec.nv хочет writing extension methods на встроенный []T:

export fn []T @map[U](f fn(T) -> U) -> []U { ... }
export fn []T @filter(pred fn(T) -> bool) -> []T { ... }

Bootstrap не парсит []T как receiver type. Это требует:

  1. Парсер: []T в receiver position — type с inferred type-parameter.
  2. Codegen: ✅ DONE (D119) — generation специализированных функций для каждой комбинации (T, U) через mono pass.

Варианты:

  1. Полная поддержка generic methods на built-in типах. Codegen ✅ готов (D119). Остался parser-side работа: []T в receiver position.
  2. Free functions с TYpe parameters. fn map[T, U](xs []T, f fn(T) -> U) -> []U. Просто, но теряется method-syntax (xs.map(f)).
  3. Prelude-методы только. Compiler знает фиксированный набор []T.map/.filter/.fold и т.п., user не расширяет. Простой bootstrap-уровень.

Предложение: 3 на bootstrap (text status), 1 на production (codegen уже готов через D119 — нужен только parser pass для []T receiver). User generic types — уже работают.

Связь: D27, D35, D72, D119, Q-array-api.


Q-assert-without-parens. assert cond без parentheses?

Контекст. std/data/sql.nv писал assert n == 42 как keyword-style assert (без скобок), но Nova assert это обычная функция, требует assert(n == 42).

Варианты:

  1. assert — функция (текущее). assert(cond) обязательны parens. Nova-консистентно (println("..."), Mem.live() — все функции с parens).
  2. assert — keyword. assert cond — отдельный statement. Прецедент: Rust assert!(cond) (макрос); Java assert cond (statement).
  3. Trailing-block style. assert { cond }? Нелепо для assertions.

Предложение: 1 — keep current. Nova не имеет macros, и выделять assert как special-form нет причин. Обновить файлы где было assert ... без скобок.

Связь: D40 (function call syntax).


Q-source-annotations. CLI --no-annotate-source (default-on) ✅ ЗАКРЫТО (2026-05-07)

Реализованы annotations /* SRC: <Nova-исходник> */ перед каждым statement’ом / fn-body / trailing-expr сгенерированного .c файла. По умолчанию включены (user-driven решение 2026-05-07): полезно настолько часто (отладка, code-review, понимание codegen), что должно быть default-on. Off — через явный --no-annotate-source для CI-friendly стабильных diff’ов.

Прецеденты: Cython (по умолчанию вставляет Python-исходник), Crystal (--emit-line-numbers), Rust LLVM (#line директивы). Nova выбрал opt-in — потому что (a) Nova-исходник может содержать non-ASCII (UTF-8 в строках/идентификаторах), MSVC иногда хочет /utf-8 flag; (b) тестовая сюита диффает .c, аннотации дают разные diffs.

Реализация: CEmitter::set_source_for_annotations(src: String), emit_source_annotation_for_stmt(stmt) hook в начале emit_stmt. Snippet берётся из span’а statement’а, первая строка, escaped *//*, truncated до 120 символов.


Q-stdlib-minimal-api. Минимальный stdlib API surface, выявленный из practical libraries

Контекст. Реализация пяти практических stdlib-либ (math/complex.nv, data/semver.nv, encoding/json.nv, identifiers/uuid.nv, encoding/base64.nv, encoding/url.nv, checksums/crc32.nv, identifiers/ulid.nv) выявила минимальный набор API, без которого парсеры и сериализаторы не пишутся. Этот набор — ориентир для bootstrap stdlib implementation.

Каждая API ниже используется по крайней мере в двух из перечисленных файлов. Это не пожелания, а измеренные требования.

Эффекты

// Random — детерминизм через handler-substitution в тестах
type Random effect {
    u64() -> u64                        // 64 случайных бита
    bytes(n int) -> []byte              // массовая генерация
}

// Pre-defined handlers
fn seeded(seed u64) -> Effect[Random]      // PRNG с фиксированным seed
fn secure() -> Effect[Random]               // CSPRNG для production

// Time — Unix-timestamp + sleep
type Time effect {
    now_ms() -> u64                     // Unix timestamp в миллисекундах
    now_ns() -> u64                     // наносекунды (для high-precision)
    sleep(d Duration) -> ()
}

// Pre-defined handlers
fn fixed_ms(ms u64) -> Effect[Time]         // время заморожено
fn system_clock() -> Effect[Time]            // реальные часы OS

Use cases:

  • Random — uuid.nv (v4), ulid.nv, любая криптография
  • Time — uuid.nv (v7), ulid.nv, expiration, retry backoff

Парсинг чисел из строки

Реализовано (Plan 91.18, 2026-06-19):

  • str @parse_int(radix int = 10) -> int Fail[ParseIntError] — throwing (bare = throw)
  • str @try_parse_int(radix int = 10) -> Result[int, ParseIntError] — Result
  • str @parse_int_opt(radix int = 10) -> Option[int] — Option convenience

(D178 amend V3; прежняя parse_int -> Option[int] (D178 amend V2) заменена.)

fn int.try_from(s str) -> Result[int, ParseIntError]
fn u64.try_from(s str) -> Result[u64, ParseIntError]
fn i64.try_from(s str) -> Result[i64, ParseIntError]
fn f64.try_from(s str) -> Result[f64, ParseFloatError]
// ... для всех числовых типов

D77 даёт обе формы: int.from(s) Fail[ParseIntError] -> int синтезируется.

ParseIntError / ParseFloatError — отдельные типы (D30 convention).

Базовые str-методы (предполагаются в prelude)

fn str @len() -> int                        // длина в байтах (или codepoint'ах? — Q-string-len)
fn str @char_at(i int) -> Option[char]      // codepoint на позиции i
fn str @chars() -> Iter[char]               // итератор codepoint'ов
fn str @bytes() -> []byte                   // UTF-8 байты
fn str @slice(from int, to int) -> str      // подстрока (D78 — открытый Q что значит i)
fn str @starts_with(prefix str) -> bool
fn str @ends_with(suffix str) -> bool
fn str @contains(needle str) -> bool
fn str @find(needle str) -> Option[int]     // позиция или None
fn str @replace(from str, to str) -> str
fn str @to_ascii_lower() -> str             // ASCII-only (prelude); Unicode: to_lower() из std.unicode
fn str @to_ascii_upper() -> str             // ASCII-only (prelude); Unicode: to_upper() из std.unicode
fn str @trim_ascii() -> str                 // ASCII-only (prelude); Unicode: trim() из std.unicode
fn str @split(sep str) -> []str
fn str @strip_prefix(p str) -> Option[str]  // None если не starts_with
fn str @strip_suffix(s str) -> Option[str]

Amend (Plan 91.18, 2026-06-19): bare-имена to_lower/to_upper/trim/trim_start/ trim_end/split_whitespace теперь Unicode-семантика под import std.unicode. Из prelude доступны только _ascii_-варианты. s.to_upper() без import → E7320.

Static-методы str (конструкторы)

fn str.from(c char) -> str                  // 1-char string
fn str.from(n int) -> str                   // через D74 conversion
fn str.from(b bool) -> str                  // "true" / "false"
fn str.from(f f64) -> str                   // лучше через format spec
fn str.from_codepoint(code int) Fail[InvalidCodepoint] -> str  // 1 codepoint → str
// Plan 176 Ф.0.5 (D325-канон): from_bytes возвращает Result, не Fail — падающий
// декод открывается через match/`?`, тип ошибки несёт byte_offset.
fn str.from_bytes(b []u8) -> Result[str, Utf8Error]            // checked UTF-8 decode
fn str.from_bytes_unchecked(b []u8) -> str                     // escape hatch (no validation)
fn str.from_bytes_lossy(b []u8) -> str                         // U+FFFD replacement

[]T API

fn []T.new() -> []T                          // empty с capacity 0
fn []T.with_capacity(n int) -> []T           // empty с зарезервированной памятью
fn []T mut @push(item T)
fn []T @len() -> int
fn []T @is_empty() -> bool
fn []T @get(i int) -> Option[T]              // safe indexing
// arr[i] — panic on bounds (D13), arr.get(i) — Option
fn []T mut @clear()
fn []T mut @remove(i int) -> Option[T]
fn []T @first() -> Option[T]
fn []T @last() -> Option[T]
fn []T @contains(value T) -> bool             // требует @eq на T
fn []T @iter() -> Iter[T]

Buffer API (Q-buffer закрыто, реализовано)

fn Buffer.new() -> Buffer
fn Buffer.with_capacity(n int) -> Buffer
fn Buffer.from(s str) -> Buffer
fn Buffer.from(b []byte) -> Buffer

fn Buffer mut @add_str(s str)
fn Buffer mut @add_bytes(b []byte)
fn Buffer mut @add_byte(b byte)
fn Buffer mut @add_char(c char)              // UTF-8 encode 1-4 bytes

fn Buffer @into() -> []byte                   // consume → bytes (infallible)
fn Buffer @into() Fail[Utf8Error] -> str      // consume → str (UTF-8 validate)
fn Buffer @try_into() -> Result[str, Utf8Error]
fn Buffer @into_str_unchecked() -> str        // escape hatch
fn Buffer @len() -> int
fn Buffer @capacity() -> int
fn Buffer @clone() -> Buffer

Option[T] методы

fn Option[T] @is_some() -> bool
fn Option[T] @is_none() -> bool
fn Option[T] @unwrap() -> T                   // panic on None
fn Option[T] @unwrap_or(default T) -> T
fn Option[T] @unwrap_or_else(f fn() -> T) -> T
fn Option[T] @map[U](f fn(T) -> U) -> Option[U]
fn Option[T] @and_then[U](f fn(T) -> Option[U]) -> Option[U]
fn Option[T] @ok_or[E](err E) -> Result[T, E]

Result[T, E] методы

fn Result[T, E] @is_ok() -> bool
fn Result[T, E] @is_err() -> bool
fn Result[T, E] @unwrap() -> T                // panic on Err
fn Result[T, E] @unwrap_err() -> E             // panic on Ok
fn Result[T, E] @ok() -> Option[T]             // Result → Option (D77)
fn Result[T, E] @err() -> Option[E]
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]
fn Result[T, E] @and_then[U](f fn(T) -> Result[U, E]) -> Result[U, E]

HashMap[K, V] API (для json.nv)

fn HashMap[K Hashable, V].new() -> HashMap[K, V]
fn HashMap[K, V].with_capacity(n int) -> HashMap[K, V]
fn HashMap[K, V] mut @insert(key K, value V) -> Option[V]
fn HashMap[K, V] @get(key K) -> Option[V]
fn HashMap[K, V] @contains(key K) -> bool
fn HashMap[K, V] mut @remove(key K) -> Option[V]
fn HashMap[K, V] @len() -> int
fn HashMap[K, V] @entries() -> Iter[(K, V)]
fn HashMap[K, V] @keys() -> Iter[K]
fn HashMap[K, V] @values() -> Iter[V]

Iter[T] composers

fn Iter[T] @map[U](f fn(T) -> U) -> Iter[U]
fn Iter[T] @filter(pred fn(T) -> bool) -> Iter[T]
fn Iter[T] @fold[Acc](init Acc, f fn(Acc, T) -> Acc) -> Acc
fn Iter[T] @count() -> int
fn Iter[T] @collect() -> []T                  // в массив; collect[Out]() — Q-collect-mechanism

Числовые операции (D74 instance methods)

Уже зафиксировано в D74. Минимум для парсеров и математики:

fn f64 @sqrt() / @cbrt() / @sqr()
fn f64 @sin() / @cos() / @atan2(other) / @hypot(other)
fn f64 @abs()
fn f64 @is_finite() / @is_nan() / @is_infinite()
fn f64 @floor() / @ceil() / @round() / @trunc()
fn f64 @min(other) / @max(other)

fn int @abs()
fn int @pow(n int)
fn int @signum()
fn int @min(other) / @max(other)

Static константы:

f64.PI / f64.E / f64.NAN / f64.INFINITY / f64.MAX / f64.EPSILON
int.MAX / int.MIN

Ошибки парсинга — стандартные типы в prelude

По D30 convention Parse<TypeName>Error:

// Реализовано (2026-06-08) как sum type (не record):
type ParseIntError | Empty | InvalidDigit | Overflow | InvalidRadix

// Placeholder (ещё не реализованы — структура может измениться):
type ParseFloatError { value str, reason str }
type Utf8Error { position int, byte byte }
type InvalidCodepoint { value int }

Примечание: ParseIntError изначально планировался как record { value str, reason str } (для structured payload), но для MVP реализован как sum type с четырьмя вариантами. Payload (какой символ невалидный, на какой позиции) — followup [M-91.fe2-parse-int-error-payload].

Что отсутствует (намеренно — не для MVP)

  • Regex — отдельная либа, не prelude. Q-regex.
  • Date/Time formatting — кроме Time.now_ms(), format/parse сложен.
  • JSON parser — пользовательская либа (std.json), не prelude.
  • Crypto primitives — отдельная либа.
  • Async I/O — через эффекты Net/Fs, не prelude API.

Приоритеты реализации

Tier 1 (без них stdlib не пишется):

  • str @parse_int / @try_parse_int / @parse_int_opt ✅ реализовано (D178 amend V3, Plan 91.18, 2026-06-19)
  • int.try_from(s) / u64.try_from(s) / f64.try_from(s)
  • str методы (@len, @char_at, @chars, @slice, @find, @contains, @starts_with)
  • []T базовые (new, with_capacity, push, len, get, iter)
  • Buffer (уже реализован 2026-05-07)
  • Option/Result основные методы
  • Format spec (foundation)${expr:?} debug-format в string interpolation (Plan 91.14 2026-06-05; promoted из Tier 3). Foundation ${expr:?} lands в Plan 91.14; extensions (:hex, :pad-N, etc.) — [M-91.14-format-dsl-extensions] followup.

Tier 2 (для production):

  • Random / Time handler’ы (включая seeded / fixed_ms)
  • HashMap
  • Iter composers
  • str manipulation (@to_ascii_lower, @to_ascii_upper, @trim_ascii, @replace) ✅ реализовано (Plan 91.18, 2026-06-19); Unicode-варианты (@to_lower, @to_upper, @trim) под import std.unicode

Tier 3 (nice-to-have):

  • Regex
  • Format spec — promoted в Tier 1 (Plan 91.14, см. выше); только DSL-extensions остаются nice-to-have
  • Расширенные числовые (f64 @atan2, etc.)

Что уже реализовано в bootstrap (по состоянию на 2026-05-07)

После раундов 4–5 codegen+runtime закрыли часть Tier 1/2:

  • str: @bytes() / @chars() / @split(sep) — раунд 4 (eager, []byte / []int / []str)
  • Pattern alternation Some(A) | Some(B) => body в match-arms — раунд 4
  • Buffer API (Q-buffer закрыто) — реализован
  • Channel[T] base API (D79): Channel.new(cap), @send/@recv/@try_send/@try_recv/@close/@is_closed/@len/@capacity, drain-семантика — раунд 5
    • Tier 1+ (для concurrent stdlib); select { ... } — pending до spawn-block fix
  • D28 effect inference — private fn с throw авто-получает Fail
  • char-литералы (Q-char-literals закрыто) — 'a' / '\n' / '\u{...}'

Pending Tier 1/2:

  • int.try_from(s) / u64.try_from(s) / f64.try_from(s)D77 spec есть, runtime нет
  • Random / Time handler’ы — нужны для AI-first тестов через handler-substitution
  • HashMap[K, V] — base нет в runtime
  • Iter[T] composers (@map, @filter, @fold, @count, @collect)

Связь

  • D26 — prelude содержит часть этого набора; D26 нужно расширить под этот список.
  • D74 — math instance methods.
  • D77 — TryFrom для парсинга.
  • Q-buffer — закрыто.
  • Q-char-literals — закрыто.
  • Q-string-indexing — open: что значит i в str @char_at(i int) (байты или codepoint’ы).
  • Q-collect-mechanism — open: collect[Out]().
  • std/ — все либы используют этот набор.

Статус. Open question — не decision потому что это накопительный список, не финальный. Каждая новая stdlib-либа может выявить что-то ещё. Но текущий набор уже измерен на 8 практических файлах и является обязательным минимумом для bootstrap stdlib implementation.


Q-parallel-tuple. parallel { ... } блок с typed tuple-result

Контекст. D50 рекомендует mut-захваты для гетерогенного fan-out:

mut a = 0
mut b = 0
spawn { a = compute_a() }
spawn { b = compute_b() }

Это race-prone в production-runtime (D14 с preemption) и допустимо только в D71 single-threaded bootstrap. После принятия D79 (channels) есть safe-альтернатива для streaming/pipelines, но для 2-N разнородных задач channel тяжеловат.

Предложение. parallel { ... } блок с typed tuple-result:

ro (a, b) = parallel {
    compute_a(),    // → A
    compute_b()     // → B
}

ro (users, posts, count) = parallel {
    fetch_users(),     // []User
    fetch_posts(),     // []Post
    count_active()     // int
}

Семантика:

  • Каждое выражение запускается в отдельном fiber’е параллельно.
  • Блок ждёт завершения всех.
  • Результат — tuple типов выражений в порядке объявления.
  • При throw в любом fiber — отмена остальных через cancel-propagation (как parallel for сегодня).
  • Никакого shared mut — программист не пишет race-prone захваты.

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

  1. Safe by construction. Нет shared state, только структурное агрегирование результатов.
  2. Типизировано. Compiler знает типы каждого выражения, собирает правильный tuple-тип.
  3. AI-friendly. Один паттерн вместо двух (mut-захват vs channels) для типичного fan-out.
  4. Композиция с D75. Если нужен kill-switch снаружи — используем supervised(cancel: tok).

Implementation hint — overload-семья (bootstrap)

В Nova нет variadic generics (есть только variadic для одного типа []T через D69). Для bootstrap- времени реализация — explicit overload-семья N=2..8:

fn parallel[A, B](
    a fn() -> A,
    b fn() -> B,
) -> (A, B)

fn parallel[A, B, C](
    a fn() -> A,
    b fn() -> B,
    c fn() -> C,
) -> (A, B, C)

// ...до N=8 (стандартный лимит, как Rust tuple impls)

Особенности:

  • parallellibrary function, не language keyword. Это упрощает парсер.
  • Overloading по arity (число параметров) — D46 разрешает overloading по типу аргумента, по arity тоже работает.
  • Generic-параметры выводятся из типов lambda-выражений в позиции аргумента.

Использование как блок-выражение возможно благодаря trailing-block-стилю (D43):

ro (a, b) = parallel(
    || compute_a(),
    || compute_b(),
)

Долгосрочная цель — variadic generics

В будущем (отдельный Q-variadic-generics) вместо overload-семьи — один generic:

fn parallel[T...](fns ...fn() -> T) -> (T...)

Где T... — variadic generic-параметр (как Rust tuple[T...] или TypeScript [...T]). Это отдельный Q, не блокер для parallel-tuple сейчас.

Тонкости

  1. Cancellation на throw. Если первая задача throw’ит, остальные должны отмениться. Реализация через supervised-style scope под капотом. Если нужен «keep going on error» — программист пишет Result[T, E] в каждой ветке явно.

  2. Empty / 1-arg parallel. parallel() или parallel(f) — тривиальные случаи. parallel(f)(f(),) (single-element tuple) — runtime overhead не оправдан, лучше compile-warning.

  3. Effect-row. parallel(f, g) имеет union эффектов f и g. В bootstrap при overload-семье — статическая union. С variadic generics — динамическая.

  4. Async-context. parallel использует suspension (ambient, D62) — сигнатуры fn() -> T чистые, suspension implicit.

Прецеденты

ЯзыкКонструкцияNotes
Rusttokio::join!(f, g)macro, возвращает tuple
OCaml 5Domain.spawn + manual syncбез tuple-builder
Erlangrpc:multicallдля distributed
Goerrgroup.Group{}.Go(...)через group, не tuple
Swiftasync let a = ...per-binding async
KotlinawaitAll(deferred1, deferred2)возвращает list

Nova parallel(...) -> (T1, T2, ...)гетерогенный typed tuple, самая близкая аналогия — Rust tokio::join!.

Статус

Не зафиксировано. После принятия D79 (channels) parallel-tuple — естественное дополнение для гетерогенного fan-out. Решение:

  • Bootstrap path: overload-семья parallel[A,B], parallel[A,B,C], …, parallel[A,B,…,H] в prelude.
  • v2 path: variadic generics + единая parallel[T...] функция.

Связь: D14 (suspension ambient), D50 (concurrency model), D69 (variadic для одного типа), D79 (channels — solution для streaming, parallel-tuple — для fan-out 2..N).


Q-build-pgo. Profile-Guided Optimization для C-backend

Контекст. Nova C-backend через compiler-codegen сейчас не интегрирован с PGO. Прирост от PGO в production-компиляторах обычно 15-30% на hot path’ах (rustc bootstrap +12-14%, Chrome core rendering +15-25%). Это самая большая «бесплатная» оптимизация после LTO для backend-программ.

Зависит от:

  • Plan 09 (Clang migration) должен быть завершён до PGO работы. На MSVC PGO существует, но слабее: Clang/LLVM имеет IR-based профили (более точные), instrumentation flags (-fprofile-generate, -fprofile-use), tooling (llvm-profdata).

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

  1. AutoFDO vs обычный PGO?

    • Обычный PGO (-fprofile-generate) — инструментирует binary счётчиками, training run медленнее обычного, но точнее.
    • AutoFDO (-fprofile-sample-use) — использует perf sampling, training run без overhead’а, но менее точные профили (sampling на ~100kHz). AutoFDO проще для CI (не нужен инструментированный binary), обычный PGO даёт чуть больше прироста. Не решено.
  2. Profile в репо или нет?

    • За хранение: training run может занимать минуты, удобно закоммитить готовый профиль.
    • Против: профили платформо-специфичные (x86-64-v3 vs ARM), устаревают при изменении кода, blow up репозиторий.
    • Cargo (Rust) — рекомендует не коммитить. Программист сам делает training run.
    • Решается при написании полного плана.
  3. PGO как часть nova build или отдельный workflow?

    • Integrated: nova build --pgo делает three-step pipeline автоматически (instrument → user training run → use). Удобно, но скрывает магию.
    • Manual: программист сам пишет три команды (--pgo-instrument, run, --pgo-use). Гибче для CI.
    • Скорее всего оба (default — manual, --pgo shorthand для стандартного workflow).
  4. Каков канонический training workload?

    • User-defined — программист передаёт свой workload (nova build --pgo-train="./bench/representative.sh").
    • Auto-generated — Nova prepare’ит из tests/benchmarks автоматически.
    • Рекомендация: оба пути; для stdlib-разработки используем bench/ suite (план 09 Ф.6).
  5. PGO для stdlib и user-кода — раздельно или один профиль?

    • Один профиль — проще, но если stdlib обновляется чаще user-кода, профиль устаревает.
    • Раздельно — stdlib имеет свой PGO профиль (один раз обновляется при release), user-код имеет свой.
    • Скорее всего сначала один профиль (простота), cargo pgo-style refinement позже.

Не open question: само решение «использовать PGO» — да, очевидно полезно. Open это как интегрировать.

Связь:

  • Plan 09 — Clang migration, prerequisite.
  • Plan 10 — stub для PGO работы. Полный план будет написан после плана 09.
  • [docs/simplifications.md] → [P-no-pgo-integration] — пометка про текущее отсутствие.

Когда закроется: после реализации Plan 10 (PGO integration) с benchmark’ами показывающими ≥10% прирост vs --release без PGO.


Q-keyword-symmetry. Симметрия keyword’ов в declaration и literal: effect/protocol vs handler

РЕШЕНО 2026-05-22 (Plan 97). Вариант 4 (полная симметрия) принят и зафиксирован в D142:

  1. (B) keyword handler снят, литерал эффекта пишется через effect X { ops }. Builtin тип Effect[E, IRT] переименован в Effect[E, IRT] (см. D87 «Plan 97 amendment»).
  2. (D) анонимный protocol-литерал введён — protocol X { ops } в expression-position для one-off implementations (Channel-style capability-split factory pattern). Type-position также получил protocol { sig* } (анонимный protocol-тип в bound’ах / параметрах), см. D53 §628 и Plan 15 [P-15-anon-protocol-bound] (снят).

Реализовано в Plan 97 Ф.2 (anon-protocol type-position), Ф.3 (handler→effect rename, lexer/parser/prelude/sweep), Ф.4 (protocol-literal expression). Clean break — backwards-compat намеренно не сохраняется.

Решающий аргумент: capability-split factory pattern окупает вариант 4, а симметрия declaration↔literal согласована с D52/D53 (kind-token система) и D61/D87 (effect позиционная dispatch’ация).

Историческое обсуждение оставлено ниже как справка.


Контекст. Сейчас Nova использует разные keyword’ы для declaration и literal-формы одной сущности:

// Declaration:
type Cron effect   { run() -> () }
type Fan  protocol { run() -> () }

// Literal (только для effect):
ro h = effect Cron { run() => () }       // keyword `handler`, не `effect`
ro p = ???                                 // для protocol — нет literal-формы вообще

Возникает вопрос: унифицировать ли keyword’ы — использовать effect/protocol и в declaration, и в literal-position?

// Предложение:
ro h = effect Cron   { run() => () }      // keyword `effect`
ro p = protocol Fan  { run() => () }      // новый — anonymous protocol-литерал

Развилка 1 — effect vs handler для литерала эффекта:

  • (A) Оставить handler (текущее). Точнее в expression-position: чтение let h = effect Logger { ... } сразу говорит «это обработчик эффекта, не сам эффект». effect Logger { ... } может вводить в заблуждение: «это значение типа эффекта Logger?»
  • (B) Переименовать на effect. Симметрия с declaration (effecteffect). Breaking change, нужен migration sweep по всем тестам и spec’у.

Развилка 2 — anonymous protocol-литералы:

  • (C) Не делать (текущее). Protocol реализуется через типы с методами. Идиома Rust/Go/Swift. Аргумент: для reusable протоколов (Hashable, Iter) named-форма естественна; anonymous- форма экономит мало в этих случаях.
  • (D) Делать protocol Fan { run() => () }. Аналог Kotlin object : Runnable { ... } / Java anonymous classes / TS object-literal. Удобно для one-off реализаций без объявления отдельного типа.

Уточнение: protocols в Nova бывают двух типов use-case:

  • Reusable (Hashable, Iter, From, Into) — лучше named-форма. Тип нужен в bound’ах, generic-сигнатурах, документации.
  • One-off (Channel-style factory results, see use-case ниже) — выигрывают anonymous-форму. Тип нужен только как return-type factory-функции.

Old assumption «protocols обычно reusable» — частично верна. Для большинства protocols (~80%) да. Но structural-pattern «factory возвращает interface-implementations» делает one-off случай частым в concurrency- и I/O-API.

Важная аналогия: Nova уже имеет anonymous protocol-impl — это effect Logger { ... } для эффектов. Эффект структурно тот же контракт (набор методов с сигнатурами) что и protocol. Различия:

EffectProtocol
Структура контрактаметоды (operations)методы
Anonymous literaleffect X { ... } ✅ естьнет (текущее)
Применяется вwith X = h { ... }parameter / generic-bound
Типичный use-caseone-off (mock в тесте, transaction)reusable (Hashable, Iter)

Реальная причина разной идиомы — частота one-off vs reusable использования, не философское различие. Для эффектов anonymous- форма окупается, потому что handler’ы почти всегда одноразовые. Для протоколов экономия меньше — программист один раз пишет type MyIter и переиспользует.

Слабые аргументы (отвергнутые при анализе):

  • «Реализации спрятаны, не находятся grep’ом» — найдутся через grep "protocol Fan". То же что для handler X.
  • «Размывает AI-first locality» — closures уже приняты в Nova (D22 closure-light/full); anonymous protocol — обобщение closure на multi-method, та же категория.
  • «Captures complexity» — managed heap (D6) разрешает капчуры для closures, для protocol-литералов та же семантика.

Реальный аргумент против (D):

  • D40 «один очевидный путь». Если protocol чаще reusable (named-type идиома лучше), anonymous-форма добавляет второй путь без существенной новой выразительности — анти-паттерн Nova.
  • Прецедент Swift. Языки с extension-системой (Swift) обходятся без anonymous-impl. Nova-методы (fn Type @method) работают как extensions — Swift-подобная модель.

Реальный аргумент за (D):

  • Симметрия с handler-литералом — оба «inline implementation of a method-contract». Текущая асимметрия неестественна.
  • Multi-method ad-hoc удобен для случаев когда нужно реализовать protocol с 2-3 методами разово (closures покрывают только single-method case).
  • Прецеденты Kotlin/Java/TS — устоявшийся паттерн.

Прецеденты:

ЯзыкEffect-literal/handlerAnonymous protocol/interface
Nova (current)effect X { ... }нет
Kokawith effect X { ... }нет
Effhandler { ... }нет
Java— (нет effect system)new Runnable() { ... }
Kotlin— (нет effect system)object : Runnable { ... }
Rust— (нет effect system)нет (только impl Trait for Type)
Go— (нет effect system)нет (только конкретные типы)
Swift— (нет effect system)нет (только extension Type: Protocol)
TypeScriptobject-literal удовлетворяет interface структурно ✓
OCamlнет (только functor/module)

Для effect-литералов прецеденты не помогают — Koka/Eff используют свой keyword handler, как Nova сейчас. Для anonymous protocol картина расколота: Kotlin/Java/TS — за, Rust/Go/Swift/OCaml — против.

Почему мейнстрим без anonymous protocol-impl — разные причины, не один отвергнутый аргумент:

  • Rust — невозможно из-за ownership/borrow-checker (нужен concrete type на стадии анализа). К Nova не применимо (нет ownership).
  • Go — методы требуют named receiver-типа (Go-специфика). Можно через var r Runner = (myStruct{}).func() обходные пути. К Nova применимо частично (метод привязан к типу через fn Type @m).
  • Swiftextension Type: Protocol { ... } достаточно идиоматичен, нет потребности в anonymous. Nova близка к Swift (extension-style методы).
  • OCaml — functor/module system покрывает похожие use-cases.

Почему мейнстрим с anonymous (Kotlin/Java):

  • Java — исторически (до 1.8 нет lambdas, anonymous classes были единственным способом передать callback).
  • Kotlin — унаследовал, оставил для multi-method контрактов.
  • TypeScript — структурная типизация делает любой object-литерал потенциальной impl интерфейса автоматически.

Аргументы в Nova-контексте:

  1. AI-first locality (R5.1). Anonymous protocol-impl затрудняет поиск реализаций — программист (или LLM) не может grep’ом найти все impls protocol’а если часть из них в expression-position.
  2. Цена в symbols. Anonymous-impl экономит ~2-3 строки vs type X {} + fn X @m(). Это малая выгода.
  3. Симметрия в declaration↔literal — слабый аргумент. В Rust struct X { f: int } объявление и X { f: 42 } литерал тоже не имеют symmetric keyword’ов (нет struct X { f: 42 } в expression-position). Так делают большинство языков.
  4. handler keyword — узкий и точный. Не пересекается ни с чем, парсер прост.

Объём работ:

  • (B) переименование handlereffect в literal: правка lexer, парсер, ~30+ тестов в nova_tests/, ~10 spec-документов, AST-узел, codegen, interp. Среднее изменение.
  • (D) добавление protocol X { ... } литерала: новый AST-узел ProtocolLit, парсер, type-checker (структурная проверка соответствия protocol’у), codegen (синтез anonymous-типа + методов). Большое изменение.

Варианты комбинаций:

#Effect-literalAnon protocolОбъёмNet
1handler (A)нет (C)0статус-кво
2effect (B)нет (C)среднийсимметрия без новой фичи
3handler (A)protocol (D)большойновая фича без переименования
4effect (B)protocol (D)большой+полная симметрия

Конкретный use-case — capability-split factory pattern (обнаружен 2026-05-10):

Канонический паттерн «factory возвращает несколько связанных interface’ов с общим скрытым state», каждый interface — отдельный capability на одну сущность.

Пример (гипотетический):

// Гипотетический Lock с capability-split:
type Locker   protocol { lock() -> () }
type Unlocker protocol { unlock() -> () }

fn Lock.new() -> (Locker, Unlocker) {
    ro state = MutexState { ... }
    ro l = protocol Locker {
        lock() -> () => state.lock()
    }
    ro u = protocol Unlocker {
        unlock() -> () => state.unlock()
    }
    (l, u)
}

Без anonymous protocol-литерала нужно объявить два named-типа (LockerImpl, UnlockerImpl) с явными методами + обернуть. Цена — ~3-4 лишних строки и два типа в namespace которые больше нигде не используются (полностью one-off).

Сравнение с другими языками для этого use-case:

ЯзыкBoilerplateЭквивалент
Nova-named (текущее)среднийдва named-типа + методы + constructor
Nova-anonymous (D)минимальныйкак в примере выше
Kotlinминимальныйobject : Locker { override fun lock() = ... }
TypeScriptминимальныйobject-literal удовлетворяет structurally
Rustбольшойвнутренние struct LockerImpl + impl Trait
Goбольшойnamed-типы lockerImpl, unlockerImpl
Swiftбольшойtype-erasing wrapper или внешние structs

Use-case — прямое противоречие аргументу «D40 один путь»: named-path работает, но дороже на каждый capability-split API.

Замечание о текущем Channel в Nova: Nova уже имеет Channel[T] (D79), но по Go-модели — один объект, у которого есть и send, и recv. Это другая модель, не capability-split.

Capability-split (вторая модель из Rust/Python/TS) — отдельная дизайн-задача. Если когда-нибудь в Nova появится отдельный split_channel() API (по образцу tokio::sync::mpsc, MessageChannel JS, multiprocessing.Pipe Python) — anon protocol будет идиомой.

Реалистичные кандидаты в Plan 18 stdlib:

  • Process.spawn(cmd) -> (Stdin, Stdout, Stderr) — child-process с тремя capabilities.
  • HttpServer.bind() -> (Acceptor, ShutdownHandle) — слушатель + capability для graceful shutdown.
  • Db.transaction() -> (TxReader, TxWriter, Commit) — три role’а в транзакции.

Эти API точно появятся в зрелой stdlib. Тогда anon protocol — естественная идиома.

Предложение (обновлено). Use-case есть, не «когда-нибудь появится». Текущая дилемма:

  1. Если приоритет — минимальный bootstrap — статус-кво (named-типы), документировать через guide «как писать Channel- style API в Nova». Стоимость в каждом stdlib-API — 3-4 строки.
  2. Если приоритет — идиоматический stdlib — реализовать (D) до начала Plan 18 (stdlib roadmap). Channel-API и другие sync-primitives получают чистый идиом.

Решение между (1) и (2) зависит от того, когда начнётся реальная stdlib работа. Если она через 2-3 сессии — (2) разумно сделать сейчас. Если откладывается — статус-кво до v1.0-аудита.

До решения — статус-кво (вариант 1: handler + no anon protocol). Anonymous-форма для протоколов не запрещена принципиально — просто пока не реализована. Use-case Channel-style зафиксирован как сильный аргумент для приоритезации.

Связь:

  • D42, D53 — protocol как структурный контракт.
  • D61handler keyword в литерале.
  • D10, R5.1 — AI-first locality.
  • Q23 — группировка методов (methods Type { ... }-блок) — другая related фича про синтаксис методов.

Q-mn-*. M:N runtime — открытые вопросы

Источник: Plan 23 — M:N runtime roadmap. Эти вопросы выявлены при проработке архитектуры перехода с N:1 (single-thread bootstrap) на M:N (work-stealing scheduler на пуле OS-thread’ов). Закрываются D-блоками до старта реализации M:N.

Q-mn-1. Memory model для shared mut при M:N

Что происходит когда fiber A на worker’е 1 и fiber B на worker’е 2 оба пишут в одно managed-heap поле без synchronization?

Варианты:

  • (a) UB (Rust-style): запрещено компилятором через ownership analysis.
  • (b) Atomic-required: shared mut между fiber’ами требует Atomic[T] обёртки. Type-checker enforce’ит.
  • (c) Channel-only: shared mut между fiber’ами в принципе запрещён; общение только через Channel. Owner-actor pattern obligatorily.

Влияет на: type-checker rules, std.sync API surface, generic-bounds.

Связь: D6, D79, D91, Plan 18 std.sync.

Q-mn-2. Fiber-migration boundary для realtime nogc

realtime nogc { body } (D64) обещает «no GC pauses, no suspension». При M:N — fiber может мигрировать между worker’ами; миграция через GC safepoint = pause. Решения:

  • (a) Pin fiber’а к worker’у на время блока.
  • (b) Запрет миграции через атрибут fiber’а (no_migrate).
  • (c) Запрет M:N для fiber’ов, прошедших через realtime-блок (downgrade в N:1 на время).

Связь: D64.

Q-mn-3. Concurrent GC choice

BDW-GC (libgc) drop-in vs свой Go-style concurrent mark-and-sweep с write-barrier’ами. Trade-off в Plan 23 «Слой 6».

Рекомендация Plan 23: BDW-GC сначала (быстрый путь к работающему M:N), свой GC — отдельный milestone после v1.0 при необходимости.

Решение фиксируется отдельным D-блоком (D-mn-gc) до старта реализации.

Связь: D6.

Q-mn-4. Worker count auto-tuning

По умолчанию nproc? Через NOVA_THREADS env? Через nova.toml? Configurable runtime через Runtime.set_workers(n) API?

Прецеденты: Go — GOMAXPROCS env + runtime API; Tokio — worker_threads в build’е; Erlang — +S N флаг.

Q-mn-5. Blocking-effect — pool size

ОБНОВЛЕНО (Plan 91.15 P0 + Plan 83.3). Эффекта Blocking больше нет (отозван — см. Q12.6 выше). Вопрос переформулируется как «размер threadpool для blocking{}→offload» (codegen-уровень, без эффекта). Сам вопрос о размере пула остаётся открытым, но привязан к blocking{}-примитиву (D50 §4), а не к удалённому эффекту.

Plan M:N добавляет honest blocking-pool thread’ов для blocking{} offload (D50). Размер пула — fixed, auto-grow, либо bounded?

Прецеденты: Go’s blocking-pool unbounded (один thread per blocking call); Tokio spawn_blocking — bounded (default 512); JVM ManagedBlocker — fork-join адаптивный.

Q-mn-6. Effect handler stack — concurrent access

with X = h { spawn { ... } } — после spawn fiber имеет snapshot handler-stack’а (D80). Под M:N — handler-объект может быть shared между worker’ами. Если handler stateful (captures let mut через closure) — UB.

Spec должен явно описать:

  • Handler — immutable после capture? Mutable но требует internal synchronization?
  • Если handler-method вызывается одновременно на двух worker’ах — это race? Или sequential consistency гарантируется?

Связь: D80, D61.

Q-mn-7. SIGINT / signal handling в multi-thread runtime

Какой thread получает SIGINT при Ctrl+C? libuv даёт uv_signal_t — на какой uv_loop_t его прибивать?

Сценарии:

  • (a) Dedicated signal-thread, шлёт uv_async_send на main-worker.
  • (b) Signal handler на main-worker’е напрямую (libuv позволяет на одном thread’е).
  • (c) Каждый worker регистрирует — broadcast, идемпотентность через nova_cancel_token_cancel(main_scope_token).

Связь: D92 (implicit main-scope, Plan 22 Ф.5).


Q-cancel_scope-lambda-syntax. ЗАКРЫТО (2026-05-14) — cancel_scope keyword удалён, отмена через supervised(cancel:)

ЗАКРЫТО (2026-05-14). Решение: не превращать cancel_scope (и прочие keyword-scope’ы) в trailing-fn функции. Вместо этого keyword cancel_scope удалён полностью, а внешняя отмена выражается именованным аргументом cancel: у supervised: supervised(cancel: tok) { body } (ревизия D75, зависит от D102 «именованные аргументы»).

Почему не trailing-fn-функция: supervised — неустранимый keyword (точка регистрации spawn-fiber’ов, D14/D50). Делать его функцией не выгодно — компилятор всё равно спецкейсит. А cancel: как именованный аргумент keyword-конструкции не требует ни нового keyword’а, ни scope-introduced tok => binding.

Token-scope enforcement: проблема не escape, а aliasing. Токен теперь caller-owned by construction (создаётся вне scope’а), поэтому «no escape» нечего защищать. Защищается double-bind — runtime bind-check «один токен → один живой scope», без affine/linear-типов. tok.cancel() на завершённом scope’е — безвредный no-op.

Реализация / миграция bootstrap’а — Plan 47. Ниже — исходный анализ вопроса (сохранён для контекста).

Контекст. Сейчас cancel_scope — keyword-конструкция со специальным синтаксисом (D75):

cancel_scope { tok =>
    spawn { do_thing(tok) }
    spawn { do_other(tok) }
}

Здесь tok => — это не lambda-arrow, а scope-introduced token binding. Token tok имеет тип CancelToken и доступен внутри блока для передачи в spawn / для последующего tok.cancel() снаружи.

Это согласовано с другими keyword-scope конструкциями в Nova:

  • supervised { ... } — keyword-block без token
  • parallel for x in iter { ... } — keyword-for
  • forbid X { ... } — keyword-capability
  • with_timeout(5.s) { ... } — keyword-block с param
  • cancel_scope { tok => ... } — keyword-block с token

Предложение: унификация на trailing-fn (D43)

cancel_scope мог бы быть обычной функцией prelude с handler-param:

fn cancel_scope[T](body fn(CancelToken) Fail -> T) Fail -> T

Вызов через trailing-fn (D43):

cancel_scope() fn(tok) {
    spawn { do_thing(tok) }
    spawn { do_other(tok) }
}

Плюсы

  • Единообразие. Один pattern «trailing-fn с params» вместо special-cased keyword-syntax для cancel_scope.
  • AI-генерация safer. LLM знает trailing-fn pattern из list.map(...) fn(x) { ... }, не нужно учить ещё один edge-case.
  • First-class. cancel_scope можно передавать как value (let cs = cancel_scope), что невозможно с keyword’ом.
  • Парсер проще. Нет special-casing для cancel_scope token — обычный fn-call.

Минусы

  • Token leak. tok теперь lambda-параметр, не scope-binding — его можно сохранить в outer let mut t = ...; cancel_scope() fn(tok) { t = tok } и использовать после exit’а из scope’а. Compile-time enforce «tok доступен только внутри scope’а» теряется. Mitigation — либо runtime-check (cancel’нуть уже мёртвый scope = error), либо ownership-rules (token не Copyable, потребитель owns).
  • Codegen overhead. Сейчас cancel_scope — keyword с custom codegen (token-init + scope-bind за один шаг). При обычной fn-call нужно либо inline’ить body (compiler optimization), либо overhead от dynamic dispatch closure’а.
  • Асимметрия со supervised / parallel for / forbid. Если унифицировать только cancel_scope — оно станет outlier’ом среди keyword’ов. Если унифицировать все — это большой refactor structured concurrency (отдельный D-блок).

Связанные конструкции для унификации

Если идти этим путём, тот же rationale применяется к:

// Текущий → trailing-fn:
supervised { ... }              → supervised() fn() { ... }
parallel for x in iter { ... }  → parallel_for(iter) fn(x) { ... }
forbid Net { ... }              → forbid([Net]) fn() { ... }   // принимает list of effects?
with_timeout(5.s) { ... }       → with_timeout(5.s) fn() { ... }  // уже совместимо!
race { a, b }                   → race([a, b])                     // values, не block

with_timeout уже близок к trailing-block paradigm — он принимает duration param. cancel_scope идёт следующим natural candidate’ом для унификации.

Что не решено

  1. Compile-time token-scope enforcement — есть ли способ сохранить «tok недоступен вне scope’а» без keyword-syntax? Возможно через linear type / borrow checker аналог.
  2. Codegen efficiency — компилятор должен inline’ить trailing-fn тело чтобы избежать closure overhead. Это требует guarantee от спеки.
  3. Breaking change для existing code. Текущий cancel_scope { tok => ... } используется в nova_tests/concurrency/cancel_scope_test.nv и cancel_stress_test.nv — нужна миграция либо backward-compat period.
  4. Все keyword-scope конструкции одновременно или только cancel_scope? Симметрия требует все, но это significant breaking change для всей structured-concurrency surface.

Прецеденты

  • Kotlinsynchronized(lock) { body } это обычная функция inline fun synchronized(lock: Any, block: () -> R): R. Trailing lambda is the norm.
  • SwiftwithCheckedContinuation { continuation in ... } — обычная функция, trailing closure with param.
  • ScalaUsing.resource(r) { res => ... } — function call.
  • Rustthread::scope(|s| { ... }) — function call с closure-param.
  • Go — нет analog’а (нет structured concurrency primitives).

Все прецеденты используют function + closure-param, не keyword. Это аргумент в пользу унификации Nova’ы.

Статус

ЗАКРЫТО (2026-05-14). Итоговое решение — в выноске вверху секции. Кратко: ни cancel_scope, ни остальные keyword-scope’ы не становятся trailing-fn функциями. cancel_scope удаляется, отмена выражается supervised(cancel: tok) (именованный аргумент, D102). Унификации всех keyword-scope’ов в функции не происходитsupervised остаётся keyword’ом из-за spawn-registration магии.

Что из исходных «что не решено» как разрешилось:

  1. Compile-time token-scope enforcement — признано ненужным: токен caller-owned, escape = no-op; защищается только aliasing, через runtime bind-check.
  2. Codegen efficiencysupervised(cancel:) остаётся keyword’ом с custom codegen, closure-overhead вопрос снят.
  3. Breaking change для existing codecancel_scope_test.nv и cancel_stress_test.nv мигрируются в Plan 47.
  4. Все keyword-scope’ы или только cancel_scope — только cancel_scope (удаляется); supervised/parallel for/select остаются keyword’ами. Асимметрии нет — cancel_scope не «становится функцией», а схлопывается в аргумент существующего keyword’а.

Связь:

  • D43 — trailing-fn syntax.
  • D102 — именованные аргументы; supervised(cancel:) опирается на D102.
  • D75 — ревизованный: cancel_scope удалён, отмена через supervised(cancel:).
  • D50supervised/parallel for/spawn.
  • Plan 47 — реализация / миграция. keyword’ы (одна team — либо все унифицируются, либо все остаются).
  • Q-keyword-symmetry — related concern про symmetry keyword’ов declaration/literal.

Q-D93-sync-async-stop. Sync-vs-async stop_cb contract в D93 API ✅ ЗАКРЫТО

Закрыто: Plan 22 Ф.8 (2026-05-11) — NovaStopMode enum {SYNC, ASYNC} добавлен в D93 API. nova_sched_cancel_all_pending различает: SYNC → unpark immediate, ASYNC → ждёт backend wake. Sleep stop_cb стал ASYNC, close-wait busy-loop удалён из _nova_sleep_via_libuv. Подробно — D93

  • Plan 22 Ф.8 секция. Историческая запись сохранена ниже.

Контекст. D93 park/wake API определяет NovaCancelStopCb — callback, который вызывается из nova_cancel_token_cancel через nova_sched_cancel_all_pending. После stop_cb idempotent loop делает parked[i] = false (synchronous unpark) — fiber resumes immediate, видит cancel_requested = true, throw’ает.

Текущее предположение: stop_cb выполняется synchronously — после возврата из stop_cb всё необходимое для cleanup’а handle’а уже сделано. Под этим предположением parked[i] = false сразу после stop_cb безопасен.

Это верно для sleep handle с текущей Ф.4/Ф.6 реализацией: stop_cb делает uv_timer_stop + uv_close(close_cb), но fiber всё равно делает busy-wait через uv_run NOWAIT пока close_cb не выполнится — поэтому handle уже фактически освобождён к моменту возврата stop_cb.

Проблема — Plan 22 Ф.8 (close-cb state machine):

Production-grade refactor хотел убрать busy-loop wait и сделать park ждать close_cb напрямую (без второго park’а). Архитектура: timer_cb инициирует close (НЕ wake), close_cb делает wake. Один park на весь lifecycle. Stop_cb (для cancel) тоже только initiates close — не делает synchronous wait.

Это сломало контракт с cancel_all_pending: после stop_cb он делает parked[i] = false, fiber resume’ится до close_cb, и sanity-check (stage == CLOSED) abort’ит. Откат к Ф.6 версии.

Что нужно: D93 должен формализовать sync-vs-async stop_cb semantic, чтобы cancel_all_pending мог различать:

typedef enum {
    NOVA_STOP_SYNC,    /* handle полностью freed после stop_cb return */
    NOVA_STOP_ASYNC,   /* stop_cb лишь инициировал close; ждём wake'а от backend */
} NovaStopMode;

typedef NovaStopMode (*NovaCancelStopCb)(void* handle);

cancel_all_pending для SYNC делает parked[i] = false сразу; для ASYNC — оставляет parked, полагается на backend wake (uv close_cb).

Use-cases:

  • Sleep handle (Plan 22 Ф.8) — ASYNC: stop_cb инициирует uv_close, wake придёт из close_cb.
  • Channel waitlist (Plan 21 Ф.1) — SYNC: stop_cb отвязывает waitlist node, handle (waitlist node) полностью убран immediate.
  • Socket read (Plan 23+ std.net) — ASYNC: stop_cb делает uv_read_stop + uv_close, wake из close_cb.
  • File read (Plan 23+ std.fs) — ASYNC: stop_cb делает uv_cancel на in-flight uv_fs_t, wake из request callback.

Status (2026-05-11). Plan 22 Ф.8 — DEFERRED. Прототип выявил проблему, откат к Ф.6 семантике. Текущая ms-busy-loop на close_cb (через uv_run NOWAIT) — pragmatically acceptable (1-2 iterations typical), не блокер production deployment.

Когда фиксировать. Перед Plan 21 (Channel) реализацией. Каналы требуют чёткого SYNC контракта; sleep и socket — ASYNC. Без формального enum смешать оба в одном API — UB.

Связь: Plan 22 Ф.8 (deferred), Plan 21 channel waitlist, Plan 23 socket-read/file-read, D93 spec.


Q-axiom-binder-type. Тип binder в axiom: Option<TypeRef> vs отдельный enum

Контекст. EffectAxiom.binders: Vec<(String, Option<TypeRef>)>None означает “тип не указан явно”. В SMT encoding при None делается inference из usage в формуле, а если inference не находит — дефолт SortRef::Int.

Проблема. None читается как “отсутствует/нет значения”, хотя семантически это “untyped” — совсем другое намерение. Скрывает смысл на call-сайтах.

Предлагаемое именование:

pub enum BinderType {
    Untyped,           // axiom name(id) => ...      — inference + дефолт Int
    Typed(TypeRef),    // axiom name(id int) => ...  — явный sort
    Generic,           // axiom name[T](id T) => ... — T из generics (V2)
}

Тогда match на call-сайтах читается как документация, а не загадка None.

Когда менять. При добавлении третьего варианта (Generic как отдельный BinderType, а не флаг is_generic на уровне аксиомы) — тогда рефактор окупится. Пока два варианта (Option<TypeRef>) работает корректно, это техдолг читаемости.

Связь: Plan 33.3 Ф.9 (contracts), compiler-codegen/src/ast/mod.rs EffectAxiom, verify/pipeline.rs encode_axiom.


Q-with-deadline-vs-within. with_deadline[T](deadline_ms, body) — отмена по точке времени

Контекст. В stdlib std/concurrency/cancellation.nv есть within[T](timeout_ms, body) — отмена через duration от вызова. В distributed системах (Go context, gRPC, Tower) принято передавать deadline (абсолютный timestamp) между сервисами, чтобы каждый hop подсчитывал свой timeout как min(local_timeout, deadline - now).

Предложение. Добавить with_deadline[T](deadline_ms_unix, body fn() -> T) -> Option[T]:

ro deadline = parent_request_deadline_ms()   // unix-ms, например, 1716470000000
ro r = with_deadline(deadline, || fetch_data())

Реализация — тривиальная обёртка над within:

fn with_deadline[T](deadline_ms int, body fn() -> T) -> Option[T] {
    ro now = Time.now()                  // unix-ms (через Time.now_ms когда будет)
    ro remaining = if deadline_ms > now { deadline_ms - now } else { 0 }
    within(remaining, body)
}

Зависимости: Time.now() возвращает unix-ms (или новый Time.now_ms() с правильной semantics — runtime сейчас now() returns monotonic ms, не unix).

Trade-off. with_deadline удобен для RPC-цепочек, но Time.now_ms() vs Time.monotonic — два разных concept’а (wall vs monotonic clock). within работает на monotonic; deadline обычно на wall. Нужна decision про какие часы используем.

Связь: std/concurrency/cancellation.nv, Time effect (std/time/duration.nv §289 comment).


Q-tok-checked. tok.checked() — cooperative yield + cancel-check одной операцией

Контекст. Сейчас в CPU-bound loop’е без Time.sleep / Channel.recv fiber может не yield’нуть scheduler’у долго → отмена «не успевает»:

for i in 0..10_000_000 {
    if tok.is_cancelled() { return }      // флаг проверяем, scheduler не дёргаем
    heavy_computation(i)
}

Если tok.cancel() из другого fiber’а — этот fiber может НИКОГДА не yield’нуть, и cancel не сработает до конца loop’а.

Предложение. Метод tok.checked() — explicit yield + cancel-throw одной операцией:

for i in 0..10_000_000 {
    tok.checked()                  // 1) yield; 2) if cancel → throw CANCEL
    heavy_computation(i)
}

Аналоги: Go runtime.Gosched() + ctx.Err(), Rust tokio::task::yield_now().

Реализация: nova_cancel_token_checked — wrapper над nova_fiber_yield() + nova_throw_cancel_reason(scope.cancel_reason_ptr) если cancelled.

Trade-off. Может быть путаница с is_cancelled() (non-throwing bool). API surface ширится. Зато явный pattern для CPU-loops.

Связь: Plan 49 Ф.2 (cooperative cancel), nova_fiber_yield (fibers.h).


Q-cancel-token-with-timeout. CancelToken.with_timeout(ms) factory — auto-cancelling token

Контекст. Хочется factory который создаёт CancelToken и сам отменяет его через заданное время — как AbortSignal.timeout(5000) в TC39 (WHATWG DOM standard).

Желаемое API:

ro tok = CancelToken.with_timeout(5000)   // через 5 сек авто-cancel
do_long_work(tok)

Проблема — design choice. Кто-то должен в фоне ждать 5 секунд и вызвать cancel(). Варианты:

  1. Background fiber outside structured scope — нарушает structured concurrency (Plan 47 D50/D75: spawn только внутри supervised). Token живёт каллер-side, fiber’у нужен parent-scope для drop’а. Сложно без leak’а.

  2. OS timer callback (libuv uv_timer_t) — callback в event-loop thread вызывает nova_cancel_token_cancel(). Обходит fiber-runtime, нужен thread-safe path.

  3. Lazy timer queue — separate fire-and-forget queue для таких timer’ов с GC ownership. Новая infrastructure.

Текущий workaround (works today, чуть более многословно):

ro tok = CancelToken.new()
supervised(cancel: tok) {
    spawn { Time.sleep(5000); tok.cancel("timeout") }
    spawn { do_long_work(tok) }
}

Это уже within[T] pattern в stdlib (std/concurrency/cancellation.nv).

Trade-off. Factory удобнее (один-liner для async patterns), но требует или нарушения structured-concurrency (background fiber outside scope), или новой timer-queue infrastructure. Решение влияет на rest of cancellation design.

Связь: Plan 49 (cancellation), Plan 22 (libuv timers), TC39 AbortSignal.timeout proposal.


Q-context-value-equivalent. Go context.Value (typed request-scoped values) для Nova

Контекст. В distributed системах request-scoped values (trace ID, user ID, locale, deadline) пробрасываются через всё дерево вызовов. Передавать каждый параметром → много boilerplate; глобальные variables → не thread-safe / не fiber-scoped.

Go-style (минусы):

ctx := context.WithValue(parent, traceKey, "abc-123")
trace := ctx.Value(traceKey).(string)   // type-assert, no compile-time safety

Key — interface{}, value — interface{}. Нет type safety. Конвенция использовать private types для key чтобы избежать collision’ов — ad hoc.

Желаемое API (typed):

context.set[TraceId]("abc-123")
context.set[UserId](42)

ro trace = context.get[TraceId]()      // -> Option[TraceId], typed
ro user  = context.get[UserId]()       // -> Option[UserId]

Альтернатива — Nova effects (уже есть):

type Trace effect { current() -> str }
with Trace = trace_handler("abc-123") {
    do_work()    // внутри: Trace.current() → "abc-123"
}

Effects дают тот же use-case с typed dispatch + handler swap.

Trade-off. Два пути:

  1. Использовать existing effects (не вводить новый API) — паритет с Go context, но handler-ceremony более многословно.
  2. Ввести typed context.set/get[T]() API — короче на use-site, но new infrastructure (где живут values: TLS / fiber-locals / scope-stack?), propagation rules через spawn / handler boundary не определены.

Решение: Нужен полноценный design plan (Plan 51 tentative). Use-cases собрать, сравнить effects-based vs typed-context API, решить storage model. Не в текущем sprint.

Связь: Plan 47/49 (cancellation), spec/decisions/04-effects.md (effects design), Go context package, Rust tokio::task_local!, TC39 AsyncContext.


Q-multi-bound. Intersection-multi-bound syntax [T A + B]

🟡 PROPOSED — Plan 101.3 (closes this). Закрывается через +-syntax в [T Bound1 + Bound2 + ...]. Параллель Rust <T: A + B>.

Контекст. D72 §«Multiple bounds» сейчас требует anonymous-protocol или named-composition:

fn cache[T protocol { hash() -> u64, eq(other Self) -> bool }](xs []T) -> ...
// или
type HashableEq protocol { hash() -> u64, eq(other Self) -> bool }
fn cache[T HashableEq](xs []T) -> ...

Q-bounds §«Тонкие места 1» оставило открытым inline-syntax для multi-bound.

Решение (Plan 101.3 proposal): [T A + B] — intersection-bounds через +, параллель Rust. + выбран vs & (TS) потому что:

  • Familiar для Rust-программистов (Nova target audience overlaps).
  • Не конфликтует с , (multi-param separator) и & (bitwise).
  • […] — pure type context, no arithmetic possible → unambiguous.

Применим везде где D72 bound допустим: free fn (fn dedup[T A + B]), type-decl (type Cache[K A + B, V]), fn[T] prefix (fn[T A + B] []T @method).

Параллель индустрии: Rust +, TS &, Kotlin where clause, Go embedded composition. Nova + совпадает с Rust.

Status: proposed в Plan 101.3 (P3, ~1 dev-day). Закроется как часть Plan 101 closure.

См. также: D72, D145, Plan 101.3.


Q-representation-bound. Concrete-type bounds (fn[T int], fn[T User])

🟡 PROPOSED — Plan 102 future (out of scope Plan 101).

Контекст. D72 фиксирует «bound — это protocol-тип». Concrete types — newtype’ы (type UserId u64 per D52), records (type User { ... }) — не могут быть bound’ами.

type UserId u64        // D52 newtype
fn[T u64] []T @method   // currently ❌ — u64 не protocol

type User { id u64, name str }
type Profile { use user User, avatar Url }   // D39 embed (not subtype)
fn[T User] T @method                          // currently ❌

Use cases:

  1. Newtype-aware bounds: UserId/SessionId/OrderId все — u64 newtype’ы; хочется generic method для всех «u64-representable»: fn[T : repr u64] []T @sum() -> u64.
  2. Embed-aware bounds: Profile embeds User (D39); хочется generic method для всех «User-embedding» record’ов: fn[T : has User] T @greet() -> str.

Дизайн options:

  1. Auto-derived protocol from concrete-type shape:
    • Record type User { id u64, name str } auto-derives protocol User-shape { id() -> u64, name() -> str }.
    • fn[T User]fn[T User-shape] (structural conformance).
    • Profile (via D39 embed) auto-satisfies через delegated accessors.
  2. Explicit representation-bound: fn[T : repr u64] — T’s runtime representation = u64.
  3. Explicit embed-bound: fn[T : has User] — T contains User field (record-only).
  4. Combination: все три.

Cross-language precedent: Nova-уникальная — Rust/Go/TS/Kotlin/Scala все требуют explicit trait/protocol/interface declaration. Auto-derive from record-shape — это Nova edge opportunity.

Risks:

  • Compiler must walk type-tree для shape-matching.
  • Может ввести silent matches («user думал что не satisfies, а satisfies»).
  • Дизайн compatibility с D17 «no inheritance».

Status: proposed для Plan 102 (post Plan 101). Отдельная design phase Ф.0 нужна — это значительное расширение semantic model. P3 (polish over correctness).

Источник: обсуждение 2026-05-24 во время Plan 101 design.

См. также: D72, D145, D52 (newtype), D39 (record-embed).


Q-memory-model. Memory model между fiber’ами в production-runtime

Статус: ✅ ЗАКРЫТО D167 (Plan 103.1, 2026-05-25).

Исходный вопрос (из D79 §«Что отложено»): в preemptive M:N runtime нужно явно зафиксировать memory ordering — в bootstrap’е single-threaded scheduler делает все ordering’и semantically equivalent.

Решение D167:

  • MemOrdering enum: Relaxed | Acquire | Release | AcqRel | SeqCst
  • fence(MemOrdering) module function
  • Default ordering для simple-overload methods — SeqCst
  • Bootstrap-runtime (single-threaded): orderings reduce к sequenced-before
  • M:N runtime (Plan 23/83.4.5): полный C11 __atomic_* memory model; happens-before через paired Acquire/Release
  • Channel send/recv — happens-before (Go-style, D79)

Связь: D167, Plan 103.1.


Q-send-timeout. send с timeout у Channel

Статус: 🟡 ОТКРЫТО — Channel-specific, не sync scope. За пределами Plan 103.x.

Исходный вопрос (из D79 §«Что отложено»): @send_timeout(v T, d Duration) — нужна ли отдельная вариация API. Текущая идиома: select с timeout arm, но громоздкая.

Не входит в Plan 103.x scope (sync primitives). Отложено до stdlib-фазы работы над Channel API эволюцией.


Q-char-arithmetic. Арифметика на char — advance/retreat by codepoint count

Статус: 🟡 ОТКРЫТО — discussed 2026-05-29, дизайн не зафиксирован.

Контекст. В Plan 91.8a.2 part 3 добавлены comparison operators на char (codepoint-based, через native operators + char.@compare(other char) -> int). Открыт вопрос про арифметику.

User asked про возможность:

  • char + n -> char (advance codepoint by N)
  • char - n -> char (retreat)
  • char - char -> int (distance, useful для ranges)

Issues:

  1. Overflow / invalid codepoints. Unicode max = U+10FFFF. 'a' + 1000000 даёт invalid codepoint. Поведение?

    • Panic при invalid → runtime check overhead
    • Wrap-around → silent corruption
    • Return Option[char] → typed safety, чуть громоздко
  2. Surrogate gap U+D800-DFFF. Invalid Unicode чтобы char туда попал. Arithmetic должна пропускать surrogates? Или ошибаться?

  3. Semantic surprise. '9' + 1 == ':' (не '10'!). Программисты могут ожидать decimal increment.

  4. Implicit promotion (Java style)'a' + 1 == 98 (int), теряя char type. Nova избегает silent type changes — отвергнуто как anti-pattern.

Предлагаемые варианты

Variant A. Explicit fallible API (Rust-style):

fn char @try_advance(n int) -> Option[char] {
    ro cp = (@ as int) + n
    if cp < 0 || cp > 0x10FFFF { return None }
    if cp >= 0xD800 && cp <= 0xDFFF { return None }
    Some(cp as char)
}
fn char @minus(other char) -> int =>
    (@ as int) - (other as int)
// `c + n` operator — НЕ добавляем (forces explicit `c.try_advance(n)`)

Pros: explicit, safe, no silent corruption. Cons: verbose.

Variant B. Operator overload с panic:

fn char @plus(n int) -> char Fail[OverflowError] {
    ro cp = (@ as int) + n
    if cp < 0 || cp > 0x10FFFF { panic }
    cp as char
}

Pros: ergonomic. Cons: hidden panics в hot loop.

Variant C. Operator overload с wrap-around:

fn char @plus(n int) -> char =>
    (((@ as int) + n) & 0x10FFFF) as char

Pros: no runtime check. Cons: silent invalid codepoints.

Рекомендация

Variant A (explicit fallible API) — соответствует Nova design philosophy (D9 «один очевидный путь», D131 safety > convenience, аналогично Result-based try_* convention в std).

Когда решать

Не блокирует release 0.1 — workaround (c as int) + n доступен для arithmetic. Решать в рамках dedicated подплана Plan 91.X char-ops или string processing followup.

Связь


Финальное напоминание

Прежде чем продолжать дизайн, прочитай:

  1. README.md — главные тезисы
  2. decisions/ — все принятые решения с обоснованием
  3. discussion-log (личный, в отдельной репе) — путь к этим решениям

Прежде чем менять решение — прочитай его обоснование. Многие решения поддерживают друг друга. Изменение одного может потребовать пересмотра нескольких.

Q24. Шардирование I/O driver thread под high-I/O workload

Контекст. D228 фиксирует один centralized I/O driver thread на process. Это намеренный trade-off (single owner timer-state → cross-thread visibility races eliminated), но driver — потенциальный bottleneck при extreme timer/cancel workload’е (>1M timers/sec на worker pool из 16+ ядер).

Возможное направление. Sharded driver pool: driver_id = hash(scope) % N_drivers, каждый driver — свой uv_loop_t + own job queue. Scope привязан к одному driver’у пожизненно (стабильный hash), все timers данного scope’а живут на одном driver loop’е. Cross-scope cancel (cascade через cancelled_by) — через scoped routing.

Тонкие места:

  1. Load balancing: hash(scope_addr) % N может неравномерно распределить горячие scope’ы.
  2. Cross-driver cancel cascade (parent driver A → child driver B) добавляет visibility race обратно.
  3. pending_driver_jobs counter работает per-scope, поэтому остаётся sound при шардировании.
  4. Нужны measurements: какой % real workload’ов хотя бы упирается в driver throughput? Сейчас гипотеза, не наблюдение.

Решение когда. Если perf-bench (Plan 83.11 Ф.8 + последующие) покажет driver throughput < 50% от theoretical при ≥8 workers — открыть Plan для sharding. Иначе оставить single driver.



Q25. Explicit drain-and-cancel barrier API

Контекст. D228 §6 описывает pending_driver_jobs counter как internal runtime invariant — nova_supervised_run_impl сам спинит на нём перед return. Но user-code, который хочет «отмени все outstanding и дождись пока driver thread обработает», сейчас не имеет API: tok.cancel() возвращается immediately, фактическая обработка асинхронна.

Возможное API.

ro tok = CancelToken.new()
supervised(cancel: tok) {
    spawn { Time.sleep(10_000) }
    spawn { Time.sleep(10_000) }
    tok.cancel()
    tok.barrier()   // дождаться, пока driver обработает CANCEL_SCOPE job
    // теперь гарантированно: все armed timers closed, fiber'ы пробуждены
}

Use cases.

  • Тесты, которые хотят deterministic observation “cancel fully delivered”.
  • Graceful shutdown с временным окном (tok.cancel(); tok.barrier_until(deadline)).

Тонкие места:

  1. Идиоматически это уже делается выходом из supervised блока — он сам барьер.
  2. Если открыть barrier user-коду, нужно решить семантику barrier на cascade-cancel: ждать только свой scope или всех linked-children?
  3. Может конкурировать с idea «cancel должен быть fire-and-forget» из D75.

Решение когда. Если появится конкретный use-case в stdlib (test harness, server shutdown), вынести в Plan. Сейчас — speculative.



Q26. Tunable ctx_pins[] doubling threshold для embedded

Контекст. NovaFiberQueue.ctx_pins[] — GC-root anchor array per supervised scope (D228 §5). Удваивается на степенях 2: 16 → 32 → 64 → ... → 1024 → 2048 → .... Каждое удвоение = nova_alloc = potential GC trigger.

Для типичного Nova-кода это норм (10-100 fibers per scope). Для embedded таргетов с ограниченной памятью (target ≤256KB heap), или для micro-services с тысячами коротких scope’ов, fixed 16-cap initial может быть либо overkill (waste), либо undersized (frequent grow).

Возможные direction’ы.

  • Env var NOVA_SCOPE_PINS_INITIAL_CAP=<int> — компромисс между memory и grow-frequency.
  • Compile-time #[scope_pins_cap = N] attribute на supervised блок.
  • Auto-tune via runtime statistics (Tokio approach).

Тонкие места:

  1. Менять initial cap во время runtime опасно — race с already-allocated scopes.
  2. Embedded таргетов у Nova ещё формально нет (Plan 83.13 precise-GC в research stage).
  3. Это micro-optimization до measured benchmark’а.

Решение когда. Когда появится первый embedded таргет или benchmark покажет ≥5% времени в ctx_pins realloc loop’ах.



Q27. Sysmon introspection: экспонировать pending_driver_jobs

Контекст. D228 §6 вводит pending_driver_jobs counter per scope. Сейчас он internal — нет API для observability. Long-running cancel storm может зависнуть с large counter (driver не успевает разгребать), и user не увидит этого до timeout’а.

Возможное API.

use std.runtime

// Через nova-info (CLI sysmon):
//   $ nova runtime introspect --supervised-scopes
//   scope#0: pending_driver_jobs=42, armed_sleeps=12

// Через library:
fn check_health() -> RuntimeStats {
    runtime.driver_stats()  // { queue_depth, longest_pending_ms, ... }
}

Use cases.

  • Production monitoring: alerting на driver-queue-saturation.
  • Тестирование: assertion’ы вроде assert(runtime.driver_stats().queue_depth == 0).
  • Debug: какой scope генерирует cancel storm?

Тонкие места:

  1. Atomic load counter’а cheap (relaxed read), но aggregation по всем scope’ам требует walk runtime structures — нужен read-lock или snapshot mechanism.
  2. Пересекается с Plan 76 (Mem effect для leak/growth тестов) — возможно один общий runtime-introspection API.
  3. Может потребовать stable spec API (std.runtime.driver_stats()), что добавляет surface area к 0.1 release.

Решение когда. Если появится Plan для production observability (likely 0.2+), включить driver-queue-depth туда. Сейчас — defer.



Q-vec-vs-slice — Vec[T] vs []T: which to use?

Status: Answered (Plan 131, 2026-06-08)

Короткий ответ

Используй []T по умолчанию. Переключайся на Vec[T] только когда T — это value-struct (Option[U], named tuple, value-record >8 bytes) и тебе нужно typed storage без int64-slot erasure.

[]T (built-in slice, NOVA_ARRAY_DECL macro)

  • Default — весь существующий код использует []T.
  • Primitives (int, f64, bool, char, str, u8…): полное typed storage (native C type per element).
  • Records / sum-types: указатель в int64-slot — reference semantics, GC-tracked. Работает корректно для heap-allocated T.
  • Не работает для value-struct элементов (Option[T], tuple >8 bytes, value-record >8 bytes): int64-slot не вмещает структуру, side-channel readback покрывает только pointer-элементы (D141).
  • Compiler magic required: NOVA_ARRAY_DECL(T) macro.

Vec[T] (Nova-native, std.collections.vec_owned)

  • Generic record с priv mut data *mut T + len + cap (writable buffer — явный *mut T: L3 pointee-mut из типа, D246; flip-scan D245 отклонён, pointee-mut НЕ наследуется от mut-binding).
  • Элементы хранятся по РЕАЛЬНОМУ C-типу T в contiguous buffer.
  • Vec[Option[int]] хранит NovaOpt_nova_int inline (16 bytes/element).
  • Vec[MyValueRecord] хранит NovaValue_MyValueRecord inline.
  • Работает для ЛЮБОГО T включая Option[U], tuple, value-record.
  • Pure Nova реализация — compiler magic не нужен.
  • Та же производительность (те же alloc и ptr arithmetic).

Decision table

T[]TVec[T]
int, f64, bool, str, u8
Record (heap pointer)
Option[int] (value-struct)❌ int64-erasure
Named tuple >8 bytes
Value-record (D228)

Путь миграции

Update 2026-06-10 (Plan 138.1 Ф.1-Ф.6 CLOSED, D239 partial): []T теперь резолвится в Vec[T] в единицах трансляции, которые импортируют std.collections.vec_owned.{Vec} (typed-storage gap закрыт для value-struct элементов именно в таких юнитах). Универсальный flip (для Vec-free / primitive-only юнитов) — следующий шаг: Plan 138.2 ([M-138.1-vec-in-prelude]), добавляющий Vec в prelude.

Таблица решений обновляется: при явном импорте Vec (или после Plan 138.2) []Option[int], []tuple, []value-record хранятся типизированно через Vec[T]. NOVA_ARRAY_DECL остаётся load-bearing до завершения Plan 138.2.

Строки из таблицы ниже со статусом ❌ для []T корректны для Vec-free юнитов (текущее состояние до Plan 138.2). После Plan 138.2 все строки станут ✅.

В будущем []T станет сахаром над Vec[T] для всех юнитов (Plan 138.2 + universalization). Пока для value-struct элементов без явного Vec-импорта — используй Vec[T] напрямую (см. D141 для текущего статуса []T gap).

[]fn и [N]T — вне scope

  • []fn(...) -> T (closure-array, NovaArray_void_p*) — вне scope Plan 138.1 и 138.2. Followup: [M-138.1-closure-array].
  • [N]T (FixedArray, stack allocation) — вне scope, отдельное решение нужно для stack-vs-heap contract. Followup: [M-138.1-fixed-array].

Cross-refs

  • D232 — Vec[T] spec.
  • D239[]T as sugar over Vec[T] (Plan 138.1).
  • D141[]T bulk slice API + value-struct limitation.
  • [M-91.1-value-struct-array-elem] — исходный gap marker (закрыт через Vec[T] Plan 131).
  • Plan 138.2 — universalization: Vec в prelude, NovaArray retirement.

Q28. Postfix dereference syntax: p.* vs *p — ✅ ЗАКРЫТО (2026-06-13, оставлен prefix *p)

Вопрос: заменить ли prefix dereference *p на postfix p.* (Zig) или p* (чистый постфикс)?

Мотивация

Postfix читается слева направо при цепочках:

p.*         // vs *p
p.*.field   // vs (*p).field
arr[i].*.method()  // vs (*arr[i]).method()
p.* = v     // vs *p = v

Варианты

A. p.* (postfix с точкой, Zig-стиль)

  • Однозначно: . после выражения не бывает умножением → parser без ambiguity
  • Прецедент: Zig использует именно ptr.*
  • Читается как «доступ к dereferenced значению» — согласуется с obj.field
  • Минус: добавляет . в deref-синтаксис, который иначе используется только для member access

B. p* (чистый postfix без точки)

  • Минималистично, но парсер сложнее: p* + 1(p*) + 1 или p * (+1)?
  • Pratt-parser трактует * после выражения как binary если за ним идёт начало expr → p* + 1 = p * (+1) (неверно)
  • Требует whitespace-чувствительности или особых правил

C. Оставить *p (prefix, C/Rust-стиль)

  • Привычно, без изменений
  • Минус: при цепочках нужны скобки (*p).field

Решение (2026-06-13, design-workflow: 4 независимые перспективы + синтез-судья, факты выверены по коду)

→ Вариант C: оставить prefix *p. Постфиксная мотивация (left-to-right при цепочках) реальна, но для Nova не окупается, а точные постфиксные формы либо неграмматичны, либо ломают симметрию.

  • B p* (чистый постфикс) — НЕГРАММАТИЧЕН (подтверждено): лексер выбрасывает пробелы и НЕ хранит adjacency-флаг (lexer/mod.rs:309) → a*, a *, a*⏎b дают идентичный поток Ident Star …; а * — И prefix-deref (parse_unary:6320) И infix-mul (parse_mul:6161) → p* неотличим от начала p * y без unbounded lookahead. Настоящая ambiguity. (Аналогично мёртв p^ Pascal: ^ уже занят bitwise-xor — lexer:265BinOp::BitXor.) Ни один зрелый язык не взял голый p* — именно поэтому постфиксные deref всегда добавляют разделитель (Zig ., Pascal/Odin ^).
  • A p.* (Zig) — грамматичен (ведущая . снимает ambiguity; ~3 строки в parse_postfix:6340), но не окупается для Nova: сырой *p редок (~57 сайтов, почти всё тест-фикстуры, 1 production-сайт в std/collections/vec_owned.nv), а форма (*p).field, которую постфикс бы починил, встречается в 1 файле из 2989 — авто-deref (p.field) + индексация (p[i]) поглощают весь chaining. A ценой ~50-65 переписываний ломает симметрию *T/*p и пару &/*, зацементированную Plan 147/D246.
  • C *p — WIN на всех 4 осях: парсится (зашипён), эргономика совпадает с реальным usage (deref почти всегда bare-unary *p = v / ro v = *p), доминирующая конвенция (C/C++/Rust/Go/D + Carbon, чистый лист 2020-х, явно рассмотрел постфикс/whitespace-disambig и оставил prefix, добавив -> сахар), симметричен с prefix-типом *T и инверсной парой &/*. Ноль миграции.

Симметрия «тип справа → deref справа» (мотив автора; Zig = *T + .* её валидирует) — реальная ось дизайна, но конкурирует с «* префиксит то, к чему применяется» (*T/*p; &x/*p-инверсия). Тай-брейк решают парсер (B/p^ мёртвы) + cost/benefit (A не окупается).

Отложенная опция

p.* (Zig) — единственная грамматичная постфиксная форма; вернуться только если сырой deref станет частым или потребуется chained-навигация (сейчас — нет). Тогда: ~3 строки в parse_postfix (ветка DotStar) + миграция ~50-65 сайтов + spec/docs (D216 §5, examples/typed_pointers/basic_pointer.nv).

Cross-refs

  • Plan 118 / Plan 147 (D216 §5, D246) — типизированные указатели *T / *mut T / *unsafe T (prefix-тип).
  • Plan 138.5 — pointee-мутируемость в типе (*mut T) vs reassign-ability в binding (ro/mut); ортогонально deref-выражению *p этого Q28.

Q29. Safe references: ref T / ref mut T — нужны ли?

Вопрос: вводить ли safe reference тип (ref T, ref mut T) как альтернативу raw pointer для in-place мутации и передачи больших стековых значений?

Мотивация

ro r = a[1]    // r: int       — копия
ref r = a[1]   // r: ref int   — ссылка на элемент (без копии)

fn process(x ref BigStruct) -> int   // передача без копии, без unsafe
fn mutate(x ref mut BigStruct)       // мутация caller-значения

ref vs &: &x занят как addr_of (Plan 118.1) → ключевое слово ref.

Соответствие

NovaC++Rust
ref intconst int&&i32
ref mut intint&&mut i32
*intconst int**const i32
*mut intint**mut i32

Аргументы ЗА

  • get_mut() -> ref mut T вместо *mut T — без unsafe, чище API
  • Большие named tuples на стеке без heap-промоции
  • a[1] как ref int — единая семантика вместо lvalue-магии

Аргументы ПРОТИВ

  • Heap-типы (records, []T, str) уже передаются как GC-pointer — references ничего не добавляют
  • Value records автопромотируются в heap (Plan 127) — большинство кейсов покрыто
  • a[1] = 5 lvalue-синтаксис покрывает мутацию элементов
  • Lifetime tracking — дополнительная сложность в checker даже без аннотаций
  • Go обходится без references; Nova с GC нуждается ещё меньше

Текущий статус

2026-06-21 (владелец): explicit ref T / ref mut T как ТИП НЕ вводим. Вместо явных ссылок-типов — автоматический вывод компилятором: by-reference vs by-value (и heap vs stack) — это ABI/placement-решение лоуэринга, выводимое из типа + escape-анализа, а НЕ часть синтаксиса/типа (D315: «ABI выводится, не хранится»). Конкретные оптимизации (авто-by-ref параметров; авто heap↔stack, в т.ч. нерекурсивный sum→стек) вынесены в Q-value-abi-auto-placement / Plan 172.4.

⚠ 2026-06-26 (владелец) — ЧАСТИЧНО ПЕРЕОТКРЫТО: ref как РЕЖИМ ПАРАМЕТРА вводим (D326 / Plan 172.5). Снимается отвержение для param-mode формы ro ref a T / mut ref a T (модель Swift inout / C# in+ref: param-only, БЕЗ типа, БЕЗ лайфтаймов, узкая call-site эксклюзивность E_REF_ALIAS_OVERLAP; call-site маркер inc(ref x)). Причина пересмотра: обоснование 2026-06-21 «safe in-place мутация покрыта lvalue + mut @» имеет дыру — mut @ мутирует только ресивер, НЕ покрывает out-параметры / мутацию не-ресивера / несколько mut-аргументов (swap(mut ref a, mut ref b)). Формализуются также @mut/ro ref @ и -> @ref @. ref как ТИП остаётся отвергнут (D326 R1). Авто-ro ref + @/-> @ + heap↔stack — это Plan 172.4. НЕ переносить в history/rejected.md — фича частично принята.

Update (Plan 138.5 → FINAL три оси Plan 147 D246, 2026-06-12; D245 flip-scan отклонён): raw-pointer mut-модель финализирована как три ортогональные оси (D246): L1 binding (reassignability имени), L2 view (транзитивный freeze owned-графа, стена на *), L3 pointee-capability (из ТИПА, постфикс). В типе указателя мутируемость = L3 pointee (target), позиционно-независимо: *mut T = writable target, *T = ro. *T ≡ *ro T УНИВЕРСАЛЬНО (восстановлено): bare *T НЕ наследует binding (flip-scan отклонён — тип не был самодостаточен); writable pointee — только через явный *mut T. *ro TE_REDUNDANT_POINTER_RO (fix-it *T); запись через ro-pointee → E_POINTER_RO_ASSIGN. Перепривязываемость указателя — L1 binding (ro/mut, D36), независима от L3. Prefix-модификаторы перед * (mut * T / ro * T / unsafe * T) запрещены (E_POINTER_PREFIX_MODIFIER), Unsafe(Pointer) retired, nullable = Option[*T] (NPO). Right-binding propagation через Pointer (D216 §V3.3) и safe-стоппер (§V3.4) отозваны. Это снимает мотивацию «ref T как способ избежать путаницы raw-pointer mut»: путаницы больше нет; остаётся лишь чистый эргономический вопрос safe in-place мутации без unsafe.

Cross-refs

  • Plan 118 — *T / *mut T raw pointers
  • Plan 127 — value record heap promotion
  • Plan 120 — named tuples (stack allocation)
  • Plan 138.5 — FINAL pointer model (pointee-mut postfix; reassignability = binding; nullable = Option[*T]; §V3.3 propagation + safe-stopper retired)
  • Q28 — postfix deref p.*

Q-value-abi-auto-placement — единый value-ABI + авто-placement (by-ref / heap↔stack) (2026-06-21)

Статус: 🟡 OPEN, направление согласовано с владельцем 2026-06-21. Зависит от единого носителя ResolvedType (D315) — ABI/placement выводятся из ОДНОГО окна типа, поэтому это локальная правка ОДНОГО лоуэринга, а не разбросанные ветки. Делать ПОСЛЕ MVP-консолидации 172.1 (сейчас byte-identical).

Вопрос: компилятор должен единообразно обращаться со всеми value-типами и автоматически выбирать передачу/размещение, без явных аннотаций (Q29: explicit ref T отвергнут).

Три связанных пункта:

  1. Единый value-ABI (убрать раздельную обработку). type Job value {}, Tuple(int, str), Tuple(a int, b str), named tuple — это ОДИН класс value-типов, но сейчас лоуэрятся РАЗНЫМ кодом (NovaValue_X vs NovaTuple_X; приёмник: heap-record / value-record by-pointer, а named-tuple ro @ by-valuePlan 128/§4072 02-types.md). Эта раздельность — источник багов (владелец, 2026-06-21). Цель: ОДИН путь representation+приёмника+передачи из ResolvedType. Вектор — U.5/U.6 (свернуть tuple/value-record/sum-schema дубли).

  2. Авто-by-ref передачи параметров. Большие value-значения передавать указателем автоматически (вместо копии), когда копия не наблюдаема — это и есть «ref T автоматически» (закрывает ЗА-аргумент Q29 «передача больших стековых значений»). Foundation: escape_analyze.rs (Plan 127) уже авто-промоутит value-records; обобщить на by-ref-передачу.

  3. Авто heap↔stack placement, в т.ч. нерекурсивный sum→стек. Sum-типы сейчас по умолчанию heap (Nova_X*, 02-types.md:2454) — это правильно для рекурсивных (деревья/связи). Но большинство sum — перечисления/нерекурсивные и могут быть авто-свёрнуты в стек-value (tagged-union), как уже сделано для билтина Option (NovaOpt stack-value-sum) vs Result (heap) — Plan 62.A.bis. Foundation готов: детектор рекурсии существует (size-калькулятор, 08-runtime.md:8211-8235: «рекурсивный heap-тип — указательный лист»), value-стек-механизм — D228/D277/D290. Дедик-пункта на это НЕТ — фиксируется здесь. (Узкий частный случай — payload-less explicit-base enum → typedef <c_type> — это Plan 105, proposed.)

Зачем: перф (нет лишних аллокаций/копий — bench-цель §2 conventions, «быстрый сгенерированный код») + меньше кода/багов (одно окно). Граница: соундность важнее — placement выводится консервативно (любая неопределённость/рекурсия → heap, как Plan 127 over-promote).

Cross-refs: Q29 (no explicit ref T); D315 (ABI выводится); Plan 127 (escape→promote); Plan 62.A.bis (NovaOpt stack / Result heap); Plan 128 (receiver ABI — раздельность под унификацию); Plan 105 (explicit-base enum); D228/D277/D290 (value-record stack); 172.1 U.5/U.6 (единый носитель/repr).


Q30. Computed properties: @name() getter / mut @name(val) setter

Вопрос: вводить ли механизм computed properties — методы с именем поля (@name() / mut @name(val)), которые прозрачно вызываются при чтении/записи через dot-синтаксис?

Мотивация

Типичный паттерн: приватное поле + публичный доступ с валидацией или lazy-вычислением:

type Circle {
    priv radius f64   // приватное поле — снаружи недоступно
}

fn Circle @radius() -> f64 => @radius              // getter: @radius = поле (Plan 132)
fn Circle mut @radius(r f64) {                      // setter с валидацией
    if r < 0.0 { panic("negative radius") }
    @radius = r                                     // @radius = поле (Plan 132)
}
fn Circle @area() -> f64 => 3.14159 * @radius * @radius  // computed, без backing field

Аналог: C# properties, Swift var { get set }, Kotlin val/var с get()/set(), Python @property.

Связь с Plan 132

Plan 132 (2026-06-09) установил инвариант:

  • @name (без ()) в теле метода = поле name
  • @name() = вызов метода name
  • Поле и метод могут иметь одно имя на одном типе (коллизия разрешается синтаксически)

Это создаёт фундамент: @name() = метод-читатель, mut @name(val) = метод-писатель.

Проблема: call-site синтаксис

Три варианта для внешнего вызова геттера:

Вариант A — Невидимое свойство (C#-style)

circle.radius      // вызывает @radius() если определён, иначе поле
circle.radius = 5  // вызывает mut @radius(5) если определён, иначе запись поля
  • ✅ Чистый API для клиента
  • ❌ Нарушает принцип D37: «поле или вызов — непонятно»; читатель кода не знает

Вариант B — @ на call site

circle.@radius      // явно геттер — @ сигнализирует «магия»
circle.@radius = 5  // явно сеттер
  • ✅ Однозначно: obj.name = поле, obj.@name = computed property
  • ✅ Нет конфликта с Plan 132: obj.@method удалён как hard error E_BOUND_METHOD_REMOVED — синтаксис свободен, старой семантики нет; новая семантика (property-call) ложится на чистое место

Вариант C — Явные скобки (текущее поведение)

circle.radius()       // геттер — просто обычный метод
circle.set_radius(5)  // сеттер — именовая конвенция
  • ✅ Никаких изменений в компиляторе
  • ❌ API выглядит как метод, не как свойство; нет единого синтаксиса для = записи

Аргументы ЗА (любой вариант)

  • Инкапсуляция с валидацией без изменения external API
  • Computed/derived поля (area, full_name) без side-effecting методов
  • Протоколы могут требовать геттер: type Named protocol { @name() -> str }
  • Согласуется с @index / mut @index (Plan 138) как единая magic-методов модель

Аргументы ПРОТИВ

  • D37 явно отклонил невидимые properties (Вариант A)
  • Nova делает скобки обязательными — «методы видны» (Вариант B/C меньше страдают)
  • Для большинства кейсов val() + set_val(v) достаточно

Текущий статус

Не реализовано. D37 отклонил Вариант A. Варианты B и C — открыты.

Вариант B наиболее Nova-идиоматичен: @ уже означает «магия/оператор» в языке, obj.@name не имеет старого значения (Plan 132 вычистил bound-value до hard error), синтаксис свободен для новой семантики.

Cross-refs

  • D37 — «скобки обязательны; property-механизм отклонён»
  • Plan 132 — field/method одного имени; удаление obj.@method bound-value
  • Plan 138 — @index / mut @index (write indexing) — родственный механизм
  • Q29 — safe references (альтернативный путь к in-place мутации)

Q31. Conditional protocol bound ([T Clone]) на generic instance-методе

Вопрос: должен ли язык поддерживать conditional bound на параметре типа, объявленный на уровне отдельного метода, более узкий чем bound на самом типе?

Контекст (Plan 138.3, 2026-06-10)

Clone зафиксирован как deep/recursive (D230 amend). Коллекции клонируются поэлементно, и Vec[T].clone() имеет смысл только если T реализует Clone. Желаемая форма — зеркало Rust impl<T: Clone> Clone for Vec<T>:

type Vec[T] { ... }                       // сам тип — без Clone-bound (хранит любой T)
export fn Vec[T Clone] @clone() -> Self {  // метод — добавляет Clone-bound на T
    mut out = Vec[T].with_capacity(@len)
    mut i = 0
    while i < @len {
        out.push(unsafe { @data[i] }.clone())
        i = i + 1
    }
    out
}

Тип Vec[T] хранит любой T (push/from/extend — без bound). Но @clone требует T Clone. То есть bound на методе уже bound на типе.

Открытые места

  1. Поддерживает ли bootstrap-checker narrowing-bound [T Clone] на instance-методе, когда сам тип объявлен как Vec[T] (без bound)? (R1 риск Plan 138.3 — проверяется в Ф.2.)
    • Если даVec[non-Clone-T].clone() корректно даёт ошибку bound на call-site.
    • Если нет — fallback: отдельный free-fn vec_clone[T Clone](v) -> Vec[T], либо bound на весь тип (ломает Vec[T] без Clone), либо document gap.
  2. Сообщение об ошибке на call-site Vec[Widget].clone() где Widget не Clone: должно указывать на отсутствие #impl(Clone) у Widget, не на сам Vec.
  3. Взаимодействие с auto-derive #impl(Clone) для records: Vec[Record].clone() компилируется только если у Record есть #impl(Clone) (D230 §Field-eligibility). Bare records без аннотации → bound-ошибка. Нужно ли расширить auto-derive (followup [M-138.3-autoclone-records])?

Статус

Отвечен эмпирически (Plan 138.3 Ф.2-Ф.4, 2026-06-10; codegen-dispatch ЗАКРЫТ Plan 138.4 Ф.1, 2026-06-11).

  1. Method-level narrowing-bound [T Clone] — ПРИНИМАЕТСЯ bootstrap-парсером и type-checker’ом (нет CC-FAIL на самой сигнатуре). Для record element-типов emit корректен: Vec[Point].clone() даёт верную Point.@clone() рекурсию.
  2. Codegen-dispatch для примитивного T — ✅ ИСПРАВЛЕН (был [M-138.3-clone-bound-unsupported], CLOSED в Plan 138.4 Ф.1 G-C, commits 88432dd6f02 + 363f4b53788). ROOT CAUSE: НЕ монформизатор — single-key last-wins method_receivers["clone"] instance-fallback роутил unbound primitive-T .clone() в неродственный @clone. FIX: PrimBuiltin::Identity.clone() на примитиве = bitwise self + record/heap identity-clone arm. Deep Vec[int].clone() / deep HashMap[str,int].clone() теперь GREEN.
  3. Deep collection-@clone с [T Clone] — АКТИВНА (восстановлена в Plan 138.4 после первоначального VERIFY-OR-KEEP shallow в 138.3). Spec-контракт (D230 amend) — impl догнал prose; §«KNOWN GAP» помечен закрытым.
  4. Auto-derive #impl(Clone) для records — работает (plan126_2 p3/p7 PASS); bare-record без #impl(Clone) → bound-error. [M-138.3-autoclone-records] остаётся открытым followup на случай расширения покрытия.

Остаётся открытым только в части языкового дизайна (нужен ли method-level narrowing-bound как first-class фича) — практический блокер (codegen-dispatch) закрыт в Plan 138.4.

Cross-refs

  • D230 amend (Plan 138.3) — Clone = deep, collection element-wise
  • D237 — protocol naming (Clone, метод @clone)
  • Plan 138.3 — home plan; Plan 138.4 Ф.1 — codegen-dispatch fix (G-C)
  • [M-138.3-clone-bound-unsupported] — ✅ CLOSED (Plan 138.4 Ф.1, 88432dd6f02 + 363f4b53788)
  • [M-138.3-autoclone-records] — auto-derive Clone для records (если bound-audit вскроет gap)

Q32. Семантика structural-== для циклических record-графов

Вопрос: как должно вести себя структурное равенство (==) для типов с циклической ссылочной структурой — type A { b B }; type B { a A }, или self-referential type Node { next Node }?

Контекст (Plan 141, 2026-06-11)

Plan 141 заменил побитовый memcmp на field-by-field structural-== (D109 amend): кортежи, sum-payload и record-поля сравниваются рекурсивно по своей структурной схеме. Для не-циклических типов схема конечна — рекурсия терминируется.

Для циклических record-графов наивная structural-рекурсия не терминируется (бесконечный обход цикла). V1 ставит depth-cap guard в codegen (предотвращает runaway-эмиссию), но это не определяет семантику равенства цикла.

Открытые места

  1. Что значит a == b для двух циклических графов? Варианты:
    • Bisimulation / graph-isomorphism (Python-style == с memo-set посещённых пар) — корректно, но O(N²) и требует runtime visited-set.
    • Identity-eq fallback для self-referential полей (сравнить pointer ссылки на цикл) — дёшево, но «структурное» только частично.
    • Запрет structural-== на циклических типах (E_EQ_CYCLIC_TYPE) — заставить пользователя писать explicit @equal.
  2. Когда обнаруживать цикл? Type-check (как E_AUTO_DERIVE_CYCLE для Clone, D109 amend Plan 126) или runtime (visited-set)?
  3. Совместимость с auto-derive #impl(Equal)Clone уже даёт E_AUTO_DERIVE_CYCLE на циклических типах; следует ли Equal зеркалить это поведение, или поддержать bisimulation как отличие?

Статус

Открыт. V1 (Plan 141) — depth-cap guard в codegen; циклические record-графы out-of-scope. Практически тип-граф в текущем коде ацикличен (sum/record без self-reference через value-поля). Закрыть при появлении реального self-referential value-record use-case.

Cross-refs


Q33. Глубина ro и эксклюзивность в GC-aliased модели (3-axis D246)

Вопрос: как далеко должна простираться немутабельность ro, и нужна ли когда-нибудь эксклюзивность владения, в модели с GC + aliasing без borrow-checker?

Контекст (Plan 147, D246, 2026-06-12)

3-axis модель (L1 binding / L2 view / L3 pointee-capability) приняла сознательные trade-off’ы: ro = per-PATH запрет записи через данное имя, НЕ глобальная иммутабельность объекта. Это sound в GC-модели (C4: нет borrow-checker, aliasing разрешён), но оставляет 3 открытых места.

Открытые места

  1. Deep-immutable сквозь *mut нельзя навязать снаружи (C++ shallow-const). -> ro VR где VR { p *mut T }: морозит слоты VR, но unsafe{ *v.p = w } проходит (L2 — стена на *). Deep-ro требует, чтобы производитель объявил поле *T (как str { ptr *u8 }). Нужен ли когда-нибудь consumer-imposable deep-ro? V1 — нет.
  2. Shared-mut heap-record под чужим ro. Handle A ro a, handle B mut b алиасят один GC-объект; B мутирует → A видит вопреки ro. Per-path-семантика, не баг. Нужна ли opt-in эксклюзивность (uniq/owned) для гарантии object-freeze?
  3. owned-vs-aliased heap-record статически неразличим → граница СИНТАКСИЧЕСКИ на * (L2 стоп на *), не по aliasing-статусу (неразрешим без ownership-tracking). Нужны ли ownership-аннотации для точной границы (heap-record over-restrictive / *mut under-restrictive)?

Возможные направления (НЕ V1)

  • Ownership-tracking / borrow-checker-lite (Rust) — отвергнут C4 ради GC-простоты; пересмотр если понадобится compile-time гарантия object-immutability/exclusivity.
  • uniq/owned opt-in (Pony-style reference capabilities) — точечная эксклюзивность без полного borrow.
  • Принять per-path семантику финальной (контракт: ro = «это имя не пишет», не «объект заморожен»).

Связь

  • D246 — 3-axis модель + §trade-off (Plan 147).
  • C4 (GC + aliasing + нет borrow-checker) — фундамент.
  • Cross-ref: D175 §V2 (L2 binding-dominates / wall-at-*), D216 §V2.6 restored (*T≡*ro T).

Q34. Политика enforce-in-release для контрактов (Plan 140, D24 amend)

Вопрос: какие именно политические настройки даёт разработчику новая модель «contracts enforced in release» (D24 amend, Plan 140), и каковы их дефолты?

Контекст (Plan 140, D24 amend, 2026-06-12)

Plan 140 заменил legacy-модель «недоказанные контракты стираются в release (zero-cost, как assert/NDEBUG)» на enforce-with-elision:

  • Z3-доказанный контракт элидируется на codegen (zero-cost);
  • недоказанный — runtime-проверка остаётся и в release; нарушение → nova_contract_violation (fail-fast abort);
  • снять проверку с недоказанного — явно: #unchecked (per-fn) или --contracts=off (build).

Семантика принята (D24 amend). Открытыми остаются три политических параметра — детали, не дизайн; фиксируются по ходу Ф.1–Ф.5.

РЕШЕНО (Plan 140 Ф.2, 2026-06-12): пункты 1 и 3 закрыты как реализованные. (1) Default build-policy = enforce; единственный полный build-opt-out — --contracts=off (enforce|warn|off трёхуровневый отвергнут — warn не убирает silent-UB-риск). (2) Violation в release = abort (реализовано Ф.1; panic-unwind — followup [M-140-contract-panic-unwind]). (3) Гранулярность = per-fn #unchecked + build --contracts=off; per-module — отложено ([M-140-contract-levels]).

ЗАКРЫТО (Plan 140 Ф.5, 2026-06-12): Q34 closed. Все три политических параметра зафиксированы (выше); proven-элидирование измерено zero-cost (микробенч perf_contract_hot_loop: Z3-elide ≈ --contracts=off baseline; Trivial-checked ≈ +12% на contract-saturated loop — см. D24 amend Perf-блок). Plan 140 закрыт целиком (Ф.0–Ф.5, F1–F7 met). Остаточные followups ([M-140-contract-panic-unwind], [M-140-contract-levels], [M-140-bounds-as-contract]) — по запросу, не блокеры.

Открытые места

  1. Default build-policy. ✅ РЕШЕНО (Ф.2): enforce-by-default + явный opt-out (--contracts=off восстанавливает legacy zero-cost целиком). Альтернативы отвергнуты: opt-in (--contracts=enforce по требованию — возвращает дыру silent-UB по умолчанию); трёхуровневый enforce|warn|off (warn не снимает risk). enforce default; off — единственный полный opt-out на build-уровне.

  2. Поведение violation в release: abort vs panic-unwind.

    • abort (предложение, дефолт): nova_contract_violation печатает структурное сообщение (PRE/POST, fn, src, file:line) и немедленно завершает процесс (fail-fast, не разматывая стек). Просто, надёжно под NDEBUG/LTO, не зависит от состояния fiber-runtime. Согласуется с D13 («нарушение контракта — это panic»), но в наиболее жёсткой форме (panic = abort, без unwind/okrescue).
    • panic-unwind: нарушение → обычный Nova-panic, ловится supervised-scope / okrescue. Гибче (graceful degrade сервиса), но дороже и тоньше под release/LTO; контракт-violation становится «recoverable», что размывает fail-fast-инвариант.
    • Предложение: в release abort (контракт-violation = баг программы, не recoverable ошибка); panic-unwind — возможный followup [M-140-contract-panic-unwind] если потребуется graceful degrade.
  3. Гранулярность opt-out. ✅ РЕШЕНО (Ф.2): V1 = per-fn + build.

    • per-fn #unchecked — точечно для hot-path (✅ реализовано Ф.2).
    • build --contracts=off — глобально, legacy (✅ реализовано Ф.2).
    • per-module #unchecked_module / манифест — отложено; followup [M-140-contract-levels] по запросу (Eiffel-style гранулярность pre/post/invariant — там же).

Возможные направления / followups

  • [M-140-contract-panic-unwind]РЕШЕНО (Plan 140.3, 2026-06-14) — переосмыслено: не «abort→unwind» (assert/контракт в файбере уже разматываются к fail-frame), а унификация классификации: nova_assert_loc/ nova_contract_violation теперь тегают fail-frame error_kind = NOVA_THROW_PANIC (как nv_panic, D188) → пойманное consume/supervised классифицируется как Panic (не recoverable Failure), по spec D13. Commit 60e909a0.
  • [M-140-contract-levels]РЕШЕНО (Plan 140.3, 2026-06-14) — per-module opt-out (#unchecked перед module X, голое имя) + Eiffel per-kind #unchecked(requires/ensures/invariant) (fn И module). ContractOptOut + per-kind gate contracts_elided_for(kind). Commit 77932ea3.
  • [M-140-bounds-as-contract]РЕШЕНО (Plan 140.2) — Vec @index OOB как requires 0 <= i && i < @len (D256/D257).

Связь

  • D24 — AMEND (Plan 140): enforce-with-elision; «стираются в release» retracted.
  • D106verify_status/verify_mode; proven feeds release-элидирование.
  • 08-runtime.md → D13 — нарушение контракта в runtime — это panic (abort-vs-unwind уточняется здесь).
  • Plan 140 docs/plans/140-contracts-enforced-in-release.md — Ф.0–Ф.5, acceptance F1–F7, risk register.

Q35. Сравнение с bool-операндом / chained comparison (a <= b < c) — ✅ ЗАКРЫТО (2026-06-13, Plan 150 / D248)

Вопрос: что Nova должна делать с выражением a <= b < c (chained comparison) и со сравнением, у которого операнд — bool?

Контекст (2026-06-12, всплыло при Plan 140 [M-140-bounds-as-contract])

Сравнения лево-ассоциативны и возвращают bool. Поэтому 0 <= i < @len парсится как (0 <= i) < @len = bool < @len. Nova молча принимает bool < int (coerce bool→0/1 + сравнение): результат всегда true (bool ∈ {0,1} < @len при @len>1) — вакуумно-истинно, range-check НЕ выполняется. Подтверждено: assert(0 <= 100 < 10) → PASS (хотя 100 вне [0,10)).

Тихий footgun. Особо опасен для контрактов: requires 0 <= i < @len стал бы вакуумным контрактом = ноль безопасности (пишется естественно, выглядит правильно, не проверяет ничего).

Варианты

(a) Hard error на bool-операнд сравнения (</<=/>/>=; ==/!= осмысленны для bool — не трогать) → E_COMPARISON_BOOL_OPERAND с fix-it- подсказкой «возможно, имелось в виду a <= b && b < c?». Минимально, ловит footgun, без нового синтаксиса. Прецедент: Rust/C++ (a < b < c = type error).

(b) Полноценный chained comparison (Python-стиль) — a <= b < ca <= b && b < c, средний операнд вычисляется ОДИН раз (нужен temp). Читаемо для range-check, но: новый парсер-правило + взаимодействие с прочими операторами. Прецедент: Python (единственный мейнстрим; C/Rust/Go/Java — нет).

(c) Статус-квоbool < int валиден. Отвергается: тихий footgun без выгоды.

Предложение

(a) hard error для V1 (дёшево, закрывает footgun, без нового синтаксиса); (b) chained — отдельным предложением, если будет спрос (ergonomics). НЕ (c).

Решение автора (2026-06-13, после design-workflow + adversarial-judge)

Plan 150: вариант (a) hard-error. Chained comparison ОТКЛОНЁН — как Go/Rust/Kotlin/Java/Swift (только Python чейнит; a <= b && b < c явно, без нового синтаксиса). a OP1 b OP2 cE_CMP_CHAIN_UNSUPPORTED + machine-applicable fix-it a OP1 b && b OP2 c (Rust-grade). bool/unit как операнд </<=/>/>=E_RELATIONAL_OPERAND_NOT_ORDERED (==/!= на bool — легальны). Канонная форма диапазона = &&. Если в будущем будет спрос на chaining — отдельное предложение (не сейчас).

✅ ЗАКРЫТО 2026-06-13 — Plan 150 Ф.0-Ф.1 landed: D248 spec + parser (parse_cmp/parse_eq: ≥2 сравнений → E_CMP_CHAIN_UNSUPPORTED, paren-aware) + checker (bool/unit relational-операнд → E_RELATIONAL_OPERAND_NOT_ORDERED). Полный check-sweep 2938 файлов = 0 ложных срабатываний; 8 негативных + 5 позитивных фикстур (nova_tests/plan150/).

Связанное

  • [M-comparison-bool-operand-or-chaining] (backlog, home Plan 150) — реализация.
  • [M-140-bounds-as-contract] (Plan 140): контракт должен быть requires 0 <= i && i < @len, НЕ 0 <= i < @len.
  • 03-syntax.md — операторы сравнения, precedence/ассоциативность.

Q-vec-alias-completeness — []T — чистый алиас Vec[T] (Plan 153.0)

Контекст. D239 объявил []T ≡ Vec[T] (синтаксический псевдоним). Открытый вопрос: завершён ли алиас (нет остаточных спец-кейсов []T в компиляторе), и консолидирован ли модуль (vec.nvvec_owned.nv дубль).

✅ ЗАКРЫТО 2026-06-13 — Plan 153.0 (plan-153 commit 2a5df8e4):

  • Vec[T] переехал в folder-модуль std/collections/vec/ (co-equal слои), модуль collections.vec_owned ретайрнут, vec.nv свёрнут, prelude re-export’ит из folder. Дубля больше нет.
  • []T ≡ Vec[T] подтверждён (D239 CONFIRM, 02-types.md): литерал [1,2,3] строит Vec[int], инферированный Vec[int] передаётся в []int-параметр, []T-методы резолвятся на Vec[T].

Остаточный спец-кейс (отложен). ЯВНАЯ аннотация ro v Vec[int] = … НЕ коэрсится в []int-параметр (E7301), хотя инферированный коэрсится — pre-existing на main, residual непрозрачность алиаса для explicit-annotation coercion. Home: [M-153-d239-explicit-vec-to-slice-param] (type-checker coercion fix). Pin: nova_tests/plan153_0/neg/vec_explicit_annotation_to_slice_neg.

Связанное (Plan 153)

  • Q-iterator-laziness / Q-iter-mut — ✅ закрыты по факту реализацией (153.2, ниже). Q-vec-operator-plus — ✅ ЗАКРЫТО (153.5, отдельная запись ниже). Q-slice-view — закрыт записью в плане 153 §4 (реализация в 153.4). Переносятся сюда по мере закрытия их sub-планов.
  • Отклонение 153.0: eager-комбинаторы вынесены в collections.vec_seq (не prelude-global) — иначе их идентификаторы засоряют каждый юнит. См. план §«Статус 153.0» + [M-153-vec-combinators-prelude-global].

Q-iterator-laziness — eager []T.map vs ленивый итератор (Plan 153.2)

Контекст. До 153.2 []T.map(f) -> []U материализовал на каждом шаге (O(n) промежуточных аллокаций на цепочке); VecIter умел только next(). Вопрос: ленивый итератор (Rust iter()+adapters / Kotlin Sequence / Java Stream) — канон, и что делать с eager-комбинаторами?

✅ ЗАКРЫТО 2026-06-14 — Plan 153.2 Phase A (plan-153.2-mono-closures, commits 996ca01a лифт + caf56226 ленивый слой; D260):

  • Ленивые адаптеры — КАНОН (этос cost-transparency, D135). Реализованы по boxed-fluent-модели: BoxIter[T] { priv step fn() -> Option[T] }, вход v.lazy() мостит VecIterBoxIter, адаптеры fluent-методы → новый BoxIter, терминаторы тянут цепочку. Промежуточных аллокаций нет (pull, по одному элементу) — доказано инструментацией (plan153_2/laziness: счётчик считает только протянутые элементы, take/find/any/all/nth коротят). A-набор: map/filter/filter_map/ enumerate/take/skip + collect/fold/reduce/count/sum/any/all/find/ for_each/min/max/nth/last.
  • Eager []T.map — НЕ deprecated, НЕ переписан в сахар. Eager collections.vec_seq оставлен без изменений как переходный eager-surface; lazy (collections.vec_lazy) — канонический allocation-free путь. Оба за раздельными explicit-import. Eager НЕ выражен сахаром над lazy сознательно — чтобы не навязывать lazy-import eager-пользователям, и потому что prelude-global closure-несущий метод засоряет каждый юнит (тот же scope-leak, что держит vec_seq вне prelude; [M-153-vec-combinators-prelude-global]).
  • Phase B (заявленный полный набор) отложен за маркерами — zip/chain/flat_map/ flatten/scan/step_by/take_while/… + into_iter. Это roadmap, НЕ упрощение.

Связанное

  • D260 — модель + codegen-инварианты.
  • [M-153.2-generic-over-source-zerocost] — zero-cost (un-boxed) апгрейд поверх boxed.
  • [M-153.2-tuple-elem-adapter] — tuple-PRESERVING-адаптер после enumerate.

Q-iter-mut — мутабельная итерация Vec[T] (Plan 153.2)

Контекст. Нужна ли мутабельная итерация (iter_mut в Rust, индексами в Kotlin) и в каком виде? Имя iter_mut или receiver-mut overload?

✅ ЗАКРЫТО (дизайн) — Plan 153.2. Мутабельная итерация — через for mut x in v (write-through в буфер) + mut @iter() (receiver-mut overload @iter, НЕ имя iter_mut). Семантика как мут-view (mut []T): write ок (write-through в буфер мастера), рост → detach (Plan 96 модель). Согласуется с accessor receiver-mut конвенцией (Plan 135, @as_ptr/mut @as_ptr).

Реализация — Phase B. В Phase-A-слое (vec_lazy.nv) реализованы read-only ленивые адаптеры/терминаторы; write-through for mut x / mut @iter() — отдельный codegen-путь (lvalue write-back в буфер), приземляется в Phase B вместе с остальным B-набором. Дизайн-решение зафиксировано (имя НЕ iter_mut, а receiver-mut overload).

Связанное

  • D260 — ленивый итератор (read-side).
  • Plan 96 / Plan 135 — мут-view mut []T write-through + receiver-mut accessor-конвенция.

Q-vec-operator-plus — оператор + для Vec[T] (Plan 153.5)

Контекст. Нужен ли Vec[T] оператор +, и какая у него семантика — мутирующая (растит левый операнд, как append) или non-mutating (новый Vec)?

✅ ЗАКРЫТО 2026-06-14 — Plan 153.5 (plan-153.5-restructure commit e8f700e4, D263):

  • a + b = НОВЫЙ Vec[T]Vec[T] @plus(other) -> Vec[T] => @concat(other) (как str @plus, D46). Операнды не мутируются (как Kotlin/Python/Ruby +). Прецедент выбран осознанно: + на коллекции = чистая конкатенация, не сайд-эффект.
  • a += ba = a + b (свежий concat-Vec) — НЕ in-place append. Единая семантика оператора (compound-assign лоуэрится через тот же @plus). Рост левого операнда in place остаётся за явным a.append(b) (mutate.nv) — две разные операции, два разных имени (инвариант I4 плана: один слой = одна семантика).
  • Codegen (emit_c.rs): operator-lowering +/+= маршрутизируется в Nova-body @plus через vec_method_call (регистрирует mono-инстанс перед эмиссией) + десугаринг +=Binary{Add} (сырой C a += b на struct/pointer-операнде нелегален).
  • Тесты plan153_5/restructure (POS: non-mutation +/concat, += append) — релизный nova C-codegen, 5/5 PASS.

Q36. Scope call-site instantiation для @field-контрактов (Plan 140.2, D256/D257) — ✅ ЗАКРЫТО (2026-06-13)

Вопрос: при поддержке @field в контрактах (D256) — какой MVP-scope у call-site instantiation? (a) within-function (метод доказывает о своём @field в ensures/invariant) vs (b) full cross-function (caller доказывает контракт callee, подставляя свой receiver: для @index requires 0<=i && i<@len_selfv, iidx на сайте v[idx]).

Контекст (2026-06-13, всплыло при Plan 140.2 A.0)

Для элизии bounds (for i in 0..v.len() { …v[i]… }) нужен вариант (b): caller инстанцирует @index requires на сайте v[i]. Investigation (5-агентный sweep) показал: общей call-site precondition-проверки в верификаторе НЕТ вообще (только decreases для self-recursion, и она не детектит даже method-форму obj.m(args)); Plan-140 элизия keyed по (fn_name, contract_span) — элидит ВНУТРИ callee («всё-или-ничего»), непригодна для per-site. Полная cross-function instantiation (доказывать requires ВСЕХ callee на ВСЕХ вызовах) — большой риск: внезапно всплыли бы десятки недоказанных preconditions по всему codebase → broken build.

Решение (2026-06-13)

Специализированная per-index-site instantiation, НЕ общая cross-function. B.4 prove_vec_index_sites (verify/pipeline.rs) инстанцирует ТОЛЬКО @index requires на v[idx] READ-сайтах (_selfv, iidx), доказывает под loop-bound for i in lo..hi. Это покрывает A3 («минимум для loop-var индекса с границей v.len()/@len») без риска массовых новых ошибок: pass только ДОБАВЛЯет proven-сайты, никогда не создаёт diagnostic. Soundness — через frame-check «длина v инвариантна в цикле» (Z3 моделит _field_len_int(v) как фиксированное, а Nova переоценивает 0..v.len() каждую итерацию).

Расширено (2026-06-13, [M-140.2-elision-writeback] ✅ §1–§3):

  • §1 write-back — frame-check read-only → len-инвариантный (v[i]=val СОХРАНЯЕТ длину); for i in 0..v.len() { v[i]=f(v[i]) } элидирует И запись, И чтение.
  • §2 slicev[a..b] slice-bounds (0<=a && a<=b && b<=v.len()); inline slice-проверка элидируется на доказанных сайтах.
  • §3 cross-fn (callee-side)v[i] внутри fn helper(v,i) requires 0<=i && i<v.len() элидируется (bound из requires; contract-based proven-set, codegen элидит только при включённых контрактах — под --contracts=off/#unchecked проверка остаётся, т.к. requires там не enforced).

Отложено (остаток): caller-side требование (caller доказывает callee requires на call-site → элидировать сам requires-CHECK callee) — это per-method all-or-nothing (требует доказательства на ВСЕХ call-site), low-value (requires-check дёшев); декларативный requires на @index(Range) (slice safety уже через inline-gate); экзотические не-loop-var индексы (Z3-арифметика уже покрывает большинство).

Связанное

  • D256@field/@method() self-access в контрактах.
  • D257 — Vec @index bounds как элидируемый контракт.
  • Q34 — enforce-in-release (Plan 140, base-механизм элизии).
  • [M-140.2-elision-writeback] (backlog) — расширение элизии за пределы read-only-MVP.

Q37. Конформность протоколов: opt-in (структурная) vs required (номинальная) — 🟡 ЧАСТИЧНО (2026-06-13, Plan 154.1 / D268)

Контекст (Plan 154.1, D268, 2026-06-13)

#impl(P) стал применим и к метод-декларациям (fn int @display), не только к типам. Конформность в Nova — структурная: тип с подходящим методом удовлетворяет бонд [T P] БЕЗ всякой пометки. #impl(P)необязательная добавка (проверка подписи + явная привязка протокола к типу). Это путь Go (структурный), не Rust (номинальный/required).

Решение (для V1)

Opt-in#impl не обязателен, отсутствие не ошибка. Зафиксировано в D268. Не ломает существующий код, миграция не нужна.

Открытое место

Стоит ли когда-нибудь сделать конформность required (как Rust: явный #impl(P) обязателен для использования типа там, где ожидается P)? Плюсы: ясность намерения, ловит «случайную» конформность; минусы: миграция всего корпуса, теряется Go-лёгкость. Не для V1 — отдельным шагом, если вообще. Трекается как [M-154.1-required-conformance].

Связь

  • D268 — opt-in #impl (решение V1).
  • D186#impl(P) на типах (auto-derive opt-in).
  • [M-154.1-required-conformance] (backlog) — возможный переход к required.

Q-loop-opt-thresholds. Loop/leaf codegen opt — порог элизии + расширение safe-set + точность call-графа (Plan 143 §2.A/§2.B / D270/D271) — 🟡 OPEN (minor, 2026-06-14)

Part A+B [M-opt-preempt-strided-loop] (D270, merge 7c047a1b) и [M-opt-leaf-preempt-entry-elision] (D271, ветка plan-143-leaf-preempt-elision) приземлены. Открытые design-вопросы (minor, НЕ блокеры — непокрытое всегда даёт корректный conservative fallback: KEEP / per-element цикл):

§A — Loop preempt-elision + copy-loop (D270):

  1. Порог preempt-elision (count ≤ 1024) — захардкожен. Делать tunable (env/attr)? Какое значение оптимально (vectorization-win vs preempt-latency)? Снимется SIGURG’ом (variable-bound без порога).
  2. Copy-loop safe-set — сейчас только flat-POD элемент (nova_* / pointer _p). Расширить на value-record (str) и доказуемо-flat composite? Нужно per-T flatness-доказательство (slot-copy == value-copy).
  3. Обобщить copy-loop lowering за пределы Vec[T] (на []T-views / raw *mut T)? Сейчас — только Vec.

§B — Leaf entry-check elision (D271): 4. Cross-module precision — pre-pass видит entry module.items + импортируемые peer_files; callee из модуля, чьё тело недоступно в эмит-юните, → conservative KEEP. Точнее: ahead-of-time экспорт per-fn KEEP-флага в манифест (как exports_consume_types, D164) → cross-crate элизия резолвимых форвардеров. 5. Minimal-SCC-cut вместо KEEP-all-cycle-members — сейчас ВСЕ члены рекурсивного цикла держат safepoint; соундности достаточно ≥1 на цикл. Минимальный feedback-vertex-set (или просто 1 член по entry-точке) разблокировал бы элизию остальных. Trade-off: сложность анализа vs выгода (циклы обычно короткие). 6. Synthesized-conversion arg-typingstr.from(int/f64) не имеет FnDecl в AST → форвардер StringBuilder.append(f32) держится в KEEP. Регистр synthesized-conversions с known-return-type снял бы это без потери соундности (сейчас — корректный conservative-KEEP, не упрощение).

Связь: D270/D271, Plan 143 §2.A/§2.B, [M-opt-preempt-strided-loop] (SIGURG-часть open) / [M-opt-leaf-preempt-entry-elision] (✅ done).

Q-interpreter-future — судьба древесного интерпретатора — ✅ RESOLVED (2026-06-14)

Интерпретатор (nova run) сделан UNSUPPORTED (D274, Plan 157): команда громко ошибается, мёртвые interp-тесты удалены, модуль interp/ оставлен «для справки». Резолюция трёх открытых пунктов:

  1. Полная вырезка vs сохранение. Решено сохранить interp/ «для справки» (consistent с «пока не поддерживаем» / D274); из сборки-контракта исключён. Полное удаление модуля сознательно ОТЛОЖЕНО (не отменено) — единственный residual.
  2. Внутренний dev-инструмент nova-codegen run / test-interp — ✅ застаблен (2026-06-14): handlers больше не конструируют interp::Interpreter, а громко ошибаются (exit ≠ 0) с указанием на C-codegen; clap doc-строки помечены [UNSUPPORTED]. Регресс: compiler-codegen/tests/interp_tool_unsupported.rs (neg run/test-interp + pos compile).
  3. docs/nova-codegen.md/.ru.md — ✅ выверены: run/test-interp помечены [UNSUPPORTED], interp/ в дереве описан как «kept for reference, не подключён».

Residual (не блокирует закрытие): возможное полное удаление модуля interp/ ЛИБО его оживление как будущий REPL — см. [M-interp-unsupported]. «пока» намеренно открытое.

Связь: D274, Plan 157, [M-interp-unsupported].

Q38. Генерация keyword-списков хайлайтеров из лексера vs ручная поддержка — 🟡 OPEN (2026-06-14, Plan 104.9 / D278)

Контекст

D278 зафиксировал: лексер (compiler-codegen/src/lexer/mod.rs) — единственный источник истины для множества keyword’ов, а хайлайтеры (VSCode/vim/Zed/Helix/Neovim/сайт) — производные. Сейчас каждый хайлайтер держит ручную копию списка, а от дрейфа защищает conformance-тест (syntax_highlight_conformance.rs дёргает живой lex()) + node-guard сайта. Это ловит рассинхрон, но:

  • дублирует «правду» в N артефактах (+ авторитетный список в самом тесте);
  • при добавлении нового keyword’а в лексер тест НЕ упадёт автоматически, пока его не добавят в ACTIVE-список теста (residual, прикрыт чеклистом в editors/README.md).

Открытое место

Генерировать keyword-списки из лексера (codegen-шаг: nova-codegen emit-highlight или build-script, пишущий keyword-секции VSCode/vim/scm/JS из enumerable-набора лексера) — единый источник, ноль дублирования, новый keyword автоматически попадает во все хайлайтеры. Минусы: нужен enumerable keyword-API в лексере (сейчас набор выражен только match-арми в lex_ident_or_keyword); генерируемые файлы vs handcrafted (scope-категории VSCode/цвета — не выводятся из лексера автоматически, keyword’ы — лишь часть грамматики); cross-repo доставка в сайт.

Решение (для V1)

Ручная поддержка + conformance-guard (Plan 104.9 / D278). Авто-генерация — потенциальный следующий шаг, не для V1.

Связь

Q-infinite-value-type — dedicated E_INFINITE_TYPE для genuinely-infinite value-типов — ✅ RESOLVED (2026-06-15)

Контекст (branch fix-checker-recursive-type-overflow, [M-checker-recursive-type-overflow])

Boxing-aware фикс size-walk’а (D280) сделал ВАЛИДНЫЕ рекурсивные heap-типы конечными (heap-ссылка = указатель) и убрал stack-overflow. Для genuinely-infinite value-self-cycletype N value { next N } (без указательной индирекции, не имеет конечного layout) — depth-budget (MAX_TYPE_SIZE_DEPTH = 128) в const_fn_eval останавливает рекурсию и возвращает None вместо краша. No-crash гарантирован.

Решение (✅ РЕАЛИЗОВАНО 2026-06-15, коммиты ae1e2906 + 3ffa7714, ветка fix-infinite-value-type)

Добавлен value-containment cycle-detector в compiler-codegen/src/types/mod.rs, функция check_infinite_type, вызываемая из check_type_decl. DFS по inline-containment-графу (value Record / NamedTuple / Newtype / Alias / Tuple / FixedArray — INLINE; heap Sum / heap Record / *T / []T / str / Option / примитивы — STOP, разрывают цикл) с on-path visited-set; при back-edge эмитит E_INFINITE_TYPE с ясным сообщением (имя типа + поле + подсказка: sum / Option / *T / []T).

Классификация зеркалит type_size_or_align_resolved_d (D280) точно → нулевые false-positive.

Проверено:

  • type N value { next N }E_INFINITE_TYPE, чистое сообщение, nonzero exit.
  • Взаимный цикл (A value { b B } + B value { a A }), newtype-цикл, self-ref в tuple / [N]T — все флагируются.
  • type H { t Tree }, деревья/списки через sum/Option/*T/[]T → PASS, E_INFINITE_TYPE не выдаётся.
  • nova check std/0 false-positive по всему stdlib.
  • 827/0 lib-тестов без регрессий.
  • const_fn_eval depth-guard сохранён как defense-in-depth (два независимых барьера).

Связь

  • D280 — heap-ссылка = pointer-size; boxing-aware size-walk + depth-guard.
  • [M-checker-recursive-type-overflow] (backlog, ✅ CLOSED) — robustness-фикс, оставивший этот residual.

Q-reach-dce-precision — точность reachability-codegen DCE: coarse-by-name method-DCE + полная lazy-module-resolution — 🟡 OPEN (minor, 2026-06-15)

Контекст (Plan 159, D283, [M-reachability-codegen-dce] Ф.1-core ✅)

Plan 159 зашипил reachability-codegen (вариант A, Zig-модель): эмиссия в C только достижимого от main (free fns + const/ro-таблицы + методы), kill-switch NOVA_REACH_DCE. Замер 10606→2494 строк, G0 консервативная корректность соблюдена. Два места намеренно оставлены менее точными, чем потенциальный идеал — оба over-keep’ят (никогда не over-prune’ят), поэтому это DX/оптимизация, НЕ корректность.

Открытые места

  1. Method-DCE coarse-by-name. Метод T.m режется только если И тип-имя T, И селектор m недостижимы. Это пересечение по ИМЕНИ, не по точному (тип, метод)-ребру графа вызовов: если в коде есть достижимый A.foo() и недостижимый-сам-по-себе B.foo(), селектор foo считается достижимым, и B.foo over-keep’ится. Точная альтернатива — per-(тип,метод) reachability через настоящий call-graph + monomorphization-collector (rustc-модель). Риск точного варианта — пропустить codegen-injected (desugar) селектор и over-prune’нуть (G0-blocker). Текущий список засеянных desugar- селекторов (for-iter/операторы/concat/index/string-interp) собран регрессией (concat нашёлся только sweep’ом plan131), а не систематическим перечнем всех call-site-видов. [M-159-method-pruning] (P3) — per-kind аудит редких инъекций (drop/finalizer, embed auto-proxy, closure-captured методы) + фикстура на каждый вид.

  2. Полная lazy-module-resolution. Ф.4 (no-import char-методы) сделана Option A — import-резолвер инжектит import std.unicode в entry-модуль при детекте char-method-call-селектора (cycle-free, DCE затем срезает таблицы → zero-cost). Это НЕ полная ленивая загрузка/тайп-чек тел модулей при первой ссылке на символ (что снимало бы цикл prelude↔unicode фундаментально и работало бы для произвольных prelude-forward-declared builtin-методов, не только char-Unicode). [M-159-lazy-module-resolution] (P3) — полный ленивый резолв, если Option A окажется узким.

Решение (для V1)

Coarse-by-name method-DCE (over-keep на name-collision) + Option A для no-import. Оба корректны и финальны для V1 под G0; точные варианты — потенциальные следующие шаги (DX/размер), не для V1. Делать, если появится мотивирующий профиль (заметный over-keep на реальной программе) или фундаментальная нужда в ленивом резолве произвольных prelude-методов. 2026-06-16: §2 (lazy-module-resolution) ПРОМОУТ в Plan 162 — см. Q-module-resolution-model.

Связь

  • D283 — reachability-codegen policy (executable-срез / library-полнота / kill-switch / G0).
  • docs/plans/159-reachability-codegen.md — план + замеры + «Статус по завершении».
  • [M-159-method-pruning] / [M-159-lazy-module-resolution] (backlog, P3) — точные варианты.
  • [M-152.3b-char-methods-no-import] (backlog, ✅ CLOSED через Ф.4 Option A).
  • [M-checker-recursive-type-overflow] (backlog, ✅ CLOSED) — robustness-фикс, оставивший этот residual (теперь закрыт).

Q-module-resolution-model — Rust-модель резолва: ленивый резолв + «методы едут с типом» + extension-policy — ✅ RESOLVED (2026-06-16)

Решение: Rust-strict — inherent=no import needed; extension=import required; реализовано Plan 162 Ф.1-Ф.5 (2026-06-16).

Контекст (Plan 162, промоут [M-159-lazy-module-resolution])

Текущий резолвер — жадный рекурсивный inline-merge → межмодульные циклы падали стеком (prelude→std.unicode→std.collections→prelude). Из-за этого no-import char-методы были сделаны хардкодом (Plan 159 Ф.4: список имён + auto-инъекция). Plan 162 перевёл на Rust-модель: collect-signatures-first → lazy-bodies (циклы безвредны, D285, амендит Plan 42 Rule A), method resolution по типу получателя (D286 TypeMethodMap; inherent @-метод без import), char-методы → prelude (D286), extension-политика Rust-strict (D287), Ф.4-хардкод снят.

Решено (ПОЛНОСТЬЮ)

  • Направление — Rust-strict (реализовано 2026-06-16): inherent=без import; extension=import required; источник истины — .nv, не хардкод.
  • import нужен только для имён (типы, свободные функции); inherent-методы импорта не требуют (резолв по типу значения через TypeMethodMap D286).
  • Extension-method policy — Rust-strict: E_EXTENSION_METHOD_NEEDS_IMPORT при вызове метода из неимпортированного чужого модуля.
  • char-методы (is_alphabetic, …) перенесены в std/prelude/core.nv → стали inherent → работают без import std.unicode.
  • CHAR_UNICODE_METHOD_SELECTORS + needs_unicode_injection + auto-инъекция удалены из компилятора (grep = 0 в src/).

Связь

Q-import-glob-hygiene — запрет glob-форм import/export: ban import m vs redefine-as-qualified — ✅ RESOLVED (2026-06-16)

Решение: E_IMPORT_GLOB добавлен в compiler-codegen/src/types/mod.rs check_module(). Fires for bare import X without .{} selector and without as alias (prelude auto-imports exempt). All ~100 pre-existing whole-module imports migrated to import X as X form. 3 new plan163 tests: f3 (negative E_IMPORT_GLOB), f4 (positive as-alias qualified call), f5 (positive selective .{name}).

Контекст (Plan 163, [M-import-glob-forbid])

import = inline-merge. Токена * нет, но whole-формы функционально glob/barrel: import m (без .{}/as) = unqualified glob; export import m (без .{}) = barrel (неконтролируемый публичный API). В коде все export import именованные, грамматика whole-форму допускала.

Решено (ПОЛНОСТЬЮ)

  • export import m (whole) → запрещено E_REEXPORT_GLOB (D288, нулевая миграция; barrel-дыра закрыта).
  • import m (whole, unqualified) → запрещено E_IMPORT_GLOB (D289, вариант a); ~100 файлов мигрированы.
  • Named (m.{a,b}) + alias (m as x) + prelude (именованный re-export) — оставить.
  • Prelude auto-imports (std.prelude.*) — exempt (compiler-internal).
  • Не банить фичи целиком (re-export нужен для prelude/фасадов; named import нужен).

Связь

Q-method-type-private — priv(type) на методах: type-private method — 🟡 OPEN (2026-06-16)

Контекст ([M-160-methods-module-visibility])

Все методы без export сейчас module-private по умолчанию (D5/Plan 35). Поле priv(type) = type-private (только методы самого типа). Симметрично ли сделать method-level priv(type) — метод видим только другим методам того же типа?

export type Account {
    priv(type) fn @verify_balance() -> bool { ... }  // видно только методам Account?
}

Что уже решено

  • priv (bare) на поле = module-private (D281, Plan 160).
  • priv(type) на поле = type-private (D220, Plan 124.1).
  • priv(type) на методе = пока не поддерживается (синтаксис не разбирается).
  • Методы без export = module-private (видны в пределах папки-модуля).

Открытое

  1. Нужен ли priv(type) на методах? Use-case: вспомогательный метод, который не должен вызываться даже другими файлами того же модуля.
  2. Синтаксис: priv(type) fn @helper() или fn priv(type) @helper()?
  3. Vs. module-private по умолчанию: module-private уже достаточно строгий для большинства случаев.

Рекомендация: реализовать при явной потребности (module-private достаточно). Следить за user feedback.

Связь

  • D220 (priv(type) семантика поля), D281 (module-level type privacy), Plan 124.1.

Q-if-let-chain-multi — множественные let-паттерны через && в if/while — 🟡 OPEN (2026-06-17)

Контекст (Plan 106, D34)

Plan 106 реализовал && guard-условие после одного паттерна:

if Some(x) = expr && x > 5 { }         // ✅ реализовано
while Some(item) = q.pop() && item.valid { }  // ✅ реализовано

Следующий шаг — множественные let-паттерны через && (Rust let-chains RFC 2497):

if Some(x) = get_x() && Some(y) = get_y(x) && x + y > 10 { }
//           ^^^^^^^^^^^^^^^^^  второй let в chain — НЕ реализовано

Проблема реализации

Текущий AST: ExprKind::IfLet { pattern, scrutinee, guard: Option<Expr>, ... } — один паттерн + опциональный guard.

Для chain нужно: Vec<IfCond> где IfCond = LetBind(pattern, scrutinee) | BoolExpr(Expr).

Это требует:

  1. Новый enum IfCond в ast/mod.rs
  2. Замена IfLet/WhileLet на унифицированные If { conds: Vec<IfCond> } / While { conds: Vec<IfCond> }
  3. Обновление всех call-sites (parser, types, emit_c, may_gc, interp)
  4. Scope semantics: cond[i] видит биндинги cond[0..i-1]

Трудоёмкость: ~1-2 dev-day, изолированная задача.

Открытое

  1. Нужно ли? Workaround — вложенные if:
    if Some(x) = get_x() {
        if Some(y) = get_y(x) && x + y > 10 { }
    }
    
    Менее идиоматично, но читаемо.
  2. Приоритет: низкий — guard (Plan 106) покрывает 90% use-cases.

Связь

Q-labeled-loops — метки циклов + адресный break/continue — 🟡 OPEN (2026-07-02)

Контекст (cross-language syntax-gap survey 2026-07-02)

Единственная из survey-находок со статусом genuinely absent (consider, medium): выход/continue сразу из ВНЕШНЕГО цикла. Не покрыто ничем — interrupt выходит из with-блока эффекта, return/throw — из всей функции, mut-флаг + post-loop reconstruction = ровно тот boilerplate, что это убирает.

for outer in grid {
    for cell in outer {
        if bad(cell)  { break outer }      \ выход из ВНЕШНЕГО цикла
        if skip(cell) { continue outer }   \ следующая итерация внешнего
    }
}

Применимо к while outer { … } и loop outer { … }. Bare break/continue целят в ближайший цикл (без изменений).

Форма (важно)

  • Только identifier — метка-имя перед ключевым словом цикла, break/continue берут её операндом.
  • 'outer (Rust) невозможен лексически: ' — делимитер char-литерала → 'outer = незакрытый char.
  • :-форма (Go/Kotlin outer:) чужда грамматике — в Nova нет : в биндингах (ro a i64, не a: i64).
  • Метка живёт в отдельном namespace (loop-label, не value-binding) → не шэдоит переменную outer.
  • Value-carrying break outer x — вне scope (пересекается с block-as-expression и match).

Реализация

break/continue сейчас без операнда → добавить identifier-after-break продукцию. Десугарится в существующий loop { match _it.next() {…} }, прокидывая exit целевого цикла во внутренний break. Новой value-семантики нет — break outer даёт () как сейчас.

Открытое

  1. Нужно ли? Идиома Nova для «глубокий скан» — вынести во вспомогательную функцию с типизированным return (лучше для «LLM пишет / человек проверяет»). Метки рискуют поощрять вложенность вместо декомпозиции.
  2. Приоритет: низкий. Ниша — hot grid/matrix-сканы, где function boundary мешает mut-capture / нельзя continue внешний.

Связь

Q-nested-or-patterns — |-альтернативы внутри варианта/позиции — 🟡 OPEN (2026-07-02)

Контекст (cross-language syntax-gap survey 2026-07-02)

consider, low. Top-level or-patterns уже реализованы (Pattern::Or: Red | Yellow =>, 0 | 1 | 2 =>). Брешь — только вложенная позиция (AST-комментарий: «не вкладывается внутрь других patterns»):

match x {
    Some(1 | 2 | 3) => "small"     \ сейчас требует дублировать тело или guard
    Some(n)         => "other"
    None            => "none"
}
match pair {
    (0 | 1, y) => y                \ и в tuple/array-позициях
    (x, _)     => x
}

Сейчас: Some(1) => b, Some(2) => b (дублирование тела) либо guard Some(n) if n == 1 || n == 2 => b (теряет exhaustiveness-reasoning, хуже читается для литерал-списков).

Реализация

Крошечная: hoist |-сбора из parse_match (top-level арм) в parse_pattern (вложенные позиции), тот же Pattern::Or, тот же инвариант — все альтернативы биндят одинаковый набор имён (bootstrap берёт биндинги из первой альтернативы). Nullary-payload альтернативы; guard остаётся на уровне арма. Нет грамматической неоднозначности: в позиции паттерна | не конкурирует с bit-or (тот — только в expression).

Открытое

  • Это обобщение (снятие top-level-only ограничения), не новая фича → делает язык внутренне консистентнее (почему | в арме работает, а на уровень глубже нет?).
  • Граница: только вложенный |. Range-in-arm ('a'..='z' =>) — НЕ добавляем (полностью покрыт guard c if c >= 'a' && c <= 'z'; настоящий feature-creep).

Связь

Q-extensible-sum-types — #extensible: sum-тип может расти без breaking change — 🟡 OPEN (2026-07-03)

Проблема (semver на границе пакетов)

Exhaustive match (ядро Nova) × растущая библиотека = конфликт: добавление варианта в экспортированный sum ломает каждый downstream-match. Прецедент уже был: D302 добавил PermissionDenied/ConnectionReset в NetError — honest breaking change, проглочен только «pre-release окном». После стабилизации std каждый error-enum (NetError, HttpError, IoError.kind, ParseIntError) встанет перед выбором: заморозить варианты навсегда / ломать всех / #extensible.

Дизайн-набросок

#extensible
export type NetError | Timeout | Refused | ...
  • Через границу пакета match по #extensible-типу обязан иметь _-арм → E_MATCH_EXTENSIBLE_NEEDS_WILDCARD; потребитель сам выбирает поведение ветки (panic / generic-error / лог).
  • Внутри своего пакета exhaustiveness остаётся полной (как Rust #[non_exhaustive]) — refactoring-workflow «добавил вариант → компилятор дал список мест» не страдает.
  • Будущее расширение на records: поля могут добавляться → запрет литерал-конструирования/полной деструктуризации извне.

Нейминг (решено в обсуждении 2026-07-03)

#extensible — называет обещание автора («тип будет расти»), консистентно с рядом #pure/#realtime/#deprecated/#share (свойство декларации, не поведение чекера). Отвергнуты: #non_exhaustive (механизм вместо намерения, двойное отрицание — известная критика Rust-имени), #open (перегружено), инверсия дефолта #frozen/#sealed Swift-style (расширяемость по умолчанию убивает ценность exhaustive-match).

Отвергнутая альтернатива: авто-_ => panic

«Всегда добавлять _ автоматом с паникой» — превращает compile-time гарантию в runtime-краш: добавил вариант в СВОЙ тип → 15 match’ей молча скомпилировались → узнаёшь паникой в проде. Ровно тот класс багов, ради которого sum-типы существуют (C-switch; Kotlin в 1.7 сделал when-exhaustiveness обязательной по той же причине). Платит цену везде ради проблемы, существующей только на границе пакетов.

Когда делать

Не сейчас: пока весь код в одном репо и нет внешних потребителей — атрибут ничего не даёт (чисто межпакетный контракт). Гейт: стабилизация std / central registry (Plan 03.3). До того — фиксировать breaking-добавления вариантов честно (как D302).

Связь

  • D302 — прецедент боли (NetError варианты)
  • D139 — version-диапазоны; D105#deprecated-семейство (соседний API-evolution инструмент)
  • Rust RFC 2008 #[non_exhaustive], Swift @frozen/@unknown default — прецеденты обеих полярностей

Q-resolved-type-c-name — переход на более правильное формирование C-имени типа (после ретайра type_ref_to_c) — 🟡 OPEN (2026-06-21, Plan 172.1 U.4.6)

Контекст (D315, Plan 172.1 U.4.6)

U.4.6 строит resolved_type_to_c (единый C-лоуэринг от канонического ResolvedType) как byte-identical зеркало legacy type_ref_to_c. Это ОСОЗНАННЫЙ MVP, не утверждение, что legacy «правильный». Причина byte-identical — разделение двух вещей, которые делает «принтер»:

  • C-имя (мангл) — это ABI-контракт: один тип обязан манглиться в одно C-имя ВЕЗДЕ, иначе программа не слинкуется. Имя нельзя «улучшить» в одиночку — менять пришлось бы атомарно во всех местах. Поэтому при миграции сайтов (U.4.7/U.4.8) выход обязан совпадать до байта; byte-identical — это контракт безопасной миграции, а не «legacy верен».
  • Резолв (что это за тип) — вот ЗДЕСЬ legacy неправильный (делает в codegen, дублируя чекер, §0-анти-паттерн). U.4 это чинит (резолв уезжает в чекер). То есть архитектуру делаем правильной, а вывод (имя) сохраняем.

Но в самом legacy-формировании C-имени есть bootstrap-компромиссы / возможные неточности, которые MVP сознательно зеркалит как есть:

  • int64-стирание Option/Result (NovaOpt_nova_int, NovaRes_nova_int_nova_str*) для erased-generic;
  • legacy NovaArray_nova_int* (boxed-pointer slots) для record/user-элементов массива;
  • выбор concrete-vs-erased по C-СТРОКЕ (is_generic_stub_c, ____-эвристика) вместо структуры типа.

Открытое

Когда type_ref_to_c ретайрится и resolved_type_to_c становится ЕДИНСТВЕННЫМ источником (U.4.8) — пересмотреть на ОДНОЙ функции, где C-имя «не совсем верное», и перейти на более правильное формирование (например, выбор erasure по структуре ResolvedType, а не по C-строке; не-erased typed-storage для record-массивов). Делать отдельными изолированными ревьюемыми коммитами ПОСЛЕ объединения, потому что:

  1. смена мангл-имени = смена ABI → менять консистентно/атомарно, может затронуть рантайм-представление;
  2. смешивать «объединить движки» с «изменить семантику» во время миграции = §7-анти-паттерн (непрослеживаемые регрессии — не понять, объединение сломало или «улучшение»).

Прецедент дисциплины: баг uint = -1 (U.5.2) сохранили byte-identical при объединении, починили отдельным коммитом d58e0092.

Решение (для V1 / MVP)

U.4.6 — byte-identical зеркало legacy C-имени (MVP); резолв уезжает в чекер (архитектура чинится), байты вывода сохраняются. Улучшение самого C-имени — потенциальный следующий шаг ПОСЛЕ U.4.8 (один источник), не во время объединения. Делать, если появится мотивирующая нужда (typed-storage массивов / точная erasure) — каждый случай отдельным behavior-change коммитом с D-блоком.

Связь

  • D315ResolvedType единый носитель; ABI выводится лоуэрингом, не хранится.
  • Plan 172.1 U.4.6/U.4.7/U.4.8 — построить resolved_type_to_c до паритета → мигрировать объявленные типы → удалить type_ref_to_c.
  • [M-172.1-U4.6-state-heavy-arms] — отложенные state-heavy армы (generic-mono/Array/Tuple) = U.4.6b.

Q-value-category-overloading — consume/ref как оси перегрузки (C++ T&&-overloading) — ✅ RESOLVED (2026-06-26): НЕ добавляем

Вопрос

Разрешить перегрузку по value-category / способу передачи: f(consume p T) + f(p T) (+ f(mut ref p T) / f(ro ref p T)) как РАЗНЫЕ перегрузки с авто-диспатчем по аргументу — временное/last-use → consume-перегрузка, именованное-с-дальнейшим-использованием → by-value, явный f(ref x)mut ref. Аналог C++ f(T&&) vs f(const T&).

Сейчас (эмпирически, 2026-06-26)

НЕТ. По D84 перегрузки различаются типами параметров / арностью / возвратом; consume и refрежимы передачи, НЕ оси перегрузки. Пробник fn which(consume p Point) + fn which(p Point)error: duplicate definition 'which' with same signature.

Аргументы ПРОТИВ (почему не добавляем)

  1. Для value-типов copy ≡ move наблюдательно (нет идентичности; GC) → consume-перегрузка и by-value-перегрузка вели бы себя идентично → выбирать нечего, бессмысленно.
  2. C++ это нужно ради move-конструкторов (украсть heap-буфер + обнулить источник) — у Nova их НЕТ (GC шарит указатель, value = байты, красть нечего) → исчезает сам use-case.
  3. Притащит сложнейшую/footgun-часть C++ — ref-overloading + perfect-forwarding + ref-collapsing (матрица T&&/const T&/T&) ради ~нуля выгоды.
  4. consume = контракт владения (D131/D133): авто-выбор по value-category → одна и та же запись f(x) то отдаёт владение, то нет, скрытно → footgun (читатель вызова не видит, потратили x или нет).
  5. Противоречит принятому: оптимизация временных невидима/автоматическая (RVO/NRVO + move-on-last-use, Plan 172.4 Ф.6), а НЕ выбирается перегрузкой; формализация ref (D326) тоже не делает ref user-overload-осью.

Что покрывает потребность БЕЗ этой фичи

  • эффективные временные → авто (RVO, Plan 172.4 Ф.6) на любом by-value параметре;
  • хочешь владеть → одна функция consume p T (временные глотает даром, именованное — отдаёшь);
  • «in-place для временного, копия для именованного» → одна by-value-функция + RVO: fn norm(p T) -> T { mut q = p; /*…*/; q } (временное → move-in 0 копий; именованное → копия, оригинал цел).

Решение (владелец, 2026-06-26)

НЕ добавляем. То, ради чего value-category overloading существует в C++ (move-ctor), у Nova отсутствует по построению (GC); фича притащила бы C++-сложность без выгоды и противоречила бы «оптимизация временных невидима». Reopen — только при конкретном кейсе, который одна consume/by-value-функция + RVO доказуемо не покрывают.

Связь

  • D84 — оси перегрузки (consume/ref не входят).
  • D131/D133 (02-types.md) — consume-контракт (владение, affine).
  • D326ref как режим, не тип, не overload-ось.
  • Plan 172.4 Ф.6 — RVO/copy-elision (невидимая оптимизация временных).
  • Q29 — родственное (ref как тип отвергнут).

Q-concurrency-combinators-followup — any/fallible-race + supervisor-strategy-набор (после сессии 2026-06-26) — 🟡 OPEN

Парковка открытых вопросов из дизайн-сессии concurrency 2026-06-26 (продолжим позже). Базовые решения уже записаны (cross-ref ниже); здесь — что осталось спроектировать.

Осталось спроектировать

  1. any / fallible-race — варианты на падающих конкурентах. Базовый race[T](...funcs []fn() -> T) -> T (первый ЗАВЕРШИВШИЙСЯ, чистый, канальный) записан в 173.1 §2a. Остаётся:

    • fallible-race — конкуренты fn() Fail[E] -> T; первый завершившийся может УПАСТЬ → сам race fallible: race[T,E](...funcs []fn() Fail[E] -> T) Fail[E] -> T.
    • any / «первый успешный» — ждать первого Ok, игнорируя падения; упасть только если упали ВСЕ (Java anySuccessfulResultOrThrow). На канале: recv до первого Ok или закрытия. Отличие от race: быстрый сбой НЕ выигрывает («быстрейшая ЖИВАЯ реплика»).
    • Открыто: имена (any/race_ok/first_ok); агрегация ошибок при «все упали» (MultiError vs последняя); гейт closures-in-generic-array (Plan 48 Ф.4) — общий с race.
  2. Supervisor-strategy-набор для файберов — финализировать. 173 §3b зафиксировал направление: default cancel/Escalate/Stop/aggregate; Erlang-restart — анти-паттерн на shared-memory (нет «чистого сброса»). Остаётся:

    • точный API/синтаксис Stop/Escalate (per-fiber решение vs scope-policy);
    • restart ТОЛЬКО для ИЗОЛИРОВАННЫХ файберов (opt-in): как выражается изоляция (рычаг 173.3 #share/consume-в-spawn), какие стратегии (OneForOne/OneForAll/RestForOne) сохраняются и только для изолированных;
    • пересмотр Ф.0-врезки supervised(strategy:…, max_restarts:…, period:…).

Уже записано (НЕ переоткрывать)

  • Гетерогенный parallel(f,g) → кортеж — отдельный Q-parallel-tuple (выше в этом файле).
  • Целевой race (общий N-арный, канальный) + drop race2 — 173.1 §2a.
  • parallel(timeout:|deadline:|…) for call-site + таймаут → throw Timeout + opt-in []Result — 173.1 §2 п.5.
  • defer completes-by-default + scope-deadline/timeout + нет force-cleanup-timeout + watchdog — 173 §3a.

Координация

Plan 173 (umbrella) / 173.0 (рантайм cancel/shield) / 173.1 (parallel/supervised/race) / 173.2 (supervision) / 173.3 (isolation #share) / Plan 48 Ф.4 (closures-in-generic-array) / Plan 175 (Monotonic/Duration; бывш. 179 до renumber std-блока).