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-типы. На эффекты не распространён — вопрос остаётся открытым.
Тонкие места:
- Неявный
selfдля эффектов — это «магия» или унификация? - Куда отнести функции с несколькими эффектами (
Net Db Log Fail)?fn (Net, Db, Log) @method— некрасиво, иselfне один. - Переписывание 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, localconstdecls, 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/matchallowed; 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 intT-independent body. T reflection (size_of[T]) → V3. - ✅ First-class alias:
const ALIAS = const_fnallowed; 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 ClosureFullfn(x T) -> R => bodysyntax. - ✅ Ф.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— отменено в D15type X = { ... }с=для record — отменено в D17type X = { методы }для интерфейсов — заменено наprotocol X { методы }в D42effect 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— стандартные эффекты, не определены их операции. Частично закрыто:Time— D316-D318 (Plan 175,std/time);Net— D301/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 API — xs.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 testnova check --fragmentnova 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 fnattribute сохранён (см. 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.syncstdlib, не 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: задача переживает родителя, отмена не
прорастает. Варианты:
spawnвсегда внутри scope’а (supervised,parallel,nursery-блок). Unstructured нет вообще.spawnструктурный,detachотдельный для долгоживущих задач.detachтребует явного эффектаDetachили capability.- Текущее поведение
audit.nv—spawnfire-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 могла бы:
- Один scheduler на процесс, M-потоков (Go-style) — простая модель, но shared state требует синхронизации.
Domainкак явный примитив — изоляция кучи, передача данных через каналы. Сложнее, но безопаснее.- 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.syncstdlib, не 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 (
MemOrderingenum +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 — СНЯТО
~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_middlewareLLM не видит, что функция параллельна. Сигнатура не полна. - Реализация 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 - Граница
AsyncvsPar(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’а.
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:
- Cross-module callee-резолюция. Вызов в функцию из другого модуля, чей
FnDeclне в текущей вселенной графа, → unresolved → MayGC. Точнее было бы тянуть may-GC-эффект через manifest/ exports (какexports_consume_typesв D164), чтобы доказуемо-NoGC peer-функции не отравляли коллера. - Точность str-literal-interning. Сейчас интернированный str-литерал (
static const u8[]) — в allowlist как non-allocating, но граница «интернирован vs строится буфер» (интерполяция,from-конверсии) консервативна; тонкая классификация дала бы больше NoGC-листьев. - Более тонкая классификация 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:
- 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’а вместо «сканировать как один указатель». Прецедент: Gogcdataна display-класс, JVM/.NET display-классы, MLton (Plan 144 §7.6 Q10). Гейтится на H5 (захват by-value boxing vs interior-in-frame) — Ф.0. - Generic / erased инстанциации. Неразрешённый generic-слот / erased
nova_int-boxed элемент / cross-module тип, чейTypeDeclне во вселенной графа, →unresolved→ весь объект сканируется консервативно. Точнее было бы тянуть layout per-instantiation (как мономорфизация в codegen) или через manifest/exports (родственноexports_consume_typesD164), чтобы доказуемо-скалярные поля не раздували mark. - 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. StdlibBitflags[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 над int — type 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, customwith_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 — только зафиксированные примитивы языка. Причины:
- AI-first: один способ передать closure — обычный аргумент
f((x) => body). Не два способа делать одно. - Парсер однозначен:
{послеwith/supervised/region/race/parallel/select/with_timeout/try— это блок, не record/ handler-литерал. Иначе грамматика становится контекстно-зависимой. - Принцип «не плодить специальные сущности» (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) — безdeferresource 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:
-
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».
-
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 в public — D28 остаётся, 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, Swiftstruct/protocol, Gostruct/interface. Единый keyword — у структурных/динамических языков (TypeScript runtime — duck typing). - Handler-литералы. Сейчас форма литерала однозначно связана с
keyword объявления: у
type X—X { field: value }, уprotocol X—X { op(args) => body }. С единымtypeLLM может смешать формы в одном литерале (синтаксически корректно для разных имён, семантически бессмыслица). - 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):
-
Локальность. Тип и методы в одном месте файла.
-
Группировка по теме. Несколько
methods-блоков для одного типа — конструкторы отдельно, базовые операции отдельно, conditional methods (если введут bounds) отдельно. Прецедент Rust. -
Extension methods из чужого модуля видны явно.
import HashMap from std methods HashMap[K, V] where K: Json, V: Json { fn @to_json() -> str => ... }Сейчас в Nova расширение чужого типа делается через
fn ForeignType @methodгде-нибудь в своём модуле — визуально неотличимо от «своих» методов. С блоком — заявка явная. -
Совместимость с
protocol.type/protocolостаются раздельными. Структурная совместимость работает как сейчас: компилятор смотрит, какие методы определены у типа (черезmethodsили старый стиль), и сравнивает с протоколом. -
D1, D17, D42 не меняются.
typeостаётся чистым (только данные),protocol— чистым контрактом. Меняется только D35: методы переезжают вmethods-блок.
Тонкие места:
-
Один способ или два? Текущий стиль
fn Type @methodостаётся валидным или нет?- Только блок — чище, но breaking change для существующих spec/examples (кода мало, миграция дешёвая).
- Оба разрешены — совместимо, но «два способа одного» нарушает AI-first.
Рекомендация: только
methods-блок, миграция один раз. -
Static vs instance в блоке. Та же разметка:
fn name(...)— static (конструктор/factory),fn @name(...)— instance,fn mut @name(...)— mutating instance. Без новых правил. -
Множественные блоки для одного типа. Разрешены (как Rust многократный
impl). Программист сам группирует по теме. -
where-clauses для conditional methods — зависит от Q-bounds. Если bounds в MVP нет,whereтоже нет, conditional откладывается. -
Visibility. Каждый метод сам декларирует
export/private (D47). Блокmethodsсвою видимость не имеет. -
Embed/delegation (D39). Прокси- методы при
use Typeгенерируются на основе всехmethods-блоков типа. Override метода — отдельный метод во внешнемmethods-блоке обёртки, явный вызов@Inner.method()для делегации.
Прецеденты:
- Rust
impl Type { ... }— ровно эта идиома, 10+ лет, любят. - Swift
extension Type { ... }— то же для своих и чужих типов. - Kotlin — методы внутри
class+ extension functions снаружи (два способа, что у нас неприемлемо). - Go —
func (r Type) method()отдельно, как Nova сейчас. Жалоба сообщества — нет визуальной группировки.
Цена:
- Новый keyword
methods. Короткий, читаемый, семантически точен. - Миграция всех существующих
fn Type @methodвmethods-блоки — делается раз, кода пока мало. - Семантика множественных блоков для одного типа должна быть зафиксирована.
Решение пока: не трогать. Текущий стиль (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](Swiftsome-style) — нестандартно.
Тонкие места:
- Несколько bounds на один параметр —
[T FromRow & Hashable]?[T (FromRow, Hashable)]? Лучше — анонимный structural type или композиция протоколов. - Связь с эффектами в bounds — может ли protocol-bound включать
эффект-операции? (Эффекты — это
protocol, D18, так что технически да.) - Что бывает с уже принятыми решениями про 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.
Аргументы за:
- DRY жёстче. Имя типа никогда не повторяется → переименование
Box → Containerточечное. - AI-консистентность. LLM не «забывает» дописать generic-
параметры (
Box[T]vsBox). - Один способ (D40).
Аргументы против:
- Имя типа читается лучше для коротких типов.
User,Box— нагляднее чемSelf. Self экономит для generic’ов с параметрами. - Прецедентов нет. Rust, Swift, Scala —
Selfвсегда опционален. Strict-Self нет ни в одном языке. - Конфликт с существующим стилем. Большая часть spec/examples написана с явными именами типов. Миграция значительная.
- AI-training data. LLM обучен на языках без обязательного
Self— генерирует имена типов. Hard-rule вызовет постоянные compile errors при first generation. - Гибче через линтер. Если хочется единообразия — это 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. Оставить как есть. Обе формы валидны без рекомендации.
Тонкие места:
- Свободные функции с generic-параметрами не имеют receiver’а —
Selfзапрещён по D66. Hard-rule не применим в свободных функциях. - Bound
[T From[Self]]в свободной функции —Selfзапрещён, нужно явное имя или другой generic. Это уже исключение. - Sum-варианты —
fn Tree @clone() -> Selfвалиден, но в телеmatch @ { Leaf => Leaf, ... }нельзяLeaf => Self.Leaf(конструктор). Self применим только в типовых позициях. - Миграция существующих файлов — большая часть 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) |
| Swift | enum CodingKeys: String, CodingKey | compile-time через protocol |
| Kotlin | @SerialName("id") + serialization plugin | compile-time |
| OCaml | [@@deriving yojson] | compile-time PPX |
| C# / Java | [JsonPropertyName("id")] / @JsonProperty | runtime 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 обходит эффект-систему.
Тонкие места:
-
Несколько потребителей одного поля (
json+sql+validate). В B — несколько атрибутов рядом с полем, шумно но локально. В C — несколько отдельных schema-объектов, поле повторяется по числу форматов. Что лучше — открыто. -
Skip-семантика. Поля, которые не сериализуем: отдельный atom
@json(skip), отсутствие атрибута → не включать, илиprivfield modifier (D220) автоматически skip’нут? (privfields — implementation detail, не serialize’ятся by default.) -
Default-имя. Если 95% полей сохраняют имя один-к-одному (snake_case в коде ↔ snake_case в wire), не нужно ли по умолчанию маппить, отмечать атрибутом только исключения? Это сильно срезает boilerplate.
-
Имена derive’ов.
@derive(Json)или@derive(Encoder[Json])или@derive(SerializableTo[Json])? Упирается в дизайн stdlib (Q9). -
Composability с эффектами. Если
Json.encodeимеет эффекты (например,Fail[EncodeError]) — генерируемая функция должна правильно их пробрасывать. Это согласуется с D28 (вывод эффектов в private), но требует, чтобы comptime-генератор корректно вычислял эффект-сигнатуру. -
Совместимость с
readonly/mut-полями (D36). При decode’е из wire-формата нужно создавать новыйUser(потому чтоid—readonly), не мутировать существующий. Генератор должен это учитывать.
Связь.
- Q7 (macros/comptime) — блокирует вариант B.
- Q9 (стандартная библиотека) — определяет
Json,FromRow,Validateи т.п. - Q15 (representation tags для enum’ов) — смежная задача, тоже про сериализацию, может решаться тем же механизмом.
Статус. Открыт. Рекомендуемый путь:
- Сейчас — оставить вариант A (ручной маппинг). Это работает, у Nova нет реализации, спешить некуда.
- После Q7 — вернуться, рассмотреть B+C: атрибуты для частых случаев, schema для сложных. Это не взаимоисключающие варианты.
- Никогда — вариант 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 убирает шум при использовании.
Главные минусы введения:
- Subtyping — серьёзное расширение системы типов. Нужно решить variance, type inference, exhaustiveness, dispatch. Сейчас Nova эти концепции не имеет.
- Runtime-cost. Каждое значение
IntOrStrнесёт runtime-tag (иначеis-проверка не работает). Boxing на границах. Для статически типизированного языка — реальная цена. - Прецедентов мало. 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}}`
Открытые вопросы:
- Number representation в JsonValue.
Number(f64)теряет int-precision.Int(i64) | Float(f64)точнее, но coercion42ambiguous (i64 или f64?).Number(NumberKind)гдеNumberKind | I(i64) | F(f64)— двухуровневое, гибко но громоздко. Прецеденты: Rust serde_json — opaqueNumberс методамиas_i64()/as_f64(). - Object representation.
HashMap[str, JsonValue]теряет порядок ключей. Альтернатива —[]Field. Большинство JSON-парсеров используют HashMap. - Compile-time JSON-парсинг через
json\…“. Нужен Q7 (macros/comptime), без него — runtime. 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`.- Где разместить. В 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/:
-
Однозначно лучше с coercion:
export fn Duration.from_secs(n i64) -> Duration => { nanos: n * 1e9 } // ^^^ имя из аннотации, чище -
Явный конструктор лучше из-за визуальной симметрии в match:
match @buckets[idx] { Occupied { value } => Some(value) // лучше Some(value) _ => None // ← unit, не coerce'ится } // С coercion: `value` слева, `None` справа — асимметрично, читать сложнее. -
Явный конструктор лучше в
letбез аннотации:ro ip_value = if e.ip != "" { Some(e.ip) } else { None } // нет аннотации — coercion не работает; нужны явные Some/None. -
Спорно —
{ {...} }после 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). Slicingxs[a..b]остаётся отложенным (Q-array-slicing). Embeduse []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.rsinstance~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» (realNovaArray_<Composite>struct) ломает 47 тестов: весь stdlib (HashMap-бакеты, tuple, JSON) держится на int64-erasure + side-channelarray_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_int→NovaOpt_<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 Tstorage, без 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()также не добавлен (при[]T≡Vec[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_empty—len == 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) — разница
Из текущих примеров используются оба:
[]Slot[K, V].with_capacity(cap)(decisions/03-syntax.md → D38).[]T.alloc(n)(мой usage вexamples/stdlib_vec.nv).
Если оба существуют:
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 Type | B. голый Type | C. embed Type |
|---|---|---|---|
| Keyword | use (multi-purpose) | (нет) | embed (специфичный) |
| Длина | средняя | короткая | средняя |
| Прецедент | D mixin, partial Rust | Go | OCaml 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"
}
Правила (предлагаемые):
Cons(..)— все поля игнорируются (какCons(_, _)сейчас).Move(x, ..)— первое поле в bind, остальные игнорируются.Move(.., z)— последнее в bind, начальные игнорируются.Move(x, .., z)— первое и последнее, среднее игнорируется.
Прецеденты
- Rust:
..работает в обеих формах (Variant(_, _)иVariant(..)),Variant(x, ..)/Variant(.., x)тоже. - Swift:
case .variant(_, _, _)явно,..нет — все поля прописываются. - OCaml:
Cons (_, _)явный wildcard, нет..для tuple. - Haskell: wildcard
_для каждого поля.
Rust — единственный mainstream-прецедент. Но Rust-сообщество
любит .. — частая идиома.
Цена
- Новая форма pattern. Парсер должен различать
Variant(..),Variant(x, ..),Variant(.., x),Variant(x, .., y). - Конфликт с record-формой
... В{ field, .. }..стоит после,. В позиционной(x, ..)тоже после,. Парсер различает по виду внешних скобок ({}vs()), что согласовано с D17. - Тонкость с одним полем:
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 + ASTEffectMethod.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-контексте означает «конкретный реализующий тип» (как SwiftSelfв 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 не
работает без:
- Bound’ов на дженериках —
Out: FromIter[T](Q-bounds, отвергнуты в MVP). - Static-method в protocol —
FromIter[T] { .from_iter(...) -> Self }(Q-static-method-protocol). - 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). Не зафиксировано:
- Reverse range —
5..0(start > end). Что значит:- Пустой range (Rust-style — для прямого направления)?
- Идущий назад (5, 4, 3, 2, 1, 0)?
- 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, не
знает:
- Что формально означает
resume(v)? Возвращает ли он что-то? - One-shot или multi-shot? Можно ли вызвать
resumeдважды? Что произойдёт? - Тип
resume.fn(R) -> ()илиfn(R) -> T_with_block? - Что если
resumeне вызван? handler возвращает за весьwith-блок? Какое значение? - Запрещён ли
resumeдляnever-операций (Fail.throw)? resume()без аргумента для unit-операций — сахар или обязательная форма?
Ключевые design choices
One-shot vs multi-shot
| Модель | Прецедент | Стоимость | Use-cases |
|---|---|---|---|
| One-shot | Koka, OCaml 5, Eff | дёшево, stack-based | Fail, Db, Log, Time, Random — 95% реальных эффектов |
| Multi-shot | Multicore-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_block — resume возвращает финальное
значение всего 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» — близкий аналог RustFrom<E>через D73, но взаимодействие с D55 sum-coercion требует точной формализации (что приоритетнее: явный.map_errили auto-wrap?). Trigger: реальная боль в stdlib — когда ≥5 функций требуют.map_err(...)?boilerplate и pattern регулярен (один wrap-конструктор). Варианты решения перечислены ниже (auto-deriveFromчерез#[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.
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 — implicitstr.from(n)(D73)? Или требовать явный? (Plan 17 закрыл это:${expr}— sugar надstr.from(expr).)
Альтернативы синтаксиса
"${expr}"— JS-style (привычно большинству)"\(expr)"— Swift (без $-конфликта со shell)"{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?
Открытые пункты
- Формальный порядок — нужно зафиксировать sequence: spread → punning → field-coercion → sum-coercion (или другой).
- Многошаговая coercion:
let r SomeRecord = { x: 5 }гдеSomeRecord.x: SomeSumType. Двушаговая: int → SomeVariant → field. Зафиксировано ли «не более одного уровня coercion»? - 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соответствующим методом?
Используется в
- revolutionary.md R5.7 — обратимость spec ↔ impl,
ссылается на handler-state в
ensures. - revolutionary.md R4 — пример с
Db.balanceв ensures. - 09-tooling.md D24 — упоминание.
За
- Без этого ключевая фича 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, но формальная семантика этих ключевых слов не зафиксирована.
Открытые вопросы
-
Что такое
resultдля функции сFail? В сценарииfn withdraw() Fail[Overdraft] -> ()функция формально возвращает().result.is_okозначает «функция завершилась без throw». Но тогдаresult.is_okдля() -> ()всегда true (ничего не бросает) — что отличает отFail[E] -> ()где результат может бытьis_err? -
Семантика
resultдля Result-типа. Если функция возвращаетResult[T, E],result.is_okэто вызов method’а на возвращённом Result, аresult.value/result.error— извлечение payload’а? Тогда два механизма (Fail и Result) дают разные значенияresult.is_ok. -
old(expr)— глубина копии.old(acc.balance)копирует поле,old(arr)копирует массив или ссылку? -
Композиция контрактов. Можно ли использовать другие функции в
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 }
}
}
Открытые подвопросы:
- Объявление
Alloc[R]. Эффект параметризуется именем региона. Откуда берётсяR? Это compile-time имя (как lifetime в Rust), тип-параметр функции, или identifier из enclosingregion { }? - Handler для
Alloc[R]. Кто его ставит? Блокregion { }автоматически? Или программист пишетwith Alloc[r] = arena_handler { ... }? - Сигнатура
region { body }. Что body может делать с эффектом? Body — лямбдаfn() Alloc[r] -> T? ТипTуезжает из региона как — копируется, references запрещены? - Multi-region. Можно ли вложить
region { region { ... } }? ПолучитсяAlloc[outer],Alloc[inner]— две арены. Как escape между ними? Сейчас D6 показывает sequentiallet scratch = region { ... }; region { finalize(scratch) }, но не вложенный случай. - Связь с
realtime nogc { }. D64 говорит «внутриrealtime nogc— только region-allocations и стек». То естьAlloc[R]в сигнатуре функции = «эта функция совместима сrealtime nogc { }контекстом». Нужна формальная связь. - Coercion / inference. Если функция
f() Alloc[r] -> Tвызывается внутриregion { }, должен ли компилятор автоматически связатьrс регионом блока? Или программист пишет явно? - Сравнение с прецедентами:
- Rust — lifetimes
'aчерез borrow checker. - Koka —
<alloc<r>>эффект в effect row. - Encore — capabilities + parallel regions.
- Cyclone — region-based memory с явными аннотациями.
- Rust — lifetimes
Статус. 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" }).
Преимущества:
...— явный маркер. Парсер однозначен без type-directed parsing (в отличие отname({...})который мог бы быть либо одним record-аргументом, либо named-form).- Согласовано с D60. Spread уже расширяет литералы — теперь расширяет и call-site.
- Закрывает named arguments без отдельной фичи.
- Refactoring безопасен — добавил параметр, spread-вызовы получают compile error «missing field».
Тонкости:
- Конфликт с variadic spread (D69).
f(...xs)для variadic = «развернуть массив[T]».f(...rec)для record = «развернуть по именам». Различимы по типу spread-выражения (массив vs record), но требует type-check для resolution — мягкий type-directed step. - Дублирует паттерн опций-record в простых случаях. Если уже
есть
fn connect(opts ConnArgs), тоconnect(...opts_record)≈connect(opts_record). Преимущество только когда не хочется заводить отдельный record-тип под опции (т.е. для разнородных параметров). - Композиция с D60:
name(...{ ...defaults, x: 9 })— nested spread внутри record-литерала, потом spread record’а в аргументы. Двухуровневый spread. - Парсер vs type-checker. Сейчас D60 spread — чисто синтаксический. Здесь — семантический: раскладка на этапе type-check’а. Шаг к усложнению, но не критичный.
mut-параметры.fn deposit(mut acc Account, amount money)— spread...{acc, amount}должен сохранятьmut-семантику. Технически тот же self-resolution, что и при обычном вызове.- 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-тип под каждый набор опций.
Прецеденты:
- JavaScript —
f(...obj)spread для массивов, для объектов — только в литералах ({...obj}). Разворачивания в args нет. - Python —
f(**kwargs)разворачивает dict в named-args. Прямой прецедент семантики. - Ruby —
f(**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
Floatprotocol сsin/cos/...(HaskellFloating, Rustnum_traits::Float). Лишняя indirection, generics с bounds для каждой математической функции усложняют сигнатуры.
Решение принято для stdlib: математика на конкретных типах, не через protocol. Но это блокирует пользовательский generic-код.
Вопрос: ввести ли Float (и Numeric) 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. Ввести иерархию Numeric ⊂ Float ⊂ … (как Haskell Num /
Floating / Real). Гранулярные bounds — функция использующая
только +/* требует Numeric, использующая sin/cos — Float.
Лучше типобезопасность, дороже сложность.
type Numeric protocol {
@plus(other Self) -> Self
@times(other Self) -> Self
@neg() -> Self
// ... только арифметика
}
type Float protocol {
@sqrt() -> Self
@sin() -> Self
// ... тригонометрия
// Float наследует Numeric? — открытый подвопрос protocol-наследования
}
Тонкости:
- Конфликт с D74 «отвергнут Float protocol». Формулировка D74 касалась stdlib-реализации. Можно уточнить: stdlib пишет instance-методы напрямую (никакой indirection), а дополнительно объявленный protocol существует только для типизации generic- bound’ов — без runtime-overhead через мономорфизацию.
- Какие методы включить в
Float? Полный набор D74 (~25 методов) делает protocol тяжёлым. Минимум:@sqrt,@sin,@cos,@abs,@hypot,@atan2— что нужно дляComplex/Vector. Остальное — расширения (FloatExtra,Hyperbolic). - Protocol-наследование (Q-protocol-inheritance) — если хочется
Float : Numeric, нужно решение про composition protocol’ов (открытый вопрос D42). Numericдляint? int не имеет sqrt/sin, но имеет +/*/-. Generic-функцияsum[T Numeric](xs []T) -> Tхочет работать и с int, и с f64. Нужна отдельная иерархия.- Прецеденты:
- Rust —
num_traits::Float,num_traits::Num, целая иерархия. Работает, но сложно для новичков. - Haskell —
Num,Floating,Real— каноническая иерархия. Известная сложность для обучения. - Swift —
BinaryFloatingPoint,BinaryInteger,Numeric— протоколы в stdlib. Используется в обобщённой математике. - Go — нет (generics с bounds появились поздно). Дублирование.
- Julia —
AbstractFloat, через duck-typing. Просто, но слабо проверяемо.
- Rust —
Связанные открытые вопросы:
- 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 к существующему типу
(Complex → Complex[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` без скобок
Тонкости:
- Парсер.
[T = f64]— после имени параметра идёт=потом тип. Грамматически чисто (нет конфликтов с другими=в Nova, потому что generic-список окружён[]). - Несколько параметров с default.
[K = str, V = int]— все опциональны, можно не указывать ни одного. Если только часть с default — обязательные должны идти до опциональных ([T, U = f64]✅,[T = f64, U]❌). - Default через bound.
[T Float = f64]— boundFloat+ defaultf64. Парсер:name bound = default. - Inference vs default. Если компилятор может вывести
Tиз аргументов — default не нужен:
Default — это «когда не выводится и не указан».fn first[T = int](xs []T) -> Option[T] first([1, 2, 3]) // T = int (вывод из []int, не default) first[]([]) // []? — пусто, default не помогает // (тип []T неизвестен) - Прецеденты:
- Rust —
struct Vec<T, A: Allocator = Global>. Работает. - C++ —
template<typename T = int>. Работает. - TypeScript —
interface Foo<T = string>. Работает. - Swift, Kotlin — нет default-параметров для generic’ов.
- Java — нет.
- Rust —
Конфликт с D9 «один очевидный путь»:
Default делает Complex и Complex[f64] эквивалентами — это нарушает
«один путь». Но это не выбор для программиста (как с default
arguments — там программист может опустить или передать), это
сокращённая запись. Аналогично D58 implicit iter в for-loop
(for x in xs ≡ for 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
Тонкости:
- Конфликт с tuple-индексом?
t.0,t.1(D37) использует точку и цифру. Char-литерал начинается с'— нет конфликта. - Конфликт со str-литералом?
"a"этоstr,'a'этоchar. Чёткое разделение: одинарные = char, двойные = string. - Single-quote string как в Python? Нет — Python разрешает оба варианта для строк. Nova использует одинарные только для char.
charэто codepoint или byte? Codepoint (как Rust, Swift). Размер 4 байта (Unicode scalar). Не байт-char как в C.- 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) / codepoints.as_chars().nth(i)O(n)). Единственныйstr[..]— byte-range slices[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. Byte | s[i]: byte (u8) | O(1) | Go |
| B. Codepoint ✅ | s[i]: Option[char] | O(i) | Python |
| C. Запрещено | Только s.bytes()[i] / s.chars().nth(i) | n/a | Rust |
Принят вариант 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-levelrolazy-static, D199). НЕ хардкодим вручную, НЕ зависим от ICU/ОС.--check— CI-guard (расхождение с UCD → fail). Прецедент: Rustunicode-*(codegen), Gomaketables. 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. Развилка форм:
str— материализованная строка из 1–3 code point’ов.- Итератор /
CharsView— как Rustchar::to_uppercase() -> ToUppercase: Iterator<Item=char>(lazy, без аллокации для 1-cp случая).Выбран
strпо трём причинам: (1)CharsViewдля char-case в Nova не существует и не нужен — расширение ограничено 1–3 cp (крошечное), lazy-выигрыш нулевой; (2) симметрия со string-levelto_uppercase(s) -> str(case.nv, 152.4.4) — одна ментальная модель; (3)strнапрямую конкатенируется/сравнивается ('ß'.to_uppercase() == "SS") без промежуточногоcollect. Прецедент противоположного выбора (Rust-итератор) обоснован у Rust отсутствием выделенного короткого-строкового типа вcore; у Novastr— 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-функцию без копирования.
Варианты:
- Always nul-terminated.
sliceкопирует с\0. Простой C-interop ценой O(n) на slice. Прецедент: Java Native Interface (черезGetStringUTFChars). - Никогда не гарантировано. Удалить
\0из concat, литералы — честный slice указатель. Любой C-interop — через явныйs.as_cstr() -> *const c_char(копирует если нужно). Прецедент: Rust (&str→CString— явная аллокация). - Текущее 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) ==.
Варианты:
- Auto-intern для коротких строк. Python-style — runtime автоматически интернирует строки до N байт. Скрытая cost.
- Явный
Atomтип. Erlang-style —Atomэто always-interned immutable identifier. Создаётся черезAtom("foo"), сравнение O(1) pointer equality. Sym[T]newtype. Тоньше — пользователь сам объявляет какие строки интернируются (type UserId = Sym[str]). Управление pool’ом через runtime API.- Просто
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 (
externalkeyword), 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 | Финализация | Особенности |
|---|---|---|---|
| Go | bytes.Buffer + strings.Builder | разные методы | разделены: bytes vs UTF-8 |
| Rust | Vec<u8> + String | String::from_utf8 | Vec |
| Java | ByteArrayOutputStream + StringBuilder | .toString() | разделены |
| Python | bytearray + ''.join | manual | разделены |
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.
Прецеденты
| Язык | Тип | Финализация |
|---|---|---|
| Java | StringBuilder | .toString() infallible |
| Go | strings.Builder | .String() infallible |
| Rust | String (с push_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() -> []byteinfallible (любые байты валидны как[]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 crate | WriteBytesExt trait | LE/BE explicit |
Go encoding/binary | binary.LittleEndian.PutUint32 | namespace 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 copy —
ReadBuffer.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 byteorder | ReadBytesExt trait | read_u32::<LE>(...) Result |
Go binary.Read | binary.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 декларации:
- Применяет mangling rules → C-name.
- Применяет Nova→C type mapping → C-prototype.
- Эмитит prototype в сгенерированный header.
- При встрече вызова
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-wins | Java, C++, Swift |
| По arity (разное число аргументов) | ❌ Last-wins | C# 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с разными типами параметра).
Варианты:
- Полная ad-hoc overload (Java/Swift-style). Компилятор резолвит
по статическим типам аргументов. Требует переделки
method_receiversвHashMap<name, Vec<Sig>>+ dispatch-логику. - Только D73-style dispatch. Программист пишет несколько
T.from(V1),T.from(V2)— это уже работает (D73 specifically дляfrom). Расширить same-name multiple-defines на любые статические методы — но с явной семантикой “разные параметры значит разные dispatch-ключи”. - Запретить overload, требовать разные имена. Текущее состояние
bootstrap.
add_str/add_bytesetc. Lower expressiveness, но очень предсказуемо. - 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() -> XvsT.@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 должен:
- Вытащить expected type из контекста выражения (let-annotation, return-position, argument-type вызывающей функции, поле record-литерала).
- Применить как фильтр 3 в D84 resolve: отбросить кандидатов с несовместимым return-type.
- Если после фильтра остался один — выбрать его.
- Если несколько / ноль — 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]vsVec[T] @into() -> Set[T].Json @into() -> UservsJson @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 | Расширения |
|---|---|---|
| Rust | per-type (Clone trait) | derive(Clone) рекурсивно поля |
| Java | shallow (Object.clone) | override для deep |
| Go | value-types (assignment копирует поля) | references — share |
| Python | copy.copy shallow, copy.deepcopy recursive | разные функции |
| JS/TS | Object.assign({}, x) shallow | structuredClone(x) deep |
Варианты для Nova:
- 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 уже-клонированных) или запрет.
- Shallow по умолчанию, явный
@deep_clone(). Дешевле default, но программист должен помнить про share-mut между clone’ами. - Не вводить
@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 даёт
readonlymodifier на отдельных полях record. - НЕТ
readonly Tкак type-modifier для целого типа. - НЕТ
keyof T/ mapped types.
Use-cases:
s.bytes()хочет вернуть[]byteбез mutate-возможности — сейчас нет способа. Workaround: копировать (что и делает D26).- API возвращает «config», который не должен меняться вызывающим — сейчас только конвенция «не меняй».
- Snapshot vs live view — сейчас выражается копированием.
Варианты:
- Полный TS-style.
keyof T, mapped types,readonly T,DeepReadonly<T>. Большая type-system фича. Compile-time only (нет runtime enforcement в managed heap без borrow-checker). Read[T]effect-marker. Маркируем функции «читает только» через эффект — runtime может проверять (через GC marker?) или просто compile-time hint. Согласовано с Nova effect-system.const Tnewtype. Простой type-flag.const []byte— это отдельный тип,[]byte→const []byteчерезas, mutate methods compile-time error.- Не вводить. Полагаться на конвенцию + копирование там где надо независимость.
Тонкость: 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'.
Варианты (на момент обсуждения):
- Запретить keywords как identifiers вообще. Все keywords —
зарезервированы. Программист переименовывает (
input,inq). Самый простой, согласован с большинством языков (Rust/Go/Java). - Контекстно-чувствительные keywords.
inkeyword только вfor x in iter-конструкции, везде ещё — обычный ident. Сложнее парсер, но эргономичнее. Прецедент: Swift, C# (contextual keywords). - Raw-identifier escape.
r#in— keyword как ident через префикс. Прецедент: Rustr#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'. Также синтаксис некорректен по двум
причинам:
effect— kind-token при declaration of named type (D18/D61), не используется в позиции типа значения inline.mutна operation в effect-declaration не описан spec’ом — effects описывают operations безmut(D61).
Варианты (на момент обсуждения):
- Поддержать anonymous effect types. Парсер видит
effect {...}в type-position, парсит method-block, создаёт anonymous effect. Согласовано с D53 (protocol как type). - Только named effects. Программист объявляет
effect Iter[T] { mut next() -> Option[T] }отдельно, потом использует имя. Простой, less expressive. Iter[T]protocol в prelude. Стандартный protocol для итераторов; пользователь принимаетit Iter[T]без объявления. Прецедент: RustIntoIterator, SwiftIteratorProtocol.
Принятое решение: Variant 3 — Iter[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. Это требует:
- Парсер:
[]Tв receiver position — type с inferred type-parameter. - Codegen: ✅ DONE (D119) — generation специализированных функций
для каждой комбинации
(T, U)через mono pass.
Варианты:
- Полная поддержка generic methods на built-in типах. Codegen ✅ готов
(D119). Остался parser-side работа:
[]Tв receiver position. - Free functions с TYpe parameters.
fn map[T, U](xs []T, f fn(T) -> U) -> []U. Просто, но теряется method-syntax (xs.map(f)). - 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).
Варианты:
assert— функция (текущее).assert(cond)обязательны parens. Nova-консистентно (println("..."),Mem.live()— все функции с parens).assert— keyword.assert cond— отдельный statement. Прецедент: Rustassert!(cond)(макрос); Javaassert cond(statement).- 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-8flag; (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]— Resultstr @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/Timehandler’ы (включая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
- Tier 1+ (для concurrent stdlib);
- 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/Timehandler’ы — нужны для AI-first тестов через handler-substitutionHashMap[K, V]— base нет в runtimeIter[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 захваты.
Преимущества:
- Safe by construction. Нет shared state, только структурное агрегирование результатов.
- Типизировано. Compiler знает типы каждого выражения, собирает правильный tuple-тип.
- AI-friendly. Один паттерн вместо двух (
mut-захват vs channels) для типичного fan-out. - Композиция с 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)
Особенности:
parallel— library 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 сейчас.
Тонкости
-
Cancellation на throw. Если первая задача throw’ит, остальные должны отмениться. Реализация через
supervised-style scope под капотом. Если нужен «keep going on error» — программист пишетResult[T, E]в каждой ветке явно. -
Empty / 1-arg parallel.
parallel()илиparallel(f)— тривиальные случаи.parallel(f)≡(f(),)(single-element tuple) — runtime overhead не оправдан, лучше compile-warning. -
Effect-row.
parallel(f, g)имеет union эффектов f и g. В bootstrap при overload-семье — статическая union. С variadic generics — динамическая. -
Async-context.
parallelиспользует suspension (ambient, D62) — сигнатуры fn() -> T чистые, suspension implicit.
Прецеденты
| Язык | Конструкция | Notes |
|---|---|---|
| Rust | tokio::join!(f, g) | macro, возвращает tuple |
| OCaml 5 | Domain.spawn + manual sync | без tuple-builder |
| Erlang | rpc:multicall | для distributed |
| Go | errgroup.Group{}.Go(...) | через group, не tuple |
| Swift | async let a = ... | per-binding async |
| Kotlin | awaitAll(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).
Открытые вопросы:
-
AutoFDO vs обычный PGO?
- Обычный PGO (
-fprofile-generate) — инструментирует binary счётчиками, training run медленнее обычного, но точнее. - AutoFDO (
-fprofile-sample-use) — используетperfsampling, training run без overhead’а, но менее точные профили (sampling на ~100kHz). AutoFDO проще для CI (не нужен инструментированный binary), обычный PGO даёт чуть больше прироста. Не решено.
- Обычный PGO (
-
Profile в репо или нет?
- За хранение: training run может занимать минуты, удобно закоммитить готовый профиль.
- Против: профили платформо-специфичные (x86-64-v3 vs ARM), устаревают при изменении кода, blow up репозиторий.
- Cargo (Rust) — рекомендует не коммитить. Программист сам делает training run.
- Решается при написании полного плана.
-
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,
--pgoshorthand для стандартного workflow).
- Integrated:
-
Каков канонический training workload?
- User-defined — программист передаёт свой workload
(
nova build --pgo-train="./bench/representative.sh"). - Auto-generated — Nova prepare’ит из tests/benchmarks автоматически.
- Рекомендация: оба пути; для stdlib-разработки используем
bench/suite (план 09 Ф.6).
- User-defined — программист передаёт свой workload
(
-
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:
- (B) keyword
handlerснят, литерал эффекта пишется черезeffect X { ops }. Builtin типEffect[E, IRT]переименован вEffect[E, IRT](см. D87 «Plan 97 amendment»).- (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 (effect≡effect). Breaking change, нужен migration sweep по всем тестам и spec’у.
Развилка 2 — anonymous protocol-литералы:
- (C) Не делать (текущее). Protocol реализуется через типы с методами. Идиома Rust/Go/Swift. Аргумент: для reusable протоколов (Hashable, Iter) named-форма естественна; anonymous- форма экономит мало в этих случаях.
- (D) Делать
protocol Fan { run() => () }. Аналог Kotlinobject : 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. Различия:
| Effect | Protocol | |
|---|---|---|
| Структура контракта | методы (operations) | методы |
| Anonymous literal | effect X { ... } ✅ есть | нет (текущее) |
| Применяется в | with X = h { ... } | parameter / generic-bound |
| Типичный use-case | one-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/handler | Anonymous protocol/interface |
|---|---|---|
| Nova (current) | effect X { ... } | нет |
| Koka | with effect X { ... } | нет |
| Eff | handler { ... } | нет |
| 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) |
| TypeScript | — | object-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). - Swift —
extension 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-контексте:
- AI-first locality (R5.1). Anonymous protocol-impl затрудняет поиск реализаций — программист (или LLM) не может grep’ом найти все impls protocol’а если часть из них в expression-position.
- Цена в symbols. Anonymous-impl экономит ~2-3 строки vs
type X {} + fn X @m(). Это малая выгода. - Симметрия в declaration↔literal — слабый аргумент. В Rust
struct X { f: int }объявление иX { f: 42 }литерал тоже не имеют symmetric keyword’ов (нетstruct X { f: 42 }в expression-position). Так делают большинство языков. handlerkeyword — узкий и точный. Не пересекается ни с чем, парсер прост.
Объём работ:
- (B) переименование
handler→effectв literal: правка lexer, парсер, ~30+ тестов вnova_tests/, ~10 spec-документов, AST-узел, codegen, interp. Среднее изменение. - (D) добавление
protocol X { ... }литерала: новый AST-узелProtocolLit, парсер, type-checker (структурная проверка соответствия protocol’у), codegen (синтез anonymous-типа + методов). Большое изменение.
Варианты комбинаций:
| # | Effect-literal | Anon protocol | Объём | Net |
|---|---|---|---|---|
| 1 | handler (A) | нет (C) | 0 | статус-кво |
| 2 | effect (B) | нет (C) | средний | симметрия без новой фичи |
| 3 | handler (A) | protocol (D) | большой | новая фича без переименования |
| 4 | effect (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 есть, не «когда-нибудь появится». Текущая дилемма:
- Если приоритет — минимальный bootstrap — статус-кво (named-типы), документировать через guide «как писать Channel- style API в Nova». Стоимость в каждом stdlib-API — 3-4 строки.
- Если приоритет — идиоматический 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 как структурный контракт.
- D61 —
handlerkeyword в литерале. - 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 гарантируется?
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 функции. Вместо этого keywordcancel_scopeудалён полностью, а внешняя отмена выражается именованным аргументомcancel:уsupervised:supervised(cancel: tok) { body }(ревизия D75, зависит от D102 «именованные аргументы»).Почему не trailing-fn-функция:
supervised— неустранимый keyword (точка регистрацииspawn-fiber’ов, D14/D50). Делать его функцией не выгодно — компилятор всё равно спецкейсит. Аcancel:как именованный аргумент keyword-конструкции не требует ни нового keyword’а, ни scope-introducedtok =>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 без tokenparallel for x in iter { ... }— keyword-forforbid X { ... }— keyword-capabilitywith_timeout(5.s) { ... }— keyword-block с paramcancel_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_scopetoken — обычный fn-call.
Минусы
- Token leak.
tokтеперь lambda-параметр, не scope-binding — его можно сохранить в outerlet 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’ом
для унификации.
Что не решено
- Compile-time token-scope enforcement — есть ли способ сохранить «tok недоступен вне scope’а» без keyword-syntax? Возможно через linear type / borrow checker аналог.
- Codegen efficiency — компилятор должен inline’ить trailing-fn тело чтобы избежать closure overhead. Это требует guarantee от спеки.
- Breaking change для existing code. Текущий
cancel_scope { tok => ... }используется вnova_tests/concurrency/cancel_scope_test.nvиcancel_stress_test.nv— нужна миграция либо backward-compat period. - Все keyword-scope конструкции одновременно или только
cancel_scope? Симметрия требует все, но это significant breaking change для всей structured-concurrency surface.
Прецеденты
- Kotlin —
synchronized(lock) { body }это обычная функцияinline fun synchronized(lock: Any, block: () -> R): R. Trailing lambda is the norm. - Swift —
withCheckedContinuation { continuation in ... }— обычная функция, trailing closure with param. - Scala —
Using.resource(r) { res => ... }— function call. - Rust —
thread::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 магии.
Что из исходных «что не решено» как разрешилось:
- Compile-time token-scope enforcement — признано ненужным: токен caller-owned, escape = no-op; защищается только aliasing, через runtime bind-check.
- Codegen efficiency —
supervised(cancel:)остаётся keyword’ом с custom codegen, closure-overhead вопрос снят. - Breaking change для existing code —
cancel_scope_test.nvиcancel_stress_test.nvмигрируются в Plan 47. - Все 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:). - D50 —
supervised/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) —
NovaStopModeenum{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-flightuv_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().
Варианты:
-
Background fiber outside structured scope — нарушает structured concurrency (Plan 47 D50/D75: spawn только внутри supervised). Token живёт каллер-side, fiber’у нужен parent-scope для drop’а. Сложно без leak’а.
-
OS timer callback (libuv
uv_timer_t) — callback в event-loop thread вызываетnova_cancel_token_cancel(). Обходит fiber-runtime, нужен thread-safe path. -
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. Два пути:
- Использовать existing effects (не вводить новый API) — паритет с Go context, но handler-ceremony более многословно.
- Ввести 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:
- Newtype-aware bounds: UserId/SessionId/OrderId все —
u64newtype’ы; хочется generic method для всех «u64-representable»:fn[T : repr u64] []T @sum() -> u64. - Embed-aware bounds: Profile embeds User (D39); хочется generic
method для всех «User-embedding» record’ов:
fn[T : has User] T @greet() -> str.
Дизайн options:
- Auto-derived protocol from concrete-type shape:
- Record
type User { id u64, name str }auto-derives protocolUser-shape { id() -> u64, name() -> str }. fn[T User]≡fn[T User-shape](structural conformance).- Profile (via D39 embed) auto-satisfies через delegated accessors.
- Record
- Explicit representation-bound:
fn[T : repr u64]— T’s runtime representation = u64. - Explicit embed-bound:
fn[T : has User]— T contains User field (record-only). - 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:
MemOrderingenum:Relaxed | Acquire | Release | AcqRel | SeqCstfence(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:
-
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, чуть громоздко
-
Surrogate gap U+D800-DFFF. Invalid Unicode чтобы char туда попал. Arithmetic должна пропускать surrogates? Или ошибаться?
-
Semantic surprise.
'9' + 1 == ':'(не'10'!). Программисты могут ожидать decimal increment. -
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.
Связь
- Plan 91.8a.2 part 3 — char @compare добавлен.
- D183 amendment — Comparable + synthesis chain.
Финальное напоминание
Прежде чем продолжать дизайн, прочитай:
- README.md — главные тезисы
- decisions/ — все принятые решения с обоснованием
- 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.
Тонкие места:
- Load balancing:
hash(scope_addr) % Nможет неравномерно распределить горячие scope’ы. - Cross-driver cancel cascade (parent driver A → child driver B) добавляет visibility race обратно.
pending_driver_jobscounter работает per-scope, поэтому остаётся sound при шардировании.- Нужны 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)).
Тонкие места:
- Идиоматически это уже делается выходом из
supervisedблока — он сам барьер. - Если открыть barrier user-коду, нужно решить семантику barrier на cascade-cancel: ждать только свой scope или всех linked-children?
- Может конкурировать с 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).
Тонкие места:
- Менять initial cap во время runtime опасно — race с already-allocated scopes.
- Embedded таргетов у Nova ещё формально нет (Plan 83.13 precise-GC в research stage).
- Это 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?
Тонкие места:
- Atomic load counter’а cheap (relaxed read), но aggregation по всем scope’ам требует walk runtime structures — нужен read-lock или snapshot mechanism.
- Пересекается с Plan 76 (
Memeffect для leak/growth тестов) — возможно один общий runtime-introspection API. - Может потребовать 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_intinline (16 bytes/element).Vec[MyValueRecord]хранитNovaValue_MyValueRecordinline.- Работает для ЛЮБОГО T включая Option[U], tuple, value-record.
- Pure Nova реализация — compiler magic не нужен.
- Та же производительность (те же alloc и ptr arithmetic).
Decision table
| T | []T | Vec[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 —
[]Tas sugar overVec[T](Plan 138.1). - D141 —
[]Tbulk 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:265→BinOp::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 (ветка
Dot→Star) + миграция ~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.
Соответствие
| Nova | C++ | Rust |
|---|---|---|
ref int | const int& | &i32 |
ref mut int | int& | &mut i32 |
*int | const int* | *const i32 |
*mut int | int* | *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] = 5lvalue-синтаксис покрывает мутацию элементов- 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 T → E_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 Traw 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 отвергнут).
Три связанных пункта:
-
Единый value-ABI (убрать раздельную обработку).
type Job value {},Tuple(int, str),Tuple(a int, b str), named tuple — это ОДИН класс value-типов, но сейчас лоуэрятся РАЗНЫМ кодом (NovaValue_XvsNovaTuple_X; приёмник: heap-record / value-record by-pointer, а named-tuplero @by-value —Plan 128/§4072 02-types.md). Эта раздельность — источник багов (владелец, 2026-06-21). Цель: ОДИН путь representation+приёмника+передачи изResolvedType. Вектор — U.5/U.6 (свернуть tuple/value-record/sum-schema дубли). -
Авто-by-ref передачи параметров. Большие value-значения передавать указателем автоматически (вместо копии), когда копия не наблюдаема — это и есть «
ref Tавтоматически» (закрывает ЗА-аргумент Q29 «передача больших стековых значений»). Foundation:escape_analyze.rs(Plan 127) уже авто-промоутит value-records; обобщить на by-ref-передачу. -
Авто heap↔stack placement, в т.ч. нерекурсивный sum→стек. Sum-типы сейчас по умолчанию heap (
Nova_X*, 02-types.md:2454) — это правильно для рекурсивных (деревья/связи). Но большинство sum — перечисления/нерекурсивные и могут быть авто-свёрнуты в стек-value (tagged-union), как уже сделано для билтина Option (NovaOptstack-value-sum) vsResult(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 errorE_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.@methodbound-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 на типе.
Открытые места
- Поддерживает ли 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.
- Если да —
- Сообщение об ошибке на call-site
Vec[Widget].clone()гдеWidgetне Clone: должно указывать на отсутствие#impl(Clone)уWidget, не на самVec. - Взаимодействие с 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).
- Method-level narrowing-bound
[T Clone]— ПРИНИМАЕТСЯ bootstrap-парсером и type-checker’ом (нет CC-FAIL на самой сигнатуре). Для record element-типов emit корректен:Vec[Point].clone()даёт вернуюPoint.@clone()рекурсию. - Codegen-dispatch для примитивного
T— ✅ ИСПРАВЛЕН (был[M-138.3-clone-bound-unsupported], CLOSED в Plan 138.4 Ф.1 G-C, commits88432dd6f02+363f4b53788). ROOT CAUSE: НЕ монформизатор — single-key last-winsmethod_receivers["clone"]instance-fallback роутил unbound primitive-T.clone()в неродственный@clone. FIX:PrimBuiltin::Identity—.clone()на примитиве = bitwise self + record/heap identity-clone arm. DeepVec[int].clone()/ deepHashMap[str,int].clone()теперь GREEN. - Deep collection-
@cloneс[T Clone]— АКТИВНА (восстановлена в Plan 138.4 после первоначального VERIFY-OR-KEEP shallow в 138.3). Spec-контракт (D230 amend) — impl догнал prose; §«KNOWN GAP» помечен закрытым. - 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-эмиссию), но это не определяет семантику равенства цикла.
Открытые места
- Что значит
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.
- Bisimulation / graph-isomorphism (Python-style
- Когда обнаруживать цикл? Type-check (как
E_AUTO_DERIVE_CYCLEдляClone, D109 amend Plan 126) или runtime (visited-set)? - Совместимость с 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
- D109 amend (Plan 141) — structural-
==field-by-field. - D109 amend (Plan 126) —
E_AUTO_DERIVE_CYCLEдляClone(прецедент cycle-detection). - Plan 141 — home plan.
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 открытых места.
Открытые места
- 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 — нет. - Shared-mut heap-record под чужим
ro. Handle Aro a, handle Bmut bалиасят один GC-объект; B мутирует → A видит вопрекиro. Per-path-семантика, не баг. Нужна ли opt-in эксклюзивность (uniq/owned) для гарантии object-freeze? - owned-vs-aliased heap-record статически неразличим → граница СИНТАКСИЧЕСКИ на
*(L2 стоп на*), не по aliasing-статусу (неразрешим без ownership-tracking). Нужны ли ownership-аннотации для точной границы (heap-record over-restrictive /*mutunder-restrictive)?
Возможные направления (НЕ V1)
- Ownership-tracking / borrow-checker-lite (Rust) — отвергнут C4 ради GC-простоты; пересмотр если понадобится compile-time гарантия object-immutability/exclusivity.
uniq/ownedopt-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=offbaseline; 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]) — по запросу, не блокеры.
Открытые места
-
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).enforcedefault;off— единственный полный opt-out на build-уровне. -
Поведение 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.
- abort (предложение, дефолт):
-
Гранулярность 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 — там же).
- per-fn
Возможные направления / followups
[M-140-contract-panic-unwind]✅ РЕШЕНО (Plan 140.3, 2026-06-14) — переосмыслено: не «abort→unwind» (assert/контракт в файбере уже разматываются к fail-frame), а унификация классификации:nova_assert_loc/nova_contract_violationтеперь тегают fail-frameerror_kind = NOVA_THROW_PANIC(какnv_panic, D188) → пойманное consume/supervised классифицируется как Panic (не recoverable Failure), по spec D13. Commit60e909a0.[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 gatecontracts_elided_for(kind). Commit77932ea3.[M-140-bounds-as-contract]✅ РЕШЕНО (Plan 140.2) — Vec@indexOOB какrequires 0 <= i && i < @len(D256/D257).
Связь
- D24 — AMEND (Plan 140): enforce-with-elision; «стираются в release» retracted.
- D106 —
verify_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 < c ≡
a <= 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 c → E_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.nv ↔ vec_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()моститVecIter→BoxIter, адаптеры 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, НЕ переписан в сахар. Eagercollections.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 []Twrite-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 += b≡a = 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}(сырой Ca += 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 — _self↦v, i↦idx на сайте 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-сайтах (_self↦v, i↦idx), доказывает под
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 slice —
v[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
@indexbounds как элидируемый контракт. - 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):
- Порог preempt-elision (count ≤ 1024) — захардкожен. Делать tunable (env/attr)? Какое значение оптимально (vectorization-win vs preempt-latency)? Снимется SIGURG’ом (variable-bound без порога).
- Copy-loop safe-set — сейчас только flat-POD элемент (
nova_*/ pointer_p). Расширить на value-record (str) и доказуемо-flat composite? Нужно per-T flatness-доказательство (slot-copy == value-copy). - Обобщить 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-typing — str.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/ оставлен «для
справки». Резолюция трёх открытых пунктов:
- Полная вырезка vs сохранение. Решено сохранить
interp/«для справки» (consistent с «пока не поддерживаем» / D274); из сборки-контракта исключён. Полное удаление модуля сознательно ОТЛОЖЕНО (не отменено) — единственный residual. - Внутренний 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). 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.
Связь
- D278 — source-of-truth + conformance-тест (решение V1).
- docs/plans/104.9-syntax-highlight-keyword-sync.md.
[M-treesitter-grammar-keyword-bump](backlog) — связанный followup по tree-sitter-грамматике.
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-cycle — type 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/оптимизация, НЕ корректность.
Открытые места
-
Method-DCE coarse-by-name. Метод
T.mрежется только если И тип-имяT, И селекторmнедостижимы. Это пересечение по ИМЕНИ, не по точному (тип, метод)-ребру графа вызовов: если в коде есть достижимыйA.foo()и недостижимый-сам-по-себеB.foo(), селекторfooсчитается достижимым, иB.fooover-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 методы) + фикстура на каждый вид. -
Полная 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/).
Связь
- Plan 162 — реализация. D285/D286/D287 — spec.
- Plan 42 Rule A/D, Plan 159.
[M-159-lazy-module-resolution]→ ✅ CLOSED (Plan 162).
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 нужен).
Связь
- Plan 163 — реализация. D288/D289 — spec.
- Plan 162, Plan 42 Rule C / 42.09 re-export.
[M-import-glob-forbid]→ ✅ CLOSED (Plan 163).
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 (видны в пределах папки-модуля).
Открытое
- Нужен ли
priv(type)на методах? Use-case: вспомогательный метод, который не должен вызываться даже другими файлами того же модуля. - Синтаксис:
priv(type) fn @helper()илиfn priv(type) @helper()? - 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).
Это требует:
- Новый
enum IfCondв ast/mod.rs - Замена
IfLet/WhileLetна унифицированныеIf { conds: Vec<IfCond> }/While { conds: Vec<IfCond> } - Обновление всех call-sites (parser, types, emit_c, may_gc, interp)
- Scope semantics:
cond[i]видит биндингиcond[0..i-1]
Трудоёмкость: ~1-2 dev-day, изолированная задача.
Открытое
- Нужно ли? Workaround — вложенные
if:
Менее идиоматично, но читаемо.if Some(x) = get_x() { if Some(y) = get_y(x) && x + y > 10 { } } - Приоритет: низкий — guard (Plan 106) покрывает 90% use-cases.
Связь
- D34 — grammar
if-cond - Plan 106 — реализованный
&&guard - Rust RFC 2497 let-chains — stable 1.64
- Cross-language syntax-gap survey 2026-07-02 — подтверждён как единственный
consider-кандидат в этой зоне; выигрыш над nested-ifмал.
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/Kotlinouter:) чужда грамматике — в 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 даёт () как сейчас.
Открытое
- Нужно ли? Идиома Nova для «глубокий скан» — вынести во вспомогательную функцию с типизированным
return(лучше для «LLM пишет / человек проверяет»). Метки рискуют поощрять вложенность вместо декомпозиции. - Приоритет: низкий. Ниша — hot grid/matrix-сканы, где function boundary мешает
mut-capture / нельзяcontinueвнешний.
Связь
- Cross-language syntax-gap survey 2026-07-02
- Прецедент отказа: Go-mistakes audit #34 «Меток циклов нет» — пересматривается этой записью.
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' =>) — НЕ добавляем (полностью покрыт guardc if c >= 'a' && c <= 'z'; настоящий feature-creep).
Связь
- Cross-language syntax-gap survey 2026-07-02
- D34 — pattern-bind grammar; существующий
Pattern::Orинвариант.
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-массивов). Делать отдельными изолированными ревьюемыми коммитами ПОСЛЕ
объединения, потому что:
- смена мангл-имени = смена ABI → менять консистентно/атомарно, может затронуть рантайм-представление;
- смешивать «объединить движки» с «изменить семантику» во время миграции = §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-блоком.
Связь
- D315 —
ResolvedTypeединый носитель; 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.
Аргументы ПРОТИВ (почему не добавляем)
- Для value-типов copy ≡ move наблюдательно (нет идентичности; GC) →
consume-перегрузка и by-value-перегрузка вели бы себя идентично → выбирать нечего, бессмысленно. - C++ это нужно ради move-конструкторов (украсть heap-буфер + обнулить источник) — у Nova их НЕТ (GC шарит указатель, value = байты, красть нечего) → исчезает сам use-case.
- Притащит сложнейшую/footgun-часть C++ — ref-overloading + perfect-forwarding + ref-collapsing (матрица
T&&/const T&/T&) ради ~нуля выгоды. consume= контракт владения (D131/D133): авто-выбор по value-category → одна и та же записьf(x)то отдаёт владение, то нет, скрытно → footgun (читатель вызова не видит, потратилиxили нет).- Противоречит принятому: оптимизация временных невидима/автоматическая (RVO/NRVO + move-on-last-use, Plan 172.4 Ф.6), а НЕ выбирается перегрузкой; формализация
ref(D326) тоже не делаетrefuser-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). - D326 —
refкак режим, не тип, не 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 ниже); здесь — что осталось спроектировать.
Осталось спроектировать
-
any/ fallible-race— варианты на падающих конкурентах. Базовыйrace[T](...funcs []fn() -> T) -> T(первый ЗАВЕРШИВШИЙСЯ, чистый, канальный) записан в 173.1 §2a. Остаётся:- fallible-
race— конкурентыfn() Fail[E] -> T; первый завершившийся может УПАСТЬ → самracefallible:race[T,E](...funcs []fn() Fail[E] -> T) Fail[E] -> T. any/ «первый успешный» — ждать первогоOk, игнорируя падения; упасть только если упали ВСЕ (JavaanySuccessfulResultOrThrow). На канале: recv до первогоOkили закрытия. Отличие отrace: быстрый сбой НЕ выигрывает («быстрейшая ЖИВАЯ реплика»).- Открыто: имена (
any/race_ok/first_ok); агрегация ошибок при «все упали» (MultiError vs последняя); гейт closures-in-generic-array (Plan 48 Ф.4) — общий сrace.
- fallible-
-
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:…).
- точный API/синтаксис
Уже записано (НЕ переоткрывать)
- Гетерогенный
parallel(f,g) → кортеж— отдельный Q-parallel-tuple (выше в этом файле). - Целевой
race(общий N-арный, канальный) + droprace2— 173.1 §2a. parallel(timeout:|deadline:|…) forcall-site + таймаут →throw Timeout+ opt-in[]Result— 173.1 §2 п.5.defercompletes-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-блока).