/
nv-lang
/
nova
Обзор
Документация
Войти
/
nv-lang
/
nova
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
main
compiler-codegen/src/codegen/emit_c.rs
66 470 строк
4 MB
Evgeniy Golovin
registry(#592): qualified Vec[<elem>].method() turbofish drops int erasure shortcut
11 авг 2026, 21:47
11 авг 2026, 21:47
c45dfa8
Код
Авторство
О чём код?
use crate::ast::*; use crate::diag::Span; use crate::parser::impl_spec_base_name; use std::cell::RefCell; use std::collections::{HashMap, HashSet, BTreeMap}; use std::fmt::Write as FmtWrite; // №240 [M-detach-box-while-loop-read-after]: `emit_detach` + its // `hoist_box_decl` helper live in a child module (arch-ratchet headroom — // scripts/guards/arch-ratchet.sh only measures this file). See that // module's doc comment. mod emit_detach; /// Plan 11 Ф.1: одна signature метода в multi-overload registry (`method_overloads`). /// /// Plan 172.1 U.2.5 (§0, [M-172-sig-registry]): `MethodSig` СВЁРНУТ в единый тип /// [`crate::sig_registry::CodegenView`] — поля byte-идентичны (`param_c_types`/`return_c_type`/ /// `is_instance`/`is_external`/`is_delegated`/`c_name`/`variadic_last`/`param_defaults`/ /// `recv_mutable`). Один тип вместо двух копий; `method_overloads` хранит `Vec<CodegenView>`. /// (ИСТОЧНИК построения `method_overloads` пока codegen-локальный — byte-identical-унификация /// источника = U.2.4, заблокирована mangling-фрагментацией, см. `[M-172.1-U2.4-mangling-fragmented]`.) pub type MethodSig = crate::sig_registry::CodegenView; /// Plan 172.1 (§0 single source): типы, ЗАранее определённые в C-runtime-хедерах /// (`nova_rt/*.h`: array.h, sync_primitives.h, effects.h) или как hand-written typedef /// (`nova_str`). Codegen НЕ эмитит для них ни struct-body, ни forward-decl `typedef /// struct Nova_X` — определение приходит из хедера (иначе typedef redefinition, напр. /// `struct Nova_MutexGuard` vs header `struct Nova_MutexGuard_s`). ЕДИНЫЙ список, /// потребляемый ОБОИМИ сайтами: `emit_type_decl` (skip struct-body) И type-fwd-decl /// loop в `emit_module` (skip forward typedef). Раньше дублировался (fn-local в /// emit_type_decl + неявно через BUILTIN_RUNTIME_TYPES external-loop), что пропускало /// fwd-decl для INLINED (через `import`) sync-типов → redefinition (Plan 172.1 U.1.3b). pub(crate) const RUNTIME_DEFINED_TYPES: &[&str] = &[ // core prelude (C structs/constructors в nova_rt/array.h) "Option", "Result", "Error", "RuntimeError", // effect vtables (nova_rt/effects.h) // Plan 175 Ф.1: TimerMetrics — read-only introspection effect split out // of Time (Q1). Direct-C dispatch (Nova_TimerMetrics_timer_*, no // vtable). Schema built from its .nv decl (single source). // // "Mem" REMOVED (D76 amend, [M-mem-effect-demote-to-namespace], // 2026-08-01): no longer an effect — demoted to a plain namespace type // (`export type Mem`, std/prelude/effects.nv), same RUNTIME_DEFINED_ // TYPES-exempt shape as any other ordinary user type (no special-case // needed — a bare unit-type struct-body is fine to emit normally). // // Plan 175 Ф.2-v3 (снос рукописного `NovaVtable_Time`): "Time" REMOVED // from this list — Time теперь генерируется ЧЕРЕЗ ОБЩИЙ effect-codegen // путь (`emit_effect_type`, тот же что user-эффекты), НЕ через // hand-written struct в nova_rt/effects.h (которая снесена). Time // declaration живёт в `std/time/duration/time_effect.nv` (Plan 175.2 // Ф.2-v4 П6 moved it OUT of prelude — module name is irrelevant to this // list/codegen path, keyed purely by the effect NAME "Time" regardless // of which module declares it) — струкура/dispatch/handler-slot // генерируются codegen'ом как для любого `type X effect {...}`. // `#default_handler` (bare form, П7/D431; fn `real_time`, П8-rename) — // ambient-fallback (Time работает без явного `with`/`import`) больше // не хардкод-C-путь, а ОБЫЧНЫЙ generic `#default_handler` механизм; // registration — compile-unit-wide (см. `check_default_handlers`), не // требует prelude-резидентства. "Fail" остаётся (владелец: Fail — // сильно встроенный, хардкод намеренно НЕ трогается). "Fail", "TimerMetrics", // sync (sync_primitives.h): MemOrdering + sized atomics. // Plan 207 (2026-07-16 consolidation): AtomicPtr removed (int-proxy // duplicate, no generic [T] yet — Plan 103.7); Isize/Usize spellings // renamed to AtomicInt/AtomicUint (Nova has no isize/usize types). "MemOrdering", "AtomicI8", "AtomicI16", "AtomicI32", "AtomicI64", "AtomicU8", "AtomicU16", "AtomicU32", "AtomicU64", "AtomicInt", "AtomicUint", // Plan 248 (wave 3, D447 #no_copy): was missing — a pre-existing gap // invisible before this wave (Newtype-kind types are gated by the // SEPARATE `debt_is_runtime_backed_newtype` list, which DID include // "AtomicBool"; this const only mattered for Record/Sum-kind checks, // and AtomicBool was never Record-kind until this wave). Missing here // made emit_type_decl's RUNTIME_DEFINED_TYPES skip-gate not fire for // AtomicBool once it became `value priv {...}` (Record+Value kind) — // codegen auto-emitted a COMPETING `NovaValue_AtomicBool` struct // definition, conflicting with the hand-written one in // sync_primitives.h ("typedef redefinition with different types"). "AtomicBool", // sync sum-types pre-declared (OnceState, WaitResult) "OnceState", "WaitResult", // consume guard types (sync_primitives.h `_s`-suffix structs) "MutexGuard", "ReadGuard", "WriteGuard", "Permit", "OnceGuard", // lang-item str → hand-written typedef nova_str (nova_rt.h) "str", // Plan 207 [M-cas-return-witnessed-value]: CAS-witness value structs // (`NovaTuple_CasRaw*` in sync_primitives.h) — raw (ok, witness) pair // returned by the private `@cmpxchg` intrinsic; the public // compare_exchange/_weak wrapper (plain .nv fn) builds Result[(), T] from it. "CasRawI8", "CasRawI16", "CasRawI32", "CasRawI64", "CasRawU8", "CasRawU16", "CasRawU32", "CasRawU64", "CasRawInt", "CasRawUint", "CasRawBool", // [M-consume-block-cancelerror-bare-cu]: `CancelError` — the D314 // consume-cleanup codegen desugar (`assign_scope_outcome_from_frame`, // below) hand-emits `Nova_CancelError`/`nova_alloc(sizeof(Nova_CancelError))` // UNCONDITIONALLY on every CANCEL/FromFrame exit path of ANY consume- // cleanup (`@cleanup(outcome ScopeOutcome)`), regardless of whether the // fn body ever narrows `err is CancelError`. `ScopeOutcome` (needed for // the `@cleanup` signature itself) lives in `std/prelude/core.nv` — a // separate, always-on prelude sub-module (Plan 62.F split) from // `CancelError` (`std/prelude/errors.nv`, opt-in via `#prelude(errors)`). // A `#prelude(core, ...)` compile unit that never selects `errors` (or // never otherwise merges it) never gets the `.nv` struct decl, so // codegen's hardcoded C reference had no typedef → CC-FAIL (`use of // undeclared identifier 'Nova_CancelError'`). Listing it here — mirrors // Error/RuntimeError — makes the C layout (`nova_rt/array.h`) the // always-available source regardless of prelude-subset selection; // `emit_type_decl`/the fwd-decl loop skip the `.nv`-driven emission when // (as in the default full prelude) the type IS ALSO merged, so no // redefinition. `err is CancelError` narrowing is unaffected — the // checker still requires the `.nv` `CancelError` name to be visible // (via the `errors` prelude sub-module) to type-check that syntax at // all; this entry only guarantees the C layout backing it. "CancelError", ]; /// №390 fix: the subset of `RUNTIME_DEFINED_TYPES` that are confirmed /// `AllocKind::Value` records with a hand-written `NovaValue_<Name>` C /// struct (`sync_primitives.h`) — needed by `escape_analyze`'s `X.new(...)` /// constructor-call inference (`infer_value_record_from_expr`), which /// otherwise only sees types reachable via `module.items`/`peer_files`. /// These sync-primitive types are RUNTIME_DEFINED_TYPES-gated (their /// TypeDecl is skipped for forward-decl emission — "the header owns the /// struct", see the `RUNTIME_DEFINED_TYPES.contains` branch a few hundred /// lines below) and consequently do NOT reliably appear in the flattened /// `Module` escape_analyze receives whenever their home module /// (`std/src/runtime/sync.nv`) isn't a *direct* peer of the entry file /// (only transitively reached, e.g. via `std.net`'s own internal use) — /// confirmed empirically: `docs/plans/221.1-bug-sweep.md` №390 root cause, /// `std/src/net/tcp.nv::TcpStream.from_raw`'s `mut counter = AtomicInt.new(1)` /// silently failed to heap-promote for exactly this reason, leaving `&counter` /// dangling once `from_raw` returned. /// /// Deliberately NARROWER than the full `RUNTIME_DEFINED_TYPES` list: entries /// like `Option`/`Result`/`Error` are NOT plain value-records in this sense /// (generic/tagged, no address-escaping `X.new(...)` pattern used against /// them by user `.nv` code the way `AtomicInt`/`AtomicBool`/etc. are) — only /// list types actually safe to treat this way. pub(crate) const RUNTIME_VALUE_RECORD_CTOR_TYPES: &[&str] = &[ "AtomicI8", "AtomicI16", "AtomicI32", "AtomicI64", "AtomicU8", "AtomicU16", "AtomicU32", "AtomicU64", "AtomicInt", "AtomicUint", "AtomicBool", ]; /// Plan 39 Issue A: classification of `with`-block trail type for /// choosing which NovaInterruptFrame slot to use. /// /// - `IntLike`: `nova_int`, `nova_bool`, `nova_byte`, `nova_char`, plain /// integers that fit in `nova_int` slot. /// - `Pointer`: any C type containing `*` — `Nova_X*`, `NovaArray_X*`, /// `void*`. Stored in `value_ptr` directly. /// - `ValueStruct`: heap-stored value structs (`NovaOpt_X`, `NovaResult_X_E`, /// tuples). Stored via heap-alloc'd slot pointed to by `value_ptr`. /// - `UnitVoid`: unit / void — no value, slot unused. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum WithResultCategory { IntLike, Pointer, ValueStruct, UnitVoid, } /// D109: встроенные методы примитивных типов. /// Plan 138.4 Ф.1 G-C: `Identity` — built-in `.clone()` на примитиве/POD = /// bitwise self-copy (примитивы immutable / value-семантика). Эмитится как /// receiver-выражение без изменений; return-type = тип receiver'а. enum PrimBuiltin { Fn(&'static str), BinOp(&'static str), Identity } /// Plan 39 Issue A: walk handler expression (typically a ClosureLight / /// ClosureFull / HandlerLit) и найти первый `interrupt VAL` — вернуть /// C-тип VAL. Используется в `infer_expr_c_type` для `With`, когда body /// не имеет trailing (тогда тип blocка определяется handler'ом). /// /// Plan 196.3 (мелочь-пакет, one-window inventory) verdict: LEGIT-LOWERING, /// не кандидат на прямую миграцию к `resolved_types`/`resolved_callees` — /// это структурный AST-walk (найти узел `Interrupt`, не переизобретённая /// проверка типа), а фактический C-тип VAL уже читается через /// `emitter.infer_expr_c_type(v)`, который сам «channels FIRST, legacy /// LAZY» (Plan 172.1 §0/§1, см. :48691) — то есть чтение канала уже /// происходит на один уровень ниже. Прямое чтение `resolved_types` ЗДЕСЬ /// невозможно by design: оба вызывающих (`probe_handler_ty` / :8883, /// `probe_handler_ty_ro` / :8922) намеренно временно переопределяют тип /// первого параметра handler'а на РЕАЛЬНЫЙ payload-тип `Fail[E]` ПЕРЕД /// вызовом этой функции, потому что чекер типизирует `|e| interrupt /// Some(e)` по WIRE-схеме эффекта (hardcoded `nova_str`, effects.h), а не /// по конкретной инстанциации `E` — известное ограничение задокументировано /// ещё в Plan 120 (`docs/plans/120-named-tuples-and-allocation-contract.md` /// `[M-D215-defaults-handler-lambda-type]`) и закрыто этим rebind-приёмом. /// Значит `resolved_types[interrupt_val.id]` на момент вызова либо /// отсутствует, либо содержит устаревший WIRE-тип — не источник истины. pub fn infer_handler_interrupt_ty(emitter: &CEmitter, handler: &Expr) -> Option<String> { use crate::ast::{ClosureBody, FnBody, ElseBranch}; fn walk_expr(emitter: &CEmitter, e: &Expr, out: &mut Option<String>) { if out.is_some() { return; } match &e.kind { ExprKind::Interrupt(Some(v)) => { *out = Some(emitter.infer_expr_c_type(v)); } ExprKind::Interrupt(None) => { *out = Some("nova_int".into()); } ExprKind::Block(b) => { walk_block(emitter, b, out); } ExprKind::If { then, else_, .. } => { walk_block(emitter, then, out); if let Some(eb) = else_ { match eb { ElseBranch::Block(b) => walk_block(emitter, b, out), ElseBranch::If(if_expr) => walk_expr(emitter, if_expr, out), } } } ExprKind::Match { arms, .. } => { use crate::ast::MatchArmBody; for a in arms { match &a.body { MatchArmBody::Expr(e) => walk_expr(emitter, e, out), MatchArmBody::Block(b) => walk_block(emitter, b, out), } } } _ => {} } } fn walk_stmt(emitter: &CEmitter, s: &Stmt, out: &mut Option<String>) { match s { Stmt::Expr(e) => walk_expr(emitter, e, out), Stmt::Return { value: Some(e), .. } => walk_expr(emitter, e, out), _ => {} } } fn walk_block(emitter: &CEmitter, b: &Block, out: &mut Option<String>) { for s in &b.stmts { walk_stmt(emitter, s, out); } if let Some(t) = &b.trailing { walk_expr(emitter, t, out); } } let mut out: Option<String> = None; match &handler.kind { ExprKind::ClosureLight { body, .. } => match body { ClosureBody::Expr(e) => walk_expr(emitter, e, &mut out), ClosureBody::Block(b) => walk_block(emitter, b, &mut out), }, ExprKind::ClosureFull(sb) => match &sb.body { FnBody::Expr(e) => walk_expr(emitter, e, &mut out), FnBody::Block(b) => walk_block(emitter, b, &mut out), FnBody::External => {} }, ExprKind::Lambda { body, .. } => walk_expr(emitter, body, &mut out), ExprKind::HandlerLit { methods, .. } => { use crate::ast::HandlerMethodBody; for m in methods { match &m.body { HandlerMethodBody::Expr(e) => walk_expr(emitter, e, &mut out), HandlerMethodBody::Block(b) => walk_block(emitter, b, &mut out), } if out.is_some() { break; } } } _ => {} } out } /// Plan 39 Issue A: pick category from C type string. pub fn with_result_category(c_type: &str) -> WithResultCategory { let t = c_type.trim(); if t == "nova_unit" || t == "void" || t.is_empty() { return WithResultCategory::UnitVoid; } if t == "nova_int" || t == "nova_bool" || t == "nova_byte" || t == "nova_char" || t == "nova_i8" || t == "nova_i16" || t == "nova_i32" || t == "nova_i64" || t == "nova_u8" || t == "nova_u16" || t == "nova_u32" || t == "nova_u64" || t == "nova_f32" || t == "nova_f64" { return WithResultCategory::IntLike; } if t.contains('*') { return WithResultCategory::Pointer; } // String types are wrappers (nova_str is a struct by value). // Value structs: NovaOpt_X, NovaResult_X_Y, tuples, user-defined records by value. WithResultCategory::ValueStruct } /// Plan 81 Ф.7.2 — compiler-level reachability dead-code elimination for /// free functions. /// /// Returns the set of **monomorphic free-function names** unreachable from /// any codegen root: their forward declaration and body are omitted from /// the generated C (smaller `.c`, faster C compilation — the final binary /// was already trimmed by linker-level DCE, Ф.7.1). /// /// **Scope of THIS wrapper — only the monomorphic free-function subset.** /// - Generic free functions are already emitted lazily via the /// monomorphization worklist — an uninstantiated one is never emitted. /// - This wrapper returns `.dead_fns` only. **Method-level DCE IS implemented** /// (Plan 159) — `compute_dead_decls_with` collects every monomorphic method /// as `(receiver_type, method_name, refs)` and *fires* it (keeps body+fwd, /// anchors callees) iff BOTH the receiver-type name AND the method name are /// reachable (the intersection at the method-firing loop, ~:418-424); never /// fired ⇒ `dead_method_keys`, whose fwd+body are skipped at the emission /// gates (~:3091-3100). Granularity is coarse-by-name (`[M-159-method-pruning]`): /// a reachable name-collision over-keeps (never over-prunes). Consts/`ro`-globals /// likewise pruned via `.dead_consts`. See `compute_dead_decls_with`. /// /// **Soundness.** A monomorphic free function is only ever *called* by its /// name appearing syntactically — a bare `Ident` (same-module / merged /// calls) or a `Member` selector (`mod.func()` module-qualified calls, /// collected by `collect_used_names` since Ф.7.2). `collect_used_names` /// performs a complete AST walk over every expression / statement / type / /// contract position. Roots: `main`; every name referenced by a /// non-candidate item (methods, generic fns, tests, benches, consts, /// types, externals — all emitted unconditionally); and exported free /// functions (cross-module API surface). The transitive closure over /// candidate→candidate call edges is the reachable set; the dead set is /// its complement. Over-approximation is always toward keeping a function /// (e.g. a name shared by an overload, a local, or a struct field) — never /// toward dropping a reachable one. /// /// **DCE is anchored to `fn main`.** If the module has no `main`, it is /// not an executable — a library compiled standalone, or a `nova check` / /// negative-test target — and there is no entry point to prune against: /// every free function is kept. (When such a library is later *imported* /// into an executable its functions are merged into that executable's /// module and pruned there, anchored by the executable's `main`.) This /// also keeps negative `EXPECT_CC_ERROR` fixtures intact — their /// erroneous, `main`-less function bodies must still reach the C compiler. fn compute_dead_free_fns(module: &Module) -> HashSet<String> { compute_dead_decls(module).dead_fns } /// Plan 159 Ф.1: kill-switch for reachability DCE. /// /// `NOVA_REACH_DCE` unset or any value ≠ `"0"` → reachability DCE **enabled** /// (the new behaviour, default). Set to exactly `"0"` → **disabled**, byte-for-byte /// identical to the pre-159 output (export-as-root + consts/ro-globals emitted /// unconditionally). Read once via `OnceLock`. fn reach_dce_enabled() -> bool { use std::sync::OnceLock; static ENABLED: OnceLock<bool> = OnceLock::new(); *ENABLED.get_or_init(|| { match std::env::var("NOVA_REACH_DCE") { Ok(v) => v != "0", Err(_) => true, } }) } /// Result of the module-level reachability analysis (Plan 159 Ф.1). /// /// `dead_fns` — monomorphic free-function names unreachable from any root /// (forward decl + body omitted). `dead_consts` — module-level `const` / /// `ro` lazy-static global names unreachable from any root (their giant /// `static` table definitions omitted). Both are the *complement* of one /// shared reachable set computed in a single closure, so const→fn, fn→const /// and const→const edges all resolve. #[derive(Default)] struct DeadDecls { dead_fns: HashSet<String>, dead_consts: HashSet<String>, /// Plan 159 Ф.1: `(receiver_type, method_name)` pairs that can never run — /// their receiver type is never constructed/named OR their name is never /// invoked in reachable code (both are necessary for a method to execute). /// Such a method's body + forward decl are omitted (otherwise they would /// dangle on the pruned free fns / consts they call). Empty when DCE is /// disabled or in library mode → the method gate is a no-op there. dead_method_keys: HashSet<(String, String)>, } /// Plan 81 Ф.7.2 + Plan 159 Ф.1 — compiler-level reachability dead-code /// elimination over **free functions AND module-level consts / ro-globals**. /// /// ## Roots & candidates /// - **Candidates** (prunable): monomorphic free fns (no receiver, no generics, /// not `External`); module-level `Item::Const`; and `ro` lazy-static /// `Item::Let` globals (single `Ident` / unit-`Variant` name, non-ghost — /// mirrors the emission gate at §1b1). A candidate is dropped only if it is /// unreachable from every root. /// - **Roots**: `main`; and every name referenced by a truly-unconditional item /// (generic fns, externals, tests, benches, type *declarations*). These items /// are always emitted, so anything they name must be kept. /// Consts/ro-globals are **no longer auto-roots** (Plan 159 Ф.1): a table is /// kept only if a reachable fn/const actually reads it. /// - **Methods** (`receiver.is_some()`) **undergo body-level DCE** (Plan 159): a /// method is kept iff it *fires* — its *referenced names* are anchored to their /// **receiver type's reachability** AND the method name is itself reachable /// (the `(type-name ∧ method-name)` intersection at ~:418-424), rather than /// being unconditional roots. Never-fired ⇒ `dead_method_keys`, fwd+body /// skipped (~:3091-3100). A method `T.m()` can only ever be invoked on a value /// of type `T`; constructing such a value makes the name `T` appear in /// reachable code, so the receiver-type-name reaches the closure exactly when /// a live `T` can exist at runtime. Until `T` is reached, `m`'s callees are /// not anchored — this is what lets an executable that imports `std.unicode` /// but never names `Collator`/`Normalizer` drop the collate/normalize tables. /// Conservative: if `T` is named ANYWHERE reachable, ALL of `T`'s methods /// (and everything they call) are kept (over-keep, never over-prune). /// - **Exported free fns**: under the old policy (`NOVA_REACH_DCE=0`) an exported /// fn is a root (cross-module API surface). Under the new policy, in an /// **executable** (`has_main`) an exported Nova fn is *not* a root — Nova has /// no C-ABI export (FFI is Nova→C only via `is_external`/extern-abi), so an /// executable's exported fns are never linked externally and follow normal /// reachability. In library mode (no `main`) the whole module is kept anyway. /// /// ## Soundness (G0 — conservative: over-keep, never over-prune) /// `collect_used_names` is a complete AST walk collecting bare `Ident` names /// and `Member` selectors. A candidate is *kept* whenever its name appears /// anywhere reachable — so a const/fn name that merely collides with a local, /// field, or overload is conservatively kept. Over-approximation is always /// toward keeping. Method-anchoring keys on the receiver type *name*; any /// syntactic appearance of that name (collision included) keeps the methods. /// /// ## `no main` guard /// No `main` → not an executable (standalone library / `nova check` / negative /// `EXPECT_CC_ERROR` fixture) → nothing to anchor against → **keep everything** /// (empty dead sets). This guard is preserved verbatim from Plan 81. fn compute_dead_decls(module: &Module) -> DeadDecls { compute_dead_decls_with(module, reach_dce_enabled()) } /// Plan 159 Ф.1: testable core of [`compute_dead_decls`] with the reachability-DCE /// flag passed explicitly (the public wrapper reads it from the process-global /// `NOVA_REACH_DCE` kill-switch via a `OnceLock`, which a unit test cannot toggle /// between cases). `enabled = false` reproduces the pre-159 policy byte-for-byte: /// consts / `ro`-globals are not candidates (never pruned), exported free fns are /// roots, and no method-body DCE. fn compute_dead_decls_with(module: &Module, enabled: bool) -> DeadDecls { let is_fn_candidate = |f: &FnDecl| -> bool { f.receiver.is_none() && f.generics.is_empty() && !matches!(f.body, crate::ast::FnBody::External) }; // Name of a `ro` lazy-static global candidate (mirrors §1b1 emission gate), // or `None` if this `Let` is not a single-named non-ghost binding. let let_candidate_name = |l: &LetDecl| -> Option<String> { if l.is_ghost { return None; } match &l.pattern { Pattern::Ident { name, .. } => Some(name.clone()), Pattern::Variant { path, kind: VariantPatternKind::Unit, .. } if path.len() == 1 => { Some(path[0].clone()) } _ => None, } }; // No `main` → not an executable → keep everything. let has_main = module.items.iter().any(|it| { matches!(it, Item::Fn(f) if f.name == "main" && f.receiver.is_none()) }); if !has_main { return DeadDecls::default(); } // Candidate name sets (kept separate so we can split the dead complement // back into fns vs consts/globals at the end). Const + ro-global names // are only candidates under the new policy. let mut fn_candidates: HashSet<String> = HashSet::new(); let mut const_candidates: HashSet<String> = HashSet::new(); for item in &module.items { match item { Item::Fn(f) if is_fn_candidate(f) => { fn_candidates.insert(f.name.clone()); } Item::Const(c) if enabled => { const_candidates.insert(c.name.clone()); } Item::Let(l) if enabled => { if let Some(name) = let_candidate_name(l) { const_candidates.insert(name); } } _ => {} } } if fn_candidates.is_empty() && const_candidates.is_empty() { return DeadDecls::default(); } // Union view for the closure membership test. let is_candidate = |name: &str| -> bool { fn_candidates.contains(name) || const_candidates.contains(name) }; // refs_of[name] — names referenced by candidate decl `name` (overloads / // same-named items are unioned — kept or dropped together). let mut refs_of: HashMap<String, HashSet<String>> = HashMap::new(); // Methods, keyed by `(receiver_type, method_name)` → union of names the // method(s) reference. A method `T.m` can actually run only when BOTH a value // of type `T` exists AND `m` is invoked by name: // * `T` reachable — `T` is constructed / named in reachable code (any // constructor / annotation / variant spells `T`); // * `m` reachable — `….m(…)` (direct OR protocol dynamic dispatch, which // also writes `m` at the call site) or `….m` taken as a fn value, both // collected by `collect_used_names` as a `Member` selector. // We anchor a method's callees (and keep its body) iff its type AND its name // are both reachable — the intersection. This is conservative (each // condition is necessary for the method to ever run) yet precise enough to // drop `char.is_uppercase`/`Collator.key` chains in a program that only // calls free `is_alphabetic`, even though common names like `key` collide. let mut methods: Vec<(String, String, HashSet<String>)> = Vec::new(); // Worklist seed: `main` is always a root. let mut worklist: Vec<String> = vec!["main".to_string()]; // Plan 175 Ф.2-v2 (Ф.2-v3: `_nova_time_default_ctor` special-case gone, // seeding rationale unchanged): `#default_handler(X)` fns are referenced // ONLY via a raw C-text fn-pointer assignment codegen emits INSIDE the // generic `Nova_X_<op>()` dispatcher body (`emit_effect_type`, // `if (!_nova_handler_X) { _nova_handler_X = ctor(); }`) — invisible to // `collect_used_names`'s AST walk, so without this seed a program that // never calls an effect op by name in a way DCE can see (e.g. only via // the `#default_handler` ctor's OWN body) would get its default-handler // fn pruned as dead, leaving the generated fn-pointer assignment // referencing an undefined symbol (mirrors the `main` seed above — this // fn is always "called", just not through an AST `Call` node). for it in &module.items { if let Item::Fn(f) = it { if f.doc_attrs.iter().any(|a| matches!(a, crate::ast::DocAttr::DefaultHandler(_))) { worklist.push(f.name.clone()); } } } for pf in &module.peer_files { for it in &pf.items_here { if let Item::Fn(f) = it { if f.doc_attrs.iter().any(|a| matches!(a, crate::ast::DocAttr::DefaultHandler(_))) { worklist.push(f.name.clone()); } } } } // Plan 209 Ф.2 (HashMap-mono-gap fix): D39 embed-delegation proxies // (`emit_embed_proxies`) are SYNTHESIZED directly in codegen — never AST // `Item::Fn` nodes — so `collect_used_names` above can never see the call // they make into the embedded type's base method (e.g. `Set[T]`'s // auto-generated `merge_from`/`values` proxy calling `HashMap[K,V]`'s own // `merge_from`/`values`). That left the base method's `(type, name)` // reachability firing on the type alone (the wrapper's type decl always // names the embedded type, satisfying the `ty_anchored` half) while the // NAME half could stay unreached whenever nothing in Nova SOURCE TEXT // literally spells the method name (e.g. `for x in some_set {}` reaches // `values()` only via codegen-internal iteration lowering, never as an // AST `Member` node) — the base method's body was then dropped as dead, // while the always-unconditional proxy still called it: `static` linkage // + C-compiler DCE masked this (an unreached proxy was itself stripped // before the linker saw its bad reference); external-linkage promotion // (Plan 209 `top_level_storage()`) makes the proxy survive regardless of // use, turning the gap into `undefined symbol` at link time. // // Fix: any type that is ever embedded (`use <field> <EmbeddedType>[...]`) // has ALL of its instance-method NAMES anchored reachable by name alone — // mirrors the existing over-keep pattern below for concrete-slice // receivers (`ty.starts_with("[]")`): conservative (never over-prune) // rather than precisely replaying `emit_embed_proxies`' own // override-precedence logic here. let embedded_type_names: HashSet<String> = module.items.iter() .filter_map(|it| match it { Item::Type(t) => match &t.kind { TypeDeclKind::Record(fields) => Some(fields.iter() .filter(|f| f.is_embed) .filter_map(|f| match &f.ty { TypeRef::Named { path, .. } => Some(path.join("_")), _ => None, }) .collect::<Vec<_>>()), _ => None, }, _ => None, }) .flatten() .collect(); for item in &module.items { let mut refs: HashSet<String> = HashSet::new(); crate::lints::collect_used_names(std::slice::from_ref(item), &mut refs); match item { Item::Fn(f) if is_fn_candidate(f) => { if f.is_export && !enabled { // Old policy: exported free fn is a cross-module API root. // New policy in an executable: exported fns are NOT linked // externally (Nova has no C-ABI export) → follow normal // reachability, so do NOT seed as a root. worklist.push(f.name.clone()); } refs_of.entry(f.name.clone()).or_default().extend(refs); } Item::Fn(f) if enabled && f.receiver.is_some() => { // Method (monomorphic only — generic methods emit lazily via the // mono worklist). Anchored to the type∧name intersection below. if f.generics.is_empty() { let ty = f.receiver.as_ref().unwrap().type_name.clone(); // Plan 209 Ф.2: embedded-type method — anchor the NAME half // unconditionally (see doc above `embedded_type_names`). if embedded_type_names.contains(&ty) { worklist.push(f.name.clone()); } methods.push((ty, f.name.clone(), refs)); } else { // Generic method: leave its refs as unconditional roots (it // is emitted on demand by the monomorphizer; be conservative). worklist.extend(refs); } } Item::Const(c) if enabled => { refs_of.entry(c.name.clone()).or_default().extend(refs); } Item::Let(l) if enabled && let_candidate_name(l).is_some() => { let name = let_candidate_name(l).unwrap(); refs_of.entry(name).or_default().extend(refs); } _ => { // Truly-unconditional item (method [old policy] / generic fn / // external / test / bench / type decl — and, under the old // policy, const / let) is emitted unconditionally, so every name // it references is a reachability root. worklist.extend(refs); } } } // Closure over fn/const/global edges, with methods firing on the type∧name // intersection. Because a fired method can make new type-names/method-names // reachable, we re-run the name closure until no further method fires // (monotone fixpoint — `reachable` only grows). `live_method` tracks which // methods have already fired so each fires at most once. let mut reachable: HashSet<String> = HashSet::new(); let mut live_method = vec![false; methods.len()]; loop { // Drain the name worklist to a fixpoint over fn/const/global edges. while let Some(name) = worklist.pop() { if !reachable.insert(name.clone()) { continue; } if is_candidate(&name) { if let Some(refs) = refs_of.get(&name) { worklist.extend(refs.iter().cloned()); } } } // Fire any method whose receiver type AND name are now both reachable. // Plan 174.1 [M-174.1-concrete-slice-recv-method-resolve]: a CONCRETE- // slice receiver's type name ("[]u8", "[]str", "[]int" — std/text.nv // join, std/sort.nv sum, std/runtime/string to_str family) can never // syntactically appear as a collected `Ident`/`Member` name, so the // type∧name intersection could NEVER fire for it — the body was dropped // in every executable (fn-main) build while the call site was kept → // implicit-decl CC-FAIL / P67 ICE. Anchor those methods on the method // NAME alone (over-keep, never over-prune — G0-conservative: they are a // handful of small std fns). let mut fired = false; for (i, (ty, name, refs)) in methods.iter().enumerate() { let ty_anchored = reachable.contains(ty) || ty.starts_with("[]"); if !live_method[i] && ty_anchored && reachable.contains(name) { live_method[i] = true; worklist.extend(refs.iter().cloned()); fired = true; } } if !fired { break; } } // Dead methods: those that never fired (type or name unreachable). let dead_method_keys: HashSet<(String, String)> = methods .iter() .enumerate() .filter(|(i, _)| !live_method[*i]) .map(|(_, (ty, name, _))| (ty.clone(), name.clone())) .collect(); DeadDecls { dead_fns: fn_candidates.difference(&reachable).cloned().collect(), dead_consts: const_candidates.difference(&reachable).cloned().collect(), dead_method_keys, } } // Plan 172.15 Ф.1: перечисление форм тела (`SretBodyForm::Leaf`/`ChainTo`) // снято — периметр `__sret` задаётся свойством, а не разбором формы записи. // См. `CEmitter::sret_fn_eligible`. pub struct CEmitter { out: String, /// File-scope handler impl function bodies (ctx structs + forward decls + bodies) deferred_impls: String, /// File-scope lambda forward declarations (static fn sig only). Flushed before fn definitions. lambda_forward_decls: String, /// Plan 36 followup: user-type forward decls (`typedef struct Nova_T Nova_T;`). /// Splice'ятся в `/*__USER_TYPE_FWD_DECLS__*/` ДО NovaOpt typedef'ов, /// чтобы `NovaOpt_Nova_T_p { Nova_T* value; }` не падал с /// `unknown type name 'Nova_T'`. Fills'ится pre-pass'ом в emit_module. user_type_fwd_decls: String, /// Plan 91.12 fix: value-record struct definitions (complete bodies, not /// just forward typedefs). Must appear BEFORE /*__MONO_TUPLE_TYPEDEFS__*/ /// because tuples may carry value-records by value (complete type required). /// [реестр 221.1 №139 Round 3]: no longer written to by /// `emit_value_record_type`/`emit_record_type` (see `pending_value_nodes` /// below) — kept only so the `/*__VALUE_RECORD_DEFS__*/` marker still has /// something to `.replace()` with (always empty now, splices to nothing). value_record_defs_buf: String, /// [реестр 221.1 №139 Round 3 — unified value-type topo-sort] One entry /// per value-type node awaiting placement: `(typedef_tag_name, /// field_c_types, rendered_text)`. Populated by `emit_value_record_type` /// (user value-records, `NovaValue_<name>`), `emit_record_type` (heap /// records, `Nova_<name>`), and `drain_generic_type_worklist` (Record- /// kind generic instances, value OR heap form). Consumed exactly once by /// `render_unified_value_types`, which topologically sorts by by-value /// field dependency (a field whose C-type — pointer suffix stripped — /// matches ANOTHER node's tag name here is an edge) and renders the /// result as ONE section spliced at `/*__GENERIC_TYPE_DEFS__*/` (the /// safely-late position both old markers' content used to converge /// toward under the round-1/2 hoists — see that fn's doc for the full /// rationale of why a single fixed two-marker order can never be /// correct here, only a real per-CU topological sort). Rendering itself /// (the actual struct-body TEXT) still goes through the ORIGINAL, /// untouched renderer functions — only the DECISION of when each node's /// already-rendered text is allowed to appear is unified. pending_value_nodes: Vec<(String, Vec<String>, String)>, /// File-scope lambda implementations (structs + function bodies). Flushed before fn definitions. lambda_impls: String, indent: usize, /// Depth of `unsafe { }` blocks currently being emitted. >0 = unsafe context. unsafe_depth: usize, tmp_counter: usize, /// Monotonic counter for handler literals — used to generate stable, predictable IDs handler_counter: usize, /// Plan 97.1 Ф.2 (D142): monotonic counter for protocol literals /// (`protocol Name { ... }` в expression-position). Stable IDs для /// synthetic ctx struct, free fn methods, vtable instance. protocol_lit_counter: usize, /// Monotonic counter for spawn expressions — stable IDs for pre-scan matching spawn_counter: usize, /// Plan 83.3 Ф.4.2: monotonic counter for `blocking { }` extracted bodies. /// Emit-side only (no pre-scan) — forward-decl emitted into /// `lambda_forward_decls` directly from `emit_blocking`. blocking_counter: usize, /// Plan 83.4.5.2 Ф.2: monotonic counter for orphan `detach { }` extracted /// bodies. Используется аналогично spawn_counter но префиксует /// orphan-fiber entry function names как _nova_detach_N. detach_counter: usize, /// Monotonic counter for supervised scopes — used to name local NovaFiberQueue variables. supervised_counter: usize, /// When inside a `supervised { }` scope, holds the C name of the local NovaFiberQueue. /// `spawn` inside such a scope goes into the queue via nova_fiber_spawn_into. /// Outside a scope, this is None and `spawn` uses the eager-blocking nova_fiber_run path. current_scope_queue: Option<String>, /// When emitting a spawn-entry body, captures are accessed via `(*_c->name)`. /// We DON'T use `#define` macros for this — they leak into nested supervised /// scopes where `name` could appear as a struct field-declarator and get /// rewritten by the preprocessor. Instead, ExprKind::Ident checks this set /// and rewrites references inline. current_spawn_captures: Option<HashSet<String>>, /// Subset of current_spawn_captures that are captured by value (T field, not T*). /// These names rewrite to `_c->name`, not `(*_c->name)`. current_spawn_capture_by_value: Option<HashSet<String>>, /// Maps variable name → C type string (best-effort) var_types: HashMap<String, String>, /// [M-property-testing-rot] (Plan 172.13 батч 3): declared Nova-level /// `TypeRef`s of the CURRENTLY-EMITTED monomorphized fn's params /// (set/restored by `emit_monomorphized_fn`). A NESTED generic call inside /// the mono body that forwards such a param (`property[T]` body calling /// `property_with(gen, body, cfg)`) cannot infer the callee's type args /// from C types alone — a protocol-typed param (`gen Generator[T]`) erases /// to `void*`. `resolve_mono_type_args` (Source 2f) unifies the callee's /// declared param TypeRefs against these caller-declared TypeRefs, lowering /// leaves through `current_type_subst`. current_fn_param_typerefs: HashMap<String, crate::ast::TypeRef>, /// [M-property-testing-rot]: recursion guard for /// `infer_protocol_structural_binding` — protocol method signatures may /// mention OTHER protocols (or themselves: `Iter[T]`-returning methods on /// `Iter[T]`), so the structural walk must be depth-bounded or it recurses /// forever (protocol → method position → protocol → ...). proto_unify_depth: std::cell::Cell<u8>, /// [M-consume-rebind-nested-block-shadow] (Plan 172.13): spans of /// `consume x = expr` `Stmt::Let`s that `alpha_rename` determined rebind a /// binding declared in an ENCLOSING (not current) scope — copied verbatim /// from `Module::consume_reuse_spans` at `emit_module` entry. `Stmt::Let` /// consults this by span to emit a plain reassignment (reusing the /// existing, still-live C variable) instead of a fresh block-scoped C /// declaration. consume_reuse_spans: HashSet<Span>, /// Plan 172.5 (D326 R5): names of the current function's `ro ref`/`mut ref` /// parameters. Such a parameter is lowered to a C pointer (`T*`); every /// value-position use of its name in the body is auto-dereferenced /// (`name` → `(*name)`), and assignment through it (`name = v`) becomes /// `(*name) = v`, so writes land in the caller's storage. Populated at fn /// body emission and restored afterwards (methods can nest via closures). ref_params: std::collections::HashSet<String>, /// Plan 248 (wave 3, third mega-CU regression, /// [M-detach-capture-mut-param-not-in-var-mutable]): names of the current /// function's genuinely `mut x T` (non-`consume`) parameters — populated /// in the SAME loop as `ref_params` (fn body emission entry), scoped/ /// restored the same way. Deliberately SEPARATE from `var_mutable` /// (which tracks only `let mut` locals, never params): `var_mutable` /// drives several OTHER capture/mutability decisions (handler-literal /// capture-by-value-to-avoid-a-dangling-pointer, `is_place_mutable`, /// overload mode tiebreak, …) that deliberately treat a captured PARAM /// as immutable/by-value for reasons unrelated to `detach{}`'s box /// mechanism — broadening `var_mutable` itself to cover params was tried /// and reverted (risked flipping those unrelated decisions too). Only /// `emit_detach`'s (and, if the same class of bug is found there, /// `emit_spawn`'s) own capture classification consults this set, to /// correctly recognize a captured `mut` PARAM as a live, mutating /// capture rather than a read-only value snapshot. mut_param_names: std::collections::HashSet<String>, /// Plan 48 method-param mono (Plan 63 followup, 2026-05-17): per-name override /// for closure param types, consulted by `infer_expr_c_type` Ident arm BEFORE /// `var_types`. Needed because `infer_mono_method_ret_with_args` takes `&self` /// (it's called from `infer_expr_c_type(&self)`), so we cannot mutate /// `var_types` directly. The caller fills/clears the RefCell around its /// `infer_expr_c_type(closure_body)` call. closure_param_type_overrides: RefCell<HashMap<String, String>>, /// Plan 48 method-param mono (Plan 63 followup, 2026-05-17): per-name override /// for type-param substitution (e.g. `T → nova_int`), consulted by /// `type_ref_to_c` BEFORE `current_type_subst`. Same `&self` reason as above. type_subst_overrides: RefCell<HashMap<String, String>>, /// Plan 62.D bis-1 (2026-05-18): per-name override for match-arm pattern /// bindings, consulted by `infer_expr_c_type` Ident arm BEFORE `var_types`. /// Needed because `infer_expr_c_type(Match)` recurses into arm bodies in /// `&self` context — pattern bindings (e.g. `Some(r) => r`) can't be /// installed into `var_types`, and stale entries from prior bodies (e.g. /// `let r = Range...` in another test file) would leak into the lookup, /// returning wrong type. The Match path installs/restores the bindings via /// this RefCell around the body-inference recursion. pattern_binding_overrides: RefCell<HashMap<String, String>>, /// Names of variables declared as `let mut` (mutable) — used by spawn-capture /// to decide between copy-by-value (immutable scalar) and capture-by-pointer. var_mutable: HashSet<String>, /// Plan 100.8 (D166) C-codegen fix: variables that were pre-declared /// (hoisted) before a setjmp handler in `enter_defer_scope` because they /// are referenced in an errdefer/defer body. When `emit_stmt` encounters the /// corresponding `Stmt::Let`, it emits only the assignment (no type), since /// the declaration was already emitted above the setjmp. The name is removed /// from this set on first use so sibling scopes with the same name work normally. hoisted_let_vars: HashSet<String>, /// Plan 72 P0 (E7201): variables whose declared Nova type is a protocol type /// (erased to void*). Maps var_name → protocol type name for diagnostics. /// Used to detect method calls on erased protocol values at compile time. protocol_vars: HashMap<String, String>, /// Plan 72 P1-C: variables declared as Result[T, E]. Maps var_name → /// (ok_c_type, err_c_type) so that .unwrap() / .ok() / .err() etc. /// return the correct T or E type instead of the hardcoded nova_int. result_type_params: HashMap<String, (String, String)>, /// Plan 72 P2-A: functions whose declared return type is `Result[T, E]`. /// Maps a call-site key (`fn_name` for free fns, `Type.method` for static /// methods) → (ok_c_type, err_c_type). Lets `let r = f(...)` (call RHS, no /// annotation) populate `result_type_params[r]` instead of falling back to /// the hardcoded `(nova_int, nova_str)`. fn_result_type_params: HashMap<String, (String, String)>, /// Plan 72 P3-B: free functions with protocol-typed parameters. Maps /// `fn_name` → per-parameter `Some((proto, type_args))` (protocol param, /// lowered to `NovaBox_*`) or `None`. Lets a call site box concrete /// arguments passed where a protocol parameter is expected. fn_protocol_params: HashMap<String, Vec<Option<(String, Vec<String>)>>>, /// Plan 174.3 (D53): callees with `any`-typed parameters. Maps the callee key /// (same scheme as `fn_protocol_params`: `fn_name` / `Type.method`) → per-param /// `true` when the param type is the `any` top-type. Lets a call site box a /// concrete argument (implicit upcast `T → any`) into a `NovaAny` before the /// call, exactly like the protocol-param pre-box hook. fn_any_params: HashMap<String, Vec<bool>>, /// Plan 72 P3-B: protocol method registry. /// Maps protocol_name → (type_param_names, method_signatures). /// Populated when a Protocol TypeDecl is processed. protocol_method_registry: HashMap<String, (Vec<String>, Vec<EffectMethod>)>, /// [fix M-user-type-name-collides-with-stdlib-type-in-c-symbol, реестр 221.1 /// №154] `Protocol name → declaring file_id`. `emit_protocol_box_typedef` /// lowers a protocol's OWN method param/return types (building the /// `NovaVtable_<Proto>` struct) OUTSIDE the `current_emit_file_id`-setting /// scope that guards the normal type-decl emission loop (that loop only /// wraps `emit_type_decl`, while the protocol vtable pre-emit runs earlier, /// at module top). Without a defining file to resolve FROM, `ref_type_base` /// falls through all its resolution branches for a BARE colliding type name /// referenced inside the protocol's own file (e.g. std's `Fmt.sign() -> Sign` /// referencing `runtime.fmt_buf`'s OWN `Sign`, colliding with a user `Sign` /// elsewhere in the CU) and returns the bare, unqualified name — producing /// `Nova_Sign*` in the vtable while the ACTUAL struct was correctly /// qualified to `Nova_runtime_fmt_buf_Sign` by `def_type_base` (which DOES /// know its own file). CC-FAIL `unknown type name 'Nova_Sign'`. Populated at /// both `protocol_method_registry` insert sites (non-generic + generic) from /// the protocol `TypeDecl`'s own `span.file_id`; consulted by /// `emit_protocol_box_typedef` to temporarily set `current_emit_file_id` /// while lowering the protocol's method signatures — mirrors the existing /// `any_type_file_collision()`-gated save/restore idiom used elsewhere /// (e.g. `emit_monomorphized_method_scoped_inner`). Empty/no-op for any CU /// without a collision (byte-identical). protocol_decl_file: HashMap<String, crate::diag::FileId>, /// Plan 72 P3-B: companion vtable C variable for each protocol-typed var. /// Maps var_name → vtable_c_var_name (e.g. "x" → "__vt_x"). /// Present only when a concrete type was assigned at declaration. protocol_var_vtable: HashMap<String, String>, /// Plan 72 P3-B: emitted vtable struct type names to avoid duplication. emitted_vtable_types: HashSet<String>, /// Plan 72 P3-B: emitted vtable instances — (vtable_struct, concrete_mangled). emitted_vtable_instances: HashSet<(String, String)>, /// Maps struct name → field name → C type record_schemas: HashMap<String, HashMap<String, String>>, /// D215 amend (Plan 91.8b follow-up): named tuple field defaults. /// Maps type_name → per-field Vec<Option<Expr>> in declaration order. /// Used at constructor call sites to fill omitted fields with their defaults. named_tuple_field_defaults: HashMap<String, Vec<(String, Option<crate::ast::Expr>)>>, /// Plan 124.8 V2 (D226): names of value-records — `type X value { ... }`. /// emit_record_lit checks this set чтобы emit stack-init code path /// instead of heap-alloc (Nova_X*) path. is_value_type recognizes /// `NovaValue_` prefix; this set isolates user-declared value-records /// from runtime types. value_record_names: HashSet<String>, /// Maps sum type name → variant name → field types (positional) sum_schemas: HashMap<String, HashMap<String, Vec<String>>>, /// [M-sync-crossmodule-samename-type-collision] Collision-aware nominal-type /// mangling (D381). Set of user-type SIMPLE names declared in ≥2 DISTINCT /// modules within this CU. ONLY these names get module-qualified C bases /// (`Nova_<modpath>_<Name>` instead of `Nova_<Name>`); every non-colliding /// name stays byte-identical. Empty for any CU without a cross-module /// same-name collision → all qualification code below is a no-op (byte- /// identical guarantee). Built in `emit_module` from `peer_files`. colliding_type_names: HashSet<String>, /// `[M-178-server-typed-body]`: names of plain (non-receiver) free fns /// declared in ≥2 DISTINCT modules of this CU that need file-discriminated /// mangling (see the `colliding_fn_names` computation in `emit_module`, /// mirrors `colliding_type_names` above). Consulted by the D84 free-fn /// overload-registration pass to EXCLUDE these names from the shared /// `method_overloads` sentinel-key registry (same reasoning as the /// existing `f.file_private` exclusion just above it: registering them /// there mixes UNRELATED same-name declarations from different modules /// under one dispatch key). Empty for any CU without a cross-module /// free-fn collision → byte-identical for everything else. colliding_fn_names: HashSet<String>, /// [M-sync-crossmodule…] `FileId → module_name` for every peer file. Gives a /// TypeDecl its DEFINING module (`t.span.file_id → module`) at emission, and /// the current-file context for reference-site qualification. Built from /// `peer_files` in `emit_module`. emit_file_module: HashMap<crate::diag::FileId, Vec<String>>, /// [M-sync-crossmodule…] Per-file resolution of a COLLIDING simple name to /// its defining module AS VISIBLE in that file (`(file_id, name) → module`). /// A file sees a bare colliding name either because its own module declares /// it or because it selectively imports it from exactly one module (checker /// invariant: a bare type name is unambiguous within a file). Only populated /// for names in `colliding_type_names`. Built in `emit_module`. file_type_module: HashMap<(crate::diag::FileId, String), Vec<String>>, /// [M-198-f4c-1-privfile-type-not-discriminated]: file-discriminated C base /// for a `priv(file) type` whose simple name collides with ANOTHER /// declaration (any visibility) in a peer file of the SAME folder-module — /// D381 above only qualifies collisions across DISTINCT modules; two peer /// files sharing one `module` declaration (folder-module, D281 Rule C) were /// unhandled, so both `priv(file) type Rect` decls emitted the SAME bare /// `Nova_Rect` C struct -> C "redefinition" (mirrors the analogous /// `private_const_c_names` file-keyed map for `priv(file) const`, Plan /// 170/D307). Keyed `(declaring file_id, source name) -> mangled base` /// (unprefixed, e.g. `Rect_f7` -- callers still add `Nova_`/`NOVA_TAG_`/etc. /// themselves). Consulted FIRST by `def_type_base`/`ref_type_base`, before /// the D381 module check -- empty for every non-colliding name, so byte- /// identical outside this specific collision. file_priv_type_c_names: HashMap<(crate::diag::FileId, String), String>, /// [M-172.1-self-ref-slice-variant-erasure] sum types currently mid-emission /// in `emit_sum_type`. A self-referential variant payload (`Node([]Self)`, /// e.g. json `JsonValue.Array([]JsonValue)`) lowers its slice element via /// `type_ref_to_c` BEFORE the type is registered in `sum_schemas`, so /// `debt_is_generic_stub_c` would mistake the concrete `Nova_<Self>*` element for an /// unresolved generic-param stub and erase the Vec element to `nova_int` (→ /// `Nova_Vec____nova_int*` payload vs `Nova_Vec____Nova_Self_p*` signature → /// CC-FAIL). A type being defined is concrete by construction; this guards it. being_defined_sum_types: HashSet<String>, /// [M-option-self-recursive-record-mono] (Plan 186, recursive-mono): mirror of /// `being_defined_sum_types`, but for a plain RECORD currently having its own /// fields lowered (`emit_record_type`). `record_schemas` is only populated /// AFTER the field loop completes, so a self-referential field /// (`type Node { value int; next Option[Node] }`) sees "Node" as unregistered /// while its OWN `next` field is being resolved — `rt_named_is_stub` then /// misclassifies it as an unresolved generic-param stub and `Option[Node]` /// erases to `NovaOpt_nova_int` (the int-boxed erased Option) instead of the /// concrete `NovaOpt_Nova_Node_p`, producing a field-type mismatch CC-FAIL at /// every write site. A type being defined is concrete by construction; this /// guards it — consulted UNCONDITIONALLY (not gated by the `full` parameter /// like the sum-type guard above), because the exact call site that hits this /// bug (`Option`'s inner-type check in `resolved_named_to_c`) passes /// `full=false`. being_defined_record_types: HashSet<String>, /// Maps effect name → method name → (param_types, return_type) effect_schemas: HashMap<String, HashMap<String, (Vec<String>, String)>>, /// Maps method name → (type_name, is_instance) for user-defined methods. /// Used at call sites to resolve `obj.method(args)` → `Nova_T_method_m(obj, args)`. method_receivers: HashMap<String, (String, bool)>, /// Реестр 221.1 №581/№577: single canonical name for a STATIC array-ext /// own-generic blanket method (`fn[T Bound] []T.method() -> R`, e.g. /// `[]T.reflect()`). Maps method name → the C base ident the DECLARATION /// itself emits under (`Self::receiver_type_c_ident(recv.type_name)`, /// e.g. `"NovaArray_nova_int"`). /// Реестр 221.1 №592: this base name is now used ONLY as a fallback for /// the residual nested-generic call shape (`T.method()` inside another /// type's own mono'd body, e.g. `Option[T].reflect()`'s `T.reflect()` /// when `T` itself resolves to an array/`Vec____` mono — the ONE call /// site still left routing here, see its own doc, ~46617 as of this /// writing). The DIRECT `[]<concrete>.method()` call site no longer /// consults this map — it now monomorphizes per real element via /// `array_ext_static_generic_fn` + `register_mono_method_instance` /// instead of reusing this single erased (`T` defaulted to `nova_int`) /// body for every element. Populated ONLY here, at method registration /// (~8218). array_ext_static_c_base: HashMap<String, String>, /// Реестр 221.1 №592: FULL declaration of a STATIC array-ext own-generic /// blanket method (same shape as `array_ext_static_c_base` above — method /// name → the one `FnDecl` — there is exactly one blanket per method name /// in this family). Consulted by the `[]<concrete>.method()` call site /// (`Path(["__array", elem])`, D38) to monomorphize a FRESH body per /// concrete element instead of the old single erased (`nova_int`-named) /// body every element silently shared. Populated alongside /// `array_ext_static_c_base` at method registration (~8218). array_ext_static_generic_fn: HashMap<String, crate::ast::FnDecl>, /// Plan 06 Ф.1: multi-key registry — `(type_name, method_name) → is_instance`. /// `method_receivers` single-key страдает от last-wins (если два типа имеют /// одноимённый method, второй вытесняет первый). `all_methods` хранит все. /// Используется в for-in для Iter[T] dispatch: проверяем /// `all_methods.contains((iter_struct, "next"))`. all_methods: HashSet<(String, String)>, /// Типы-ресиверы, у которых есть ХОТЬ ОДИН метод. `all_methods` /// ключуется парой, поэтому вопрос «есть ли у типа методы вообще» /// отвечался перебором всего множества — на каждом сайте вызова /// (реестр 221.1 №522). all_method_recv_types: HashSet<String>, /// Plan 11 Ф.1: multi-overload registry. Key = `(type_name, method_name)`, /// value = list of overloaded signatures (param C types, is_instance, /// is_external). Используется на call-site для resolve по arg-types /// (Ф.2). Single-key `method_receivers` остаётся для backward compat — /// single-overload пути ссылаются на него. // Plan 145.2: BTreeMap (не HashMap) — детерминированная итерация. Эта мапа // итерируется при эмиссии (embed-proxies ~9891, registration ~3123) и влияет // на ПОРЯДОК генерируемого C + порядок enqueue в mono-worklist. HashMap-порядок // (рандом per-run) делал эмиссию недетерминированной и обнажал латентные // order-зависимые баги prelude (init-order OOB, var_boxed leak). Ключ Ord. method_overloads: BTreeMap<(String, String), Vec<MethodSig>>, /// Типы, у которых есть ХОТЬ ОДНА перегрузка. `method_overloads` /// ключуется парой (тип, метод), поэтому вопрос «есть ли у типа /// перегрузки вообще» отвечался перебором ВСЕХ ключей — на шести /// сайтах в горячем пути резолва имени (реестр 221.1 №522). /// Наполняется ТОЛЬКО через `register_method_overload`, чтобы не /// заводить инвариант «не забудь обновить оба». method_overload_types: HashSet<String>, /// Plan 125 followup `[M-125-method-call-never-detection]`: set of /// (receiver_c_type, method_name) pairs OR (`"<free>"`, fn_name) where /// the declared AST return type is `never`. Populated during method/fn /// registration (preamble pre-pass). Queried by `expr_diverges_125` для /// trailing-divergence detection в Plan 125 result-type inference. /// /// Conservative: только AST-level `never` (TypeRef::Named{path:["never"]}). /// НЕ trait'им -> nova_int placeholder для real-int returns. Соответствует /// type-checker-level Ty::Never (Plan 76 bottom-type). never_returning_methods: HashSet<(String, String)>, /// Plan 12: builtins.nv-driven external dispatch registry. /// Single source of truth для StringBuilder/WriteBuffer/ReadBuffer/ /// str.from(char) — `std/runtime/builtins.nv`. Codegen читает AST /// и применяет mangling/type-mapping автоматически (вместо hard-coded /// таблиц). Загружается один раз в `CEmitter::new()`. pub external_registry: super::external_registry::ExternalRegistry, /// Plan 91.12 Ф.-1 (D282): function names declared with `extern "C" fn`. /// These resolve to their literal C name (no `nova_fn_` prefix). /// Populated during emit_module from the user's module items. c_literal_extern_fns: HashSet<String>, /// D39 / Plan 11 Ф.9: embed-поля per record-type. /// Key = wrapper type name; value = list of (field_name, embedded_type_name, /// is_anonymous). Используется для auto-proxy generation после AST-walk fn-items. // Plan 145.2: BTreeMap — детерминированная итерация (keys() при registration // ~3118 и эмиссии embed-proxies ~9886/9891). Ключ Ord. embed_fields: BTreeMap<String, Vec<(String, String, bool)>>, /// Plan 06 Ф.3: для каждого типа Coll с методом `mut @iter() -> IterT` /// запоминаем имя IterT. Используется в for-in: при `for x in coll` /// (где `coll: Coll`) вставляем implicit `.iter()` и emit'им loop /// против IterT. iter_returns: HashMap<String, String>, /// target_type → list of source_types for which `target.from(src V)` is /// explicitly defined. [D73/D77 retraction 2026-07-06]: the `.into()` /// auto-derive consumer of this registry was removed; it survives only /// for the unrelated CancelToken cross-type cascade compile-time check /// (`.cancelled_by`, naming-convention precondition, not a protocol). from_targets: HashMap<String, Vec<String>>, /// Maps tuple variable name → per-element C types. /// Used at field access `pair.0` to cast back to the original element type when needed. tuple_element_types: HashMap<String, Vec<String>>, /// Maps (type_name, variant_name, field_name) key → C type for record variants. /// Key format: "TypeName::VariantName::field_name" record_variant_field_types: HashMap<String, String>, /// Maps "TypeName::VariantName" → ordered list of field names (insertion order). record_variant_field_order: HashMap<String, Vec<String>>, /// [M-172.1-option-eq-record-structural]: maps plain-record type name → /// ordered list of field names (declaration order). The `record_schemas` /// value is a `HashMap` (unordered) — structural eq recursion needs a stable /// field order for deterministic codegen. Analog of `record_variant_field_order` /// for non-variant records. record_field_order: HashMap<String, Vec<String>>, /// [M-172.1-option-container-eq-structural]: mono'd container C-types /// (`Nova_Vec____<elem>`) whose `@equal` mono needs instantiating because a /// NESTED comparison (Option[Vec]/sum-Vec-field) emits a call to it. Mono /// instantiation is `&mut self`; `emit_field_eq`/`register_novaopt_decl` are /// `&self`, so they record the request here (RefCell) and a `&mut` post-pass /// (the mono_worklist drain) instantiates each. `container_eq_requested` /// dedups (the drain is monotone → terminates). Vec-only for now: HashMap has /// no `@equal` Nova-body yet. pending_container_eq_monos: std::cell::RefCell<Vec<String>>, container_eq_requested: std::cell::RefCell<HashSet<String>>, /// Return type of the currently-emitting function, used for match result type inference. current_fn_return_ty: Option<String>, /// [M-generic-method-self-recursive-return] (Plan 186, recursive-mono): name /// of the method CURRENTLY being emitted by `emit_monomorphized_method` /// (`None` outside that path — free fns/erased bodies don't need it, see /// below). A generic method that calls ITSELF recursively on a value of its /// own receiver type (`fn LinkedList[T] @map[U](f) -> LinkedList[U] { ... /// t.map(f) ... }`) has a return type (`LinkedList[U]`) that mentions an /// in-scope type-param — the checker's `infer_method_call_channel_type` /// deliberately does NOT channel such a return (would need erased-body /// `current_type_subst`, not available at check time), and the LEGACY /// `fn_ret_<recv>_<method>` registry has no entry either (methods are /// registered under the GENERIC template name, and mono emission of THIS /// exact call happens WHILE the entry is being built, not before) — /// `infer_expr_c_type` used to hard-panic (`[P67-LEGACY]`). Paired with /// `current_receiver_type`/`current_fn_return_ty`: a call matching BOTH the /// current method's own name AND its own (mono'd) receiver type is /// self-recursion by construction, so the CURRENT function's own return /// C-type (already known) is the exact, sound answer — see the fallback /// site in `infer_expr_c_type`'s `Call`/`Member` arm. current_fn_name: Option<String>, /// Plan 72 P3-B return: when the currently-emitting function declares a /// protocol return type (e.g. `-> Iter[int]`), holds `(proto_name, /// [concrete C type args])`. Return values are wrapped into a `NovaBox_*` /// fat pointer. `None` for ordinary functions. current_fn_returns_protocol: Option<(String, Vec<String>)>, /// Plan 174.3 (D53): `true` when the currently-emitting function declares an /// `any` return type — concrete return values are boxed (implicit upcast /// `T → any`) via `wrap_any_return`, mirroring the protocol-return wrap. current_fn_returns_any: bool, /// Plan 33.3 Ф.9.2 (D24): record-invariants per-type. Map struct_name → /// list of (invariant-expr, span). Используется emit_record_lit'ом для /// wrap'а конструкции в runtime-check (`if (!Inv(tmp)) violation; tmp`). /// Заполняется в emit_module pre-pass. /// Plan 33.3 Ф.9.2 (D24): record-type invariant clauses, keyed by struct /// name. Each entry is `(expr, span, message, message_expr, debug_only)` — /// `message` (Plan 140.1 Ф.2, D24 amend) is the optional user message for /// the location-first violation diagnostic (`<file>:<line>: invariant /// failed: <msg> (<expr>)`); `debug_only` (Plan 194 A2.2, D421 §3) marks a /// `#debug invariant` clause — erased outside `checked` mode. record_invariants: HashMap<String, Vec<(Expr, Span, Option<String>, Option<Expr>, bool)>>, /// №466 Ф.1: сколько `#debug`-контрактов стёр режим сборки. Cell, потому /// что `mode_erases_debug` вызывается из &self-контекстов. Сообщается один /// раз в конце эмиссии (`report_debug_erasure`) — молчаливое стирание /// защиты в release было ложным обещанием безопасности. debug_contracts_erased: std::cell::Cell<usize>, /// Plan 33.1 Ф.4 (D24): если установлено — функция имеет ensures-контракты, /// и все `Stmt::Return X` подменяются на `{ _nova_result = X; goto <label>; }`. /// Trailing block-expression также. После label эмитятся ensures-checks /// и финальный return. contracts_post_label: Option<String>, /// Plan 33.3 Ф.9.1+Ф.9.7: имена ghost-vars в scope текущей fn. /// Используется в Stmt::AssertStatic / Stmt::Assume / inject'нутых /// loop invariants — если expression читает ghost-var, runtime check /// skip'ается (ghost эрейзится в codegen, не доступен в C-output). /// Type-check уже catches non-ghost reads ghost (Ф.9.7); это просто /// allow spec-position reads silently не падать на C-level. ghost_vars: std::collections::HashSet<String>, /// Plan 33.3 Ф.9.9 (D24): proven контракты (fn_name, span.start). /// Заполняется через set_proven_contracts из VerificationPipeline result. /// Codegen skip emit для runtime check'ов помеченных как proven — /// true zero-cost даже в debug. proven_contracts: std::collections::HashSet<(String, usize)>, /// Plan 140.2 Part B (D257 / B.4): proven Index-сайты `v[idx]`/`v[a..b]` /// (по span.start), доказанные из LOOP/CODE. Codegen элидит inline /// bounds-check ВСЕГДА (unconditional — не через contracts-режим). proven_index_sites: std::collections::HashSet<usize>, /// Plan 140.2 followup §2: Index-сайты, доказанные ТОЛЬКО с fn-`requires` /// (cross-fn). Элидируются ТОЛЬКО при включённых контрактах — если /// requires этой fn не enforced, элизия unsound (Plan 194 A2.1: legacy /// build-level `--contracts=off` retired; Plan 194 A4: per-fn/module /// `#unchecked` opt-out тоже retired — requires теперь ВСЕГДА enforced). proven_index_sites_contract: std::collections::HashSet<usize>, /// Plan 172.1 U.4.1: per-Expr resolved-type annotations (ExprId → ResolvedType) /// from the semantic pass — codegen reads them (equivalence-checked in debug) /// instead of re-deriving (`infer_expr_c_type`, §0/§1). Part 2: literals. /// (Read only by the debug equivalence-assert until U.4.2 makes it the /// authoritative type source; release-`dead_code` allowed in the interim.) #[cfg_attr(not(debug_assertions), allow(dead_code))] resolved_types: std::collections::HashMap<crate::ast::ExprId, crate::types::ResolvedType>, pattern_variant_types: std::collections::HashMap<Span, String>, // №279: pattern span → sum name /// Plan 172.1 U.4.3: the resolved-callee channel (call-site `ExprId` → chosen /// callee `FnDecl` declaration `Span`) the checker populated in `f1_check_call` /// (`ModuleEnv.resolved_callees`, U.3.4-prep). The `FnDecl.span` is the stable /// cross-layer callee IDENTITY both layers hold (§7.7) — codegen looks up ITS OWN /// view of that callee (`fn_ret_by_span`) and lowers it, instead of re-resolving the /// overload (§0). Stage (a): free-fn single-overload — read by the debug /// equivalence-assert in `infer_expr_c_type` (proves channel == legacy over the /// corpus) before later stages flip codegen to read it authoritatively. /// [M-172.1-U4-typedir-substrate] #[cfg_attr(not(debug_assertions), allow(dead_code))] resolved_callees: std::collections::HashMap<crate::ast::ExprId, crate::diag::Span>, /// Plan 196.5 Stage-A: the subst-value channel (call-site `ExprId` → ordered /// `(generic-param name → concrete ResolvedType)`) the checker populated /// (`ModuleEnv.node_substs`). Mirrors `resolved_callees`'s plumbing. Stage-B1 /// (`resolve_mono_type_args_ch`, ~19315) reads it unconditionally (propose-then-verify /// against the legacy re-inference, `NOVA_NODE_SUBSTS_TRACE` hit/fallback tally) — no /// longer dead in release. [M-196.5-node-substs] node_substs: std::collections::HashMap<crate::ast::ExprId, Vec<(String, crate::types::ResolvedType)>>, /// Plan 172.1 U.4.3 (stages a+b+c1): codegen's OWN view of a callee's return C-type, /// keyed by the declaration `Span`. Built from the same `ret` codegen registers as /// `fn_ret_<name>` / `fn_ret_<recv>_<name>` (the CONCRETE non-mono forward-decl pass) /// for free fns [a], STATIC methods [b] AND INSTANCE methods [c1] — every callee that /// reaches that path is non-generic concrete (generic / receiver-generic / own-generic- /// param callees returned early into the mono pipeline and are NOT indexed, so the mono /// lowering stays codegen's job — legitimate, stage d). It is the codegen-native /// "CodegenView" of the callee the `resolved_callees` channel points at — no /// `SigRegistry` needed in codegen (U.2.4 not merged; mangling/return are codegen /// lowering, §0/§7.7). Used by the U.4.3 equivalence-assert. #[cfg_attr(not(debug_assertions), allow(dead_code))] fn_ret_by_span: std::collections::HashMap<crate::diag::Span, String>, /// Plan 140.4 ([M-opt-elide-proven-overflow-checks]): proven `int` `+`/`-`/`*` /// сайты (по span.start), чей результат доказан в диапазоне i64 из LOOP/CODE. /// Codegen элидит `nova_int_checked_*` ВСЕГДА (unconditional — не через /// contracts-режим/`#unchecked`). proven_overflow_sites: std::collections::HashSet<usize>, /// Plan 140.4: `int`-арифм. сайты, доказанные ТОЛЬКО с fn-`requires`. /// Элидируются ТОЛЬКО при включённых контрактах — если requires этой fn /// не enforced, элизия была бы unsound (Plan 194 A2.1: legacy /// build-level `--contracts=off` retired; Plan 194 A4: per-fn/module /// `#unchecked` opt-out тоже retired — requires теперь ВСЕГДА enforced, /// т.е. `contracts_elided_here()` константно `false`). proven_overflow_sites_contract: std::collections::HashSet<usize>, /// Plan 194 A2.1 (замена Plan 140 Ф.2 / D24 amend `contracts_off: bool`): /// build-policy режим `--contracts=checked|optimized|verified`. Legacy /// `off` (глобальный unconditional bypass) убран. Plan 194 A4: `#unchecked` /// per-fn/module opt-out РЕТРАКТИРОВАН — предикаты элизии /// (`contracts_elided_for` / `invariants_elided_here`) теперь читают /// ТОЛЬКО доказательство (`proven_contracts`) / `#debug`-эрозию /// (`mode_erases_debug`); `mode` сам по себе всё ещё не добавляет /// собственную ветку элизии для requires/ensures/invariant (задел под /// будущую Z3-driven дифференциацию `optimized`/`verified`, атомы A3+). contracts_mode: crate::ast::ContractsMode, /// Maps array variable name → actual element C type (e.g. "Nova_Box*"). /// The array always uses nova_int storage but elements may be pointers to records. array_element_types: HashMap<String, String>, /// Maps Option variable name → inner boxed type when value is a heap-boxed struct pointer. /// E.g. "outer" → "NovaOpt_nova_int*" when outer = Some(Some(42)). option_inner_types: HashMap<String, String>, /// Set during emit_call when boxing a struct for nova_make_Option_Some. /// Consumed by next Stmt::Let to annotate the bound variable's inner type. pending_option_inner_type: Option<String>, /// Plan 63 Fix F [M-result-erased-no-mono]: Result Ok-payload boxed type tracking. /// Maps Result variable name → boxed inner C type (e.g. "_NovaTuple_2_8_nova_str_8_nova_int*" /// для Result[(str, int), E]). Used at destructure для proper unboxing tuple/struct args. result_ok_inner_types: HashMap<String, String>, /// Set during emit_call when boxing struct/tuple для nova_make_Result_Ok. /// Consumed by next Stmt::Let. pending_result_ok_inner_type: Option<String>, /// Plan 63 Fix F+ [M-result-erased-no-mono] production-grade: per-fn registry /// — fn_name → boxed Ok inner type (e.g. "_NovaTuple_2_8_nova_str_8_nova_int*"). /// Populated при emit_fn для fn'ов с return type Result[Tuple(concrete), E]. /// Lookup'ится в helper `try_get_result_ok_inner_type_for_expr` чтобы: /// (a) propagate через function-call returns (let r = parse_kv(...)) — /// Fix F's pending mechanism не работает потому что boxing happens /// внутри callee scope, не виден caller'у; /// (b) inline `match parse_kv(...) { Ok((k, v)) => ... }` — no let-binding, /// но scr_tmp получает payload type через helper. fn_result_ok_inner_types: HashMap<String, String>, /// Set of array variable names that store boxed nova_str* (as nova_int). /// Index access on these arrays must dereference: *(nova_str*)(arr->data[i]). str_box_arrays: HashSet<String>, /// C type of the current method receiver (e.g. "Nova_Box"), for resolving `Self`. current_receiver_type: Option<String>, /// Plan 172.12 A2b — structural `ResolvedType` twin of `current_receiver_type`, /// kept in lock-step with it (`sync_receiver_rt` after every mutation). The /// mono receiver instance is still string-native at its producers, so the value /// is the `ResolvedType::Raw` transitional debt of the decorated C-key (verbatim, /// no parse). The `TypeParam` printer arm reads THIS carrier through the marked /// receiver-debt helpers (`debt_receiver_typeparam`/`debt_receiver_erased`) instead /// of re-parsing the C-string in the printer body — moving every `Nova_`/`____` /// receiver-key decode out of `resolved_type_to_c`. Byte-identical to the pre-A2b /// inline parse (every value is `Raw(current_receiver_type)`); goes structurally /// alive when the instance producers become RT-native, and dies with the string /// carrier (A4). current_receiver_rt: Option<crate::types::ResolvedType>, /// 172.4 Ф.3 (блокер-1, 2026-07-04): return-позиция `-> @` value-record /// метода — SelfAccess эмитится как `nova_self` (ptr), не `(*nova_self)`. in_recv_ptr_return_position: std::cell::Cell<bool>, /// Plan 135 Ф.2: whether the currently-emitting method has a `mut` receiver /// (`fn Type mut @method`). Used at call-sites to tiebreak overloads when /// the receiver is `SelfAccess` (i.e. `@other_method()` inside the body). current_receiver_is_mut: bool, /// [M-static-selfreturn-value-mangle-conflict] (Plan 172.13): whether the /// currently-emitting fn is a STATIC namespace fn (`fn Type.method(...)`, /// `ReceiverKind::Static` — no actual `self`/`@` VALUE) as opposed to an /// INSTANCE method (`fn Type @method(...)`). Consulted ONLY by the `Self` /// arm of `resolved_named_to_c`: a static fn's `-> Self` denotes a FRESH /// value being constructed (lower like an ordinary reference to the type /// — value-form for named-tuple/value-record), whereas an instance fn's /// `-> Self` (fluent `-> @`) denotes the EXISTING receiver (pointer-form, /// unchanged — `receiver_c_type`). Set only at the two PRIMARY forward- /// decl/definition sites (mirrors `current_receiver_is_mut`); defaults to /// `false` (existing pointer-form behavior) everywhere else so mono/ /// generic-instance paths this doesn't touch stay byte-identical. current_receiver_is_static: bool, /// Expected struct type для anonymous record literal `=> { ... }` — /// устанавливается при эмите function body, когда нужно использовать /// declared return type как target для anonymous record (D55). expected_record_type: Option<String>, /// [M-178-variant-ctor-target-sum] (D381 gap-close): bare NOVA-level sum-type /// name the current expr subtree is expected to construct — set from a /// FIELD's declared C type (unwrapping one level of `Option[Sum]`) while /// emitting that field's value, so a NESTED bare variant constructor call /// (`Net(e)` inside `Some(Net(e))`) can disambiguate against a colliding /// same-named variant in a DIFFERENT sum. Consulted by /// `debt_find_variant_ctx` as a signal MORE PRECISE than /// `debt_current_fn_return_sum` (the whole enclosing FUNCTION's return /// type, which misses when the variant sits inside an Option-wrapped /// STRUCT FIELD of a fn returning something else entirely — e.g. /// `HttpError.from_net() -> HttpError` building `source: Some(Net(e))` /// where `Net` collides between `std.http.ErrSource` and `std.tls.TlsError`, /// M-178). `None` almost everywhere (only set around field-value emission /// in `emit_record_field_value`) — byte-identical when unset since /// `debt_find_variant_ctx` only consults it among ≥2 already-ambiguous /// plain candidates for the SAME variant name. expected_sum_hint: Option<String>, /// [221.1 №250/№251] `NovaOpt_<T>` C-type a bare `None` nested in /// `Ok(None)`/`Err(None)` is expected to construct — same shape as /// `expected_sum_hint` above, sibling gap; full rationale + the actual /// priority-resolution logic live in `option_none_hint` (this module's /// `None` is unset almost everywhere, byte-identical then). expected_option_elem_hint: Option<String>, /// D73/D84: When emitting `ro x T = v.into()`, set to T before emitting /// the RHS so the .into() resolver can prefer `T.from(v)` over other /// targets that also accept v (e.g. StringBuilder.from(str)). expected_into_target: Option<String>, /// Hint for empty/uninferable array literals: element C type (e.g. "nova_str"). /// Set when target type is NovaArray_X* so `[]` emits nova_array_new_X not nova_int. current_array_elem_hint: Option<String>, /// Plan 91.8a.2 followup: when emitting array literal for `[]Protocol` /// annotation, holds (proto_name, type_args) so emit_array_lit boxes /// each element through box_value_for_protocol + heap-alloc into /// void_p slot. None for non-protocol-array contexts. current_array_protocol_box: Option<(String, Vec<String>)>, /// Plan 91.8a.2 [M-91.8a.2-default-body-general] — generalized default /// body synthesis. Tracks (TypeName, MethodName) pairs for which the /// compiler has already emitted a synthesized `Nova_<T>_method_<m>` /// function via try_synthesize_default_method (replaces the previous /// hardcoded MVP equals/fmt inline emission). Used to (a) avoid /// duplicate emission, (b) detect synthesis cycles (active set tested /// during recursive resolution). synthesized_default_methods: HashSet<(String, String)>, /// Plan 91.8a.2 [M-91.8a.2-default-body-general] — active synthesis /// in-progress. Used for cycle detection (E_SYNTH_CYCLE) when a /// default body itself triggers further synthesis on the same /// (TypeName, MethodName). synthesizing_default_methods: HashSet<(String, String)>, /// Plan 91.9 (D186) — per-type explicit protocol opt-in list from /// `#impl(P1 + P2 + ...)` annotation. Populated during forward-decl /// pass когда мы видим `Item::Type` declarations. Used by /// try_synthesize_default_method to gate bare-call synthesis: types /// without `#impl(P)` cannot have P's default-body methods called /// в ambient contexts (bare method call, interpolation). Bound / /// coercion остаются structural — gate_on_impl=false. type_impl_protocols: HashMap<String, HashSet<String>>, /// Plan 196.8 [M-primitive-receiver-bounded-blanket-dispatch]: D310 /// type-set declarations (`type Ints set i8 | i16 | ... | uint`) — maps /// set name (e.g. "Ints") → canonical Nova member names. `type_impl_ /// protocols` above ONLY covers `#impl(P)` protocol opt-ins and is NEVER /// populated for primitives, so a bounded blanket whose bound is a /// type-set (not a protocol) could not previously recognize that a /// primitive receiver (e.g. `i64`) satisfies it — see `protocols_match` /// at the Plan 164 Ф.3 guard call site. type_set_members: HashMap<String, Vec<String>>, /// Maps local variable name → (param_c_types, return_c_type) for function-typed parameters. /// Used to emit proper function pointer calls for `body(args)` where body is a fn param. fn_param_sigs: HashMap<String, (Vec<String>, String)>, /// [D52-амендмент, ОКНО-5 M-newtype-over-fn-type-unsupported / M-alias-of- /// fn-type-not-callable] call-through: name of a declared `type X fn(A) -> /// B` newtype OR `type X alias fn(A) -> B` (any alias chain resolving to /// a fn-type) → the underlying `TypeRef::Func`. Populated ONCE up-front /// in `emit_module` (scans `module.items` — cheap, independent of /// `emit_type_decl`'s own ordering) so `resolve_fn_typeref` can treat a /// PARAM/local of type `X` exactly like a bare `fn(A) -> B` value for the /// `fn_param_sigs` call-dispatch mechanism below — `next(req)` on a /// `Handler`-typed `next` forwards to the SAME `NOVA_CLOS_CALL_*` / /// `NovaClosBase` codegen a plain fn-typed param already gets (§C-runtime, /// D35), no new call-emission path needed. Newtype unwraps exactly ONE /// level (mirrors D55 `single_wrap_candidates`'s single-wrap design — /// the grammar only permits `type X fn(...)` directly, no chaining); /// alias unwraps FULLY transparently (D52 alias-transparency, any depth). fn_newtype_sigs: HashMap<String, TypeRef>, /// Plan 172.1 D402: unannotated ClosureLight let-bindings whose fn_param_sigs entry /// defaults to nova_int. Maps binding name → (param_names, body_expr). At call sites, /// we re-derive param/return types from the actual argument types + body inference /// instead of using the nova_int defaults — fixing width collapse for `|v| v`. unanno_light_clos: HashMap<String, (Vec<String>, Expr)>, /// Plan 55 Ф.1: maps variable name of `[]fn(P...) -> R` type → element closure /// signature `(P_c_tys, R_c_ty)`. Used in `emit_for` so that `for f in fns { f() }` /// can register the loop binding in `fn_param_sigs` and route `f()` through /// `NOVA_CLOS_CALL_*` / `NovaClosBase` dispatch instead of treating `f` as a free /// function name (which previously emitted undefined `nova_fn_f()`). array_param_fn_sigs: HashMap<String, (Vec<String>, String)>, /// Plan 14 Ф.3: signature of every top-level user fn (`fn name(...)`). /// Используется для emit free-fn-as-value: при `let f = inc` или /// `xs.map(inc)` — нужно знать sig чтобы построить thunk и closure. /// Обновляется при register_fn в первом проходе. user_fn_sigs: HashMap<String, (Vec<String>, String)>, /// [M-196-freefn-arity-overload-default-ret-mismatch] fix: `user_fn_sigs` /// above is keyed by BARE name only and last-declaration-wins — correct /// for the common single-overload free fn, but arity-BLIND and WRONG /// whenever ≥2 free-fn overloads share a name with DIFFERENT return /// types (the return-type inference for a call picks whichever overload /// happened to be registered LAST, regardless of the call's actual /// arg count). By the time B10f reads a return type, any call that /// needed default-arg backfill has ALREADY been normalized to the /// EXACT positional arg count of its resolved candidate /// (`callnorm.rs`'s two-phase Block rewrite runs before codegen) — so /// disambiguating by `params.len() == args.len()` at THIS layer (same /// criterion as the checker-side Q1 rtbuf-producer, `types/mod.rs`'s /// Ident/Call arm) is sound and requires no default-fill logic of its /// own. Parallel to `user_fn_sigs` (never removes/replaces it — same /// registration sites, additively pushing every overload's own /// `(params.len(), ret_c)` instead of overwriting) so every OTHER /// `user_fn_sigs` consumer (thunk emission, HOF param inference, arity /// checks) stays byte-identical. free_fn_ret_by_arity: HashMap<String, Vec<(usize, String)>>, /// Bidirectional inference: maps (callee_name, param_index) → inner closure /// signature (param_types, ret_type) when the HOF parameter at that position /// has type `fn(T...) -> R`. Populated during register_fn pass; consulted in /// emit_call when a ClosureLight argument needs its parameter types inferred. hof_param_fn_sigs: HashMap<(String, usize), (Vec<String>, String)>, /// Plan 14 Ф.6 (D69): set of variadic-fn names. На call-site /// `emit_call` для имени из этого set'а собирает args[N-1..] в /// синтезированный ArrayLit и передаёт как последний аргумент. user_fn_variadic: HashSet<String>, /// Plan 14 Ф.6: guard от infinite-recursion в `emit_call`. После /// преобразования variadic args → ArrayLit мы recurse'имся в /// `emit_call` с новыми args; флаг говорит recursion'у пропустить /// variadic-routing-check (он уже сделан). suppress_variadic_routing: bool, /// Plan 14 Ф.3: имена user fn'ов, для которых уже эмитнут thunk /// (envless adapter `static <ret> nova_fn_<name>_thunk(void* env, args) /// { return nova_fn_<name>(args); }`). Дедупликация — несколько /// references к одной fn делят один thunk. emitted_fn_thunks: HashSet<String>, /// Plan 14 Ф.2: имена const'ов с runtime-init (record-литерал, call, /// и т.д.). На use-site `Ident(name)` для них эмитится `nova_const_<name>()` /// (lazy-init геттер) вместо имени переменной. Тип сохраняется в /// `var_types[name]` (как для обычных const'ов). lazy_consts: HashSet<String>, /// `[M-lazy-const-init-race]` (2026-07-09): pending lazy-const init /// bodies, collected as each lazy const (`const X = <non-constexpr>` / /// module-level `ro X = <runtime-expr>`) is emitted, and combined at /// finalize (`emit_module`, just before `emit_main_wrapper`) into ONE /// topologically-sorted `nova_consts_init()` function — replaces the old /// per-const check-then-act lazy getter (non-atomic `_init` flag + /// no publication barrier for the value = data race under M:N: a worker /// can observe `_init=1` before the value write is visible, or two /// workers can race the init itself). `nova_consts_init()` runs ONCE from /// the driver's main-path, before any worker spawns (`nova_runtime_auto_arm`) /// — so reads become a bare global-value access, no branch/call, and no /// concurrent initializer can race. (name, init-body C statements, free /// identifiers referenced by the initializer — used for the dependency /// topo-sort, filtered down to other lazy-const names at finalize time). pending_const_inits: Vec<(String, String, Vec<String>)>, /// Plan 14 Ф.4: fn-typed поля record'ов — `(record_name, field_name) /// → (param_c_tys, ret_c_ty)`. Заполняется при `emit_type_decl` /// для record-полей с TypeRef::Func. Используется в Member-call /// для routing'а через `NOVA_CLOS_CALL_*`. record_field_fn_sigs: HashMap<(String, String), (Vec<String>, String)>, /// Monotonic counter for trailing block functions — generates unique names. trailing_block_counter: usize, /// Counter for lambda/closure static functions. lambda_counter: usize, /// Maps function name → (param_c_tys, ret_c_ty) when the function returns a fn(...) type. /// Used to register let-bindings of function-call results in fn_param_sigs. fn_returns_fn_sig: HashMap<String, (Vec<String>, String)>, /// Set of function names that are generic (have type parameters). /// Generic functions are emitted with void* erasure; call sites must box/unbox. generic_fns: HashSet<String>, /// Set of type names that are generic (have type parameters). /// Methods on these types have void*-erased params; call sites must box/unbox. generic_types: HashSet<String>, /// Plan 62.E + merge-fix 2026-05-19: set of protocol type names (e.g. `Iter`, /// `Display`, `Hashable`). Protocol values имеют нулевой runtime footprint — /// erase в void* в emit context'е (type_ref_to_c / erased_type_ref_c). /// До Plan 62.E protocols жили только как known-by-name compiler builtins /// (`Iter` хардкоден); теперь formal declarations в `std/prelude/collections.nv` /// + `std/prelude/protocols.nv` требуют explicit erasure registration. protocol_types: HashSet<String>, /// Plan 100.5 (D163): Set of `external type X consume` opaque FFI type names. /// These are concrete types (not generic stubs) — `Nova_File*` should NOT be /// treated as an erased generic placeholder by `debt_is_generic_stub_c`. Inserting /// the name here ensures Result[File, IoErr] monomorphizes to the correct /// `NovaRes_Nova_File_p_Nova_IoErr*` rather than the erased fallback. opaque_ffi_types: HashSet<String>, /// Maps generic function name → tuple arity when the function returns a tuple of type params. /// Used to populate tuple_element_types at call sites. generic_fn_tuple_arity: HashMap<String, usize>, /// Maps type alias name → resolved C type string (e.g. "Name" → "nova_str"). /// Type aliases don't use pointer indirection; their C type is used directly. type_aliases: HashMap<String, String>, /// D71 / Plan 173.1 Ф.2: `parallel for → []T` collection mode. When Some, /// the next `emit_spawn` becomes a parallel-for CHILD: at the spawn site it /// clones the parent `Nova_ChanWriter*` into its ctx (`_nova_par_tx` — /// clone-in-parent at spawn moment, refcount++ BEFORE the parent tx closes), /// its trailing-expression value is SENT into the channel (value types /// boxed, heap types by reference — see `parfor_chan_repr`), and the clone /// is closed on every fiber-exit path (success/throw/cancel) so the channel /// closes by sender-refcount and the drain fiber's `recv()` sees `None`. /// Tuple: (parent_tx_c_var_name, element_c_type). current_parfor_send: Option<(String, String)>, /// Optional Nova source text — when Some, используется для (1) line:col /// в codegen-ошибках (Plan 14 std-fix) и (2) `/* SRC: ... */` комментов /// при `annotation_enabled=true`. Set via `--annotate-source` CLI flag /// активирует комментарии; источник передаётся всегда. annotation_source: Option<String>, /// Plan 140.1 Ф.2 (D24/D13 amend): source file display name used as the /// `<file>` part of the location-first diagnostic prefix /// (`<file>:<line>: <kind> failed: <expr>`) emitted at contract / /// assert violation sites. Set via `set_source_file_name` /// from the build driver (main.rs / test_runner.rs). Defaults to /// `"<unknown>"` when not set (e.g. internal/test emitters). source_file_name: String, /// Plan 14 std-fix: контролирует только эмит SRC-комментариев. Source /// в `annotation_source` остаётся для line:col в ошибках. annotation_enabled: bool, /// Plan 14 Ф.1: typedef'ы NovaOpt_<T> для T без NOVA_ARRAY_DECL в /// runtime — эмитятся лениво при первом упоминании в type_ref_to_c. /// /// Buffer накапливает строки typedef'ов в registration order /// (нижние слои — innermost — регистрируются первыми, что даёт /// правильный topological order: NovaOpt_X должен быть до /// NovaOpt_NovaOpt_X в файле). /// /// На preamble эмитится маркер `/*__NOVAOPT_TYPEDEFS__*/`. После /// полного emit_module маркер заменяется содержимым буфера — /// типы попадают в file scope сразу после tuple-typedef'ов. /// /// Interior mutability: используется из `&self`-методов /// (type_ref_to_c, infer_expr_c_type). novaopt_typedefs_buf: std::cell::RefCell<String>, /// Set when generating nova_opt_eq_ body (inside ensure_opt_typedef). /// In this mode, emit_field_eq uses pointer equality for sum types to /// avoid "incomplete type" C errors: opt_eq fns are emitted before the /// sum type struct definitions, so member access is forbidden. novaopt_early_gen: std::cell::RefCell<bool>, /// [M-153.2-flat-map-inner-option]: NovaOpt typedefs where the payload is a /// value-record (NovaValue_… by-value, needs complete struct before use in /// field decl). Spliced at /*__NOVAOPT_VR_TYPEDEFS__*/ which is placed AFTER /// /*__GENERIC_TYPE_DEFS__*/ so the struct body is always defined first. novaopt_vr_typedefs_buf: std::cell::RefCell<String>, /// [M-172.1-option-eq-record-structural] (L1 proto-ordering): structural /// `nova_opt_eq_<X>` FUNCTIONS (for heap user sum/record payloads) that may /// call a record/field `@equal` METHOD. Spliced at /*__NOVAOPT_EQ_FNS__*/ /// which is placed AFTER both fn-forward-decls and /*__MONO_FWD_DECLS__*/, so /// the method prototypes are visible (else a late opt_eq makes an implicit /// decl → "conflicting types" CC-FAIL). Only the eq FN is late here; the /// NovaOpt typedef stays early (pointer field needs only a forward typedef). /// Unifies sum + record structural eq under one ordering discipline (§0 единый /// источник + фаза-корректность). novaopt_eq_fns_buf: std::cell::RefCell<String>, /// 172.4 Ф.3 A3: ранние ПРОТОТИПЫ per-type by-value обёрток user-@equal /// (тела — в novaopt_eq_fns_buf; прототип нужен ранним inline opt_eq). vr_ueq_protos_buf: std::cell::RefCell<String>, /// Set sanitized-имён NovaOpt_<X> которые уже эмитированы в /// `novaopt_typedefs_buf` (для dedup'а). Pre-populated в `new()` /// из `NOVA_ARRAY_DECL` списка в `nova_rt/array.h` — runtime их /// уже даёт, не нужен duplicate typedef. novaopt_decls_seen: std::cell::RefCell<std::collections::HashSet<String>>, /// [M-result-direct-recursive-enum] / [M-option-self-recursive-record-mono] /// (Plan 186, recursive-mono): stack of heap sum/record type-names (bare, /// no `Nova_`/`*`) currently being expanded by `emit_field_eq`'s structural /// recursion. A self- or mutually-recursive heap type (`type X | V(X)` / /// `type Node { next Option[Node] }`) previously re-inlined the FULL /// per-variant/per-field comparison at EVERY nesting level (bounded only by /// the generic `MAX_EQ_DEPTH` cutoff) — for an N-variant self-recursive sum /// this is O(branching^depth) STRING growth, confirmed to consume multiple /// GB / hang well before the depth-32 bail. When `emit_field_eq` re-enters /// structural-eq synthesis for a type ALREADY on this stack (a genuine /// cycle), it stops inlining and routes through a NAMED per-type comparison /// function instead (`emit_named_struct_eq_call` below) — the self- /// referential field is a real C heap pointer, so a plain recursive /// FUNCTION CALL terminates at runtime on the actual (finite) data instead /// of being unrolled at compile time. Non-cyclic types are unaffected /// (identical inline expansion, byte-for-byte as before). struct_eq_stack: std::cell::RefCell<Vec<String>>, /// Dedup-set of type-names for which a `nova_struct_eq_<T>` named function /// has already been requested/emitted (idempotent, mirrors /// `novaopt_decls_seen`). Body spliced into `novaopt_eq_fns_buf` — same /// late marker (`/*__NOVAOPT_EQ_FNS__*/`, after struct bodies AND method /// forward-decls) so `->tag`/`->payload`/field derefs and any `@equal` /// method call inside the body are always valid C. struct_eq_fn_requested: std::cell::RefCell<std::collections::HashSet<String>>, /// Forward-declaration prototypes for `nova_struct_eq_<T>` named functions /// (see `struct_eq_fn_requested`/`emit_named_struct_eq_call`). Spliced at /// `/*__STRUCT_EQ_PROTOS__*/` — placed right before `/*__NOVAOPT_EQ_FNS__*/` /// (after all struct bodies + method fwd-decls, same phase as the eq-fn /// bodies themselves). A prototype only needs the `Nova_<T>` forward-typedef /// (emitted far earlier, alongside the type's own struct definition), so an /// early proto guarantees correct C ordering even for MUTUAL recursion /// between two distinct named struct-eq functions (A's body calling B's /// function before B's own definition appears later in the file, or vice /// versa) — plain self-recursion doesn't strictly need this (a function may /// call itself inside its own body), but a cross-type cycle does. struct_eq_protos_buf: std::cell::RefCell<String>, /// [M-option-self-recursive-record-mono] (Plan 186, recursive-mono): `(sanitized, /// c_ty)` pairs whose `nova_opt_eq_<sanitized>` STRUCTURAL body construction was /// deferred by `register_novaopt_decl` because `c_ty` self-references a type /// CURRENTLY mid-emission (`being_defined_record_types`/`being_defined_sum_types` /// — e.g. `Option[Node]` registered while lowering `Node`'s own `next` field). /// The typedef + prototype are still emitted immediately (early, safe — a /// pointer field only needs the forward-typedef); only the eq-fn BODY (which /// needs `record_schemas`/`sum_schemas` to be fully populated for the referenced /// type) waits. Drained by `drain_pending_structural_eq` right after ALL /// non-generic type declarations have been emitted (`emit_module` §1), by which /// point every such schema is guaranteed complete. pending_structural_eq_bodies: std::cell::RefCell<Vec<(String, String)>>, /// Plan 54 Ф.9: sanitized NovaOpt-id → real C-type значения. Нужно /// чтобы pattern_bind_typed для `Some(v) => v` где scrutinee /// `NovaOpt_Nova_X_p` (sanitized) восстановил correct `v` тип /// `Nova_X*` (не sanitized "Nova_X_p"). Без map'a `t_from_scr` = /// strip("NovaOpt_") даёт sanitized, что breaks pointer types. novaopt_value_types: std::cell::RefCell<std::collections::HashMap<String, String>>, /// Plan 59 Ф.7.5: lazy mono'd `NovaRes_<OkC>_<ErrC>` Result typedefs — /// per-(T,E) value-type структуры, аналог `novaopt_typedefs_buf`. /// Splice'ится в маркер `/*__NOVARES_TYPEDEFS__*/` после emit_module. novares_typedefs_buf: std::cell::RefCell<String>, /// [M-181-result-over-named-tuple-codegen]: NovaRes typedefs where the Ok /// (or Err) payload is a by-value LATE-emitted struct — a named tuple /// (`NovaTuple_<Name>`) or mono'd value-record (`NovaValue_…____…`). The /// `NovaRes_<n>` struct embeds that payload BY VALUE (`struct { <T> _0; } Ok`), /// so a forward typedef is NOT enough — the complete struct body must precede /// it. The forward typedef stays early in `/*__NOVARES_TYPEDEFS__*/` (pointer /// use in fn prototypes is fine), while the struct BODY + `nova_make_*` /// constructors are spliced at `/*__NOVARES_VR_TYPEDEFS__*/` (placed right /// after `/*__NOVAOPT_VR_TYPEDEFS__*/`, i.e. after the named-tuple/value-record /// struct bodies). Exact mirror of the NovaOpt VR-routing ([M-153.2], D215). novares_vr_typedefs_buf: std::cell::RefCell<String>, /// Plan 59 Ф.7.5: dedup-set уже эмитированных `NovaRes_<ok>_<err>` имён. novares_decls_seen: std::cell::RefCell<std::collections::HashSet<String>>, /// Plan 59 Ф.7.5: mangled `<ok_s>_<err_s>` → (ok_c, err_c). Восстановление /// (T,E) из C-типа `NovaRes_<n>*` (sanitized ≠ c_ty для pointer-типов). novares_value_types: std::cell::RefCell<std::collections::HashMap<String, (String, String)>>, /// Plan 59: registry mono'd tuple types. Каждая Vec<String> — element /// C types для конкретной mono'd tuple (e.g. `["nova_str", "nova_int"]` /// для `(str, int)`). При emit_module — выводим struct typedef для /// каждой registered tuple. Used by `apply_type_subst_to_ref` для /// Tuple case, и codegen для TupleLit / destructure. mono_tuple_instances: std::cell::RefCell<std::collections::HashSet<Vec<String>>>, /// Plan 148 Ф.4 [M-codegen-unify-tuple-repr]: on-demand registry of the /// LEGACY all-`nova_int` `_NovaTupleN` arities actually requested. The /// blanket `_NovaTuple1..8` pre-declaration is retired — these typedefs /// are now emitted only for arities the (rare) erased-generic fallback /// path genuinely uses (in practice only arity 2, from erased HashMap/Set /// `(K, V)` pairs). Concrete tuples always use the typed mono'd path. legacy_tuple_arities: std::cell::RefCell<std::collections::BTreeSet<usize>>, /// [M-fixed-array-value-semantics] (2026-07-10, D27-амендмент): registry of mono'd /// `[N]T` INLINE struct instances — `(N, elem_c_ty)` pairs. Mirrors `mono_tuple_instances` /// (dedup + finalize-pass typedef emission), but for the fixed-size array value class: /// `typedef struct { T data[N]; } _NovaFixArr_<N>_<len>_<elem>;` — no heap pointer, no /// len/cap runtime fields (N is compile-time). Used by `register_mono_fixed_array` / /// `compute_mono_fixed_array_c_name` / the `ExprKind::Index` + `ArrayLit` codegen arms. mono_fixed_array_instances: std::cell::RefCell<std::collections::HashSet<(usize, String)>>, /// Accumulated lint warnings from codegen (e.g. anonymous-embed override). /// Returned from emit_module instead of printed directly to stderr, /// so test runner can route them to captured_stderr rather than leaking /// to the terminal. warnings: std::cell::RefCell<Vec<String>>, /// Plan 70 Ф.B0: accumulated strict-mode E7001 errors from cascade-blocked /// sites that detected `nova_int` silent fallback. Populated by /// `record_strict_error`. Checked at `emit_module` finalization — non-empty /// → `Err(aggregated)` returned, codegen pass fails, no `.c` written. /// /// Используется вместо `?` propagation в местах где function signature /// нельзя menять без massive caller-chain refactor (infer_expr_c_type /// returns `String`, register_mono_instance returns `()`, etc.). Effect /// equivalent: build fails with detailed diagnostic; user sees all E7001s /// в одном compile pass (better UX чем fail-fast). strict_errors: std::cell::RefCell<Vec<String>>, /// Plan 139 Ф.6: literal interning. Maps a string-literal's raw content /// → the C symbol name of its single shared `static const uint8_t[]` /// rodata buffer. Identical literals (same bytes) reuse one buffer + /// one `static const nova_str` value, so `"abc"` appearing N times /// references one buffer (rodata dedup, size/perf win, identity-stable /// ptr). Semantically invisible: str eq/hash are byte-content (Ф.3), so /// pointer-identity coincidence cannot be observed by programs. The /// symbol name is content-hash-based (stable, collision-resistant; R15). interned_str_literals: HashMap<String, String>, /// Plan 139 Ф.6: insertion-ordered list of interned literals to emit at /// the `/*__INTERNED_STR_LITERALS__*/` preamble marker. Each entry = /// (symbol, escaped_c_string, byte_len). Ordered (not just the HashMap) /// for deterministic emitted-C output. interned_str_emit: Vec<(String, String, usize)>, /// Имена, уже занятые в `interned_str_emit`. Нужно РОВНО для /// защиты от коллизии имён: без него она перебирала весь Vec на /// каждый новый литерал, то есть O(N²) по числу различных /// литералов (реестр 221.1 №522). interned_str_syms: std::collections::HashSet<String>, /// Plan 186 (D412): blob-literal interning. Maps blob CONTENT (raw /// bytes of `x"..."` / `embed(...)`) -> the C symbol of its single shared /// `static const uint8_t nova_blob_<hash>[]` rodata buffer. Identical /// blobs (same bytes) share one buffer -- mirror of interned_str_literals. interned_blob_literals: HashMap<Vec<u8>, String>, /// Plan 186 (D412): insertion-ordered blob list rendered at the /// `/*__INTERNED_STR_LITERALS__*/` preamble marker (appended after the /// str literals). Ordered for deterministic emitted-C output. interned_blob_emit: Vec<(String, Vec<u8>)>, /// Близнец `interned_str_syms` для блобов — та же квадратичность. interned_blob_syms: std::collections::HashSet<String>, /// Plan 210 Ф.8 (Go-паритет+, 2026-07-17, OPT-IN): when `Some(dir)`, blob /// statics are emitted as C23 `#embed "<sym>.bin"` (sidecar file written /// under `dir`) instead of the default `0x%02X,` hex-text array (~5.3x /// smaller-than-payload `.c` text, ~×5.3 expansion avoided entirely — /// see `render_interned_blob_literals`). `None` (default — set ONLY when /// the caller opts in via `NOVA_C23_EMBED=1`, `nova-cli/src/main.rs`'s /// `build` command) keeps the existing hex behavior byte-identical — ZERO /// change to any pipeline that doesn't explicitly ask for this. Even when /// `Some`, a cached runtime feature-probe (`embed_c23_supported`, /// test_runner.rs) gates the actual `#embed` emission — CI's clang 18 /// (verified via `docker run ubuntu:24.04`) does NOT support C23 `#embed` /// (`invalid preprocessing directive`), so probe failure silently falls /// back to hex regardless of this field. blob_sidecar_dir: Option<std::path::PathBuf>, /// Plan 70.1: set of imported-module prefix names visible в this module /// (alias + last-segment of import path). Used в emit_call Member dispatch /// чтобы распознать `<alias>.func(args)` или `<module>.func(args)` pattern /// и переписать в bare `func(args)` (imported fns доступны без префикса /// в text scope; префикс — namespace hint, не actual C-name component). /// /// Populated в emit_module pre-pass из `module.imports` + each peer_file /// imports. Aliases (`import X as th`) и last-segments (`import X.Y` → `Y`) /// оба добавляются. Без этого fix codegen эмитит `th.func(args)` напрямую /// → undeclared identifier `th` в C → CC-FAIL. imported_modules: HashSet<String>, /// Plan 81 Ф.6.2: имя свободной функции → путь объявляющего модуля. /// Заполняется в `emit_module` из `module.peer_files`; `free_fn_c_name` /// использует для mangling `nova_fn_<modpath>_<name>`. fn_module_map: HashMap<String, Vec<String>>, /// Plan 91.12 (D126 retract followup): module-private `const` C-name /// mangling. Per-file resolution: ((file_id, source_name) → mangled C name). /// Поскольку codegen эмитит всё в один TU, одноимённые private consts /// в разных модулях коллидят в global symbol table. Mangling даёт /// уникальный C-symbol (`Nova_const_<modpath>_<name>`), а per-file /// lookup at Ident emission attribute'ит ссылку к её peer'у через /// `expr.span.file_id`. Exported consts → no mangle. private_const_c_names: HashMap<(crate::diag::FileId, String), String>, /// [fix M-samename-export-const-cross-module-c-symbol-collision, реестр /// 221.1 №151] Names of `export const` declared in ≥2 DISTINCT modules of /// this CU (the EXPORT counterpart of `colliding_type_names`/ /// `colliding_fn_names` — those axes already qualify colliding types/fns; /// `export const` had NO qualification at all: `private_const_c_names` /// above is populated ONLY for non-exported consts by original design /// ("Exported consts не mangle'ятся ... collision — ambiguity error /// type-checker'а уровня D29" — a check the checker never actually /// implements, so two `export const`s sharing a bare name in different /// modules emit the SAME bare `_nova_const_<name>_value` C global → /// CC-FAIL `redefinition ... with a different type`). Built in /// `emit_module` from `peer_files`, mirroring the D381 `type_def_modules` /// collision count. Empty for any CU without a same-name export-const /// collision → every lookup below is a no-op (byte-identical). colliding_const_names: HashSet<String>, /// [fix №151] name → qualified C base for the MOST RECENTLY processed /// declaration of that (colliding) name (`emit_const_decl` overwrites this /// entry every time it processes a same-name colliding export const). /// Mirrors — deliberately — the SAME "last-processed-wins" characteristic /// `var_types` (keyed flatly by bare source name, Ident type-inference) /// already has for a same-name collision: a genuinely AMBIGUOUS reference /// (a file importing BOTH colliding candidates unqualified, e.g. /// `mcrepro/step4.nv`) has no clean disambiguation signal available at /// this layer (the resolved-type channel does not carry per-const import /// identity yet), so reference-site resolution reads THIS map — built /// from the SAME processing order that decided `var_types[name]`'s final /// type — guaranteeing the read's C TYPE and its C SYMBOL stay consistent /// (eliminating the C-level redefinition/type-mismatch), even though /// WHICH candidate wins a genuine same-file dual-import ambiguity remains /// processing-order-dependent — not a new behavior, only extended /// consistently from type-inference to symbol-selection. A real fix /// (checker-side ambiguity diagnostic or a resolved-const-identity /// channel) is a followup, not attempted here (see report). const_qualified_by_name: HashMap<String, String>, /// Plan 170 (D307): file-private FREE-FN C-name mangling. Same per-file /// resolution model as `private_const_c_names`: `((file_id, source_name) /// → file-discriminated C name)`. A `priv(file) fn helper` gets a unique /// C symbol per declaring file (`nova_fn_<modpath>_f<file_id>_<name>`), so /// two same-named file-private helpers in peer files never collide at link /// time. Resolved at both the function-definition emit and every call-site /// via `current_emit_file_id` inside `free_fn_c_name`. File-private fns are /// kept OUT of `method_overloads` (file-local, never cross-file overloads). file_priv_fn_c_names: HashMap<(crate::diag::FileId, String), String>, /// [Facet-B D307 §1/§3] Same key as `file_priv_fn_c_names`, but the VALUE /// is the declaring `FnDecl` itself (cloned) rather than just its C name. /// Lets a generic-mono call-site distinguish "does MY OWN declaring file /// have a `priv(file)` overload of this name, and is it generic or /// concrete" — needed because `generic_fns`/`mono_fn_decls` are plain /// bare-name (file-oblivious, last-registration-wins) maps: a same-named /// `priv(file)` GENERIC in some OTHER peer file would otherwise hijack /// EVERY same-named call in the whole folder-CU (including a call to a /// more-specific CONCRETE `priv(file)` overload living in the caller's /// own file — D84 requires the concrete file-local one to win, and it /// must never be resolved through a different peer's mono instance). file_priv_free_fn_decls: HashMap<(crate::diag::FileId, String), crate::ast::FnDecl>, /// Plan 170 (D307): the current file being emitted (function definition or /// the body whose call-sites are being lowered). Threaded so `free_fn_c_name` /// can resolve a file-private free fn to its declaring-file C symbol. /// `None` outside any per-file emission context (no file-private resolution). current_emit_file_id: Option<crate::diag::FileId>, /// Plan 20 Ф.4: stack of active defer/errdefer scopes during emission. /// Each block that contains a `defer`/`errdefer` stmt pushes a `DeferScope` /// on entry and pops on exit. `Stmt::Return`/`Break`/`Continue` walk the /// stack to invoke pending defers in LIFO before the actual jump. /// `errdefer` cleanup is gated on a per-scope `is_error` flag set by /// `setjmp`-handled fail-frame. defer_scopes: Vec<DeferScope>, /// Plan 20 Ф.4: monotonic block-ID counter for stable, unique C names /// (`_defer_<BLKID>_<N>_active`, `_defer_cleanup_<BLKID>`, etc.). defer_block_counter: usize, // План 253.4 Ф.1 (№480, 2026-08-08): ПОЛЕ `reconsume_scopes` СНЯТО. // Это был реестр №1 из двух: стек активных re-consume блоков // `consume X { … }` — (binding, consume defer block-id) — который // `disarm_var_for` опрашивал первым, чтобы ВЫЧИСЛИТЬ имя флага // `_defer_<id>_0_active` по номеру блока. Имя флага теперь выводится из // C-идентификатора переменной и живёт в самой defer-записи; обе роли // реестра (резолв дизарма, множество активных guard'ов) читаются из // `defer_scopes` — см. `disarm_var_for` / `reconsume_active_names`. /// Plan 217 (D-новый, гибрид C): имена типов (plain Nova, НЕ C-ident) с /// эффект-чистым `@cleanup` (`f.effects.is_empty()`) — extern И user- /// defined ОБА считаются (в отличие от `consume_cleanup_types`, который /// исключает extern ради ccount-поля). Populated by an `emit_module` /// pre-pass mirroring `LinearityRegistry::build`'s checker-side twin /// (types/mod.rs) — must stay in sync (both scan the same `f.name == /// "cleanup" && recv.consume && Instance && effects.is_empty()` shape). auto_cleanup_types: HashSet<String>, /// Plan 217: arm-sites for bare `consume X = e;` (non-block) bindings of /// an auto-cleanup-eligible type, collected by `enter_defer_scope`'s /// prologue scan (keyed by the `LetDecl`'s span — stable within one /// function body). Consumed (removed) when `emit_stmt` actually reaches /// that `Stmt::Let`, to arm the shield + `_active` flag with the exact /// C names declared in the prologue. `(block_id, entry_idx, init_c_type)`. auto_cleanup_arm_sites: HashMap<Span, (usize, usize, String)>, /// Plan 217 BUGFIX (folder-CU regression `guard_cross_scope_transfer.nv` /// "Guard passed to helper function and consumed there" — MutexGuard /// double-`unlock`): free-fn NAME → set of ARG POSITIONS that are /// `consume`-mode on AT LEAST ONE overload of that name (union across /// overloads — conservative, favors disarming over leaking since the /// alternative, discovered empirically, is an ACTIVE double-cleanup /// crash). `do_work_under_lock(g, counter)` — `g` at position 0 must /// disarm `g`'s auto-cleanup in the CALLER before the call, mirroring /// what the checker's `consume_args`/`consume_idxs` already does for /// the STATIC obligation (this closes the codegen-side gap that left /// `_active` armed after a legitimate transfer). free_fn_consume_param_positions: HashMap<String, HashSet<usize>>, /// Plan 217 BUGFIX (same as above): `(receiver_type_name, method_name)` /// → set of consume-mode ARG positions (0-based, receiver excluded) — /// covers `recv.method(g)` where `g` is an auto-cleanup binding passed /// as a consume-mode argument to a METHOD call (not the receiver /// itself, which `consume_receiver_methods` already handles). method_consume_param_positions: HashMap<(String, String), HashSet<usize>>, /// Plan 217: type_name (plain Nova) → set of method names declared with /// a `consume` receiver on that type (mirrors checker's `LinearityRegistry /// ::consume_methods`, types/mod.rs). Gates the Stmt::Expr bare-statement /// receiver-disarm (`X.method()`): only an ACTUAL consuming method call /// disarms auto-cleanup — a bare read-only/`ro`/`mut`-receiver method /// call on the same tracked binding must NOT falsely disarm it (that /// would leak the resource by skipping the real cleanup at scope-exit). consume_receiver_methods: HashMap<String, HashSet<String>>, /// №465 (A8.29, [M-124.8-zero-on-move-auto-inject]): source type name → /// `true` if `AllocKind::Value` record (needs the promotion guard — a /// PARTICULAR binding may have been heap-promoted by escape analysis, /// in which case its C type is `Nova_<X>*`, not `NovaValue_<X>`, and /// zeroing it would alias the new owner's storage), `false` if named /// tuple / ordinary newtype (no promotion concept — `NovaTuple_<X>` / /// `Nova_<X>` scalar-or-pointer typedef is ALWAYS an independent /// byte-copy on move, safe unconditionally). Only populated for types /// that ALSO have `consume` (checker `E_ZERO_ON_MOVE_REQUIRES_CONSUME`) /// and are NOT heap-allocated records (checker /// `E_ZERO_ON_MOVE_ALIASED_STORAGE`) — heap records move by pointer- /// aliasing (old/new binding share the identical block); zeroing the /// pointee would corrupt the value for the new owner, so they are never /// inserted here even defensively (see `debt_is_runtime_backed_newtype` /// guard at the populate site for the Newtype sync-primitive case). /// Consulted by `zero_on_move_rewrite_call` (consume-param-arg sites) /// and the `Stmt::Return` bare-Ident disarm block (consume-return /// site). Receiver-consuming-call sites (`x.method()`) are NOT covered /// — `prepare_method_recv` passes `&x` (alias, not copy) even for /// `AllocKind::Value`, and escape_analyze.rs deliberately excludes the /// method-call receiver position from its escape-sink tracking, so /// there is no compiler-verified guarantee the callee doesn't stash /// the pointer. Empirically confirmed via scratch465/probe1-3.nv. zero_on_move_types: HashMap<String, bool>, // План 253.4 Ф.1 (№480, 2026-08-08): ПОЛЕ `auto_cleanup_active` СНЯТО. // Это был реестр №2 из двух: `(name, block_id, entry_idx)` взведённых // auto-cleanup-биндингов, наполнявшийся ОТДЕЛЬНЫМ третьим шагом // (`emit_auto_cleanup_arm`) — тем самым, который форма // `spawn consume a, b { … }` пропускала (№456). Сам drop-флаг // (§8а п.6(а): читается только на выходе из скоупа, ветка-потребитель // гасит, ветка-непотребитель оставляет — MaybeConsumed-safe) НИКУДА не // делся: это и есть C-переменная `_defer_<c-имя>_active`, объявляемая // рядом с переменной. Ушёл только реестр, который её РАЗЫСКИВАЛ. /// [M-217-break-continue-loop-boundary-bleed] BUGFIX: one entry per /// currently-open loop body (`for`/`while`/bare `loop`), pushed/popped by /// `emit_loop_body_inline_ex` alongside its own `enter_defer_scope`/ /// `leave_defer_scope` call — `true` iff THAT call actually pushed a real /// `DeferScope` (i.e. the loop's OWN top-level statements contain a /// `defer` or an auto-cleanup-qualifying bare `consume` let). `enter_ /// defer_scope` early-returns `block_id=0` (no scope, no C boilerplate) /// for a loop body with neither — a deliberate perf optimization for the /// (common) trivial-loop case. But `Stmt::Break`/`Stmt::Continue`'s /// `emit_early_exit_cleanup(stop_at_loop=true)` walks `self.defer_scopes` /// from the top looking for the NEAREST `is_loop_body` scope to stop at — /// when the loop being broken registered NO scope of its own, that /// marker is simply ABSENT from the stack, so the walk "overshoots" past /// the (nonexistent) loop boundary straight into whatever ENCLOSING, /// still-open scope happens to be on top instead (e.g. the spawn/fn /// body's own auto-cleanup consume-let scope) — firing that OUTER /// scope's `@cleanup` prematurely (the outer scope has not actually /// exited; a `break` out of a nested trivial loop should touch NOTHING /// beyond the loop's own — absent — boundary). Found via `[M-217-spawn- /// closure-consume-cleanup-undefined]` follow-up regression /// (`readguard_writeguard_separated.nv`'s "multiple ReadGuards can /// coexist": `consume rg = rw.read(); loop { … if … { break } … }; /// rg.unlock()` — the CAS-retry loop's `break` wrongly fired `rg`'s /// `@cleanup` early, then the manual `.unlock()` after the loop fired /// AGAIN → double-release, "read_unlock() called without a matching /// read()"). Reproducible with NO spawn involved at all (plain `fn /// main`) — a pre-existing gap in the shared break/continue early-exit /// machinery, merely never exercised via a spawn/parallel-for body /// before (spawn bodies never registered a real auto-cleanup scope /// prior to this same wave's `emit_spawn` fix). `Stmt::Break`/`Continue` /// consult `.last()`: `Some(false)` (nearest loop registered no scope) /// → skip `emit_early_exit_cleanup` entirely (sound: a loop with no /// scope of its own can, by construction, have nothing nested inside it /// still open on `self.defer_scopes` at this point); `Some(true)` or /// empty (defensive fallback, preserves old behavior) → proceed as /// before, which already correctly stops AT a genuinely-registered loop /// scope. loop_body_has_scope: Vec<bool>, /// Closure mut-capture heap-box registry. Maps variable name → C box-pointer /// variable name (`_box_<name>`). When a mut local is captured by a closure, /// it is heap-promoted: a `T* _box_x = nova_alloc(sizeof(T)); *_box_x = x;` /// is emitted, followed by `#define x (*_box_x)` so all subsequent caller-side /// reads/writes go through the box. The closure env stores `_box_x` directly /// (no dangling-ptr risk on escape). Cleared and #undef'd at function exit. var_boxed: HashMap<String, String>, /// №240 [M-detach-box-while-loop-read-after]: `(byte-offset, indent)` /// into `self.out`, set once per top-level C function body by /// `emit_block_stmts` (right after the function's own opening brace). /// `codegen/emit_c/emit_detach.rs::hoist_box_decl` retroactively inserts /// bare box-pointer declarations here, so a `detach{}` nested inside a /// `while`/`if`/match-arm C block still declares its box pointer in a /// scope that dominates reads occurring after that nested block closes. detach_box_hoist: Option<(usize, usize)>, /// Plan 48: generic FnDecls for monomorphization worklist drain. /// Key = Nova fn name (e.g. "within"). Populated during pre-pass. mono_fn_decls: HashMap<String, crate::ast::FnDecl>, /// Plan 184 Р10: free-fn name → per-positional-param "is by-pointer in-out" /// flag (a value/primitive `mut x T` param). Drives call-site address-of /// injection in `synthesize_inout_refargs`. Populated for every free fn /// (generic and non-generic) in `emit_fn_forward_decl`. free_fn_inout_params: HashMap<String, Vec<bool>>, /// Plan 172.14 Ф.1: C-имя value-struct'а (`NovaValue_X`/`NovaTuple_X`) → /// УПОРЯДОЧЕННЫЙ список C-типов его полей. Заполняется при эмиссии /// typedef'ов (`emit_value_record_type`/`emit_named_tuple_type`) — до /// fn-forward-decl пасса. Источник для точного C-размера структуры /// (`value_struct_size_align`, layout идентичен эмитируемому C). value_struct_field_tys: HashMap<String, Vec<String>>, /// Plan 172.14 Ф.1: free-fn имя → per-параметр `(имя, auto-by-ref)` для /// БОЛЬШИХ (>16Б по C-ABI, порог SysV владельца) read-only value-struct /// параметров. Такой параметр лоуерится в `const`-семантику `T*`: /// сигнатура получает `*`, тело auto-deref через `ref_params`, call-site /// оборачивает аргумент в `RefArg` (rvalue — materialize-temp в эмиссии /// RefArg). Имя, отсутствующее в карте, «отравлено» или не имеет /// кандидатов (перегрузки/дефолты/ссылки-как-значение → консервативно /// by-value). Строится ОДНИМ пре-пассом `build_free_fn_byref_map` ДО /// fn-forward-decl цикла — сигнатуры/тела/call-sites читают одну карту. free_fn_byref_params: HashMap<String, Vec<(String, bool)>>, /// [M-172.14-methods-byref]: зеркало `free_fn_byref_params` для МЕТОДОВ /// (receiver, не свободная функция). Ключ — `(Type, method_name)` (не /// плоское имя — методы с одинаковым именем на разных типах разные C- /// функции). Receiver сам НЕ трогается (уже by-ptr, D181/D326) — карта /// касается только value-параметров метода. Строится /// `build_method_byref_map` сразу после `build_free_fn_byref_map`. method_byref_params: HashMap<(String, String), Vec<(String, bool)>>, /// Plan 48: generic instance-method FnDecls for method monomorphization. /// Key = (receiver_type_name, method_name). Methods with own type params (e.g. @execute[T,E]). mono_method_decls: HashMap<(String, String), crate::ast::FnDecl>, mono_method_decls_by_span: HashMap<crate::diag::Span, crate::ast::FnDecl>, // №130: by-span twin, not last-wins /// Plan 11 Follow-up: receiver-generic method FnDecls для Self.method() fast-path /// (mono enrollment в Path/Member emit). Отдельная карта, чтобы не trigger /// другие code paths которые observe mono_method_decls. self_method_decls: HashMap<(String, String), crate::ast::FnDecl>, /// Plan 48: monomorphization worklist — (nova_fn_name, type_subst, mangled_c_name). /// Plan 172.12 A1‴: `type_subst` carries `ResolvedType` (was C-string) — structural /// identity; the C-name is derived by the printer at body-seeding time. String producers /// route through the `lift_c_name`/`subst_vec_from_c_pairs` debt point (§0). mono_worklist: Vec<(String, Vec<(String, crate::types::ResolvedType)>, String)>, /// Plan 48: already-instantiated mangled names (for dedup). mono_instantiated: HashSet<String>, // [M-138.2-generic-method-overload-mono] maps a mono'd method's FINAL C name -> // the exact overload FnDecl chosen at the call-site. Without it the worklist // drain re-finds the FnDecl by BARE name (first-wins) and emits the wrong // overload body (e.g. a 0-arg getter body under the setter's suffixed name). mono_method_fndecl_for_name: HashMap<String, crate::ast::FnDecl>, /// Plan 48: active type substitution during monomorphized fn emission. /// Maps type_param_name → concrete C type. Set/cleared around emit_monomorphized_fn. current_type_subst: HashMap<String, crate::types::ResolvedType>, /// [M-91.1-method-turbofish-dispatch] Plan 91 Ф.1: transient explicit /// method-level type-args from `obj.method[U,...](args)` (parsed as /// `Call{func: TurboFish{base: Member, type_args}}`). Set by emit_call /// right before recursing on the Member base; consumed (mem::take) by /// resolve_method_level_subst to SEED subst_slots before arg-inference. /// Empty otherwise; taken (not borrowed) so nested calls don't inherit. current_method_turbofish: Vec<crate::ast::TypeRef>, /// [M-exp-promotion-blockers: retry E_UNUSED_PREFIX_TYPEVAR] the ACTIVE /// `with Fail[E] = ...`-block's concrete E (current_type_subst-resolved /// C type), set/restored by `emit_with` around body emission. A bare /// `Ok(x)`/`Err(x)` constructor call deep inside that body (e.g. /// `Ok(body())` inside `with Fail[E] = |e| interrupt Err(e) { Ok(body()) }`) /// cannot see the enclosing `bindings` from `emit_call` — its own /// Err-side default would otherwise hardcode `nova_str` regardless of /// the REAL, mono'd E. `None` when no `Fail[E]` binding is active /// (preserves the pre-existing hardcoded-default behaviour everywhere /// else, byte-identical). current_fail_e_hint: Option<String>, /// Plan 48: forward declarations for monomorphized functions. /// Spliced into output via /*__MONO_FWD_DECLS__*/ marker. mono_fwd_decls: String, /// Plan 186 [bug-2 audit-197 fix]: extern C prototypes for user-declared /// FREE external fn (`extern "nova"/"C" fn foo(...) -> (A, B)`) that /// return a TUPLE. Without an explicit prototype the call site is the /// ONLY occurrence of the symbol in the generated `.c` — C falls back to /// an implicit `int foo()` declaration, which cannot initialize the /// `_NovaTupleN_...` struct temp the call is assigned into /// (`initializing '_NovaTuple_2_...' with an expression of incompatible /// type 'int'`, examples/ffi/sqlite_mini.nv `mini_sqlite_open`/ /// `mini_sqlite_prepare`). Scalar/pointer-returning extern fns don't hit /// this (implicit-int silently "works" for them), so the fix is scoped /// to the one case that actually breaks: tuple returns. Populated in the /// same `emit_module` pre-pass that already registers the mono tuple /// typedef for these declarations (Plan 115 D214); spliced via /// `/*__EXTERN_FN_PROTOS__*/`, placed AFTER `/*__MONO_TUPLE_TYPEDEFS__*/` /// so the tuple typedef the prototype references is already defined. extern_fn_tuple_protos: String, /// Plan 48 Ф.3: template declarations for generic types (record/sum). /// Stored here instead of immediately emitting — instantiated lazily per usage. generic_type_templates: HashMap<String, crate::ast::TypeDecl>, /// Plan 48 Ф.3: worklist for lazy generic type instance emission. /// Each entry: (base_type_name, type_args, mangled_name). /// Uses RefCell so type_ref_to_c (&self) can enqueue instances. /// Plan 172.12 A1‴: `type_args` carries `ResolvedType` (was C-string) — structural /// identity; the C-name is derived by the printer. Producers freeze their already-lowered /// C-string at write time through the single `lift_c_name`/`args_lift` debt point (§0) — a /// context-independent `Raw` carrier, byte-identical to the pre-A1‴ stored string (deferring /// re-lowering to read time would re-bind residual type-params under a divergent mono context). generic_type_worklist: std::cell::RefCell<Vec<(String, Vec<crate::types::ResolvedType>, String)>>, /// Plan 48 Ф.3: already-emitted generic type instances (by mangled name). emitted_generic_type_instances: HashSet<String>, /// Plan 48 Ф.3: methods per generic type template. /// Key = base type name, value = Vec of FnDecl for that type's methods. generic_type_methods: HashMap<String, Vec<crate::ast::FnDecl>>, /// Plan 95 Ф.1.1: type-parameter names for builtin sum-types /// (`Option`/`Result`) that participate in method-mono **without** /// being registered as generic templates. Captured from /// `Item::Type(t)` at section 1a (where `t.name == "Option"|"Result"` /// is `continue`-skipped from `generic_type_templates`). Used by /// `receiver_c_type` to resolve the value-type form `NovaOpt_<T>` / /// `NovaRes_<ok>_<err>` from `current_type_subst` keyed by these names /// — see Plan 95 Ф.2.1. /// /// Example: after scanning `type Option[T] | Some(T) | None` → /// `{"Option": ["T"]}`. For `type Result[T, E]` → `{"Result": /// ["T", "E"]}`. builtin_sum_type_params: HashMap<String, Vec<String>>, /// Plan 95 Ф.2.4: forward declarations for monomorphized Nova-body /// methods on `Option`/`Result`. **Separate buffer from /// `mono_fwd_decls`** because the signature carries `NovaOpt_<T>` / /// `NovaRes_<ok>_<err>*` by-value/by-pointer — types that are lazy- /// declared at the `/*__NOVAOPT_TYPEDEFS__*/` / `/*__NOVARES_TYPEDEFS__*/` /// placeholders (file position Y), AFTER the standard /// `/*__MONO_FWD_DECLS__*/` placeholder (file position X < Y). /// Emitting a fwd-decl referencing `NovaOpt_<T>` by-value at position X /// → CC-fail `incomplete type`. This buffer is spliced at a NEW /// placeholder `/*__BUILTIN_SUM_METHOD_FWD_DECLS__*/` (file position /// Z, with X < Y < Z < body-emit position). builtin_sum_method_fwd_decls: String, /// Plan 48 Ф.3: buffer for generic type instance definitions. /// Emitted separately and spliced into output before fn definitions via marker. /// [реестр 221.1 №139 Round 3]: still the destination for NON-Record /// generic-instance kinds (Sum/Newtype/etc. — unchanged, direct path); /// Record-kind instances are captured into `pending_value_nodes` /// instead (see that field's doc) and no longer land here. generic_type_defs_buf: String, /// [реестр 221.1 №139 Round 3] Side-channel: `emit_generic_type_instance_body`'s /// Record arm sets `Some((struct_c_name, field_c_tys))` right before /// rendering; `drain_generic_type_worklist` reads it immediately after /// the call to decide whether THIS instance goes into /// `pending_value_nodes` (Record kind) or `generic_type_defs_buf` /// (everything else, unchanged). `None` for non-Record kinds. last_generic_record_instance: Option<(String, Vec<String>)>, /// Plan 48 Ф.3: mangled type name → (base_type_name, type_args). /// Uses RefCell so type_ref_to_c (&self) can register instances. /// Plan 172.12 A1‴: `type_args` carries `ResolvedType` (was C-string) — structural /// identity; producers freeze their lowered C-string via `lift_c_name`/`args_lift`, and /// readers that need the C-name lower each arg through the printer (`subst_val_c` — `Raw` /// prints verbatim, byte-identical to the pre-A1‴ stored string). generic_type_instance_info: std::cell::RefCell<HashMap<String, (String, Vec<crate::types::ResolvedType>)>>, /// Plan 48 Ф.7.6: maximum monomorphization-worklist drain depth. /// Default 500; overridable via CLI `--mono-depth=N` or env var /// `NOVA_MONO_DEPTH` (CLI wins). Guards against polymorphic recursion. mono_depth_limit: usize, /// Plan 49 Ф.6 P0 fix: per-variable Nova-level CancelToken[T] tracking. /// key = local variable name (`tok`), value = T's C-type (`nova_int`). /// Populated при `let tok CancelToken[T] = ...` или `let tok = CancelToken[T].new()`. /// Использовано emit_call для `tok.reason()` — emit'ит per-T un-box вместо /// runtime-fixed `nova_cancel_token_reason_str` (которая молча возвращает /// garbage для T≠str). Без entry — default str-form (backward compat). cancel_token_t_map: HashMap<String, String>, /// Plan 49 Ф.6 cross-type cascade: module-wide dedup для converter /// wrappers (`_nova_cancel_conv_<A>_from_<B>`). lambda_impls очищается /// между fn-bodies, поэтому single per-test contains-check НЕ ловит /// re-emit между tests. Этот set tracks все уже emitted wrappers. emitted_cancel_converters: HashSet<String>, /// Plan 57: bench mode. When true, emit_main_wrapper генерирует /// `bench main` (вызовы `nova_bench_run` per BenchDecl) вместо /// обычного main или test runner. test items в этом режиме игнорируются. /// Активируется set_bench_mode(true) из nova-cli `nova bench`. bench_mode: bool, /// Plan 61 Ф.1: TypeId registry. `type-name-mangled` → `NovaTypeId`. /// Populated по мере встречи user-types в throw/handler-arm/any-cast /// контекстах. Эмитятся как `#define NOVA_TID_<mangled> N` в preamble /// (см. `emit_typeid_defines`). /// /// Ключ — sanitized C-name (без `*`, без `Nova_`-префикса; обычно /// nova-side type name, e.g. `ParseError`, `Result_nova_int_nova_str`). /// Значение — monotonic ID starting from NOVA_TID_USER_BASE (17). type_id_registry: HashMap<String, u32>, /// Plan 61 Ф.1: next free TID (counter); starts at USER_BASE. next_type_id: u32, /// Plan 174.3 (D53/D54 v1): per-type `NovaTypeInfo` statics required by /// `any`-boxing (`v as any` → `nova_any_box(&NOVA_TYPEINFO_<sani>, …)`). /// Key = sanitized ident used in the `NOVA_TYPEINFO_<key>` static name; /// value = (tid-macro, display-name). Emitted at the `__TYPEID_DEFINES__` /// splice (right after the `NOVA_TID_*` `#define`s the statics reference). /// `BTreeMap` for deterministic emit order. any_typeinfos: std::collections::BTreeMap<String, (String, String)>, /// Plan 61 Ф.2: per-handler-binding `with Fail[E] = |e| ...` mapping /// from binding-var-name to E's C-type. Используется emit_throw для /// корректной dispatch precedence (per-E typed → erased → unwind). /// Ф.3 будет расширять для per-E mono'd dispatch. fail_e_map: HashMap<String, String>, /// Plan 61 followup #4: per-E typed Fail dispatch — set of E types /// (C-mangled names) used as Fail[E]. Populated через emit_with / /// emit_throw scan. В preamble splice эмиттится per-E vtable typedef /// + TLS slot + `_nova_throw_typed_<E>(E* payload)` fast-path dispatcher. /// /// Production-grade complement к payload-in-frame hybrid: per-E direct /// call (vs indirect через erased + cast). Backward compat: legacy /// `_nova_handler_Fail` (string slot) тоже install'тся (dual-install). per_e_fail_types: HashSet<String>, /// Plan 62.A.bis Ф.1: layered sum-type schema registry. Populated в /// `CEmitter::new()` через `init_hardcoded_baseline()` (4 entries: /// Option, NovaOpt_nova_int alias, Result, RuntimeError). Phase 2+ /// будет добавлять `DeclaredFromPrelude` entries из `std/prelude/*.nv` /// (через `init_prelude_decls` call в `emit_module()`). /// /// **Phase 1 invariant:** registry populated но никто (кроме unit /// tests) его не читает — legacy `sum_schemas: HashMap<...>` field /// продолжает быть единственным источником истины для всех 12 /// hardcoded dispatch points. Поэтому генерируемый C байт-идентичен /// pre-Plan-62.A.bis baseline'у. /// /// См. `docs/plans/62.A.bis-sum-schema-registry.md` §«Phase 1». pub(crate) sum_schema_registry: super::sum_schema_registry::SumSchemaRegistry, /// Plan 103.5: true when currently emitting inside a `realtime { }` block. /// Used to detect blocking Once/OnceCell/Lazy method calls that may park the /// fiber, which is forbidden in realtime context (E_EFFECT_REALTIME_VIOLATION). in_realtime: bool, /// Plan 103.6: true when currently emitting inside a `blocking { }` block. /// Park-ing sync calls are forbidden; wake-only calls are allowed. in_blocking: bool, /// Plan 127 Ф.2: result of value-record escape analysis. Computed once /// per module at the top of `emit_module()`. Consulted при emit_let / /// emit_record_lit для switching `AllocKind::Value` → /// `AllocKind::ValueHeapPromoted` when escape walker detected `&v` /// escape для конкретного local. None until analysis runs (test /// scenarios constructing CEmitter directly). escape_result: Option<crate::escape_analyze::EscapeResult>, /// Plan 127 Ф.3: fn-id key for current emission scope (matches /// `escape_analyze::fn_id` convention — bare name for free fn, /// `<recv_type>::<name>` for method). Set/cleared by emit_fn at /// body-emit boundary. None outside fn body emission. current_fn_id: Option<String>, /// Plan 127 Ф.3: per-binding promoted-marker set, populated при /// emit_let when escape_result says the binding is promoted. Codegen /// downstream (Member access, prepare_method_recv) reads this к /// switch С-ABI form (`-> instead of `.`, pass без `&`). /// Keyed by binding NAME (within current fn scope). Cleared at fn- /// emit boundary. promoted_value_record_locals: HashSet<String>, /// Plan 118 Ф.1: primitive-scalar locals whose address escapes — promoted /// to heap (`Type* name = nova_alloc(sizeof(Type)); *name = val`). /// Cleared/restored at fn-emit boundary like promoted_value_record_locals. promoted_primitive_locals: HashSet<String>, /// Plan 127 Ф.3: transient signal set by emit_stmt Stmt::Let RIGHT /// BEFORE calling emit_expr (which routes to emit_record_lit). /// If `Some(type_name)`, the imminent record literal must be heap- /// allocated as `NovaValue_<type_name>*` instead of stack-init. /// Consumed (taken) immediately by emit_record_lit, restoring to None. pending_value_record_heap_promote: Option<String>, /// Plan 172.14 (sret/_out §3): armed-сигнал стек-плейсмента дескриптора. /// Взводится в emit_stmt Stmt::Let перед emit RHS, когда placement- /// предикат прошёл (ro-биндинг, не эскейпит, RHS — прямой sret-вызов / /// Range-срез Vec): `(адрес-слота-C-выражение, ExprId RHS)`. Потребитель /// (range-slice ветка / точка sret-вызова) сверяет свой `expr.id` с /// сохранённым — защита от утечки armed во ВЛОЖЕННЫЙ вызов (аргумент /// внешнего вызова записал бы слот и утёк) — и take()'ит сигнал. /// Сбрасывается безусловно после emit RHS. sret_out_dest: Option<(String, crate::ast::ExprId)>, /// Plan 172.15 Ф.1: базовый C-тип S (без `*`) буфера, на который взведён /// `sret_out_dest`. Точка переписи (`sret_maybe_rewrite_call`) сверяет его /// с типом из реестра `sret_fns` — предикат допускает звено цепочки, не /// зная его mono-типа, и без этой сверки в буфер могла бы попасть /// структура другого типа. `None` — сверять не с чем (сигнал не взведён). sret_out_dest_struct: Option<String>, /// Plan 172.14 (sret/_out §2): реестр sret-eligible функций — classic /// C-имя → базовый C-тип S (без `*`). Потребитель: перехват ExprKind::Call /// (armed call-site / tail-проброс цепочки) переписывает `f(args)` → /// `f__sret(args, dest)`. sret_fns: HashMap<String, String>, /// Plan 172.14 (sret/_out §2): активен ТОЛЬКО при эмиссии тела /// `__sret`-варианта — имя C-параметра назначения (`_out`). Потребители: /// (a) emit_record_lit — Self-литерал типа возврата конструируется в _out /// без nova_alloc; (b) emit_block_stmts trailing — взводит sret_out_dest /// для tail-Call (проброс _out сквозь цепочку). sret_fn_out: Option<String>, /// Plan 172.14 (sret/_out §2): режим второй эмиссии — текущий emit_fn /// эмитит `__sret`-вариант (имя+`__sret`, параметр `S* _out`). sret_variant_emit: bool, /// Plan 143.2 [M-opt-leaf-preempt-entry-elision]: whole-program set of /// FnKeys whose function-prologue `nova_preempt_check();` MUST be kept. /// Computed once per module in `emit_module` (source-level call-graph /// pre-pass, see `preempt_keep`). Consulted in `emit_fn` prologue: a fn /// whose key is absent may ELIDE its entry-check (provably-leaf). Default /// `populated()==false` → KEEP everything (unit-test / direct construction). preempt_keep: crate::codegen::preempt_keep::PreemptKeepSet, /// Plan 173 Ф.5 (#8, D188 R2): receiver-type C-idents (via /// `receiver_type_c_ident`) of USER (non-extern) `consume @cleanup` /// methods in this module — computed by an emit_module pre-pass. /// Drives (a) the hidden `int _consume_ccount;` field appended by /// `emit_record_type` and (b) the exactly-once prologue in `emit_fn`. consume_cleanup_types: HashSet<String>, /// Реестр 221.1 №583: receiver-type C-idents of EVERY declared `consume /// @cleanup` method, EXTERN included (unlike `consume_cleanup_types` /// above, which deliberately excludes `extern "nova"` cleanups — /// MutexGuard etc. — because those hand-written C structs manage their /// own `_consume_ccount`-equivalent bookkeeping and don't need the /// generated hidden field). Populated by the SAME `emit_module` pre-pass, /// alongside `consume_cleanup_types`. Consulted ONLY by /// `emit_consume_entry_cleanup`'s dispatch-or-skip gate: a type ABSENT /// from this set has NO `Nova_<T>_consume_cleanup` under ANY emission /// path (user OR extern) — genuinely nothing to call. A type PRESENT /// here always has one, extern or not, and the call must fire (skipping /// it for an extern type like MutexGuard would silently leave a lock /// held — a real regression caught empirically: reusing the narrower /// `consume_cleanup_types` here made an unrelated, consume-free fixture /// hang under `nova test` where it used to pass in ~31s). consume_cleanup_declared_types: HashSet<String>, /// Plan 175 Ф.2-v2: `#default_handler(EffectName)` registry — effect /// name → plain Nova name of the zero-arg free fn tagged as its default /// handler-factory (checker already validated arity/return-type/ /// uniqueness/cycles — see `check_default_handlers`). Populated by an /// `emit_module` pre-pass (mirrors `consume_cleanup_types` above). /// GENERIC mechanism — consulted by `emit_effect_type`'s per-op dispatch /// wrapper: an effect with a registered default gets an inline /// once-per-thread lazy-construct-then-install check /// (`if (!_nova_handler_X) { _nova_handler_X = <ctor>(); }`) before the /// vtable call; an effect with none keeps today's behaviour (crash on /// NULL `_nova_handler_X` if used without an enclosing `with`) unchanged. default_handler_fns: HashMap<String, String>, /// Plan 173 Ф.5 (#8): struct names that ACTUALLY received the hidden /// `_consume_ccount` field (heap records only). The `emit_fn` prologue /// gates on THIS set (not `consume_cleanup_types`) so the two can never /// desync into a C compile error (field read without field emit). consume_ccount_structs: HashSet<String>, /// `[M-178-consume-field-ctor-from-var]` × D188 v3 (2026-07-13): source /// type name (record, or sum-variant name for record-payload variants) → /// names of its `consume`-marked fields. Populated by an emit_module /// pre-pass (module.items + peer_files). Consulted by /// `collect_reconsume_occurrences_rec` / `emit_expr_with_reconsume_disarm`: /// a guarded re-consume binding appearing as a consume-field init of a /// record literal in tail/return position = sanctioned move-out → the /// block cleanup must be DISARMED at literal construction (mirror of the /// consume-call-argument disarm). record_consume_fields: HashMap<String, HashSet<String>>, /// Companion of `record_consume_fields`: ALL field names per record type / /// record-payload variant — resolves ANONYMOUS record literals /// (`type_name: None`) by unique structural field-set match (mirror of /// checker's `ConsumeRegistry::consume_fields_for_lit`). record_field_names: HashMap<String, HashSet<String>>, /// Plan 209 Ф.1 (A1): multi-TU codegen split flag. When `false` (default), /// ALL top-level definitions (free/method/mono/lambda/thunk/test/bench/eq/ /// supervisor/timeout fns + file-scope globals) keep `static` (internal /// linkage) exactly as before Plan 209 — output is byte-identical to /// pre-209. When `true` (env `NOVA_MULTI_TU=1`), `top_level_storage()` / /// `top_level_storage_inline()` emit `""` instead, promoting those symbols /// to external linkage so a post-finalize splitter (`split_tu`, A2) can /// scatter definitions across N `_partK.c` translation units that all /// `#include` one `_common.h` (declarations only) — see /// docs/plans/209-recon-notes.md §2/§5. Mangled names are already /// CU-unique (D381 collision-aware mangle), so promoting `static` → /// external never collides; `assert_multi_tu_symbol_uniqueness` (A3) /// double-checks this invariant defensively. multi_tu_enabled: bool, } /// Plan 20 Ф.4: per-defer-stmt entry — tracks one `defer { ... }` statement. /// /// Plan 173 Ф.1 (#4): `errdefer`/`okdefer`/`defer |result|` ретрактнуты (D189); /// все defer'ы теперь плейн — бегут на ВСЕХ exit-paths. `DeferKind` и /// path-selective skip-логика удалены как мёртвая поверхность. struct DeferEntry { /// C variable name of the `int` activation flag. Initialized to 0 /// at block start; set to 1 inline at the defer's textual position /// (so partial-init exits run only defers that already executed). active_var: String, /// AST body to re-emit at cleanup point. AST stores defer body as /// arbitrary `Expr` (parser wraps `defer { ... }` in ExprKind::Block). /// Unused (placeholder `UnitLit`) for a consume-flavored entry — its /// cleanup is the synthetic `_consume_cleanup` call driven by `consume_policy`. body: Expr, /// Plan 173 Ф.2.B2 (D314): `defer(o ScopeOutcome) { … }` — имя outcome- /// биндинга. `None` = плейн `defer` (тело эмитится как раньше, byte-identical). /// `Some(name)` = перед телом на каждом exit-path материализуется /// `Nova_ScopeOutcome*` и `#define`'ится как `name` (тело видит исход). outcome_binding: Option<String>, /// Plan 173 Ф.2.B3-merge (D314 §3): `Some` marks this entry as the /// consume-cleanup of a `consume X = e { … }` scope — its cleanup runs /// `Nova_<T>_consume_cleanup(binding, o)` (not a user defer body) plus the /// consume-only policy (cancel-shield leave, ResourceTrace on_resource_exit). /// `None` for every plain `defer` / `defer(o)` entry (unchanged path). consume_policy: Option<ConsumePolicy>, } /// Plan 173 Ф.2.B3-merge (D314 §3): consume-specific policy re-homed onto a /// consume-flavored defer-entry. Carries the data the four defer run-sites need /// to emit the `@cleanup` dispatch + cancel-shield leave + ResourceTrace exit. #[derive(Clone)] struct ConsumePolicy { /// Stripped type name (`Nova_` prefix + trailing `*` removed) — drives the /// pinned cleanup symbol `Nova_<type_name>_consume_cleanup` (R2) and the /// ResourceTrace label. `[§0]` symbol comes from the type name, never hardcoded. type_name: String, /// C variable holding the captured resource (`_consume_<binding>_<id>`), /// passed as the receiver of the cleanup call. c_binding: String, /// C `int64_t` local holding the shield's previous deadline (returned by /// `nv_consume_enter_shield`) — restored by `nv_consume_leave_shield`. prev_deadline_var: String, /// Whether the `ResourceTrace` effect is in scope (emit on_resource_exit). has_resource_trace: bool, /// Plan 173 Ф.5 (#8, D188 R2): C `int` local — genuine RUNTIME /// exactly-once counter for this consume-scope's cleanup dispatch. /// Checked+incremented at the actual `Nova_<T>_consume_cleanup` call /// site (`emit_consume_entry_cleanup`), NOT derived from the structural /// `active_var` flag — a second invocation (bug, or D188-r2-manual-on-exit /// checker bypassed via aliasing) hits `_consume_count >= 1` and panics /// with `D188-on-exit-double-invocation` instead of silently no-op'ing. count_var: String, /// Plan 173 Ф.5 п.2 (D192-ретракт): C `int` local holding the resolved /// 3-level watchdog THRESHOLD (ms) — armed around the cleanup call only /// (`nv_cleanup_watchdog_arm/disarm`) and compared against the measured /// cleanup duration for the ResourceTrace exit-event `overrun` flag. threshold_var: String, /// План 253.4 Ф.1 (решение владельца 2026-08-08): NOVA-имя биндинга, /// которое эта запись сторожит. Обязательное поле конструктора — завести /// consume-запись, не назвав её биндинг, СИНТАКСИЧЕСКИ невозможно (тип /// не построится), поэтому «забыть зарегистрировать» больше нечего: /// регистрации нет, есть сам объект. `None` = владение УШЛО из этого /// кадра насовсем (`spawn`/`detach consume` передаёт биндинг файберу, /// см. `disarm_outer_auto_cleanup_for_fiber_body`) — запись остаётся в /// стеке ради своего run-site'а, но по имени больше не находится. /// Единственный ключ дизарм-резолва (`disarm_var_for`); прежние два /// РЕЕСТРА (`reconsume_scopes`, `auto_cleanup_active`) сняты целиком. nova_binding: Option<String>, /// План 253.4 Ф.1: это Plan-201 re-consume-скоуп (`consume X { … }`)? /// Заменяет прежний реестр `reconsume_scopes` в его ВТОРОЙ роли — /// построении множества «активных guard'ов» для /// `emit_expr_with_reconsume_disarm` (см. `reconsume_active_names`). re_consume: bool, } /// Plan 173.1 Ф.2 (D71): element transport representation over the mono /// (nova_int-slotted) channel in the `parallel for → []T` collection lowering. /// See `Emitter::parfor_chan_repr` for the classification policy. #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum ParforChanRepr { /// Integer scalar ≤ 64 bit — value cast directly into the slot. IntScalar, /// Heap pointer — cast through `intptr_t` (element travels by reference). Pointer, /// Value type (f64 / str / value-record / tuple / Option / Result…) — /// copied into a GC-heap box; the box pointer travels; the drain fiber /// dereferences and pushes the copy. Boxed, } /// Plan 173 Ф.2.B3-merge (D314 §4a): how a consume-cleanup that itself fails /// composes into the enclosing run-site's error transport. enum ConsumeTail { /// FAIL run-site: promote scope fail-frame to PANIC (dominance) or append to /// the suppressed chain (D158) — via `emit_fail_cleanup_compose`. FailChain { failframe: String, chain: String }, /// LEAVE run-site: cleanup-PANIC dominates, else fill the leave-compose slot. LeaveComp { comp_has: String, comp_msg: String, comp_kind: String, comp_payload: String, comp_tid: String, comp_chain: String, }, /// INTERRUPT / early-exit: a cleanup-failure during unwind is dropped (the /// interrupt value / early-exit control-flow must reach its target); the /// cancel-shield has already been left. [M-173-consume-unwind-cleanup-throw] Swallow, } /// Plan 173 Ф.2.B2 (D314): исход scope-exit для материализации `defer(o ScopeOutcome)`. /// Каждый из 4 run-site'ов defer-kernel строит свой вариант. enum DeferOutcome<'a> { /// normal end-of-scope / `return v` → `Success`. Success, /// throw / panic / cancel — выбор по `<frame>.error_kind`: /// PANIC→`Panic(msg)`, CANCEL→`Failure("cancel: "+msg)`, else→`Failure(msg)`. FromFrame(&'a str), /// `interrupt v` → `Failure("interrupt")` (спека core.nv:130 «interrupt→Failure»). Interrupt, } /// Plan 20 Ф.4: per-block defer state. One scope per block that contains /// at least one defer/errdefer. /// /// `needs_failframe` / `failframe_var` / `intframe_var` сейчас не читаются /// напрямую (codegen использует `failframe_popped_var` / `intframe_popped_var` /// + проверки на `entries.iter().any(_)`), но сохраняются как diagnostic /// metadata + точки расширения для будущей debug-инфраструктуры. #[allow(dead_code)] struct DeferScope { /// Unique block ID for naming. block_id: usize, /// All `defer` entries registered in this block, in textual order. /// Cleanup walks this in reverse for LIFO semantics. entries: Vec<DeferEntry>, /// Running index into `entries` — incremented each time emit_stmt /// reaches a `defer` and activates its flag. next_idx: usize, /// Diagnostic-only (Plan 173 Ф.1 #4): fail-frame теперь ставится всегда /// при наличии defer, не по path-selective признаку (коего больше нет). needs_failframe: bool, /// Name of the C NovaFailFrame variable when `needs_failframe`. failframe_var: String, /// Name of the C `int` "fail-frame popped" flag (set to 1 by /// early-exit cleanup so leave_defer_scope skips the second /// `nova_fail_pop()`). Valid when `needs_failframe`. failframe_popped_var: String, /// Plan 20 Ф.8 (2): name of C NovaInterruptFrame variable. Always /// present when any defer is in the block — defers run on interrupt- /// path тоже (D90 п.8). intframe_var: String, /// Plan 20 Ф.8 (2): name of C `int` "interrupt-frame popped" flag /// (set to 1 by early-exit cleanup / interrupt-path handler). intframe_popped_var: String, /// `true` if this scope is loop-body — break/continue stop here /// rather than walking outer scopes. is_loop_body: bool, } /// Plan 209 Ф.1 (A4): result of `CEmitter::emit_module_multi_tu`. See that /// function's doc for the byte-identity guarantee on the `Single` arm. pub enum EmitOutput { /// Multi-TU disabled, or CU under threshold: the single `.c` string, /// byte-identical to what `emit_module` alone would have returned. Single(String), /// Multi-TU enabled AND CU over threshold: one `_common.h` (decl-only) /// + N `_partK.c` bodies (`split_tu`, A2). Split { common_h: String, parts: Vec<String> }, } /// Plan 209 Ф.1 (A4), recon-notes §6: multi-TU only pays for CUs above /// ~2MB of finalized output OR (approximately) ~200 top-level function /// definitions — below that, split+multi-file-link overhead isn't worth /// it and the CU stays a single `.c` even with `NOVA_MULTI_TU=1`. const MULTI_TU_SIZE_THRESHOLD_BYTES: usize = 2 * 1024 * 1024; const MULTI_TU_FN_COUNT_THRESHOLD: usize = 200; /// Plan 209 Ф.1 (A2 §3): target bytes per `_partK.c` once split. /// Plan 209 Ф.4 (замер, `docs/plans/wip/209-f4-measures.md`): было 500КБ (→26 частей на /// conformance mega-CU, 13МБ) — каждая часть инклудит ВЕСЬ `_common.h`, N лишних перепарсов /// огромного заголовка съедали выигрыш параллели. Замер {500КБ, 1.5МБ, 4МБ} на 3 целях /// (aggregator/conformance mega-CU/std-collections) показал 1.5МБ (~7-9 частей) БЫСТРЕЕ обеих /// соседних точек на ОБОИХ multi-part таргетах — conformance 134.14s→108.52s (24→8 частей, /// -19.1%), aggregator 33.25s→23.22s (4→2 части, -30.2%); 4МБ (мало/большие части — 3 части) /// РЕГРЕССИРУЕТ на conformance (266.32s, компилятор-суперлинейность внутри большой TU /// перевешивает экономию на заголовке). std/collections (per-file CU, всегда 1 часть, /// negative-контроль) не показал влияния порога, как и ожидалось. const MULTI_TU_PART_THRESHOLD_BYTES: usize = 1536 * 1024; /// Cheap (single linear scan, no tokenizing) approximation of "is this CU /// big enough to bother splitting?" — exact top-level function counting is /// `split_tu`'s job (which this function deliberately avoids duplicating /// for a CU that may be many MB; that's exactly the cost Plan 209 exists to /// amortize, so the GATE deciding whether to pay it must itself stay cheap). /// `") {"` is a reasonable proxy for "a function signature's closing paren /// immediately followed by its opening brace" — it can only ever /// UNDER-count (a `") {"` inside a string/comment would over-count, but /// none of this codebase's generated top-level text contains that /// particular 3-byte sequence inside a literal/comment in practice) or /// slightly over/under vs. the true count; either way it only feeds a /// coarse "> 200" threshold decision, never correctness. fn exceeds_multi_tu_threshold(finalized: &str) -> bool { if finalized.len() > MULTI_TU_SIZE_THRESHOLD_BYTES { return true; } finalized.matches(") {").count() > MULTI_TU_FN_COUNT_THRESHOLD } impl CEmitter { pub fn new() -> Self { Self { out: String::new(), deferred_impls: String::new(), lambda_forward_decls: String::new(), user_type_fwd_decls: String::new(), value_record_defs_buf: String::new(), pending_value_nodes: Vec::new(), lambda_impls: String::new(), indent: 0, unsafe_depth: 0, tmp_counter: 0, handler_counter: 0, protocol_lit_counter: 0, spawn_counter: 0, blocking_counter: 0, detach_counter: 0, supervised_counter: 0, current_scope_queue: None, current_spawn_captures: None, current_spawn_capture_by_value: None, var_types: HashMap::new(), current_fn_param_typerefs: HashMap::new(), proto_unify_depth: std::cell::Cell::new(0), consume_reuse_spans: HashSet::new(), ref_params: std::collections::HashSet::new(), mut_param_names: std::collections::HashSet::new(), closure_param_type_overrides: RefCell::new(HashMap::new()), type_subst_overrides: RefCell::new(HashMap::new()), pattern_binding_overrides: RefCell::new(HashMap::new()), var_mutable: HashSet::new(), hoisted_let_vars: HashSet::new(), protocol_vars: HashMap::new(), result_type_params: HashMap::new(), fn_result_type_params: HashMap::new(), fn_protocol_params: HashMap::new(), fn_any_params: HashMap::new(), protocol_method_registry: HashMap::new(), protocol_decl_file: HashMap::new(), protocol_var_vtable: HashMap::new(), emitted_vtable_types: HashSet::new(), emitted_vtable_instances: HashSet::new(), record_schemas: HashMap::new(), named_tuple_field_defaults: HashMap::new(), value_record_names: HashSet::new(), sum_schemas: HashMap::new(), colliding_type_names: HashSet::new(), colliding_fn_names: HashSet::new(), emit_file_module: HashMap::new(), file_type_module: HashMap::new(), file_priv_type_c_names: HashMap::new(), being_defined_sum_types: HashSet::new(), being_defined_record_types: HashSet::new(), effect_schemas: HashMap::new(), method_receivers: HashMap::new(), array_ext_static_c_base: HashMap::new(), array_ext_static_generic_fn: HashMap::new(), method_overloads: BTreeMap::new(), method_overload_types: HashSet::new(), never_returning_methods: HashSet::new(), external_registry: super::external_registry::ExternalRegistry::load_builtins() .expect("failed to load std/runtime/*.nv (Plan 13 Ф.8)"), c_literal_extern_fns: HashSet::new(), embed_fields: BTreeMap::new(), all_methods: HashSet::new(), all_method_recv_types: HashSet::new(), iter_returns: HashMap::new(), from_targets: HashMap::new(), tuple_element_types: HashMap::new(), record_variant_field_types: HashMap::new(), record_variant_field_order: HashMap::new(), record_field_order: HashMap::new(), pending_container_eq_monos: std::cell::RefCell::new(Vec::new()), container_eq_requested: std::cell::RefCell::new(HashSet::new()), current_fn_return_ty: None, current_fn_name: None, current_fn_returns_protocol: None, current_fn_returns_any: false, contracts_post_label: None, ghost_vars: std::collections::HashSet::new(), proven_contracts: std::collections::HashSet::new(), proven_index_sites: std::collections::HashSet::new(), proven_index_sites_contract: std::collections::HashSet::new(), resolved_types: std::collections::HashMap::new(), pattern_variant_types: std::collections::HashMap::new(), resolved_callees: std::collections::HashMap::new(), node_substs: std::collections::HashMap::new(), fn_ret_by_span: std::collections::HashMap::new(), proven_overflow_sites: std::collections::HashSet::new(), proven_overflow_sites_contract: std::collections::HashSet::new(), contracts_mode: crate::ast::ContractsMode::Checked, record_invariants: HashMap::new(), debug_contracts_erased: std::cell::Cell::new(0), array_element_types: HashMap::new(), option_inner_types: HashMap::new(), pending_option_inner_type: None, result_ok_inner_types: HashMap::new(), pending_result_ok_inner_type: None, fn_result_ok_inner_types: HashMap::new(), str_box_arrays: HashSet::new(), current_receiver_type: None, current_receiver_rt: None, in_recv_ptr_return_position: std::cell::Cell::new(false), current_receiver_is_mut: false, current_receiver_is_static: false, expected_record_type: None, expected_sum_hint: None, expected_option_elem_hint: None, expected_into_target: None, current_array_elem_hint: None, current_array_protocol_box: None, synthesized_default_methods: HashSet::new(), synthesizing_default_methods: HashSet::new(), type_impl_protocols: HashMap::new(), type_set_members: HashMap::new(), fn_param_sigs: HashMap::new(), fn_newtype_sigs: HashMap::new(), unanno_light_clos: HashMap::new(), array_param_fn_sigs: HashMap::new(), user_fn_sigs: HashMap::new(), free_fn_ret_by_arity: HashMap::new(), hof_param_fn_sigs: HashMap::new(), user_fn_variadic: HashSet::new(), suppress_variadic_routing: false, emitted_fn_thunks: HashSet::new(), lazy_consts: HashSet::new(), pending_const_inits: Vec::new(), record_field_fn_sigs: HashMap::new(), trailing_block_counter: 0, lambda_counter: 0, fn_returns_fn_sig: HashMap::new(), generic_fns: HashSet::new(), generic_types: HashSet::new(), protocol_types: HashSet::new(), opaque_ffi_types: HashSet::new(), generic_fn_tuple_arity: HashMap::new(), type_aliases: HashMap::new(), current_parfor_send: None, annotation_source: None, source_file_name: "<unknown>".to_string(), annotation_enabled: false, // Plan 14 Ф.1: NovaOpt_<T> lazy-decl. Pre-populated с T-ками, // которые `nova_rt/array.h` уже декларирует через NOVA_ARRAY_DECL. // Для прочих — typedef эмитится в novaopt_typedefs_buf и // splice'ится через маркер /*__NOVAOPT_TYPEDEFS__*/. novaopt_typedefs_buf: std::cell::RefCell::new(String::new()), novaopt_early_gen: std::cell::RefCell::new(false), novaopt_vr_typedefs_buf: std::cell::RefCell::new(String::new()), novaopt_eq_fns_buf: std::cell::RefCell::new(String::new()), vr_ueq_protos_buf: std::cell::RefCell::new(String::new()), novaopt_decls_seen: { let mut s = std::collections::HashSet::new(); s.insert("nova_int".to_string()); // Plan 70.3: nova_char NovaOpt declared в array.h — skip lazy. s.insert("nova_char".to_string()); s.insert("nova_byte".to_string()); s.insert("nova_bool".to_string()); s.insert("nova_str".to_string()); s.insert("nova_f64".to_string()); // Plan 70.4: nova_f32 NovaOpt declared в array.h — skip lazy. s.insert("nova_f32".to_string()); // Plan 70.4 Ф.2: sized-int NovaOpt declared in array.h — skip lazy. for t in ["int32_t", "int16_t", "int8_t", "uint32_t", "uint16_t", "uint64_t"] { s.insert(t.to_string()); } std::cell::RefCell::new(s) }, struct_eq_stack: std::cell::RefCell::new(Vec::new()), struct_eq_fn_requested: std::cell::RefCell::new(std::collections::HashSet::new()), struct_eq_protos_buf: std::cell::RefCell::new(String::new()), pending_structural_eq_bodies: std::cell::RefCell::new(Vec::new()), // Plan 54 Ф.9: pre-populated primitive sanitized → c_ty // pairs (для них sanitized совпадает с c_ty). // Plan 70.3: nova_char added — runtime declares NovaOpt_nova_char // в array.h; lazy emit must skip чтобы не redefine. novaopt_value_types: { let mut m = std::collections::HashMap::new(); for t in ["nova_int", "nova_char", "nova_byte", "nova_bool", "nova_str", "nova_f64", "nova_f32"] { m.insert(t.to_string(), t.to_string()); } // Plan 70.4 Ф.2: sized-int types. for t in ["int32_t", "int16_t", "int8_t", "uint32_t", "uint16_t", "uint64_t"] { m.insert(t.to_string(), t.to_string()); } std::cell::RefCell::new(m) }, mono_tuple_instances: std::cell::RefCell::new(std::collections::HashSet::new()), mono_fixed_array_instances: std::cell::RefCell::new(std::collections::HashSet::new()), legacy_tuple_arities: std::cell::RefCell::new(std::collections::BTreeSet::new()), novares_typedefs_buf: std::cell::RefCell::new(String::new()), novares_vr_typedefs_buf: std::cell::RefCell::new(String::new()), novares_decls_seen: std::cell::RefCell::new(std::collections::HashSet::new()), novares_value_types: std::cell::RefCell::new(std::collections::HashMap::new()), defer_scopes: Vec::new(), defer_block_counter: 0, auto_cleanup_types: HashSet::new(), auto_cleanup_arm_sites: HashMap::new(), free_fn_consume_param_positions: HashMap::new(), method_consume_param_positions: HashMap::new(), consume_receiver_methods: HashMap::new(), zero_on_move_types: HashMap::new(), loop_body_has_scope: Vec::new(), var_boxed: HashMap::new(), detach_box_hoist: None, warnings: std::cell::RefCell::new(Vec::new()), strict_errors: std::cell::RefCell::new(Vec::new()), interned_str_literals: HashMap::new(), interned_str_emit: Vec::new(), interned_str_syms: std::collections::HashSet::new(), interned_blob_literals: HashMap::new(), interned_blob_emit: Vec::new(), interned_blob_syms: std::collections::HashSet::new(), blob_sidecar_dir: None, imported_modules: HashSet::new(), fn_module_map: HashMap::new(), private_const_c_names: HashMap::new(), colliding_const_names: HashSet::new(), const_qualified_by_name: HashMap::new(), file_priv_fn_c_names: HashMap::new(), file_priv_free_fn_decls: HashMap::new(), current_emit_file_id: None, mono_fn_decls: HashMap::new(), free_fn_inout_params: HashMap::new(), value_struct_field_tys: HashMap::new(), free_fn_byref_params: HashMap::new(), method_byref_params: HashMap::new(), mono_method_decls: HashMap::new(), mono_method_decls_by_span: HashMap::new(), self_method_decls: HashMap::new(), mono_worklist: Vec::new(), mono_instantiated: HashSet::new(), mono_method_fndecl_for_name: HashMap::new(), current_type_subst: HashMap::new(), current_method_turbofish: Vec::new(), current_fail_e_hint: None, mono_fwd_decls: String::new(), extern_fn_tuple_protos: String::new(), generic_type_templates: HashMap::new(), generic_type_worklist: std::cell::RefCell::new(Vec::new()), emitted_generic_type_instances: HashSet::new(), generic_type_methods: HashMap::new(), builtin_sum_type_params: HashMap::new(), builtin_sum_method_fwd_decls: String::new(), generic_type_defs_buf: String::new(), last_generic_record_instance: None, generic_type_instance_info: std::cell::RefCell::new(HashMap::new()), // Plan 48 Ф.7.6: NOVA_MONO_DEPTH env var still honored as a // fallback; CLI `--mono-depth=N` overrides via set_mono_depth_limit. mono_depth_limit: std::env::var("NOVA_MONO_DEPTH").ok() .and_then(|s| s.parse::<usize>().ok()) .filter(|n| *n > 0) .unwrap_or(500), cancel_token_t_map: HashMap::new(), emitted_cancel_converters: HashSet::new(), bench_mode: false, /* Plan 61 Ф.1: TypeId registry; starts at USER_BASE = 17 * (1..16 reserved для primitives). */ type_id_registry: HashMap::new(), next_type_id: 17, any_typeinfos: std::collections::BTreeMap::new(), fail_e_map: HashMap::new(), per_e_fail_types: HashSet::new(), // Plan 62.A.bis Ф.1: registry populated через // init_hardcoded_baseline() — 4 entries (Option, NovaOpt_nova_int // alias, Result, RuntimeError). Phase 1 invariant: ничего не // читает (кроме unit tests), legacy sum_schemas остаётся source // of truth для всех dispatch points. sum_schema_registry: { let mut r = super::sum_schema_registry::SumSchemaRegistry::new(); r.init_hardcoded_baseline(); r }, in_realtime: false, in_blocking: false, // Plan 127 Ф.2: escape analysis result populated на emit_module // entry. None при direct construction (unit tests). escape_result: None, current_fn_id: None, promoted_value_record_locals: HashSet::new(), promoted_primitive_locals: HashSet::new(), pending_value_record_heap_promote: None, sret_out_dest: None, sret_out_dest_struct: None, sret_fns: HashMap::new(), sret_fn_out: None, sret_variant_emit: false, // Plan 143.2: default empty/unpopulated → KEEP everything until // emit_module runs the pre-pass. preempt_keep: crate::codegen::preempt_keep::PreemptKeepSet::default(), consume_cleanup_types: HashSet::new(), consume_cleanup_declared_types: HashSet::new(), default_handler_fns: HashMap::new(), consume_ccount_structs: HashSet::new(), record_consume_fields: HashMap::new(), record_field_names: HashMap::new(), // Plan 209 Ф.6: переворот умолчания ОТКАЧЕН 2026-08-11 тем же днём. // Флаг снова opt-in — но уже не «просто не взведён», а ЗАБЛОКИРОВАН // измерением: с `NOVA_MULTI_TU=1` мега-CU одного файла не // линкуется (`undefined symbol: nova_fn_T_reflect`) — реестр №577. // Пока линковка не починена, включать по умолчанию нельзя. multi_tu_enabled: std::env::var("NOVA_MULTI_TU") .map(|v| v == "1" || v.eq_ignore_ascii_case("true")) .unwrap_or(false), } } /// Plan 209 Ф.1 (A1): storage-class prefix for a top-level `static` /// definition/global (function body or object-with-initializer). Returns /// `"static "` when multi-TU split is disabled (default — byte-identical /// to pre-209 output) or `""` when enabled (promotes to external linkage /// so `split_tu`, A2, may place the definition in a different `_partK.c` /// than its call sites). Do NOT use for `static inline` header-style /// helpers meant to be duplicated verbatim per-TU via `_common.h` /// inclusion (e.g. `nova_typeid_user_name`, per-E throw fast-path) — those /// stay hardcoded `"static inline "` always (see 209-recon-notes.md §2). #[inline] fn top_level_storage(&self) -> &'static str { if self.multi_tu_enabled { "" } else { "static " } } /// Plan 209 Ф.1 (A1): variant of `top_level_storage()` for definitions /// currently written `static inline` that are NOT meant to be duplicated /// per-TU (e.g. `nova_opt_eq_*`, once/lazy property methods, sum-variant /// constructors) — recon-notes §4 routes their BODIES into exactly one /// `_partK.c` (promoted, external), not `_common.h`. `inline` is dropped /// too when promoting (mixing `extern`+`inline` C linkage across TUs is /// its own can of worms; plain external is simplest and correct here). #[inline] fn top_level_storage_inline(&self) -> &'static str { if self.multi_tu_enabled { "" } else { "static inline " } } /// Plan 61 followup #4: register an E type для per-E Fail dispatch. /// Triggered by emit_with (Fail[E] binding) и emit_throw (typed payload). /// Idempotent. После register'ации, preamble splice эмитит per-E /// infrastructure для этого E. fn register_fail_e_type(&mut self, e_c: &str) { if e_c == "nova_str" || e_c == "void*" || e_c.is_empty() { return; /* legacy string или unknown — никакого per-E */ } // Plan 61 followup #4: skip primitives — per-E dispatch только для // user-defined types. Throw для primitives (`throw 42` Fail[int]) // идёт через erased nova_throw_typed (which использует NOVA_TID_nova_int // not NOVA_TID_USER_*). if Self::primitive_type_id(e_c).is_some() { return; } // Auto-register TypeId если ещё нет. let _ = self.debt_typeid_macro_for(e_c); self.per_e_fail_types.insert(e_c.to_string()); } /// [M-exp-promotion-blockers: toml] (Plan 172.13): reverse the mono-name /// "sanitized pointer" marker. A pointer-typed `T*` cannot appear literally /// inside a generated C identifier (`*` isn't ident-safe), so mono struct /// names encode it as a trailing `_p` instead — e.g. `Option[ParseTomlError]` /// (a heap sum-type, C type `Nova_ParseTomlError*`) mono-names to /// `NovaOpt_Nova_ParseTomlError_p` (see `debt_mangled_has_nested_placeholder`'s /// "sanitized-pointer marker" comment for the same convention elsewhere). /// Some call sites extract the element/inner segment from such a mono name /// to use it AS a real C type (e.g. `Option[T].unwrap()`'s inferred return /// type) — those must reverse the marker back into `T*`, or the emitted C /// references a type that was never declared (`T_p` isn't a real type — /// CC-FAIL "unknown type name"). Other call sites re-use the extracted /// segment to build ANOTHER mangled identifier (e.g. a per-T trampoline /// name) — those must NOT reverse it, and do not call this helper. fn debt_unmangle_ptr_suffix(mangled: &str) -> String { match mangled.strip_suffix("_p") { Some(base) if !base.is_empty() => format!("{}*", base), _ => mangled.to_string(), } } /// Plan 61 followup #4: consistent mangling для per-E identifiers /// (vtable / TLS slot / throw entry / adapter). Mirrors debt_typeid_macro_for /// чтобы NOVA_TID_USER_<X> matched name. fn debt_per_e_mangle(c_type: &str) -> String { let base = c_type.trim_end_matches('*').trim(); let base_unwrapped = base.strip_prefix("Nova_").unwrap_or(base); Self::sanitize_c_for_ident(base_unwrapped) } /// Plan 61 followup #4: helper для emit per-E vtable + TLS slot + /// dispatcher для each registered E. Вызывается в finalize. fn render_per_e_fail_decls(&self) -> String { if self.per_e_fail_types.is_empty() { return String::new(); } let mut out = String::new(); out.push_str("/* Plan 61 followup #4: per-E Fail dispatch infrastructure.\n"); out.push_str(" * Per-(E) vtable + TLS slot + fast-path throw entry. Backward compat:\n"); out.push_str(" * legacy _nova_handler_Fail (nova_str) тоже install'тся в emit_with через\n"); out.push_str(" * adapter wrapper — current handler arm body unchanged. */\n"); let mut entries: Vec<&String> = self.per_e_fail_types.iter().collect(); entries.sort(); for e_c in &entries { let mangled = Self::debt_per_e_mangle(e_c); let e_arg: String = if e_c.ends_with('*') { (**e_c).clone() } else { format!("{}*", e_c) }; // Используем line-by-line push_str чтобы избежать invalid // escape sequences (`\<spaces>` в Rust string literal). out.push_str(&format!("typedef struct NovaVtable_Fail_{m} {{\n", m = mangled)); out.push_str(" void* ctx;\n"); out.push_str(&format!(" nova_unit (*fail)(void* ctx, {ea} err);\n", ea = e_arg)); out.push_str(&format!(" struct NovaVtable_Fail_{m}* prev;\n", m = mangled)); out.push_str(" struct NovaInterruptFrame* owner_iframe;\n"); out.push_str(&format!("}} NovaVtable_Fail_{m};\n", m = mangled)); // Plan 209 Ф.1: mutable cross-TU TLS state (installer/throw in // different `_partK.c` must observe the SAME slot) — promoted // per recon-notes.md §2 (unlike the throw fast-path below, which // stays `static inline` and is safely duplicated per-TU). out.push_str("#ifdef _MSC_VER\n"); out.push_str(&format!( "__declspec(thread) {storage}NovaVtable_Fail_{m}* _nova_handler_Fail_{m} = NULL;\n", storage = self.top_level_storage(), m = mangled )); out.push_str("#else\n"); out.push_str(&format!( "{storage}__thread NovaVtable_Fail_{m}* _nova_handler_Fail_{m} = NULL;\n", storage = self.top_level_storage(), m = mangled )); out.push_str("#endif\n"); out.push_str("/* Per-E throw entry: prefer per-E slot, fallback на erased nova_throw_typed.\n"); out.push_str(" * Plan 173 Ф.4 #6 (D158 model B): cleanup-unwind bypasses handler dispatch —\n"); out.push_str(" * a failing cleanup composes into the suppressed pocket, не hijack'ит handler. */\n"); out.push_str(&format!( "static inline nova_unit _nova_throw_typed_{m}({ea} payload) {{\n", m = mangled, ea = e_arg )); // Plan 173 Ф.4 #5/#6: stamp the STABLE snapshot BEFORE per-E dispatch // (mirror of Nova_Fail_fail «capture BEFORE dispatch»): the arm usually // `interrupt`s (longjmp) — the erased fallback below never runs, so // without this the fresh throw would neither reset the suppressed // pocket (previous catch's chain would leak into this one) nor expose // the typed identity to interrupt-unwound cleanups. out.push_str(&format!( " nova_last_error_set(nova_str_from_cstr(\"<{m}>\"), NOVA_THROW_USER_TYPED, (void*)payload, NOVA_TID_USER_{m});\n", m = mangled)); out.push_str(&format!( " if (_nova_handler_Fail_{m} && !nova_in_cleanup_unwind()) {{\n", m = mangled)); out.push_str(&format!(" NovaVtable_Fail_{m}* current = _nova_handler_Fail_{m};\n", m = mangled)); out.push_str(" NovaInterruptFrame* saved_if = _nova_current_handler_iframe;\n"); out.push_str(&format!(" _nova_handler_Fail_{m} = current->prev;\n", m = mangled)); out.push_str(" _nova_current_handler_iframe = current->owner_iframe;\n"); out.push_str(" current->fail(current->ctx, payload);\n"); out.push_str(&format!(" _nova_handler_Fail_{m} = current;\n", m = mangled)); out.push_str(" _nova_current_handler_iframe = saved_if;\n"); out.push_str(" }\n"); out.push_str(" /* Fallback к erased path (preserves typed payload в fail-frame). */\n"); out.push_str(" return nova_throw_typed(\n"); out.push_str(&format!(" nova_str_from_cstr(\"<{m}>\"),\n", m = mangled)); out.push_str(&format!(" (void*)payload, NOVA_TID_USER_{m});\n", m = mangled)); out.push_str("}\n\n"); } out } /// Plan 61 Ф.1: Register a TypeId for a user-defined type name. /// Returns the assigned monotonic NovaTypeId (>= NOVA_TID_USER_BASE = 17). /// Idempotent: повторный вызов с тем же name возвращает existing ID. /// /// Primitive типы (nova_int, nova_str, nova_bool, etc.) имеют reserved /// IDs 1..7 и НЕ должны передаваться сюда — для них используется /// `primitive_type_id` lookup helper. fn register_type_id(&mut self, name: &str) -> u32 { if let Some(&id) = self.type_id_registry.get(name) { return id; } let id = self.next_type_id; self.next_type_id += 1; self.type_id_registry.insert(name.to_string(), id); id } /// Plan 61 Ф.1: TypeId для известного primitive C-type, или None если /// не primitive (caller должен `register_type_id`). fn primitive_type_id(c_type: &str) -> Option<u32> { match c_type.trim_end_matches('*').trim() { "nova_int" => Some(1), "nova_str" => Some(2), "nova_bool" => Some(3), "nova_f64" => Some(4), "nova_f32" => Some(5), "nova_byte" => Some(6), "nova_unit" => Some(7), _ => None, } } /// Plan 61 Ф.2: получить NOVA_TID_<name> identifier для given C-type. /// Для primitives — fixed; для user-types — auto-register. /// Возвращает `(macro_name, sanitized_name)` для embed в emitted C. fn debt_typeid_macro_for(&mut self, c_type: &str) -> String { let base = c_type.trim_end_matches('*').trim(); if Self::primitive_type_id(base).is_some() { return format!("NOVA_TID_{}", base); } // Unwrap Nova_ prefix для cleaner macro name let nova_name = base.strip_prefix("Nova_").unwrap_or(base); let sanitized = Self::sanitize_c_for_ident(nova_name); self.register_type_id(&sanitized); format!("NOVA_TID_USER_{}", sanitized) } /// Plan 174.3 (D53/D54 v1): register (idempotent) a per-type `NovaTypeInfo` /// static for boxing `T → any`. Ensures `T`'s TID is registered (Plan 61 /// reuse — NOT a hardcode per type) and records the static so finalize emits /// `static const NovaTypeInfo NOVA_TYPEINFO_<sani> = { NOVA_TID_…, "<name>" };` /// at the `__TYPEID_DEFINES__` splice. Returns the static's C identifier. fn debt_register_any_typeinfo(&mut self, c_type: &str) -> String { let base = c_type.trim_end_matches('*').trim(); let tid_macro = self.debt_typeid_macro_for(c_type); let nova_name = base.strip_prefix("Nova_").unwrap_or(base); let sani = Self::sanitize_c_for_ident(nova_name); self.any_typeinfos .entry(sani.clone()) .or_insert_with(|| (tid_macro, nova_name.to_string())); format!("NOVA_TYPEINFO_{}", sani) } /// Plan 174.3: box a concrete value `value_c` (of C-type `inner_c`) into /// `any` — emits a payload temp then a `nova_any_box(&NOVA_TYPEINFO_…, &tmp, /// sizeof(tmp))` expression (the temp gives an addressable, correctly-aligned /// payload for the memcpy the runtime does). Works uniformly for scalars, /// value-structs (nova_str) and pointers (Nova_X*). fn emit_any_box(&mut self, inner_c: &str, value_c: &str) -> String { let tinfo = self.debt_register_any_typeinfo(inner_c); let tmp = self.fresh_tmp(); self.line(&format!("{} {} = {};", inner_c, tmp, value_c)); format!("nova_any_box(&{}, &{}, sizeof({}))", tinfo, tmp, tmp) } /// Plan 57: enable bench-mode. Activated by `nova bench` CLI. /// emit_main_wrapper будет генерить bench main вместо test/main. pub fn set_bench_mode(&mut self, enable: bool) { self.bench_mode = enable; } /// Plan 48 Ф.7.6: CLI override for the monomorphization-worklist drain /// depth limit. Called from `nova build` / `nova test` / `nova test-build` /// when `--mono-depth=N` is set; takes priority over `NOVA_MONO_DEPTH`. pub fn set_mono_depth_limit(&mut self, n: usize) { if n > 0 { self.mono_depth_limit = n; } } /// Enable source-annotation mode: codegen will insert `/* SRC: ... */` /// comments before each statement showing the originating Nova source. /// On by default since Plan 14 std-fix (нужно для line:col в ошибках); /// SRC-комментарии в C-output контролируются `annotation_enabled` flag. pub fn set_source_for_annotations(&mut self, src: String) { self.annotation_source = Some(src); self.annotation_enabled = true; } /// Plan 210 Ф.8 (Go-паритет+, OPT-IN): directory the blob sidecar `.bin` /// files should be written to (must be the SAME directory the caller /// will write the resulting `.c` file to — C23 `#embed "foo.bin"` /// resolves relative to the including file, same rule as `#include /// "foo.h"`). Caller opts in explicitly (`NOVA_C23_EMBED=1`); when never /// called, `blob_sidecar_dir` stays `None` and `render_interned_blob_literals` /// keeps emitting the existing hex-text array — zero behavior change. pub fn set_blob_sidecar_dir(&mut self, dir: std::path::PathBuf) { self.blob_sidecar_dir = Some(dir); } /// Plan 140.1 Ф.2 (D24/D13 amend): set the source file display name used /// as the `<file>` part of contract/assert violation diagnostics. The /// build driver passes the originating `.nv` path (file name preferred — /// keeps the prefix short and click-to-line friendly). When not set the /// emitter falls back to `"<unknown>"`. pub fn set_source_file_name(&mut self, name: String) { self.source_file_name = name; } /// Plan 140.1 Ф.2: compute the `(file, line)` location for a contract / /// assert violation diagnostic from a byte-offset `span_start`. `file` /// is the configured `source_file_name`; `line` is resolved from the /// annotation source (1-based) or `0` when no source is available. The /// returned `file` is already C-escaped so it can be dropped straight /// into an emitted string literal. fn loc_for_span(&self, span_start: usize) -> (String, usize) { let line = match &self.annotation_source { Some(src) => crate::diag::byte_to_line_col(src, span_start).0, None => 0, }; (Self::escape_c_str(&self.source_file_name), line) } /// [P67-LEGACY→honest-error] (окно p401b-p67-class, реестр 221.1 №401, /// "ПЕРЕОТКРЫТ"): terminal fallback for the legacy `infer_call_ret_c` / /// `infer_expr_c_type_legacy` dispatch chain — reached when the checker /// failed to annotate an expr's C-type (`resolved_types`/ /// `resolved_callees`, compiler-conventions.md §0) AND none of the legacy /// re-derive heuristics ahead of the call site matched either. /// /// Every one of these sites used to be a raw `panic!("[P67-LEGACY] ...")`, /// caught by the CLI's global panic hook (`nova-cli/src/main.rs`) and /// printed as `nova: internal error at ... / This is a bug in nova. /// Please report it.` — misleading (the single reproduced carrier for /// this class, `Io.write_out("")` bound to a `ro` without `import /// std.io`, is a missing import, not a compiler defect) AND, in a /// multi-file batch run (`nova test <dir>`), fatal to the WHOLE process /// via `std::process::exit(101)` in that same hook — one file's gap kills /// every file queued after it (реестр 221.1 №401, "цена"). /// /// This prints a real compile-error diagnostic instead: a stable /// `[E_CODEGEN_TYPE_UNKNOWN]` code (pinnable by a neg-fixture, /// test-conventions.md rule 5) plus a `file:line:col` location when /// `annotation_source` is available, and exits with the ordinary /// compile-error convention (`exit(1)`, no "please report a bug" framing, /// no panic-hook backtrace noise). The user-visible outcome is the same /// as before (this compile unit does not finish), but HONESTLY reported — /// this does NOT fix the underlying §0 gap (the checker still did not /// annotate the expression), it only replaces an internal-error crash /// with a diagnosable stop, which is the explicitly sanctioned outcome /// for the remainder of this class (окно p401b brief, Step 4: "Замена /// паники на честную диагностику с кодом — приемлемый исход"). fn fatal_codegen_type_unknown(&self, detail: &str, span: crate::diag::Span) -> ! { let (line, col) = match &self.annotation_source { Some(src) => crate::diag::byte_to_line_col(src, span.start), None => (0, 0), }; // Окно p401b-p67-class: a genuine `panic!`, NOT `std::process::exit` // — deliberately, so it unwinds and can be caught by // `nova_codegen::test_runner::catch_unit_panic` (wraps both the // `nova test` per-file compile call AND `nova build`'s // `emit_module_multi_tu` call). `process::exit` bypasses unwinding // entirely and cannot be caught by anything, which — verified by // probe — re-broke the exact "one file's gap kills the whole batch" // regression this window exists to close (an EARLIER version of // this helper used `process::exit(1)`; a 4-file batch with one // `E_CODEGEN_TYPE_UNKNOWN` carrier among three good files printed // the diagnostic for the bad file and then produced NO summary line // at all — the good files after it never ran). The panic-hook // banner ("internal error ... please report it") is suppressed // whenever a catcher is active (`catching_panic_active`); when NONE // is active (a panic escaping every catcher — should not happen for // this specific message, kept as defense in depth) the hook still // prints its own generic banner ahead of this message, and the // outer `catch_unwind` in `nova-cli::main::run_catching` exits 101 — // same terminal outcome as any other unanticipated internal panic. panic!( "[E_CODEGEN_TYPE_UNKNOWN] {}\n --> {}:{}:{}\n hint: the type-checker did not annotate this expression's C type (compiler-conventions.md §0) — this is usually a missing `import` for the type/effect used on the left of `.`; if the import is already present, this is a compiler bug, please report it.", detail, self.source_file_name, line, col ); } /// Plan 140.1 Ф.2 (D24 amend): render the optional contract `user_msg` /// argument for `nova_contract_violation`. `None` → `"NULL"` (format A, /// no message); `Some(msg)` → a C-escaped string literal `"<msg>"` /// (format B: `<msg> (<expr>)`). The location is auto-prefixed by the /// runtime — the user message must NOT include it. fn contract_msg_arg(message: &Option<String>) -> String { match message { None => "NULL".to_string(), Some(m) => format!("\"{}\"", Self::escape_c_str(m)), } } /// Plan 140.3 ([M-140.1-message-interpolation]): эмитит ОДИН контракт-чек /// `if (!(cond)) <violation>;`. Статическое сообщение (или его отсутствие) — /// прежний zero-cost one-liner через `nova_contract_violation`. ИНТЕРПОЛИРОВАННОЕ /// (`contract.message_expr` = `InterpolatedStr`) — failure-БЛОК, который строит /// сообщение LAZY (только при провале, внутри `if`) через interp-машинерию и /// маршрутизируется через `nova_contract_violation_dyn` (nova_str user_msg). /// `cond_c` — уже сэмиченное C-условие; `kind_c` ∈ {NOVA_CONTRACT_PRE/_POST/_INV}; /// `raw_src` — НЕэкранированный source контракта для диагностики. fn emit_contract_check( &mut self, cond_c: &str, kind_c: &str, fn_name: &str, raw_src: &str, file_lit: &str, line: usize, message: &Option<String>, message_expr: &Option<Expr>, ) -> Result<(), String> { let esc_src = Self::escape_c_str(raw_src); if let Some(msg_expr) = message_expr { if let ExprKind::InterpolatedStr { parts } = &msg_expr.kind { self.line(&format!("if (!({})) {{", cond_c)); self.indent += 1; // Capture the emitted message-build region so an `ensures` message // can have `result` rewritten to the collected `_nova_result` C var // (mirror of the condition's `substitute_result_var`), letting // `${result}` interpolate the actual return value. For `requires` // (PRE) `result` is illegal, so the region is left untouched. let region_start = self.out.len(); let msg_var = self.emit_interpolated_str(parts)?; if kind_c == "NOVA_CONTRACT_POST" { let region = self.out.split_off(region_start); self.out.push_str(&Self::substitute_result_var_in_code(®ion)); } self.line(&format!( "nova_contract_violation_dyn({}, \"{}\", \"{}\", \"{}\", {}, {});", kind_c, fn_name, esc_src, file_lit, line, msg_var )); self.indent -= 1; self.line("}"); return Ok(()); } } let msg_arg = Self::contract_msg_arg(message); self.line(&format!( "if (!({})) nova_contract_violation({}, \"{}\", \"{}\", \"{}\", {}, {});", cond_c, kind_c, fn_name, esc_src, file_lit, line, msg_arg )); Ok(()) } /// Plan 14 std-fix: выключает SRC-комментарии но оставляет source для /// line:col в codegen-ошибках. Вызывается main.rs когда `--annotate-source` /// не передан. pub fn disable_source_annotations(&mut self) { self.annotation_enabled = false; } /// Plan 33.3 Ф.9.9 (D24): передать список доказанных контрактов от /// VerificationPipeline. Codegen для proven контрактов **не эмитит** /// runtime check даже в debug — true zero-cost для доказанного. /// Key: (fn_name, contract span start byte offset). pub fn set_proven_contracts(&mut self, proven: &[(String, crate::diag::Span)]) { self.proven_contracts.clear(); for (name, span) in proven { self.proven_contracts.insert((name.clone(), span.start)); } } /// Plan 140.2 Part B (D257 / B.4): proven Index-сайты (loop/code-based) для /// элизии inline bounds-check (по span.start Index-выражения). pub fn set_proven_index_sites(&mut self, sites: &[crate::diag::Span]) { self.proven_index_sites.clear(); for span in sites { self.proven_index_sites.insert(span.start); } } /// Plan 140.2 followup §2: contract-based proven Index-сайты (доказаны через /// fn-`requires`). Элидируются ТОЛЬКО при включённых контрактах. pub fn set_proven_index_sites_contract(&mut self, sites: &[crate::diag::Span]) { self.proven_index_sites_contract.clear(); for span in sites { self.proven_index_sites_contract.insert(span.start); } } /// Plan 172.1 U.4.1: install the per-Expr resolved-type annotations produced by /// the semantic pass (literal seed from `number_exprs`; checker for U.4.2+). /// Mirrors the `set_proven_*` channel. Codegen reads these via /// `infer_expr_c_type`'s equivalence check (authoritative type source post-U.4.2). pub fn set_resolved_types( &mut self, m: &std::collections::HashMap<crate::ast::ExprId, crate::types::ResolvedType>, ) { self.resolved_types = m.clone(); } pub fn set_pattern_variant_types(&mut self, m: &std::collections::HashMap<Span, String>) { self.pattern_variant_types = m.clone(); } /// Plan 172.1 U.4.3: feed the resolved-callee channel (`ExprId` → chosen callee /// `FnDecl.span`) the checker populated. Mirrors `set_resolved_types`. Codegen reads /// it via the U.4.3 equivalence-assert (stage a: free-fn); later stages make it the /// authoritative dispatch source, deleting the codegen re-resolution (§0). #[cfg_attr(not(debug_assertions), allow(dead_code))] pub fn set_resolved_callees( &mut self, m: &std::collections::HashMap<crate::ast::ExprId, crate::diag::Span>, ) { self.resolved_callees = m.clone(); } /// Plan 196.5 Stage-A: feed the subst-value channel (`ExprId` → ordered /// `(generic-param name → concrete ResolvedType)`) the checker populated. Mirrors /// `set_resolved_callees`. Stage-B1 reads it (`resolve_mono_type_args_ch`). pub fn set_node_substs( &mut self, m: &std::collections::HashMap<crate::ast::ExprId, Vec<(String, crate::types::ResolvedType)>>, ) { self.node_substs = m.clone(); } /// Plan 172.1 U.4.1: lower a checker `ResolvedType` to its C-type string — the /// thin lowering that will replace codegen's re-derivation (`infer_expr_c_type`, /// §0/§1). Reuses the single primitive table `primitive_name_to_c` (§0/§2 — no /// drift). Part 2 handles the types the literal seed produces; the `_` marker /// makes the equivalence-assert fire loudly if a non-literal annotation slips in. #[cfg_attr(not(debug_assertions), allow(dead_code))] /// Plan 172.1 U.4.6: lower the canonical `ResolvedType` (D315) to its C type, the /// SINGLE lowering that will replace `type_ref_to_c` (whose resolve-half duplicates the /// checker, §0-anti-pattern). State-aware (`&self`: mono-subst, schema/template/alias /// registries, receiver) — it reproduces `type_ref_to_c`'s ABI dispatch by reading /// `ResolvedType` fields instead of re-resolving a `TypeRef`, recursing on `ResolvedType`. /// After U.4.6b ALL structural families are mirrored (generic-mono Named / `Array` /// Vec-flip+legacy / `Tuple`) → full parity. Returns `None` ONLY where `type_ref_to_c` /// returns `Err` (`usize`/`isize`/`ptr` removed, `Self`-without-receiver) — the parity /// gate skips `None` (and the matching `Err`). /// Side-effects it shares with `type_ref_to_c` (`register_novaopt_decl`) are idempotent /// (D315 spike), so running both under the gate is safe. NOT yet wired as authoritative /// (no `type_ref_to_c` site flips here — U.4.7/U.4.8); this builds + proves parity. /// /// MVP framing (deliberate): this MIRRORS the legacy C NAME byte-identically — the mangle /// is an ABI contract (one type → one C name everywhere, or it won't link during the /// site migration), NOT a claim the legacy name is "right". Legacy bootstrap imprecisions /// it faithfully mirrors (Option/Result int64-erasure, boxed `NovaArray_nova_int*`, /// concrete-vs-erased by C-string) → a MORE correct C name is deferred to /// `Q-resolved-type-c-name` (spec/open-questions.md): do it AFTER `type_ref_to_c` is gone /// (one source, U.4.8), as separate behavior-change commits (§7 — don't mix with the merge). /// Plan 172.12 A1′ — THE single `String`→`ResolvedType` lift point for /// `current_type_subst` values (§0: one marked debt point, not smeared). A mono /// type-arg C-name has no `ResolvedType` at the subst layer (producers read the /// string-native mono registries), so it is wrapped VERBATIM in `ResolvedType::Raw` /// — no parse, no fabricated structure. Every write into `current_type_subst` of a /// C-string routes through here (directly or via `subst_map_from_c_pairs`). Deleted /// when A1″ makes the producers store a real `ResolvedType`. fn lift_c_name(c: String) -> crate::types::ResolvedType { crate::types::ResolvedType::Raw(c) } /// Plan 172.12 A2b — keep `current_receiver_rt` in lock-step with the string /// `current_receiver_type` after every mutation of the latter. The mono receiver /// instance is string-native at its producers, so the RT carrier holds the /// `ResolvedType::Raw` transitional debt of the decorated C-key (verbatim, through /// the single `lift_c_name` debt point). Called immediately after each assignment /// to `current_receiver_type`, so the two carriers never diverge → the receiver-debt /// helpers reading the RT carrier are byte-identical to the pre-A2b inline parse. fn sync_receiver_rt(&mut self) { self.current_receiver_rt = self.current_receiver_type.clone().map(Self::lift_c_name); } /// Plan 172.12 A1′ — build a `current_type_subst` map from string-pair mono subst /// (`name → C-name`), lifting each value through the single `lift_c_name` debt point. /// Used at the `mem::replace(&mut self.current_type_subst, …)` producer sites whose /// source is a `Vec<(String,String)>` / `HashMap<String,String>` mono subst. fn subst_map_from_c_pairs<I>(pairs: I) -> HashMap<String, crate::types::ResolvedType> where I: IntoIterator<Item = (String, String)>, { pairs.into_iter().map(|(k, v)| (k, Self::lift_c_name(v))).collect() } /// Plan 172.12 A1‴ — build the ordered `Vec<(name, ResolvedType)>` mono subst carried /// by `mono_worklist`, lifting each C-string value through the single `lift_c_name` debt /// point (§0). The worklist carrier is now `ResolvedType`-typed WHOLE (no `String` arm); /// the string producers (call-site mono inference) route here, and the worklist-fed body /// seeders (`emit_monomorphized_fn`/`_method`) load `current_type_subst` directly from the /// RT-typed pairs — byte-identical to the pre-A1‴ `subst_map_from_c_pairs` re-lift, but /// the carrier no longer holds bare C-strings. fn subst_vec_from_c_pairs(pairs: &[(String, String)]) -> Vec<(String, crate::types::ResolvedType)> { pairs.iter().map(|(k, v)| (k.clone(), Self::lift_c_name(v.clone()))).collect() } /// Plan 172.12 A1‴ — lift a `Vec<String>` of already-lowered type-arg C-names into the /// `ResolvedType`-typed form carried by `generic_type_worklist`/`generic_type_instance_info`, /// freezing each C-string through the single `lift_c_name` debt point (§0). Byte-identical /// to the pre-A1‴ stored `Vec<String>` — readers recover the C-name via `subst_val_c`. fn args_lift(type_args_c: &[String]) -> Vec<crate::types::ResolvedType> { type_args_c.iter().map(|c| Self::lift_c_name(c.clone())).collect() } /// Plan 172.12 A1‴ — lower a registry type-arg back to its C-name (`Raw` verbatim, real /// RT via the printer). Mirror of `subst_val_c` for the instance/worklist registries. fn arg_c(&self, rt: &crate::types::ResolvedType) -> String { self.subst_val_c(rt) } /// Plan 172.12 A1″ — structural mono-inference twin of `infer_type_param_binding`, /// operating on a REAL `ResolvedType` argument (from the checker channel /// `resolved_types[ExprId]`, D315) instead of a decomposed C-string. Unifies the /// declared `param_ty` (a `TypeRef` from the fn/ctor/variant declaration) against the /// argument's `ResolvedType`, filling `slots[name] = Some(RT)` for each template /// type-param — PURELY structural (no C-string parse, no registry lookup: the RT /// already carries the full nesting). This is the §0-honest source of subst truth the /// A1′ carrier was widened for; the string `infer_type_param_binding` stays in parallel /// (it feeds the still-string-native registries / `type_args_c`, A1‴), and the /// byte-identity guard in `subst_map_adopt_rt` only ADOPTS an RT slot when it lowers to /// the exact same C-name the string path produced — so a channel-miss / representation /// mismatch degrades to the `Raw` debt, never diverges. fn infer_type_param_binding_rt( &self, param_ty: &crate::ast::TypeRef, arg_rt: &crate::types::ResolvedType, slots: &mut Vec<(String, Option<crate::types::ResolvedType>)>, ) { use crate::types::ResolvedType as R; use crate::ast::TypeRef as TR; // View-transparent on the RT side (mirrors the C-lowering `readonly T` ≡ `T`). let arg_rt = arg_rt.peel_view(); // [M-vec-of-fn-newtype-codegen] (реестр 221.1 №47): the checker types a generic // method body (e.g. `Vec[T]`'s own `@cap`) ONCE, genuinely generically — its // channel RT for an arg built from `T` (e.g. `@data *mut T`) is therefore // `TypedPtr(_, TypeParam("T"))`, naming the ENCLOSING body's OWN still-abstract // template param, never the concrete per-instantiation type (checker has no // per-mono pass to re-check against). At EMIT time, though, we ARE inside one // specific mono'd instantiation, and `current_type_subst` already carries the // REAL substitution for that exact name (Stage-C2 precedent, ~22349, "at EMIT // time current_type_subst already carries exactly that binding") — a hit there is // the SAME "already-established mono truth" signal that precedent trusts, just // read one level deeper (through a pointee, not the call's own turbofish). Redirect // through it before the opacity guard below so this genuinely-resolved substitution // (possibly itself a `Raw` C-name — the bridge's own transitional debt carrier, NOT // a placeholder, see its doc) gets to participate in the structural unification // instead of being read as "still generic". A miss (no entry — truly still-generic/ // erased context) falls through unchanged to the ordinary exclusion. let redirected; let arg_rt: &R = if let R::TypeParam(name) = arg_rt { match self.current_type_subst.get(name.as_str()) { Some(sub) => { redirected = sub.clone(); &redirected } None => arg_rt, } } else { arg_rt }; // Never bind a residual/opaque RT as a "concrete" arg (the string path skips // empty / `void*` for the same reason): a still-unredirected `TypeParam`/`Any`/ // `Unit` arg is not a resolved substitution and would make the printer re-emit an // erased placeholder. `Raw` is deliberately NOT excluded here (unlike the pre- // [M-vec-of-fn-newtype-codegen] version of this guard): the checker channel this // function normally reads from never produces a top-level `Raw` (it is an // EMITTER-side carrier, `Emitter::lift_c_name`-only) — the only way `arg_rt` is // `Raw` at this point is via the redirect just above, where it is BY CONSTRUCTION // an already-resolved mono substitution, never a placeholder. if matches!(arg_rt, R::TypeParam(_) | R::Any | R::Unit) { return; } match param_ty { // Bare `T` → bind `T = arg_rt` (structural counterpart of the string bare-T arm). TR::Named { path, generics, .. } if generics.is_empty() => { let name = path.join("_"); if let Some(slot) = slots.iter_mut().find(|(n, _)| n == &name) { if slot.1.is_none() { slot.1 = Some(arg_rt.clone()); } } } // `[]T` ≡ `Vec[T]` — descend into the element RT (channel carries `Named{Vec,[elem]}` // per `from_type_ref`'s D239 canonicalization; the internal category key `R::Array` // is handled too for robustness). No registry re-parse needed. TR::Array(inner, _) => match arg_rt { R::Named { name, args, .. } if name == "Vec" && args.len() == 1 => { self.infer_type_param_binding_rt(inner, &args[0], slots); } R::Array(elem) => self.infer_type_param_binding_rt(inner, elem, slots), _ => {} }, // Generic named `Base[..]` (Option[T] / Result[T,E] / user Box[T] / HashMap[K,V]): // positionally unify the declared generics against the RT's structural args. TR::Named { generics, .. } if !generics.is_empty() => { if let R::Named { args, .. } = arg_rt { if args.len() == generics.len() { for (g, a) in generics.iter().zip(args.iter()) { self.infer_type_param_binding_rt(g, a, slots); } } } } // [M-vec-of-fn-newtype-codegen] (реестр 221.1 №47) `*T`/`*mut T`/`*unsafe T` // param — structural counterpart of the string Pointer arm (~22945). Unifies // against the arg's `TypedPtr` RT directly, so a pointee whose C-lowering // erases to `void*` (fn-newtype-over-closure, bare closure, `any`) still binds // T to its REAL structural type (e.g. `Named{QH}`) — the string sibling can // only strip a `*` off the ALREADY-erased C-name and gives up the moment the // pointee reads `void*` (indistinguishable, at the string layer, from a // genuinely-unresolved/erased slot). `resolve_mono_type_args`'s RT-fallback // source lowers the bound RT back to C afterwards — that lowering legitimately // produces `void*` for a closure-shaped T, which is now a STRUCTURALLY-earned // answer, not a guessed placeholder. TR::Pointer(inner, _) => { let base = match inner.as_ref() { TR::Mut(ti, _) | TR::Uninit(ti, _) => ti.as_ref(), other => other, }; if let R::TypedPtr(_, pointee) = arg_rt { self.infer_type_param_binding_rt(base, pointee, slots); } } TR::Mut(inner, _) | TR::Uninit(inner, _) => { if let TR::Pointer(p_inner, _) = inner.as_ref() { if let R::TypedPtr(_, pointee) = arg_rt { self.infer_type_param_binding_rt(p_inner, pointee, slots); } } else { self.infer_type_param_binding_rt(inner, arg_rt, slots); } } // fn(..)->T and other shapes: no structural binding (string path skips them too). _ => {} } } /// Plan 172.12 A1″ — channel RT for an argument expression: `resolved_types[ExprId]` /// (checker-annotated, D315), or `None` when the id is unset / the channel misses. fn channel_arg_rt(&self, expr: &crate::ast::Expr) -> Option<crate::types::ResolvedType> { if expr.id.is_set() { self.resolved_types.get(&expr.id).cloned() } else { None } } /// Plan 172.12 A1″ — build the `Vec<(name, Option<RT>)>` slots for a set of /// (declared-param-type, call-arg) pairs by structural channel inference. `slot_names` /// are the template/fn type-params (in order); a slot stays `None` when no arg pins it /// or the channel misses (→ the string binding falls back to the `Raw` debt). fn rt_slots_from_args<'a>( &self, param_tys: impl Iterator<Item = &'a crate::ast::TypeRef>, args: &[crate::ast::CallArg], slot_names: &[String], ) -> Vec<(String, Option<crate::types::ResolvedType>)> { let mut slots: Vec<(String, Option<crate::types::ResolvedType>)> = slot_names.iter().map(|n| (n.clone(), None)).collect(); for (param_ty, arg) in param_tys.zip(args.iter()) { if let Some(rt) = self.channel_arg_rt(arg.expr()) { self.infer_type_param_binding_rt(param_ty, &rt, &mut slots); } } slots } /// Plan 196.5 Stage-B5 — channel-first twin of `rt_slots_from_args`: the SOURCE of the /// slots becomes a SINGLE `node_substs[call_id]` lookup (196.5 §6.4) — the checker /// producers (`f1_check_call`/`resolve_return_channel`, Stage-A) already solved the FULL /// per-call subst map — falling back to the per-arg structural re-derive (the /// `rt_slots_from_args` body) only for names the channel MISSES (no `node_substs` entry /// for this call-site, or the entry omits that name — the §6.2 `ordered.len()== /// generics.len()` completeness gate can legitimately be NARROWER than the legacy /// per-arg engine on residual/erased forms, mirroring `shadow_check_node_substs`'s MISS /// framing — not a divergence). The byte-identity guard in `subst_map_adopt_rt` still /// gates per-key ADOPTION, so a channel/legacy mismatch degrades to the `Raw` debt /// exactly as before — this only changes the CANDIDATE source, never the safety net. /// `NOVA_B5_TRACE` tallies per-call-site hit (all slots resolved from the ONE channel /// lookup, per-arg engine not engaged) vs fallback (channel missing/partial → per-arg /// re-derive engaged for the remainder) for yield measurement. [M-196.5-node-substs] fn rt_slots_from_call<'a>( &self, call_id: crate::ast::ExprId, param_tys: impl Iterator<Item = &'a crate::ast::TypeRef>, args: &[crate::ast::CallArg], slot_names: &[String], ) -> Vec<(String, Option<crate::types::ResolvedType>)> { // ONE authoritative lookup — the channel carries the whole per-call map (§6.2). let channel = self.node_substs.get(&call_id); let mut slots: Vec<(String, Option<crate::types::ResolvedType>)> = slot_names .iter() .map(|n| { let rt = channel .and_then(|c| c.iter().find(|(cn, _)| cn == n)) .map(|(_, rt)| rt.clone()); (n.clone(), rt) }) .collect(); let all_hit = !slots.is_empty() && slots.iter().all(|(_, o)| o.is_some()); // [M-196-closeout, П4] temporary reachability probe (NOVA_B5_MEANINGFUL_TRACE, // NOT NOVA_B5_TRACE — that one fires on every trivial 0-generic call too, drowning // out the signal): only meaningful when slot_names is non-empty AND the channel // left at least one slot `None` (a REAL partial/total miss, not a vacuous // 0-generic "fallback"). let had_real_miss = !slot_names.is_empty() && !all_hit; let pre_fallback_nones: usize = slots.iter().filter(|(_, o)| o.is_none()).count(); if !all_hit { // Transitional fallback (Q9 path): per-arg structural re-derive fills ONLY // still-`None` slots (`infer_type_param_binding_rt` never overwrites a bound // one), so a channel HIT for some names and a MISS for others merges cleanly. for (param_ty, arg) in param_tys.zip(args.iter()) { if let Some(rt) = self.channel_arg_rt(arg.expr()) { self.infer_type_param_binding_rt(param_ty, &rt, &mut slots); } } } if had_real_miss && std::env::var_os("NOVA_B5_MEANINGFUL_TRACE").is_some() { let post_fallback_nones = slots.iter().filter(|(_, o)| o.is_none()).count(); let recovered = pre_fallback_nones.saturating_sub(post_fallback_nones); if recovered > 0 { eprintln!( "[B5-MEANINGFUL] RECOVERED call={:?} names={:?} channel={:?} recovered={}", call_id, slot_names, channel, recovered, ); } } if std::env::var_os("NOVA_B5_TRACE").is_some() { if all_hit { eprintln!("[B5] hit call={:?} names={:?}", call_id, slot_names); } else { eprintln!( "[B5] fallback call={:?} names={:?} channel={:?}", call_id, slot_names, channel, ); } } slots } /// Plan 172.12 A1″ — seed slots (positionally) from explicit turbofish type-args, the /// DECLARATION-`TypeRef` structural source (`obj.method[U]` / `fn[U](...)`): `from_type_ref` /// resolves the written type to a real `ResolvedType` (NO C-name parse), highest priority /// (mirrors the string `resolve_mono_type_args` Source 1). The byte-identity guard in /// `subst_map_adopt_rt` still validates the lowering, so a residual/type-param turbofish /// (rare, nested-generic) safely degrades to `Raw`. fn rt_slots_seed_turbofish( &self, slots: &mut [(String, Option<crate::types::ResolvedType>)], turbofish_refs: &[crate::ast::TypeRef], ) { for (slot, tr) in slots.iter_mut().zip(turbofish_refs.iter()) { if slot.1.is_none() { slot.1 = Some(crate::types::ResolvedType::from_type_ref(tr)); } } } /// Plan 172.12 A1″ — seed a `current_type_subst` map from the string mono subst, /// ADOPTING the structurally-inferred `ResolvedType` for each entry whose RT lowers to /// the EXACT same C-name (byte-identity guard). Entries with no RT slot, a channel-miss, /// or a lowering mismatch keep the `Raw` transitional debt — so the output is /// byte-identical to the pre-A1″ all-`Raw` seeding BY CONSTRUCTION, while every adopted /// entry replaces a `Raw` with real structural type truth (the A1″ goal). The /// `NOVA_A1PP_TRACE` env var tallies adopted-vs-Raw for yield measurement. fn subst_map_adopt_rt( &self, pairs: &[(String, String)], rt_slots: &[(String, Option<crate::types::ResolvedType>)], ) -> HashMap<String, crate::types::ResolvedType> { let trace = std::env::var_os("NOVA_A1PP_TRACE").is_some(); // [M-property-testing-rot] (Plan 172.13 батч 3): the byte-identity // guard below lowers the candidate RT under the CALLER's // `current_type_subst` — so an rt that still MENTIONS one of the slot // names (e.g. `Named{T}` for a nested generic call forwarding the // enclosing mono body's `T`) passes the guard (it lowers to the right // C-name in the caller context) yet would make the ADOPTED map // self-referential (`T → Named{T}`), sending every later lowering of // `T` under the NEW subst into infinite recursion (stack overflow). // Such an rt must degrade to the Raw C-name (self-contained). let slot_names: Vec<&str> = pairs.iter().map(|(k, _)| k.as_str()).collect(); fn mentions_slot(rt: &crate::types::ResolvedType, names: &[&str]) -> bool { use crate::types::ResolvedType as R; match rt { R::TypeParam(n) => names.contains(&n.as_str()), R::Named { name, module, args } => { (module.is_empty() && args.is_empty() && names.contains(&name.as_str())) || args.iter().any(|a| mentions_slot(a, names)) } R::Array(i) | R::Readonly(i) | R::TypedPtr(_, i) => mentions_slot(i, names), R::Tuple(elems) => elems.iter().any(|e| mentions_slot(e, names)), R::Func { params, ret, .. } => { params.iter().any(|p| mentions_slot(p, names)) || mentions_slot(ret, names) } _ => false, } } pairs .iter() .map(|(k, v)| { let adopted = rt_slots .iter() .find(|(n, _)| n == k) .and_then(|(_, o)| o.clone()) .filter(|rt| !mentions_slot(rt, &slot_names)) .filter(|rt| self.resolved_type_to_c(rt).ok().as_deref() == Some(v.as_str())); match adopted { Some(rt) => { if trace { eprintln!("[A1PP] adopt {}={} rt={:?}", k, v, rt); } (k.clone(), rt) } None => { if trace { eprintln!("[A1PP] raw {}={}", k, v); } (k.clone(), Self::lift_c_name(v.clone())) } } }) .collect() } /// Plan 196.5 Stage-A — SHADOW verification (debug-only, read-only, no effect on emitted /// output): where a legacy engine has ALREADY computed its own per-call subst /// (`pairs: &[(name, C-type-string)]`, the SAME map `subst_map_adopt_rt` above adopts /// structural RT into), cross-check it against `node_substs[call_id]` — the checker /// channel (196.5 §6.2) written by `f1_check_call`/`resolve_return_channel`. For every /// name BOTH sides have an opinion on, the channel's `ResolvedType` must lower /// (`resolved_type_to_c`, D315) to the EXACT same C-string the legacy engine derived — /// byte-identity, mirroring the `subst_map_adopt_rt` guard and the 196.4 Stage-1a /// `resolve_return_channel` SHADOW-assert precedent. A MISS (no `node_substs` entry for /// this call-site, or the entry omits this name) is NOT a divergence: the channel's /// completeness gate (§6.2 `ordered.len()==generics.len()`) can legitimately be /// NARROWER than a legacy per-arg engine that tries harder on partial/erased info — only /// a name-for-name MISMATCH is a bug. Stage-A does not read this channel for codegen /// (Stage-B); this only proves the channel's fidelity against the corpus ahead of that. /// [M-196.5-node-substs] #[cfg(debug_assertions)] fn shadow_check_node_substs(&self, call_id: crate::ast::ExprId, pairs: &[(String, String)]) { let Some(channel) = self.node_substs.get(&call_id) else { return }; for (name, legacy_c) in pairs { let Some((_, rt)) = channel.iter().find(|(n, _)| n == name) else { continue }; let channel_c = self.resolved_type_to_c(rt).ok(); debug_assert_eq!( channel_c.as_deref(), Some(legacy_c.as_str()), "[M-196.5-node-substs] SHADOW mismatch: node_substs[{:?}][{}] lowers to {:?}, \ legacy pairs gave {:?}", call_id, name, channel_c, legacy_c, ); } } /// Plan 172.12 A1′ — lower a `current_type_subst` VALUE to its C-name. The debt /// carrier `Raw(s)` returns `s` verbatim (byte-identical to the pre-A1′ stored /// `String`); a real `ResolvedType` (post-A1″) goes through the printer, exactly as /// the task prescribes («читатели, которым нужна C-строка, получают её через принтер»). fn subst_val_c(&self, rt: &crate::types::ResolvedType) -> String { match rt { crate::types::ResolvedType::Raw(s) => s.clone(), other => self.resolved_type_to_c(other).unwrap_or_default(), } } /// Plan 172.12 A1′ — read a `current_type_subst` entry as its C-name (`None` if /// absent). Byte-identical replacement for the pre-A1′ `current_type_subst.get(k)` /// (which returned the stored `&String`). fn subst_c(&self, key: &str) -> Option<String> { self.current_type_subst.get(key).map(|rt| self.subst_val_c(rt)) } /// Plan 172.12 A2 — STRUCTURAL erased-stub test for the printer's concreteness /// decisions (the `Tuple` / `Option` / `Result` / Vec-flip arms). Answers "does /// `rt` lower to a bare `Nova_<param>*` placeholder — what `debt_is_generic_stub_c` /// detects on the lowered C-name — vs a concrete type?" decided from the /// `ResolvedType` STRUCTURE + the concrete-type registries, WITHOUT parsing a /// `Nova_`/`____` C-string in the printer body. `full` selects the registry set: /// `true` mirrors `debt_is_generic_stub_c` (record / sum / being-defined-sum / generic /// / opaque-ffi), `false` the narrower `Option`/`Tuple` inline set (record / sum /// / generic). The residual shapes whose concreteness is STILL carried as a /// C-string at the mono layer — a `Named`/type-param resolved through the string /// `current_type_subst` / overrides / alias table, a module-qualified colliding /// name, `Self`, `TypedPtr`, and the `Raw` transitional debt — route through the /// single marked debt sink `debt_lowered_is_stub`. Those go RT-native (and the /// sink dies) once the subst/registry producers stop freezing C-strings (A2+/A4). fn rt_is_erased_stub(&self, rt: &crate::types::ResolvedType, full: bool) -> bool { use crate::types::ResolvedType as R; match rt { // Primitives / value-erased / functions never lower to `Nova_<param>*`. R::Scalar { .. } | R::Float { .. } | R::Bool | R::Str | R::Unit | R::Never | R::Ptr | R::Any | R::Func { .. } => false, // `readonly T` ABI ≡ `T` — transparent in the C lowering. R::Readonly(inner) => self.rt_is_erased_stub(inner, full), // Arrays (`NovaArray_*` / `Nova_Vec____*`) and tuples (mono struct / // `_NovaTupleN`) always lower to a concrete C-form — never a bare stub. // [M-fixed-array-value-semantics]: `[N]T` mono struct — same, never a stub. R::Array(_) | R::Tuple(_) | R::FixedArray(..) => false, R::Named { name, module, args } => self.rt_named_is_stub(name, module, args, full), // Type-params resolve through the (still string-carried) subst/receiver // context; typed pointers and the `Raw` debt carry concreteness as a // C-name — classify by lowering in the marked debt sink. R::TypeParam(_) | R::TypedPtr(..) | R::Raw(_) => self.debt_lowered_is_stub(rt, full), } } /// Plan 172.12 A2 — the `Named` case of `rt_is_erased_stub`, mirroring the /// `resolved_named_to_c` decision tree structurally (same registries, no /// C-string parse). Names resolved through the string subst / overrides / alias /// table, module-qualified colliding names, and `Self` still carry concreteness /// as a C-string → the debt sink. fn rt_named_is_stub( &self, name: &str, module: &[String], args: &[crate::types::ResolvedType], full: bool, ) -> bool { let full_name = if module.is_empty() { name.to_string() } else { format!("{}_{}", module.join("_"), name) }; let as_rt = || crate::types::ResolvedType::Named { name: name.to_string(), module: module.to_vec(), args: args.to_vec(), }; // Empty-arg names resolved through the string subst / overrides / alias table // (and module-qualified colliding names) carry concreteness as a C-string. if args.is_empty() && (self.type_subst_overrides.borrow().contains_key(&full_name) || self.current_type_subst.contains_key(&full_name) || self.type_aliases.contains_key(&full_name)) { return self.debt_lowered_is_stub(&as_rt(), full); } if self.colliding_type_names.contains(name) { return self.debt_lowered_is_stub(&as_rt(), full); } // Primitives never lower to a bare `Nova_<param>*`. if Self::primitive_name_to_c(&full_name).is_some() { return false; } match full_name.as_str() { // NovaOpt_/NovaRes_/NovaArray_/void*/nova_int/NovaVtable_/NovaCancelToken*/ // Nova_StringBuilder* — none is a bare `Nova_<param>*` stub. // D419 (Plan 152.7.2): `Fmt` mirrors `Write` — hardcoded erasure to a // concrete C type (`Nova_FmtCtx*`), not a generic protocol stub. "Option" | "Result" | "Vec" | "any" | "never" | "Effect" | "CancelToken" | "Write" | "Fmt" | "usize" | "isize" | "ptr" => return false, // `Self` lowers via the (string) receiver context. "Self" => return self.debt_lowered_is_stub(&as_rt(), full), _ => {} } // Protocol (value-erased) → `NovaBox_<proto>` / void*. if self.protocol_types.contains(&full_name) || self.protocol_types.contains(name) { return false; } // Generic template WITH args → mono mangling (`____`) or `NovaValue_` — concrete. if !args.is_empty() && self.generic_type_templates.contains_key(&full_name) { return false; } // Bare generic-template name (empty args → `Nova_<template>*`) or an unknown // name is an erased stub; a concrete user record/sum is not. Same registry // set as `debt_is_generic_stub_c` (gated by `full`), applied structurally. // // [M-option-self-recursive-record-mono] (Plan 186): a type currently // mid-emission (`being_defined_sum_types`/`being_defined_record_types`) // is concrete by construction REGARDLESS of `full` — checked // unconditionally here (unlike `opaque_ffi_types`, still `full`-gated, // unchanged). The bug this closes is exactly a `full=false` call site // (`Option`'s inner-type check in `resolved_named_to_c`) that used to // skip the mid-emission guard entirely, misclassifying a self- // referential `Option[Self]` record field as an erased stub. let concrete = self.record_schemas.contains_key(&full_name) || self.sum_schemas.contains_key(&full_name) || self.generic_types.contains(&full_name) || self.being_defined_sum_types.contains(&full_name) || self.being_defined_record_types.contains(&full_name) || (full && self.opaque_ffi_types.contains(&full_name)); !concrete } /// Plan 172.12 A2 — TRANSITIONAL DEBT sink for `rt_is_erased_stub`: classify a /// residual lowering as an erased generic stub by its C-name. Reached ONLY for /// the shapes whose type truth is still string-carried at the mono layer /// (subst / overrides / alias / colliding / `Self` / `TypedPtr` / `Raw`). This is /// the single marked point where a printer concreteness decision reads a /// C-string; it reproduces the pre-A2 inline check EXACTLY — `debt_is_generic_stub_c` /// for `full`, its narrower record/sum/generic-`____` variant otherwise. Dies /// when the subst/registry producers become RT-native (`Raw` removed). fn debt_lowered_is_stub(&self, rt: &crate::types::ResolvedType, full: bool) -> bool { match self.resolved_type_to_c(rt) { Ok(c) => { if full { self.debt_is_generic_stub_c(&c) } else { match c.trim_end_matches('*').trim().strip_prefix("Nova_") { Some(nm) => { !(self.record_schemas.contains_key(nm) || self.sum_schemas.contains_key(nm) || self.generic_types.contains(nm) || c.contains("____")) } None => false, } } } Err(_) => true, } } /// Plan 172.12 A2b — TRANSITIONAL DEBT: the bare registry key of the current /// method receiver (the `Nova_<X>*` / template-name form used to index /// `generic_type_instance_info`). Reads the RT receiver carrier /// `current_receiver_rt`; for the `Raw` debt value (all instance producers still /// string-native) it strips the `Nova_` decoration + `*` verbatim — the single /// place a receiver key is string-decoded. A real `R::Named` receiver lowers /// structurally through the printer (dormant until producers RT-native). Dies /// with the string receiver carrier (A4). fn debt_receiver_bare(&self) -> Option<String> { use crate::types::ResolvedType as R; let recv = self.current_receiver_rt.as_ref()?; let c = match recv { R::Raw(s) => s.clone(), _ => self.resolved_type_to_c(recv).ok()?, }; Some(c.trim_start_matches("Nova_").trim_end_matches('*').to_string()) } /// Plan 172.12 A2b — TRANSITIONAL DEBT: resolve a type-param `n` from the mono /// RECEIVER instance context (moved out of the `TypeParam` printer arm so /// `resolved_type_to_c` carries no `Nova_`/`____` receiver-key decode). Two /// receiver-carried sources, in the pre-A2b order: /// (1) slice-extension mono (`fn []T @m` receiver = `NovaArray_<elem>`) — the /// element IS the substitution (structural for a real `R::Array`; the /// `NovaArray_<elem>` C-key, non-`____`, for the `Raw` debt value); /// (2) receiver-instance registry: the bare key → `(template, args)` → the arg /// at the position of `n` in the template generics (RT-native since A1‴). /// Returns `None` when the receiver context does not bind `n` (falls through to /// the fn-level subst / erased checks, exactly as before). Byte-identical to the /// pre-A2b inline parse (every receiver value is `Raw(current_receiver_type)`). fn debt_receiver_typeparam(&self, n: &str) -> Option<String> { use crate::types::ResolvedType as R; // (1) slice-extension: element = substitution. match self.current_receiver_rt.as_ref()? { R::Array(elem) => { if let Ok(ec) = self.resolved_type_to_c(elem) { if !ec.is_empty() && !ec.contains("____") { return Some(ec); } } } R::Raw(s) => { let bare = s.trim_start_matches("Nova_").trim_end_matches('*'); if let Some(elem) = bare.strip_prefix("NovaArray_") { if !elem.is_empty() && !elem.contains("____") { return Some(elem.to_string()); } } } _ => {} } // (2) receiver-instance registry lookup. let bare = self.debt_receiver_bare()?; let info = { let m = self.generic_type_instance_info.borrow(); m.get(&bare) .or_else(|| m.get(&format!("Nova_{}", bare))) .cloned() }; if let Some((tmpl, args)) = info { if let Some(td) = self.generic_type_templates.get(&tmpl) { if let Some(pos) = td.generics.iter().position(|g| g.name == *n) { if let Some(c) = args.get(pos) { // A1‴: registry arg is `ResolvedType` — lower to C-name. return Some(self.arg_c(c)); } } } } None } /// Plan 172.12 A2b — TRANSITIONAL DEBT: is the receiver an ERASED template /// context for type-param `n` (receiver = the template name `[]T` / a generic /// template carrying `n` at an empty subst)? The erased body is emitted ONCE with /// int-erasure — `nova_int` is the truth of the erased representation, not a guess. /// Reads the RT receiver carrier via `debt_receiver_bare`; byte-identical to the /// pre-A2b inline `current_receiver_type` parse. fn debt_receiver_erased(&self, n: &str) -> bool { let Some(bare) = self.debt_receiver_bare() else { return false; }; bare == format!("[]{}", n) || (self.generic_type_templates.contains_key(&bare) && self .generic_type_templates .get(&bare) .map_or(false, |td| td.generics.iter().any(|g| g.name == *n))) } // ---- Plan 172.12 A4 — consumer-side debt sinks ------------------------------- // Extracted VERBATIM from their call sites (pure extract-function, zero logic // change → byte-identical by construction). Each classifies/derives from an // ALREADY-LOWERED C-name string (`elem_c`/`c_ty`/mangled name) — the value itself // was produced upstream by `type_ref_to_c`/`resolved_type_to_c`/mono registries, // not decoded from user input. A structural (RT-native) replacement needs the // call site to carry a `ResolvedType` alongside the C-string (many don't, by // signature) — that plumbing is out of A4's mechanical-ripple scope (would touch // caller signatures transitively, A1′-substrate scale). Marked here as the single // named point of remaining string-based decision, per §0. /// `emit_module` forward-decl scan: skip a `Vec[elem]` fwd-decl only for a genuine /// unresolved type-param stub (`Nova_K*`), not a concrete mono instance (which also /// contains `____` but IS concrete — e.g. `Nova_Vec____nova_int*` from `[][]int`). fn debt_skip_array_fwd_decl(&self, elem_c: &str) -> bool { self.debt_is_generic_stub_c(elem_c) && !elem_c.contains("____") } /// `Nova_<Name>*` → `<Name>` (repeated-prefix `trim_start_matches` + `*`-trim). fn debt_strip_nova_trim_start(&self, c_ty: &str) -> String { c_ty.trim_start_matches("Nova_").trim_end_matches('*').trim().to_string() } /// Same idiom, borrowed (no alloc) — for call sites that only need a `&str`. fn debt_strip_nova_trim_start_ref(c_ty: &str) -> &str { c_ty.trim_start_matches("Nova_").trim_end_matches('*').trim() } /// Repeated-prefix `trim_start_matches("Nova_")` only (no `*`-trim) — the /// receiver-short-name idiom used by the `@minus` overload disambiguation. fn debt_strip_nova_trim_start_bare(s: &str) -> String { s.trim_start_matches("Nova_").to_string() } /// Is `c` a bare (non-mono) heap struct pointer `Nova_<Name>*` — concrete, so /// `fn_ret_by_span` may safely cache it (a `____` mono name is excluded here /// because its distinct-mono identity needs the full mono-context path, not /// this early forward-decl cache). fn debt_is_bare_nova_ptr(&self, c: &str) -> bool { c.starts_with("Nova_") && c.ends_with('*') && !c.contains("____") } /// Is lowered type-arg `c` a bare unresolved type-param placeholder (`Nova_<G>*` /// where `<G>` is one of the template's own generic names) — used to skip /// emitting a mono instance that would reference a non-existent placeholder /// struct (recursive generic-call context, no concrete arg yet). fn debt_type_arg_is_bare_placeholder(&self, c: &str, generic_names: &std::collections::HashSet<String>) -> bool { let trimmed = c.trim_end_matches('*').trim(); trimmed.strip_prefix("Nova_") .map(|name| generic_names.contains(name)) .unwrap_or(false) } /// `drain_generic_type_worklist`: is registry-arg `c` (RT, `arg_c`-lowered here) /// a bare OR (for `value` outer templates) nested unresolved type-param /// placeholder — see call site for the by-value-embedding rationale. fn debt_worklist_arg_is_placeholder( &self, c: &crate::types::ResolvedType, generic_names: &std::collections::HashSet<String>, outer_is_value: bool, ) -> bool { // A1‴: registry args carry `ResolvedType` — lower to the C-name // (`Raw` verbatim) before the placeholder string-checks. let c = self.arg_c(c); let trimmed = c.trim_end_matches('*').trim(); if let Some(name) = trimmed.strip_prefix("Nova_") { if generic_names.contains(name) { return true; } } outer_is_value && self.debt_mangled_has_nested_placeholder(trimmed) } /// `emit_value_record_type`: restrict a forward-typedef to GUARANTEED struct /// tags (mono `____` instance, `NovaValue_`, `NovaTuple_`) so a non-struct /// newtype alias typedef is never shadowed. /// `emit_bchk_double_array_access`: physical elem storage type held in a /// `NovaArray_<elem>`'s `data[]` — `nova_int` for an erased nested array. /// `emit_generic_method_erased`/`emit_fn` (erased-body fn-typed-param sigs, /// Plan 70 Cat B intentional erasure): normalize an unknown `Nova_<X>*` (X not /// a registered record/sum/generic — i.e. still an unresolved type-param at /// erased-emission time) to `nova_int` for consistent pointer-stomping. /// `register_mono_method_instance`/`emit_monomorphized_method`: bind nested /// `Self` for a generic MONO instance receiver (`recv_type` carries `____`) so /// the fwd-decl and body agree on `Self`'s mangled arg (else the emitted /// method's return/param mono diverges from the call-site temp). No-op for /// non-mono/heap-generic/primitive receivers (`value_aware_generic_c_type` /// leaves those unchanged); `.or_insert` so a pre-existing `Self` binding wins. fn debt_bind_self_for_mono_recv(&mut self, recv_type: &str) { if recv_type.contains("____") { let self_c = self.value_aware_generic_c_type(&format!("Nova_{}*", recv_type)); self.current_type_subst.entry("Self".to_string()).or_insert_with(|| Self::lift_c_name(self_c)); } } /// Strip a single leading `Nova_` prefix, or return `s` unchanged if absent /// (mangled-name base-name extraction — same idiom repeated at ~10 call sites). fn debt_strip_nova_prefix(s: &str) -> &str { s.strip_prefix("Nova_").unwrap_or(s) } /// Strip a single leading `Nova_` prefix, or `""` if absent (base-name /// extraction where a non-`Nova_` input is treated as "no match" upstream). fn debt_strip_nova_prefix_or_empty(s: &str) -> &str { s.strip_prefix("Nova_").unwrap_or("") } /// `Option`-returning single-strip of a `Nova_`/`NovaArray_` prefix — thin /// named wrapper so the decode textually lives in one debt-marked spot /// instead of ad-hoc at each dispatcher call site. fn debt_strip_nova_prefix_opt(s: &str) -> Option<&str> { s.strip_prefix("Nova_") } fn debt_strip_novaarray_prefix_opt(s: &str) -> Option<&str> { s.strip_prefix("NovaArray_") } fn debt_strip_novatuple_prefix_or_empty(s: &str) -> &str { s.strip_prefix("NovaTuple_").unwrap_or("") } /// `NovaArray_<elem>` → `<elem>`, panicking (P67-LEGACY loud-fail convention) /// if the input isn't actually a `NovaArray_` name — same textual template /// repeated at ~9 dispatcher call sites. fn debt_strip_novaarray_prefix_or_panic(s: &str) -> &str { s.strip_prefix("NovaArray_").unwrap_or_else(|| panic!("[P67] nova_int collapse")) } // [196.5 Stage-D волна-3] `debt_strip_novaarray_prefix_or_panic_legacy` // REMOVED — its sole caller was the B11x_novaarray_methods legacy arm // (removed same wave, byte-identity-verified duplicate of the channel/ // Vec-unified inference). The non-legacy twin above stays (dispatcher // call sites). /// `NovaValue_<X>` / `Nova_<X>` / `NovaTuple_<X>`, first match wins, identity /// fallback — the by-value/heap/named-tuple receiver-prefix priority order /// repeated at several dispatcher call sites. fn debt_strip_value_nova_tuple_prefix(s: &str) -> &str { s.strip_prefix("NovaValue_") .or_else(|| s.strip_prefix("Nova_")) .or_else(|| s.strip_prefix("NovaTuple_")) .unwrap_or(s) } /// Same priority order, `""` fallback (empty = "no match" upstream). fn debt_strip_nova_value_tuple_prefix_or_empty(s: &str) -> &str { s.strip_prefix("Nova_") .or_else(|| s.strip_prefix("NovaValue_")) .or_else(|| s.strip_prefix("NovaTuple_")) .unwrap_or("") } /// Is `s` the `____` mono-instance arg separator — the single shared textual /// test for "is this a concrete monomorphized name" repeated at many sites. fn debt_contains_mono_sep(s: &str) -> bool { s.contains("____") } /// `NovaValue_<X>` (by-value generic) or `Nova_<X>` (heap generic), first /// match wins — order matters (`NovaValue_` embeds `Nova_` as a substring at /// a DIFFERENT position, so trying `Nova_` first would mis-parse it). fn debt_strip_value_or_nova_prefix_opt(s: &str) -> Option<&str> { s.strip_prefix("NovaValue_").or_else(|| s.strip_prefix("Nova_")) } /// By-value generic value-record receiver (`NovaValue_<short>`) — strip /// THAT prefix first (single `strip_prefix`); else fall back to the (repeated) /// `Nova_` strip. Preserves the asymmetric strip semantics of the original /// inline call sites exactly (single-strip vs `trim_start_matches` repeat). fn debt_strip_value_prefix_or_nova_trim_start(s: &str) -> String { s.strip_prefix("NovaValue_") .map(|s| s.trim_end_matches('*').trim().to_string()) .unwrap_or_else(|| s.trim_start_matches("Nova_").trim_end_matches('*').trim().to_string()) } // №248/№145 §0: `resolved_types[id]`'s bare NON-generic name, `record_schemas`-validated. fn channel_named_type(&self, id: crate::ast::ExprId) -> Option<String> { self.resolved_types.get(&id).and_then(|rt| if let crate::types::ResolvedType::Named { name, args, .. } = rt { args.is_empty().then(|| name.clone()) } else { None }).filter(|n| self.record_schemas.contains_key(n)) } /// `Nova_<X>` or `NovaValue_<X>`, first match wins (opposite priority from /// `debt_strip_value_or_nova_prefix_opt` — preserved verbatim per call site). fn debt_strip_nova_or_value_prefix(s: &str) -> &str { s.strip_prefix("Nova_").or_else(|| s.strip_prefix("NovaValue_")).unwrap_or(s) } /// `register_novaopt_decl`/`register_novaopt_decl_forced` forward-typedef /// polluting-check: only mono'd (`____`-bearing) `Nova_`-prefixed names need /// the guard (nova_int/nova_str/runtime-defined structs are excluded). /// /// [221.1 №250/№251] `NovaRes_<ok>_<err>` also needs it: its full body /// splices at `/*__NOVARES_TYPEDEFS__*/`, textually AFTER this buffer's own /// `/*__NOVAOPT_TYPEDEFS__*/` — a `NovaOpt_` pointer-wrap of a non-canonical /// pair (anything but the ONE `array.h`-hardcoded `(nova_int, nova_str)`) /// referenced the struct before its declaration. Harmless for the canonical /// pair too (redundant compatible C11 typedef redecl, 6.7p3). fn debt_is_mono_nova_name(inner_name: &str) -> bool { (Self::debt_contains_mono_sep(inner_name) && inner_name.starts_with("Nova_")) || inner_name.starts_with("NovaRes_") } /// Plan 91.12 V2 / Plan 173.3 (D415 §2 detach-migration fix, 2026-07-11): /// pointer-handle sync primitives whose `type X(*())`/`type X[T](*())` /// declaration is a THIN Nova-level wrapper over a C struct/typedef that /// already lives hand-written in `nova_rt/sync_*.h` (`Condvar`/`OnceCell`/ /// `Lazy` since Plan 91.12; the `#share`-carrying `Atomic*`/`Mutex`/… /// family added by 173.3). Codegen must NEVER emit its own competing /// `typedef struct Nova_<X> Nova_<X>;` for these names — not from /// `emit_type_decl`'s own `Newtype` arm (already guarded, see below), and /// NOT as an incidental forward-declare when one of these types is used /// as a FIELD elsewhere (`emit_record_type`'s pointer-field pre-pass) — /// the runtime header's typedef is a different underlying representation /// (found via [M-atomicint-record-field-typedef-collision]: `struct /// Nova_AtomicInt Nova_AtomicInt;` forward-decl collided with the /// existing `#include`d `nova_rt` typedef — "typedef redefinition with /// different types"). Single source of truth for the name list (was /// duplicated inline at two `emit_type_decl` match arms). fn debt_is_runtime_backed_newtype(name: &str) -> bool { const RUNTIME_BACKED_NEWTYPES: &[&str] = &[ "OnceCell", "Lazy", "Condvar", "Mutex", "RwLock", "ReentrantMutex", "WaitGroup", "Once", "Barrier", "CountDownLatch", "Semaphore", "AtomicI64", "AtomicI32", "AtomicI16", "AtomicI8", "AtomicU64", "AtomicU32", "AtomicU16", "AtomicU8", // Plan 207 (2026-07-16 consolidation): AtomicInt absorbs the // former Isize spelling AND the former int32-backed legacy // AtomicInt (both removed as separate names); AtomicUint // absorbs the former Usize spelling; AtomicPtr removed // (int-proxy duplicate, no generic [T] yet — Plan 103.7). "AtomicInt", "AtomicUint", "AtomicBool", ]; RUNTIME_BACKED_NEWTYPES.contains(&name) } /// Same idiom as `debt_strip_nova_trim_start`, without the extra `.trim()` /// (byte-identical here since C-type strings never carry surrounding /// whitespace, but kept distinct to avoid ANY behavioral assumption). fn debt_strip_nova_trim_start_no_ws(&self, c_ty: &str) -> String { c_ty.trim_start_matches("Nova_").trim_end_matches('*').to_string() } fn debt_erase_unknown_nova_ptr(&self, c: String) -> String { if let Some(inner) = c.strip_prefix("Nova_").and_then(|s| s.strip_suffix('*')) { let name = inner.trim(); if !self.record_schemas.contains_key(name) && !self.sum_schemas.contains_key(name) && !self.generic_types.contains(name) { return "nova_int".to_string(); } } c } fn debt_novaarray_elem_storage(inner_arr_ty: &str) -> String { inner_arr_ty .strip_prefix("NovaArray_") .map(|s| s.trim_end_matches('*').trim().to_string()) .unwrap_or_else(|| "nova_int".to_string()) } fn debt_is_guaranteed_struct_tag(&self, pointee: &str) -> bool { // Plan 248 (wave 3, D447 #no_copy): the 11 value-inside atomics are // `NovaValue_`-prefixed like an ordinary user value-record, but // their struct is hand-written in sync_primitives.h as an ANONYMOUS // struct (`typedef struct { ... } NovaValue_AtomicI64;`, no tag) — // the SAME shape the `_NovaTuple2`/mono-tuple exclusion below // documents. A `typedef struct NovaValue_AtomicI64 NovaValue_AtomicI64;` // forward-decl (as the generic `NovaValue_` prefix match just below // would otherwise emit for a by-pointer field, e.g. `TcpStream.rc // *mut AtomicInt`) declares a DIFFERENT, tagged type under the same // name — "typedef redefinition with different types" — never // needed anyway, since the header's own typedef already precedes // every reference (it's `#include`d first). if matches!(pointee, "NovaValue_AtomicI64" | "NovaValue_AtomicI32" | "NovaValue_AtomicI16" | "NovaValue_AtomicI8" | "NovaValue_AtomicU64" | "NovaValue_AtomicU32" | "NovaValue_AtomicU16" | "NovaValue_AtomicU8" | "NovaValue_AtomicInt" | "NovaValue_AtomicUint" | "NovaValue_AtomicBool") { return false; } pointee.contains("____") || pointee.starts_with("NovaValue_") || pointee.starts_with("NovaTuple_") // [реестр 221.1 №139 Round 3] Positional MONO tuple/fixed-array // tags (`_NovaTuple_2_..`/`_NovaFixArr_..` — NOTE the trailing // underscore: `_NovaTuple_` deliberately excludes the LEGACY // all-int erased form `_NovaTupleN`, e.g. `_NovaTuple2` — THAT // one is `typedef struct { ... } _NovaTuple2;`, an ANONYMOUS // struct with no tag at all; forward-declaring `typedef struct // _NovaTuple2 _NovaTuple2;` for it is a real, different type — // "typedef redefinition with different types", caught by // building this exact fixture) were MISSING here — harmless // historically because mono tuple/fixarr always rendered at an // early, fixed marker position (before anything that could // reference them by pointer), so a forward-decl was never // actually needed in practice. Now that they join the unified // topo-sort (`render_unified_value_types`) alongside value- // records/generic-instances, a pointer-referencing node (e.g. a // Vec-mono's `_NovaTuple_..* data`) can legitimately sort BEFORE // its pointee (pointer refs are not edges — only a forward-decl // is required, order between them is free) — this pre-pass is // what supplies that forward-decl. || pointee.starts_with("_NovaTuple_") || pointee.starts_with("_NovaFixArr_") } fn resolved_type_to_c(&self, rt: &crate::types::ResolvedType) -> Result<String, String> { use crate::types::ResolvedType as R; use crate::ast::PointerModifier as PM; Ok(match rt { R::Scalar { .. } => { let name = rt.int_name().expect("Scalar always yields an int_name"); Self::primitive_name_to_c(name).expect("int primitive always in table").to_string() } R::Float { width } => { let name = if *width == 32 { "f32" } else { "f64" }; Self::primitive_name_to_c(name).unwrap().to_string() } R::Bool => Self::primitive_name_to_c("bool").unwrap().to_string(), R::Str => Self::primitive_name_to_c("str").unwrap().to_string(), R::Unit => "nova_unit".to_string(), // 172.1.2 Шаг 1: residual generic-параметр — подстановка из mono-контекста // (overrides → current_type_subst); промах = Err → channel-miss → legacy. // НИКОГДА не Nova_T*/nova_int (§1: Err-вместо-лжи). Erased-тела (пустой // subst) автоматически получают Err → erased-поведение сохранено. R::TypeParam(n) => { // RECEIVER-instance mapping ПЕРВЫМ: для параметров УРОВНЯ ТИПА // (T у D119Box[T]) источник правды — сам эмитируемый инстанс // (generic_type_instance_info), НЕ fn-level subst: имена могут // коллидировать с method-level параметрами (gap #2, пойман // d119: cast (nova_str)(T-поле) при mono map[U=str]). // Plan 172.12 A2b: the receiver-key normalization (slice-extension // `NovaArray_<elem>` + `generic_type_instance_info` lookup) is read // from the RT receiver carrier `current_receiver_rt` in the marked // receiver-debt helper — no `Nova_`/`____` decode in the printer body. if let Some(c) = self.debt_receiver_typeparam(n) { return Ok(c); } if std::env::var_os("NOVA_TP_TRACE").is_some() { eprintln!("[TP] n={} recv={:?} ov={:?} subst={:?}", n, self.current_receiver_type, self.type_subst_overrides.borrow().get(n.as_str()), self.current_type_subst.get(n.as_str())); } if let Some(c) = self.type_subst_overrides.borrow().get(n.as_str()) { return Ok(c.clone()); } if let Some(c) = self.subst_c(n.as_str()) { return Ok(c); } // ERASED-контекст (2026-07-04): ресивер = ШАБЛОННОЕ имя ("[]T", // "Set") при пустом subst — тело эмитится ОДИН раз с int-erasure // (документированный erased-контракт legacy) → nova_int это // ПРАВДА представления erased-тела, не guess. // Plan 172.12 A2b: read from the RT receiver carrier in the marked // receiver-debt helper — no `Nova_` decode in the printer body. if self.debt_receiver_erased(n) { return Ok("nova_int".to_string()); } return Err(format!("unsubstituted type-param `{}`", n)); } // `never` — bottom-type ABI placeholder (mirrors type_ref_to_c `never` arm). R::Never => "nova_int".to_string(), // Opaque `ptr` (Plan 115) — only from the NullPtrLit literal seed; not a // `type_ref_to_c` parity input (no TypeRef lowers to it). R::Ptr => "void*".to_string(), // `any` — value-erased (mirrors type_ref_to_c anon-`Protocol` → void*). R::Any => "void*".to_string(), // U.5.5(a): L2 `readonly` view transparent for C (`readonly T` ABI ≡ `T`). R::Readonly(inner) => self.resolved_type_to_c(inner)?, // L3 typed pointer (mirrors type_ref_to_c Pointer arm): `*() = void*`; // `*ro T → const T*`; `*mut T`/`*unsafe T → T*`. R::TypedPtr(modifier, inner) => { if matches!(inner.as_ref(), R::Unit) { "void*".to_string() } else { let inner_c = self.resolved_type_to_c(inner)?; match modifier { PM::Ro => format!("const {}*", inner_c), PM::Mut | PM::Uninit => format!("{}*", inner_c), } } } // Function type — opaque void* (mirrors type_ref_to_c Func arm). R::Func { .. } => "void*".to_string(), // Plan 172.12 A1′ — transitional debt carrier: a `current_type_subst` mono // type-arg that is still a C-string (see `ResolvedType::Raw`). Printed VERBATIM // (the C-name IS the value) — the round-trip `lift_c_name(c)` → `Raw(c)` → // `resolved_type_to_c` = `c` guarantees byte-identity while the subst carrier // is `ResolvedType`-typed. Removed when A1″ populates real `ResolvedType`. R::Raw(s) => s.clone(), R::Named { name, module, args } => self.resolved_named_to_c(name, module, args)?, // Array `[]T` (D239 ≡ Vec[T]) — mirrors type_ref_to_c's Array arm. R::Array(inner) => self.resolved_array_to_c(inner)?, // [M-fixed-array-value-semantics] (2026-07-10, D27-амендмент): `[N]T` — // INLINE value mono struct `{ T data[N]; }`, NOT the `[]T`/Vec heap-pointer // path above. `register_mono_fixed_array` is idempotent (dedup by (N, elem)). R::FixedArray(n, inner) => { let elem_c = self.resolved_type_to_c(inner)?; self.register_mono_fixed_array(*n, &elem_c) } // Tuple — mono'd struct if all elems concrete, else legacy `_NovaTupleN` // (Plan 59/148). Mirrors type_ref_to_c's Tuple arm; `register_mono_tuple` / // `register_legacy_tuple` side-effects are shared + idempotent. R::Tuple(elems) => { let mut elem_cs: Vec<String> = Vec::with_capacity(elems.len()); let mut all_concrete = true; for e in elems { match self.resolved_type_to_c(e) { Ok(c) => { // Plan 172.12 A2: concreteness decided from the element // `ResolvedType` structure (narrow set), not by re-parsing // the lowered C-name for a `Nova_<param>*`/`____`. if self.rt_is_erased_stub(e, false) { all_concrete = false; break; } elem_cs.push(c); } Err(_) => { all_concrete = false; break; } } } if all_concrete && !elem_cs.is_empty() { self.register_mono_tuple(&elem_cs) } else { self.register_legacy_tuple(elems.len()) } } }) } /// U.4.6b: lower `[]T` (D239 ≡ `Vec[T]`) — mirrors `type_ref_to_c`'s Array arm driven by /// the element `ResolvedType`. Closure-array → `NovaArray_void_p*`; Vec-flip to the /// `Nova_Vec____<elem>*` mono when the `Vec` template is present (worklist + instance-info /// side-effects, idempotent); else the legacy primitive-keyed `NovaArray_<prim>*` (the /// `elem_key` reconstructed from the `ResolvedType`: `Scalar`/`Float`/`Str`/`Bool` → /// primitive name, `Named` type-param/user → `current_type_subst` back-map, exactly as the /// legacy `Named{path:[..]}` path). Always `Ok` (Array never errors, like type_ref_to_c — /// an un-lowerable element falls through to the legacy primitive-keyed fallback). fn resolved_array_to_c(&self, inner: &crate::types::ResolvedType) -> Result<String, String> { use crate::types::ResolvedType as R; // Closure-array `[]fn(...)` → NovaArray_void_p* ([M-138.1-closure-array]). if matches!(inner, R::Func { .. }) { return Ok("NovaArray_void_p*".to_string()); } // D239 `[]T ≡ Vec[T]`: Vec-flip, unconditional (Plan 172.12 A8 — the old // `generic_type_templates.contains_key("Vec")` outer gate is gone: it used // to skip straight to a primitive-keyed `NovaArray_<prim>*` legacy-runtime // name whenever the Vec template wasn't registered yet, WITHOUT even // trying the general resolver. That legacy runtime (`array.h` // NOVA_ARRAY_DECL/IMPL) is retired this pass, so there is no NovaArray // name left to fall back to — and the general resolver doesn't actually // need the Vec template to lower a concrete element type, so gating on // it was never load-bearing for the common case. // // Empirically confirmed reachable (2026-07-08, probe): a CU whose // `#prelude(...)` opts out of `collections` (so the Vec generic template // is never merged in) while still using bare `[]<primitive>` hits this // path — a real, if niche, corner of the D62.F partial-prelude feature, // tracked separately as [M-partial-prelude-primitive-method-registry]. // Previously it silently built legacy `NovaArray_<T>*` (which happened to // work by layout-compatible luck); now it always attempts the Vec[T] mono // — if the Vec template genuinely isn't registered anywhere in the CU, the // struct never gets emitted (`drain_generic_type_worklist` skips unknown // base names gracefully) and referencing `Nova_Vec____<T>*` fails at the // C-compile stage with a clear "unknown type name" diagnostic instead of // silently emitting now-deleted runtime calls. let elem_c: String = if let Ok(mut c) = self.resolved_type_to_c(inner) { // int64-erasure of an UNRESOLVED type-param element (mirror legacy). // Plan 172.12 A2: the stub test is structural on `inner` (full // registry set), not a `Nova_`/`____` parse of the lowered `c`. if self.rt_is_erased_stub(inner, true) { c = "nova_int".to_string(); } c } else { // The general resolver couldn't lower `inner` at all (genuinely // unresolved nested type-param). Narrower structural fallback: // `elem_key` reconstructed from the ResolvedType (type_ref_to_c keys // off the bare `Named{path:[name]}`; in RT primitives are // Scalar/Float/Str/Bool, `char`/user/type-param are Named) — then // mapped to the same concrete C element names the Vec mono expects // (was: `NovaArray_<prim>*` legacy-runtime name, pre-A8). let elem_key: &str = match inner { R::Str => "str", R::Bool => "bool", R::Float { width: 32 } => "f32", R::Float { .. } => "f64", R::Scalar { .. } => inner.int_name().unwrap_or("int"), R::Named { name, module, args } if module.is_empty() && args.is_empty() => { // type-param / user type: subst back-map (mirror legacy current_type_subst). match self.subst_c(name).as_deref() { Some("nova_str") => "str", Some("nova_byte") | Some("uint8_t") => "u8", Some("nova_bool") => "bool", Some("nova_f64") | Some("float") | Some("double") => "f64", Some("nova_f32") => "f32", Some("int32_t") => "i32", Some("int16_t") => "i16", Some("int8_t") => "i8", Some("uint32_t") => "u32", Some("uint16_t") => "u16", Some("uint64_t") => "u64", Some("nova_char") => "char", _ => name.as_str(), // user type / unmapped → nova_int erasure } } _ => "", // non-bare-Named inner → nova_int erasure }; match elem_key { "str" => "nova_str", "u8" => "nova_byte", "bool" => "nova_bool", "f64" => "nova_f64", "f32" => "nova_f32", "char" => "nova_char", "i32" => "int32_t", "i16" => "int16_t", "i8" => "int8_t", "i64" => "nova_int", // i64 == nova_int "u32" => "uint32_t", "u16" => "uint16_t", "u64" => "uint64_t", "uint" => "uint64_t", _ => "nova_int", } .to_string() }; let type_args_c = vec![elem_c]; let mangled = Self::compute_generic_type_c_name("Vec", &type_args_c); if !self.emitted_generic_type_instances.contains(&mangled) { let mut wl = self.generic_type_worklist.borrow_mut(); if !wl.iter().any(|(_, _, m)| m == &mangled) { wl.push(("Vec".to_string(), Self::args_lift(&type_args_c), mangled.clone())); } } self.generic_type_instance_info .borrow_mut() .entry(mangled.clone()) .or_insert_with(|| ("Vec".to_string(), Self::args_lift(&type_args_c))); Ok(format!("{}*", mangled)) } /// U.4.6: the `Named` ABI dispatch of `resolved_type_to_c`, mirroring `type_ref_to_c`'s /// `Named` arm driven by `ResolvedType` fields. `full` = `module ++ [name]` joined by /// `_` (the `path.join("_")` equivalent, U.5.5(a) made `module` lossless). /// [M-sync-crossmodule-samename-type-collision] (D381) — module-qualified /// C base for a colliding user type; `name` unchanged for every other type /// (byte-identical). `module` must be the DEFINING module path. /// [M-198-f4c-1-privfile-type-not-discriminated]: true when EITHER type- /// collision map is non-empty (D381 cross-module `colliding_type_names`, /// or this fixture's same-module per-file `file_priv_type_c_names`). Every /// `current_emit_file_id`-setting gate below used to check only /// `colliding_type_names` — a CU with ONLY a same-module priv(file) type /// collision (no cross-module collision) never set `current_emit_file_id` /// during forward-decl / type-def / mono-body emission, so `ref_type_base` /// (which reads `current_emit_file_id`) never saw a chance to resolve the /// per-file name — bare `Nova_<Name>` kept leaking into signatures. Both /// maps are empty for the overwhelming common case → byte-identical. fn any_type_file_collision(&self) -> bool { !self.colliding_type_names.is_empty() || !self.file_priv_type_c_names.is_empty() } fn qualify_type_base(&self, name: &str, module: &[String]) -> String { if self.colliding_type_names.contains(name) && !module.is_empty() { format!("{}_{}", module.join("_"), name) } else { name.to_string() } } /// DEFINITION-side base: a TypeDecl's C base, qualified by its own defining /// module (via `file_id`) when the simple name collides. Non-colliding → /// bare `name` (byte-identical). Used by every type-DEFINITION emitter so /// the struct/tag/ctor/schema key of a colliding type is unique per module. fn def_type_base(&self, name: &str, file_id: crate::diag::FileId) -> String { // [M-198-f4c-1-privfile-type-not-discriminated]: same-module, per-FILE // collision (checked first — narrower than the cross-module D381 check // below, and `colliding_type_names` never includes these same-module // names since D381's own collision pass only compares DISTINCT // modules). if let Some(mangled) = self.file_priv_type_c_names.get(&(file_id, name.to_string())) { return mangled.clone(); } if !self.colliding_type_names.contains(name) { return name.to_string(); } match self.emit_file_module.get(&file_id) { Some(m) if !m.is_empty() => self.qualify_type_base(name, m), _ => name.to_string(), } } /// REFERENCE-side base: resolve a (possibly bare) colliding type reference to /// its qualified C base using the CURRENT emission file's view. Falls back to /// the syntactic `module` (or bare `name`) when the collision cannot be /// resolved (non-colliding names always return bare `name`, byte-identical). fn ref_type_base(&self, name: &str, syntactic_module: &[String]) -> String { // [M-198-f4c-1-privfile-type-not-discriminated]: same-module, per-FILE // collision — checked first, via the CURRENT emission file (a // reference to a `priv(file) type` can only legally occur in its own // declaring file — resolver enforces this — so `current_emit_file_id` // always matches the declaring file_id when set). if let Some(fid) = self.current_emit_file_id { if let Some(mangled) = self.file_priv_type_c_names.get(&(fid, name.to_string())) { return mangled.clone(); } } if !self.colliding_type_names.contains(name) { return name.to_string(); } // Prefer the current file's resolution (handles bare same/cross-module // references); this is unambiguous within a file (checker invariant). if let Some(fid) = self.current_emit_file_id { if let Some(m) = self.file_type_module.get(&(fid, name.to_string())) { return self.qualify_type_base(name, m); } } // Fall back to any syntactic qualifier the reference carried. if !syntactic_module.is_empty() { return self.qualify_type_base(name, syntactic_module); } name.to_string() } /// [M-sync-crossmodule…] (D381): resolve a BARE variant name to its sum, /// disambiguating a variant SHARED across colliding sums (`Other` in three /// `ErrorKind`s) by the current call-site context (the fn's expected return /// sum). Byte-identical for a UNIQUE variant: a single candidate falls /// straight through to `find_variant_compat` (the legacy first-wins), so no /// non-colliding program changes. Returns the disambiguated sum base + its /// variant field-C-types, or `None` if the name is not a variant. fn debt_find_variant_ctx(&self, variant: &str, argc: Option<usize>) -> Option<(String, Vec<String>)> { // Only disambiguate among PLAIN (non-generic-mono) sums. A mono instance // name carries the `____` type-arg separator; the generic path owns its // ctor emission + instance queuing, so overriding to a mono base here // would emit a call WITHOUT queuing the instance (undefined symbol). The // collision this targets is between plain non-generic sums (three // `ErrorKind`s), so this filter loses nothing for the intended case. let plain: Vec<String> = self .sum_schema_registry .variant_sum_candidates(variant) .into_iter() .filter(|c| !c.contains("____")) .collect(); if plain.len() >= 2 { let field_count = |sum: &str| -> Option<usize> { self.sum_schema_registry .lookup_sum_schema(sum) .and_then(|e| e.variants.iter().find(|v| v.variant_name == variant)) .map(|v| v.field_c_types.len()) }; let build = |sum: String| -> Option<(String, Vec<String>)> { self.sum_schema_registry.lookup_sum_schema(&sum).and_then(|e| { e.variants .iter() .find(|v| v.variant_name == variant) .map(|v| (sum.clone(), v.field_c_types.clone())) }) }; // (1) Arity filter: `InvalidData(msg)` (1 arg) vs a same-named UNIT // variant in another colliding sum — the payload count disambiguates. // A single arity match wins outright. if let Some(n) = argc { let by_arity: Vec<&String> = plain.iter().filter(|s| field_count(s) == Some(n)).collect(); if by_arity.len() == 1 { return build(by_arity[0].clone()); } // (2) Among the arity-matching subset: prefer the field/param- // local `expected_sum_hint` (M-178) — MORE precise than the // whole function's return sum, it survives into a NESTED bare // ctor call (`Net(e)` inside `Some(Net(e))`) that the return- // sum heuristic below can't see (the enclosing fn may return // an unrelated struct that merely CONTAINS an `Option[Sum]` // field). Falls back to the enclosing fn's return sum (handles // `Other(x)` — 1-arg in every ErrorKind — by the enclosing // fn's return category). if by_arity.len() >= 2 { if let Some(hint) = self.expected_sum_hint.as_ref() { if by_arity.iter().any(|c| *c == hint) { return build(hint.clone()); } } if let Some(base) = self.debt_current_fn_return_sum() { if by_arity.iter().any(|c| **c == base) { return build(base); } } } } // (3) Arity-agnostic context fallback: same priority (hint before // fn-return-sum) as (2) above. if let Some(hint) = self.expected_sum_hint.as_ref() { if plain.iter().any(|c| c == hint) { return build(hint.clone()); } } if let Some(base) = self.debt_current_fn_return_sum() { if plain.iter().any(|c| *c == base) { return build(base); } } } self.sum_schema_registry.find_variant_compat(variant) } /// [M-codegen-cross-module-ctor-emission] fix: emit a `Sum.Variant(payload)` /// constructor when the explicit receiver names a sum that owns `variant`. /// /// An explicit-receiver payload-variant CALL (`NetError.IoError(msg)`) /// otherwise mis-routes to the static-method dispatch /// (`Nova_NetError_static_IoError`, undefined) because the payload variant is /// ALSO registered in `method_receivers` as a pseudo-static overload of its /// sum (variant-name doubles as a static-method key). Unit variants /// (`NetError.ConnectionReset`) escape this — they are member-access exprs, /// not calls — so only the payload form breaks, and only at the emit-call /// site. A variant belongs unambiguously to its sum; the explicit receiver /// disambiguates perfectly, so the variant constructor wins over any /// same-named static form or same-named type-in-scope (the D381 collision fix /// makes `<Sum>` mangling collision-aware; the ctor prefix mirrors it here). /// /// Returns `Ok(Some(call))` when `recv_type` is a NON-generic sum with a /// variant `variant` of arity `args.len()`, else `Ok(None)` (caller keeps its /// normal dispatch). Generic sums keep their own mono-aware bare-`Ident` /// variant path (arg boxing + instance queuing) — not intercepted here. fn try_emit_explicit_variant_ctor( &mut self, recv_type: &str, variant: &str, args: &[CallArg], ) -> Result<Option<String>, String> { // Collision-aware sum base: mirrors the ctor DEFINITION prefix // (`nova_make_<base>_<variant>`), qualified per module for colliding sums. let sum_base = self.ref_type_base(recv_type, &[]); // Generic sums own their mono ctor path — do not intercept. if self.generic_types.contains(&sum_base) || self.generic_types.contains(recv_type) { return Ok(None); } // Receiver sum must OWN a payload variant of matching arity — need its // field_c_types (not just a bool), see [M-155.a] below (221.1, NOTES.md). let find_fields = |me: &Self, key: &str| -> Option<Vec<String>> { me.sum_schema_registry .lookup_sum_schema(key) .and_then(|e| { e.variants.iter() .find(|v| v.variant_name == variant && v.field_c_types.len() == args.len()) .map(|v| v.field_c_types.clone()) }) }; let field_c_types = match find_fields(self, &sum_base).or_else(|| find_fields(self, recv_type)) { Some(f) => f, None => return Ok(None), }; // [M-155.a-flagship-anon-record-literal-enum-payload] (221.1, rationale // in NOTES.md / commit 2c1edf56c): scope expected_record_type per arg // from the variant's OWN field type — mirrors [M-181] for Ok/Err. let saved_expected = self.expected_record_type.clone(); let mut arg_strs = Vec::with_capacity(args.len()); for (a, fty) in args.iter().zip(field_c_types.iter()) { self.expected_record_type = Self::debt_struct_name_from_c_type(fty); arg_strs.push(self.emit_expr(a.expr())?); } self.expected_record_type = saved_expected; Ok(Some(format!( "nova_make_{}_{}({})", sum_base, variant, arg_strs.join(", ") ))) } /// [M-sync-crossmodule…] (D381): the plain (non-mono) sum base named by the /// current fn's return C-type, if any — used to disambiguate a variant shared /// across colliding sums by call-site context. fn debt_current_fn_return_sum(&self) -> Option<String> { let ret = self.current_fn_return_ty.as_ref()?; let base = ret .strip_prefix("Nova_") .unwrap_or(ret) .trim_end_matches('*') .trim() .to_string(); if base.contains("____") { None } else { Some(base) } } fn resolved_named_to_c( &self, name: &str, module: &[String], args: &[crate::types::ResolvedType], ) -> Result<String, String> { let full = if module.is_empty() { name.to_string() } else { format!("{}_{}", module.join("_"), name) }; // Type-param mono-subst (mirrors type_ref_to_c top: only for empty type-args). if args.is_empty() { if let Some(concrete) = self.type_subst_overrides.borrow().get(&full) { return Ok(concrete.clone()); } if let Some(concrete) = self.subst_c(&full) { return Ok(concrete); } } if let Some(c) = Self::primitive_name_to_c(&full) { return Ok(c.to_string()); } Ok(match full.as_str() { // U.4.8: removed types lower to nothing — carry the SAME Err message the deleted // `type_ref_to_c_impl` produced. `resolved_type_to_c` is the single type→C path now // (D315), so the failure reason lives HERE (Err), not in a fallback. Plan 133/134. "usize" => return Err("type `usize` is removed — use `int` (Plan 133)".to_string()), "isize" => return Err("type `isize` is removed — use `int` (Plan 133)".to_string()), "ptr" => return Err("type `ptr` is removed — use `*()` (Plan 134)".to_string()), "never" => "nova_int".to_string(), // Plan 174.3 (D53): `any` top-type — type-erased `void*` pointing at a // heap `NovaAny` box (see typeid.h). Single C-lowering source, so both // `type_ref_to_c(any)` and the resolved-types channel agree. Was the // `_ =>` fallthrough → bogus `Nova_any*` (undeclared struct) — `any` // never had a working value representation before this plan. "any" => "void*".to_string(), "Option" => { if let Some(inner) = args.first() { // [M-option-fn-field-record-literal-elem-type-int] (реестр // 221.1 №116): a `Func`-shaped inner (`Option[fn(A) -> B]`) // ALWAYS lowers to bare `"void*"` via `resolved_type_to_c`'s // own `R::Func => "void*"` arm (the correct, universal // representation for an ORDINARY bare fn-typed field/local — // closures dispatch through the separate `fn_param_sigs`/ // `record_field_fn_sigs` channel, not the static C type) — // but the VERY NEXT check below treats `"void*"` as a proxy // for "erased/unresolved" and collapses the WHOLE `Option` // to `NovaOpt_nova_int`. Correct for a genuinely-erased // generic-param stub, WRONG for a `Func`: `Option`'s own // NPO/tagged representation needs a CONCRETE, distinguishable // pointer type to build `NovaOpt_<X>` around — matching // whatever the closure-LITERAL construction site itself // allocates (`Self::clos_struct_name` — `NovaClos_ii*` for // `fn(int) -> int`, `NovaClosBase*` for any other arity/ // shape). Reusing that SAME naming function here (instead of // the generic `resolved_type_to_c`) guarantees the two sides // always agree — this was the root cause of `Some(closure)` // against an `Option[fn(...)->...]` field/let/return // CC-FAILing (`NovaOpt_nova_int` vs `NovaOpt_NovaClos_*`). // [M-option-fn-field-record-literal-elem-type-int] follow-up // (реестр 221.1 №116, integrator A/B repro 2026-07-26): // `inner` isn't ALWAYS a literal `R::Func` even when it truly // denotes a callable — `Option[Handler]` where `type Handler // fn(A) -> B` (D52 newtype/alias-over-fn) lowers `Handler` to // `R::Named{"Handler"}` here, NOT `R::Func`. `resolved_type_ // clos_ptr_c` peels a `Named` through `fn_newtype_sigs` (the // SAME registry `resolve_fn_typeref`/the checker's newtype- // to-Func chain already use) to its underlying `Func` shape // FIRST, so a fn-newtype/alias name is treated identically to // a literal `Func` — this keeps the field-declaration site // in agreement with every OTHER `Option[Handler]`-touching // site that already needed the same peel (`Some(v)` // construction — see that fn's own doc for the full // integrator-repro story, `server_router.nv`'s `mut fallback // Option[Handler]`). let inner_c = match self.resolved_type_clos_ptr_c(inner) { Some(c) => c, None => self.resolved_type_to_c(inner)?, }; if inner_c == "void*" { return Ok("NovaOpt_nova_int".to_string()); } // Plan 172.12 A2: erase to the int-boxed `NovaOpt_nova_int` when // the inner is an unresolved generic-param stub — decided from the // inner `ResolvedType` structure (narrow set), not a `Nova_`/`____` // parse of `inner_c`. if self.rt_is_erased_stub(inner, false) { return Ok("NovaOpt_nova_int".to_string()); } // [M-196-gen] canonical sink (was inlined sanitize+register+format // here, byte-identical dup of resolve_result_option_ret's Option // arm) — see opt_repr_c_type doc. self.opt_repr_c_type(&inner_c) } else { "NovaOpt_nova_int".to_string() } } "Result" => { if args.len() == 2 { // U.4.8: mirror the deleted `type_ref_to_c_impl` Result arm EXACTLY — an // un-lowerable inner (removed type / `Self`-without-receiver → Err) makes the // whole Result fall to the erased fallback; it does NOT propagate the Err // (legacy used `if let (Ok, Ok)`). Only when BOTH inners lower does it pick // concrete-vs-erased. The arm must therefore NOT use `?`: an enclosing // `Option[Result[usize, E]]` then sees the concrete erased Result (Ok), // exactly like legacy, instead of erroring out. if let (Ok(ok_c), Ok(err_c)) = (self.resolved_type_to_c(&args[0]), self.resolved_type_to_c(&args[1])) { // Plan 172.12 A2: the ok/err stub tests are structural on the // arg `ResolvedType`s (full registry set), not a `Nova_`/`____` // parse of the lowered `ok_c`/`err_c`. if !ok_c.is_empty() && ok_c != "void*" && !err_c.is_empty() && err_c != "void*" && !self.rt_is_erased_stub(&args[0], true) && !self.rt_is_erased_stub(&args[1], true) { return Ok(self.result_repr_c_type(&ok_c, &err_c)); } } } "NovaRes_nova_int_nova_str*".to_string() } "Self" => match &self.current_receiver_type { // [M-static-selfreturn-value-mangle-conflict] (Plan 172.13): // a STATIC namespace fn's `Self` (`fn Type.method() -> Self`) // denotes a FRESH value being constructed — no actual receiver // exists yet, so `receiver_c_type`'s ALWAYS-pointer-for-value- // types rule (built for the receiver's mutation-propagation // ABI) does not apply. Lower it like an ORDINARY reference to // the type instead (value-form for named-tuple/value-record — // matches how the constructor body's record-literal `return // Type(...)` is actually emitted). An INSTANCE method's `Self` // (fluent `-> Self`/`-> @`, returning the receiver itself) // is UNCHANGED — stays on `receiver_c_type`, byte-identical. Some(recv) if self.current_receiver_is_static => self.resolved_named_to_c(recv, &[], &[])?, Some(recv) => self.receiver_c_type(recv, false), // U.4.8: `Self` outside a receiver context — carry the SAME Err the deleted // `type_ref_to_c_impl` produced (Plan 11 follow-up: hard error, not a fallback, // so it never silently lowers to a bogus `Nova_Self*`). None => return Err("Self type used outside receiver context (free function or top-level expression). Self valid only внутри `fn Type[..].method(...)` или `fn Type[..] @method(...)`.".to_string()), }, "Effect" => match args.first() { Some(crate::types::ResolvedType::Named { name: en, module: em, .. }) => { let eff_full = if em.is_empty() { en.clone() } else { format!("{}_{}", em.join("_"), en) }; format!("NovaVtable_{}*", eff_full) } _ => "void*".to_string(), }, "CancelToken" => "NovaCancelToken*".to_string(), // Plan 248 (wave 3, D447 #no_copy): hardcoded name→C-type for the // 11 value-inside atomics — does NOT depend on `type_aliases` // having already been populated by an earlier processing of // sync.nv's OWN module (a real, hit-in-practice ordering gap: a // DIFFERENT module referencing `AtomicInt`, e.g. `*mut AtomicInt` // in `std/net/tcp.nv`'s `TcpStream.rc` field, resolved this // BEFORE sync.nv's early registration ran in this CU, producing // the stale `Nova_AtomicInt*` heap-pointer fallback — doubly // wrong once wrapped in the outer pointer: `Nova_AtomicInt**`). // Bare-value uses (`AtomicInt` local/field, no pointer) get this // string as-is (`NovaValue_AtomicInt`); `*mut AtomicInt`/`*AtomicInt` // wrap it in one more `*` at the `TypedPtr` call site, unaffected. "AtomicI64" | "AtomicI32" | "AtomicI16" | "AtomicI8" | "AtomicU64" | "AtomicU32" | "AtomicU16" | "AtomicU8" | "AtomicInt" | "AtomicUint" | "AtomicBool" => format!("NovaValue_{}", full), "Write" => "Nova_StringBuilder*".to_string(), // Plan 208 Ф.2 (D422, was D419/Plan 152.7.2): `Fmt` protocol — same // V1 erasure strategy as `Write` above (concrete C type, vtable // upgrade deferred). The ONLY production implementor is `FmtCtx` // (std/prelude/protocols.nv, a real Nova record — compiles to this // same "Nova_FmtCtx*" via the ordinary record path), constructed by // the interpolation codegen at EVERY `${x}`/`${x:?}`/`${x:SPEC}` // call site now (D422 unifies `@display`/`@debug` onto `(mut f // Fmt)` — no more bare-`Write`/`@display_fmt`-optional-hook split). "Fmt" => "Nova_FmtCtx*".to_string(), // D239 `[]T ≡ Vec[T]` (D315 §0/§3): the NOMINAL `Vec[T]` and the slice sugar // `[]T` (both now `Named{Vec}`) lower through the SINGLE canonical Vec→C source // `resolved_array_to_c` — closure-array (`NovaArray_void_p*`), unresolved-stub // erasure, and the `Nova_Vec____<elem>*` mono worklist/instance-info side-effects // all live in ONE place, not a divergent generic-path copy. `[]T` is byte- // identical (its lowering was ALREADY `resolved_array_to_c`); `Vec[T]` now matches // it (was the divergent generic path — D239 demands they be identical). Only the // bare `Vec` (empty module) intercepts; a qualified `Vec` stays on the generic // path below (unreachable for the prelude `Vec`). "Vec" if args.len() == 1 => return self.resolved_array_to_c(&args[0]), _ => { // Protocol (value-erased): non-generic → NovaBox_<proto> (the bare name, // mirroring type_ref_to_c's `path.last()`); generic → void*. if self.protocol_types.contains(&full) || self.protocol_types.contains(name) { if args.is_empty() { return Ok(format!("NovaBox_{}", name)); } return Ok("void*".to_string()); } // Type alias (skip when referenced WITH args AND a known generic template — // mirrors type_ref_to_c's guard so the mono path is not bypassed). let is_generic_template_with_args = !args.is_empty() && self.generic_type_templates.contains_key(&full); if !is_generic_template_with_args { if let Some(aliased_c) = self.type_aliases.get(&full).cloned() { return Ok(aliased_c); } } // Generic-template instance (mono mangling + worklist/instance-info // side-effects, idempotent). U.4.6b: mirrors type_ref_to_c's generic arm // driven by ResolvedType — type-args lowered via resolved_type_to_c // (parity-protected recursion), erased (None→"nova_int") EXACTLY like // type_ref_to_c's `unwrap_or(nova_int)` partial-mono tolerance. if !args.is_empty() && self.generic_type_templates.contains_key(&full) { // 172.1.2 Шаг 1: TypeParam-Err ПРОПАГИРУЕТСЯ (channel-miss → legacy), // не стирается в nova_int; для прочих Err — прежняя partial-mono // толерантность (byte-identical, пока продюсеры не мигрированы). let mut type_args_c: Vec<String> = Vec::with_capacity(args.len()); for a in args { let c = if matches!(a, crate::types::ResolvedType::TypeParam(_)) { self.resolved_type_to_c(a)? } else { self.resolved_type_to_c(a) .unwrap_or_else(|_| "nova_int".to_string()) }; type_args_c.push(c); } let mangled = Self::compute_generic_type_c_name(&full, &type_args_c); if !self.emitted_generic_type_instances.contains(&mangled) { let mut wl = self.generic_type_worklist.borrow_mut(); if !wl.iter().any(|(_, _, m)| m == &mangled) { wl.push((full.clone(), Self::args_lift(&type_args_c), mangled.clone())); } } self.generic_type_instance_info .borrow_mut() .entry(mangled.clone()) .or_insert_with(|| (full.clone(), Self::args_lift(&type_args_c))); if self.is_value_generic_template(&full) { return Ok(format!("NovaValue_{}", Self::debt_mono_short_name(&mangled))); } return Ok(format!("{}*", mangled)); } // User-defined concrete record/sum — pointer to struct. // Plan 172.1 Session C (gap-recon): SAFE env-gated instrumentation — log a // bare-stub return for a generic-TEMPLATE name (or bare type-param `T`/`K`) with // EMPTY type-args and NO subst entry. That is exactly the mono-subst-population // gap (container-receiver erased-body emits with empty current_type_subst → // `Nova_Lru*` instead of `Nova_Lru____...*`). Behavior-UNCHANGED (only eprintln), // zero cost in release (`#[cfg(debug_assertions)]`) — §7 «измерять до правки». #[cfg(debug_assertions)] if args.is_empty() && (self.generic_type_templates.contains_key(&full) || (full.len() <= 2 && full.chars().next().map_or(false, |c| c.is_ascii_uppercase()))) && std::env::var("NOVA_BAIL_GAPLOG").is_ok() { eprintln!( "[bail-gap] bare-stub name={} subst_len={} overrides_len={}", full, self.current_type_subst.len(), self.type_subst_overrides.borrow().len() ); } // [M-sync-crossmodule…] (D381): concrete user record/sum. For a // COLLIDING simple name, qualify by the DEFINING module resolved // from the referencing file (`ref_type_base`); every other name // keeps the exact legacy `full` (byte-identical). // // [M-198-f4c-1-privfile-type-not-discriminated]: `colliding_type_names` // is D381's CROSS-MODULE collision set only — a same-module, // per-file `priv(file) type` collision (this fixture's `Rect`) // never enters it, so the gate must ALSO fire when the current // emission file has a `file_priv_type_c_names` entry for `name` // (checked first, internally, by `ref_type_base`). Non-colliding // names hit neither condition — byte-identical `full`. let file_priv_collision = self.current_emit_file_id.map_or(false, |fid| { self.file_priv_type_c_names.contains_key(&(fid, name.to_string())) }); if self.colliding_type_names.contains(name) || file_priv_collision { format!("Nova_{}*", self.ref_type_base(name, module)) } else { format!("Nova_{}*", full) } } }) } /// Plan 140.2: можно ли элидировать inline bounds-check Index-сайта `span`? /// loop/code-доказанные — всегда; contract-доказанные — только если контракты /// для этой fn enforced (Plan 194 A2.1: legacy build-level `--contracts=off` /// retired; Plan 194 A4: per-fn/module `#unchecked` opt-out тоже retired — /// `contracts_elided_here()` константно `false`, гейт всегда проходит). fn index_site_elided(&self, span_start: usize) -> bool { self.proven_index_sites.contains(&span_start) || (!self.contracts_elided_here() && self.proven_index_sites_contract.contains(&span_start)) } /// Plan 140.4 ([M-opt-elide-proven-overflow-checks]): proven `int`-overflow /// сайты. `always` — доказаны из loop/code (элидируются всегда); `contract` — /// только с fn-`requires` (элидируются лишь при включённых контрактах). pub fn set_proven_overflow_sites(&mut self, always: &[crate::diag::Span], contract: &[crate::diag::Span]) { self.proven_overflow_sites.clear(); for span in always { self.proven_overflow_sites.insert(span.start); } self.proven_overflow_sites_contract.clear(); for span in contract { self.proven_overflow_sites_contract.insert(span.start); } } /// Plan 140.4: можно ли элидировать `nova_int_checked_*` на сайте `span`? /// loop/code-доказанные — всегда; contract-доказанные — только если контракты /// (`requires`) для этой fn enforced (Plan 194 A2.1: legacy build-level /// `--contracts=off` retired; Plan 194 A4: per-fn/module `#unchecked` /// opt-out тоже retired — `contracts_elided_here()` константно `false`). /// Та же логика, что `index_site_elided` — overflow-panic = soundness-guard, /// элидируется ТОЛЬКО пруфом. fn overflow_site_elided(&self, span_start: usize) -> bool { self.proven_overflow_sites.contains(&span_start) || (!self.contracts_elided_here() && self.proven_overflow_sites_contract.contains(&span_start)) } /// Plan 194 A2.1 (замена `set_contracts_off`): build-policy режим /// `--contracts=checked|optimized|verified`. `off` (глобальный /// unconditional bypass) убран — см. doc `contracts_mode` поля и /// `contracts_elided_for` ниже. pub fn set_contracts_mode(&mut self, mode: crate::ast::ContractsMode) { self.contracts_mode = mode; } /// Plan 194 A2.2 (D421 §3): предикат для эрозии `#debug`-контрактов/ /// `#debug assert` по build-режиму. `checked` (dev-дефолт) — `#debug` /// работает (byte-identical старому поведению без различения режима); /// `optimized`/`verified` (release) — `#debug`-клаузы/statement'ы /// СТИРАЮТСЯ (не эмитятся, zero-cost). Не-`#debug` контракты этим /// предикатом НЕ гейтятся — они always-on независимо от режима (сайт- /// specific sound-элизия идёт отдельно через `proven_contracts`). fn mode_erases_debug(&self) -> bool { let erases = matches!(self.contracts_mode, crate::ast::ContractsMode::Optimized); if erases { // №466 Ф.1 (решение владельца 2026-08-08 «исправить до тега»): // МОЛЧАЛИВОЕ стирание `#debug`-контрактов в release — ложное // обещание защиты. Разведка дверей контрактов показала: в dev // `panic: invariant failed`, в release то же значение живёт с // exit 0 и БЕЗ ЕДИНОГО предупреждения — притом что CLAUDE.md // требует release-сборку как ОБЯЗАТЕЛЬНЫЙ авторитетный гейт, то // есть защита исчезает ровно там, где её считают включённой. // Считаем стёртое и сообщаем ОДИН раз за сборку (см. вызов // `report_debug_erasure` в конце эмиссии модуля). self.debug_contracts_erased.set(self.debug_contracts_erased.get() + 1); } erases } /// №466 Ф.1: сообщить пользователю, что часть проверок выключена режимом. /// Печатается ОДИН раз за компиляцию и только когда действительно стёрли — /// молчание при нулевом счётчике, чтобы не шуметь на обычных сборках. fn report_debug_erasure(&self) { let n = self.debug_contracts_erased.get(); if n == 0 { return; } eprintln!( "note: режим сборки выключил {} проверок(и) `#debug` (контракты/инварианты). В этой сборке они НЕ проверяются. Соберите с `--mode dev` (или `checked`), если нужна их проверка. См. реестр 221.1 №466.", n ); } /// Plan 140 Ф.2 / Plan 194 A4 (ретракт `#unchecked`): контракт-проверки /// элидируются для тела текущей fn? Единственный оставшийся per-fn/module /// opt-out (`#unchecked`) убран вместе с языковой фичой — requires/ /// ensures/invariant теперь ВСЕГДА enforced (кроме Z3/loop/code-proven /// сайтов, которые гейтятся отдельно через `proven_contracts` / /// `proven_overflow_sites*`). Оставлен как named predicate (а не удалён /// целиком) — вызывающие сайты читаются как «элизия для kind `X`» /// декларативно; будущая mode-based sound-элизия (A3+) заполнит тело. fn contracts_elided_here(&self) -> bool { false } /// Plan 140.3 / Plan 194 A4: элидируется ли контракт-вид `kind` здесь? /// Ретракт `#unchecked` убрал единственный источник элизии этого /// предиката (module/fn opt-out) — константно `false` до будущей /// mode-based Z3-driven дифференциации (`optimized`/`verified`, A3+). fn contracts_elided_for(&self, _kind: crate::ast::ContractKind) -> bool { false } /// Plan 140.3 / Plan 194 A4: элидируется ли type-`invariant`-страховка /// здесь? Ретракт `#unchecked` убрал единственный источник элизии — /// константно `false` (type invariants теперь всегда enforced). fn invariants_elided_here(&self) -> bool { false } /// Get the Span of a statement (where in source it came from). fn stmt_span(stmt: &Stmt) -> Span { match stmt { Stmt::Let(d) => d.span, Stmt::Const(d) => d.span, Stmt::Expr(e) => e.span, Stmt::Assign { span, .. } | Stmt::Return { span, .. } | Stmt::Throw { span, .. } | Stmt::Defer { span, .. } | Stmt::AssertStatic { span, .. } | Stmt::Assume { span, .. } | Stmt::Apply { span, .. } | Stmt::Calc { span, .. } | Stmt::Reveal { span, .. } | Stmt::ConsumeScope { span, .. } => *span, Stmt::Break(s) | Stmt::Continue(s) => *s, Stmt::TupleAssign { span, .. } => *span, } } /// If source-annotation mode is on, emit `/* SRC: <line> */` before the /// statement's C code. Multi-line statements get just the first line + /// `…` ellipsis to keep .c readable. fn emit_source_annotation_for_stmt(&mut self, stmt: &Stmt) { self.emit_source_annotation_for_span(Self::stmt_span(stmt)); } /// Same as `..._for_stmt` but for trailing-expression of a block (which /// parser routes into `block.trailing` instead of `block.stmts`). fn emit_source_annotation_for_expr(&mut self, expr: &Expr) { self.emit_source_annotation_for_span(expr.span); } fn emit_source_annotation_for_span(&mut self, span: Span) { // Plan 14 std-fix: SRC-комментарии теперь управляются отдельно // от наличия source (source нужен для line:col в ошибках). if !self.annotation_enabled { return; } // Only annotate spans from the main (user) file. Spans from stdlib/ // imported modules carry a different file_id and their byte offsets // are meaningless against annotation_source (the user file's text). if span.file_id != crate::diag::MAIN_FILE_ID { return; } let Some(src) = self.annotation_source.clone() else { return; }; let snippet = src .get(span.start..span.end) .unwrap_or("") .lines() .next() .unwrap_or("") .trim(); if snippet.is_empty() { return; } // Sanitize for C comment: replace `*/` sequences with `* /` to avoid // closing the comment prematurely. Lone `*` and `/` are kept (so // multiplication and division operators читаемы). Truncate long lines. let truncated: String = snippet.chars().take(120).collect(); let suffix = if snippet.chars().count() > truncated.chars().count() { " …" } else { "" }; let safe = truncated.replace("*/", "* /"); self.line(&format!("/* SRC: {}{} */", safe, suffix)); } /// Единственная точка наполнения `method_overloads`: держит /// `method_overload_types` в согласии с картой по построению, а не по /// памяти правящего (реестр 221.1 №522). fn register_method_overload(&mut self, key: (String, String), sig: MethodSig) { self.method_overload_types.insert(key.0.clone()); // NB: обращаемся к карте НАПРЯМУЮ. Здесь единственное место, где это // законно, — и единственное, где легко получить бесконечную рекурсию: // глобальная замена «прямая вставка → вызов этого метода» попадает и // сюда, если тело написано той же строкой. Получено 2026-08-09, стоило // stack overflow на КАЖДОМ `nova test`. self.method_overloads.entry(key).or_default().push(sig); } pub fn emit_module(mut self, module: &Module) -> Result<(String, Vec<String>), String> { // [M-consume-rebind-nested-block-shadow] (Plan 172.13): copy the // alpha_rename-computed span set so `Stmt::Let` can look up its own // span below. self.consume_reuse_spans = module.consume_reuse_spans.clone(); // Plan 127 Ф.2/Ф.3: run value-record escape analysis upfront so // codegen знает which value-record locals must be heap-promoted // (AllocKind::ValueHeapPromoted). Cheap: single AST walk; result // is empty (no allocations) если module has no value-records. self.escape_result = Some(crate::escape_analyze::analyze_module(module)); // [D52-амендмент, ОКНО-5] `fn_newtype_sigs` pre-scan: `type X fn(A)->B` // (newtype, one level) / `type X alias fn(A)->B` (alias, transitive // through any alias chain) → the underlying `Func` shape. Independent // pre-pass over `module.items` (whole-CU merge, same source // `all_fns` above reads) — cheap (name → TypeRef clone), no ordering // dependency on `emit_type_decl`'s own emission loop, so every // function body emitted below already sees the full map. { let mut newtype_raw: HashMap<String, TypeRef> = HashMap::new(); let mut alias_raw: HashMap<String, TypeRef> = HashMap::new(); for item in &module.items { if let Item::Type(t) = item { match &t.kind { TypeDeclKind::Newtype(TypeRef::Func { .. }) => { if let TypeDeclKind::Newtype(inner) = &t.kind { newtype_raw.insert(t.name.clone(), inner.clone()); } } TypeDeclKind::Alias(inner) => { alias_raw.insert(t.name.clone(), inner.clone()); } _ => {} } } } // Resolve alias chains transitively (D52 full transparency); a // newtype-over-fn name reached at the END of an alias chain also // counts (an alias of a newtype-over-fn is still call-through-able // through the newtype, D52 §alias-transparency). fn resolve_chain( name: &str, newtype_raw: &HashMap<String, TypeRef>, alias_raw: &HashMap<String, TypeRef>, depth: u32, ) -> Option<TypeRef> { if depth > 16 { return None; // cycle guard } if let Some(f) = newtype_raw.get(name) { return Some(f.clone()); } if let Some(inner) = alias_raw.get(name) { match inner { TypeRef::Func { .. } => return Some(inner.clone()), TypeRef::Named { path, generics, .. } if generics.is_empty() => { if let Some(n2) = path.last() { return resolve_chain(n2, newtype_raw, alias_raw, depth + 1); } } _ => {} } } None } for name in newtype_raw.keys().chain(alias_raw.keys()) { if let Some(func_ty) = resolve_chain(name, &newtype_raw, &alias_raw, 0) { self.fn_newtype_sigs.insert(name.clone(), func_ty); } } } // Plan 143.2 [M-opt-leaf-preempt-entry-elision]: whole-program // call-graph pre-pass computing which fns must KEEP their prologue // preempt-check. Source-level over `module.items` (flat entry merge) // PLUS every peer file's `items_here` (covers imported-module // FnDecls). Conservative: any cycle/indirect/FFI/address-taken/ // unresolved callee => KEEP. Computed BEFORE the emit loop so // `emit_fn` can consult it. Sound over monomorphization (KEEP status // of a source template inherited by all instances). { let mut all_fns: Vec<&FnDecl> = Vec::new(); for item in &module.items { if let Item::Fn(f) = item { all_fns.push(f); } } for pf in &module.peer_files { for item in &pf.items_here { if let Item::Fn(f) = item { all_fns.push(f); } } } self.preempt_keep = crate::codegen::preempt_keep::compute_preempt_keep_set(all_fns); } // Plan 175 Ф.2-v2: pre-pass — collect `#default_handler(X)` free fns // (checker already validated arity/return-type/uniqueness/cycles; // codegen just needs the plain Nova name to resolve its mangled C // symbol — consulted by `emit_effect_type`'s dispatch-wrapper below). { // Plan 175.2 Ф.2-v4 (П7, D431): `#default_handler` argument is // now optional — bare form infers `X` from `f`'s own // `-> Effect[X]` return type (checker already validated this // shape in `check_default_handlers`; codegen just re-derives // the same string here, mirroring // `default_handler_infer_effect_name` in types/mod.rs). let infer_eff = |rt: &Option<crate::ast::TypeRef>| -> Option<String> { match rt { Some(crate::ast::TypeRef::Named { path, generics, .. }) if path.len() == 1 && path[0] == "Effect" && generics.len() == 1 => { match &generics[0] { crate::ast::TypeRef::Named { path: ip, .. } => ip.last().cloned(), _ => None, } } _ => None, } }; let mut collect = |items: &[Item]| { for item in items { if let Item::Fn(f) = item { for attr in &f.doc_attrs { if let crate::ast::DocAttr::DefaultHandler(eff_opt) = attr { if let Some(eff) = eff_opt.clone().or_else(|| infer_eff(&f.return_type)) { self.default_handler_fns.insert(eff, f.name.clone()); } } } } } }; collect(&module.items); for pf in &module.peer_files { collect(&pf.items_here); } } // Plan 173 Ф.5 (#8, D188 R2): pre-pass — collect receiver types of // USER (non-extern) `consume @cleanup` methods. These heap-record // structs get a hidden `int _consume_ccount;` field and the generated // `Nova_<T>_consume_cleanup` gets an exactly-once prologue (the single // chokepoint every invocation path goes through — scope dispatch AND // any manual call that escaped the compile-time D188-r2 checker via a // function boundary). Extern "nova" cleanups (MutexGuard etc., D194 // hot path, hand-written C structs) are excluded by `!f.is_external`. { let mut collect = |items: &[Item], declared: &mut HashSet<String>, ccount: &mut HashSet<String>| { for item in items { if let Item::Fn(f) = item { if f.name != "cleanup" { continue; } if let Some(recv) = &f.receiver { if recv.consume && matches!(recv.kind, ReceiverKind::Instance) { let ident = Self::receiver_type_c_ident(&recv.type_name); // Реестр 221.1 №583: EVERY declared consume // @cleanup (extern included) has a real C // definition somewhere — register it here // unconditionally. declared.insert(ident.clone()); if !f.is_external { ccount.insert(ident); } } } } } }; let mut declared = std::mem::take(&mut self.consume_cleanup_declared_types); let mut ccount = std::mem::take(&mut self.consume_cleanup_types); collect(&module.items, &mut declared, &mut ccount); for pf in &module.peer_files { collect(&pf.items_here, &mut declared, &mut ccount); } self.consume_cleanup_declared_types = declared; self.consume_cleanup_types = ccount; } // Plan 217 (гибрид C, §8а п.1) + D432-амендмент 2026-08-04 (№315 // fix): pre-pass — PLAIN Nova type names whose `@cleanup` is // protocol-shaped, ANY effect-row now (amendment lifted the prior // `f.effects.is_empty()` gate; checker enforces the triggering fn // declares those effects — `types/mod.rs` `check_obligations_at_exit`/ // `validate_consume_scope_init`). extern "nova" cleanups COUNT here // (MutexGuard/TcpStream/…) — eligibility depends only on shape. // Mirrors `LinearityRegistry::build`'s `cleanup_effect_rows` — MUST // stay in sync (mismatch = silent leak, feedback-zero-tolerance-bugs). { let mut collect = |items: &[Item]| { for item in items { if let Item::Fn(f) = item { if f.name != "cleanup" { continue; } if let Some(recv) = &f.receiver { // BUGFIX (folder-CU regression, 2026-07-20): // name-only match false-positived on // `DbConnection consume @cleanup() -> ()` // (`cross_pkg_consume_via_protocol_ok.nv`) — a // ZERO-ARG method for an unrelated ad-hoc // `Resource` protocol, not `Cleanup[E]`'s // `@cleanup(outcome ScopeOutcome) -> ()` shape. // Must mirror the checker-side gate // (types/mod.rs `LinearityRegistry::build`) // EXACTLY, or the two diverge (checker // suppresses D133 for a type codegen doesn't // treat as auto-cleanup, or vice versa — either // way a silent leak / CC-FAIL). let is_cleanup_protocol_shape = f.params.len() == 1 && matches!(&f.params[0].ty, TypeRef::Named { path, .. } if path.last().map_or(false, |s| s == "ScopeOutcome")); if recv.consume && matches!(recv.kind, ReceiverKind::Instance) && is_cleanup_protocol_shape { self.auto_cleanup_types.insert(recv.type_name.clone()); } } } } }; collect(&module.items); for pf in &module.peer_files { collect(&pf.items_here); } } // №465 (A8.29): pre-pass — collect `#zero_on_move` types eligible for // auto-inject. Re-derives the safe-kind classification independently // of the checker (defensive — codegen doesn't blindly trust an // external invariant, same posture as the other pre-passes here): // `AllocKind::Value` record → `true` (needs the per-binding // promotion guard at the injection site); `NamedTuple` / ordinary // (non-runtime-backed) `Newtype` → `false` (always safe, no // promotion concept). Heap-allocated records and runtime-backed // newtypes (Mutex/Condvar/Atomic*/…, `debt_is_runtime_backed_newtype` // — checker rejects heap records via `E_ZERO_ON_MOVE_ALIASED_STORAGE` // and rejects non-`consume` types via // `E_ZERO_ON_MOVE_REQUIRES_CONSUME`, which already excludes today's // std runtime-backed types since none of them are `consume`) are // never inserted — see `zero_on_move_types` field doc. { let mut collect = |items: &[Item]| { for item in items { if let Item::Type(t) = item { if !t.zero_on_move || !t.consume { continue; } match &t.kind { TypeDeclKind::Record(_) if t.allocation == AllocKind::Value => { self.zero_on_move_types.insert(t.name.clone(), true); } TypeDeclKind::NamedTuple(_) => { self.zero_on_move_types.insert(t.name.clone(), false); } TypeDeclKind::Newtype(_) if !Self::debt_is_runtime_backed_newtype(t.name.as_str()) => { self.zero_on_move_types.insert(t.name.clone(), false); } _ => {} } } } }; collect(&module.items); for pf in &module.peer_files { collect(&pf.items_here); } } // Plan 217: pre-pass — collect ALL consume-receiver method names per // type (not just `cleanup`) — mirrors checker's `LinearityRegistry:: // consume_methods` exactly. Gates the auto-cleanup bare-statement // receiver-disarm (`X.method()`): only a genuine consuming method // call may disarm — a `ro`/`mut`-receiver helper method on the same // type must NOT (that would falsely skip the real cleanup). { let mut collect = |items: &[Item]| { for item in items { if let Item::Fn(f) = item { if let Some(recv) = &f.receiver { if recv.consume { self.consume_receiver_methods .entry(recv.type_name.clone()) .or_default() .insert(f.name.clone()); } } } } }; collect(&module.items); for pf in &module.peer_files { collect(&pf.items_here); } } // Plan 217 BUGFIX (folder-CU regression, `guard_cross_scope_ // transfer.nv` "Guard passed to helper function and consumed // there" — MutexGuard double-`unlock`): pre-pass — collect, per // free-fn NAME and per `(type, method)`, the ARG POSITIONS that are // `consume`-mode on at least one overload (union — conservative). // Drives the call-arg disarm in `disarm_auto_cleanup_receiver_call` // (despite the name, that fn now ALSO handles this case) — without // it, `consume g = mu.lock(); helper(g)` where `helper`'s param IS // `consume`-mode leaves `g`'s `_active` flag armed after a // legitimate ownership transfer, double-firing `@cleanup` at the // caller's scope-exit on top of whatever `helper` itself did. { let mut collect = |items: &[Item]| { for item in items { if let Item::Fn(f) = item { let modes = Self::fn_param_modes(f); let consume_positions: HashSet<usize> = modes.iter() .enumerate() .filter(|(_, &m)| m == 2) .map(|(i, _)| i) .collect(); if consume_positions.is_empty() { continue; } match &f.receiver { None => { self.free_fn_consume_param_positions .entry(f.name.clone()) .or_default() .extend(consume_positions); } Some(recv) => { self.method_consume_param_positions .entry((recv.type_name.clone(), f.name.clone())) .or_default() .extend(consume_positions); } } } } }; collect(&module.items); for pf in &module.peer_files { collect(&pf.items_here); } } // `[M-178-consume-field-ctor-from-var]` × D188 v3: pre-pass — collect // consume-field names per record type / record-payload sum variant // (mirror of checker's ConsumeRegistry.record_consume_fields; keyed by // source name, same lookup as the RecordLit type_name last segment). { let mut rcf: HashMap<String, HashSet<String>> = HashMap::new(); let mut rfn: HashMap<String, HashSet<String>> = HashMap::new(); { let mut collect_fields = |type_name: &str, fields: &[crate::ast::RecordField]| { let all: HashSet<String> = fields.iter() .map(|f| f.name.clone()) .collect(); let consume: HashSet<String> = fields.iter() .filter(|f| f.consume) .map(|f| f.name.clone()) .collect(); if !all.is_empty() { rfn.insert(type_name.to_string(), all); } if !consume.is_empty() { rcf.insert(type_name.to_string(), consume); } }; let mut collect_types = |items: &[Item]| { for item in items { if let Item::Type(td) = item { match &td.kind { TypeDeclKind::Record(fields) => collect_fields(&td.name, fields), TypeDeclKind::Sum(variants) => { for v in variants { if let crate::ast::SumVariantKind::Record(fields) = &v.kind { collect_fields(&v.name, fields); } } } _ => {} } } } }; collect_types(&module.items); for pf in &module.peer_files { collect_types(&pf.items_here); } } self.record_consume_fields = rcf; self.record_field_names = rfn; } // Plan 70.1: register imported-module prefix names (aliases + last-segments) // для emit_call Member dispatch rewrite. См. поле `imported_modules` doc. let register_imports = |imports: &[crate::ast::Import], target: &mut HashSet<String>| { for imp in imports { if let Some(alias) = &imp.alias { target.insert(alias.clone()); } if let Some(last) = imp.path.last() { target.insert(last.clone()); } } }; register_imports(&module.imports, &mut self.imported_modules); for pf in &module.peer_files { register_imports(&pf.imports, &mut self.imported_modules); } // Plan 81 Ф.6.2: атрибуция свободных функций к объявляющим // модулям — для symbol mangling `nova_fn_<modpath>_<name>`. // Первый peer с данным именем побеждает (cross-module overload // с одним именем — редкий edge; param-суффикс всё равно // разводит C-имена). // // [M-exp-promotion-blockers: uuid_namespace duplicate-symbol] // "первый побеждает" — это глобальный, keyed-только-по-имени кэш // (`fn_module_map: HashMap<String, Vec<String>>`, БЕЗ модуля/файла // в ключе). Когда ДВЕ РАЗНЫЕ private free-fn с одинаковым именем // (`fn rotl32(...)`, не `priv(file)`) объявлены в РАЗНЫХ модулях // одного CU (`crypto.md5` и `crypto.sha1`), обе получают mangled- // имя ПЕРВОГО объявившего модуля — `nova_fn_..md5..rotl32` // эмиттится ДВАЖДЫ (для sha1's ДРУГОГО тела тоже) → C "redefinition". // Не name-collision между модулями — тот же symbol эмиттится дважды // с разным содержимым. // // Fix (mirrors [M-sync-crossmodule-samename-type-collision] D381's // collision-aware type qualification, applied here to free-fn // mangling): detect names declared as a plain (non-receiver) free // fn in ≥2 DISTINCT modules, and route THOSE through the existing // `file_priv_fn_c_names` per-(file_id, name) map (D307's // `priv(file)` machinery) instead of the naive global cache — // `free_fn_c_name` already checks `file_priv_fn_c_names` FIRST, at // both the definition-emit and every call-site (via // `current_emit_file_id`), so no change needed there. Non-colliding // names are completely unaffected (byte-identical). // // Plan 202 Ф.1b (D78 rev-4 §5): `pf.module_name` is a DECLARATION, and // D78 rev-4 legalizes two PHYSICALLY DISTINCT modules sharing the exact // same 2-segment `parent.target` declaration (research 2026-07-13 §2а // — e.g. `a/neg/helper.nv` and `b/neg/helper.nv`, both forced to // declare `module neg.helper`). Every grouping/mangling site below (fn // axis — this block — AND the D381 type axis right after it) used to // key by `pf.module_name` alone: a `BTreeSet<Vec<String>>` of // declarations silently collapsed the two physically distinct modules // into ONE entry (not detected as colliding), and even a detected // collision would still mangle/qualify both to the IDENTICAL C symbol // (decl-based) — observed as a hard `CC-FAIL "redefinition of // nova_fn_..."` the moment Ф.1's registry fix let both physical // modules' items reach codegen (previously the resolver silently // swallowed the second one, so this never got this far). `phys_key_of` // / `decl_phys_groups` / `effective_modpath` give every peer file its // PHYSICAL identity (canonical directory for a folder-module peer, // canonical file path for a single-file module — mirrors // `imports::canonical_module_key`) and append a stable, deterministic // `dupN` discriminator to the declaration ONLY for peers of a // declaration shared by ≥2 physically distinct modules in THIS CU. // Every other declaration (the overwhelming common case) is untouched // — `effective_modpath(pf) == pf.module_name` whenever the declaration // maps to exactly one physical module, so mangling/qualification stays // byte-identical for the whole existing corpus (0 CUs today have a // decl shared by ≥2 physical modules — that used to be swallowed, not // merely rare). Shared by BOTH the fn-axis block and the type-axis // (D381) block below. let mut phys_key_of: HashMap<u32, Vec<String>> = HashMap::new(); let mut decl_phys_groups: HashMap<Vec<String>, std::collections::BTreeSet<Vec<String>>> = HashMap::new(); for pf in &module.peer_files { let phys = crate::imports::canonical_module_key(std::slice::from_ref(&pf.path)); phys_key_of.insert(pf.file_id, phys.clone()); decl_phys_groups.entry(pf.module_name.clone()).or_default().insert(phys); } let effective_modpath = |pf: &crate::ast::PeerFile| -> Vec<String> { let groups = match decl_phys_groups.get(&pf.module_name) { Some(g) if g.len() >= 2 => g, _ => return pf.module_name.clone(), }; let phys = phys_key_of.get(&pf.file_id).cloned().unwrap_or_default(); let idx = groups.iter().position(|p| p == &phys).unwrap_or(0); let mut m = pf.module_name.clone(); m.push(format!("dup{}", idx)); m }; { // [батч 3, follow-up]: сигнатурно-РАЗЛИЧИМЫЕ коллизии (разная // арность/типы параметров — `encode_with` у hex и base64) уже // консистентно разруливаются overload-суффиксами D84 (mangle_fn + // call-sites через method_overloads) — их регистрация в // file_priv_fn_c_names ЛОМАЛА эту согласованность (fwd-decl // квалифицирован, call-site суффиксован → linker/implicit-decl). // В карту идут только коллизии с ИДЕНТИЧНОЙ сигнатурой (rotl32 // md5/sha1) — их overload-путь различить не может (один want_params // → первый c_name для обоих тел, старый duplicate-symbol баг). // // [M-178-server-typed-body] fix (2026-07-12): the identical-sig-only // condition above misses a DIFFERENT-signature cross-module // collision where at least one side is module-PRIVATE (no // `export`) — e.g. `std.http.client`'s private // `serialize_response(status, headers, body) -> str` vs // `std.http.server`'s EXPORTED `serialize_response(resp) -> []u8`. // A private declaration is by D307/D78 construction NEVER a // legitimate D84 overload candidate for callers outside its own // module (nothing outside the module can even name it) — the // "already handled via overload suffixes" escape above only // holds when EVERY colliding declaration is `export`ed (so a // downstream call site genuinely importing both could pick // between them by arg-shape). When any side is private, the // naive single-entry `fn_module_map` can silently steal a // same-module call site (`mock_dispatch` in `client/mock.nv` // calling its own peer's private `serialize_response`) and mangle // it to the OTHER module's symbol — right name, wrong body, // wrong C type. Route those through the same safe per-file path. let mut name_modules: HashMap<String, std::collections::BTreeSet<Vec<String>>> = HashMap::new(); let mut name_sigs: HashMap<String, std::collections::BTreeSet<String>> = HashMap::new(); let mut name_all_exported: HashMap<String, bool> = HashMap::new(); for pf in &module.peer_files { for item in &pf.items_here { if let Item::Fn(f) = item { if f.receiver.is_none() && !f.is_external { name_modules.entry(f.name.clone()) .or_default() .insert(effective_modpath(pf)); let sig: String = f.params.iter() .map(|p| self.type_ref_to_c(&p.ty) .unwrap_or_else(|_| "?".into())) .collect::<Vec<_>>() .join(","); name_sigs.entry(f.name.clone()).or_default().insert(sig); name_all_exported.entry(f.name.clone()) .and_modify(|all| *all = *all && f.is_export) .or_insert(f.is_export); } } } } let colliding_fn_names: std::collections::HashSet<String> = name_modules.into_iter() .filter(|(name, mods)| { if mods.len() < 2 { return false; } let identical_sig = name_sigs.get(name).map_or(false, |sigs| sigs.len() == 1); let any_private = !name_all_exported.get(name).copied().unwrap_or(true); identical_sig || any_private }) .map(|(name, _)| name) .collect(); // Persist for the D84 free-fn overload-registration pass below // (~line 5700s), which must exclude these names from the shared // `method_overloads` sentinel-key registry — see field doc. self.colliding_fn_names = colliding_fn_names.clone(); // [M-178-server-typed-body] fix: `file_priv_fn_c_names` is keyed // by the SINGLE declaring peer-file's `file_id` — correct for a // TRUE `priv(file)` item (only ever called from its own // declaring file, so caller's `current_emit_file_id` always // equals the declarer's), but a module-private (D307 default, // no modifier) colliding fn is callable from ANY peer file of // its OWN module (folder-module = one namespace across // co-equal files, D29/D78) — e.g. `client/wire.nv`'s // `serialize_response` called from the peer file // `client/mock.nv`. Registering the mangled name under ONLY // `wire.nv`'s file_id left `mock.nv`'s call site (a DIFFERENT // `current_emit_file_id`) unresolved, falling through past both // `file_priv_fn_c_names` AND `fn_module_map` (excluded once // colliding) to the bare `nova_fn_<name>` legacy fallback — a // symbol nothing emits (implicit-int C compile error). Register // the mangled name under EVERY peer file's `file_id` that // shares the declaring `pf.module_name`, so the shadow lookup // succeeds regardless of which peer file the call site is // emitted from. // Plan 202 Ф.1b: grouped by `effective_modpath`, NOT raw `pf.module_name` // — so peers of a decl-AMBIGUOUS module (see doc above) are grouped only // with their TRUE physical siblings, never with the other physically // distinct module sharing the same declaration. let mut module_file_ids: HashMap<Vec<String>, Vec<u32>> = HashMap::new(); for pf in &module.peer_files { module_file_ids.entry(effective_modpath(pf)) .or_default() .push(pf.file_id); } for pf in &module.peer_files { for item in &pf.items_here { if let Item::Fn(f) = item { if f.receiver.is_none() { let modpath = effective_modpath(pf); if colliding_fn_names.contains(&f.name) { let mangled = Self::mangle_free_fn(&modpath, &f.name); if let Some(file_ids) = module_file_ids.get(&modpath) { for &fid in file_ids { self.file_priv_fn_c_names .entry((fid, f.name.clone())) .or_insert_with(|| mangled.clone()); } } else { self.file_priv_fn_c_names .entry((pf.file_id, f.name.clone())) .or_insert(mangled); } } else { self.fn_module_map .entry(f.name.clone()) .or_insert_with(|| modpath); } } } } } } // [M-sync-crossmodule-samename-type-collision] (D381) — collision-aware // module-qualified nominal-type mangling. A user sum/record type is // mangled to `Nova_<Name>` by SIMPLE name (tag `NOVA_TAG_<Name>_<V>`, // ctor `nova_make_<Name>_<V>`, schema keys). Two DIFFERENT types with the // same simple name from DIFFERENT modules in one CU collide into one C // struct/tag-enum/registry-entry (`ErrorKind` from std.io / std.http / // std.encoding.compress). Fix: qualify ONLY the colliding names by their // DEFINING module — every non-colliding name stays byte-identical, so a // CU without collisions produces identical C (`colliding_type_names` // empty → all qualification helpers are no-ops). // // Built from `peer_files`: each PeerFile carries its `file_id` + // `module_name` + `items_here`. Only NON-generic concrete sum/record/ // newtype names participate (generic templates mangle through the mono // path). `emit_file_module` gives any TypeDecl its defining module via // `t.span.file_id`; `file_type_module` resolves a bare colliding // reference to its defining module as visible in the referring file. { use std::collections::BTreeSet; // file_id → module_name (all peer files, incl. imported modules). // Plan 202 Ф.1b: `effective_modpath`, not raw `pf.module_name` — see // the shared doc comment above the fn-axis block. Byte-identical // for every peer whose declaration isn't shared by ≥2 physically // distinct modules (the entire existing corpus). for pf in &module.peer_files { self.emit_file_module.insert(pf.file_id, effective_modpath(pf)); } // name → set of DEFINING modules (distinct module paths declaring a // non-generic type of that simple name). let mut type_def_modules: HashMap<String, BTreeSet<Vec<String>>> = HashMap::new(); // [M-d78-dup-decl-type-cross-import-ambiguous] fix: name → cand // (effective_modpath, possibly `dupN`-tagged) → the DEFINING peer // file's physical anchor (`phys_key_of`, same canonical // dir-for-folder-module/file-for-single-file key the fn/type dup // axis above already computes). Used ONLY as a fallback in branch // (2) below, when the declared-name suffix match can't disambiguate // a `dupN`-tagged cand (its synthetic tag never appears in a real // import path) — see doc there. Empty whenever no cand in this CU // carries a `dupN` tag (the entire pre-Ф.1b corpus), so the // fallback is never even consulted for byte-identical output. let mut type_def_files: HashMap<String, HashMap<Vec<String>, Vec<String>>> = HashMap::new(); let record_def = |td: &TypeDecl, mods: &mut HashMap<String, BTreeSet<Vec<String>>>, m: &[String]| -> bool { // Collision-qualify concrete (non-generic) nominal types with a // POINTER `Nova_<name>*` struct/tag identity used uniformly at // def+ref without alias indirection: Sum and HEAP Record. Value- // records (`NovaValue_`), NamedTuple, Effect/Protocol/TypeSet, // Opaque (nova_rt headers) and generics (mono-path naming) are a // distinct axis — excluded so def/ref qualification stays // consistent (followup for those kinds). // // [fix M-user-type-name-collides-with-stdlib-type-in-c-symbol, // реестр 221.1 №154, форма (б)] Newtype (`type X(i8)`) is now ALSO // qualifiable — its bare `typedef <inner> Nova_<name>;` collides // exactly like Sum/Record when a same-named type exists in // another module (e.g. user `type Sign(i8)` vs std // `runtime.fmt_buf.Sign`, a Sum) → CC-FAIL `typedef redefinition // with different types`. Newtype has NO `Nova_<name>*` pointer // identity (its alias indirection in `type_aliases` is keyed by // the bare SOURCE name, same convention as Sum/Record's // `sum_schemas`/`record_schemas` — only the EMITTED typedef text // needs the qualified base), so it is safe to fold into the same // detection set. Runtime-backed newtypes (OnceCell/Mutex/Atomic* // — hand-written struct in `nova_rt/*.h`, no typedef ever emitted // by us at all, see `debt_is_runtime_backed_newtype`) are excluded // — nothing to qualify. let heap_record = matches!( (&td.kind, td.allocation), (TypeDeclKind::Record(_), crate::ast::AllocKind::Heap) ); let plain_newtype = matches!(td.kind, TypeDeclKind::Newtype(_)) && !Self::debt_is_runtime_backed_newtype(td.name.as_str()); let qualifiable = matches!(td.kind, TypeDeclKind::Sum(_)) || heap_record || plain_newtype; if qualifiable && td.generics.is_empty() && !RUNTIME_DEFINED_TYPES.contains(&td.name.as_str()) { mods.entry(td.name.clone()).or_default().insert(m.to_vec()); true } else { false } }; if module.peer_files.is_empty() { for item in &module.items { if let Item::Type(td) = item { record_def(td, &mut type_def_modules, &module.name); } } } else { for pf in &module.peer_files { for item in &pf.items_here { if let Item::Type(td) = item { // Plan 202 Ф.1b: physical identity, not raw decl — // mirrors the fn axis (see shared doc comment above). let m = effective_modpath(pf); if record_def(td, &mut type_def_modules, &m) { if let Some(phys) = phys_key_of.get(&pf.file_id) { type_def_files .entry(td.name.clone()) .or_default() .entry(m) .or_insert_with(|| phys.clone()); } } } } } } // Colliding = simple name declared in ≥2 distinct modules. for (name, mods) in &type_def_modules { if mods.len() >= 2 { self.colliding_type_names.insert(name.clone()); } } // Per-file resolution for colliding names: which defining module does // this file see for a bare `Name`? Its own module (if it declares it) // or the single module it selectively imports it from. if !self.colliding_type_names.is_empty() { for pf in &module.peer_files { for name in &self.colliding_type_names { let cands = match type_def_modules.get(name) { Some(c) => c, None => continue, }; // (1) The peer's OWN module declares this type — every peer // of a folder-module shares that module's declarations by // Rule C (D281), referencing them WITHOUT any import. So the // peer sees `name` as its own module regardless of which // sibling file physically declares it. // Plan 202 Ф.1b: compare/store `effective_modpath(pf)` — // `cands` is now keyed by physical identity (see above), // not the raw (possibly decl-ambiguous) `pf.module_name`. let pf_modpath = effective_modpath(pf); if cands.contains(&pf_modpath) { self.file_type_module .insert((pf.file_id, name.clone()), pf_modpath); continue; } // (2) Selectively imported from exactly one candidate module. // The import path is the full PACKAGE path (`std.encoding. // compress`) while a module's declared `module_name` may omit // the package root (`encoding.compress`), so match by suffix // relationship (either path is a suffix of the other) rather // than exact equality. The candidate `cand` is always the // DEFINING `module_name`, so `hit` carries that base — keeping // reference qualification identical to the definition base. let path_matches = |cand: &[String], ipath: &[String]| -> bool { cand == ipath || (cand.len() <= ipath.len() && ipath.ends_with(cand)) || (ipath.len() <= cand.len() && cand.ends_with(ipath)) }; // [M-d78-dup-decl-type-cross-import-ambiguous] fix: // `cand` may carry a synthetic `dupN` physical // discriminator (`effective_modpath` above, Plan 202 // Ф.1b — two PHYSICALLY DISTINCT modules forced to // share one `module` declaration). A real import path // is a plain filesystem path from a package root // (`imports::resolve_module_paths` treats `parts` as // a relative `PathBuf`) and NEVER contains a `dupN` // segment, so `path_matches` above can't ever line up // with such a `cand` — every `dupN`-tagged candidate // silently fails to match, regardless of how many // modules actually select `name` (previously this // degraded straight to "no hit", same as a genuine // ambiguity: colliding type left unqualified → C // redefinition risk the moment BOTH physical modules' // colliding type is selectively imported and // referenced by name from outside). Fall back to // matching the DEFINING file's actual on-disk path // (`type_def_files`, built alongside `type_def_modules` // above) against the import path as filesystem // components — this is exactly how the loader itself // resolves `imp.path` to a file, so it disambiguates // `dupN` siblings the declared-name check cannot. let phys_matches = |cand: &[String], ipath: &[String]| -> bool { let Some(phys) = type_def_files.get(name).and_then(|m| m.get(cand)) else { return false; }; let Some(raw) = phys.first() else { return false; }; let mut comps: Vec<String> = std::path::Path::new(raw) .components() .filter_map(|c| match c { std::path::Component::Normal(s) => { Some(s.to_string_lossy().into_owned()) } _ => None, }) .collect(); if let Some(last) = comps.last_mut() { if let Some(stripped) = last.strip_suffix(".nv") { *last = stripped.to_string(); } } ipath.len() <= comps.len() && comps.ends_with(ipath) }; let mut hit: Option<Vec<String>> = None; for imp in &pf.imports { let selects = match &imp.items { None => true, // whole-module import Some(items) => items.iter().any(|it| { it.name == *name || it.alias.as_deref() == Some(name.as_str()) }), }; if !selects { continue; } let found = cands .iter() .find(|c| path_matches(c, &imp.path)) .or_else(|| cands.iter().find(|c| phys_matches(c, &imp.path))); if let Some(cand) = found { if hit.is_none() { hit = Some(cand.clone()); } else if hit.as_deref() != Some(cand.as_slice()) { hit = None; // ambiguous — leave unqualified (checker would reject) break; } } } // (3) [M-http-compress-errorkind-crosspkg-collision]: this // peer's OWN imports didn't resolve it — check its SIBLING // peers (same physical folder-module group, `pf_modpath`). // A bare colliding reference can appear in a peer file that // never itself imports the type by name — e.g. a folder- // module test file (`client_test.nv`) that only imports // `http.{Http}` while matching `e.kind` on a `HttpError` // value (imported transitively via a SIBLING peer's `import // http.{…, ErrorKind, …}`, `client.nv`). The checker // resolves this fine (field access needs no import of the // field's own type), but codegen's per-file bare-name // resolution only consulted THIS file's imports. Every // peer file of one folder-module shares one namespace // (Rule C, D281) — so a resolving import anywhere in the // SAME physical group is authoritative for every sibling, // exactly like a same-group type DECLARATION already is // (condition 1 above). Same ambiguity guard (>1 distinct // resolving module across the whole group → leave // unqualified) as condition 2's own-file scan. if hit.is_none() { for sib in &module.peer_files { if effective_modpath(sib) != pf_modpath { continue; } for imp in &sib.imports { let selects = match &imp.items { None => true, Some(items) => items.iter().any(|it| { it.name == *name || it.alias.as_deref() == Some(name.as_str()) }), }; if !selects { continue; } if let Some(cand) = cands.iter().find(|c| path_matches(c, &imp.path)) { if hit.is_none() { hit = Some(cand.clone()); } else if hit.as_deref() != Some(cand.as_slice()) { hit = None; break; } } } } } if let Some(m) = hit { self.file_type_module.insert((pf.file_id, name.clone()), m); } } } } } // [M-198-f4c-1-privfile-type-not-discriminated]: file-discriminated // naming for a `priv(file) type` colliding with ANOTHER declaration in // a peer file of the SAME folder-module (the D381 block just above // only detects collisions across DISTINCT modules — two peer files // sharing one `module` declaration never populate `colliding_type_names` // for their shared-name types). Detected per physical peer-group // (`effective_modpath`, same physical-identity axis as the fn/type // blocks above): a simple name declared >=2 times within one group is // a collision; every FILE-PRIVATE declaration among those gets a // file-keyed mangled base (`<Name>_f<file_id>`) via // `file_priv_type_c_names`, consulted first by `def_type_base`/ // `ref_type_base`. A name declared only once per group (the // overwhelming common case for `priv(file) type`) never enters this // map — byte-identical C output. { let mut group_type_name_counts: HashMap<Vec<String>, HashMap<String, u32>> = HashMap::new(); for pf in &module.peer_files { let modpath = effective_modpath(pf); let counts = group_type_name_counts.entry(modpath).or_default(); for item in &pf.items_here { if let Item::Type(td) = item { *counts.entry(td.name.clone()).or_insert(0) += 1; } } } for pf in &module.peer_files { let modpath = effective_modpath(pf); let Some(counts) = group_type_name_counts.get(&modpath) else { continue; }; for item in &pf.items_here { if let Item::Type(td) = item { if td.file_private && counts.get(&td.name).copied().unwrap_or(0) >= 2 { let mangled = format!("{}_f{}", td.name, pf.file_id); self.file_priv_type_c_names .entry((pf.file_id, td.name.clone())) .or_insert(mangled); } } } } } // [fix M-samename-export-const-cross-module-c-symbol-collision, // реестр 221.1 №151]: collision-aware qualification for `export // const` — mirrors D381's `type_def_modules` axis (see // `colliding_const_names` field doc for the full rationale + // reference-side resolution strategy/known limit). Count DISTINCT // declaring modules per export-const simple name across the WHOLE // CU; ≥2 → colliding. `effective_modpath`/`emit_file_module` are // already populated by the D381 block above (unconditional, same // scope) — reused here for the definition-site qualifier. { use std::collections::BTreeSet; let mut export_const_def_modules: HashMap<String, BTreeSet<Vec<String>>> = HashMap::new(); if module.peer_files.is_empty() { for item in &module.items { if let Item::Const(c) = item { if c.is_export { export_const_def_modules.entry(c.name.clone()) .or_default() .insert(module.name.clone()); } } } } else { for pf in &module.peer_files { let m = effective_modpath(pf); for item in &pf.items_here { if let Item::Const(c) = item { if c.is_export { export_const_def_modules.entry(c.name.clone()) .or_default() .insert(m.clone()); } } } } } for (name, mods) in &export_const_def_modules { if mods.len() >= 2 { self.colliding_const_names.insert(name.clone()); } } } // Plan 91.12 [M-91.12-const-resolution-via-types] (closed 2026-06-01): // module-private const C-name mangling, production-grade per-peer // resolution (заменил span-based fallback workaround). // // **Алгоритм (паттерн NameResCtx Rule C):** // // 1. Group peer_files по `(parent_dir, module_name)` ключу — каждая // группа = module's name resolution scope (peers одной folder- // module делят declarations namespace). // 2. Для каждой группы: collect её private consts с mangled C-names. // 3. Для каждого peer file (file_id) populate `(peer.file_id, name)` // → mangled для ВСЕХ consts его module-group. Это обеспечивает // корректный lookup на любом Ident use-site внутри module-group // (Rule C: peers share decls namespace). // // Это closes followup [M-91.12-const-resolution-via-types]: lookup // на use-site больше не нуждается в single-candidate fallback — // (file_id, name) однозначно резолвится к module-group's const. // // Exported consts не mangle'ятся — их имя стабильно как cross-module // API; collision между export'нутыми consts двух модулей — ambiguity // error type-checker'а уровня D29. { // Step 1: group by (parent_dir, module_name). use std::path::PathBuf; let mut group_consts: HashMap< (PathBuf, Vec<String>), Vec<(String, String)>, // (source_name, mangled_c_name) > = HashMap::new(); let mut peer_group_key: HashMap<crate::diag::FileId, (PathBuf, Vec<String>)> = HashMap::new(); for pf in &module.peer_files { if pf.module_name.is_empty() { continue; } let dir_key = pf.path.parent() .map(|p| p.to_path_buf()) .unwrap_or_else(|| pf.path.clone()); let group_key = (dir_key, pf.module_name.clone()); peer_group_key.insert(pf.file_id, group_key.clone()); let group_entry = group_consts.entry(group_key).or_default(); for item in &pf.items_here { if let Item::Const(c) = item { if !c.is_export { // Plan 170 (D307): a `priv(file)` const is file-local // — it must NOT be distributed across the group (two // peers may declare the same name with different // values). It gets a file-discriminated C name keyed // ONLY to its own file_id (handled in the dedicated // pass below), and is skipped here so Rule-C // distribution never overwrites a peer's same-named // file-private const with the wrong group name. if c.file_private { continue; } let mangled = format!( "Nova_const_{}_{}", pf.module_name.join("_"), c.name ); group_entry.push((c.name.clone(), mangled)); } } // [M-175-lazy-const-crossmodule-collision] (2026-07-22, // found via spec_tests/conformance/standalone/ // repro_const_dup.nv): module-level `ro NAME = expr` // (`Item::Let`, Pattern::Ident — LetDecl has NO `is_export` // field at all, D24 grammar never allows `export ro` at // module scope) was COMPLETELY absent from this // module-qualification pass — it always fell to the bare // `_nova_const_<name>_value` C symbol (see // `emit_lazy_const`), with NO collision protection // whatsoever. Harmless while every such binding's bare // name happened to be globally unique; broke the moment // Plan 175 Ф.2-v3 made `std.time.duration` (which has // `export const ZERO/SECOND/MINUTE/HOUR Duration = …`) // transitively reachable from EVERY compile-unit (via // `std/prelude/effects.nv`'s new `Time`-schema import) — // a user file's own private `ro ZERO = …` collided with // `Duration.ZERO` at the C symbol level (two DIFFERENT // Nova consts, correctly resolved by the checker in their // own files, but emitting the IDENTICAL unqualified C // name). Treat exactly like a non-exported, non-file- // private `Item::Const` — module-qualified `Nova_const_ // <module>_<name>`, distributed Rule-C style to every peer // in Step 2 below (a module-level `ro` has no file-private // variant to special-case). if let Item::Let(l) = item { // [M-175-lazy-const-crossmodule-collision] fix: a // module-level `ro NAME = expr` binding's pattern is // NOT always `Pattern::Ident` — an ALL-CAPS/PascalCase // bare name (`ZERO`, `SECOND`, …) parses as // `Pattern::Variant { path: [name], kind: Unit }` (the // parser's unit-variant-pattern shape, since a bare // capitalized identifier is AMBIGUOUS with an enum // unit-variant pattern) — mirrors EXACTLY the shape // the pre-existing `Item::Let` emission loop below // (in `emit_module`, right before `emit_lazy_const`) // already matches. Missing this arm is why this whole // branch never fired for `repro_const_dup.nv`'s // `ZERO`/`SECOND`/`THIRD`/`FOURTH` on the first attempt. let name = match &l.pattern { Pattern::Ident { name, .. } => Some(name.clone()), Pattern::Variant { path, kind: VariantPatternKind::Unit, .. } if path.len() == 1 => Some(path[0].clone()), _ => None, }; if let Some(name) = name { let mangled = format!( "Nova_const_{}_{}", pf.module_name.join("_"), name ); group_entry.push((name, mangled)); } } } } // Step 2: distribute group's consts к каждому peer's file_id — // обеспечивает correct lookup на любом Ident use-site внутри // module-group (Rule C). file-private consts excluded above; their // per-file names are populated directly in the pass below. for pf in &module.peer_files { if let Some(gk) = peer_group_key.get(&pf.file_id) { if let Some(consts) = group_consts.get(gk) { for (name, mangled) in consts { self.private_const_c_names .entry((pf.file_id, name.clone())) .or_insert_with(|| mangled.clone()); } } } } } // Plan 170 (D307): file-private FREE-FN mangling. For each peer file, // every `priv(file) fn helper` (free fn, non-external) gets a unique // C symbol keyed by (file_id, source_name). The file discriminator is // the stable peer file_id, so two same-named file-private helpers in // different peer files map to DISTINCT C symbols — no link collision. // The discriminated name is resolved both at the definition emit and // at every call-site (via current_emit_file_id in free_fn_c_name). { for pf in &module.peer_files { if pf.module_name.is_empty() { continue; } for item in &pf.items_here { if let Item::Fn(f) = item { if f.file_private && f.receiver.is_none() && !f.is_external { let mangled = format!( "nova_fn_{}_f{}_{}", pf.module_name.join("_"), pf.file_id, f.name, ); self.file_priv_fn_c_names .entry((pf.file_id, f.name.clone())) .or_insert(mangled); // [Facet-B D307 §1/§3] mirror into the FnDecl-keyed // map (see field doc) — same key, same condition, // so generic-mono call-site dispatch can tell a // file-local CONCRETE overload from a file-local // GENERIC one without consulting the bare-name // (file-oblivious) `mono_fn_decls`/`generic_fns`. self.file_priv_free_fn_decls .entry((pf.file_id, f.name.clone())) .or_insert_with(|| f.clone()); } } } } } // Plan 170 (D307): file-private CONST mangling. A `priv(file) const` // is file-local: it gets a file-discriminated C name keyed ONLY to its // declaring file_id (NOT distributed Rule-C across the group), so two // peers may declare the same name with different values without C // symbol collision. Reuses `private_const_c_names` (the const lookup // map) — the consumer at the Ident use-site already keys by // (file_id, name), and call-sites of a file-private const can only // occur in its own file (resolver enforces E_FILE_PRIV_LEAK otherwise). { for pf in &module.peer_files { if pf.module_name.is_empty() { continue; } for item in &pf.items_here { if let Item::Const(c) = item { if c.file_private && !c.is_export { let mangled = format!( "Nova_const_{}_f{}_{}", pf.module_name.join("_"), pf.file_id, c.name, ); // Insert directly for THIS file only (overwrite any // stale group entry — file-private wins in its file). self.private_const_c_names .insert((pf.file_id, c.name.clone()), mangled); } } } } } // Plan 115 D214: user-level `external fn` registration. Pre-populates // `external_registry` с entries из текущего module — даёт unmangled // `nova_fn_<name>` resolve в emit_call (без него Nova-mangling // делает `nova_fn_<modpath><name>` который не matches C shim). // // Plan 115 v1 ограничение: только FREE external fn (без receiver) и // с simple types. Generic / receiver-method external fn — followup // `[M-115-external-fn-method]`. match super::external_registry::ExternalRegistry::from_module(module) { Ok(user_reg) => { for (key, decls) in user_reg.by_key { // [M-172.1-d174] (U.1.3b sync-inline): дедуп по c_name — // inline-merge того же .nv через `import` (sync.nv) нёс ТЕ ЖЕ // декларации, что load_builtins; без дедупа overload-count // удваивался → call-site манглился param-суффиксом // (`Nova_AtomicI64_method_fetch_add__int64_t`), которого нет // в C-рантайме → undefined symbol на линковке. // [M-172.1-extern-cname-dedup-overloads] (Plan 174.6 M1): // dedup key is (c_name, param_c_types), NOT c_name alone. A // TRUE duplicate — same C symbol AND same signature, from the // builtin-supply vs `import` double-feed — is silently // collapsed (byte-identical to the prior behaviour). But two // decls sharing a c_name with DIFFERENT param_c_types are a // genuine FFI overload collision: one C symbol cannot carry // two ABIs, and the prior `c_name`-only dedup would silently // drop the second signature → mis-resolved overload at the // call site. Reject with a compile error instead of swallowing. let slot = self.external_registry.by_key.entry(key).or_default(); for d in decls { if let Some(existing) = slot.iter().find(|e| e.c_name == d.c_name) { if existing.param_c_types != d.param_c_types { return Err(format!( "[E_FFI_C_NAME_OVERLOAD_CONFLICT] external fn `{}` maps \ two different signatures onto one C symbol `{}` \ (param C-types {:?} vs {:?}) — a C symbol has exactly \ one ABI. Give the overloads distinct C names, or unify \ them to a single signature.", d.name, d.c_name, existing.param_c_types, d.param_c_types, )); } // identical signature → true duplicate, skip. } else { slot.push(d); } } } // Plan 172.1 REG-0 (ADDITIVE keystone, §10-предусловие): import-resolved модуль // ТАКЖЕ несёт receiver_types + type_decls (`from_module` их строит, но merge ранее // МОЛЧА выбрасывал — вливал только by_key). Вливаем dedup'нуто (паттерн // `merge_from_module`) → codegen-реестр знает типы из import-resolved std ДО снятия // `include_str!` (REG-5). Ничего НЕ удаляем: пока `load_builtins` снабжает — dedup // отсекает дубли (byte-identical на корпусе), для не-снабжаемых модулей — добавляет. for rt in user_reg.receiver_types { if !self.external_registry.receiver_types.contains(&rt) { self.external_registry.receiver_types.push(rt); } } for td in user_reg.type_decls { if !self.external_registry.type_decls.iter().any(|t| t.name == td.name) { self.external_registry.type_decls.push(td); } } } Err(_) => { // Type-checker emits a более user-friendly diagnostic для // type-resolution issues — registry merge молча skip'ает. } } // Plan 91.12 Ф.-1 (D282): collect `extern "C" fn` names — these are called // with their literal C name (no nova_fn_ prefix, not in external_registry). for item in &module.items { if let Item::Fn(f) = item { if f.is_external && f.extern_abi.as_deref() == Some("C") { self.c_literal_extern_fns.insert(f.name.clone()); } } } // Plan 115 D214: pre-register mono'd tuple instances для external fn // return types — гарантирует emit'ит typedef в финальный output (без // этого call-site видит unresolved `_NovaTuple_2_8_nova_ptr_8_nova_int`). for item in &module.items { if let Item::Fn(f) = item { if !f.is_external { continue; } if let Some(TypeRef::Tuple(elems, _)) = &f.return_type { let mut elem_cs: Vec<String> = Vec::with_capacity(elems.len()); let mut all_concrete = true; for e in elems { match self.type_ref_to_c(e) { Ok(c) if !c.is_empty() && c != "void*" => elem_cs.push(c), _ => { all_concrete = false; break; } } } if all_concrete && !elem_cs.is_empty() { self.register_mono_tuple(&elem_cs); } // Plan 186 [bug-2 audit-197 fix]: without an explicit // prototype, the call site (buried in some method body // below) is the ONLY mention of this symbol in the // generated `.c` — C falls back to an implicit `int // foo()` declaration and the assignment into the // `_NovaTupleN_...` temp fails to compile ("incompatible // type 'int'"). Scoped to FREE fns (no receiver) — the // shape actually hit by user FFI declarations // (`extern "nova"/"C" fn foo(...) -> (A, B)`); receiver // methods resolve through a separate dispatch path. // Computed SEPARATELY from `elem_cs` above (own loop, own // `all_concrete_full`): the pre-existing loop deliberately // excludes bare `void*` elements from ITS registration // (an unrelated erased-generic guard) — but `*()` (a // genuinely concrete opaque pointer, sqlite_mini.nv's // `nova_fn_sqlite3_open`-style handle) also resolves to // literal `"void*"`, so that guard would silently skip // the one case this fix targets. The prototype's element // types must match what the call-site destructure ACTUALLY // registers (unfiltered `type_ref_to_c`, `void*` allowed) // — the compiler error confirms the call site itself // already resolves + registers the real (unfiltered) // tuple typedef; only the missing prototype is the gap. if f.receiver.is_none() { let mut full_elem_cs: Vec<String> = Vec::with_capacity(elems.len()); let mut all_concrete_full = true; for e in elems { match self.type_ref_to_c(e) { Ok(c) if !c.is_empty() => full_elem_cs.push(c), _ => { all_concrete_full = false; break; } } } if all_concrete_full && !full_elem_cs.is_empty() { let mut param_cs: Vec<String> = Vec::with_capacity(f.params.len()); let mut params_ok = true; for p in &f.params { match self.type_ref_to_c(&p.ty) { Ok(c) if !c.is_empty() => param_cs.push(c), _ => { params_ok = false; break; } } } if params_ok { let mangled = self.register_mono_tuple(&full_elem_cs); let c_name = self.free_fn_c_name(&f.name); self.extern_fn_tuple_protos.push_str(&format!( "extern {} {}({});\n", mangled, c_name, param_cs.join(", "), )); } } } } } } // Plan 33.3 Ф.9.2 (D24): pre-pass — собрать invariants для record-типов. // Used в emit_record_lit для wrap'а конструкции в runtime-check. for item in &module.items { if let Item::Type(td) = item { if !td.invariants.is_empty() { let invs: Vec<(Expr, Span, Option<String>, Option<Expr>, bool)> = td.invariants.iter() .map(|c| (c.expr.clone(), c.span, c.message.clone(), c.message_expr.clone(), c.debug_only)).collect(); self.record_invariants.insert(td.name.clone(), invs); } } } // Plan 78 Ф.2 (2026-05-22): pre-populate `sum_schemas["Option"]` / // `["NovaOpt_nova_int"]` УДАЛЁН. Option полностью мономорфизирован // (Plan 14/59) — `NovaOpt_<T>` value-struct; pattern-matching // резолвит variants через mono-тип scrutinee, не через legacy // `sum_schemas`. (Result-pre-populate удалён ещё Plan 62.A.bis Ф.4 — // тот же приём; см. docs/plans/78-prelude-codegen-single-source.md.) // D26 prelude: Error — record для quick errors с msg. // Декларирован в spec/decisions/08-runtime.md; runtime тип // в nova_rt/array.h (Nova_Error). Регистрируем schema чтобы // codegen видел Nova_Error*.msg как nova_str и эмитил // Nova_Error_static_new для Error.new(...). { let mut err_schema = HashMap::new(); err_schema.insert("msg".to_string(), "nova_str".to_string()); self.record_schemas.insert("Error".to_string(), err_schema); self.method_receivers.insert( "new".to_string(), ("Error".to_string(), false), ); } // Plan 53 Ф.6.4: register Nova_ChannelPair schema so the generic // `pattern_bind_typed` path can destructure `let { tx, rx } = Channel.new(cap)` // without a hardcoded special-case. `Nova_ChannelPair` is a C-runtime // value-type (declared in nova_rt/channel.h) — schema mirrors the C // struct layout. `is_value_type` already returns true for it so the // generic path uses `.` accessor. { let mut cp_schema = HashMap::new(); cp_schema.insert("tx".to_string(), "Nova_ChanWriter*".to_string()); cp_schema.insert("rx".to_string(), "Nova_ChanReader*".to_string()); self.record_schemas.insert("ChannelPair".to_string(), cp_schema); } // [M-open-range-len-source-hardcoded] Ф.1 (REVERTED — regression // 2026-07-24, integrator mega-CU): a global `record_schemas["str"]` // registration was tried here to make str's `len` visible to // `structural_len_field_ty`/`structural_len_field_access`. That // regressed `neg_str_from_retracted` (D410: `str.from(5)` must stay // a compile error) — putting `"str"` in `record_schemas` at all // revives an UNRELATED generic method/`.from` resolution path that // keys off "is this name in record_schemas", independent of the // `len` field content. str never needs the GLOBAL registry entry // for slice-materialization purposes anyway: its open-range `end` // is computed by its OWN dedicated branch below (`obj_ty == // "nova_str"`), which the generic structural reroute explicitly // excludes BEFORE ever consulting `record_schemas` (see the // `obj_ty != "nova_str"` guard ahead of `structural_len_field_ty` // in the `ExprKind::Index` Range-arm) — so str's `len` access never // actually went through this global entry in practice. Removed // rather than narrowed: no minimal "ignore me for `.from`" tag on a // schema entry existed to reach for, and str doesn't need the // entry at all. // Plan 103.1 Ф.6: Pre-register MemOrdering in sum_schemas + // sum_schema_registry so test files can use `Relaxed`/`Acquire`/etc. // as unqualified variant names without `import std.runtime.sync`. // MemOrdering variants are unique — no collision with user types. // Coordinated with NOVA_TAG_MemOrdering_* in sync_primitives.h. { let mem_variants = ["Relaxed", "Acquire", "Release", "AcqRel", "SeqCst"]; if !self.sum_schemas.contains_key("MemOrdering") { let mut mem_schema: HashMap<String, Vec<String>> = HashMap::new(); for v in &mem_variants { mem_schema.insert(v.to_string(), Vec::new()); } let variant_order: Vec<String> = mem_variants.iter().map(|s| s.to_string()).collect(); self.sum_schema_registry.register_user_sum( "MemOrdering", &mem_schema, "Nova_MemOrdering", super::sum_schema_registry::SumAbi::PointerErrorLike, &variant_order, ); self.sum_schemas.insert("MemOrdering".to_string(), mem_schema); } } // Plan 78 Ф.2 (2026-05-22): хардкод pre-populate `sum_schemas // ["RuntimeError"]` + `record_variant_field_*` для IndexOutOfBounds // УДАЛЁН. RuntimeError объявлен в `std/prelude/errors.nv` — // `emit_type_decl` (RUNTIME_DEFINED_TYPES ветка) строит sum-schema // и record-variant-метаданные из этой декларации. Единственный // источник правды — `.nv`, без хардкод-зеркала. // Pre-register Fail as a built-in effect (D25 / D62 / D65). // Operation: `fail(msg str) -> nova_unit`. `throw expr` desugars to // `Fail.fail(expr)` — same dispatch path as any other effect operation. // Default handler installed by runtime (Nova_Fail_fail) calls nova_throw, // which longjmp's to nearest fail-frame (test_frame or spawn-entry frame). // User can override via `with Fail = (msg) => ... { body }` (D31 sugar). { let mut fail_schema: HashMap<String, (Vec<String>, String)> = HashMap::new(); fail_schema.insert("fail".to_string(), (vec!["nova_str".into()], "nova_unit".into())); self.effect_schemas.insert("Fail".to_string(), fail_schema); } // Plan 175 Ф.1 (D316 — единый источник схемы): хардкод // `effect_schemas["Time"]` УДАЛЁН. `Time` (D11/D14/D62) объявлен в // `std/time/duration/time_effect.nv` (Plan 175.2 Ф.2-v4 П6 — // moved OUT of prelude; was `std/prelude/effects.nv` Ф.2-v2..Ф.3) — // `emit_type_decl` (RUNTIME_DEFINED_TYPES ветка, TypeDeclKind::Effect) // строит `effect_schemas["Time"]` из этой декларации (симметрично // RuntimeError/MemOrdering sum-schema, 172.1 U.1) — module path is // irrelevant to this lookup (keyed by effect NAME). Единственный // источник правды — `.nv`, без хардкод-зеркала. Int-провод сохранён // (Ф.1 не меняет поведение): // `sleep(ms int)->()`, `now_unix_ms()->int`, `now_monotonic_ns()->int` // (D316 amend, 2026-07-06: операции переименованы с единицей в // имени, owner-side-task вне формальной Ф-нумерации плана 175; // wire raw i64; Nova-side оборачивается в Monotonic — см. // std/time/duration.nv). Типизация опов — Ф.2/Ф.3. // // Plan 65 Ф.5: `Time.after(ms)` REMOVED — заменён // `ChanReader.close_after(Duration)` (D94); legacy-форма → E5101. // // Plan 175 Ф.1 / Q1: 5 timer-observability-счётчиков вынесены из // `Time` в отдельный `TimerMetrics`-эффект (тоже из .nv, direct-C // dispatch `Nova_TimerMetrics_timer_*` в nova_rt/channels.h). // Pre-registered `Mem` built-in effect schema REMOVED (D76 amend, // [M-mem-effect-demote-to-namespace], 2026-08-01): `Mem` is no // longer an effect — `export type Mem` (std/prelude/effects.nv) is // now a plain namespace type, and `Mem.alloc_count()`/etc. are // ordinary static-method fns (Nova-body wrappers over `extern "C" // nova_gc_alloc_count`/etc.) that flow through the SAME generic // static-method codegen path as any other user type (e.g. // `RawMem.copy_n`) — no schema pre-registration needed. // Plan 04 Этап 6: Buffer удалён из языка (REMOVED). Заменён на // StringBuilder/WriteBuffer/ReadBuffer split. Старая Q-buffer // регистрация (record_schemas + method_receivers для add_*/ // into_str_unchecked) — удалена. // Plan 12: register built-in opaque types и method_receivers // automatically из ExternalRegistry (single source of truth — // std/runtime/builtins.nv). Hard-coded таблицы для StringBuilder/ // WriteBuffer/ReadBuffer удалены. for recv_ty in self.external_registry.receiver_types.clone() { // primitive receivers (str; Plan 196.3 added f64/f32/int via // std/runtime/math.nv's now-imported extern decls) — не record, // не нужен schema. Только user-defined opaque types // (StringBuilder/WriteBuffer/...). if matches!(recv_ty.as_str(), "str" | "f64" | "f32" | "int") { continue; } self.record_schemas.entry(recv_ty.clone()) .or_insert_with(HashMap::new); } // method_receivers (single-key, last-wins) — для backward compat // dispatch'ей которые ещё не мигрированы на multi-overload путь. // Plan 11 multi-overload + Plan 12 registry — основные пути; этот // legacy registry остаётся для conservative routing. // // NOTE: используем `entry().or_insert()` чтобы НЕ перетирать // existing entries из prelude (Error.new etc.). Single-key // registry — last-wins, но prelude занят сначала. for (key, decls) in self.external_registry.by_key.clone().into_iter() { let (recv_ty, method_name) = key.clone(); if recv_ty.is_empty() { continue; } // free fns if let Some(decl) = decls.first() { self.method_receivers.entry(method_name) .or_insert((recv_ty, decl.is_instance)); } // Plan 103.2: also register ALL ExternalRegistry entries into // method_overloads so multi-overload dispatch (line ~17000) can // select the correct suffix-bearing C name. // Single-overload: c_name has no suffix → same as fallback. // Multi-overload: c_name has _T or _MemOrdering suffix → required. for decl in &decls { let sig = MethodSig { param_c_types: decl.param_c_types.clone(), return_c_type: decl.return_c_type.clone(), is_instance: decl.is_instance, is_external: true, is_delegated: false, c_name: decl.c_name.clone(), variadic_last: false, param_defaults: vec![None; decl.param_c_types.len()], // Plan 128 Ф.1: external registry methods have no AST mutability // marker (FFI/runtime entries) — default false. Ф.2 can extend // ExternalDecl/ExternalRegistry to carry mutability if needed. recv_mutable: false, // Plan 184 (Р13/Р14): external entries carry no per-param mode // markers — empty vector degrades matchers to pre-184 behaviour. param_modes: Vec::new(), // U.4.3 c2.2: external-registry entries have no single source FnDecl. fn_span: None, }; self.register_method_overload(key.clone(), sig); // Plan 83.12: register novares struct for non-trivial Result returns // (e.g. Result[TcpListener,str] → NovaRes_Nova_TcpListener_p_nova_str*). // Must happen before any code is emitted so that novares_value_types // is populated when infer_expr_c_type / resolve_result_te are called. if let Some((ok_c, err_c)) = &decl.result_ok_err { self.register_novares_decl(ok_c, err_c); } } } self.emit_preamble(); // Plan 36 followup: pre-pass — forward-decl всех user types // через `typedef struct Nova_T Nova_T;`. Splice'ится в маркер // `/*__USER_TYPE_FWD_DECLS__*/` (ставится в preamble ДО // `/*__NOVAOPT_TYPEDEFS__*/`). Без этого NovaOpt_<T> typedef'ы // ссылаются на не-объявленный `Nova_T` (NovaOpt splice'ится // ПЕРЕД emit_type_decl). // Collect locally-defined type names first. let local_types: HashSet<String> = module.items.iter() .filter_map(|i| if let Item::Type(t) = i { Some(t.name.clone()) } else { None }) .collect(); // Locally-defined effect types — emit_effect_type generates an anonymous // typedef struct, which would conflict with our named forward decl. let local_effects: HashSet<String> = module.items.iter() .filter_map(|i| if let Item::Type(t) = i { if matches!(t.kind, TypeDeclKind::Effect(_)) { Some(t.name.clone()) } else { None } } else { None }) .collect(); for item in &module.items { if let Item::Type(t) = item { // Plan 172.1 U.1.3b (§0 single source): типы, определённые в C-runtime-хедерах // (RUNTIME_DEFINED_TYPES), НЕ получают codegen forward-typedef — header даёт их // typedef; иначе redefinition с другим struct-тегом (`Nova_MutexGuard` vs header // `Nova_MutexGuard_s`) при INLINE sync/atomics через `import`. Тот же список // skip'ает struct-body в emit_type_decl (§0). До этого fwd-decl-loop его НЕ // проверял → inline guard-типов давал typedef redefinition. if RUNTIME_DEFINED_TYPES.contains(&t.name.as_str()) { // Plan 248 (wave 3, D447 #no_copy): the 11 `Atomic*` types // are RUNTIME_DEFINED_TYPES *value*-records now (moved off // the old pointer-newtype `(*())` shape) — their C struct // lives hand-written in sync_primitives.h as `NovaValue_ // <Name>` (same prefix every OTHER value-record uses, §0 // single source). Register the `type_aliases`/ // `value_record_names` entries HERE, at the SAME early // point the ordinary value-record branch just below does // (Plan 91.12 V2 comment: "pre-body passes that run // before emit_value_record_type fills the alias") — // `emit_type_decl`'s own RUNTIME_DEFINED_TYPES-gated // branch (struct-BODY skip, since the header owns it) // runs too LATE: `emit_fn_forward_decl` computes a // method's `nova_self` C type (`receiver_c_type` → // `type_aliases.get`) BEFORE every type in the module // has been visited, so without this early registration // it fell to the generic-fallback `Nova_<Name>*` heap- // pointer convention — confirmed by a real CC-FAIL // (`compare_exchange`'s forward decl declaring `nova_self` // as `Nova_AtomicI64*`, an undeclared type once the // struct itself is `NovaValue_AtomicI64`). No forward- // decl TEXT is pushed (unlike the ordinary branch below) // — the header already provides the typedef. if let TypeDeclKind::Record(_) = &t.kind { use crate::ast::AllocKind; if matches!(t.allocation, AllocKind::Value) { self.type_aliases.insert( t.name.clone(), format!("NovaValue_{}", t.name)); self.value_record_names.insert(t.name.clone()); } } continue; } match &t.kind { TypeDeclKind::Record(_) => { // Plan 139.1 (lang-item str): `str` — value-record, но // его C-тип — hand-written typedef `nova_str` в // nova_rt/nova_rt.h (ABI-bridge), НЕ `NovaValue_str`. // Skip любой forward-decl для str — иначе // `typedef struct NovaValue_str` конфликтует с // `nova_str`. Symmetric с RUNTIME_DEFINED_TYPES skip // в emit_type_decl. См. core.nv `type str value priv`. if t.name == "str" { continue; } // Plan 124.8 V2 (D226): value-records emit own // `typedef struct NovaValue_X NovaValue_X;` в // emit_value_record_type. Skip Nova_X forward decl // (которая wouldn't have a corresponding struct // definition) для value-records. use crate::ast::AllocKind; if matches!(t.allocation, AllocKind::Value) { self.user_type_fwd_decls.push_str(&format!( "typedef struct NovaValue_{0} NovaValue_{0};\n", t.name)); // Pre-register type_alias so type_ref_to_c returns // the correct value type (not Nova_X*) when called // from emit_effect_type or other pre-body passes that // run before emit_value_record_type fills the alias. self.type_aliases.insert( t.name.clone(), format!("NovaValue_{}", t.name)); self.value_record_names.insert(t.name.clone()); } else { // [M-sync-crossmodule…] fwd-decl must match the qualified // definition base for a colliding heap record. let fb = self.def_type_base(&t.name, t.span.file_id); self.user_type_fwd_decls.push_str(&format!( "typedef struct Nova_{0} Nova_{0};\n", fb)); } } TypeDeclKind::Sum(variants) => { // [M-sync-crossmodule…] qualified base for a colliding sum. let fb = self.def_type_base(&t.name, t.span.file_id); if variants.is_empty() { // Plan 72 P1-B: empty sum → `typedef int64_t Nova_X` (no struct). // Emit the typedef here (into user_type_fwd_decls, which precedes // /*__NOVAOPT_TYPEDEFS__*/) so that NovaOpt_Nova_X_p references to // `Nova_X*` compile without "unknown type name Nova_X". if !RUNTIME_DEFINED_TYPES.contains(&t.name.as_str()) { self.user_type_fwd_decls.push_str(&format!( "typedef int64_t Nova_{};\n", fb)); } } else { self.user_type_fwd_decls.push_str(&format!( "typedef struct Nova_{0} Nova_{0};\n", fb)); } } _ => {} } } } // Also forward-decl external user types and effect vtables referenced // in this module's type fields and function signatures. Without this, // imported types (e.g. Duration from std.time.duration) and effect // vtables (e.g. NovaVtable_Random from Handler[Random]) cause // 'unknown type name' errors. { let mut external_names: HashSet<String> = HashSet::new(); let mut vtable_names: HashSet<String> = HashSet::new(); for item in &module.items { match item { Item::Type(t) => Self::collect_typeref_names_in_typedecl( t, &mut external_names, &mut vtable_names), Item::Fn(f) => { for p in &f.params { Self::collect_typeref_names(&p.ty, &mut external_names, &mut vtable_names); } if let Some(r) = &f.return_type { Self::collect_typeref_names(r, &mut external_names, &mut vtable_names); } // Direct effect annotations on fn → vtable names for e in &f.effects { if let TypeRef::Named { path, .. } = e { if let Some(n) = path.last() { vtable_names.insert(n.clone()); } } } } _ => {} } } const BUILTIN_TYPE_NAMES: &[&str] = &[ "int", "uint", "i64", "i32", "i16", "i8", "u64", "u32", "u16", "u8", // Plan 133: usize/isize removed — int/uint are address-sized. "f64", "f32", "bool", "str", "char", // Plan 97 Ф.3 (D142): `Handler` → `Effect`. "Option", "Result", "Self", "Effect", "CancelToken", // Plan 76: bottom-тип `never` — строчный встроенный примитив. "never", "Error", // Plan 62.C: RuntimeError имеет real struct в array.h // (skipped в RUNTIME_DEFINED_TYPES → emit_type_decl skipped // → нет local emit, нужен fwd-decl skip чтобы не дублировать // forward-decl). ReadBufferError — НЕ скипается т.к. codegen // сам эмитит его struct через emit_sum_type, и fwd-decl // нужен для cross-file refs (нативный flow). "RuntimeError", ]; // Runtime types defined in nova_rt/*.h with anonymous typedef'd structs. // A named forward decl `typedef struct Nova_X Nova_X;` would conflict // with the runtime's `typedef struct { ... } Nova_X;` (different types). const BUILTIN_RUNTIME_TYPES: &[&str] = &[ "Result", "Error", "RuntimeError", // Plan 109 (D179): StringBuilder removed from BUILTIN_RUNTIME_TYPES — // now a Nova-defined record type; needs local typedef struct fwd-decl. // Plan 91.12 (D126 retract): WriteBuffer и ReadBuffer удалены отсюда — // тоже Nova-defined records, нуждаются в local fwd-decl. "ChanReader", "ChanWriter", "ChannelPair", "AtomicInt", "AtomicBool", "Mutex", "WaitGroup", "Once", // Plan 103.3: RwLock + ReentrantMutex pre-declared in sync_primitives.h. "RwLock", "ReentrantMutex", "Timestamp", // Plan 103.1: MemOrdering pre-declared in sync_primitives.h. // Named forward decl `typedef struct Nova_MemOrdering Nova_MemOrdering;` // would conflict with the pre-declared typedef struct. "MemOrdering", // Plan 103.2: sized integer atomics, all pre-declared in // sync_primitives.h. No local fwd-decl needed. Plan 207 // (2026-07-16 consolidation): AtomicPtr removed (int-proxy // duplicate, Plan 103.7 pending); AtomicUint here is the // former Usize spelling (AtomicInt already listed above, // absorbing both the former Isize spelling and the former // int32-backed legacy AtomicInt). "AtomicI8", "AtomicI16", "AtomicI32", "AtomicI64", "AtomicU8", "AtomicU16", "AtomicU32", "AtomicU64", "AtomicUint", // Plan 103.9 (D174): consume guard types pre-declared in sync_primitives.h // with a `_s`-suffix anonymous struct. Named fwd-decl `typedef struct // Nova_MutexGuard Nova_MutexGuard;` conflicts (different tag). Skip. "MutexGuard", "ReadGuard", "WriteGuard", "Permit", "OnceGuard", // === PLAN-103.4 PREDECLARED TYPES (alphabetical, parallel-agent) === /* AGENT-B */ "Barrier", /* AGENT-D */ "Condvar", "WaitResult", /* AGENT-C */ "CountDownLatch", /* AGENT-A */ "Semaphore", // === END PLAN-103.4 PREDECLARED TYPES === ]; // [M-codegen-emission-nondeterminism] fix (2026-07-20): `external_names`/ // `vtable_names` are `std::collections::HashSet<String>` — Rust's default // `RandomState` hasher seeds per-PROCESS, so iterating them directly gives // a DIFFERENT fwd-typedef order on every `nova build` invocation (same set // of names, shuffled). Sort into a Vec by name (stable, meaningful key — // matches the C symbol identity) before emitting; this only reorders sibling // fwd-decls that have no dependency on each other (plain `typedef struct X X;` // opaque forward decls — order among them is semantically inert), so this is // purely a tie-break, not a topo-order change. let mut external_names_sorted: Vec<String> = external_names.into_iter().collect(); external_names_sorted.sort(); for name in external_names_sorted { if local_types.contains(&name) { continue; } if BUILTIN_TYPE_NAMES.contains(&name.as_str()) { continue; } if BUILTIN_RUNTIME_TYPES.contains(&name.as_str()) { continue; } // Only emit forward decl for names starting with uppercase // (user-defined types map to Nova_Name*). if name.chars().next().map(|c| c.is_uppercase()).unwrap_or(false) { self.user_type_fwd_decls.push_str(&format!( "typedef struct Nova_{0} Nova_{0};\n", name)); } } // Built-in vtables defined in nova_rt/effects.h — skip. // №277: "Time" RESTORED — unconditional (unlike `local_effects`, // which needs module.items presence); mirrors emit_effect_type's // own unconditional name=="Time" skip. // "Mem" REMOVED (D76 amend, [M-mem-effect-demote-to-namespace], // 2026-08-01): no longer an effect, no vtable — a plain // namespace type, forward-declared (if at all) through the // ordinary `external_names`/`local_types` path above, not this // vtable-specific allowlist. const BUILTIN_VTABLE_NAMES: &[&str] = &["Fail", "TimerMetrics", "Time"]; // [M-codegen-emission-nondeterminism] fix: same HashSet-order issue as // `external_names` above — sort before emitting. let mut vtable_names_sorted: Vec<String> = vtable_names.into_iter().collect(); vtable_names_sorted.sort(); for name in vtable_names_sorted { if BUILTIN_VTABLE_NAMES.contains(&name.as_str()) { continue; } // Local effects — emit_effect_type generates an anonymous typedef // which would conflict with a named forward decl. Skip. if local_effects.contains(&name) { continue; } if name.chars().next().map(|c| c.is_uppercase()).unwrap_or(false) { self.user_type_fwd_decls.push_str(&format!( "typedef struct NovaVtable_{0} NovaVtable_{0};\n", name)); } } } // Plan 91.9 (D186) — populate type_impl_protocols registry from // `#impl(P1 + P2 + ...)` annotations. Used to gate bare-call // synthesis (try_synthesize_default_method). for item in &module.items { if let Item::Type(t) = item { if !t.impl_protocols.is_empty() { self.type_impl_protocols.insert( t.name.clone(), t.impl_protocols.iter().cloned().collect(), ); } } } // Plan 196.8 [M-primitive-receiver-bounded-blanket-dispatch]: populate // type_set_members from D310 `type Name set A | B | C` declarations // (e.g. `Ints` in prelude/protocols.nv). A bounded blanket's bound may // be a type-set rather than a protocol — `type_impl_protocols` above // never carries membership for those (primitives get no `#impl` // entry), so the Plan 164 Ф.3 dispatch guard's `protocols_match` // would treat EVERY type-set-bounded blanket as never matching a // primitive receiver. Members are stored under their bare Nova name // (`i64`, `int`, `u8`, ...) — the same vocabulary `debt_nova_type_ // name_from_c` produces for a primitive receiver's materialized C type. for item in &module.items { if let Item::Type(t) = item { if let TypeDeclKind::TypeSet(members) = &t.kind { let names: Vec<String> = members.iter() .filter_map(|m| match m { crate::ast::TypeRef::Named { path, .. } => path.last().cloned(), _ => None, }) .collect(); if !names.is_empty() { self.type_set_members.insert(t.name.clone(), names); } } } } // Plan 154.1 (D268) — method-level `#impl(P)` (`#impl(Debug)` before a // `fn T @m`) ALSO binds P to the receiver type T, exactly as if P were // listed on T's `type` declaration. Same `type_impl_protocols` registry, // same bare-call-synthesis gate. The checker has already validated the // signature (verify_method_impl_protocols); here we only record the bond. for item in &module.items { if let Item::Fn(f) = item { if f.impl_protocols.is_empty() { continue; } if let Some(recv) = &f.receiver { let entry = self.type_impl_protocols .entry(recv.type_name.clone()) .or_default(); for p in &f.impl_protocols { entry.insert(p.clone()); } } } } // [M-protocol-embed-vtable-missing-method] (Plan 221 followup, found // while closing [M-protocol-box-callarg-vtable-incomplete]): the // checker flattens `use`-embeds transitively when validating a // protocol's method-set (`types/mod.rs::flatten_dfs` / // `protocol_missing_methods` — a protocol WITH `use Base` is treated // as if it directly declared `Base`'s methods too, D145). Codegen's // `protocol_method_registry` used to insert the RAW `t.kind`'s // `methods` only, ignoring `embeds` entirely — the vtable STRUCT // (built from this registry in `emit_protocol_box_typedef`) and the // vtable INSTANCE (filled in `emit_protocol_vtable_companion`) then // had no field/thunk for an embedded method, // so calling it through a protocol-box (`g.base_greet()` where // `EmbGreeter` embeds `EmbBase`) failed to compile: `no member named // 'base_greet' in struct NovaVtable_EmbGreeter`. Fix: flatten here, // BEFORE registration, so the registry (and everything built from // it) sees the same method-set the checker already validated // against. `direct` is scanned from BOTH `module.items` and // `peer_files` (cross-file embeds — e.g. a protocol declared in one // file embedding one from another peer file in the same // compile-unit — must resolve too, mirroring the checker's // CU-wide `types_get_for_file`/`self.types` lookup). Bag-union, // cycle-guarded via `seen` (a genuine `use`-cycle is diagnosed // separately by the checker's `E_PROTOCOL_EMBED_CYCLE` — here a // revisited name during the DFS just stops contributing further // methods, never infinite-loops), mirrors `types/mod.rs::flatten_dfs` // exactly (same duplicate/cycle policy — dup-detection is the // checker's job, not codegen's). let mut protocol_direct: HashMap<String, (Vec<String>, Vec<EffectMethod>, Vec<TypeRef>)> = HashMap::new(); { let mut collect = |t: &crate::ast::TypeDecl| { if let crate::ast::TypeDeclKind::Protocol { methods, embeds } = &t.kind { let type_params: Vec<String> = t.generics.iter().map(|g| g.name.clone()).collect(); protocol_direct.insert( t.name.clone(), (type_params, methods.clone(), embeds.clone()), ); } }; for item in &module.items { if let Item::Type(t) = item { collect(t); } } for pf in &module.peer_files { for item in &pf.items_here { if let Item::Type(t) = item { collect(t); } } } } fn flatten_protocol_methods_codegen( name: &str, direct: &HashMap<String, (Vec<String>, Vec<EffectMethod>, Vec<TypeRef>)>, seen: &mut HashSet<String>, out: &mut Vec<EffectMethod>, ) { if !seen.insert(name.to_string()) { return; } let Some((_, methods, embeds)) = direct.get(name) else { return; }; for m in methods { out.push(m.clone()); } for e in embeds { if let TypeRef::Named { path, .. } = e { if let Some(emb_name) = path.last() { flatten_protocol_methods_codegen(emb_name, direct, seen, out); } } } } let flattened_protocol_methods = |name: &str, direct: &HashMap<String, (Vec<String>, Vec<EffectMethod>, Vec<TypeRef>)>| -> Vec<EffectMethod> { let mut out = Vec::new(); let mut seen = HashSet::new(); flatten_protocol_methods_codegen(name, direct, &mut seen, &mut out); out }; // Plan 97.1 Ф.2 (D142): pre-register non-generic protocol-методы // в `protocol_method_registry`. Generic-protocol'ы регистрируются // в loop ниже (с t.generics.is_empty() == false), но non-generic // (`type Locker protocol { ... }` без [T]) тоже нужны для // emit_protocol_lit (protocol-литерал в expression-position). let mut non_generic_protocols: Vec<String> = Vec::new(); for item in &module.items { if let Item::Type(t) = item { if t.generics.is_empty() { if let crate::ast::TypeDeclKind::Protocol { .. } = &t.kind { self.protocol_types.insert(t.name.clone()); let flat_methods = flattened_protocol_methods(&t.name, &protocol_direct); self.protocol_method_registry.insert( t.name.clone(), (Vec::new(), flat_methods), ); // [fix №154] declaring file — see `protocol_decl_file` doc. self.protocol_decl_file.insert(t.name.clone(), t.span.file_id); non_generic_protocols.push(t.name.clone()); } } } } // Plan 97.1 Ф.3 (D142): pre-emit NovaVtable_<Proto> + NovaBox_<Proto> // typedef'ы для non-generic protocol'ов. Это делает их доступными // в type_ref_to_c (которая `&self`-only и не может вызывать // emit_protocol_box_typedef on-demand). for proto_name in non_generic_protocols { let _ = self.emit_protocol_box_typedef(&proto_name, &[]); } // [M-protocol-box-callarg-vtable-incomplete] (Plan 221 A-B4): pre-pass — // register `fn_protocol_params` (D142 protocol-typed parameter → // NovaBox_* pre-box info) for EVERY fn BEFORE any fn body is emitted. // Previously this table was populated only inside `emit_fn` itself // (see below, "Plan 72 P3-B: protocol-typed parameters lower to // NovaBox_*"), so a call site reached the pre-box hook // (`emit_call`'s `debt_call_protocol_params_key` lookup) with an // EMPTY entry whenever the callee's own `emit_fn` had not run yet // (source order: callee defined/emitted AFTER the caller, or the // caller is itself never directly emitted-through such as `main` // being emitted in item order ahead of a later-declared callee) — // the concrete argument then passed through un-boxed as a bare // `Nova_<T>*`, mismatching the callee's `NovaBox_<Proto>` parameter // type (CC-FAIL `passing 'Nova_X *' to parameter of incompatible // type 'NovaBox_<Proto>'`). Mirrors the existing non-generic-protocol // pre-emit above (same rationale: call sites need this BEFORE the // declaring fn's body pass). Byte-identical for every fn without a // protocol-typed param (both loops are no-ops); for fns WITH one, // `emit_fn`'s own registration below is now a harmless idempotent // re-insert (HashMap::insert, same key/value) — order between the // two no longer matters. { let mut register_fn_protocol_params = |f: &FnDecl, out: &mut Self| { let mut param_protos: Vec<Option<(String, Vec<String>)>> = Vec::new(); let mut any_proto = false; for p in &f.params { if let Some((proto, type_args)) = out.protocol_type_args(&p.ty) { out.emit_protocol_box_typedef(&proto, &type_args); param_protos.push(Some((proto, type_args))); any_proto = true; } else { param_protos.push(None); } } if any_proto { let key = match &f.receiver { Some(recv) => format!("{}.{}", recv.type_name, f.name), None => f.name.clone(), }; out.fn_protocol_params.insert(key, param_protos); } }; for item in &module.items { if let Item::Fn(f) = item { register_fn_protocol_params(f, &mut self); } } for pf in &module.peer_files { for item in &pf.items_here { if let Item::Fn(f) = item { register_fn_protocol_params(f, &mut self); } } } } // Plan 138.2 Ф.0c (D29 method-level shadow): names of generic types // that the user has REDECLARED in entry-peer-files with a DIFFERENT // arity (e.g. user non-generic `type Vec { x, y }` shadowing an // imported generic `type Vec[T]`). The user declaration wins entirely // (D29): we must NOT register the imported generic template / methods // for such a name, otherwise record-literals `Vec{...}` monomorphize // through the import's template (`Nova_Vec____nova_int`) instead of the // user struct, and the import's methods leak onto the user receiver. // Empty in the common (no-shadow) case → zero behavioural change. let user_shadowed_generic_types: HashSet<String> = { let mut merged_arity: HashMap<String, usize> = HashMap::new(); for item in &module.items { if let Item::Type(t) = item { let e = merged_arity.entry(t.name.clone()).or_insert(0); *e = (*e).max(t.generics.len()); } } let mut shadowed = HashSet::new(); for pf in &module.peer_files { if !pf.is_entry_module { continue; } for item in &pf.items_here { if let Item::Type(t) = item { if let Some(&m) = merged_arity.get(&t.name) { if m != t.generics.len() { shadowed.insert(t.name.clone()); } } } } } shadowed }; // 1a. Pre-populate generic_types + generic_type_templates BEFORE emit_type_decl // and method registration, so both know which types are generic templates. // // Plan 62.A: skip `Option` / `Result` — codegen имеет специальную // обработку через `NovaOpt_<T>` (value type в nova_rt/array.h) и // pre-populated sum_schemas. Регистрация template'а конфликтует // с этой parallel infrastructure: drain_generic_type_worklist // создал бы `Nova_Option____<T>` heap-allocated form, // не совпадающий с runtime helper signatures. for item in &module.items { if let Item::Type(t) = item { if !t.generics.is_empty() { // Plan 62.A: Option/Result handled via NovaOpt_<T> / // Nova_Result* infra — не регистрируем как generic // template. Error не generic (record), не попадает // в эту ветку. // // Plan 95 Ф.1.1: но захватываем имена type-параметров // ДО `continue`, чтобы `receiver_c_type` мог // разрезолвить `NovaOpt_<T>` / `NovaRes_<ok>_<err>` для // mono'd методов на builtin sum-типах // (canonical source — type-decl, не receiver). if t.name == "Option" || t.name == "Result" { let param_names: Vec<String> = t.generics.iter() .map(|g| g.name.clone()) .collect(); self.builtin_sum_type_params .insert(t.name.clone(), param_names); continue; } // Plan 62.E (`std/prelude/collections.nv` 2026-05-18) + // 2026-05-19 merge fix: skip generic protocol declarations // (e.g. `Iter[T]`). Protocols имеют нулевой runtime footprint // — value erased to void*. Регистрация template'а заставила бы // emit_type_decl + erased_type_ref_c генерировать struct и // `Nova_Iter*` для protocol-typed parameters → CC-FAIL // `unknown type name 'Nova_Iter'` (regression от merge'а // main↔plan-62-main). if let crate::ast::TypeDeclKind::Protocol { .. } = &t.kind { self.protocol_types.insert(t.name.clone()); // Plan 72 P3-B: register method signatures for vtable generation. let type_params: Vec<String> = t.generics.iter() .map(|g| g.name.clone()) .collect(); // [M-protocol-embed-vtable-missing-method]: flatten // `use`-embeds here too (see pre-pass above, // `protocol_direct`/`flattened_protocol_methods`) — // a GENERIC protocol embedding another protocol // (generic or not) must also see the embedded // methods in its vtable. let flat_methods = flattened_protocol_methods(&t.name, &protocol_direct); self.protocol_method_registry.insert( t.name.clone(), (type_params, flat_methods), ); // [fix №154] declaring file — see `protocol_decl_file` doc. self.protocol_decl_file.insert(t.name.clone(), t.span.file_id); continue; } // Plan 138.2 Ф.0c: skip the imported generic template when // the user shadows this name with a different arity — user // wins (D29). The non-generic user `type Vec` is emitted // normally below; the generic import must not register. if user_shadowed_generic_types.contains(&t.name) { continue; } self.generic_types.insert(t.name.clone()); self.generic_type_templates.insert(t.name.clone(), t.clone()); } } } // 1a-cross-module. Plan 48.1: ALSO register generic type templates // from peer_files (transitively-imported modules). Without this, // cross-module generic references (e.g. `Option[HashMap[K,V]]` где // HashMap живёт в std/collections/hashmap.nv) во время forward-decl // pass'a резолвятся как `Nova_HashMap*` (erased fallback в // `type_ref_to_c` line 4567) — а body pass позже видит mono'd type // → signature mismatch CC-FAIL. // // Mirror логики module.items pass'a: skip Option/Result (специальная // обработка), skip protocols (value-erased к void*), skip newtypes // (другая infrastructure). Регистрируем только Record/Sum generic // templates — те, что нужны для mono pipeline. for pf in &module.peer_files { for item in &pf.items_here { if let Item::Type(t) = item { if t.generics.is_empty() { continue; } if t.name == "Option" || t.name == "Result" { continue; } if matches!(t.kind, crate::ast::TypeDeclKind::Protocol { .. }) { // Protocol registration уже сделана через module.items // pass или через emit_protocol_box_typedef on-demand. continue; } // Plan 138.2 Ф.0c: user-shadowed name — skip the imported // generic template (user's non-generic decl wins, D29). if user_shadowed_generic_types.contains(&t.name) { continue; } // entry-only `insert` — module.items wins на коллизии // (test peer's own type-decl beats imported re-export). self.generic_types.insert(t.name.clone()); self.generic_type_templates .entry(t.name.clone()) .or_insert_with(|| t.clone()); } } } // Plan 138.1 Ф.1 (D239): `[]T` ≡ `Vec[T]`. Record/sum fields and fn // signatures that mention `[]T` now resolve (via type_ref_to_c) to // `Nova_Vec____<elem_c>*`. Those record struct DEFINITIONS are emitted // in step 1 (emit_type_decl) — BEFORE the generic-type mono pass — so // the `Vec[elem]` forward typedef must already be present in // `user_type_fwd_decls` (spliced into the early preamble marker), else // clang reports `unknown type name 'Nova_Vec____<elem>'` for fields // like `MultiError.suppressed []str`. // // This MUST run AFTER both the module.items AND peer_files template // registration above (the Vec template lives in // std/collections/vec_owned.nv — a peer file), so the // `generic_type_templates.contains_key("Vec")` guard is satisfied and // type_ref_to_c resolves `[]T` to the Vec mono. We scan both // module.items and peer_files (prelude types like MultiError carry // `[]str` fields). Graceful no-op if Vec template is absent (matches // the type_ref_to_c legacy fallback). if self.generic_type_templates.contains_key("Vec") { let mut array_elems: Vec<crate::ast::TypeRef> = Vec::new(); let mut scan_item = |item: &Item, acc: &mut Vec<crate::ast::TypeRef>| { match item { Item::Type(t) => Self::collect_array_elem_typerefs_in_typedecl(t, acc), Item::Fn(f) => { for p in &f.params { Self::collect_array_elem_typerefs(&p.ty, acc); } if let Some(r) = &f.return_type { Self::collect_array_elem_typerefs(r, acc); } // Plan 168 (D300): scan fn body for Vec[T] local vars // so body-only instantiations get a global forward-decl. Self::collect_array_elem_typerefs_in_fnbody(&f.body, acc); } _ => {} } }; for item in &module.items { scan_item(item, &mut array_elems); } for pf in &module.peer_files { for item in &pf.items_here { scan_item(item, &mut array_elems); } } let mut seen_vec_mono: HashSet<String> = HashSet::new(); for elem in &array_elems { // Resolve elem → C type. As a side effect this also enqueues the // Vec[elem] instance (the Array arm of type_ref_to_c). let Ok(elem_c) = self.type_ref_to_c(elem) else { continue; }; // Skip genuine type-param placeholders (`Nova_K*` etc.) — generic // stubs with no concrete struct definition (their forward-decl // would dangle). A mono'd instance name (e.g. // `Nova_Vec____nova_int*` from a nested `[][]int`) contains // `____` and IS concrete — do NOT skip it. Mirrors the // analogous concrete-mono carve-out used in the Option/Named arms. if self.debt_skip_array_fwd_decl(&elem_c) { continue; } let type_args_c = vec![elem_c]; let mangled = Self::compute_generic_type_c_name("Vec", &type_args_c); if !seen_vec_mono.insert(mangled.clone()) { continue; } if self.emitted_generic_type_instances.contains(&mangled) { continue; } self.user_type_fwd_decls.push_str(&format!( "typedef struct {0} {0};\n", mangled)); // Ensure the full struct definition is emitted by the mono pass // (type_ref_to_c above already enqueued it; guard idempotently). { let mut wl = self.generic_type_worklist.borrow_mut(); if !wl.iter().any(|(_, _, m)| m == &mangled) { wl.push(("Vec".to_string(), Self::args_lift(&type_args_c), mangled.clone())); } } self.generic_type_instance_info .borrow_mut() .entry(mangled.clone()) .or_insert_with(|| ("Vec".to_string(), Self::args_lift(&type_args_c))); } } // 1a1b. Plan 103.5: register type_decls from ExternalRegistry (sync.nv etc.). // // sync.nv declares: // - `export external type OnceCell[T]` / `Lazy[T]` → generic opaque types. // Register in generic_types + generic_type_templates so TurboFish dispatch // and drain_generic_type_worklist fire correctly. // - `export type OnceState | Fresh | Running | Done | Poisoned` — sum type // pre-declared in sync_primitives.h (RUNTIME_DEFINED_TYPES). Register in // sum_schemas so `debt_is_generic_stub_c("Nova_OnceState*")` returns false and // `infer_expr_c_type` correctly infers Nova_OnceState* for once.state(). // // These types are NOT in the compiled test module's items (they're not prelude // nv files), so emit_type_decl is never called for them without this pass. for type_decl in self.external_registry.type_decls.clone() { match &type_decl.kind { crate::ast::TypeDeclKind::Opaque => { if !type_decl.generics.is_empty() { self.generic_types.insert(type_decl.name.clone()); self.generic_type_templates.entry(type_decl.name.clone()) .or_insert(type_decl); } } // Plan 91.12 V2 (D126 retract — sync types migration): // runtime-backed newtype declarations (`type X[T](ptr)`) need // the same dispatch registration as their predecessor Opaque // form (`external type X[T]`). Without this, type-checker fails // to resolve `Lazy[int].new(closure)` as a static-method call // — codegen falls back на misrouted `Nova_int_method_new(Lazy, ...)`. crate::ast::TypeDeclKind::Newtype(_) => { if !type_decl.generics.is_empty() { self.generic_types.insert(type_decl.name.clone()); self.generic_type_templates.entry(type_decl.name.clone()) .or_insert(type_decl); } } crate::ast::TypeDeclKind::Sum(variants) => { // Register sum schema for pattern matching + debt_is_generic_stub_c. // [M-sync-crossmodule…] (D381): SKIP a colliding name here — this // external-registry pre-pass keys by the BARE `type_decl.name`, // which would create a spurious unqualified `ErrorKind` schema/ // registry entry alongside the module-qualified ones that // `emit_type_decl` emits (the colliding types ARE in // `module.items`). That bare entry then wins reference // resolution (`find_variant`/schema lookup) → bare // `nova_make_ErrorKind_*` calls with no definition. Non-colliding // names are unaffected (byte-identical). if !self.colliding_type_names.contains(&type_decl.name) && !self.sum_schemas.contains_key(&type_decl.name) { let mut schema: std::collections::HashMap<String, Vec<String>> = std::collections::HashMap::new(); let mut variant_order: Vec<String> = Vec::new(); for v in variants { let field_types: Vec<String> = match &v.kind { crate::ast::SumVariantKind::Unit => Vec::new(), crate::ast::SumVariantKind::Tuple(types) => types.iter() .filter_map(|ty| self.type_ref_to_c(ty).ok()) .collect(), crate::ast::SumVariantKind::Record(fields) => fields.iter() .filter_map(|f| self.type_ref_to_c(&f.ty).ok()) .collect(), }; variant_order.push(v.name.clone()); schema.insert(v.name.clone(), field_types); } let c_name = format!("Nova_{}", type_decl.name); self.sum_schema_registry.register_user_sum( &type_decl.name, &schema, &c_name, super::sum_schema_registry::SumAbi::PointerErrorLike, &variant_order, ); self.sum_schemas.insert(type_decl.name.clone(), schema); } } _ => {} } } // 1a2. Collect FnDecls for methods on generic types (needed for Ф.3 dispatch). // // Plan 95 Ф.1.1: also collect **Nova-body** methods on builtin sum // types (`Option`/`Result`) — they participate in method-mono via // the «method-only» channel even though their representation is // `NovaOpt_<T>` / `NovaRes_<...>` (not a generic template). // `external fn` methods stay C-routed (their bodies live in // `array.h` / `register_novaopt_decl` lazy-emit) and are filtered // out here — collecting them would shadow C dispatch for no gain. for item in &module.items { if let Item::Fn(f) = item { if let Some(recv) = &f.receiver { // [M-172.1-d174] (U.1.3b sync-inline, 2026-07-02): `!f.is_external` // и для generic-ветки — extern-методы «stay C-routed» (инвариант // из комментария выше), фильтр отсутствовал. Merged-через-import // extern generic-метод (sync.nv OnceCell.new/get/set) попадал в // mono-канал: call-sites уходили с C-runtime имён // (`Nova_OnceCell____…`) на mono-имена + эмитились ПУСТЫЕ // дубль-определения (redefinition/UB). Nova-body методы sync // (with_lock/start) регистрируются как раньше. let is_generic = !f.is_external && self.generic_types.contains(&recv.type_name); let is_builtin_mono_sum = !f.is_external && self.builtin_sum_type_params.contains_key(&recv.type_name); if is_generic || is_builtin_mono_sum { let entry = self .generic_type_methods .entry(recv.type_name.clone()) .or_default(); // Дедуп той же декларации (builtin-снабжение + inline-merge // одного .nv через `import`). // [M-153.1-append-as-slice-ccfail]: compare PARAM TYPES too — // otherwise two genuine PARAM-TYPE overloads of the same arity // on a generic type (`@tag(n int)` / `@tag(s str)`) collapse to // one `generic_type_methods` entry, the overload selection // (~28704) sees `same_name.len()==1` → index 0 → both call-sites // dispatch to the FIRST overload's mono (`Box6..._method_tag`), // passing a `nova_str` into a `nova_int` param (CC-FAIL). Same // declaration re-supplied via builtin+import has identical param // types → still deduped. let dup = entry.iter().any(|g| { g.name == f.name && g.params.len() == f.params.len() && g.params.iter().zip(f.params.iter()).all(|(gp, fp)| Self::type_ref_overload_key(&gp.ty) == Self::type_ref_overload_key(&fp.ty)) && g.receiver.as_ref().map(|r| { (r.mutable, matches!(r.kind, crate::ast::ReceiverKind::Static)) }) == f.receiver.as_ref().map(|r| { (r.mutable, matches!(r.kind, crate::ast::ReceiverKind::Static)) }) }); if !dup { entry.push(f.clone()); } } } } } // 1a3. Plan 62.A.bis Ф.1 + 62.A follow-up: hook для prelude-discovery. // // Ф.1: hook был no-op (stub). // **62.A follow-up (2026-05-18):** теперь scan'ит `module.items` для // `external fn Option[T] @method` / `external fn Result[T, E] @method` // declarations (приходящих из `std/prelude/core.nv` через R27 auto- // import). Регистрирует `DeclaredFromPrelude` entries в registry, // унаследовав method_routing от соответствующих HardcodedBaseline // entries — behavior-preserving migration источника правды. // // Order matters: hook размещён ПОСЛЕ init_hardcoded_baseline // (в `CEmitter::new()`) и ДО emit_type_decl loop'а — Prelude // entries уже регистрированы когда user type-decls попадают в // emit_type_decl (получают DeclaredFromUser source). let module_name_dotted = module.name.join("."); self.sum_schema_registry.init_prelude_decls(&module_name_dotted); self.sum_schema_registry.init_prelude_decls_from_items(&module.items); // Plan 62.D bis-1 (2026-05-18, D29 W_PRELUDE_SHADOW basic): // Build the set of items that should be SKIPPED during emit due to // user-shadow. When user file declares `type Range` while prelude // facade re-exports `Range` from `std/collections/range.nv`, both // items end up in `module.items` (merged-first, user-second per // imports.rs:218-220). Emitting both causes C "redefinition" errors. // User declaration wins per D29 — skip the merged (non-user) one. // This matches the type-checker behavior in types/mod.rs // `classify_dup`. // // For Types: skip merged item if any user item has the same name. // For Fns: skip merged item if any user item has the same key // (`<Receiver>.<name>` or `<name>`) AND matching arg-type-list (so // legitimate user-defined overloads from a different module still // emit). Approximation: skip merged fn with the same key, since // method dispatch goes through method_receivers keyed by name only // and overloads from different modules would create routing chaos. let user_type_spans: std::collections::HashSet<(String, crate::diag::Span)> = { let mut s = std::collections::HashSet::new(); for pf in &module.peer_files { if !pf.is_entry_module { continue; } for it in &pf.items_here { if let Item::Type(td) = it { s.insert((td.name.clone(), td.span)); } } } s }; let user_type_names: HashSet<String> = user_type_spans.iter() .map(|(n, _)| n.clone()).collect(); let user_fn_spans: std::collections::HashSet<(String, crate::diag::Span)> = { let mut s = std::collections::HashSet::new(); for pf in &module.peer_files { if !pf.is_entry_module { continue; } for it in &pf.items_here { if let Item::Fn(fd) = it { let key = match &fd.receiver { Some(r) => format!("{}.{}", r.type_name, fd.name), None => fd.name.clone(), }; s.insert((key, fd.span)); } } } s }; let user_fn_keys: HashSet<String> = user_fn_spans.iter() .map(|(k, _)| k.clone()).collect(); // Plan 62.F.bis Ф.2 (2026-05-18): same shadow-skip для Const items. // Без этого `const PRELUDE_VERSION = 99` в user-коде + prelude's own // `PRELUDE_VERSION` → C-level redefinition (CC-FAIL). User wins, // merged duplicate skipped. let user_const_spans: std::collections::HashSet<(String, crate::diag::Span)> = { let mut s = std::collections::HashSet::new(); for pf in &module.peer_files { if !pf.is_entry_module { continue; } for it in &pf.items_here { if let Item::Const(cd) = it { s.insert((cd.name.clone(), cd.span)); } } } s }; let user_const_names: HashSet<String> = user_const_spans.iter() .map(|(n, _)| n.clone()).collect(); // Helper: is this Type item the merged (non-user) duplicate of a // name the user has also declared? Skip if so. // [M-http-module-test-block-p67] / [M-sync-crossmodule-samename-type-collision] // (D381): a cross-module same-SIMPLE-name COLLISION is NOT a prelude-shadow // duplicate. `http.ErrorKind` and `encoding.compress.ErrorKind` are two // DISTINCT types, qualified to distinct C bases (`Nova_std_http_ErrorKind` // vs `Nova_encoding_compress_ErrorKind`) by def/ref_type_base — so emitting // BOTH carries no C-redefinition risk, and BOTH must be emitted+registered // (their schemas key by the qualified base). The name-keyed shadow-skip // below wrongly dropped the non-entry-module one, so its sum-schema never // registered → a `match` on a compress-only variant (`InvalidData(msg)`) // found no binding → `[P67-LEGACY] Ident 'msg' not in var_types`. Exempt // colliding names: the skip stays exact for genuine shadows (a re-export / // same-C-base duplicate is NOT in `colliding_type_names`). let colliding_type_names_snapshot = self.colliding_type_names.clone(); let should_skip_type = |t: &TypeDecl| -> bool { if !user_type_names.contains(&t.name) { return false; } if colliding_type_names_snapshot.contains(&t.name) { return false; } // User declared this name. If THIS span belongs to user — keep. // Otherwise (merged from import) — skip. !user_type_spans.contains(&(t.name.clone(), t.span)) }; // [M-178-server-typed-body] fix: snapshot into a local so the // `should_skip_fn` closure below doesn't hold an immutable borrow of // `self` for its whole lifetime (this fn does plenty of `&mut self` // work — `self.emit_type_decl`/etc — after the closure is defined). let colliding_fn_names_snapshot: HashSet<String> = self.colliding_fn_names.clone(); let should_skip_fn = |f: &FnDecl| -> bool { // Plan 138.2 Ф.0c (D29 method-level shadow): skip emitting an // imported generic type's method when the user has redeclared that // type name with a different arity (e.g. `type Vec { x, y }` over // imported `Vec[T]`). The method carries the import's carrier arity // (`Vec[T]` receiver → generics.len()==1) which differs from the // user's (0); a method the user wrote on their own redeclared type // would match the user arity and is kept. User wins entirely. if !user_shadowed_generic_types.is_empty() { if let Some(r) = &f.receiver { if user_shadowed_generic_types.contains(&r.type_name) && !r.generics.is_empty() { return true; } } } // [M-178-server-typed-body] fix: this D29-shadow skip assumes a // free fn matching the entry-module's key by NAME is the SAME // declaration reached via a second (merged/import) path — true // for genuine prelude-shadow re-exports, but FALSE for a // cross-module collision (`colliding_fn_names`, e.g. // `std.http.client`'s private `serialize_response` vs // `std.http.server`'s exported one) — two UNRELATED functions // that happen to share a name. Skipping the non-entry-module one // here would silently drop its body (implicit-decl CC-FAIL at // every call site, since the collision-aware mangling above // gives it its OWN distinct C symbol that then never gets // defined). Never skip those via this path. if f.receiver.is_none() && colliding_fn_names_snapshot.contains(&f.name) { return false; } let key = match &f.receiver { Some(r) => format!("{}.{}", r.type_name, f.name), None => f.name.clone(), }; if !user_fn_keys.contains(&key) { return false; } !user_fn_spans.contains(&(key, f.span)) }; let should_skip_const = |c: &ConstDecl| -> bool { if !user_const_names.contains(&c.name) { return false; } !user_const_spans.contains(&(c.name.clone(), c.span)) }; // Plan 81 Ф.7.2 + Plan 159 Ф.1: compiler-level reachability DCE — // monomorphic free functions AND module-level consts / ro-globals // unreachable from any root are not emitted (forward decl + body, or // the giant `static` table definition, dropped from the `.c`). One // shared reachable-set closure (const→fn, fn→const, const→const edges). let DeadDecls { dead_fns: dead_free_fns, dead_consts, dead_method_keys, } = compute_dead_decls(module); let is_dead_free_fn = |f: &FnDecl| -> bool { f.receiver.is_none() && f.generics.is_empty() && !matches!(f.body, crate::ast::FnBody::External) && dead_free_fns.contains(&f.name) }; // Plan 159 Ф.1: a method that can never run (its receiver type is never // constructed/named, OR its name is never invoked, in reachable code) is // dropped (body + forward decl). Dropping it also removes its references // to the free fns / consts that were pruned. Monomorphic methods only — // generic methods emit lazily via the mono worklist, and // `dead_method_keys` is empty when DCE is off / in library mode. let is_dead_method = |f: &FnDecl| -> bool { match &f.receiver { Some(r) if f.generics.is_empty() && !matches!(f.body, crate::ast::FnBody::External) => { dead_method_keys.contains(&(r.type_name.clone(), f.name.clone())) } _ => false, } }; // 1. Type declarations first (structs/unions needed by fn signatures) // // Plan 175 Ф.2-v3 Фаза 2 (D316 §Ф.2 historical finding — root cause // of the Time-typed-ops rollback, 4× before this fix): emit ALL // non-effect type decls (records/sums/value-records/named-tuples/ // aliases/newtypes — anything whose BODY a function-pointer field // might need to be COMPLETE for, e.g. a by-value `Duration` op // parameter) in a FIRST pass, and defer `TypeDeclKind::Effect` to a // SECOND pass afterward. Effect vtables are function-pointer // structs — `emit_effect_type` may declare a field like // `nova_unit (*sleep)(void*, NovaValue_Duration)`, which needs // `NovaValue_Duration`'s COMPLETE struct body already emitted // (a by-value struct parameter of an INCOMPLETE type is a hard C // error, unlike a plain forward-declared pointer) — module.items // order is whatever import/merge order produced (an effect // declared in a module that happens to be processed before the // value-record module it references would previously hit "unknown // type" / incomplete-type errors). Two passes over the same // (unordered-safe) `module.items` sidesteps the ordering question // entirely: no matter where `type Time effect {...}` (or any user // effect referencing a value-record type) sits in the merged item // list, its vtable now emits strictly after every other type body. for item in &module.items { if let Item::Type(t) = item { if matches!(t.kind, TypeDeclKind::Effect(_)) { continue; } if should_skip_type(t) { continue; } self.emit_type_decl(t)?; } } for item in &module.items { if let Item::Type(t) = item { if !matches!(t.kind, TypeDeclKind::Effect(_)) { continue; } if should_skip_type(t) { continue; } self.emit_type_decl(t)?; } } // [M-option-self-recursive-record-mono] (Plan 186): every non-generic // record/sum's `record_schemas`/`sum_schemas` entry is now complete — // build any eq-fn bodies `register_novaopt_decl` deferred because they // self-referenced a type still mid-emission at registration time. self.drain_pending_structural_eq(); // Plan 52.2 Ф.2: forward-declare mono'd struct types для // const-decls с generic-типом. Без этого `const X HashMap[K,V] = [...]` // эмитится с typeref'ом `Nova_HashMap____K__V*` ДО mono pass, // и C-compiler не знает такого type-name. // // Mono pass позже эмитит полное `struct Nova_HashMap____K__V {...}` // — forward-declare через `typedef struct X X;` достаточно для // pointer-type использования в const-decl (правда compile-time // init не работает, lazy init через nova_const_X() — pointer // OK через forward-decl). for item in &module.items { if let Item::Const(c) = item { if should_skip_const(c) { continue; } if let Some(ty) = &c.ty { self.forward_declare_generic_type(ty); } } } // 1b. Const declarations (after types, before fn forward decls) for item in &module.items { if let Item::Const(c) = item { if should_skip_const(c) { continue; } // Plan 159 Ф.1: skip a const unreachable from any root (its // giant `static` table — e.g. an unused Unicode data table — // is omitted). `dead_consts` is empty when reachability DCE is // disabled or in library mode, so this is a no-op there. if dead_consts.contains(&c.name) { continue; } self.emit_const_decl(c)?; } } // 1b1. Plan 152.4 (D199 ro-runtime side): module-level `ro NAME = EXPR` // lazy-static globals are emitted LATER — see «1b1-moved» just after the // `/*__GENERIC_TYPE_DEFS__*/` placeholder. They must come after generic // type instance definitions (a lazy-static's type can be a mono'd generic // like `HashMap[int,str]`; its `static <T> _value;` storage decl needs the // typedef first), and emitting their getter body there also lets call // routing (method_receivers §1c / generics §1d) be fully set up. // 1b2. D39 / Plan 11 Ф.9: collect embed-fields per record-type. // Используется на 1d для генерации auto-proxy methods. for item in &module.items { if let Item::Type(t) = item { if let TypeDeclKind::Record(fields) = &t.kind { let mut embeds: Vec<(String, String, bool)> = Vec::new(); // Plan 11 Ф.9.4: multi-anonymous detection. Подсчитать // count anonymous embeds per embedded-type — если ≥2 // одного типа → compile error (нет alias'а для disambig). let mut anon_counts: HashMap<String, usize> = HashMap::new(); for f in fields { if !f.is_embed { continue; } let embedded_ty_name = match &f.ty { TypeRef::Named { path, .. } => path.join("_"), _ => continue, }; if f.embed_anonymous { *anon_counts.entry(embedded_ty_name.clone()).or_insert(0) += 1; } embeds.push((f.name.clone(), embedded_ty_name, f.embed_anonymous)); } for (ty_name, count) in &anon_counts { if *count > 1 { return Err(format!( "type `{}`: multiple anonymous embeds of `{}`; \ use named alias `use <name> {}` to disambiguate", t.name, ty_name, ty_name)); } } if !embeds.is_empty() { self.embed_fields.insert(t.name.clone(), embeds); } } } } // 1c. Pre-populate method_receivers so emit_call can route obj.method() correctly // Plus D84: register free-functions в method_overloads с sentinel-key // ("", name) — единый mechanism для overload resolution. // // [M-http-compress-errorkind-crosspkg-collision] fix: this pass calls // `type_ref_to_c` on every fn's params/return type (below, D84 // overload-reg) BEFORE `emit_fn`'s per-fn `current_emit_file_id` gate // ever runs for this item (that happens later, in step 4) — so a bare // param/return type with a COLLIDING simple name (`ErrorKind` shared // between http/compress in this CU, e.g. `HttpError.new(kind // ErrorKind)`) resolved via `ref_type_base` saw a stale/`None` // `current_emit_file_id` and fell through to the unqualified `Nova_ // ErrorKind*` (D381 only gated the type-DECL emission loop and the // fn-BODY emission loop, not this earlier signature pre-scan). Fix: // set the emission-file context per-item here too, mirroring the // save/gate/restore pattern used at emit_type_decl (~15621) and // emit_fn_forward_decl (~15638/~15713) — byte-identical when this CU // has no cross-module/per-file type collision (`any_type_file_ // collision()` false). let saved_emit_file_id_1c = self.current_emit_file_id; let any_type_collision_1c = self.any_type_file_collision(); for item in &module.items { if let Item::Fn(f) = item { if any_type_collision_1c { self.current_emit_file_id = Some(f.span.file_id); } // Plan 138.2 Ф.0c (D29 method-level shadow): skip registering // an imported generic type's method when the user redeclared // that type name with a different arity (`type Vec { x, y }` // over imported `Vec[T]`). Registering it here would attempt to // resolve the import's `Self`/`Option[Self]` return against the // user's now-non-generic `Vec` → E7001 "Self outside receiver". // User wins entirely (D29). Discriminator: the import's method // carries carrier generics (`Vec[T]` → recv.generics non-empty). if !user_shadowed_generic_types.is_empty() { if let Some(r) = &f.receiver { if user_shadowed_generic_types.contains(&r.type_name) && !r.generics.is_empty() { continue; } } } // Plan 170 (D307): file-private free fns are NOT registered in // the shared free-fn overload registry — they are file-local and // never participate in cross-file overload resolution. Each gets // a file-discriminated C symbol (file_priv_fn_c_names) resolved // directly through free_fn_c_name. Registering them here would // (a) collide same-named privates from peer files under the same // sentinel key, and (b) wrongly surface them to sibling call-sites. // // [M-178-server-typed-body] fix: the same reasoning applies to a // name detected as a cross-MODULE collision (`colliding_fn_names`, // e.g. `std.http.client`'s private `serialize_response` vs // `std.http.server`'s exported one of the same name) even when // NEITHER declaration is `priv(file)` — they are two UNRELATED // functions that happen to share a name, not overloads of each // other, so they must not share a `method_overloads` sentinel // key either. Resolved instead via the (module-wide) per-file // `file_priv_fn_c_names` map built earlier in this fn. if f.receiver.is_none() && ((f.file_private && !f.is_external) || self.colliding_fn_names.contains(&f.name)) { continue; } // === D84: free-function overload registration === if f.receiver.is_none() { // Plan 70 PhaseA1.3: strict mode — free-fn overload registration // (D84). Все concrete param/return типы должны successfully // translate. Pre-mono erasure handled separately (is_generic_recv path). let param_c_types: Vec<String> = f.params.iter() .map(|p| self.type_ref_to_c(&p.ty).map_err(|e| self.err_no_int_fallback( &format!("free fn `{}` overload-reg parameter `{}`", f.name, p.name), &e, ))) .collect::<Result<Vec<_>, _>>()?; let return_c_type = match &f.return_type { Some(t) => self.type_ref_to_c(t).map_err(|e| self.err_no_int_fallback( &format!("free fn `{}` overload-reg return type", f.name), &e, ))?, // Plan 55 Ф.3: для `=> expr` body инфирим из выражения // (call-site нужен правильный type до emit_fn). // field_cache coerces FnBody::Expr → FnBody::Block(stmts=[],trailing=e) // BEFORE codegen sees AST — match both forms. None => { let is_expr_like = matches!(&f.body, FnBody::Expr(_)) || matches!(&f.body, FnBody::Block(b) if b.stmts.is_empty()); if is_expr_like { // Temporarily seed param types into var_types so // return_type_c(body-expr) can resolve param Idents // (e.g. `fn foo(x int) => x * 2` needs `x` visible). // Mirrors phase-2 forward-decl seed at lines ~9838-9847. let mut ret_seed_saved: Vec<(String, Option<String>)> = Vec::new(); for p in &f.params { if let Ok(pc) = self.type_ref_to_c(&p.ty) { if !pc.is_empty() { let prev = self.var_types.insert(p.name.clone(), pc); ret_seed_saved.push((p.name.clone(), prev)); } } } let r = self.return_type_c(f).unwrap_or_else(|_| "nova_unit".into()); for (name, prev) in ret_seed_saved { match prev { Some(old) => { self.var_types.insert(name, old); } None => { self.var_types.remove(&name); } } } r } else { "nova_unit".into() } }, }; // Sentinel-key: пустая строка вместо receiver-type. // Не конфликтует с user-types (имена ≠ пустой строке). let key = ("".to_string(), f.name.clone()); // Dedup IDENTICAL declarations (same params + return). Two peer // files of a folder-module may declare the same `extern fn` // (one external symbol forward-declared twice). Registering // both would (a) mangle the 2nd with a param-type suffix → // distinct c_name → false "ambiguous overload", and (b) emit // two prototypes. Skip the duplicate entirely — это НЕ overload // (overload требует различия по D84-осям), а повтор одной декл. // Plan 184 (Р13/Р14): parameter MODE is an overload axis, so a // mode-differing decl (`f(x H)` vs `f(mut x H)` — same param // C-types + return) is NOT a duplicate; include modes in the // dedup key so all three register (and get distinct C symbols). let new_modes = Self::fn_param_modes(f); if let Some(existing) = self.method_overloads.get(&key) { let is_dup = existing.iter().any(|s| s.param_c_types == param_c_types && s.return_c_type == return_c_type && s.param_modes == new_modes); if is_dup { continue; } } let existing_count = self.method_overloads.get(&key) .map(|v| v.len()).unwrap_or(0); let base_c_name = self.free_fn_c_name(&f.name); let c_name = if existing_count == 0 { base_c_name.clone() } else { // Mangling по param-types (тот же sanitize что для методов). let suffix = param_c_types.iter() .map(|t| t.replace('*', "_p") .replace(' ', "_") .replace('[', "_arr_") .replace(']', "")) .collect::<Vec<_>>() .join("_"); let cand = if suffix.is_empty() { base_c_name.clone() } else { format!("{}__{}", base_c_name, suffix) }; // Plan 184 (Р13/Р14): mode-overload collision — a sibling // with identical param C-types but a DIFFERENT parameter // mode already claimed this name (`f(x H)` vs `f(mut x H)` // vs `f(consume x H)`: heap params keep the handle ABI, Р6, // so the type suffix cannot separate them). Append the // param-mode tag to make the C symbol unique. Only fires on // a genuine mode difference → pure-`ro`/type-differentiated // sets are byte-identical. let new_modes = Self::fn_param_modes(f); let collides = self.method_overloads.get(&key) .map(|sigs| sigs.iter().any(|s| s.c_name == cand && s.param_modes != new_modes)) .unwrap_or(false); if collides { format!("{}__{}", cand, Self::param_mode_tag(f)) } else { cand } }; let variadic_last = f.params.last() .map(|p| p.is_variadic).unwrap_or(false); let param_defaults: Vec<Option<String>> = f.params.iter() .map(|p| p.default.as_ref().and_then(Self::simple_literal_c)) .collect(); let sig = MethodSig { param_c_types, return_c_type, is_instance: false, // free-function: not instance is_external: f.is_external, is_delegated: false, c_name, variadic_last, param_defaults, // Plan 128 Ф.1: free fns have no receiver — false. recv_mutable: false, // Plan 184 (Р13/Р14): parameter-mode overload axis. param_modes: Self::fn_param_modes(f), // U.4.3 c2.2: source FnDecl identity for the dispatch consume. fn_span: Some(f.span), }; self.register_method_overload(key.clone(), sig); // Plan 125 followup [M-125-method-call-never-detection]: // free-fn `-> never` registry (key.0 = "" for free fns). if Self::fn_return_is_never_125(f) { self.never_returning_methods.insert(key); } continue; } if let Some(recv) = &f.receiver { // Plan 48: generic methods with own type params (f.generics non-empty) are // normally handled by monomorphization. Exception: array extension methods // (recv.type_name starts with "[]") are not user-defined generic types and // never get monomorphized — register them with erased types instead. let is_array_ext = recv.type_name.starts_with("[]"); // Plan 101.1: bare-T receiver (`fn[T] T @method`) — type_name // = single-uppercase typevar. Register тоже for call-site // dispatch detection. let is_bare_typevar = recv.type_name.len() <= 2 && recv.type_name.chars().all(|c| c.is_ascii_uppercase()); if !f.generics.is_empty() && !is_array_ext && !is_bare_typevar { continue; } // Plan 48 Ф.3: methods on generic receiver types are registered here for // erased-mode dispatch (when receiver is unparameterized, e.g. `Nova_Pair*`). // Monomorphized dispatch (block 5b) fires first and returns early for // concrete instances, so no conflict. let is_instance = matches!(recv.kind, ReceiverKind::Instance); self.method_receivers.insert( f.name.clone(), (recv.type_name.clone(), is_instance), ); // Plan 06 Ф.1: multi-key для for-in Iter[T] dispatch. self.all_methods.insert((recv.type_name.clone(), f.name.clone())); self.all_method_recv_types.insert(recv.type_name.clone()); // Plan 11 Ф.1: register signature в multi-overload registry. // param_c_types — C-типы параметров без receiver'а. // Plan 48 Ф.3: for generic receiver types, use erased types so // that type-param references (e.g. Pair[B,A]) don't monomorphize. let is_generic_recv = self.generic_types.contains(&recv.type_name) || is_array_ext; let recv_type_params: HashSet<String> = if is_generic_recv { // Collect type params from receiver generics (e.g. T in []T) let from_recv = recv.generics.iter().filter_map(|tr| { if let TypeRef::Named { path, .. } = tr { path.first().cloned() } else { None } }); // For array extension methods, also erase method-level generics (e.g. U in map[U]) let from_fn = if is_array_ext { f.generics.iter().map(|g| g.name.clone()).collect::<Vec<_>>() } else { Vec::new() }; from_recv.chain(from_fn.into_iter()).collect() } else { HashSet::new() }; // Plan 70 PhaseA1.3: strict mode — method overload registration. // Generic-recv path uses erased_type_ref_c (preserves type-param erasure, // intentional Cat B). Non-generic-recv: strict translation required. // Plan 91.8a.2 (D183 amendment): set current_receiver_type для // resolution Self в param-type position (mirror return-type path // на line 2148+). Без этого `fn T @method(other Self)` даёт E7001. let prev_recv_for_params = self.current_receiver_type.replace(recv.type_name.clone()); self.sync_receiver_rt(); let param_c_types: Vec<String> = f.params.iter() .map(|p| if is_generic_recv { Ok(self.erased_type_ref_c(&Some(p.ty.clone()), &recv_type_params)) } else { self.type_ref_to_c(&p.ty).map_err(|e| self.err_no_int_fallback( &format!("method `{}.{}` parameter `{}`", recv.type_name, f.name, p.name), &e, )) }) .collect::<Result<Vec<_>, _>>()?; self.current_receiver_type = prev_recv_for_params; self.sync_receiver_rt(); // Resolve return type. `Self` → recv.type_name. // Plan 55 Ф.3: для `=> expr` body инфирим (см. free-fn выше). let return_c_type = match &f.return_type { Some(TypeRef::Named { path, .. }) if path.len() == 1 && path[0] == "Self" => { // Plan 128 Ф.1: thread recv.mutable flag (Ф.2 consumes). self.receiver_c_type(&recv.type_name, recv.mutable) } Some(t) if is_generic_recv => { self.erased_type_ref_c(&Some(t.clone()), &recv_type_params) } Some(t) => self.type_ref_to_c(t).map_err(|e| self.err_no_int_fallback( &format!("method `{}.{}` return type", recv.type_name, f.name), &e, ))?, None => { // Plan 55 Ф.3 + field_cache fix: match both FnBody::Expr AND // FnBody::Block(stmts=[], trailing=Some) — the latter is the // field_cache (Plan 123.1) coercion of a `=> expr` body. // D215: restore current_receiver_type + pre-populate // var_types["nova_self"] around return_type_c so that // SelfAccess in `=> expr` bodies (e.g. `=> @n`) can derive // the correct field C-type during the fwd-decl scan pass. let is_expr_like = matches!(&f.body, FnBody::Expr(_)) || matches!(&f.body, FnBody::Block(b) if b.stmts.is_empty()); if is_expr_like && matches!(recv.kind, ReceiverKind::Instance) { let prev_recv_for_ret = self.current_receiver_type.replace(recv.type_name.clone()); self.sync_receiver_rt(); let recv_c = self.receiver_c_type(&recv.type_name, recv.mutable); let prev_nova_self = self.var_types.insert("nova_self".into(), recv_c); let ret = self.return_type_c(f) .unwrap_or_else(|_| "nova_unit".into()); self.current_receiver_type = prev_recv_for_ret; self.sync_receiver_rt(); match prev_nova_self { Some(prev) => { self.var_types.insert("nova_self".into(), prev); } None => { self.var_types.remove("nova_self"); } } ret } else if is_expr_like { // Static receiver or no receiver: just set receiver type. let prev_recv_for_ret = self.current_receiver_type.replace(recv.type_name.clone()); self.sync_receiver_rt(); let ret = self.return_type_c(f) .unwrap_or_else(|_| "nova_unit".into()); self.current_receiver_type = prev_recv_for_ret; self.sync_receiver_rt(); ret } else { "nova_unit".into() } }, }; // For array extension methods, use the C-identifier form as the key so that // call-site lookups (which derive the key from "NovaArray_nova_int*" → "NovaArray_nova_int") // can find the registration. For regular types, keep the Nova type name. let key = if is_array_ext { (Self::receiver_type_c_ident(&recv.type_name), f.name.clone()) } else { (recv.type_name.clone(), f.name.clone()) }; let existing_count = self.method_overloads.get(&key).map(|v| v.len()).unwrap_or(0); // Mangling: для первой overload — короткое имя // (backward compat); для второй+ — с param-types suffix. let safe_recv_name = Self::receiver_type_c_ident(&recv.type_name); // Реестр 221.1 №581/№577: record the ONE canonical C base // ident for a STATIC array-ext own-generic blanket method // (`fn[T Bound] []T.method()`), so every call site that // needs to name this same function reads it from here // instead of re-deriving (and mis-deriving) its own name. if is_array_ext && !is_instance { self.array_ext_static_c_base .insert(f.name.clone(), safe_recv_name.clone()); // Реестр 221.1 №592: ALSO remember the full declaration // when this is the "own generic names the receiver's // element" shape (`fn[T] []T.method()`) — the ONLY // shape `emit_fn_scoped_inner` now skips erased-emitting // (see its own mirrored detection) in favor of real // per-element mono. `recv.type_name` is literally // `"[]T"` (the source spelling) here, so `strip_prefix` // yields the generic's OWN name, not a concrete element. if let Some(recv_elem) = recv.type_name.strip_prefix("[]") { if !recv_elem.is_empty() && f.generics.iter().any(|g| g.name == recv_elem) { self.array_ext_static_generic_fn .insert(f.name.clone(), f.clone()); } } } // Plan 100.6 (D164): consume-bit — `consume` vs `method` prefix // ловит ABI mismatch при изменении consume-статуса метода. // consume-method: Nova_{T}_consume_{name} // regular-method: Nova_{T}_method_{name} (backward compat) let base_c_name = if is_instance { if recv.consume { format!("Nova_{}_consume_{}", safe_recv_name, f.name) } else { format!("Nova_{}_method_{}", safe_recv_name, f.name) } } else { format!("Nova_{}_static_{}", safe_recv_name, f.name) }; // [M-172.1-d174] (U.1.3b sync-inline): merged-через-import // EXTERN-метод — C-имя из РЕЕСТРА деклараций (runtime-схема // overload-суффиксов, напр. `fetch_add_i64`/`_MemOrdering`), // НЕ user-мангл `__<c_type>` — иначе call-site бьёт в // несуществующий символ (undefined at link). Выбор overload'а // по арности параметров. let extern_c_name: Option<String> = if f.is_external { self.external_registry .lookup(&recv.type_name, &f.name) .and_then(|ds| { ds.iter() .find(|d| d.param_c_types.len() == param_c_types.len()) .map(|d| d.c_name.clone()) }) } else { None }; let c_name = if let Some(cn) = extern_c_name { cn } else if existing_count == 0 { base_c_name } else { // Mangling по param-types. Sanitize: `*` / `[` // не валидны в C-identifier'ах. let suffix = param_c_types.iter() .map(|t| t.replace('*', "_p") .replace(' ', "_") .replace('[', "_arr_") .replace(']', "")) .collect::<Vec<_>>() .join("_"); let cand = if suffix.is_empty() { // Plan 135 Ф.1: params identical — tiebreak by receiver mutability. // ro overload (first registered, existing_count==0): keeps base_c_name. // mut overload: __mut suffix. ro-as-second: __ro suffix. if recv.mutable { format!("{base_c_name}__mut") } else { format!("{base_c_name}__ro") } } else { format!("{}__{}", base_c_name, suffix) }; // Plan 184 (Р13/Р14): parameter-mode overload collision on a // method — same param C-types, different param mode (heap // params, Р6). Append the mode tag to keep C symbols unique. // Byte-identity: only a real mode difference triggers it. let new_modes = Self::fn_param_modes(f); let collides = self.method_overloads.get(&key) .map(|sigs| sigs.iter().any(|s| s.c_name == cand && s.param_modes != new_modes)) .unwrap_or(false); if collides { format!("{}__{}", cand, Self::param_mode_tag(f)) } else { cand } }; // Plan 14 Ф.6: variadic-флаг — true если последний // параметр is_variadic. Только последний валиден // (parser проверяет position constraint). let variadic_last = f.params.last() .map(|p| p.is_variadic).unwrap_or(false); let param_defaults: Vec<Option<String>> = f.params.iter() .map(|p| p.default.as_ref().and_then(Self::simple_literal_c)) .collect(); let sig = MethodSig { param_c_types, return_c_type, is_instance, is_external: f.is_external, is_delegated: false, // own declaration c_name, variadic_last, param_defaults, // Plan 128 Ф.1: capture recv.mutable for downstream ABI dispatch. recv_mutable: recv.mutable, // Plan 184 (Р13/Р14): parameter-mode overload axis. param_modes: Self::fn_param_modes(f), // U.4.3 c2.2: source FnDecl identity — the KEY site for the // instance-method dispatch consume (matches the checker's choice). fn_span: Some(f.span), }; // Plan 125 followup [M-125-method-call-never-detection]: // instance/static method `-> never` registry. Captures // both `fn T mut @method() -> never` и `fn T.method() -> never`. if Self::fn_return_is_never_125(f) { self.never_returning_methods.insert(key.clone()); } self.register_method_overload(key, sig); // `T.from(v V)` → from_targets[T] += V. [D73/D77 retraction // 2026-07-06]: the `.into()`-side registry (`into_targets`) // and its auto-derive consumers were removed; `from_targets` // survives only for the CancelToken cross-type `.cancelled_by` // compile-time precondition check (naming convention, not a // protocol — see field doc above). if !is_instance && f.name == "from" && !f.params.is_empty() { if let TypeRef::Named { path, .. } = &f.params[0].ty { if !path.is_empty() { self.from_targets.entry(recv.type_name.clone()) .or_default() .push(path.join("_")); } } } // Plan 06 Ф.3: instance-method `mut @iter() -> IterT` — // запоминаем `Coll → IterT` для implicit .iter() в for-in. if is_instance && f.name == "iter" { if let Some(TypeRef::Named { path, .. }) = &f.return_type { if !path.is_empty() { self.iter_returns.insert( recv.type_name.clone(), path.join("_")); } } } // [D73/D77 retraction 2026-07-06]: try_from_targets/ // try_into_targets registries + their auto-derive consumers // removed wholesale (TryFrom/TryInto protocol retracted). } } } // [M-http-compress-errorkind-crosspkg-collision] fix: restore the // pre-loop emission-file context (each iteration above set its own // per-fn value directly — see the loop-entry comment). self.current_emit_file_id = saved_emit_file_id_1c; // 1c2. D39 / Plan 11 Ф.9: register auto-proxy delegated methods. // Для каждого record-type с embed-полями: для каждого метода // embedded-типа (instance) добавить Delegated MethodSig в registry // wrapper'а. Override-precedence (Own > Delegated) применяется на // call-site в resolve_overload (Ф.9.3). Multi-anonymous detection // уже сделан в 1b2. let embed_keys: Vec<String> = self.embed_fields.keys().cloned().collect(); for wrapper_type in embed_keys { let embeds = self.embed_fields.get(&wrapper_type).cloned().unwrap_or_default(); for (field_name, embedded_ty, _is_anon) in &embeds { // Найти все instance-методы embedded-типа. let embedded_methods: Vec<(String, MethodSig)> = self.method_overloads.iter() .filter(|((t, _), _)| t == embedded_ty) .flat_map(|((_, m), sigs)| sigs.iter().map(move |s| (m.clone(), s.clone()))) .filter(|(_, s)| s.is_instance && !s.is_delegated) .collect(); for (method_name, base_sig) in embedded_methods { // Сгенерировать proxy MethodSig. let key = (wrapper_type.clone(), method_name.clone()); let existing_count = self.method_overloads.get(&key) .map(|v| v.len()).unwrap_or(0); let base_c = format!("Nova_{}_method_{}", wrapper_type, method_name); let proxy_c_name = if existing_count == 0 { base_c } else { let suffix = base_sig.param_c_types.iter() .map(|t| t.replace('*', "_p") .replace(' ', "_") .replace('[', "_arr_") .replace(']', "")) .collect::<Vec<_>>() .join("_"); if suffix.is_empty() { base_c } else { format!("{}__{}", base_c, suffix) } }; let proxy_sig = MethodSig { param_c_types: base_sig.param_c_types.clone(), return_c_type: base_sig.return_c_type.clone(), is_instance: true, is_external: false, is_delegated: true, c_name: proxy_c_name, // Plan 14 Ф.6: proxy наследует variadic-флаг // от исходного метода (тот же signature). variadic_last: base_sig.variadic_last, param_defaults: base_sig.param_defaults.clone(), // Plan 128 Ф.1: D39 embed proxy inherits base method's // recv.mutable flag. ABI of the proxied call must match // the original (Ф.2 will use this when shaping the // proxy's nova_self ABI). recv_mutable: base_sig.recv_mutable, // Plan 184 (Р13/Р14): proxy inherits base method's param modes. param_modes: base_sig.param_modes.clone(), // U.4.3 c2.2: D39 embed proxy is synthesized (no single FnDecl). fn_span: None, }; self.register_method_overload(key, proxy_sig); // all_methods (для Plan 06 Iter[T] dispatch). self.all_methods.insert((wrapper_type.clone(), method_name.clone())); // method_receivers backward compat — single-key, last-wins // OK поскольку wrapper_type registered как owner of method. if !self.method_receivers.contains_key(&method_name) { self.method_receivers.insert(method_name.clone(), (wrapper_type.clone(), true)); } let _ = field_name; // используется при emit (ниже) } } } // 1d. Pre-populate generic_fns/generic_types sets for type-erased call site handling for item in &module.items { if let Item::Fn(f) = item { if !f.generics.is_empty() { self.generic_fns.insert(f.name.clone()); if f.receiver.is_none() { // Plan 48: store for monomorphization worklist drain self.mono_fn_decls.insert(f.name.clone(), f.clone()); } } } if let Item::Type(t) = item { if !t.generics.is_empty() { // Plan 62.A: Option/Result handled via NovaOpt_<T> / // Nova_Result* runtime infrastructure — не регистрируем // как generic. Иначе variant ctor (`Err("...")`) пойдёт // через `is_generic_call` void* boxing path (line // 13549-13554), который не совпадает с runtime helper // signature `nova_make_Result_Err(nova_str)`. См. также // skip в 1a (line 1207-1213). if t.name == "Option" || t.name == "Result" { continue; } // Plan 138.2 Ф.0c: user-shadowed generic name — skip // (user's non-generic decl wins, D29). if user_shadowed_generic_types.contains(&t.name) { continue; } self.generic_types.insert(t.name.clone()); } } } // Plan 48 Ф.3: drain initial generic type usages from step 1 type declarations. // This covers types that appear in other type declarations (nested generics). self.drain_generic_type_worklist()?; // Plan 48 Ф.3: placeholder for generic type instance definitions (filled after drain). self.line("/*__GENERIC_TYPE_DEFS__*/"); // [M-153.2-flat-map-inner-option]: NovaOpt typedefs for value-record // payloads (NovaValue_… by-value) must come AFTER the generic-type-defs // splice so that the struct body is complete before NovaOpt uses it in a // field declaration. Pointer payloads (Nova_X*) only need a forward-typedef // and stay in __NOVAOPT_TYPEDEFS__ which is earlier in the file. self.line("/*__NOVAOPT_VR_TYPEDEFS__*/"); // [M-181-result-over-named-tuple-codegen]: NovaRes struct bodies whose Ok/Err // payload is a by-value LATE-emitted type (named tuple / mono value-record) // must come AFTER those struct bodies too — same reason as NovaOpt VR above. // The forward `typedef struct NovaRes_<n> NovaRes_<n>;` stays in the early // __NOVARES_TYPEDEFS__ marker (pointer use in fn prototypes is fine). self.line("/*__NOVARES_VR_TYPEDEFS__*/"); self.line("/*__VR_UEQ_PROTOS__*/"); // 1b1-moved. Plan 152.4 (D199 ro-runtime side): module-level // `ro NAME = EXPR` — a lazy-static global. The strict const/ro partition // (`check_ro_module_partition`) guarantees only a genuinely runtime RHS // (call/effect/alloc) reaches here as `ro` (a constexpr-eligible RHS is // forced to `const` and handled in §1b above). We reuse the const // lazy-init getter (`emit_lazy_const`): file-scope storage + `init` flag // + `nova_const_<name>()` built on first use; reads route through // `lazy_consts` (same desugaring as a non-constexpr `const`, Plan 14 Ф.2). // // Emitted HERE (after `/*__GENERIC_TYPE_DEFS__*/`, before fn forward // decls) — NOT in §1b — for two reasons: (1) the global's C type may be a // mono'd generic (`HashMap[int,str]` → `Nova_HashMap____nova_int__nova_str`) // whose typedef is spliced into `__GENERIC_TYPE_DEFS__`; the `static <T> // _value;` storage decl must follow that typedef. (2) The getter body // calls into user fns; method-receiver (§1c) and generic (§1d) routing // tables are fully populated by now, so the call lowers correctly. // Single named binding only (Ident, or a single-segment unit Variant for // the UPPER_CASE constant-name form), non-ghost. Thread-safety of // first-touch init: see `[M-lazy-static-thread-safety]`. // // Plan 209 Ф.3 finding: a `ro NAME = some_free_fn()` initializer with // NO explicit type annotation infers its C storage type via // `infer_expr_c_type` → `infer_call_ret_c` → the `user_fn_sigs` // lookup (B10f, the authoritative source for a bare free-fn call's // return type). But `user_fn_sigs` is populated ONLY inside // `emit_fn_forward_decl` (§2 below, "Forward declarations for all // functions") — which runs AFTER this loop. So at this point // `user_fn_sigs` is still EMPTY for every free fn in the module, the // lookup misses, every other inference channel also misses (the // checker does not annotate a module-level `ro` initializer's callee // the same way it does inside a fn body), and `ty_c` ends up the // empty string. The emitted storage line // (`{top_level_storage()}{ty_c} _nova_const_{name}_value;`) then has // NO type before the name — in single-TU/`static` mode this // compiles anyway (C's legacy "implicit int" backward-compat quirk // silently treats bare `static x;` as `static int x;`), which // masked the gap; under Plan 209's multi-TU promotion the same // malformed, TYPELESS declarator can't be safely promoted to // `extern` (`split_tu`'s `decl_from_uninitialized_global` correctly // refuses — an `extern` with no type would be nonsense C) and so // stays a verbatim tentative definition duplicated into every part // via `_common.h` → `lld-link: error: duplicate symbol` (observed: // `_nova_const_module_dim_value` / `_nova_const_module_len_value`, // `spec_tests/conformance/const_init_runtime_ok_test.nv`'s // `ro module_dim = compute_dim()` / `ro module_len = alloc_len()`). // Fix: pre-seed `user_fn_sigs` for every eligible top-level free fn // (mirrors the real registration at `emit_fn_forward_decl`'s // `user_fn_sigs.insert`, `f.receiver.is_none() && f.generics.is_empty()`) // BEFORE this loop runs, so the very same authoritative B10f lookup // that already exists succeeds here too. Best-effort (`.ok()` — a // function whose param/return type doesn't translate is simply // skipped, exactly as it already is skipped implicitly today; this // is only a re-ordering of an existing registration, not new // fallback logic). The REAL pass at §2 re-inserts the identical // value later — idempotent, zero functional change for every // already-working case. // [M-http-compress-errorkind-crosspkg-collision] fix: same class of gap // as the step-1c pre-pass above — `type_ref_to_c` on every free fn's // params here runs with whatever `current_emit_file_id` the LAST // drain-A generic-instance emission left it as (often `None`/stale), // so a bare colliding type name (`ErrorKind`) falls through // `ref_type_base` to its unqualified form. Set the emission-file // context per-item here too (restored once after the loop). let saved_emit_file_id_sigpreseed = self.current_emit_file_id; let any_type_collision_sigpreseed = self.any_type_file_collision(); for item in &module.items { if let Item::Fn(f) = item { if f.receiver.is_none() && f.generics.is_empty() && !self.user_fn_sigs.contains_key(&f.name) { if any_type_collision_sigpreseed { self.current_emit_file_id = Some(f.span.file_id); } if let Some(param_c_tys) = f.params.iter() .map(|p| self.type_ref_to_c(&p.ty)) .collect::<Result<Vec<_>, _>>() .ok() { if let Ok(ret_c) = self.return_type_c(f) { self.user_fn_sigs.insert(f.name.clone(), (param_c_tys, ret_c)); } } } } } self.current_emit_file_id = saved_emit_file_id_sigpreseed; for item in &module.items { if let Item::Let(l) = item { if l.is_ghost { continue; } let name = match &l.pattern { Pattern::Ident { name, .. } => Some(name.clone()), Pattern::Variant { path, kind: VariantPatternKind::Unit, .. } if path.len() == 1 => Some(path[0].clone()), _ => None, }; if let Some(name) = name { // Plan 159 Ф.1: skip a `ro` lazy-static global unreachable // from any root. It is in `dead_consts` only when no // reachable fn/const names it, so no emitted read routes // through `nova_const_<name>()` — dropping its storage + // getter is safe. Empty (no-op) when DCE off / library mode. if dead_consts.contains(&name) { continue; } let ty_c = if let Some(ty) = &l.ty { self.type_ref_to_c(ty)? } else { self.infer_expr_c_type(&l.value) }; // [M-175-lazy-const-crossmodule-collision]: module-level // `ro NAME = expr` is ALWAYS module-private (LetDecl has // no `is_export`) — look up the module-qualified name the // pre-pass above (Step 1, `Item::Let` branch) registered, // mirroring `emit_const_decl`'s own lookup. Falls back to // the bare name only if the pre-pass found no entry // (defensive; should always be present once peer_files is // non-empty — byte-identical fallback for any edge case // it doesn't cover). let c_name = self.private_const_c_names .get(&(l.span.file_id, name.clone())) .cloned() .unwrap_or_else(|| name.clone()); self.emit_lazy_const(&name, &c_name, &ty_c, &l.value)?; } } } // Plan 157: associated `ro Type.NAME` — see assoc_ro.rs (kept out of // emit_c.rs, arch-ratchet precedent `mono_method_registry.rs`). self.emit_assoc_ro_lazy_globals(module)?; // Plan 172.14 Ф.1: классификация больших (>16Б C-ABI) read-only // value-struct параметров free-fn'ов — ДО эмиссии forward-decl'ов // (value-typedef'ы выше уже заполнили `value_struct_field_tys`). // Сигнатуры, тела и call-sites читают одну финализированную карту. self.build_free_fn_byref_map(module); // [M-172.14-methods-byref]: тот же пре-пасс для методов (receiver.is_some()). self.build_method_byref_map(module); // 2. Forward declarations for all functions (types are now known) for item in &module.items { if let Item::Fn(f) = item { if should_skip_fn(f) { continue; } // Plan 62.D bis-1: D29 shadow if is_dead_free_fn(f) { continue; } // Plan 81 Ф.7.2: DCE if is_dead_method(f) { continue; } // Plan 159 Ф.1: method DCE self.emit_fn_forward_decl(f)?; } } // Plan 48: placeholder for mono function forward declarations (filled at end) self.line("/*__MONO_FWD_DECLS__*/"); // [M-172.1-option-eq-record-structural] (L1): placeholder for structural // `nova_opt_eq_<X>` functions (heap user sum/record payloads). Placed AFTER // both the regular fn-forward-decls and /*__MONO_FWD_DECLS__*/ so any // record/field `@equal` method prototype the eq fn calls is already visible // (else implicit decl → "conflicting types" CC-FAIL). Filled at end; the // marker line is stripped (with its newline) when no structural eq fn exists // so zero-impact files stay byte-identical. // [M-result-direct-recursive-enum] / [M-option-self-recursive-record-mono] // (Plan 186): placeholder for `nova_struct_eq_<T>` prototypes (see // `struct_eq_protos_buf`/`emit_named_struct_eq_call`). Placed just before // `/*__NOVAOPT_EQ_FNS__*/` — same phase-correctness window (struct bodies // + method fwd-decls already visible); stripped (with its newline) when // no cyclic type triggered the named-fn path, so zero-impact files stay // byte-identical to the clean binary. self.line("/*__STRUCT_EQ_PROTOS__*/"); self.line("/*__NOVAOPT_EQ_FNS__*/"); // Forward declarations for test impl functions { let mut idx = 0usize; for item in &module.items { if let Item::Test(t) = item { let safe = Self::mangle_test_name_indexed(&t.name, idx); idx += 1; self.line(&format!("{}nova_unit nova_test_{}(void);", self.top_level_storage(), safe)); } } } self.line(""); // 3. Pre-scan: emit forward decls for all handler impl functions before fn definitions. // Uses handler_counter (starts at 0 here) to assign stable IDs matching step 4. self.emit_handler_forward_decls(module)?; // 4. Function definitions — but first a pre-pass to collect lambda forward decls. // Lambda impls are collected during the first pass into lambda_forward_decls + lambda_impls. // We do a two-step emit: (a) pre-emit all fns/tests to collect lambdas, then // (b) insert lambda_forward_decls + lambda_impls before the fn output. // Simpler approach: emit all fns; before flush, emit lambda_forward_decls + lambda_impls. for item in &module.items { if let Item::Fn(f) = item { if should_skip_fn(f) { continue; } // Plan 62.D bis-1: D29 shadow if is_dead_free_fn(f) { continue; } // Plan 81 Ф.7.2: DCE if is_dead_method(f) { continue; } // Plan 159 Ф.1: method DCE self.emit_fn(f)?; } } // 4b. D39 / Plan 11 Ф.9: emit auto-proxy method bodies for embeds. self.emit_embed_proxies()?; // 5. Test function definitions { let mut idx = 0usize; for item in &module.items { if let Item::Test(t) = item { self.emit_test(t, idx)?; idx += 1; } } } // Plan 70.2: drain generic_type_worklist after test emission — test // bodies могут enqueue generic-type instances (e.g. LinkedList[int] via // Cons-constructor inference в try_infer_variant_mono_args). Без этого // drain instances enqueued at emit_test stage никогда не processed, // → "use of undeclared identifier 'Nova_LinkedList____nova_int'". self.drain_generic_type_worklist()?; // 5b. Plan 57: Bench function definitions (только в bench_mode). // В обычной сборке bench items игнорируются — tests/main работают // обычно. В bench_mode tests игнорируются (см. emit_main_wrapper). if self.bench_mode { // Plan 57: define TLS bench state once per binary. self.out.push_str("NOVA_BENCH_STATE_DEFINE;\n"); // Plan 57.C.3: heap sampler thread globals (libuv-conditional). self.out.push_str("NOVA_BENCH_HEAP_SAMPLER_THREAD_DEFINE\n\n"); let mut idx = 0usize; for item in &module.items { if let Item::Bench(b) = item { // Plan 57.B.5: grouped — каждый case даёт отдельную entry. if !b.groups.is_empty() { for grp in &b.groups { for case in &grp.cases { let composite = format!("{}/{}/{}", b.name, grp.name, case.name); let cloned = BenchDecl { name: composite, setup: case.setup.clone(), measure_body: case.measure_body.clone(), teardown: case.teardown.clone(), params: None, groups: Vec::new(), span: case.span, }; self.emit_bench(&cloned, idx)?; idx += 1; } } continue; } // Plan 57.B.3: parameterized sweep — emit одну entry // на каждый param value, с let-binding prepended. if let Some(p) = &b.params { for v in &p.values { let suffix_name = format!("{}/p={}", b.name, v); let mut prepended_setup = vec![ Self::synth_int_let(&p.var_name, *v, p.span), ]; for s in &b.setup { prepended_setup.push(s.clone()); } let cloned = BenchDecl { name: suffix_name, setup: prepended_setup, measure_body: b.measure_body.clone(), teardown: b.teardown.clone(), params: None, groups: Vec::new(), span: b.span, }; self.emit_bench(&cloned, idx)?; idx += 1; } } else { self.emit_bench(b, idx)?; idx += 1; } } } } // Plan 48: drain monomorphization worklist to fixpoint (R3: polymorphic recursion guard). // Ф.7.6: limit задаётся через CLI `--mono-depth=N` (set_mono_depth_limit) // или fallback на env var `NOVA_MONO_DEPTH`; оба читаются в `new()`. { let limit = self.mono_depth_limit; let mut safety = 0usize; while !self.mono_worklist.is_empty() || !self.pending_container_eq_monos.borrow().is_empty() { safety += 1; if safety > limit { return Err(format!( "instantiation depth limit {} exceeded (possible polymorphic recursion); \ add a non-generic base case, use explicit bounds to terminate, \ or raise via --mono-depth=N CLI flag (or NOVA_MONO_DEPTH env var)", limit )); } // [M-172.1-option-container-eq-structural] + [M-172.1-option-hashmap-eq-structural]: // instantiate the `@equal` mono of every CONTAINER (Vec/HashMap/Set) whose // nested structural eq emitted a call to `<container>____<args>_method_equal` // (recorded `&self` in emit_field_eq; mono registration is `&mut`). This may // enqueue further monos (the container body calls its element `==`) → drained // on the next loop turn. The `container_eq_requested` guard keeps it monotone. let pending: Vec<String> = std::mem::take(&mut *self.pending_container_eq_monos.borrow_mut()); for cont_c in pending { self.register_container_eq_mono(&cont_c); } let batch: Vec<_> = std::mem::take(&mut self.mono_worklist); for (fn_name, type_subst, mono_name) in batch { // Plan 48 V1 fallback: __erased__ prefix marks on-demand erased emission. if let Some(real_name) = fn_name.strip_prefix("__erased__") { if let Some(fn_decl) = self.mono_fn_decls.get(real_name).cloned() { self.emit_generic_fn_erased(&fn_decl)?; } continue; } // Plan 48: __method__TYPE::name prefix marks generic method instances. if let Some(rest) = fn_name.strip_prefix("__method__") { if let Some((recv_type, mname)) = rest.split_once("::") { let key = (recv_type.to_string(), mname.to_string()); // 1. Direct lookup (for non-generic types in mono_method_decls). // 2. Fallback via base name lookup when recv_type is mangled: // a. try mono_method_decls[base] // b. try generic_type_methods[base] (methods skipped in 1c) let base_opt: Option<String> = if self.mono_method_decls.contains_key(&key) { None } else { // recv_type from section 5b has no "Nova_" prefix; map keys do. let info = self.generic_type_instance_info.borrow(); info.get(recv_type) .or_else(|| info.get(&format!("Nova_{}", recv_type))) .map(|(b, _)| b.clone()) }; let fn_decl_opt = if let Some(fd) = self.mono_method_fndecl_for_name.get(&mono_name).cloned() { // [M-138.2-generic-method-overload-mono] exact overload chosen // at the call-site (keyed on the suffixed mono name) — authoritative // over the bare-name first-wins re-find below. Some(fd) } else if let Some(fd) = self.mono_method_decls.get(&key).cloned() { Some(fd) } else if recv_type.starts_with("Vec____") && self.mono_method_decls .contains_key(&("[]T".to_string(), mname.to_string())) { // Plan 138.2 Ф.0-final: an array-extension method // (`fn[T] []T @map`) monomorphized onto a Vec-mono // receiver. The base FnDecl is keyed under `"[]T"` // (recv.type_name), not the mangled Vec receiver, // and is NOT a `generic_type_methods["Vec"]` entry // (it never was a Vec method). Route to the `[]T` // base body; `recv_type` stays `Vec____<elem>` so // `emit_monomorphized_method` emits the // `Nova_Vec____<elem>_method_<m>__…` instance with // the typed Vec receiver — matching the call-site // symbol emitted by the mono-routing block. self.mono_method_decls .get(&("[]T".to_string(), mname.to_string())) .cloned() } else if let Some(ref base) = base_opt { self.mono_method_decls.get(&(base.clone(), mname.to_string())) .cloned() .or_else(|| { self.generic_type_methods.get(base) .and_then(|ms| ms.iter().find(|m| m.name == mname)) .cloned() }) } else { // Plan 95 Ф.1.1: builtin sum-types (`Option`/ // `Result`) — `recv_type` in worklist-key is // already the base name itself (`"Option"`, // `"Result"`), they are not registered in // `generic_type_instance_info`, so `base_opt` // is `None` here. For user generic types // `recv_type` is mangled — `.get(mangled)` // returns `None`, no harm. self.generic_type_methods.get(recv_type) .and_then(|ms| ms.iter().find(|m| m.name == mname)) .cloned() }; if let Some(fn_decl) = fn_decl_opt { let rt = recv_type.to_string(); self.emit_monomorphized_method(&fn_decl, type_subst, &mono_name, &rt)?; } } continue; } if let Some(fn_decl) = self.mono_fn_decls.get(&fn_name).cloned() { self.emit_monomorphized_fn(&fn_decl, type_subst, &mono_name)?; } } // Plan 48 Ф.3: drain new generic type instances enqueued by mono'd fn bodies. self.drain_generic_type_worklist()?; } } // 6. Handler impl function bodies (ctx structs + bodies at file scope, after fn defs) if !self.deferred_impls.is_empty() { self.out.push_str(&self.deferred_impls.clone()); self.out.push('\n'); } // `[M-lazy-const-init-race]`: every module's lazy consts have been // collected into `pending_const_inits` by now (all `emit_const_decl`/ // `ro`-global passes across every module are done) — combine into // ONE topo-sorted `nova_consts_init()`, emitted textually right // before `main()` (which `emit_main_wrapper` emits next, and whose // body calls it via the `/*__CONSTS_INIT_CALL__*/` marker below). let consts_init_fn = self.render_consts_init_fn(); self.out.push_str(&consts_init_fn); self.emit_main_wrapper(module); // Plan 36 followup: splice user-type forward decls в маркер // `/*__USER_TYPE_FWD_DECLS__*/`. Должно быть ДО NovaOpt replace, // потому что NovaOpt typedef'ы могут ссылаться на эти forward decls. let user_fwd_replacement = if self.user_type_fwd_decls.is_empty() { String::new() } else { format!( "/* Plan 36: forward decls для user types — нужны для NovaOpt_<T> */\n{}", self.user_type_fwd_decls) }; self.out = self.out.replace("/*__USER_TYPE_FWD_DECLS__*/", &user_fwd_replacement); // [реестр 221.1 №139 Round 3] `render_unified_value_types` computes // ONE topologically-sorted section covering user value-records, // Record-kind generic-instances, AND mono tuple/fixed-array // (folded in here — see that fn's doc for why they cannot stay a // separate, position-fixed category either: a NORMAL (non-late) // `Option`/`Result` payload — e.g. `Option[Utf8Error]`, std // prelude, ubiquitous — is NOT covered by the `NovaOpt`/`NovaRes` // VR-routing (`debt_is_late_emitted_value_payload` recognizes MONO // value-records/named-tuples as "late"; #361 extends it to a PLAIN // record too, but ONLY if it embeds one — else early), so it must be BEFORE // `__NOVAOPT_TYPEDEFS__`/`__NOVARES_TYPEDEFS__` — i.e. at THIS // (early) marker position, not the later `__GENERIC_TYPE_DEFS__` // position rounds 1/2/first cut of round 3 tried). Spliced here, // at the ORIGINAL `__VALUE_RECORD_DEFS__` position — the earliest // safe point, before tuples/NovaOpt/NovaRes/generic-instances all // still need it to have already run. let vr_defs = self.render_unified_value_types(); let vr_replacement = if vr_defs.is_empty() { String::new() } else { format!("/* Value-record / generic-instance / tuple / fixed-array struct \ definitions (complete, topologically sorted — реестр 221.1 №139 \ Round 3): */\n{}", vr_defs) }; self.out = self.out.replace("/*__VALUE_RECORD_DEFS__*/", &vr_replacement); // Plan 14 Ф.1: splice NovaOpt_<T> typedefs в позицию маркера. // К этому моменту все type_ref_to_c-вызовы (включая в bodies) // отработали и заполнили novaopt_typedefs_buf в правильном // topological order. let typedefs = self.novaopt_typedefs_buf.borrow().clone(); let replacement = if typedefs.is_empty() { String::new() } else { format!( "/* Plan 14 Ф.1: lazy NovaOpt_<T> typedef'ы — для T без \ NOVA_ARRAY_DECL в runtime. Order: registration */\n{}", typedefs) }; // 172.4 Ф.3 A3: прототипы vr-ueq обёрток — ПЕРЕД инлайн opt_eq этой зоны // (struct NovaValue_X определён выше, тела обёрток — в NOVAOPT_EQ_FNS). let vr_protos = self.vr_ueq_protos_buf.borrow().clone(); let replacement = if vr_protos.is_empty() { replacement } else { format!("{}{}", vr_protos, replacement) }; self.out = self.out.replace("/*__NOVAOPT_TYPEDEFS__*/", &replacement); // Plan 59 Ф.7.5: splice mono'd NovaRes_<ok>_<err> typedefs. let novares_typedefs = self.novares_typedefs_buf.borrow().clone(); let novares_replacement = if novares_typedefs.is_empty() { String::new() } else { format!( "/* Plan 59 Ф.7.5: mono'd NovaRes_<ok>_<err> Result typedef'ы. \ Order: registration */\n{}", novares_typedefs) }; self.out = self.out.replace("/*__NOVARES_TYPEDEFS__*/", &novares_replacement); // Plan 95 Ф.2.4: splice forward-deklarations mono'd builtin-sum- // method'ов. ПОСЛЕ NovaOpt/NovaRes typedef splice'ей — сигнатуры // могут содержать NovaOpt_<T> (by-value, complete-type required) // или NovaRes_<n>* (pointer, forward-typedef достаточно). let builtin_sum_fwd = self.builtin_sum_method_fwd_decls.clone(); let builtin_sum_fwd_replacement = if builtin_sum_fwd.is_empty() { String::new() } else { format!( "/* Plan 95 Ф.2.4: forward decls для mono'd Nova-body \ методов на Option/Result (placed after NovaOpt/NovaRes \ typedefs — by-value/by-pointer signature requires \ complete/forward typedef visible). */\n{}", builtin_sum_fwd) }; self.out = self.out.replace( "/*__BUILTIN_SUM_METHOD_FWD_DECLS__*/", &builtin_sum_fwd_replacement); // Plan 48 Ф.3: splice generic type instance definitions. [реестр // 221.1 №139 Round 3]: Record-kind instances no longer land here — // captured into `pending_value_nodes` instead, rendered earlier as // part of `render_unified_value_types` (spliced at the EARLIER // `__VALUE_RECORD_DEFS__` position — see that call's doc). Only // non-Record kinds (Sum/Newtype/etc.) remain in this buffer, // untouched direct path — safely able to depend on anything in the // unified section since it now comes textually BEFORE this marker. let generic_type_defs = std::mem::take(&mut self.generic_type_defs_buf); self.out = self.out.replace("/*__GENERIC_TYPE_DEFS__*/", &generic_type_defs); // [M-153.2-flat-map-inner-option]: splice value-record NovaOpt typedefs // (placed AFTER __GENERIC_TYPE_DEFS__ so struct bodies are complete). let vr_typedefs = self.novaopt_vr_typedefs_buf.borrow().clone(); let vr_replacement = if vr_typedefs.is_empty() { String::new() } else { format!( "/* [M-153.2]: NovaOpt typedefs for value-record payloads — \ after generic struct bodies */\n{}", vr_typedefs) }; self.out = self.out.replace("/*__NOVAOPT_VR_TYPEDEFS__*/", &vr_replacement); // [M-181-result-over-named-tuple-codegen]: splice NovaRes struct bodies + // constructors for wrappers whose by-value payload is a late-emitted named // tuple / value-record (placed AFTER __NOVAOPT_VR_TYPEDEFS__ so the payload // struct body is complete). The forward typedef is already in the early // __NOVARES_TYPEDEFS__ marker. let novares_vr = self.novares_vr_typedefs_buf.borrow().clone(); if novares_vr.is_empty() { // Unused → strip the marker AND its trailing newline so 0-impact .c // files stay byte-identical to the clean binary (this marker line did // not exist before this change; leaving a bare `\n` would add a blank // line to EVERY file). Mirrors `self.line(...)` emitting `marker\n`. self.out = self.out.replace("/*__NOVARES_VR_TYPEDEFS__*/\n", ""); } else { let novares_vr_replacement = format!( "/* [M-181-result-over-named-tuple-codegen]: NovaRes struct bodies + \ ctors for by-value named-tuple/value-record payloads — after struct bodies */\n{}", novares_vr); self.out = self.out.replace("/*__NOVARES_VR_TYPEDEFS__*/", &novares_vr_replacement); } // Plan 48: splice monomorphized fn forward declarations let mono_fwd = self.mono_fwd_decls.clone(); self.out = self.out.replace("/*__MONO_FWD_DECLS__*/", &mono_fwd); // [M-172.1-option-eq-record-structural] (L1): splice structural opt_eq fns // AFTER all method forward-decls (regular + mono) so any record/field // `@equal` call inside them resolves to a visible prototype. Strip the // marker AND its trailing newline when empty so zero-impact files stay // byte-identical to the clean binary (mirrors __NOVARES_VR_TYPEDEFS__). self.out = self.out.replace("/*__VR_UEQ_PROTOS__*/ ", ""); // [M-result-direct-recursive-enum] / [M-option-self-recursive-record-mono] // (Plan 186): splice `nova_struct_eq_<T>` prototypes BEFORE the eq-fn // bodies (mirrors the __NOVARES_VR_TYPEDEFS__ empty/non-empty pattern). let struct_eq_protos = self.struct_eq_protos_buf.borrow().clone(); if struct_eq_protos.is_empty() { self.out = self.out.replace("/*__STRUCT_EQ_PROTOS__*/\n", ""); } else { let struct_eq_protos_replacement = format!( "/* [M-result-direct-recursive-enum]/[M-option-self-recursive-record-mono]: \ named struct-eq fn prototypes for self-/mutually-recursive heap types */\n{}", struct_eq_protos); self.out = self.out.replace("/*__STRUCT_EQ_PROTOS__*/", &struct_eq_protos_replacement); } let eq_fns = self.novaopt_eq_fns_buf.borrow().clone(); if eq_fns.is_empty() { self.out = self.out.replace("/*__NOVAOPT_EQ_FNS__*/\n", ""); } else { let eq_fns_replacement = format!( "/* [M-172.1-option-eq-record-structural]: structural nova_opt_eq fns \ for heap user sum/record payloads — after method fwd-decls */\n{}", eq_fns); self.out = self.out.replace("/*__NOVAOPT_EQ_FNS__*/", &eq_fns_replacement); } // [M-tuple-fixarr-typedef-order] fix (2026-07-19), SUPERSEDED by // [реестр 221.1 №139 Round 3]: mono tuple/fixed-array typedefs used // to be topo-sorted (tuple-vs-fixarr mutual deps only) and spliced // here, at their OWN dedicated marker. That two-family sort is now // FOLDED IN to `render_unified_value_types` (called earlier, at // `/*__VALUE_RECORD_DEFS__*/` — see that call site's doc): a plain // user value-record used as a NORMAL `Option`/`Result` payload needs // to be available BEFORE `__NOVAOPT_TYPEDEFS__`, which sits BEFORE // this marker — so tuples/fixarr had to move to the SAME earlier // position anyway once value-records/generic-instances joined the // same graph (a generic-instance can need a tuple, round 1's // `Vec[(str,str)]` — the three categories are not independently // orderable). This marker is now a permanent no-op splice (mirrors // `/*__MONO_FIXARR_TYPEDEFS__*/`'s existing retirement below) so a // compile-unit with zero tuple/fixarr instances keeps identical // preamble output. self.out = self.out.replace("/*__MONO_TUPLE_TYPEDEFS__*/\n", ""); // Plan 186 [bug-2 audit-197 fix]: splice extern prototypes for FREE // external fn returning a tuple (see `extern_fn_tuple_protos` field // doc). Empty/non-empty pattern mirrors __NOVARES_VR_TYPEDEFS__ above. if self.extern_fn_tuple_protos.is_empty() { self.out = self.out.replace("/*__EXTERN_FN_PROTOS__*/\n", ""); } else { let replacement = format!( "/* Plan 186 [bug-2 audit-197 fix]: extern prototypes for user-declared \ FREE external fn returning a tuple — without these, C falls back to an \ implicit `int` return declaration, which cannot initialize the tuple-struct \ call-site temp. */\n{}", self.extern_fn_tuple_protos); self.out = self.out.replace("/*__EXTERN_FN_PROTOS__*/", &replacement); } // [M-tuple-fixarr-typedef-order] fix (2026-07-19): the `[N]T` INLINE // fixed-array typedefs used to be topo-sorted and spliced HERE, at // their own marker — that per-family sort is now folded into the // combined tuple+fixarr pass above (spliced at `/*__MONO_TUPLE_TYPEDEFS__*/`, // see the comment there for why). This marker is retired as a permanent // no-op — its trailing newline (from the preamble `self.line(...)` call) // is consumed together with the marker (mirrors the empty-case pattern // above for `/*__EXTERN_FN_PROTOS__*/`) so it doesn't leave a stray // blank line behind. self.out = self.out.replace("/*__MONO_FIXARR_TYPEDEFS__*/\n", ""); // Plan 148 Ф.4 [M-codegen-unify-tuple-repr]: splice the legacy // all-`nova_int` `_NovaTupleN` typedefs — emitted ON DEMAND, only for // the arities the erased-generic fallback actually requested (replaces // the retired blanket `_NovaTuple1..8` pre-declaration). In practice this // is just arity 2 (erased HashMap/Set `(K, V)` pairs). Each typedef is // idempotent-guarded so it composes with the mono'd-tuple decls above // and with any shim-header forward declarations. let legacy_arities: Vec<usize> = self.legacy_tuple_arities.borrow().iter().copied().collect(); let mut legacy_decls = String::new(); if !legacy_arities.is_empty() { legacy_decls.push_str("/* Plan 148 Ф.4: legacy all-int tuple typedefs (erased-generic fallback, on-demand). */\n"); for n in &legacy_arities { let fields: String = (0..*n).map(|i| format!("nova_int f{};", i)).collect::<Vec<_>>().join(" "); legacy_decls.push_str(&format!( "#ifndef NOVA_TUPLE_TYPEDEF__NovaTuple{n}\n#define NOVA_TUPLE_TYPEDEF__NovaTuple{n}\n", n = n)); legacy_decls.push_str(&format!( "typedef struct {{ {} }} _NovaTuple{};\n#endif\n", fields, n)); } } self.out = self.out.replace("/*__LEGACY_TUPLE_TYPEDEFS__*/", &legacy_decls); // Plan 173 Ф.5 п.2 (D192-РЕТРАКТ): CleanupTimeoutError type-id // registration + typed-throw splice УДАЛЕНЫ вместе с типом — // force-прерывания cleanup'а не существует; watchdog-варн + // duration/overrun в ResourceTrace exit-событии (D185 amend). // Plan 174 (D349): register TimeoutError type id (prelude type thrown by // the supervised scope-deadline runtime path). Gated on the real type // definition (#no_prelude discipline) so `_nova_throw_scope_timeout_impl` // is emitted only when the struct // Nova_TimeoutError + NOVA_TID_USER_TimeoutError are available. if self.record_schemas.contains_key("TimeoutError") { self.register_type_id("TimeoutError"); } // Plan 173.2 (supervision-as-effect): Supervisor decision-bridge — // computed BEFORE the __TYPEID_DEFINES__ splice below because the // bridge boxes string-throw errors into `any` and must register the // `str` NovaTypeInfo static (debt_register_any_typeinfo mutates // any_typeinfos, rendered into tid_defines). Gated on the CU knowing // BOTH the Supervisor effect schema AND the Decision sum (prelude // present) — `#no_prelude` CUs get empty splices and the runtime // falls back to Escalate-all (fn pointer stays NULL). let (sup_impl, sup_init) = if self.effect_schemas.contains_key("Supervisor") && self.sum_schemas.contains_key("Decision") { let str_tinfo = self.debt_register_any_typeinfo("nova_str"); let impl_block = format!("\ /* Plan 173.2: Supervisor decision bridge — assigned to\n\ * _nova_supervisor_decide_fn in main(). Called by the runtime's serialized\n\ * decision pass (nova_supervised_process_decisions, fibers.h) once per\n\ * retained child failure, on the scope's drive thread. Maps the ambient\n\ * Nova `Supervisor.on_child_fail(idx, err)` handler's Decision to the\n\ * NOVA_SUPERVISE_* codes. No handler → Escalate (the default policy). */\n\ static nova_int _nova_supervisor_decide_impl(void* _scope_v, nova_int _idx, const void* _err_v) {{\n\ NovaFiberQueue* _scope = (NovaFiberQueue*)_scope_v;\n\ const NovaChildError* _err = (const NovaChildError*)_err_v;\n\ NovaVtable_Supervisor* _h = _nova_handler_Supervisor;\n\ if (!_h) return (nova_int)NOVA_SUPERVISE_ESCALATE;\n\ /* Box the failure into Nova `any`: typed-throw payload keeps its type\n\ * (narrowable via `err is T`); string throws / panics box the message\n\ * as `str`. */\n\ void* _e_any = NULL;\n\ if (_err->payload != NULL && _err->tid != 0) {{\n\ _e_any = nova_any_from_boxed(_err->payload, _err->tid);\n\ }} else {{\n\ nova_str _m = nova_str_from_cstr(_err->msg ? _err->msg : \"\");\n\ _e_any = nova_any_box(&{ti}, &_m, sizeof(nova_str));\n\ }}\n\ /* Q-block (173.2): `Fail` is allowed inside the handler = Escalate-with-\n\ * handler-error. Guard the invocation with a local fail-frame so a\n\ * handler throw cannot longjmp past the drive loop (which would abandon\n\ * still-running children); the thrown error is fed into the scope's\n\ * primary machinery and the child failure escalates normally (rank\n\ * precedence decides the surviving primary — a child PANIC still wins,\n\ * D13). */\n\ NovaFailFrame _sf;\n\ nova_fail_push(&_sf);\n\ if (setjmp(_sf.jmp) == 0) {{\n\ Nova_Decision* _d = _h->on_child_fail(_h->ctx, _idx, _e_any);\n\ nova_fail_pop();\n\ if (_d == NULL) return (nova_int)NOVA_SUPERVISE_ESCALATE;\n\ switch (_d->tag) {{\n\ case NOVA_TAG_Decision_Stop: return (nova_int)NOVA_SUPERVISE_STOP;\n\ /* Escalate + defensive default: словарь Decision = Escalate|Stop\n\ * (Restart-семейство ретрактировано, D416-амендмент 2026-07-10). */\n\ case NOVA_TAG_Decision_Escalate:\n\ default: return (nova_int)NOVA_SUPERVISE_ESCALATE;\n\ }}\n\ }} else {{\n\ nova_fail_pop();\n\ nova_fiber_report_atomic_kinded(_scope, _sf.error_msg.ptr, _sf.error_kind,\n\ _sf.error_reason_ptr, _sf.error_user_payload,\n\ _sf.error_user_type_id);\n\ return (nova_int)NOVA_SUPERVISE_ESCALATE;\n\ }}\n\ }}\n", ti = str_tinfo); let init_line = " _nova_supervisor_decide_fn = &_nova_supervisor_decide_impl;".to_string(); (impl_block, init_line) } else { (String::new(), String::new()) }; self.out = self.out.replace("/*__SUPERVISOR_DECIDE_IMPL__*/", &sup_impl); self.out = self.out.replace("/*__SUPERVISOR_DECIDE_INIT__*/", &sup_init); // Plan 61 Ф.1: splice TypeId defines + overriding nova_typeid_to_name. // Каждый registered user-type получает `#define NOVA_TID_USER_<X> N`; // также emit'тся overriding `nova_typeid_to_name` switch для diagnostic. // Если registry пустой — emit'тся только пустой replacement (weak fallback // в typeid.c обеспечивает linkage). let mut tid_defines = String::new(); if !self.type_id_registry.is_empty() { tid_defines.push_str("/* Plan 61 Ф.1: per-type NovaTypeId constants. */\n"); // Stable order по ID для deterministic emit. let mut entries: Vec<(&String, &u32)> = self.type_id_registry.iter().collect(); entries.sort_by_key(|&(_, id)| *id); for (name, id) in &entries { tid_defines.push_str(&format!( "#define NOVA_TID_USER_{} ((NovaTypeId){})\n", name, id )); } // Overriding nova_typeid_to_name implementation. Weak в typeid.c // covers basic primitives; здесь добавляем user-types branches. // Тестировать что only-strong-symbol overriding работает на всех // toolchains (Clang/MSVC/GCC) — Ф.8 cross-toolchain gate. // Bootstrap: эмитим как static helper used только в diagnostic // path (`nova: unhandled typed Fail`), не overrides weak — это // меньше platform-specific risk. tid_defines.push_str("\n/* Diagnostic helper для user-types (called from Plan 61\n"); tid_defines.push_str(" * unhandled-fail path; static, не overrides typeid.c weak). */\n"); tid_defines.push_str("static inline const char* nova_typeid_user_name(NovaTypeId tid) {\n"); tid_defines.push_str(" switch (tid) {\n"); for (name, _) in &entries { tid_defines.push_str(&format!( " case NOVA_TID_USER_{}: return \"{}\";\n", name, name )); } tid_defines.push_str(" default: return nova_typeid_to_name(tid);\n"); tid_defines.push_str(" }\n"); tid_defines.push_str("}\n"); } // Plan 174.3 (D53/D54 v1): per-type NovaTypeInfo statics for any-boxing. // Emitted after the NOVA_TID_* #defines they reference (user-type TIDs // are in the block above; primitives come from typeid.h). Independent of // `type_id_registry` emptiness — a program that only boxes primitives has // an empty user-TID registry but still needs these statics. if !self.any_typeinfos.is_empty() { tid_defines.push_str( "\n/* Plan 174.3: per-type NovaTypeInfo statics (any-boxing). */\n", ); for (sani, (tid_macro, name)) in &self.any_typeinfos { tid_defines.push_str(&format!( "{}const NovaTypeInfo NOVA_TYPEINFO_{} = {{ {}, \"{}\" }};\n", self.top_level_storage(), sani, tid_macro, name )); } } self.out = self.out.replace("/*__TYPEID_DEFINES__*/", &tid_defines); // Plan 174.4: effect-registry compile-time размер marker. N = число // distinct-эффектов в реестре (built-in Fail/Time + user-defined; // "Mem" REMOVED from this count — D76 amend, no longer an effect). // Клампим к >=1 (C запрещает массив [0]; на практике built-in гарантируют // N>=3). Build-слой читает это число и прокидывает -DNOVA_MAX_EFFECT_STORAGES=N // во ВСЕ TU → silent-drop 33-го эффекта невозможен, snapshot = ровно N // указателей вместо фикс-256B, и generated/runtime TU согласованы по ABI. let effect_count = self.effect_schemas.len().max(1); self.out = self.out.replace( "/*__EFFECT_COUNT_MARKER__*/", &format!("/* nova-effect-count: {} */", effect_count), ); // Plan 173 Ф.5 п.2 (D192-ретракт): __CLEANUP_TIMEOUT_IMPL__/__INIT__ // splice удалён вместе с CleanupTimeoutError (Plan 110.9.1 retired). // Plan 174 (D349): typed TimeoutError throw impl — assigned to // _nova_throw_scope_timeout_fn in main() when TimeoutError is // referenced. Replaces the string-fallback in nova_throw_scope_timeout. let (to_impl, to_init) = if self.type_id_registry.contains_key("TimeoutError") { let impl_block = "\ /* Plan 174 (D349): typed TimeoutError throw — assigned to\n\ * _nova_throw_scope_timeout_fn in main(). Replaces the string-fallback in\n\ * nova_throw_scope_timeout when TimeoutError is referenced. */\n\ static void _nova_throw_scope_timeout_impl(int64_t deadline_ns) {\n\ Nova_TimeoutError* _e = (Nova_TimeoutError*)nova_alloc(sizeof(Nova_TimeoutError));\n\ _e->deadline_ns = (int64_t)deadline_ns;\n\ (void)nova_throw_typed(nova_str_from_cstr(\"supervised-timeout: scope deadline exceeded\"), (void*)_e, NOVA_TID_USER_TimeoutError);\n\ /* unreachable */\n\ }\n".to_string(); let init_line = " _nova_throw_scope_timeout_fn = &_nova_throw_scope_timeout_impl;".to_string(); (impl_block, init_line) } else { (String::new(), String::new()) }; self.out = self.out.replace("/*__SCOPE_TIMEOUT_IMPL__*/", &to_impl); self.out = self.out.replace("/*__SCOPE_TIMEOUT_INIT__*/", &to_init); // Plan 61 followup #4: per-E Fail dispatch splice. let per_e_decls = self.render_per_e_fail_decls(); self.out = self.out.replace("/*__PER_E_FAIL_DECLS__*/", &per_e_decls); // Plan 139 Ф.6: interned string-literal statics splice. Collected // during body emission (intern_str_literal), emitted at the preamble // marker as one shared rodata buffer + one nova_str value per content. let interned = self.render_interned_str_literals(); // Plan 186 (D412): blob statics share the same preamble marker // (plain uint8_t arrays -- no type dependencies). let interned_blobs = self.render_interned_blob_literals(); self.out = self.out.replace( "/*__INTERNED_STR_LITERALS__*/", &format!("{}{}", interned, interned_blobs), ); // Plan 70 Ф.B0 (session 2): strict-error finalization gate. // Cascade-blocked sites (infer_expr_c_type, register_mono_instance, // register_mono_method_instance, infer_mono_method_ret_with_args, etc.) // не могут propagate `?` без массивного caller-chain refactor — вместо // этого они push'ат E7001 в `strict_errors`. Здесь aggregate'им и failim // codegen pass если non-empty. Production-grade strict mode: ANY silent // fallback = build failure (default, no opt-out env var). // №466 Ф.1: сообщить, если режим сборки выключил `#debug`-проверки. // Вызов стоит ДО частичного move `self` ниже. Молчание здесь и было // ложным обещанием защиты в release-сборке. self.report_debug_erasure(); let strict_errors = self.strict_errors.into_inner(); let warnings = self.warnings.into_inner(); if !strict_errors.is_empty() { // Deduplicate (один site может быть hit multiple раз на разных // generic instantiations — diagnostic message identical). let mut seen = std::collections::HashSet::new(); let unique: Vec<String> = strict_errors.into_iter() .filter(|m| seen.insert(m.clone())) .collect(); let count = unique.len(); return Err(format!( "Plan 70 strict type propagation — {} unique silent fallback site(s) detected:\n\n{}", count, unique.join("\n\n") )); } Ok((self.out, warnings)) } /// Plan 209 Ф.1 (A4): multi-TU-aware wrapper around `emit_module`. /// /// `emit_module` itself is left COMPLETELY UNCHANGED by Plan 209 — every /// existing caller (`main.rs`, `test_runner.rs`, `bench/run.rs`) keeps /// calling it directly and keeps getting exactly the single-`.c` shape /// it always has (back-compat, per recon-notes.md §8 A4). This wrapper /// is the future Ф.2 toolchain's entry point: it runs the identical /// emission, then — ONLY if multi-TU is enabled (`NOVA_MULTI_TU`) AND /// the resulting CU is over the size/fn-count threshold (recon-notes /// §6) — hands the finalized string to `split_tu` (A2) and returns the /// split shape instead. Nothing in this repo calls this function yet /// (Ф.2 — parallel compile+link — is out of scope for Ф.1); it exists /// so Ф.2 has a stable, already-gated entry point to wire up. /// /// Byte-identity guarantee: when multi-TU is disabled (the default) OR /// the CU is under threshold, this returns `EmitOutput::Single` wrapping /// the EXACT SAME string `emit_module` would have returned — `split_tu` /// is never invoked on that path. pub fn emit_module_multi_tu( self, module: &Module, cu_name: &str, ) -> Result<(EmitOutput, Vec<String>), String> { let multi_tu_enabled = self.multi_tu_enabled; let (finalized, warnings) = self.emit_module(module)?; if !multi_tu_enabled || !exceeds_multi_tu_threshold(&finalized) { return Ok((EmitOutput::Single(finalized), warnings)); } let split = super::split_tu::split_tu(&finalized, cu_name, MULTI_TU_PART_THRESHOLD_BYTES)?; Ok(( EmitOutput::Split { common_h: split.common_h, parts: split.parts }, warnings, )) } /// Mangle a test name and append a numeric suffix to guarantee uniqueness. /// Plan 57.B.3: synthesize `let <name> = <value>;` Stmt для prepending /// param-substitution в setup. fn synth_int_let(name: &str, value: i64, span: crate::diag::Span) -> Stmt { let int_lit = Expr { kind: ExprKind::IntLit(value), span, id: crate::ast::ExprId::UNSET, debug_only: false, }; Stmt::Let(LetDecl { mutable: false, pattern: Pattern::Ident { name: name.to_string(), span, is_mut: false, is_consume: false }, ty: None, value: int_lit, span, is_ghost: false, consume: false, }) } fn mangle_test_name_indexed(name: &str, index: usize) -> String { let base: String = name.chars() .map(|c| if c.is_ascii_alphanumeric() || c == '_' { c } else { '_' }) .collect(); format!("{}_{}", base, index) } /// Plan 57: emit setup/measure/teardown functions for one BenchDecl. /// Аналогично emit_test, но эмитит ТРИ static функции: /// - nova_bench_setup_<safe>() /// - nova_bench_measure_<safe>() /// - nova_bench_teardown_<safe>() /// Orchestration делается в emit_main_wrapper bench-mode: вызывает /// nova_bench_run("name", setup, measure, teardown). fn emit_bench(&mut self, b: &BenchDecl, idx: usize) -> Result<(), String> { // [race-198 class-closure]: см. override_maps_scope_enter doc. let ovr_saved = self.override_maps_scope_enter(); let r = self.emit_bench_scoped_inner(b, idx); self.override_maps_scope_exit(ovr_saved, r.is_ok()); r } fn emit_bench_scoped_inner(&mut self, b: &BenchDecl, idx: usize) -> Result<(), String> { let safe = Self::mangle_test_name_indexed(&b.name, idx); let saved_out = std::mem::take(&mut self.out); let saved_indent = self.indent; let saved_var_types = self.var_types.clone(); let saved_var_mutable = self.var_mutable.clone(); let saved_cancel_token_t_map = self.cancel_token_t_map.clone(); let saved_protocol_vars = self.protocol_vars.clone(); // Plan 72 P0 let saved_result_type_params = self.result_type_params.clone(); // Plan 72 P1-C let saved_protocol_var_vtable = self.protocol_var_vtable.clone(); // Plan 72 P3-B // Подход: setup и teardown эмитятся как ТЕЛО функций, а measure — // отдельная функция. Setup mutates var_types — эти переменные // будут локальны для setup-функции, не видны в measure (это // ограничение MVP: TODO Phase B — поднимать setup-state в TLS // struct и передавать в measure). Для MVP реальные benches // выкручиваются через global mut consts или single inline-блок. // // Для MVP — упрощение: эмитим всё в **ОДНУ** функцию // nova_bench_main_<safe>(), которая делает: // setup_stmts; // for (sample) { for (iter) { measure; } record; } // teardown_stmts; // Это позволяет let-bindings из setup жить в одном scope с measure. // Sampling logic — embedded inline (вместо callback'ов в nova_bench_run). // // Plan 57 Phase A TODO: extract в callback'и с TLS state struct, // чтобы поддерживать sub-benchmarks (`group "..." { case "..." { } }`). self.indent = 0; self.line(&format!("{}nova_unit nova_bench_main_{}(void) {{", self.top_level_storage(), safe)); self.indent = 1; // Reset bench TLS state. self.line("memset(&_nova_bench_state, 0, sizeof(_nova_bench_state));"); // Knobs from env. self.line("uint64_t _b_warmup_ns = nova_bench_env_u64(\"NOVA_BENCH_WARMUP_NS\", NOVA_BENCH_DEFAULT_WARMUP_NS);"); self.line("uint64_t _b_target_ns = nova_bench_env_u64(\"NOVA_BENCH_TARGET_NS\", NOVA_BENCH_DEFAULT_TARGET_NS);"); self.line("uint64_t _b_n_samples = nova_bench_env_u64(\"NOVA_BENCH_SAMPLES\", NOVA_BENCH_DEFAULT_SAMPLES);"); self.line("uint64_t _b_time_budget = nova_bench_env_u64(\"NOVA_BENCH_TIME_BUDGET_NS\", NOVA_BENCH_DEFAULT_TIME_BUDGET_NS);"); self.line("if (_b_n_samples == 0) _b_n_samples = NOVA_BENCH_DEFAULT_SAMPLES;"); self.line("if (_b_n_samples > NOVA_BENCH_MAX_SAMPLES) _b_n_samples = NOVA_BENCH_MAX_SAMPLES;"); // BENCH_START marker. let escaped = Self::escape_c_str(&b.name); self.line("fputs(\"__BENCH_START__ \", stdout);"); self.line(&format!("nova_bench_print_json_str(\"{}\");", escaped)); self.line("fputc('\\n', stdout);"); self.line("fflush(stdout);"); // ── Setup ─────────────────────────────────────────────────── for stmt in &b.setup { self.emit_stmt(stmt)?; } // Plan 57.C.3: start heap sampler thread (no-op если env не set). self.line("nova_bench_heap_sampler_start();"); // Plan 57.C.4: open CPU instructions counter (Linux-only no-op // на других platforms; no-op если NOVA_BENCH_MEASURE_INSTRUCTIONS=0). self.line("nova_bench_instr_start();"); // ── Warmup ────────────────────────────────────────────────── self.line("{"); self.indent += 1; self.line("uint64_t _b_t_start = nova_bench_now_ns();"); self.line("do {"); self.indent += 1; // Inline measure body. for s in &b.measure_body.stmts { self.emit_stmt(s)?; } if let Some(t) = &b.measure_body.trailing { // trailing — expression, эмитим и discard'им result. let v = self.emit_expr(t)?; self.line(&format!("(void)({});", v)); } self.indent -= 1; self.line("} while (nova_bench_now_ns() - _b_t_start < _b_warmup_ns);"); self.indent -= 1; self.line("}"); // ── Calibration: single iter ────────────────────────────── self.line("_nova_bench_state.iters_per_sample = 1;"); self.line("uint64_t _b_calib_t0 = nova_bench_now_ns();"); self.line("{"); self.indent += 1; for s in &b.measure_body.stmts { self.emit_stmt(s)?; } if let Some(t) = &b.measure_body.trailing { let v = self.emit_expr(t)?; self.line(&format!("(void)({});", v)); } self.indent -= 1; self.line("}"); self.line("uint64_t _b_single_ns = nova_bench_now_ns() - _b_calib_t0;"); self.line("if (_b_single_ns == 0) _b_single_ns = 1;"); self.line("uint64_t _b_iters_per_sample = _b_target_ns / _b_single_ns;"); self.line("if (_b_iters_per_sample < 1) _b_iters_per_sample = 1;"); self.line("if (_b_iters_per_sample > 1000000) _b_iters_per_sample = 1000000;"); self.line("_nova_bench_state.iters_per_sample = _b_iters_per_sample;"); // ── Sample collection ───────────────────────────────────── self.line("uint64_t* _b_samples = (uint64_t*)malloc(sizeof(uint64_t) * (size_t)_b_n_samples);"); self.line("if (!_b_samples) { fprintf(stderr, \"nova bench: malloc failed\\n\"); return NOVA_UNIT; }"); self.line("uint64_t _b_collected = 0;"); self.line("int64_t _b_alloc_pre = nova_bench_alloc_count_snapshot();"); self.line("uint64_t _b_suite_start = nova_bench_now_ns();"); // Plan 57.C.4: per-sample CPU instructions counter. self.line("uint64_t* _b_instr = (uint64_t*)calloc((size_t)_b_n_samples, sizeof(uint64_t));"); self.line("if (!_b_instr) { fprintf(stderr, \"nova bench: malloc failed (instr)\\n\"); free(_b_samples); return NOVA_UNIT; }"); self.line("int _b_instr_active = nova_bench_instr_active();"); self.line("for (uint64_t _b_i = 0; _b_i < _b_n_samples; _b_i++) {"); self.indent += 1; self.line("if (_b_instr_active) nova_bench_instr_sample_reset();"); self.line("_nova_bench_state.timer_start_ns = nova_bench_now_ns();"); self.line("for (uint64_t _b_k = 0; _b_k < _b_iters_per_sample; _b_k++) {"); self.indent += 1; for s in &b.measure_body.stmts { self.emit_stmt(s)?; } if let Some(t) = &b.measure_body.trailing { let v = self.emit_expr(t)?; self.line(&format!("(void)({});", v)); } self.indent -= 1; self.line("}"); self.line("uint64_t _b_t_end = nova_bench_now_ns();"); self.line("if (_b_instr_active) _b_instr[_b_i] = nova_bench_instr_sample_read();"); self.line("uint64_t _b_elapsed = _b_t_end - _nova_bench_state.timer_start_ns;"); self.line("_b_samples[_b_i] = _b_elapsed / _b_iters_per_sample;"); self.line("_b_collected++;"); self.line("if (nova_bench_now_ns() - _b_suite_start > _b_time_budget) break;"); self.indent -= 1; self.line("}"); // Plan 57.C.4: close counter после sample loop. self.line("nova_bench_instr_stop();"); self.line("int64_t _b_alloc_post = nova_bench_alloc_count_snapshot();"); // Plan 57.C.3: stop heap sampler thread. self.line("nova_bench_heap_sampler_stop();"); // ── Teardown ──────────────────────────────────────────────── for stmt in &b.teardown { self.emit_stmt(stmt)?; } // ── Emit JSONL result ────────────────────────────────────── self.line("fputs(\"__BENCH_RESULT__ {\\\"name\\\":\", stdout);"); self.line(&format!("nova_bench_print_json_str(\"{}\");", escaped)); self.line("fprintf(stdout, \",\\\"iters_per_sample\\\":%llu\", (unsigned long long)_b_iters_per_sample);"); self.line("fprintf(stdout, \",\\\"samples_count\\\":%llu\", (unsigned long long)_b_collected);"); self.line("fputs(\",\\\"raw_ns\\\":[\", stdout);"); self.line("for (uint64_t _b_j = 0; _b_j < _b_collected; _b_j++) {"); self.indent += 1; self.line("if (_b_j > 0) fputc(',', stdout);"); self.line("fprintf(stdout, \"%llu\", (unsigned long long)_b_samples[_b_j]);"); self.indent -= 1; self.line("}"); self.line("fputc(']', stdout);"); self.line("if (_nova_bench_state.throughput_bytes) {"); self.indent += 1; self.line("fprintf(stdout, \",\\\"throughput_bytes\\\":%llu\", (unsigned long long)_nova_bench_state.throughput_bytes);"); self.indent -= 1; self.line("}"); self.line("if (_nova_bench_state.throughput_elements) {"); self.indent += 1; self.line("fprintf(stdout, \",\\\"throughput_elements\\\":%llu\", (unsigned long long)_nova_bench_state.throughput_elements);"); self.indent -= 1; self.line("}"); self.line("int64_t _b_total_iters = (int64_t)_b_iters_per_sample * (int64_t)_b_collected;"); self.line("if (_b_total_iters > 0) {"); self.indent += 1; self.line("int64_t _b_alloc_delta = _b_alloc_post - _b_alloc_pre;"); self.line("int64_t _b_per_iter = _b_alloc_delta / _b_total_iters;"); self.line("fprintf(stdout, \",\\\"allocs_per_iter\\\":%lld\", (long long)_b_per_iter);"); self.line("fprintf(stdout, \",\\\"allocs_total\\\":%lld\", (long long)_b_alloc_delta);"); self.indent -= 1; self.line("}"); // Plan 57.C.4: emit per-sample instructions array (only if counter active). self.line("if (_b_instr_active) {"); self.indent += 1; self.line("fputs(\",\\\"cpu_instructions\\\":[\", stdout);"); self.line("for (uint64_t _b_j = 0; _b_j < _b_collected; _b_j++) {"); self.indent += 1; self.line("if (_b_j > 0) fputc(',', stdout);"); // Per-iter instructions = total per batch / iters_per_sample. self.line("fprintf(stdout, \"%llu\", (unsigned long long)(_b_instr[_b_j] / _b_iters_per_sample));"); self.indent -= 1; self.line("}"); self.line("fputc(']', stdout);"); self.indent -= 1; self.line("}"); self.line("fputs(\"}\\n\", stdout);"); self.line("fflush(stdout);"); self.line("free(_b_samples);"); self.line("free(_b_instr);"); self.line("return NOVA_UNIT;"); self.indent = 0; self.line("}"); self.line(""); self.var_types = saved_var_types; self.var_mutable = saved_var_mutable; self.cancel_token_t_map = saved_cancel_token_t_map; self.protocol_vars = saved_protocol_vars; // Plan 72 P0 self.result_type_params = saved_result_type_params; // Plan 72 P1-C self.protocol_var_vtable = saved_protocol_var_vtable; // Plan 72 P3-B let bench_body = std::mem::replace(&mut self.out, saved_out); self.indent = saved_indent; if !self.lambda_forward_decls.is_empty() { self.out.push_str(&std::mem::take(&mut self.lambda_forward_decls)); } if !self.lambda_impls.is_empty() { self.out.push_str(&std::mem::take(&mut self.lambda_impls)); } self.out.push_str(&bench_body); Ok(()) } fn emit_test(&mut self, t: &TestDecl, idx: usize) -> Result<(), String> { // [race-198 class-closure]: см. override_maps_scope_enter doc. let ovr_saved = self.override_maps_scope_enter(); let r = self.emit_test_scoped_inner(t, idx); self.override_maps_scope_exit(ovr_saved, r.is_ok()); r } fn emit_test_scoped_inner(&mut self, t: &TestDecl, idx: usize) -> Result<(), String> { let safe = Self::mangle_test_name_indexed(&t.name, idx); // Buffer the test body so we can prepend any lambdas discovered during emit let saved_out = std::mem::take(&mut self.out); let saved_indent = self.indent; self.indent = 0; // Plan 54 Ф.1: snapshot var_types перед test body, restore после. // Без этого pattern-bound vars (`Some(v) => v`) и регулярные // let-bindings leak'ят между tests, что ломает match-arm inference // (например test 3 binds `v: bool`, test 6 binds `v: int` через // тот же match-pattern → infer_expr_c_type(`v`) возвращает // stale bool → match-result inferred bool → assert fails). // Same scope-cleanup для var_mutable + cancel_token_t_map. let saved_var_types = self.var_types.clone(); let saved_var_mutable = self.var_mutable.clone(); let saved_cancel_token_t_map = self.cancel_token_t_map.clone(); let saved_protocol_vars = self.protocol_vars.clone(); let saved_result_type_params_test = self.result_type_params.clone(); // Plan 72 P1-C let saved_protocol_var_vtable_test = self.protocol_var_vtable.clone(); // Plan 72 P3-B // Plan 153.2: each `test {}` block is emitted as its OWN C function, so // the closure mut-capture box registry must NOT leak between tests. Two // tests both naming a captured `mut calls` would otherwise share the // `_box_calls` entry: the box declaration is emitted in the first test's // function, but the second test reuses the cached `_box_calls` name — // referencing an identifier undeclared in ITS scope (CC-FAIL). `emit_fn` // flushes via `flush_boxed_vars`; `emit_test` must do the same, scoped to // the test body and restored afterwards. let saved_var_boxed = std::mem::take(&mut self.var_boxed); // Plan 184: ref-локалы (`ro/mut y ref T`) регистрируются в `ref_params` // (авто-деref). Как и `var_boxed`, набор ДОЛЖЕН быть scoped к телу теста // — иначе имя ref-локала (`a`, `r`) протечёт в последующие emit'ы (std- // методы с одноимённым обычным локалом → ложный `(*a)` → CC-FAIL). let saved_ref_params_test = std::mem::take(&mut self.ref_params); // Plan 170 (D307): set the emission file so a `test "…"` block that calls // a `priv(file)` helper declared in the SAME file resolves to its file- // discriminated C symbol (free_fn_c_name reads current_emit_file_id). let saved_emit_file_id = self.current_emit_file_id.replace(t.span.file_id); // Plan 172.14 (sret/_out §3): fn-id для эскейп-лукапа в тест-телах — // конвенция `test::<имя>` (зеркало escape_analyze::analyze_module, // обход Item::Test). Без этого armed-предикат стек-плейсмента в // emit_let никогда не проходит внутри test-блоков. let saved_test_fn_id = self.current_fn_id.replace(format!("test::{}", t.name)); self.line(&format!("{}nova_unit nova_test_{}(void) {{", self.top_level_storage(), safe)); self.indent = 1; self.emit_block_stmts(&t.body, "nova_unit")?; self.indent = 0; self.line("}"); self.line(""); self.current_fn_id = saved_test_fn_id; self.current_emit_file_id = saved_emit_file_id; // Restore scope-state — fixes leak (Plan 54 Ф.1). self.var_types = saved_var_types; self.var_mutable = saved_var_mutable; self.cancel_token_t_map = saved_cancel_token_t_map; self.protocol_vars = saved_protocol_vars; self.result_type_params = saved_result_type_params_test; // Plan 72 P1-C self.protocol_var_vtable = saved_protocol_var_vtable_test; // Plan 72 P3-B self.var_boxed = saved_var_boxed; // Plan 153.2: per-test box registry self.ref_params = saved_ref_params_test; // Plan 184: per-test ref-locals let test_body = std::mem::replace(&mut self.out, saved_out); self.indent = saved_indent; // Flush any lambdas discovered during this test's emit if !self.lambda_forward_decls.is_empty() { self.out.push_str(&std::mem::take(&mut self.lambda_forward_decls)); } if !self.lambda_impls.is_empty() { self.out.push_str(&std::mem::take(&mut self.lambda_impls)); } self.out.push_str(&test_body); Ok(()) } // ---- preamble ---- fn emit_preamble(&mut self) { // Plan 174.4: effect-registry compile-time size marker. MUST be line 1. // The build layer reads `nova-effect-count: N` from here and passes // `-DNOVA_MAX_EFFECT_STORAGES=N` to EVERY translation unit (this generated // .c AND the separately-listed runtime .c files — all compiled in one cc // invocation), so NovaEffectRegistry/NovaEffectSnapshot have an identical // array size across TUs (ABI safety — a per-.c `#define` would size the // generated TU differently from runtime effects.c and corrupt the TLS // registry). N = distinct effects (built-in Fail/Time + user) from the // effect_schemas registry (§0/§3, not hardcoded). Spliced in finalize; if // absent (hand-written .c), effects.h `#ifndef` falls back to 32 uniformly. self.line("/*__EFFECT_COUNT_MARKER__*/"); self.line("/* Generated by nova-codegen. Do not edit. */"); self.line("#include \"nova_rt/nova_rt.h\""); self.line(""); // Plan 148 Ф.4 [M-codegen-unify-tuple-repr]: the blanket `_NovaTuple1..8` // all-`nova_int` pre-declaration is RETIRED. Concrete tuples use the // typed mono'd path (`/*__MONO_TUPLE_TYPEDEFS__*/`, real element C-types). // The legacy all-int `_NovaTupleN` is now emitted ON DEMAND — only for // arities the erased-generic fallback genuinely requests (registered via // `register_legacy_tuple`). Splice marker filled in finalize, file scope // (avoids MSVC C2011 redefinition when used inside functions). self.line("/*__LEGACY_TUPLE_TYPEDEFS__*/"); // Plan 36 followup: forward decls user types ДО NovaOpt typedef'ов. // Иначе `NovaOpt_Nova_Range_p { Nova_Range* value; }` падает с // `unknown type name 'Nova_Range'` — типы декларируются после // марker'а в emit_type_decl, а NovaOpt splice'ится в marker. // Решение: pre-pass forward-decl всех user types через отдельный // marker, splice'ится в emit_module finalize. self.line("/*__USER_TYPE_FWD_DECLS__*/"); // Plan 61 Ф.1: TypeId defines splice marker — replaced в finalize // (`__TYPEID_DEFINES__` секция). Per-type `#define NOVA_TID_USER_<X> N` // эмитятся для каждого type, встреченного в throw / handler / any. self.line("/*__TYPEID_DEFINES__*/"); // Plan 61 followup #4: per-E Fail dispatch — splice marker. Замещён // в finalize per-E vtable + TLS slot + throw entry для each registered // E type (через `per_e_fail_types`). self.line("/*__PER_E_FAIL_DECLS__*/"); // Plan 91.12 fix: complete value-record struct definitions (NovaValue_X // bodies). Placed BEFORE /*__MONO_TUPLE_TYPEDEFS__*/ so that tuples // carrying value-records by value see a complete type (not just the // forward typedef). Spliced from value_record_defs_buf in finalize. self.line("/*__VALUE_RECORD_DEFS__*/"); // Plan 59: mono'd tuple struct typedefs — splice marker; replaced // в finalize. Layout: typedef struct { T1 f0; T2 f1; ... } // _NovaTuple____<T1>__<T2>__...;. Real types (e.g. nova_str, не // nova_int slot) — fit структуры >8 байт. Placed ПОСЛЕ user-type // fwd decls — tuple elements могут быть `Nova_X*` (pointer), которым // достаточно incomplete typedef. Value-record elements (NovaValue_X // by-value) require complete type — handled by __VALUE_RECORD_DEFS__. // // Plan 97.1 Ф.3 (D142): для tuple'ов содержащих `NovaBox_<Proto>` // (protocol-литералы capability-split factory) — NovaBox typedef'ы // эмитятся в `user_type_fwd_decls` (предшествующий marker), // **до** этого tuple marker — порядок safe. // // [M-tuple-fixarr-typedef-order] fix (2026-07-19): this marker now // ALSO carries the mono'd `[N]T` FixArr typedefs (see finalize splice // comment) — tuple and fixarr typedefs are topo-sorted together as // one DAG so `(T, [N]U)` (tuple-of-fixarr) and `[N](T,U)` // (fixarr-of-tuple) are both correctly ordered, at any nesting depth. self.line("/*__MONO_TUPLE_TYPEDEFS__*/"); // Plan 186 [bug-2 audit-197 fix]: extern prototypes for FREE external // fn returning a tuple (see `extern_fn_tuple_protos` field doc). // Placed AFTER the tuple typedefs marker above so the struct type a // prototype returns by value is always already complete. self.line("/*__EXTERN_FN_PROTOS__*/"); // [M-fixed-array-value-semantics] (2026-07-10, D27-амендмент) / retired by // [M-tuple-fixarr-typedef-order] fix (2026-07-19): `[N]T` INLINE-array struct // typedefs used to splice HERE, after the tuple marker — correct for // `[N](T1,T2)` (fixarr-of-tuple) but WRONG for `(T, [N]U)` (tuple-of-fixarr, // which needs the fixarr typedef complete BEFORE the tuple's) — a fixed // two-marker order can't satisfy both nesting directions at once. Fixarr // typedefs are now topo-sorted TOGETHER with tuple typedefs and spliced at // `/*__MONO_TUPLE_TYPEDEFS__*/` above (see comment there). This marker is // kept as a permanent no-op splice (always empty) so zero-fixarr compile-units // keep byte-identical preamble output. self.line("/*__MONO_FIXARR_TYPEDEFS__*/"); // Plan 14 Ф.1: маркер для splice'а typedef'ов NovaOpt_<T> (для T // без NOVA_ARRAY_DECL в runtime). Заполняется в `register_novaopt_decl` // в registration order (innermost first); splice'ится в финальный // `out` через `replace` после полного emit_module. self.line("/*__NOVAOPT_TYPEDEFS__*/"); // Plan 59 Ф.7.5: маркер для splice'а mono'd NovaRes_<ok>_<err> // typedef'ов. ПОСЛЕ NovaOpt-маркера — NovaRes-структуры ссылаются // на NovaOpt_<ok>/<err> (return-типы ok()/err()). self.line("/*__NOVARES_TYPEDEFS__*/"); // Plan 95 Ф.2.4: маркер для splice'а forward-deklarations // mono'd Option/Result-method'ов. **ПОСЛЕ** NovaOpt/NovaRes typedef // маркеров — сигнатура содержит `NovaOpt_<T>` by-value / `NovaRes_<n>*`, // им нужны complete-typedef и forward-typedef соответственно. // Standard `/*__MONO_FWD_DECLS__*/` маркер (file position X) идёт // ДО NovaOpt маркера (file position Y) — fwd-decl там → CC-fail // `incomplete type`. См. `builtin_sum_method_fwd_decls`. self.line("/*__BUILTIN_SUM_METHOD_FWD_DECLS__*/"); // Plan 139 Ф.6: interned string-literal statics splice marker. // Replaced in finalize with one shared `static const uint8_t[]` + // `static const nova_str` per distinct literal content. Placed at // file scope AFTER the nova_rt.h include (nova_str/uint8_t in scope). self.line("/*__INTERNED_STR_LITERALS__*/"); self.line(""); } // ---- const declarations ---- fn emit_const_decl(&mut self, c: &ConstDecl) -> Result<(), String> { let ty_c = if let Some(ty) = &c.ty { self.type_ref_to_c(ty)? // [M-const-decl-ty] declared type comes from AST TypeRef; checker doesn't write ConstDecl type to resolved_types channel yet } else { self.infer_expr_c_type(&c.value) }; // Plan 91.12 (D126 retract followup): для module-private consts // используем mangled C name из `private_const_c_names` (источник — // pre-pass в emit_module). Ключ — (file_id из span декларации, // source name). Exported consts → имя как есть, ЕСЛИ bare-имя не // коллидирует с export const'ом ДРУГОГО модуля (см. ниже). // // [fix M-samename-export-const-cross-module-c-symbol-collision, // реестр 221.1 №151]: a colliding `export const` gets THE SAME // `Nova_const_<modpath>_<name>` qualifier the non-export axis above // uses — `colliding_const_names`/`const_qualified_by_name` field docs // explain the reference-side resolution + its known ambiguous-dual- // import limit. Byte-identical when non-colliding. let c_name = self.private_const_c_names .get(&(c.span.file_id, c.name.clone())) .cloned() .unwrap_or_else(|| { if c.is_export && self.colliding_const_names.contains(&c.name) { if let Some(modpath) = self.emit_file_module.get(&c.span.file_id) { if !modpath.is_empty() { return format!("Nova_const_{}_{}", modpath.join("_"), c.name); } } } c.name.clone() }); if c.is_export && self.colliding_const_names.contains(&c.name) { self.const_qualified_by_name.insert(c.name.clone(), c_name.clone()); } // Emit as a static const variable (MSVC-safe, no VLAs or macros needed) // We emit the value as an expression; for string literals this needs // a compound literal initialiser which MSVC doesn't support at file scope. // For nova_str we use a special approach: emit a static initialiser macro. if ty_c == "nova_str" { // nova_str is {const char* ptr, size_t len} // MSVC doesn't support compound-literal initialisers at file scope in C. // Emit as a static struct with individual field initialisers. if let ExprKind::StrLit(s) = &c.value.kind { let escaped = Self::escape_c_str(s); let len = s.len(); self.line(&format!( "{}const nova_str {} = {{(const uint8_t*)\"{}\" , {}}};", self.top_level_storage(), c_name, escaped, len )); self.var_types.insert(c.name.clone(), ty_c.clone()); return Ok(()); } } // General case: emit as static const with initialiser expression // (covers int/bool/etc.). Передаём ty_c как target для integer- // литералов, чтобы emit правильный suffix/cast (например u32-const // получит `(uint32_t)NU`, не `((nova_int)NLL)` — последнее вызывает // implementation-defined signed→unsigned conversion для значений // вне диапазона int64, баг был замечен в std/checksums/fnv.nv). match self.emit_const_expr_typed(&c.value, Some(&ty_c)) { Ok(val) => { self.line(&format!("{}const {} {} = {};", self.top_level_storage(), ty_c, c_name, val)); // Регистрируем тип const'а в var_types, чтобы Ident(name) на // use-site инферился с правильным c-типом (например u32-const, // используемый как `let mut h = FOO`, должен дать `uint32_t h`, // а не nova_int — баг был замечен в std/checksums/fnv.nv). self.var_types.insert(c.name.clone(), ty_c.clone()); Ok(()) } Err(_) => { // Plan 14 Ф.2: non-constant initialiser (record-literal, // function call, и т.п.) — desugaring в lazy-init геттер. // [M-175-lazy-const-crossmodule-collision]: use the ALREADY- // computed `c_name` (module-qualified when non-exported — // see lookup above) as the wrapper qualifier, not the bare // `c.name` — this was the bug: `c_name` was computed and // then silently discarded on this path, so every lazy // (non-constexpr-initializer) const emitted the UNQUALIFIED // `_nova_const_<bare-name>_value` regardless of collision. self.emit_lazy_const(&c.name, &c_name, &ty_c, &c.value) } } } /// `[M-lazy-const-init-race]` (2026-07-09, absorbs Plan 14 Ф.2): эмит /// const'а/module-level `ro NAME = EXPR` с runtime-init. Storage-only /// здесь; the init BODY (gc_add_root + expr statements + final assign) is /// captured into `self.pending_const_inits` and combined at finalize /// (`emit_module`, right before `emit_main_wrapper`) into ONE /// topologically-sorted `nova_consts_init()` — called ONCE from the /// driver's main-path, before `nova_runtime_auto_arm()` spawns workers. /// /// Was (Plan 14 Ф.2, RETIRED — data race under M:N, see marker): a /// PER-CONST check-then-act lazy getter (`if (!_init) { ...; _init = 1; } /// return _value;`) called on every read. Non-atomic `_init` flag + no /// publication barrier for `_value` meant a second thread could observe /// `_init == 1` before the write to `_value` became visible (or two /// threads could race the init body itself) — for pointer-valued consts /// (`nova_str`/`Vec`/record) this is a crash-class bug, not just a /// redundant-init inefficiency. /// /// ```c /// static <Ty> _nova_const_<name>_value; // this fn emits just this /// // ... combined into ONE fn at finalize: /// static void nova_consts_init(void) { /// nova_gc_add_root(&_nova_const_<name>_value, ...); /// <emit_expr statements> /// _nova_const_<name>_value = <expr_val>; /// // ... every other lazy const, topo-sorted ... /// } /// ``` /// /// На use-site `Ident(name)` для lazy const'ов эмитим ГОЛОЕ /// `_nova_const_<name>_value` (не call — eager-init гарантирует, что оно /// уже populated до старта любого worker'а). /// /// [M-175-lazy-const-crossmodule-collision] (2026-07-22): `name` (bare /// SOURCE identifier) and `c_name` (the QUALIFIER substituted into the /// `_nova_const_<c_name>_value` wrapper below — NOT the final symbol by /// itself) are now DISTINCT parameters. `name` still drives every /// Nova-level registry keyed by source identifier (`lazy_consts`/ /// `var_types`/topo-sort dependency-matching via `pending_const_inits` /// — those operate on Nova-level identifiers, resolved unambiguously by /// the checker per-file, independent of any C-symbol collision). /// `c_name` is the module-qualified (or bare, if no collision risk / /// exported) name callers precompute (see `emit_const_decl`'s `c_name` /// lookup and the `Item::Let` call site's mirrored lookup; reference /// sites mirror the SAME lookup — see the `ExprKind::Ident` lazy-const /// branch), so two DIFFERENT lazy consts sharing a bare source name /// (e.g. a user's own private `ro ZERO` vs `Duration.ZERO`, both /// reachable in one CU since Plan 175 Ф.2-v3 made `std.time.duration` /// transitively pulled into every CU) no longer collide into one /// `_nova_const_ZERO_value` C global. pub(crate) fn emit_lazy_const(&mut self, name: &str, c_name: &str, ty_c: &str, value: &Expr) -> Result<(), String> { // Регистрируем имя как lazy — use-site Ident(name) станет голым // чтением `c_name`. self.lazy_consts.insert(name.to_string()); // Регистрируем тип, чтобы infer_expr_c_type(Ident(name)) возвращал // правильный c-тип (для записи в var_types — как обычный binding). self.var_types.insert(name.to_string(), ty_c.to_string()); // Эмитим storage (file-scope static; no `_init` flag anymore — the // combined `nova_consts_init()` runs it exactly once, eagerly). // [M-175-lazy-const-crossmodule-collision]: KEEP the `_nova_const_ // <X>_value` wrapper (not just a bare `c_name`) — the wrapper isn't // decorative, it's what keeps a const named after a C keyword // (`for`/`while`/…) or colliding with an unrelated bare C symbol // safe; `c_name` only substitutes the qualifier-or-bare-name PIECE // inside it (bare `name` in the overwhelmingly common non-colliding // case → byte-identical to pre-fix output; `Nova_const_<module>_ // <name>` when the pre-pass detected a cross-module bare-name clash). self.line(&format!("{}{} _nova_const_{}_value;", self.top_level_storage(), ty_c, c_name)); // Capture this const's init-BODY into its own buffer (same // side-statement-safety rationale as the old getter-body capture — // nested emits, e.g. a record-literal's helper statements, must not // interleave with file-scope declaration order). Combined with every // OTHER lazy const's body into ONE `nova_consts_init()` at finalize. let saved_out = std::mem::take(&mut self.out); let saved_indent = self.indent; self.indent = 1; // body nests one level inside `nova_consts_init() { ... }` // Plan 152.4: register the storage cell as a GC root BEFORE building. // The Boehm backend runs with GC_set_no_dls(1) (alloc_boehm.c), which // leaves the program's static/BSS data unscanned — so this file-scope // `static` pointer is NOT a GC root by default, and a large lazy value // (e.g. the Unicode HashMap tables) would be collected the first time // GC fires under memory pressure → use-after-free. Registering the // cell up front means a GC triggered mid-init sees it (still NULL — // harmless), and after assignment the object lives in a scanned root. // No-op under malloc/RC backends. self.line(&format!( "nova_gc_add_root(&_nova_const_{n}_value, (char*)(&_nova_const_{n}_value) + sizeof(_nova_const_{n}_value));", n = c_name )); // Передать ty_c как ожидаемый record-target для D55 coercion // (`const FOO = { ... }` без явного имени типа должен подхватить // тип из аннотации/typed-target). let saved_expected = self.expected_record_type.clone(); self.expected_record_type = Self::debt_struct_name_from_c_type(ty_c); // [M-d55-const-bytes-lit-not-constexpr] fix (2026-07-21): a module-level // `const c []u8 = "hi"` reaches HERE (lazy-init) because `[]u8` is a heap // Vec pointer, not C-constexpr (emit_const_expr's StrLit arm only builds a // `nova_str`-shaped struct literal, wrong C type for a bytes-slice target // — routed to Err → this lazy path by `emit_const_decl`'s caller). By this // point the CHECKER has already AST-rewritten the bare `"hi"` into // `"hi".bytes()` (D429 `#coerce` str→[]u8 view pair, `try_coerce_leaf`, // types/mod.rs) — value is `Call{Member{obj: StrLit(s), name: "bytes"}}`. // That rewrite mints a FRESH synthetic `ExprId` for the new nodes (not a // real UNSET, but never entered into `resolved_callees` — that map is // captured from the checker BEFORE this AST-mutating pass runs). The // general `emit_call` Member-dispatch's `resolved_callees` channel misses // it and falls to a slower `method_overloads[("str","bytes")]` lookup — // which ALSO misses here specifically because module-level consts are // emitted at pipeline stage "1b" (emit_module, right after types/forward- // decls), BEFORE the pass that registers std's `str @bytes()` (std/ // runtime/string/core.nv) into `method_overloads` runs. Reaching NO // matching entry, dispatch falls through to a much later, permissive // fallback and mis-resolves the bare method name "bytes" against an // UNRELATED registered symbol (`bench.bytes` → `nova_bench_set_throughput // _bytes`, Plan 57 bench DSL, external_registry.rs NAMESPACE_OVERRIDES) — // `Nova_bench_static_bytes` undefined-symbol link failure. Reordering // const-emission after method registration is out of scope here (global // pipeline reordering, wide blast radius on every other const in every // CU). Instead: recognize this ONE well-known, permanent shape directly // and emit the correct, already-proven-correct call ourselves — the SAME // `str @bytes() -> ro []u8` Nova-body method every OTHER call site of // `.bytes()` on a `str` receiver resolves to (`Nova_str_method_bytes`, // mangled `Nova_<Type>_method_<name>` — zero-copy view, `unsafe { []u8.new // (@ptr, @byte_len()) }`, std/runtime/string/core.nv) — bypassing the // method-registry timing gap entirely for this narrow, structurally- // recognizable case. Any OTHER coerce-rewritten shape (finalize lane, // consume-receiver, etc.) is NOT covered — falls through to the pre- // existing `emit_expr` dispatch unchanged (byte-identical outside this // one shape). let val = if let ExprKind::Call { func, args, .. } = &value.kind { if let ExprKind::Member { obj, name: m } = &func.kind { if m == "bytes" && args.is_empty() && matches!(obj.kind, ExprKind::StrLit(_)) && Self::is_bytes_slice_c_ty(ty_c) { let obj_c = self.emit_expr(obj)?; Some(format!("Nova_str_method_bytes({})", obj_c)) } else { None } } else { None } } else { None }; let val = match val { Some(v) => v, None => self.emit_expr(value)?, }; self.expected_record_type = saved_expected; self.line(&format!("_nova_const_{}_value = {};", c_name, val)); let body = std::mem::replace(&mut self.out, saved_out); self.indent = saved_indent; // Dependency scan (best-effort — see `collect_free_idents`'s // coverage note) for the topo-sort at finalize: any free identifier // that turns out to ALSO be a lazy-const name becomes a "must-init- // before-me" edge. Over-approximates (collects ALL free idents, not // just other consts) — filtered down against the final `lazy_consts` // set once every module has been processed. let mut free_idents = HashSet::new(); Self::collect_free_idents(value, &mut free_idents); self.pending_const_inits.push((name.to_string(), body, free_idents.into_iter().collect())); Ok(()) } /// `[M-lazy-const-init-race]`: Kahn's-algorithm topo-sort of the /// collected lazy-const init bodies by inter-const dependency (a const /// referencing another lazy const in its initializer must be emitted /// AFTER the one it depends on). Declaration-order stable for /// independent entries (queue seeded in original order). A dependency /// cycle (shouldn't occur for well-formed const initializers — Nova has /// no way to construct one without a self/mutually-referential `const`, /// itself likely a separate checker error) falls back to appending the /// unresolved remainder in declaration order rather than silently /// dropping them. fn topo_sort_const_inits(entries: &[(String, String, Vec<String>)]) -> Vec<usize> { let n = entries.len(); let index_of: HashMap<&str, usize> = entries.iter().enumerate() .map(|(i, (name, _, _))| (name.as_str(), i)) .collect(); let mut indeg = vec![0usize; n]; let mut adj: Vec<Vec<usize>> = vec![Vec::new(); n]; for (i, (name, _, deps)) in entries.iter().enumerate() { for d in deps { if d == name { continue; } // self-reference guard (no-op edge) if let Some(&dep_idx) = index_of.get(d.as_str()) { adj[dep_idx].push(i); indeg[i] += 1; } } } let mut queue: std::collections::VecDeque<usize> = (0..n).filter(|&i| indeg[i] == 0).collect(); let mut order = Vec::with_capacity(n); while let Some(i) = queue.pop_front() { order.push(i); for &j in &adj[i] { indeg[j] -= 1; if indeg[j] == 0 { queue.push_back(j); } } } if order.len() != n { let seen: HashSet<usize> = order.iter().cloned().collect(); for i in 0..n { if !seen.contains(&i) { order.push(i); } } } order } /// `[M-lazy-const-init-race]`: assemble every collected lazy-const init /// body (`self.pending_const_inits`) into ONE `static void /// nova_consts_init(void) { ... }` function, topologically sorted. /// Always emitted (possibly empty-bodied when there are no lazy consts — /// keeps `emit_main_wrapper`'s call site unconditional, no extra state /// threading). Called once from `emit_module` at finalize, just before /// `emit_main_wrapper` — so the function TEXT appears before `main()`. fn render_consts_init_fn(&self) -> String { let order = Self::topo_sort_const_inits(&self.pending_const_inits); let mut out = String::new(); out.push_str(&format!("{}void nova_consts_init(void) {{\n", self.top_level_storage())); for i in order { out.push_str(&self.pending_const_inits[i].1); } out.push_str("}\n\n"); out } /// Эмит integer-литерала с правильным C-типом (suffix + cast). /// /// Для unsigned-целевых типов важно эмитить unsigned-литерал /// (`U` / `ULL` suffix), иначе signed `(nova_int)<N>LL` cast в /// беззнаковый — implementation-defined для значений вне диапазона /// int64 (например 0xCBF29CE484222325 как FNV-64 offset). fn emit_typed_int_literal(n: i64, ty_c: &str) -> String { match ty_c { // Plan 70.4 Ф.4: nova_byte = typedef uint8_t — same U-suffix treatment. // Plan 152.8: nova_char = typedef uint32_t — also unsigned. "nova_char" | "nova_byte" | "uint8_t" | "uint16_t" | "uint32_t" => { // Unsigned 32-bit и меньше: U-suffix + cast к точному типу. // n хранится как i64; для отрицательных значений или > i32::MAX // используем явное приведение через хеш-bit-pattern. format!("(({}){}U)", ty_c, n as u32) } // Plan 172.1-K4: nova_uint (= uintptr_t, D130) — то же беззнаковое 64-бит // обращение, что uint64_t (bit-pattern u64 + ULL), иначе uint-литерал падал // в знаковый default-arm. "uint64_t" | "nova_uint" => { // ULL-suffix; используем bit-pattern u64 чтобы корректно // передать значения, не помещающиеся в i64 (FNV-64 и т.п.). format!("(({})0x{:X}ULL)", ty_c, n as u64) } "int8_t" | "int16_t" | "int32_t" => { format!("(({}){})", ty_c, n as i32) } // nova_int (= int64_t), int64_t — default LL-suffix. _ => format!("(({}){}LL)", ty_c, n), } } /// Emit a constant expression — like emit_expr but without side-effect statements. /// Used for file-scope const initialisers. /// /// `target_ty_c` (если задан) — c-тип целевого const'а. Для integer-литералов /// используется чтобы выбрать правильный suffix/cast (unsigned vs signed). fn emit_const_expr_typed(&mut self, expr: &Expr, target_ty_c: Option<&str>) -> Result<String, String> { match &expr.kind { ExprKind::IntLit(n) => { let ty_c = target_ty_c.unwrap_or_else(|| panic!("[P67] IntLit without target type context — checker must annotate")); Ok(Self::emit_typed_int_literal(*n, ty_c)) } ExprKind::CharLit(cp) => { let ty_c = target_ty_c.unwrap_or_else(|| panic!("[P67] CharLit without target type context — checker must annotate")); Ok(Self::emit_typed_int_literal(*cp as i64, ty_c)) } ExprKind::Unary { op: UnOp::Neg, operand } => { // `-IntLit` → typed-literal с минусом. ВАЖНО: для unsigned-типа // негативный литерал в const'е концептуально некорректен (заворот), // оставляем как есть (программист сам отвечает). if let ExprKind::IntLit(n) = &operand.kind { let ty_c = target_ty_c.unwrap_or_else(|| panic!("[P67] Unary-IntLit without target type context — checker must annotate")); return Ok(Self::emit_typed_int_literal(-*n, ty_c)); } let inner = self.emit_const_expr_typed(operand, target_ty_c)?; Ok(format!("(-({}))", inner)) } // Plan 172.1 [M-172.1-const-binary-typed]: propagate the const's DECLARED target type // to both operands of an ARITH/bitwise binary so `const C uint = 0x80 >> 1` keeps // UNSIGNED operands (logical shift / unsigned divide) instead of collapsing to signed // nova_int (int-collapse, D412). Only arith/bitwise ops propagate (comparison/logic // operands are NOT the const's type); those + absent target → byte-identical legacy. ExprKind::Binary { op, left, right } if target_ty_c.is_some() => { use crate::ast::BinOp; let op_str = match op { BinOp::Add => "+", BinOp::Sub => "-", BinOp::Mul => "*", BinOp::Div => "/", BinOp::Mod => "%", BinOp::BitAnd => "&", BinOp::BitOr => "|", BinOp::BitXor => "^", BinOp::Shl => "<<", BinOp::Shr => ">>", // comparison / logic / contract ops: operands are not the const's type — // delegate to the untyped const-binary path (byte-identical). _ => return self.emit_const_expr(expr), }; let l = self.emit_const_expr_typed(left, target_ty_c)?; let r = self.emit_const_expr_typed(right, target_ty_c)?; Ok(format!("(({}) {} ({}))", l, op_str, r)) } // [M-d200-assoc-const-composite-value]: record-литерал в const RHS — // top-level struct-инициализатор (.rodata), не runtime alloc/statements // (unlike `emit_record_lit`, used by module-level `ro`/lazy-init и // ordinary expression position). `inferred_map_v: None` guard mirrors // the runtime RecordLit arm — a D55 map-coercion literal never reaches // here (desugared to a HashMap-builder call long before const codegen, // which would fail check_const_constexpr_ex upstream anyway). ExprKind::RecordLit { type_name, fields, inferred_map_v: None, .. } => { self.emit_const_record_lit(type_name.as_deref(), fields, target_ty_c) } _ => self.emit_const_expr(expr), } } /// [M-d200-assoc-const-composite-value] (2026-07-23): D200 finalisation — /// composite (record-literal) assoc-const value. Builds a plain C /// aggregate initialiser `{ .field = <value>, ... }` for a `static const /// T Type_NAME = ...;` (or module-level `const NAME T = ...;`) — a real /// compile-time `.rodata` constant, NOT the runtime `emit_record_lit` /// alloc+statements path. Recurses into nested record-literal fields /// (each leaf must bottom out scalar-constexpr through /// `emit_const_expr_typed`); a field naming ANOTHER assoc/module const /// (`Ident`) is handled transparently by that same recursive call /// (its `Ident` arm already resolves other top-level consts). /// /// **Honest scope cut:** `str` fields are REJECTED with a clear /// diagnostic rather than mis-emitted — `nova_str` at file scope needs /// its OWN addressable `.rodata` byte buffer (`{(const uint8_t*)"...", /// len}` — see `emit_const_decl`'s dedicated top-level `nova_str` arm), /// which this generic struct-field path does not build. Only /// scalars + nested records-of-scalars are supported (task scope, /// backlog `[M-d200-assoc-const-composite-value]`). fn emit_const_record_lit( &mut self, type_name: Option<&[String]>, fields: &[crate::ast::RecordLitField], target_ty_c: Option<&str>, ) -> Result<String, String> { // Which record's field-schema governs this literal: an explicit // `Type { .. }` name wins (mirrors `emit_record_lit`'s own // precedence); otherwise fall back to the contextual target C-type // (the const's own type annotation, or — on a recursive call — the // enclosing field's declared C-type). let struct_name = if let Some(p) = type_name { p.last().cloned() } else { target_ty_c.and_then(Self::debt_struct_name_from_c_type) }.ok_or_else(|| { "record literal `{ .. }` in const initialiser needs a resolvable \ type — annotate the const (`const NAME Type = { .. }`) or the \ enclosing field's declared type \ [M-d200-assoc-const-composite-value]".to_string() })?; let schema = self.record_schemas.get(&struct_name).cloned().ok_or_else(|| { format!( "cannot resolve field types for `{}` in const initialiser \ (record schema not registered) [M-d200-assoc-const-composite-value]", struct_name ) })?; let mut parts: Vec<String> = Vec::new(); for f in fields { if f.is_spread { return Err( "[E_CONST_NOT_CONSTEXPR] spread `...` not allowed в const \ record-literal initialiser (D200)".to_string(), ); } let field_ty_c = schema.get(&f.name).cloned().ok_or_else(|| { format!( "unknown field `{}` on type `{}` in const record-literal initialiser", f.name, struct_name ) })?; let val_expr = f.value.as_ref().ok_or_else(|| { format!( "field shorthand `{{ {} }}` not supported in const record-literal \ initialiser — use explicit `{}: <value>` (D200)", f.name, f.name ) })?; // Honest cut (task scope): str fields need a separately-addressed // `.rodata` byte buffer this path doesn't build — reject with a // clear diagnostic instead of mis-emitting. if field_ty_c == "nova_str" || matches!(&val_expr.kind, ExprKind::StrLit(_) | ExprKind::InterpolatedStr { .. }) { return Err(format!( "str fields in assoc const not yet supported (field `{}`) — \ only scalar and nested-record-of-scalars fields are supported \ [M-d200-assoc-const-composite-value]", f.name )); } let mangled = Self::mangle_field_name(&f.name); let val_c = self.emit_const_expr_typed(val_expr, Some(&field_ty_c))?; parts.push(format!(".{} = {}", mangled, val_c)); } Ok(format!("{{ {} }}", parts.join(", "))) } fn emit_const_expr(&mut self, expr: &Expr) -> Result<String, String> { match &expr.kind { ExprKind::IntLit(n) => Ok(format!("((nova_int){}LL)", n)), ExprKind::CharLit(cp) => Ok(format!("((nova_int){}LL)", cp)), ExprKind::BoolLit(b) => Ok(if *b { "1".into() } else { "0".into() }), ExprKind::StrLit(s) => { let len = s.len(); Ok(format!("{{.ptr=\"{}\", .len={}}}", Self::escape_c_str(s), len)) } ExprKind::InterpolatedStr { .. } => { // String interpolation в const-инициализаторе требует runtime- // вычислений (StringBuilder.append + into) — не constexpr. Err("string interpolation `${...}` is not allowed in const initialiser \ (use a plain string literal or a runtime expression)".to_string()) } ExprKind::FloatLit(f) => { // То же что в emit_expr — гарантируем что C видит double-литерал, // не integer (избегает overflow на 1e20 → "100000000000000000000"). let s = if f.is_finite() && (f.abs() >= 1e16 || (f.abs() != 0.0 && f.abs() < 1e-4)) { format!("{:e}", f) } else { let raw = f.to_string(); if raw.contains('.') || raw.contains('e') || raw.contains('E') { raw } else { format!("{}.0", raw) } }; Ok(s) } ExprKind::Unary { op, operand } => { let inner = self.emit_const_expr(operand)?; let op_str = match op { UnOp::Neg => "-", UnOp::Not => "!", // Plan 234 Ф.2: `~` в const-выражении — integer-only, голый // C `~` (constexpr-литерал, ширино-таблица неприменима). UnOp::BitNot => "~", // Plan 118 D216 §4-5: pointer ops not valid в const // expressions (constexpr evaluation has no addressable // storage / runtime pointer values). UnOp::AddrOf | UnOp::RawAddrOf | UnOp::Deref => { return Err("[E_PTR_OP_IN_CONST] pointer operators \ (&/*) are not valid в const expression context" .to_string()); } }; Ok(format!("({}({}))", op_str, inner)) } // Plan 114.4 Ф.1: arithmetic/bitwise/comparison над constexpr // operands — допустимо в const RHS. C compiler constant-fold'ит. ExprKind::Binary { op, left, right } => { let l = self.emit_const_expr(left)?; let r = self.emit_const_expr(right)?; let op_str = match op { crate::ast::BinOp::Add => "+", crate::ast::BinOp::Sub => "-", crate::ast::BinOp::Mul => "*", crate::ast::BinOp::Div => "/", crate::ast::BinOp::Mod => "%", crate::ast::BinOp::Eq => "==", crate::ast::BinOp::Neq => "!=", crate::ast::BinOp::Lt => "<", crate::ast::BinOp::Le => "<=", crate::ast::BinOp::Gt => ">", crate::ast::BinOp::Ge => ">=", crate::ast::BinOp::And => "&&", crate::ast::BinOp::Or => "||", crate::ast::BinOp::BitAnd => "&", crate::ast::BinOp::BitOr => "|", crate::ast::BinOp::BitXor => "^", crate::ast::BinOp::Shl => "<<", crate::ast::BinOp::Shr => ">>", // Contract-only ops (D24) — not allowed в const initialiser. crate::ast::BinOp::Implies | crate::ast::BinOp::Iff => { return Err(format!( "contract-only operator {:?} not allowed в const initialiser", op )); } }; Ok(format!("(({}) {} ({}))", l, op_str, r)) } // Reference на another top-level const — emit its C identifier. // A module-private const is mangled to `Nova_const_<modpath>_<NAME>` // (see emit_module pre-pass); the bare source name is NOT a valid C // identifier at that point. Resolve the mangled name via // `private_const_c_names` keyed by this expression's file_id (peers // in the same module-group all carry the group's consts). Fall back // to the bare name for exported consts (emitted under their own name). ExprKind::Ident(name) => { let mangled = self.private_const_c_names .get(&(expr.span.file_id, name.clone())) .cloned() .unwrap_or_else(|| name.clone()); Ok(mangled) } // [M-d200-assoc-const-composite-value]: constructor-call RHS // (`= StatusCode.mk(200)`) is NOT extended to constexpr — a call // is a runtime dispatch regardless of purity; D200 composite // support covers ONLY record-LITERAL values (see // `emit_const_record_lit`). Explicit `E_CONST_NOT_CONSTEXPR` // wording matches the module-level const checker's vocabulary // (`types/mod.rs::check_const_constexpr_ex`, which does not walk // assoc-const RHS today — this codegen-level fallback is the // only enforcement point for assoc consts). ExprKind::Call { .. } => Err( "[E_CONST_NOT_CONSTEXPR] constructor/function call not allowed \ в const initialiser — only literals, arithmetic, record \ literals из constexpr fields, и references к other consts \ are allowed (D200).".to_string() ), _ => Err(format!("non-constant expression in const declaration: {:?}", expr.kind)), } } // ---- type mapping ---- /// Plan 70 Ф.1: Helper для формирования strict-error в местах где /// раньше был silent `unwrap_or "nova_int"` fallback. /// /// `context`: краткое описание точки — `"parameter `x`"`, /// `"field type"`, `"call argument 3"`, etc. /// `cause`: оригинальная ошибка от `type_ref_to_c` или /// inference helper. /// /// Returns formatted `Err(String)` ready для return через `?` или /// прямого Err propagation. /// /// Diagnostic code: E7001 (range E7001-E7099 reserved для Plan 70 /// strict type propagation errors). fn err_no_int_fallback(&self, context: &str, cause: &str) -> String { format!( "[E7001] cannot infer C type for {}: {}. \ Silent fallback к `nova_int` produced wrong runtime output \ для non-int types (record/string/float/bool). Add explicit \ type annotation, ensure generic is monomorphized, или \ register type в external_registry. \ См. Plan 70 ([M-no-silent-nova-int-fallback]).", context, cause ) } /// Plan 70 Ф.B0 (session 2): strict-error helper для cascade-blocked sites /// (functions whose signature can't be changed без массивного caller-chain /// refactor — `infer_expr_c_type` (135 callers), `register_mono_instance`, /// `register_mono_method_instance`, `infer_mono_method_ret_with_args` etc). /// /// Pushes `[E7001]` error в `strict_errors` field, returns `"nova_int"` /// placeholder. Codegen pass продолжает чтобы собрать **все** strict /// errors в одном run (better UX чем fail-fast); `emit_module` finalization /// проверяет `strict_errors.is_empty()` и возвращает aggregated `Err` /// если non-empty — `.c` файл не пишется, build fails. /// /// Semantic effect: equivalent к `?` propagation up the call chain, без /// signature changes. Production-grade strict mode: ANY silent fallback = /// build failure (default, no opt-out env var). /// /// Replaces session 1's `warn_silent_int_fallback` (W7001 deferred-warning) — /// все её callers мигрированы на этот helper в PhaseB1. fn record_strict_error(&self, context: &str, cause: &str) -> String { self.strict_errors.borrow_mut().push(format!( "[E7001] cannot infer C type for {}: {}. \ Silent fallback к `nova_int` would produce wrong runtime output \ для non-int types (record/string/float/bool). Add explicit \ type annotation, ensure generic is monomorphized, или \ register type в external_registry. \ См. Plan 70 ([M-no-silent-nova-int-fallback]).", context, cause )); "nova_int".to_string() } /// Plan 72 P0 (E7201): if `ty` is a protocol type (or a generic protocol /// instantiation like `Iter[int]`), return the base protocol name. /// Returns `None` for non-protocol types. fn extract_protocol_type_name(&self, ty: &TypeRef) -> Option<String> { if let TypeRef::Named { path, .. } = ty { let name = path.last()?; // Plan 152.7.1 (D374 AMEND): `Write` is special-cased in // `type_ref_to_c` to map to `Nova_StringBuilder*` (a concrete C // type), so it must NOT be treated as an erased protocol variable // — doing so would cause E7201 on every `w.write(s)` call. // Plan 208 Ф.2 (D422, was D419): `Fmt` mirrors the same erasure // (→ `Nova_FmtCtx*`) — same exclusion, same reason (`f.write(s)`/ // `f.alternate()`/`f.precision()` calls inside a user // `@display(mut f Fmt)`/`@debug(mut f Fmt)` body). if name == "Write" || name == "Fmt" { return None; } if self.protocol_types.contains(name) { return Some(name.clone()); } } None } /// Plan 72 P1-C: if `ty` is `Result[T, E]`, return (C type of T, C type of E). /// Returns `None` for non-Result types. /// /// Plan 196.3 (result-repr triple) verdict: LEGIT-LOWERING, out of /// channel scope. All three call sites feed it a literal `TypeRef` — /// `f.return_type` (declared fn/method return-type AST, `emit_fn` /// registry population for the legacy `fn_result_type_params` fallback, /// :13211), `p.ty` (declared param type, :22413), and `decl.ty` (an /// explicit `let r: Result[T,E] = ...` annotation, :24643) — never an /// inferred call-expression result. There's no checker call to resolve /// against `resolved_types` here: the annotation IS the concrete type, /// written verbatim in source, so `type_ref_to_c` on it is already the /// correct/only path (mirrors the `infer_handler_interrupt_ty` / /// `infer_func_c_name` LEGIT-LOWERING verdicts in the same audit — a /// declared-syntax reader, not a re-derivation of resolved-type info). fn extract_result_type_params(&self, ty: &TypeRef) -> Option<(String, String)> { if let TypeRef::Named { path, generics, .. } = ty { let name = path.last()?; if name == "Result" && generics.len() == 2 { let ok_c = self.type_ref_to_c(&generics[0]).ok()?; let err_c = self.type_ref_to_c(&generics[1]).ok()?; return Some((ok_c, err_c)); } } None } /// Plan 72 P3-B: if `ty` is a protocol type — generic instantiation (e.g. /// `Iter[int]`) OR a plain non-generic protocol (e.g. `Greeter`) — return /// `(proto_name, [concrete C type args])` (empty Vec for non-generic). /// Used for both protocol return types and protocol parameter types — /// they lower to the `NovaBox_*` fat pointer. Returns `None` for /// non-protocol types. /// /// [M-protocol-box-callarg-vtable-incomplete] (Plan 221 A-B4): before this /// fix, a non-empty `generics` check gated this whole fn — a NAMED /// non-generic protocol type (`fn f(g Greeter)`, no `[T]`) returned `None` /// here, so it was invisible to BOTH the call-argument pre-box hook /// (`emit_call`'s `fn_protocol_params` lookup) and the return-value /// pre-box hook (`wrap_protocol_return`'s `protocol_box_return_type_info`) /// — `type_ref_to_c` (the DECLARED-type lowering, used for the fn /// signature itself) already lowers a non-generic protocol to /// `NovaBox_<Name>` unconditionally (no gate), so the signature demanded /// a box while call/return sites kept passing/returning the bare /// concrete pointer — CC-FAIL (`passing/returning 'Nova_X *' … incompatible /// … 'NovaBox_<Proto>'`). Non-generic protocols were already known-boxable /// elsewhere (`emit_protocol_vtable_companion`'s own comment: "non-generic /// protocols → empty type_args OK"; `emit_protocol_box_typedef` already /// handles empty `type_args` for the pre-non-generic-protocol-typedef /// pass) — this fn simply never surfaced that case to its callers. fn protocol_type_args(&self, ty: &TypeRef) -> Option<(String, Vec<String>)> { let proto_name = self.extract_protocol_type_name(ty)?; let TypeRef::Named { generics, .. } = ty else { return None; }; if generics.is_empty() { return Some((proto_name, Vec::new())); } let type_args: Vec<String> = generics.iter() .filter_map(|g| self.type_ref_to_c(g).ok()) .collect(); if type_args.len() != generics.len() { return None; } Some((proto_name, type_args)) } /// Plan 72 P3-B return: if `f`'s declared return type is a generic protocol /// (e.g. `-> Iter[int]`), return `(proto_name, [concrete C type args])`. fn protocol_box_return_type_info(&self, f: &FnDecl) -> Option<(String, Vec<String>)> { self.protocol_type_args(f.return_type.as_ref()?) } /// Plan 72 P3-B: if `ty` is a generic protocol, return /// `(box_c_type, proto_name, type_args)` where `box_c_type` is the /// `NovaBox_<proto>_<args>` fat-pointer type name. Pure string derivation /// (`&self`) — the typedef itself is emitted by `emit_protocol_box_typedef`. fn protocol_box_c_type_for(&self, ty: &TypeRef) -> Option<(String, String, Vec<String>)> { let (proto, type_args) = self.protocol_type_args(ty)?; // Mirror `emit_protocol_box_typedef`'s / `type_ref_to_c`'s non-generic // mangling: no args-suffix (and no trailing `_`) when `type_args` is // empty — a NAMED non-generic protocol's C type is bare `NovaBox_<Proto>`. let box_ty = if type_args.is_empty() { format!("NovaBox_{}", proto) } else { let args_mangled: String = type_args.iter() .map(|t| Self::sanitize_c_for_ident(t)) .collect::<Vec<_>>() .join("_"); format!("NovaBox_{}_{}", proto, args_mangled) }; Some((box_ty, proto, type_args)) } /// Plan 72 P1-C: emit a C cast expression that reinterprets a `nova_int` /// storage slot as `target_ty`. For pointer types uses intptr_t relay; /// for nova_f64 uses a union bit-cast; for other scalars a direct cast. /// /// Plan 82 followup примечание (2026-05-23): для struct-target'ов /// `(StructTy)(value)` — GCC/Clang extension, под MSVC = C2440. /// Однако blanket-guard «struct → no cast» здесь регрессирует /// clang-тесты (enum/nova_unit/etc. ловятся как struct по эвристике /// is_struct_c_type, а C-cast нужен). Возможно ВСЕ struct-target /// callsite'ы — codegen-bug-via-extension, но fixed targeted-fixes /// на конкретных сайтах (tuple-field, array_push) безопаснее /// blanket-guard'а. Здесь оставляем `(T)(v)` как было. /// Plan 221.1 №286/№143 (окно p-chan): the checker's `resolved_types` /// channel now carries `Option[T]` for a `.recv()`/`.try_recv()` call /// whose channel declares a CONCRETE `T` (turbofish `Channel[T].new` or /// a `ChanReader[T]`/`ChanWriter[T]`-annotated param/local — /// `channel_elem_type`, types/mod.rs). Returns the C type of `T` when /// it is known AND genuinely non-`nova_int` (an explicit `Channel[int]` /// needs no reinterpret — the runtime's raw slot already IS the right /// shape). `None` — untracked channel (bare `Channel.new`, same /// permissive legacy path) or `T` is literally `int` — the caller falls /// back to the untouched byte-identical pre-window emission. fn channel_recv_target_c(&self, call_id: crate::ast::ExprId) -> Option<String> { if !call_id.is_set() { return None; } let rt = self.resolved_types.get(&call_id)?; let crate::types::ResolvedType::Named { name, args, .. } = rt else { return None }; if name != "Option" || args.len() != 1 { return None; } let c = self.resolved_type_to_c(&args[0]).ok()?; if c.is_empty() || c == "nova_int" { return None; } Some(c) } /// Plan 221.1 №286/№143 (окно p-chan): builds the ternary that /// reinterprets a raw `NovaOpt_nova_int`-shaped `{some_cond, raw_value}` /// pair (`some_cond` — a bool C expr, true on `Some`; `raw_value` — the /// `nova_int` slot) into the CONCRETE `opt_c` (`NovaOpt_<target_ty>`) /// shape `resolved_type_to_c` promised the checker. Two DIFFERENT /// `NovaOpt_*` layouts exist (`register_novaopt_decl`'s own NPO /// decision) and must be built differently — a pointer-sized `T` gets /// the null-pointer-optimized single-field `{ value }` struct (NO /// `.tag` member at all: `field designator 'tag' does not refer to any /// field`, measured CC-FAIL this fn fixes), while every scalar `T` /// (`nova_bool`, `nova_char`, sized ints, …) keeps the explicit /// `{ tag, value }` pair `NovaOpt_nova_int` itself already uses. fn channel_reinterpret_novaopt( some_cond: &str, raw_value: &str, opt_c: &str, target_ty: &str, ) -> String { let some_val = Self::cast_from_nova_int(raw_value, target_ty); let none_val = Self::cast_from_nova_int("0", target_ty); if target_ty.ends_with('*') { format!( "({some_cond} ? ({opt_c}){{.value={some_val}}} : ({opt_c}){{.value={none_val}}})" ) } else { format!( "({some_cond} ? ({opt_c}){{.tag=NOVA_TAG_Option_Some,.value={some_val}}} : \ ({opt_c}){{.tag=NOVA_TAG_Option_None,.value={none_val}}})" ) } } fn cast_from_nova_int(value: &str, target_ty: &str) -> String { if target_ty == "nova_int" { return value.to_string(); } if target_ty == "nova_f64" { // Plan 145 — portable bit-reinterpret (MSVC C2059): nova_bits_i2f // (array.h) вместо GNU statement-expression union-pun. return format!("nova_bits_i2f({})", value); } if target_ty.ends_with('*') { return format!("({})(intptr_t)({})", target_ty, value); } format!("({})({})", target_ty, value) } /// Plan 82 followup (MSVC compat): true для C-типов, которые в /// emit-выводе представлены `struct`-формой (а не scalar typedef). /// Скаляры белый список: nova_int/bool/char/f32/f64 + intN_t/uintN_t /// + `void`/`size_t`/`intptr_t`. Всё прочее (nova_str, _NovaTuple_*, /// Nova_*-records, NovaBox_*-protocol boxes, NovaOpt_*-monomorphs, /// NovaArr_*/NovaTbl_* и т.п.) — структуры; C-cast `(StructTy)(e)` — /// GCC/Clang extension, под MSVC cl.exe → C2440. Используется в /// `cast_from_nova_int` для подавления struct-cast эмиссии. fn is_struct_c_type(ty: &str) -> bool { match ty { // Скаляры из nova_rt.h (typedef'ы примитивов). "nova_int" | "nova_bool" | "nova_char" | "nova_f32" | "nova_f64" | "int8_t" | "int16_t" | "int32_t" | "int64_t" | "uint8_t" | "uint16_t" | "uint32_t" | "uint64_t" | "intptr_t" | "uintptr_t" | "size_t" | "ssize_t" | "ptrdiff_t" | "void" | "char" | "int" | "long" | "long long" | "short" | "unsigned" | "_Bool" | "bool" => false, _ => true, } } /// [M-channel-generic-elem-type] (a, honest-CC-FAIL fallback): can a /// value of C type `ty` be losslessly round-tripped through the /// channel runtime's single-word `nova_int` storage slot (`send_val`/ /// `recv_val`, `nova_rt/channels.h`)? The RUNTIME is not generic — every /// element is still stored as a bare `nova_int`-sized slot regardless of /// `T` (this gate is unchanged by Plan 221.1 №286/№143's window). What /// DID change (окно p-chan): `T` IS now tracked end-to-end by the /// CHECKER for a turbofish-declared (`Channel[T].new`) or param- /// annotated (`ChanReader[T]`/`ChanWriter[T]`) channel — a real per-`T` /// type mismatch (`Channel[int].send("s")`, or same-C-size-different- /// Nova-type like `Meters`/`Seconds`) is now caught EARLIER, as an /// honest `[E_CHANNEL_ELEM_TYPE_MISMATCH]` checker error, before this /// codegen gate is ever reached (`types/mod.rs`'s `channel_elem_type` + /// the `send`/`try_send` arg-assignability check). This gate therefore /// now only fires for (a) a channel the checker could NOT track (bare, /// un-annotated `Channel.new` — `T` genuinely unknown) sending a /// word-unsafe value, or (b) a CORRECTLY `T`-matched but word-unsafe `T` /// itself (`Channel[str]` — `str` fits `T` exactly, but still can't fit /// the single-word runtime slot). `.send(v)`/`.try_send(v)` used to cast `v` to /// `nova_int` UNCONDITIONALLY (`(nova_int)(v)`) regardless of `v`'s /// real type: a 2-word struct like `nova_str` happens to CC-FAIL (C /// forbids struct→int casts — loud, if cryptic), but a pointer-sized /// non-int `T` (`[]u8`, any heap record, `HashMap`, …) "compiles" via /// C's implicit pointer→integer conversion and silently round-trips /// through the same nova_int slot with the WRONG static type (sound in /// practice — the address round-trips exactly, `nova_int` being /// address-sized — but never actually type-checked, silently /// undocumented, and NOT what a user asking for `Channel[[]u8]` /// reasonably expects). `nova_bool`/`nova_char`/`int8_t..int64_t` are /// likewise safe (value-preserving widen-then-narrow through a wider /// `nova_int`). `nova_f32`/`nova_f64` are the one silently-WRONG /// scalar case — a plain `(nova_int)(v)` cast TRUNCATES the float's /// VALUE (real conversion, not the bit-pun `cast_from_nova_int` /// already uses when materializing `nova_int` BACK to `nova_f64` — no /// runtime `nova_bits_f2i` counterpart exists to make this /// symmetric), so they are excluded here too. Structs proper /// (`nova_str`, tuples, value-records — `is_struct_c_type` true and /// not erased to a pointer) can never fit a single word — genuinely /// out of scope for this channel implementation (would need the /// runtime to box non-word payloads, `docs/guide/channels.md` /// [M-channel-generic-elem-type]). fn channel_payload_c_type_ok(ty: &str) -> bool { ty == "nova_int" || ty.ends_with('*') || (!Self::is_struct_c_type(ty) && ty != "nova_f32" && ty != "nova_f64") } /// Plan 138.1 Ф.3 (D239): is this C type a raw `*mut T` storage pointer — /// i.e. a typed pointer that supports direct `ptr[i]` indexing (no `->data` /// indirection)? True for `Nova_Point**`, `nova_int*`, `nova_f64*`, `void*`, /// etc. False for the collection wrapper structs (`NovaArray_*`, /// `Nova_Vec____*`) — those carry a `data`/`len`/`cap` header and must be /// indexed via their element-access path. Also false for `nova_str` (it is a /// `{ptr,len}` value struct, indexed by codepoint via a runtime helper). fn is_raw_pointer_storage_c(ty: &str) -> bool { let t = ty.trim(); if !t.ends_with('*') { return false; } // A single-pointer collection wrapper (`NovaArray_<T>*` / `Nova_Vec____<T>*`) // is a VALUE handle indexed via its `data` header — not raw storage. // But a DOUBLE pointer to one (`Nova_Vec____<T>**` = `*mut Vec[T]`, the // backing buffer of a `Vec[Vec[T]]`) IS raw `*mut T` storage and must be // indexed directly. if (t.starts_with("NovaArray_") || t.starts_with("Nova_Vec____")) && !t.ends_with("**") { return false; } true } /// Plan 72 P3-B (updated): generate vtable struct typedef, NovaBox typedef, /// thunk functions, and vtable instance for a protocol-typed binding. /// Returns (vtable_instance_name, box_c_type) on success, None on failure. /// Caller emits the box initialization; no companion variable is generated here. /// /// Returns the companion C variable name (e.g. `"__vt_x"`) on success, /// or `None` if vtable generation is not possible (missing registry entry, /// unknown concrete type, etc.). /// /// Side-effects: /// - May append to `self.mono_fwd_decls` (vtable struct, thunks, instance). /// - Emits the companion variable declaration into the current body via `self.line()`. fn emit_protocol_vtable_companion( &mut self, proto_name: &str, type_args: &[String], // concrete C types for each protocol type param concrete_c_ty: &str, // C type of the assigned value, e.g. "Nova_IntCounter*" ) -> Option<(String, String)> { // (vtable_instance_name, box_c_type) // Plan 97.1 Ф.1 (D142): non-generic protocols → empty type_args OK. // Concrete тип всё равно pointer (Nova_X* — implementer record). if !concrete_c_ty.ends_with('*') { return None; } // Derive the concrete struct name from the C type ("Nova_IntCounter*" → "IntCounter"). let concrete_name = self.debt_strip_nova_trim_start(concrete_c_ty); if concrete_name.is_empty() { return None; } // Plan 97.1 Ф.1: mangling без args-суффикса для non-generic. let args_mangled: String = if type_args.is_empty() { String::new() } else { type_args.iter() .map(|t| Self::sanitize_c_for_ident(t)) .collect::<Vec<_>>() .join("_") }; let suffix = if args_mangled.is_empty() { String::new() } else { format!("_{}", args_mangled) }; let vtable_struct = format!("NovaVtable_{}{}", proto_name, suffix); let vtable_instance = format!("_vt_{}{}__{}", proto_name, suffix, concrete_name); // Emit (or reuse) the vtable struct + NovaBox typedef. Concrete-type // independent — shared across every implementer of this protocol. let box_c_type = self.emit_protocol_box_typedef(proto_name, type_args)?; // Get protocol method registry entry (clone to avoid borrow conflict). let (type_params, methods) = self.protocol_method_registry.get(proto_name)?.clone(); // Build type substitution: protocol type-param → concrete C type. let subst: HashMap<String, String> = type_params.iter() .zip(type_args.iter()) .map(|(k, v)| (k.clone(), v.clone())) .collect(); // ── Emit thunks + vtable instance (once per (proto, type_args, concrete)) ─ let inst_key = (vtable_struct.clone(), concrete_name.clone()); if !self.emitted_vtable_instances.contains(&inst_key) { self.emitted_vtable_instances.insert(inst_key); let old_subst = std::mem::replace( &mut self.current_type_subst, Self::subst_map_from_c_pairs(subst.clone()), ); let mut field_inits = Vec::new(); for m in &methods { let thunk_name = format!( "_thunk_{}{}_{}__{}", proto_name, suffix, m.name, concrete_name ); let ret_c = m.return_type.as_ref() .and_then(|rt| self.type_ref_to_c(rt).ok()) .unwrap_or_else(|| "nova_unit".to_string()); // Plan 91.8a.2 followup 2026-05-29: Self-typed params lower to // `void*` в vtable struct (concrete type unknown at protocol // declaration); thunk body casts back to concrete via // `(concrete_c_ty)`. Раньше Self-typed params silently // отбрасывались (type_ref_to_c(Self) errored without // receiver context) → vtable arity mismatch на call. let is_self_typeref = |ty: &TypeRef| -> bool { matches!(ty, TypeRef::Named { path, .. } if path.len() == 1 && path[0] == "Self") }; // Per-param: (vtable_sig_type, fwd_expr_template). For Self // params: sig="void*", fwd casts via `(concrete_c_ty)a{i}`. let param_specs: Vec<(String, bool)> = m.params.iter() .filter_map(|p| { if is_self_typeref(&p.ty) { Some(("void*".to_string(), true)) } else { self.type_ref_to_c(&p.ty).ok().map(|t| (t, false)) } }) .collect(); let extra_param_tys: Vec<String> = param_specs.iter() .map(|(t, _)| t.clone()).collect(); let extra_sig = extra_param_tys.iter().enumerate() .map(|(i, t)| format!(", {} a{}", t, i)) .collect::<String>(); let extra_fwd = param_specs.iter().enumerate() .map(|(i, (_, is_self))| { if *is_self { format!(", ({})a{}", concrete_c_ty, i) } else { format!(", a{}", i) } }) .collect::<String>(); let concrete_method = format!("Nova_{}_method_{}", concrete_name, m.name); // Plan 91.8a.2 [M-91.8a.2-default-body-general] 2026-05-29: // pre-emit Nova_<T>_method_<m> via general synthesis if T // lacks an explicit method. Replaces the prior hardcoded // equals/fmt MVP. Thunk then simply forwards. // Plan 91.9 (D186): coercion `let x P = u` is explicit // user opt-in → gate_on_impl=false (no #impl required). let has_explicit_method = self.all_methods .contains(&(concrete_name.clone(), m.name.clone())); if !has_explicit_method { let _ = self.try_synthesize_default_method_with_gate( &concrete_name, concrete_c_ty, &m.name, false, ); } let thunk_body = { format!( "return {concrete_method}(({cty})self{extra_fwd});", concrete_method = concrete_method, cty = concrete_c_ty, extra_fwd = extra_fwd, ) }; self.mono_fwd_decls.push_str(&format!( "{storage}{ret_c} {thunk}(void* self{extra_sig}) {{\n\ \t{body}\n\ }}\n", storage = self.top_level_storage(), ret_c = ret_c, thunk = thunk_name, extra_sig = extra_sig, body = thunk_body, )); field_inits.push(format!(" .{} = {}", m.name, thunk_name)); } self.current_type_subst = old_subst; self.mono_fwd_decls.push_str(&format!( "{}const {} {} = {{\n{}\n}};\n", self.top_level_storage(), vtable_struct, vtable_instance, field_inits.join(",\n") )); } Some((vtable_instance, box_c_type)) } /// Plan 72 P3-B: emit the vtable struct typedef and the `NovaBox_*` /// fat-pointer typedef for `(proto_name, type_args)`. Concrete-type /// independent, so it is safe to call from forward-decl emission (where the /// implementing type is not yet known). Emitted into `generic_type_defs_buf`, /// which is spliced before *both* function forward declarations and bodies. /// Idempotent — returns the `NovaBox_*` C type name on success. fn emit_protocol_box_typedef( &mut self, proto_name: &str, type_args: &[String], ) -> Option<String> { // Plan 97.1 Ф.3 (D142): skip protocols, vtable struct которых // уже определён в `nova_rt/vtables.h` (`Hash`, `Compare`). // D237: Hashable→Hash, Comparable→Compare. // Эмиссия typedef'а второй раз → C redefinition. // Эти protocol'ы ВСЁ РАВНО получают NovaBox_<X> typedef (одно // определение в generic_type_defs_buf — runtime даёт только // NovaVtable_<X>, не NovaBox). const RT_VTABLE_PROTOCOLS: &[&str] = &["Hash", "Compare", "Display"]; let rt_has_vtable = type_args.is_empty() && RT_VTABLE_PROTOCOLS.contains(&proto_name); // Plan 97.1 Ф.1 (D142): non-generic protocols (`type Locker protocol // { ... }` без [T]) теперь тоже эмитят vtable + box. Это нужно для // protocol-литерала (`protocol Locker { lock() => ... }`) и для // унифицированного dispatch'а на protocol-typed value. // Mangling без args-суффикса для non-generic. let args_mangled: String = if type_args.is_empty() { String::new() } else { type_args.iter() .map(|t| Self::sanitize_c_for_ident(t)) .collect::<Vec<_>>() .join("_") }; let suffix = if args_mangled.is_empty() { String::new() } else { format!("_{}", args_mangled) }; let vtable_struct = format!("NovaVtable_{}{}", proto_name, suffix); let box_c_type = format!("NovaBox_{}{}", proto_name, suffix); // Need the protocol method registry to lay out the vtable struct. let (type_params, methods) = self.protocol_method_registry.get(proto_name)?.clone(); if self.emitted_vtable_types.contains(&vtable_struct) { return Some(box_c_type); } self.emitted_vtable_types.insert(vtable_struct.clone()); // Substitute protocol type-params → concrete C types while lowering // method return / param types. let subst: HashMap<String, String> = type_params.iter() .zip(type_args.iter()) .map(|(k, v)| (k.clone(), v.clone())) .collect(); let old_subst = std::mem::replace( &mut self.current_type_subst, Self::subst_map_from_c_pairs(subst), ); // Plan 97.1 Ф.4 (D142): pre-compute param C types per method (для // overload-mangling, analog emit_effect_type). Если protocol имеет // overloaded methods (e.g. `get(key str)` + `get(key int)` — // Plan 33.3), field-name'ы должны быть mangled с param-type-suffix, // иначе C `duplicate member 'get'`. // Plan 91.8a.2 followup 2026-05-29: Self-typed params lower to `void*` // в vtable struct (same lowering used by thunk emission below). // [fix M-user-type-name-collides-with-stdlib-type-in-c-symbol, реестр // 221.1 №154]: lower the protocol's OWN method param/return types under // ITS declaring file — see `protocol_decl_file` doc above. GATED // (byte-identical for a CU without a collision — mirrors the existing // `any_type_file_collision()`-gated save/restore idiom used by // `emit_monomorphized_method_scoped_inner` and the type-decl loop). let saved_emit_file_id_proto = self.current_emit_file_id; if self.any_type_file_collision() { if let Some(&fid) = self.protocol_decl_file.get(proto_name) { self.current_emit_file_id = Some(fid); } } let method_param_c: Vec<(String, Vec<String>)> = methods.iter().map(|m| { let pts: Vec<String> = m.params.iter() .filter_map(|p| { if matches!(&p.ty, TypeRef::Named { path, .. } if path.len() == 1 && path[0] == "Self") { Some("void*".to_string()) } else { self.type_ref_to_c(&p.ty).ok() } }) .collect(); (m.name.clone(), pts) }).collect(); let all_method_pairs: Vec<(&str, &[String])> = method_param_c.iter() .map(|(n, p)| (n.as_str(), p.as_slice())) .collect(); let mut fields = String::new(); // [M-176-generic-wrapper-mono-inference] / [M-180-valuerecord-err-protocol-method-mono]: // a protocol method returning `Result[T, <value-record E>]` lowers its // return C-type to the CONCRETE `NovaRes_<ok>_NovaValue_<E>*` (unlike a heap // error, which `type_ref_to_c` erases to the runtime-provided // `NovaRes_nova_int_nova_str`). The vtable struct is spliced into an EARLY // buffer (`user_type_fwd_decls` for non-generic, `generic_type_defs_buf` for // generic) — BEFORE the `__NOVARES_TYPEDEFS__` marker where that mono's // typedef lands — so the concrete name is otherwise "unknown type name" at // the vtable (CC-FAIL). A pointer field only needs the struct TAG declared, // so forward `typedef struct NovaRes_<n> NovaRes_<n>;` into the SAME early // buffer, ahead of the struct; the full body is still emitted (once) by // `register_novares_decl` at its phase-correct splice (C11 6.7/3: redundant // typedef to the same type). §0 phase-correctness (declare before reference). let mut novares_fwds = String::new(); let mut novares_fwds_seen: HashSet<String> = HashSet::new(); for (m, (_, param_c_types)) in methods.iter().zip(method_param_c.iter()) { let ret_c = m.return_type.as_ref() .and_then(|rt| self.type_ref_to_c(rt).ok()) .unwrap_or_else(|| "nova_unit".to_string()); for c in std::iter::once(&ret_c).chain(param_c_types.iter()) { if let Some(n) = c.strip_prefix("NovaRes_").and_then(|s| s.strip_suffix('*')) { // `NovaRes_nova_int_nova_str` lives in nova_rt/array.h — never re-declare. if n == "nova_int_nova_str" { continue; } if novares_fwds_seen.insert(n.to_string()) { novares_fwds.push_str(&format!( "typedef struct NovaRes_{n} NovaRes_{n};\n", n = n)); } } // Plan 172.12 A8 fix: a generic-mono POINTER slot (`Nova_<Base>____<args>*` // — e.g. `[]u8` → `Nova_Vec____nova_byte*`) needs its typedef name // declared BEFORE this vtable struct. `type_ref_to_c` registers the // instance on the mono worklist, but the drain appends its // forward-typedef to `user_type_fwd_decls` AFTER this vtable was // appended (same buffer, later in time) → "unknown type name" at the // vtable line. Pre-A8 this slot lowered to the unconditionally-declared // legacy `NovaArray_<prim>*` (the Vec template isn't registered yet at // protocol-emission time, so the legacy branch fired) — retired with // array.h's DECL/IMPL snос. Mirror the `NovaRes_` accumulation above: // forward `typedef struct <n> <n>;` into the same early buffer; the // full struct body is still emitted once by the worklist drain // (C11 6.7/3: redundant typedef to the same type is valid). if let Some(n) = c.strip_suffix('*') { if n.starts_with("Nova_") && n.contains("____") && novares_fwds_seen.insert(n.to_string()) { novares_fwds.push_str(&format!( "typedef struct {n} {n};\n", n = n)); } } // Same A8 ordering fix for a composite Option VALUE slot // (`Option[[]str]` → `NovaOpt_Nova_Vec____nova_str_p`): its full // typedef is emitted by `register_novaopt_decl` into // `novaopt_typedefs_buf`, spliced AFTER this early buffer. All // NovaOpt emitters (array.h `NOVA_OPT_DECL` included) use the // NAMED struct tag `NovaOpt_<sani>`, so a forward // `typedef struct NovaOpt_<x> NovaOpt_<x>;` is compatible, and an // incomplete return/param type is valid C in a function-POINTER // declaration (completeness is only required at the call site, // which lands after the full typedef). Restrict to composite // images (a `Nova`-prefixed or pointer-sanitized payload) — the // primitive `NovaOpt_nova_*` set is pre-declared in array.h. if !c.ends_with('*') { if let Some(payload) = c.strip_prefix("NovaOpt_") { if (payload.contains("____") || payload.ends_with("_p") || payload.starts_with("Nova")) && novares_fwds_seen.insert(c.to_string()) { novares_fwds.push_str(&format!( "typedef struct {n} {n};\n", n = c)); } } } } let params_str = std::iter::once("void*".to_string()) .chain(param_c_types.iter().cloned()) .collect::<Vec<_>>() .join(", "); let mangled_name = Self::mangle_op(&m.name, param_c_types, &all_method_pairs); fields.push_str(&format!(" {} (*{})({}); \n", ret_c, mangled_name, params_str)); } // [fix №154] restore — see save above. self.current_emit_file_id = saved_emit_file_id_proto; self.current_type_subst = old_subst; // Vtable struct typedef (skip если runtime уже даёт его). let args_desc = if args_mangled.is_empty() { "(non-generic)".to_string() } else { args_mangled.clone() }; // Plan 97.1 Ф.4 (D142): для non-generic protocol (empty type_args) // — typedef'ы эмитим в `user_type_fwd_decls` (preamble marker, // splice'ится ОЧЕНЬ рано, перед mono'd tuple typedefs). Tuple'ы // вида `(Reader, Writer)` из capability-split factory ссылаются // на `NovaBox_<Proto>` — должны видеть typedef. // Generic-protocol'ы (Plan 72 P3-B) — оставляем generic_type_defs_buf // (исходный behavior, не ломаем). let target_buf: &mut String = if type_args.is_empty() { &mut self.user_type_fwd_decls } else { &mut self.generic_type_defs_buf }; if !rt_has_vtable { // Forward-decl referenced value-record `NovaRes_<n>` monos ahead of the // vtable struct (same early buffer) — see the accumulation comment above. for fwd in novares_fwds.lines() { let line = format!("{}\n", fwd); if !target_buf.contains(&line) { target_buf.push_str(&line); } } target_buf.push_str(&format!( "/* Plan 72 P3-B / Plan 97.1: vtable for {} instantiated with {} */\n\ typedef struct {vts} {{\n{fields}}} {vts};\n", proto_name, args_desc, vts = vtable_struct, fields = fields )); } // Fat-pointer box struct typedef: { void* data; const VT* vtable; }. // Эмитим всегда — runtime даёт только VT, не Box. target_buf.push_str(&format!( "typedef struct {{ void* data; const {vts}* vtable; }} {box};\n", vts = vtable_struct, box = box_c_type )); Some(box_c_type) } /// Plan 91.8a.2 [M-91.8a.2-default-body-general] 2026-05-29. /// /// Production-grade default body synthesis. Replaces the prior MVP that /// hardcoded equals/fmt patterns. Given a concrete Nova type `t_name` /// (with C type `t_c_ty` — pointer or primitive) and a `method_name`, /// searches `protocol_method_registry` for the first protocol whose /// method of that name has a `default_body` AST that can be successfully /// emitted with `Self → t_name` substitution. /// /// Side effects on success: /// - Emits `static <ret_c> Nova_<t_san>_method_<method>(<t_c_ty> nova_self, ...)` /// into `mono_fwd_decls`. Calls into this function from the regular /// method-dispatch path / vtable thunks / interpolation work uniformly. /// - Registers `(t_name, method_name)` in `synthesized_default_methods`. /// /// Cycle detection: `(t_name, method_name)` is added to /// `synthesizing_default_methods` before body emission; if the body /// transitively triggers synthesis of the same pair, the inner call /// returns `None` (caller may emit `E_SYNTH_CYCLE` or fall through). /// /// Ambiguity: if multiple protocols define `method_name` with default /// bodies and ALL would synthesize for `t_name`, the first protocol /// in registry-iteration order wins. Diagnostics for true ambiguity /// (E_SYNTH_AMBIGUOUS) are followup. /// /// Returns `Some(c_fn_name)` on success, `None` if no protocol-default /// applies or every applicable body fails to emit. fn try_synthesize_default_method( &mut self, t_name: &str, t_c_ty: &str, method_name: &str, ) -> Option<String> { // Plan 91.9 (D186): default mode is opt-in gated — bare-call sites // require T to opt-in via `#impl(P)`. Use `_with_gate(false)` from // sites where the user has already explicitly opted in (vtable // thunk for coercion, `as Protocol` cast, parameter coercion in // `func(...args []Protocol)`). self.try_synthesize_default_method_with_gate(t_name, t_c_ty, method_name, true) } fn try_synthesize_default_method_with_gate( &mut self, t_name: &str, t_c_ty: &str, method_name: &str, gate_on_impl: bool, ) -> Option<String> { let key = (t_name.to_string(), method_name.to_string()); let t_san = Self::sanitize_c_for_ident(t_name); let canonical_c_name = format!("Nova_{}_method_{}", t_san, method_name); // Cache hit — already synthesized. if self.synthesized_default_methods.contains(&key) { return Some(canonical_c_name); } // T already declares this method explicitly → caller should not // synthesize; use the existing dispatch path. if self.all_methods.contains(&key) { return None; } // Cycle guard — synthesis of (t, m) triggered recursively. if self.synthesizing_default_methods.contains(&key) { return None; } // Snapshot protocol method registry для stable iteration (mutable // borrow of `self.out` happens during emission). // Plan 91.9 (D186) gate: when `gate_on_impl=true` (bare-call / // interpolation), restrict candidates to protocols that T has // explicitly opted into via `#impl(P)`. When `gate_on_impl=false` // (vtable thunk for coercion, generic bound mono — user already // opted in explicitly), allow any matching protocol. let opted_in_protocols: Option<&HashSet<String>> = if gate_on_impl { self.type_impl_protocols.get(t_name) } else { None }; let candidates: Vec<(String, EffectMethod)> = self .protocol_method_registry .iter() .filter(|(proto_name, _)| { if !gate_on_impl { return true; } match opted_in_protocols { Some(set) => set.contains(proto_name.as_str()), None => false, } }) .flat_map(|(proto_name, (_type_params, methods))| { methods.iter() .filter(|m| m.name == method_name && m.default_body.is_some()) .map(move |m| (proto_name.clone(), m.clone())) }) .collect(); if candidates.is_empty() { return None; } self.synthesizing_default_methods.insert(key.clone()); let result = self.try_emit_default_body_candidates( t_name, t_c_ty, method_name, &candidates, &canonical_c_name, ); self.synthesizing_default_methods.remove(&key); if result.is_some() { self.synthesized_default_methods.insert(key); } result } /// Helper extracted из try_synthesize_default_method чтобы guard'ы /// (synthesizing set + cache) обрамляли единую попытку. Возвращает /// canonical C name на success, None если все кандидаты не emit'нулись. fn try_emit_default_body_candidates( &mut self, t_name: &str, t_c_ty: &str, method_name: &str, candidates: &[(String, EffectMethod)], canonical_c_name: &str, ) -> Option<String> { // [race-198 class-closure]: см. override_maps_scope_enter doc. Этот // синтез вызывается ЛЕНИВО из середины эмиссии чужого тела — scoping // обязателен в обе стороны (не утечь наружу, не унаследовать чужое). let ovr_saved = self.override_maps_scope_enter(); let r = self.try_emit_default_body_candidates_scoped_inner( t_name, t_c_ty, method_name, candidates, canonical_c_name); self.override_maps_scope_exit(ovr_saved, r.is_some()); r } fn try_emit_default_body_candidates_scoped_inner( &mut self, t_name: &str, t_c_ty: &str, method_name: &str, candidates: &[(String, EffectMethod)], canonical_c_name: &str, ) -> Option<String> { for (_proto_name, m) in candidates { // Snapshot mutable state. Если emission fails — rollback'аем // полностью, чтобы partial state не утёк в callers. let saved_out = std::mem::take(&mut self.out); let saved_indent = self.indent; let saved_var_types = self.var_types.clone(); let saved_var_mutable = self.var_mutable.clone(); let saved_recv = self.current_receiver_type.clone(); let saved_subst = self.current_type_subst.clone(); let saved_expected_record = self.expected_record_type.clone(); let saved_array_elem_hint = self.current_array_elem_hint.clone(); let saved_array_protocol_box = self.current_array_protocol_box.clone(); self.indent = 0; self.current_receiver_type = Some(t_name.to_string()); self.sync_receiver_rt(); // Substitute `Self` → `t_name` для resolution в default body. self.current_type_subst.insert("Self".to_string(), Self::lift_c_name(t_c_ty.to_string())); // Plan 172.1.1 (U.4.5 substrate, mono-side gap #1): для default-body на КОНКРЕТНОМ // generic-типе T (`Lru[str,int]`) populate generic-params в subst (`K→nova_str`, // `V→nova_int`), а не только `Self` — иначе `resolved_type_to_c(K)`/`type_ref_to_c(K)` // в теле резолвят K как stub (`Nova_K*`) вместо concrete (фаза-корректность §0; зеркало // `emit_monomorphized_method`:15082). Источник concrete-args — `generic_type_instance_info` // (mangled→(base,args)); имена параметров — `generic_type_templates[base].generics`. { let mangled = t_c_ty.trim_end_matches('*').trim().to_string(); let inst = self.generic_type_instance_info.borrow().get(&mangled).cloned(); if let Some((base, args)) = inst { let names: Vec<String> = self .generic_type_templates .get(&base) .map(|t| t.generics.iter().map(|g| g.name.clone()).collect()) .unwrap_or_default(); for (name, a) in names.into_iter().zip(args.into_iter()) { // A1‴: registry arg is already `ResolvedType` — flow straight into the // RT-typed subst carrier (no lift; `Raw` stays byte-identical). self.current_type_subst.entry(name).or_insert_with(|| a); } } } // Resolve return type / param types через type_ref_to_c // (Self уже резолвится через current_receiver_type per spec). let ret_c_result = match &m.return_type { Some(rt) => self.type_ref_to_c(rt), None => Ok("nova_unit".to_string()), }; let param_c_results: Vec<_> = m.params.iter() .map(|p| { // Self in param position → t_c_ty (same as receiver). let is_self = matches!(&p.ty, TypeRef::Named { path, .. } if path.len() == 1 && path[0] == "Self"); if is_self { Ok(t_c_ty.to_string()) } else { self.type_ref_to_c(&p.ty) } }) .collect(); let emission_ok = ret_c_result.is_ok() && param_c_results.iter().all(|r| r.is_ok()); if !emission_ok { // Restore state, try next candidate. self.out = saved_out; self.indent = saved_indent; self.var_types = saved_var_types; self.var_mutable = saved_var_mutable; self.current_receiver_type = saved_recv; self.sync_receiver_rt(); self.current_type_subst = saved_subst; self.expected_record_type = saved_expected_record; self.current_array_elem_hint = saved_array_elem_hint; self.current_array_protocol_box = saved_array_protocol_box; continue; } let ret_c = ret_c_result.unwrap(); let param_c_tys: Vec<String> = param_c_results.into_iter() .map(|r| r.unwrap()) .collect(); // Emit signature. let mut sig_parts: Vec<String> = vec![format!("{} nova_self", t_c_ty)]; for (p, pc) in m.params.iter().zip(param_c_tys.iter()) { sig_parts.push(format!("{} {}", pc, p.name)); } self.line(&format!( "{}{} {}({}) {{", self.top_level_storage(), ret_c, canonical_c_name, sig_parts.join(", ") )); self.indent = 1; // Register nova_self + params в scope. self.var_types.insert("nova_self".to_string(), t_c_ty.to_string()); for (p, pc) in m.params.iter().zip(param_c_tys.iter()) { self.var_types.insert(p.name.clone(), pc.clone()); } // Default body is a Block (parser wraps both `=> expr` and // `{ ... }` forms into Block, see parser/mod.rs line 2979+). let body = m.default_body.as_ref().expect("filter ensured Some"); let body_result = self.emit_default_body_as_return(body, &ret_c); match body_result { Ok(()) => { self.indent = 0; self.line("}"); let emitted = std::mem::replace(&mut self.out, saved_out); self.indent = saved_indent; self.var_types = saved_var_types; self.var_mutable = saved_var_mutable; self.current_receiver_type = saved_recv; self.sync_receiver_rt(); self.current_type_subst = saved_subst; self.expected_record_type = saved_expected_record; self.current_array_elem_hint = saved_array_elem_hint; self.current_array_protocol_box = saved_array_protocol_box; // Forward decl in mono_fwd_decls so callers can reference // before the body appears. self.mono_fwd_decls.push_str(&format!( "{}{} {}({});\n", self.top_level_storage(), ret_c, canonical_c_name, sig_parts.join(", ") )); self.mono_fwd_decls.push_str(&emitted); // Register synthesized method в overload/registry maps // так чтобы `infer_expr_c_type(obj.method(...))` находил // return type, callers получали правильный c-type // вместо default nova_int. let sig = MethodSig { param_c_types: param_c_tys.clone(), return_c_type: ret_c.clone(), is_instance: true, is_external: false, is_delegated: false, c_name: canonical_c_name.to_string(), variadic_last: false, param_defaults: vec![None; param_c_tys.len()], // Plan 108.4 Ф.1: EffectMethod now has `receiver_mut` field. // Using it here for correctness; checker enforcement (Ф.2) // will wire full mut-receiver dispatch for protocol methods. recv_mutable: m.receiver_mut, // Plan 184 (Р13/Р14): protocol-default params default to `ro`. param_modes: vec![0u8; param_c_tys.len()], // U.4.3 c2.2: protocol-default method is synthesized (no FnDecl). fn_span: None, }; self.method_overloads .entry((t_name.to_string(), method_name.to_string())) .or_default() .push(sig); self.all_methods.insert( (t_name.to_string(), method_name.to_string())); self.method_receivers.insert( method_name.to_string(), (t_name.to_string(), true)); return Some(canonical_c_name.to_string()); } Err(_) => { self.out = saved_out; self.indent = saved_indent; self.var_types = saved_var_types; self.var_mutable = saved_var_mutable; self.current_receiver_type = saved_recv; self.sync_receiver_rt(); self.current_type_subst = saved_subst; self.expected_record_type = saved_expected_record; self.current_array_elem_hint = saved_array_elem_hint; self.current_array_protocol_box = saved_array_protocol_box; continue; } } } None } /// Emit default body as a function body. The body may be either a /// trailing-expression block (returning a value) or a statement block /// (returning nova_unit). Handles both forms via standard emit_block / /// emit_expr mechanics — Self/@ references resolve naturally because /// `current_receiver_type` was set by the caller. fn emit_default_body_as_return( &mut self, body: &Block, ret_c: &str, ) -> Result<(), String> { if ret_c == "nova_unit" { // Statement block — emit stmts + trailing as a void-returning // body. Trailing expression (if any) emitted as a discarded expr. for s in &body.stmts { self.emit_stmt(s)?; } if let Some(t) = &body.trailing { let v = self.emit_expr(t)?; // Discard result; trailing in unit-returning fn is a stmt. self.line(&format!("(void)({});", v)); } self.line("return NOVA_UNIT;"); } else { for s in &body.stmts { self.emit_stmt(s)?; } if let Some(t) = &body.trailing { let v = self.emit_expr(t)?; self.line(&format!("return {};", v)); } else { // No trailing → emit best-effort zero-init return. return Err(format!( "default body of non-unit method has no trailing expression" )); } } Ok(()) } /// Plan 72 P3-B: box a concrete value `val` (C type `concrete_c`) into the /// `NovaBox_*` fat pointer for protocol `(proto, type_args)`. Values that /// are already boxed (`NovaBox_*`) pass through unchanged. Shared by /// protocol-return wrapping and protocol-parameter argument coercion. /// Уже ли это значение — построенный нами протокольный бокс? /// /// Единственный признак — форма литерала `(NovaBox_<X>){ ... }`, которую /// производит ТОЛЬКО `box_value_for_protocol`. Проверка снимает ведущие /// пробелы и любое число открывающих скобок: промежуточные сайты вправе /// обернуть выражение ещё одной парой, и «начинается с `(NovaBox_`» такую /// обёртку не узнаёт (`((NovaBox_X){…})` начинается с двух скобок). /// /// Заведено интегратором 2026-08-10 после слияния №546: коэрсия в /// экзистенциал сведена в одну точку, но ДРАЙВЕРОВ у неё несколько, и /// каждый обязан уметь распознать чужой результат. Без этого вернулось /// двойное боксирование на типизированной привязке (`mut g Greeter = sp`), /// уронив `standalone/f2_protocol_dispatch_method_survives` тем же /// CC-FAIL, от которого №546 и лечили. fn value_is_protocol_box(val: &str) -> bool { val.trim_start() .trim_start_matches(|c: char| c == '(' || c.is_whitespace()) .starts_with("NovaBox_") } fn box_value_for_protocol( &mut self, val: String, concrete_c: &str, proto: &str, type_args: &[String], ) -> String { if concrete_c.starts_with("NovaBox_") { return val; } // Реестр 221.1 №546 (K1): idempotency guard on the VALUE STRING, not // just the source `concrete_c`. `concrete_c` is the STATIC type of // the original source expression, computed once by each caller from // its OWN vantage point (e.g. `wrap_protocol_return`'s // `infer_expr_c_type(val_expr)`, which reads the ORIGINAL AST node, // not what `val` currently holds). When two boxing driver sites // chain on the SAME expr — `Stmt::Return`'s explicit-return path // routes `v` through `emit_expr_with_target_type(v, ret_ty)` FIRST // (which now boxes protocol-typed targets itself, see that fn's own // hook), THEN calls `wrap_protocol_return` on the already-boxed // result — `concrete_c` still reports the ORIGINAL unboxed pointer // type (`Nova_X*`), so the `concrete_c`-only guard above missed the // double-call and re-wrapped an already-boxed value, producing a // nested `(NovaBox_P){ .data = (void*)((NovaBox_P){ ... }), ... }` // literal (CC-FAIL: "operand of type 'NovaBox_P' where arithmetic or // pointer type is required" — a `NovaBox_*` fat pointer can't be // reinterpreted as `void*` any more than it can be C-cast). This // function is the ONLY producer of the `(NovaBox_<X>){ .data = ..., // .vtable = ... }` literal shape, so recognizing that shape on `val` // itself is a closed-loop, always-correct idempotency check — // covers every current AND future double-drive combination without // each call site having to separately track "did an earlier stage // already box this". // Дополнение интегратора 2026-08-10, после слияния фикса №546: проверка // формы обязана быть устойчива к ЛИШНИМ СКОБКАМ [INV-TODO: №546] // Исходная версия // сравнивала с `"(NovaBox_"`, и потому не узнавала собственный же // результат, если промежуточный сайт обернул его ещё одной парой // скобок: `((NovaBox_Greeter){ ... })` начинается с ДВУХ `(`. Ровно так // вернулось двойное боксирование на пути типизированной привязки // (`mut g Greeter = sp`) — фикстура // `standalone/f2_protocol_dispatch_method_survives` покраснела тем же // CC-FAIL, от которого №546 и лечили. Снимаем ведущие пробелы и ЛЮБОЕ // число открывающих скобок, и лишь потом сверяем форму: так проверка // закрыта относительно собственного вывода при любой обёртке. if Self::value_is_protocol_box(&val) { return val; } if let Some((inst, box_ty)) = self.emit_protocol_vtable_companion(proto, type_args, concrete_c) { return format!("({box_ty}){{ .data = (void*)({val}), .vtable = &{inst} }}"); } // Plan 72 [M-protocol-return-wrap-relies-on-infer] closed: a value that // cannot be boxed into the protocol fat pointer is a hard compile error // (E7202), not a silent passthrough that would surface downstream as a // confusing CC-FAIL. Reachable only when the concrete type is a // non-pointer (primitive / Option / tuple) — the type-checker normally // guarantees a record/sum implementer, so this is a strict guard. self.strict_errors.borrow_mut().push(format!( "error E7202: cannot box value into protocol `{}` — concrete type \ `{}` is not a boxable implementer\n \ note: only record/sum types (heap pointers `Nova_X*`) can be \ stored in a protocol fat pointer (NovaBox)\n \ help: pass/return a value of a type that implements `{}`", proto, concrete_c, proto )); val } /// Plan 72 P3-B return: if the current function declares a protocol return /// type, wrap a concrete return value `val` (source expression `val_expr`) /// into a `NovaBox_*` fat pointer. No-op for ordinary return types. fn wrap_protocol_return(&mut self, val: String, val_expr: &Expr) -> String { let Some((proto, type_args)) = self.current_fn_returns_protocol.clone() else { return val; }; let concrete_c = self.infer_expr_c_type(val_expr); self.box_value_for_protocol(val, &concrete_c, &proto, &type_args) } /// Plan 174.3 (D53): if the current function returns `any`, box a concrete /// return value (implicit upcast `T → any`). No-op for ordinary returns and /// for values already erased to `any` (void*). Mirrors `wrap_protocol_return`. fn wrap_any_return(&mut self, val: String, val_expr: &Expr) -> String { if !self.current_fn_returns_any { return val; } let concrete_c = self.infer_expr_c_type(val_expr); if concrete_c == "void*" || concrete_c.is_empty() { return val; } self.emit_any_box(&concrete_c, &val) } // **Plan 147 Ф.3 (D246, 3-axis L3):** the D33 §2 / D216 V2 binding-mut // promotion (`field_type_with_binding_mut` / `promote_pointer_pointee_mut`) // is REMOVED. Under the three-axis model pointee-mutability is read FROM // THE TYPE (`*mut T` = mut, `*T ≡ *ro T` = ro), position- and // binding-independent (L3 ⊥ L1). A `mut`-bound field never inherits a // writable pointee — a writable heap buffer is declared `*mut T` explicitly // (e.g. `Vec.data`), so its C codegen stays `Nova_T*` (no `const`) and the // forward-`typedef` still fires. Field types are now emitted verbatim. /// Plan 172.1 U.6.1.b: THE single primitive type-name → C-name table, shared by /// `type_ref_to_c` and `apply_type_subst_to_ref` (§0/§2 — eliminates the third /// hand-copied table whose missing `u32` drifted and broke `Vec[u32]` mangling, /// Plan 152.8). Returns ONLY the pure primitive mapping; the caller-specific bits /// stay caller-side: `type_ref_to_c` keeps the removed-type `Err` arms /// (usize/isize/ptr) + `never`; `apply_type_subst_to_ref` keeps the `byte` alias. /// (§2-sanctioned primitive width/sign table — the only exception, single source.) /// /// 196.3 D315: `pub(crate)` so `ExternalRegistry::type_ref_to_c` (which runs /// standalone, BEFORE any `CEmitter` exists — registry construction — and so /// cannot use the `&self` canonical `type_ref_to_c`/`resolved_type_to_c` path) /// reuses this same table instead of a 4th hand-copied primitive list. pub(crate) fn primitive_name_to_c(name: &str) -> Option<&'static str> { Some(match name { "int" => "nova_int", "i64" => "int64_t", "i32" => "int32_t", "i16" => "int16_t", "i8" => "int8_t", "u64" => "uint64_t", "uint" => "nova_uint", "u32" => "uint32_t", "u16" => "uint16_t", "u8" => "nova_byte", "f64" => "nova_f64", "f32" => "nova_f32", "bool" => "nova_bool", "str" => "nova_str", "char" => "nova_char", _ => return None, }) } /// U.4.8 (D315): thin adapter from the SYNTACTIC `TypeRef` (parser output) to the single /// canonical type→C lowering `resolved_type_to_c` (over `ResolvedType`). The duplicate /// resolution-and-lowering `type_ref_to_c_impl` is DELETED — its `Ok` outputs were proven /// byte-identical to `resolved_type_to_c` (U.4.6/U.4.7 parity, 0 divergences over the /// type-heavy corpus), and its `Err` cases (`usize`/`isize`/`ptr` removed, `Self`-without- /// receiver) now live INSIDE `resolved_type_to_c` (the lowering carries its own failure /// reason via `Result`). One resolve (checker), one lowering (`resolved_type_to_c`) — /// compiler-conventions §0/§10, the §0-anti-pattern "two windows of type truth" is gone. /// /// (Endgame U.6.1 — collapsing this `TypeRef`→`ResolvedType` adapter hop at the ~120 /// declared-type call sites — is intentionally OUT of U.4.8 scope.) pub(crate) fn type_ref_to_c(&self, ty: &TypeRef) -> Result<String, String> { self.resolved_type_to_c(&crate::types::ResolvedType::from_type_ref(ty)) } /// [D52-амендмент, ОКНО-5] call-through resolver: is `ty` — directly, or /// through a newtype-over-fn / alias-of-fn name (`fn_newtype_sigs`, /// pre-scanned in `emit_module`) — structurally a `fn(...) -> ...` /// value? Returns an OWNED `TypeRef::Func{..}` clone so every /// `fn_param_sigs`-population call site can destructure it exactly like /// the literal-`Func` guard it replaces (see call sites below). /// Readonly/Mut/Uninit wrappers are transparent (mirror `wrap_kind_of`'s /// own peel loop for the same reason: `ro next Handler` params/locals). fn resolve_fn_typeref(&self, ty: &TypeRef) -> Option<TypeRef> { match ty { TypeRef::Func { .. } => Some(ty.clone()), TypeRef::Readonly(inner, _) | TypeRef::Mut(inner, _) | TypeRef::Uninit(inner, _) => { self.resolve_fn_typeref(inner) } TypeRef::Named { path, generics, .. } if generics.is_empty() => { let name = path.last()?; self.fn_newtype_sigs.get(name).cloned() } _ => None, } } /// [Plan 228 Ф.2(a), реестр 221.1 №94-v2] Minimal `ResolvedType` → /// `TypeRef` NORMALIZER for the unified HOF-binding channel registration /// (`Stmt::Let` handling, below): the checker's `resolved_types[call_id]` /// channel may materialize a HOF-binding RHS's type as EITHER a bare name /// (`ResolvedType::Named{X}`, peeled through `fn_newtype_sigs` by /// `resolve_fn_typeref` already) OR — for a bare reference to a free /// function BY NAME (`ro m Mid = identity_mw`, no call at all — the /// checker's universal per-Ident writer, `types/mod.rs` `f1_expr_inner` /// ~7836, channels the free fn's OWN declared signature directly) — a /// literal `ResolvedType::Func{..}`. Round-tripping that second shape /// back to `TypeRef::Func` (so `nested_fn_return_sig`/the C-lowering /// below can treat both shapes identically) needs ONLY `Named`/`Func`/ /// `Unit` — the three ResolvedType variants that can ever appear inside /// a HOF-binding's own callable signature (params/return of a fn-newtype /// or plain free fn are never raw pointers/tuples/arrays in this corpus' /// forms; a shape this doesn't cover returns `None` → honest fallback to /// legacy, never a wrong type). fn resolved_type_to_typeref_named( &self, rt: &crate::types::ResolvedType, span: Span, ) -> Option<TypeRef> { use crate::types::ResolvedType as R; match rt { R::Named { name, args, .. } if args.is_empty() => Some(TypeRef::Named { path: vec![name.clone()], generics: Vec::new(), span, }), R::Func { params, ret, .. } => { let ps: Option<Vec<TypeRef>> = params.iter() .map(|p| self.resolved_type_to_typeref_named(p, span)) .collect(); let r = self.resolved_type_to_typeref_named(ret, span)?; Some(TypeRef::Func { params: ps?, effects: Vec::new(), return_type: Some(Box::new(r)), extern_abi: None, span, }) } R::Unit => Some(TypeRef::Unit(span)), // D246 L2 axis: `readonly T` is a transparent CONTENT-view for C-lowering // purposes (mirrors `resolved_type_to_c`'s own peel) — a free fn's `-> ro // N78Hnd` return (a real corpus shape: `n78_identity_mw`) must not fail // normalization just because its declared return carries a `ro` // annotation the callable SIGNATURE itself doesn't care about. R::Readonly(inner) => self.resolved_type_to_typeref_named(inner, span), // [№TBD, реестр 221.1 №403] The four arms above only ever covered the // shapes a HOF-binding's OWN callable signature carried in the corpus this // fn was ORIGINALLY written for (Named/Func/Unit/Readonly — see fn doc). // `closure_channel_param_tys` reuses this SAME fn for a broader case (ANY // let-annotated `ClosureLight`'s own param/return types, `f1_check_assign_ // let` — types/mod.rs — registers `resolved_types[closure_id]` // unconditionally for every arity), where a bare primitive param/return // (`fn(Req) -> int`/`-> bool`/`-> str`/...) is completely ordinary. The old // catch-all `_ => None` bailed the instant `Func`'s own recursive per- // param/per-return call (line ~11424/11426 above) hit a `R::Scalar`/`R:: // Bool`/`R::Float`/`R::Str` — the `?` on THAT recursive call then discarded // the WHOLE surrounding `Func` conversion (params AND return), even when // every OTHER piece resolved fine. `ResolvedType::resolved_to_typeref` // (types/mod.rs — moved out of `TypeCheckCtx` for exactly this reuse) covers // the full primitive/composite surface (Scalar/Bool/Float/Str/Named-with- // generics/Array/Tuple/TypedPtr) this fn never did; falling back to it here // fixes the closure-literal case without touching this fn's own Func/ // Readonly handling (which the general converter deliberately does NOT // cover — `resolved_to_typeref` refuses `Func` outright). Confirmed via // `spec_tests/conformance/standalone/ // m2217_26_generic_static_method_value_arg_addr_mismatch`'s `via_closure` // test (Linux-only RUN-FAIL, PASS lines printed before the crash — the // closure's OWN C parameter type silently fell to `nova_int` while the // call site correctly built `NovaValue_Req`, an ABI-visible caller/callee // signature mismatch that both x86-64 ABIs mis-execute, but only SysV's // stack-vs-register split for oversized structs reliably segfaults). _ => crate::types::ResolvedType::resolved_to_typeref(rt, span), } } /// [fix M-nested-fn-newtype-bind-then-call-broken, реестр 221.1 №78, /// форма 2]: given an ALREADY-resolved fn-typed shape (`resolve_fn_typeref`'s /// own output — a param's or binding's OWN `Func{..}`), does its RETURN /// type itself resolve (through `resolve_fn_typeref` again — same /// newtype-over-fn/alias call-through) to ANOTHER `Func`? If so, return /// that inner signature's lowered C param/return types — the exact same /// computation `emit_fn_forward_decl` already does for a FREE fn's own /// declared return type (~L16246-16265, populating `fn_returns_fn_sig`). /// /// Needed because a fn-typed PARAMETER (`m Mid` where `Mid = fn(Hnd) -> /// Hnd`) only ever got a `fn_param_sigs` entry (its OWN call signature, /// `(["void*"], "void*")` — correct for calling `m(h)` itself) — nothing /// registered what calling `m` RETURNS is itself callable with. A /// `ro h2 = m(h)` inside the fn body then had no `fn_returns_fn_sig["m"]` /// to propagate onto `h2` (the pre-existing "RHS is a call whose callee /// carries a registered HOF-return sig" let-binding producer, ~L30377, /// is name-generic and already consumes this the moment it exists) — so /// `h2` got a storage C-type (via this fix's OTHER half, the /// `infer_call_ret_c` B10m0 fallback) but NO callable signature, and /// `h2(5)` fell to the "must be a free function" default: a bogus /// `nova_fn_h2` call, undefined-symbol at link time. fn nested_fn_return_sig(&self, resolved_func: &TypeRef) -> Option<(Vec<String>, String)> { let TypeRef::Func { return_type: Some(rt), .. } = resolved_func else { return None; }; let TypeRef::Func { params: fp, return_type: rt2, .. } = self.resolve_fn_typeref(rt)? else { unreachable!("resolve_fn_typeref always returns Func or None") }; let ptys: Vec<String> = fp.iter() .map(|t| self.type_ref_to_c(t).unwrap_or_else(|_| "nova_int".into())) .collect(); let rty = match rt2.as_ref() { Some(t) => self.type_ref_to_c(t).unwrap_or_else(|_| "nova_int".into()), None => "nova_unit".to_string(), }; Some((ptys, rty)) } /// [fix M-fn-newtype-return-position-broken, №53] Shared NovaClosBase /// dispatch for a fn-typed callee reached WITHOUT going through the /// `ExprKind::Ident` + `fn_param_sigs` fast path in `emit_call` (which /// already had this exact macro-select / cast-call logic inlined for the /// "plain fn-typed variable" case). Two NEW callers use it: calling the /// receiver itself inside a method on a fn-newtype (`@(v)`, форма г) and /// calling the RESULT of a call whose return resolves to a fn-type /// (`make()(3)` / `mw(h)(v)`, формы а/в) — `callee_str` is the already- /// emitted C expression producing the `NovaClosBase`-shaped (or plain fn- /// pointer, when a `NOVA_CLOS_CALL_*` macro fits) callable value. fn emit_clos_call_dispatch( &mut self, callee_str: &str, param_tys: &[String], ret_ty: &str, args: &[CallArg], ) -> Result<String, String> { let mut arg_strs = Vec::new(); for a in args { arg_strs.push(self.emit_expr(a.expr())?); } let macro_name = Self::clos_call_macro(param_tys, ret_ty); match macro_name { Some(m) => { if arg_strs.is_empty() { Ok(format!("{}({})", m, callee_str)) } else { Ok(format!("{}({}, {})", m, callee_str, arg_strs.join(", "))) } } None => { let mut cast_params = vec!["void*".to_string()]; cast_params.extend(param_tys.iter().cloned()); let cast_params_str = cast_params.join(", "); let mut all_args = vec![format!("((NovaClosBase*)({}))->env", callee_str)]; all_args.extend(arg_strs.iter().cloned()); Ok(format!("(({ret}(*)({params}))(((NovaClosBase*)({n}))->fn))({args})", ret = ret_ty, params = cast_params_str, n = callee_str, args = all_args.join(", "))) } } } fn return_type_c(&self, f: &FnDecl) -> Result<String, String> { match &f.return_type { Some(ty) => self.type_ref_to_c(ty), // Plan 55 Ф.3: если return type не указан, infer из expression-body. // `fn foo() => expr` и `fn foo() { ...; expr }` — оба должны брать тип // из выражения. Раньше всегда возвращали nova_unit → CC-FAIL в callers // ожидающих real type (e.g. str.from(d.@as_secs_f64()) внутри Duration). None => { // Plan 55 Ф.3: для `=> expr` body — infer из выражения. // field_cache (Plan 123.1) coerces FnBody::Expr → FnBody::Block(stmts=[],trailing=e) // BEFORE codegen sees the AST. Treat that coerced form the same as FnBody::Expr. // Block body с stmts сохраняет старое поведение nova_unit: // (a) Block обычно имеет side-effect semantics в stdlib (Channel.close, mut.insert, и т.п.). let expr_opt: Option<&Expr> = match &f.body { FnBody::Expr(e) => Some(e), // field_cache coercion: empty stmts + trailing = was originally FnBody::Expr. FnBody::Block(b) if b.stmts.is_empty() => b.trailing.as_deref(), _ => None, }; if let Some(e) = expr_opt { let t = self.infer_expr_c_type(e); if t.is_empty() || t == "void*" { Ok("nova_unit".into()) } else { Ok(t) } } else { Ok("nova_unit".into()) } } } } /// [M-exp-promotion-blockers: retry E_UNUSED_PREFIX_TYPEVAR] find a /// `Fail[E]`-shaped binding among a `with`-block's bindings and resolve /// its concrete C type via `current_type_subst` (`type_ref_to_c`). /// `None` when no `Fail[...]` binding is present, or its type-arg isn't /// currently resolvable (e.g. a genuinely-concrete `Fail[str]` handler /// falls back to the pre-existing hardcoded-default path unaffected). fn active_fail_e_hint(&self, bindings: &[WithBinding]) -> Option<String> { let e_ty_ref = bindings.iter() .map(|b| &b.effect) .find_map(|eff| match eff { TypeRef::Named { path, generics, .. } if path.last().map_or(false, |s| s == "Fail") => generics.first(), _ => None, })?; self.type_ref_to_c(e_ty_ref).ok() } /// [M-exp-promotion-blockers: url tuple-shape mono conflict, sibling /// Option case] Plan 39 Issue A: probe a `with`-block's handler /// literal(s) for the first `interrupt VAL` and return VAL's inferred /// C type — used as the with-block's result type when its body has no /// (or a divergent) trailing expression (D61 §10). /// /// `infer_handler_interrupt_ty` walks the RAW handler closure (`|e| /// interrupt Some(e)`) BEFORE `emit_handler_lit`'s own param-retyping /// override runs (that happens later, when the handler is actually /// installed) — at this point `e`'s var_type is still whatever the /// Fail effect's WIRE-level schema says (hardcoded `nova_str`, /// effects.h) or simply stale/unset, so `Some(e)`'s inferred element /// type is wrong for any concrete `Fail[X]` payload (X != str), not /// just a generic E. Temporarily rebind the handler's first param to /// the binding's REAL effect payload C type (mirrors the override /// `emit_handler_lit` applies later) so the probe sees the correct /// type — the same fix in spirit as `active_fail_e_hint`/ /// `current_fail_e_hint` above, but for a bare `Some(e)` (Option) /// rather than `Ok(e)`/`Err(e)` (Result). A plain method (not a /// closure) because it needs `&mut self` (var_types insert/remove) /// while OTHER `&self` calls (`type_ref_to_c` et al.) are still live /// at the call sites — a persistent mutably-capturing closure would /// conflict with those. fn probe_handler_ty(&mut self, bindings: &[WithBinding]) -> String { for b in bindings { let first_param = match &b.handler.kind { ExprKind::ClosureLight { params, .. } if !params.is_empty() => Some(params[0].name.clone()), ExprKind::ClosureFull(fsb) if !fsb.params.is_empty() => Some(fsb.params[0].name.clone()), _ => None, }; let payload_c: Option<String> = match &b.effect { TypeRef::Named { path, generics, .. } if path.last().map_or(false, |s| s == "Fail") => generics.first().and_then(|g| self.type_ref_to_c(g).ok()), _ => None, }; let saved = match (&first_param, &payload_c) { (Some(name), Some(c)) if c != "nova_str" => Some((name.clone(), self.var_types.insert(name.clone(), c.clone()))), _ => None, }; let ty = infer_handler_interrupt_ty(self, &b.handler); if let Some((name, prev)) = saved { match prev { Some(p) => { self.var_types.insert(name, p); } None => { self.var_types.remove(&name); } } } if let Some(ty) = ty { return ty; } } "nova_unit".into() } /// `&self` twin of `probe_handler_ty` for the read-only duplicate of this /// probe inside `infer_expr_c_type`'s `ExprKind::With` arm (used e.g. for /// a `ro r = with {...}` LET-binding's declared type) — same fix, via /// `closure_param_type_overrides` (a `RefCell`, so it works under `&self`) /// instead of directly mutating `var_types`. fn probe_handler_ty_ro(&self, bindings: &[WithBinding]) -> Option<String> { for b in bindings { let first_param = match &b.handler.kind { ExprKind::ClosureLight { params, .. } if !params.is_empty() => Some(params[0].name.clone()), ExprKind::ClosureFull(fsb) if !fsb.params.is_empty() => Some(fsb.params[0].name.clone()), _ => None, }; let payload_c: Option<String> = match &b.effect { TypeRef::Named { path, generics, .. } if path.last().map_or(false, |s| s == "Fail") => generics.first().and_then(|g| self.type_ref_to_c(g).ok()), _ => None, }; let saved = match (&first_param, &payload_c) { (Some(name), Some(c)) if c != "nova_str" => { let prev = self.closure_param_type_overrides.borrow_mut() .insert(name.clone(), c.clone()); Some((name.clone(), prev)) } _ => None, }; let ty = infer_handler_interrupt_ty(self, &b.handler); if let Some((name, prev)) = saved { let mut ovr = self.closure_param_type_overrides.borrow_mut(); match prev { Some(p) => { ovr.insert(name, p); } None => { ovr.remove(&name); } } } if let Some(ty) = ty { return Some(ty); } } None } // ---- effect with/handler ---- fn emit_with(&mut self, bindings: &[WithBinding], body: &Block) -> Result<String, String> { // (effect_name, prev_var, handler_val) — handler_val нужен в Ф.61 followup #1 // для attach owner_iframe (cross-effect throw routing). let mut saves: Vec<(String, String, String)> = Vec::new(); let mut has_fail = false; // Plan 174 (D349): a `with Fail[...]` block can CATCH a throw that // propagated through an in-flight `supervised { supervised { ... } }` // whose outer run-loop never executed (the body longjmp'd past it). On // that path `_nova_active_scope` is left dangling at the freed outer // scope frame, and the NEXT scope_init would inherit a garbage // `deadline_ns` from it → a spurious immediate TimeoutError. Snapshot // the active scope at with-entry and restore it after the catch is // resolved (normal completion AND handled throw both fall through to // the finalizer-restore tail). Gated on Fail so non-catching effect // handlers stay byte-identical. let catches_fail = bindings.iter().any(|b| matches!( &b.effect, TypeRef::Named { path, .. } if path.last().map_or(false, |s| s == "Fail") )); let active_scope_save = if catches_fail { let sv = self.fresh_tmp(); self.line(&format!("NovaFiberQueue* {} = _nova_active_scope;", sv)); Some(sv) } else { None }; // Plan 110.9.3 V1.1 [M-110.9.3-register-finalizer-lifo]: detect // Application binding для init/fire finalizer stack. Local stack // var + TLS pointer swap. Fired LIFO в обоих normal + throw paths. let mut application_fs_var: Option<String> = None; let mut application_fs_prev: Option<String> = None; let has_application = bindings.iter().any(|b| matches!( &b.effect, TypeRef::Named { path, .. } if path.last().map_or(false, |s| s == "Application") )); if has_application { let fs_var = self.fresh_tmp(); let prev_var = self.fresh_tmp(); self.line(&format!( "NovaFinalizerStack {fs} = {{ NULL }}; /* Plan 110.9.3 V1.1 */", fs = fs_var )); self.line(&format!( "NovaFinalizerStack* {prev} = _nova_active_finalizer_stack;", prev = prev_var )); self.line(&format!( "_nova_active_finalizer_stack = &{fs};", fs = fs_var )); application_fs_var = Some(fs_var); application_fs_prev = Some(prev_var); } for binding in bindings { let effect_name = match &binding.effect { TypeRef::Named { path, .. } => path.join("_"), _ => return Err("non-named effect in with binding".into()), }; if effect_name == "Fail" { has_fail = true; } // Plan 19, C8 codegen (D31-rev): handler-лямбда // `with EffectName = |args| body` — sugar над handler- // литералом для эффектов с одной операцией. Десугаризуем // ClosureLight/ClosureFull в синтетический HandlerLit // перед emit_expr. let handler_val = if let Some((effect_path, lit_expr)) = self.desugar_handler_lambda(&binding.effect, &binding.handler)? { let _ = effect_path; // подсветим для clippy self.emit_expr(&lit_expr)? } else { self.emit_expr(&binding.handler)? }; // [M-property-testing-rot] (Plan 172.13 батч 3) — GC ROOT PIN: the // installed handler value must be held in a STACK local for the // block's lifetime, not ONLY in the thread-local // `_nova_handler_<eff>` slot. Boehm's conservative collector does // not scan Windows `__declspec(thread)` TLS blocks, so a handler // reachable exclusively through the TLS pointer (a factory-call // form, `with Random = th.seeded(42) { ... }`, which previously // inlined the call INTO the TLS assignment) was collected after // enough allocations inside the block (~32 `generate()+clone()` // iterations, deterministic segfault) — use-after-free on the next // effect-op dispatch. A handler expression that is already a bare // local (`_nv_tmp_N` / user `ro h = ...`) is naturally stack-rooted; // pin unconditionally — one pointer-sized local per binding, and // the conservative scan keeps the vtable (and everything reachable // from its ctx: closure envs, boxed PRNG state) alive. let handler_val = { let pin = self.fresh_tmp(); self.line(&format!( "NovaVtable_{eff}* {pin} = {hv}; /* GC root pin (TLS not scanned) */", eff = effect_name, pin = pin, hv = handler_val )); pin }; let prev_var = self.fresh_tmp(); self.line(&format!( "NovaVtable_{eff}* {prev} = _nova_handler_{eff};", eff = effect_name, prev = prev_var )); // Plan 20 Ф.8 (4): D65 правило 3 — vtable->prev = outer handler. // Nova_Fail_fail swap'ает _nova_handler_Fail = current->prev на // время invocation, чтобы re-throw в handler-body dispatch'ился // на outer handler (skip current frame). if effect_name == "Fail" { self.line(&format!( "{hv}->prev = {prev};", hv = handler_val, prev = prev_var )); } self.line(&format!( "_nova_handler_{eff} = {hv};", eff = effect_name, hv = handler_val )); // Plan 61 followup #4: dual-install в per-E slot для `with Fail[E]`. // Adapter fn (file-scope static) wraps legacy handler — sets // typed payload в fail-frame, delegates к legacy handler с // diagnostic msg. Это даёт per-E fast-path dispatch без // refactor emit_handler_lit (handler arm body unchanged — // typed `e` access via fail_e_map → payload-in-frame). if effect_name == "Fail" { let e_c_opt = if let TypeRef::Named { generics, .. } = &binding.effect { generics.first().and_then(|g| self.type_ref_to_c(g).ok()) } else { None }; if let Some(e_c) = e_c_opt { // Plan 61 followup #4: skip primitives — для них per-E // не делается (см. register_fail_e_type). Бы install // adapter с undefined NovaVtable_Fail_<primitive>. let is_primitive = Self::primitive_type_id(&e_c).is_some(); if e_c != "nova_str" && !e_c.is_empty() && e_c != "void*" && !is_primitive { self.register_fail_e_type(&e_c); let mangled_e = Self::debt_per_e_mangle(&e_c); let e_arg = if e_c.ends_with('*') { e_c.clone() } else { format!("{}*", e_c) }; // Emit adapter fn (deferred file-scope). Используем // tmp_counter для uniqueness, НЕ handler_counter — // последний sync'нут с pre-scan forward decls, его // bumping ломает emit_handler_lit numbering. let adapter_id = format!( "_per_e_adapter_{}_{}", self.tmp_counter, mangled_e ); self.tmp_counter += 1; // Forward decl ДО use-site (extern linkage — block // scope не разрешает `static`). Definition в // deferred_impls тоже без `static` (unique name per // handler_counter — name collision impossible). self.line(&format!( "extern nova_unit {ad}(void* ctx, {ea} err);", ad = adapter_id, ea = e_arg )); let _ = writeln!(self.deferred_impls, "nova_unit {ad}(void* ctx, {ea} err) {{", ad = adapter_id, ea = e_arg); let _ = writeln!(self.deferred_impls, " if (_nova_fail_top) {{"); let _ = writeln!(self.deferred_impls, " _nova_fail_top->error_user_payload = (void*)err;"); let _ = writeln!(self.deferred_impls, " _nova_fail_top->error_user_type_id = NOVA_TID_USER_{m};", m = mangled_e); let _ = writeln!(self.deferred_impls, " }}"); let _ = writeln!(self.deferred_impls, " NovaVtable_Fail* legacy = (NovaVtable_Fail*)ctx;"); let _ = writeln!(self.deferred_impls, " return legacy->fail(legacy->ctx, nova_str_from_cstr(\"<typed-{m}>\"));", m = mangled_e); let _ = writeln!(self.deferred_impls, "}}"); let _ = writeln!(self.deferred_impls); // Allocate per-E vtable + dual-install в per-E slot. let per_e_vt_var = self.fresh_tmp(); let per_e_prev_var = self.fresh_tmp(); self.line(&format!( "NovaVtable_Fail_{m}* {vt} = (NovaVtable_Fail_{m}*)nova_alloc(sizeof(NovaVtable_Fail_{m}));", m = mangled_e, vt = per_e_vt_var )); self.line(&format!( "{vt}->ctx = (void*){hv};", vt = per_e_vt_var, hv = handler_val )); self.line(&format!( "{vt}->fail = {ad};", vt = per_e_vt_var, ad = adapter_id )); self.line(&format!( "NovaVtable_Fail_{m}* {prev} = _nova_handler_Fail_{m};", m = mangled_e, prev = per_e_prev_var )); self.line(&format!( "{vt}->prev = {prev};", vt = per_e_vt_var, prev = per_e_prev_var )); self.line(&format!( "{vt}->owner_iframe = NULL; /* set after iframe alloc */", vt = per_e_vt_var )); self.line(&format!( "_nova_handler_Fail_{m} = {vt};", m = mangled_e, vt = per_e_vt_var )); // Push в saves с special effect_name `Fail_<E>` для // restore. handler_val содержит per-E vtable. saves.push(( format!("Fail_{}", mangled_e), per_e_prev_var, per_e_vt_var, )); } } } saves.push((effect_name, prev_var, handler_val)); } // For `with Fail = ... { body }`: install a fail-frame around body so // that throw inside body (Nova_Fail_fail with installed handler) ends // up unwinding back here. Body normal completion → result; throw → // handler runs (state captured), then nova_throw → fail-frame catches. // (D65 «Fail strict»: fail() is never from caller's perspective.) let fframe = if has_fail { Some(self.fresh_tmp()) } else { None }; // Plan 173 Ф.2.C: `caught` flag distinguishes «fail-frame caught a throw» // from normal/interrupt completion, so the unified terminal transport // (nova_scope_exit) can run AFTER the common pop/restore epilogue instead // of duplicating per-kind dispatch inside the catch-branch. Declared // before the setjmp (не modified between setjmp/longjmp — set only in the // caught-branch that runs after longjmp returns → well-defined). let caught_var = if has_fail { Some(self.fresh_tmp()) } else { None }; if let Some(ff) = &fframe { self.line(&format!("NovaFailFrame {};", ff)); self.line(&format!("nova_fail_push(&{});", ff)); } if let Some(cv) = &caught_var { self.line(&format!("int {} = 0;", cv)); } // Plan 39 Issue A: infer T_body to pick correct result slot. // Category of trail type decides storage: // - int/bool/unit → use NovaInterruptFrame.value (nova_int slot) // - pointer type (contains '*') → use NovaInterruptFrame.value_ptr (void* slot) // - value struct (NovaOpt_X, NovaResult_X_E, etc.) → heap-allocate, // pointer goes through value_ptr; reader dereferences. // Если body не имеет trailing (заканчивается throw/return), смотрим на // handler interrupt-VAL type — это semantically тип W (D61 §10). // Probe handler-лямбды на interrupt VAL — fallback тип W (D61 §10) // когда trailing отсутствует ИЛИ divergent (см. ниже). let trail_ty = match &body.trailing { // A divergent trailing (`throw` / `interrupt` / panic) infers as // `Nova_never*` — which is NOT a real C type. The with-block's value // is never produced through it; its semantic type W comes from the // handler's `interrupt VAL`. Without this, `result_decl_ty` became // `Nova_never*` → CC-FAIL «unknown type name 'Nova_never'». Some(t) => { let mut ty = self.infer_expr_c_type(t); if ty == "Nova_never*" || ty == "Nova_never" || self.expr_diverges_125(t) { ty = self.probe_handler_ty(bindings); } else if matches!(&t.kind, ExprKind::Ident(n) if n == "None") { // [M-exp-promotion-blockers: url tuple-shape mono conflict, // sibling Option case] mirror of the identical fix in // `infer_expr_c_type`'s `ExprKind::With` arm — a bare `None` // trailing's element type is only recoverable from a // sibling `interrupt Some(e)` handler. let probed = self.probe_handler_ty(bindings); if probed.starts_with("NovaOpt_") { ty = probed; } } else if let ExprKind::Call { func, args, .. } = &t.kind { // [M-exp-promotion-blockers: retry E_UNUSED_PREFIX_TYPEVAR] // a bare `Ok(x)`/`Err(x)` trailing has its OTHER Result[T,E] // side defaulted by `infer_expr_c_type`'s generic-variant // channel (`Ok(x)` → err hardcoded to `nova_str`; `Err(x)` → // ok hardcoded to `nova_int`) — that channel has no // visibility into an enclosing `with Fail[E] = ...` binding. // When `E` is a still-generic method-level typevar bound to // something OTHER than the hardcoded guess (e.g. inside // `RetryPolicy@execute[T,E]`, mono'd with E=nova_int), the // with-block's declared type is silently WRONG here — a // later `Err(e)` match-arm (typed via the correct // current_type_subst-driven handler-param override in // `emit_handler_lit`) produces a genuinely different // concrete Result C-type, and the two disagree at compile // time (`NovaRes_<T>_nova_str` vs the real `NovaRes_<T>_ // <E>`). Recover the real `E` from the `Fail[E]` binding // here — the one place with both `current_type_subst` AND // visibility into which Result side is ambiguous. if let ExprKind::Ident(name) = &func.kind { if (name == "Ok" || name == "Err") && args.len() == 1 { if let Some(real_e_c) = self.active_fail_e_hint(bindings) { let arg_c = self.infer_expr_c_type(args[0].expr()); let arg_c = if arg_c.is_empty() || arg_c == "void*" { "nova_int".to_string() } else { arg_c }; let (ok_c, err_c) = if name == "Ok" { (arg_c, real_e_c) } else { (real_e_c, arg_c) }; ty = self.result_repr_c_type(&ok_c, &err_c); } } } } ty } None => self.probe_handler_ty(bindings), }; let category = with_result_category(&trail_ty); // Emit interrupt frame so `interrupt v` can early-exit this with-block let iframe = self.fresh_tmp(); let result_tmp = self.fresh_tmp(); self.line(&format!("NovaInterruptFrame {};", iframe)); // Declare result_tmp with the actual trail type (not always nova_int). let result_decl_ty = match category { WithResultCategory::IntLike => "nova_int".to_string(), WithResultCategory::Pointer => trail_ty.clone(), WithResultCategory::ValueStruct => trail_ty.clone(), WithResultCategory::UnitVoid => "nova_int".to_string(), }; self.line(&format!("{} {};", result_decl_ty, result_tmp)); self.line(&format!("nova_interrupt_push(&{});", iframe)); // Plan 61 followup #1: attach owner_iframe для Fail-shaped handlers, // чтобы handler-arm `interrupt v` resolve'тся в OUR with-block, не в // _nova_interrupt_top (который может быть inner nested with-block при // cross-effect throw). См. nova_interrupt() в effects.c — он сначала // смотрит _nova_current_handler_iframe set'нутый dispatcher'ом. // // Plan 61 followup #4: same для per-E vtables (`Fail_<E_mangled>` // entries — dual-install per Fail[E] binding). for (effect_name, _prev_var, handler_val) in &saves { if effect_name == "Fail" || effect_name.starts_with("Fail_") { self.line(&format!( "{hv}->owner_iframe = &{iframe};", hv = handler_val, iframe = iframe )); } } // If we have fail-frame, wrap interrupt-setjmp inside fail-setjmp. if let Some(ff) = &fframe { self.line(&format!("if (setjmp({ff}.jmp) == 0) {{", ff = ff)); self.indent += 1; } self.line(&format!("if (setjmp({iframe}.jmp) == 0) {{", iframe = iframe)); self.indent += 1; // Body executes in the normal path self.line("{"); self.indent += 1; // Emit block statements with defer scope; if there's a trailing expr use it as the int result // [M-exp-promotion-blockers: retry] scope `current_fail_e_hint` to this // with-block's body emission — a nested `Ok(x)`/`Err(x)` ctor call // (e.g. `emit_call`'s Result-variant dispatch) reads it when its own // arg-only inference can't see the OTHER Result[T,E] side. let new_fail_e_hint = self.active_fail_e_hint(bindings); let saved_fail_e_hint = std::mem::replace(&mut self.current_fail_e_hint, new_fail_e_hint); let with_block_id = self.enter_defer_scope(body, false); for stmt in &body.stmts { self.emit_stmt(stmt)?; } if let Some(trailing) = &body.trailing { self.emit_source_annotation_for_expr(trailing); // Plan 217: `with X = ... { consume g = e; g.close() }` — the // handler-scope body's own trailing can be a bare consuming // receiver-call, same as any other block-trailing choke point. self.disarm_auto_cleanup_receiver_call(trailing); // [M-exp-promotion-blockers: url tuple-shape mono conflict, // sibling Option case] a bare `None` trailing's OWN emission // (`emit_expr`) doesn't know the sibling `interrupt Some(e)` // hint either — it would default to the generic `NovaOpt_ // nova_int` None literal, mismatching the now-correctly- // resolved `result_decl_ty`/`category` above. Construct the // properly-typed None literal directly instead of going // through the generic (context-blind) `emit_expr` path. let tv = if matches!(&trailing.kind, ExprKind::Ident(n) if n == "None") && result_decl_ty.starts_with("NovaOpt_") { self.option_none_expr(result_decl_ty.trim_start_matches("NovaOpt_")) } else { self.emit_expr(trailing)? }; match category { WithResultCategory::IntLike => { self.line(&format!("{} = (nova_int)({});", result_tmp, tv)); } WithResultCategory::Pointer | WithResultCategory::ValueStruct => { self.line(&format!("{} = ({});", result_tmp, tv)); } WithResultCategory::UnitVoid => { self.line(&format!("(void)({});", tv)); self.line(&format!("{} = ((nova_int)0LL);", result_tmp)); } } } else { match category { WithResultCategory::IntLike | WithResultCategory::UnitVoid => { self.line(&format!("{} = ((nova_int)0LL);", result_tmp)); } WithResultCategory::Pointer => { self.line(&format!("{} = NULL;", result_tmp)); } WithResultCategory::ValueStruct => { self.line(&format!("{} = ({}){{0}};", result_tmp, result_decl_ty)); } } } self.leave_defer_scope(with_block_id); self.current_fail_e_hint = saved_fail_e_hint; self.indent -= 1; self.line("}"); self.indent -= 1; self.line("} else {"); self.indent += 1; // Interrupt path: read from the slot matching the category. match category { WithResultCategory::IntLike | WithResultCategory::UnitVoid => { self.line(&format!("{} = {iframe}.value;", result_tmp, iframe = iframe)); } WithResultCategory::Pointer => { self.line(&format!("{} = ({}){iframe}.value_ptr;", result_tmp, result_decl_ty, iframe = iframe)); } WithResultCategory::ValueStruct => { // value_ptr holds heap-allocated slot of the value struct. self.line(&format!( "{} = *(({}*){iframe}.value_ptr);", result_tmp, result_decl_ty, iframe = iframe)); } } self.indent -= 1; self.line("}"); // Close fail-frame outer if we opened it if let Some(ff) = &fframe { self.indent -= 1; self.line("} else {"); self.indent += 1; // Plan 173 Ф.2.C: fail-frame caught a throw. Раньше здесь был // дублированный per-kind dispatch (if CANCEL {…throw_cancel…} / // if PANIC {…rethrow…} / USER default) — класс дефекта «кадр забыл // kind» ([M-172-with-fail-swallows-panic]). Теперь: пометить `caught` // + result=default; ЕДИНЫЙ terminal-transport (nova_scope_exit, // policy=CATCH) выполняется ПОСЛЕ общего pop/restore epilogue ниже. // USER/USER_TYPED — handler отработал, result=default (helper вернётся); // PANIC/CANCEL — helper re-throw'ит нагору (longjmp), result не важен. let cv = caught_var.as_ref().expect("caught_var present when fframe is"); self.line(&format!("{} = 1;", cv)); match category { WithResultCategory::IntLike | WithResultCategory::UnitVoid => { self.line(&format!("{} = ((nova_int)0LL);", result_tmp)); } WithResultCategory::Pointer => { self.line(&format!("{} = NULL;", result_tmp)); } WithResultCategory::ValueStruct => { self.line(&format!("{} = ({}){{0}};", result_tmp, result_decl_ty)); } } self.indent -= 1; self.line("}"); // [race-state-dump 2026-07-13] `nova_fail_pop()` here is WRONG: // it assumes `_nova_fail_top == &ff` (single-level pop), which only // holds when the throw was caught DIRECTLY at this with-block's own // setjmp. When the body called into nested Fail-signature functions // (each pushing its OWN NovaFailFrame deeper on the C stack) and the // handler recovered via `interrupt` (nova_interrupt() in effects.c // longjmps straight to the nearest NovaInterruptFrame — see D61 // comment there — WITHOUT touching `_nova_fail_top`), those nested // frames' own `nova_fail_pop()` epilogues never ran (their C stack // was discarded by the longjmp). `_nova_fail_top` is then left // dangling at one of those now-dead stack frames. A naive // `nova_fail_pop()` here walks ONE level from that dangling pointer // instead of restoring the true pre-entry value, so the corruption // survives this with-block and (in a merged multi-test-block C // process) later gets treated as a live NovaFailFrame — the next // throw writes its error fields through the dangling pointer // (corrupting whatever unrelated local/test now occupies that stack // slot) and longjmps to a garbage `jmp_buf` (crash at a // composition-dependent point). `ff.prev` was captured ONCE at // `nova_fail_push(&ff)` time and never mutated since — restoring // directly to it is a correct hard-reset in EVERY case (identical // to the old single-level pop when `_nova_fail_top == &ff` still // holds, and self-healing when it doesn't). self.line(&format!("_nova_fail_top = {}.prev;", ff)); } // Restore handlers (regardless of path) for (effect_name, prev_var, _hv) in saves.iter().rev() { self.line(&format!("_nova_handler_{eff} = {prev};", eff = effect_name, prev = prev_var)); } self.line("nova_interrupt_pop();"); // Plan 173 Ф.2.C: ЕДИНАЯ точка терминальной политики (CATCH). Выполняется // ПОСЛЕ pop(fail) + restore(handlers) + pop(interrupt) — site-специфичный // пролог сделан (контракт nova_scope_exit). Для USER/USER_TYPED helper // возвращается (result уже = default); для PANIC/CANCEL — re-throw нагору // (longjmp), минуя finalizer-fire ниже (сохраняет прежний порядок: // CANCEL/PANIC re-throw НЕ запускал Application-finalizer'ы). Порядок // restore-vs-interrupt_pop vs прежним (pop→intpop→restore) не наблюдаем — // независимые TLS-слоты. if let Some(ff) = &fframe { let cv = caught_var.as_ref().expect("caught_var present when fframe is"); self.line(&format!("if ({}) {{ nova_scope_exit(&{}, NOVA_SCOPE_EXIT_CATCH); }}", cv, ff)); } // Plan 110.9.3 V1.1 [M-110.9.3-register-finalizer-lifo]: fire // finalizers LIFO + restore prev TLS. Runs unconditionally на // both normal completion AND throw path (D195 spec). Already // past the throw-catch logic above — at this point we either // completed normally OR caught a throw that was handled. if let (Some(fs_var), Some(prev_var)) = (&application_fs_var, &application_fs_prev) { self.line(&format!( "nova_finalizer_fire_lifo(&{fs}); /* Plan 110.9.3 V1.1 LIFO fire */", fs = fs_var )); self.line(&format!( "_nova_active_finalizer_stack = {prev};", prev = prev_var )); } // Plan 174 (D349): restore the with-entry active scope (see snapshot at // the top). Runs on both normal completion and handled-throw paths. if let Some(sv) = &active_scope_save { self.line(&format!("_nova_active_scope = {};", sv)); } Ok(result_tmp) } /// Plan 19, C8 codegen (D31-rev): desugar handler-лямбды /// `with EffectName = |args| body` в синтетический HandlerLit /// с одной операцией. Возвращает Some((effect-path, synthetic /// HandlerLit-Expr)) если binding.handler — closure-light/full; /// иначе None (вызывающий код использует обычный emit_expr). /// /// Логика: /// 1. Если handler не закрытие — None. /// 2. Вытащить effect-name path. /// 3. Из effect_schemas найти единственную операцию эффекта /// (если операций > 1, возвращаем None — компилятор type-checker /// эту ситуацию обнаружит на seamntic-стадии; здесь fallback /// на обычный emit, который выдаст codegen-error). /// 4. Синтезировать HandlerMethod с params/body из closure'а. /// 5. Обернуть в ExprKind::HandlerLit. fn desugar_handler_lambda( &self, effect: &TypeRef, handler: &Expr, ) -> Result<Option<(Vec<String>, Expr)>, String> { let effect_path = match effect { TypeRef::Named { path, .. } => path.clone(), _ => return Ok(None), }; let eff_key = effect_path.join("_"); // Plan 61 Ф.3: typed Fail[E] inference. Если effect = Fail[E] (path // = ["Fail"], generics = [E]), inject E как inferred annotation для // первого handler-arm param. Это позволяет писать `with Fail[E] = // |e| ...` без явного `|e: E|` — компилятор infer'ит из effect type. let fail_e_inferred_ty: Option<crate::ast::TypeRef> = if effect_path.as_slice() == ["Fail"] { if let TypeRef::Named { generics, .. } = effect { generics.first().cloned() } else { None } } else { None }; // Извлекаем params и body в форме HandlerMethod. let (handler_params, handler_body): ( Vec<HandlerMethodParam>, HandlerMethodBody, ) = match &handler.kind { ExprKind::ClosureLight { params, body } => { let p: Vec<HandlerMethodParam> = params .iter() .enumerate() .map(|(idx, cp)| HandlerMethodParam { name: cp.name.clone(), // Plan 61 Ф.3: inject inferred E annotation для // первого param если Fail[E]. ty: if idx == 0 { fail_e_inferred_ty.clone() } else { None }, span: cp.span, }) .collect(); let b = match body { crate::ast::ClosureBody::Expr(e) => HandlerMethodBody::Expr((**e).clone()), crate::ast::ClosureBody::Block(blk) => HandlerMethodBody::Block(blk.clone()), }; (p, b) } ExprKind::ClosureFull(sb) => { let p: Vec<HandlerMethodParam> = sb .params .iter() .map(|fp| HandlerMethodParam { name: fp.name.clone(), ty: Some(fp.ty.clone()), span: fp.span, }) .collect(); let b = match &sb.body { FnBody::Expr(e) => HandlerMethodBody::Expr(e.clone()), FnBody::Block(blk) => HandlerMethodBody::Block(blk.clone()), FnBody::External => return Ok(None), }; (p, b) } _ => return Ok(None), }; // Находим единственную операцию эффекта. let op_name = if let Some(schema) = self.effect_schemas.get(&eff_key) { if schema.len() != 1 { // Не sugar-applicable: > 1 операции или 0. return Ok(None); } schema.keys().next().cloned().unwrap_or_default() } else { // Эффект не в schemas (ещё не был обработан) — // безопасный fallback: пропустить sugar, обычный emit_expr // даст более понятную диагностику. return Ok(None); }; // Синтезируем HandlerMethod с захваченным span (берём span // самого handler-выражения — он покрывает всё закрытие). let synth_method = HandlerMethod { name: op_name, params: handler_params, // Plan 175.2 Ф.2-v4 (П4): synthesized post-checker (D31 lambda- // sugar desugar) — never walked by `check_handler_op_declarations`, // so the mandatory-`ret_ty` rule doesn't apply here. Codegen // resolves the real return type from `effect_schemas` regardless. ret_ty: None, body: handler_body, span: handler.span, }; // Оборачиваем в HandlerLit. let lit_expr = Expr::new( ExprKind::HandlerLit { effect_name: effect_path.clone(), methods: vec![synth_method], }, handler.span, ); Ok(Some((effect_path, lit_expr))) } fn emit_handler_lit( &mut self, effect_name: &[String], methods: &[HandlerMethod], ) -> Result<String, String> { let eff = effect_name.join("_"); // Collect method return types from effect schema let schema = self.effect_schemas.get(&eff).cloned() .unwrap_or_default(); // Use handler_counter for a stable, predictable ID (not tmp_counter) // so the pre-scan can emit forward decls with matching names. let handler_id = format!("_nova_handler_lit_{}", self.handler_counter); self.handler_counter += 1; // ---- Collect free variables referenced in method bodies ---- // We do a simple name-scan: any Ident in the body that is in var_types // and is not a parameter of the method itself is a captured variable. let mut all_captures: Vec<(String, String)> = Vec::new(); // (name, c_type) for m in methods { let method_param_names: std::collections::HashSet<String> = m.params.iter().map(|p| p.name.clone()).collect(); // Plan 91 Ф.4: names bound *inside* this op body (let/mut, for-pattern, // match-arm, if-let, while-let) are locals of the op, NOT free variables of // the enclosing factory fn — they must not be captured. Without this, a stale // entry in the flat `var_types` map (e.g. `i` left behind by a previously-emitted // std.sort fn) makes an op-body local wrongly captured, producing both a bogus // ctx-field capture (no such local in the factory) and a var_boxed/local-copy // unpack that collides with the genuine op-body `nova_int i`. Mirror emit_spawn/detach/blocking, // which already subtract bound names via collect_bound_names_*. PER-method: a name // bound-local in one op may legitimately be an enclosing capture used by another op. let mut bound: std::collections::HashSet<String> = std::collections::HashSet::new(); match &m.body { HandlerMethodBody::Expr(e) => Self::collect_bound_names_expr(e, &mut bound), HandlerMethodBody::Block(b) => Self::collect_bound_names_block(b, &mut bound), } let refs = Self::collect_idents_in_handler_method(m); for name in refs { if method_param_names.contains(&name) { continue; } if bound.contains(&name) { continue; } if all_captures.iter().any(|(n, _)| n == &name) { continue; } if let Some(ty) = self.var_types.get(&name).cloned() { all_captures.push((name, ty)); } } } // ---- Emit context struct type inline (local typedef, valid in C99+) ---- // This goes directly into out (inside the function body) before the vtable. // MSVC supports local typedefs in function scope. let ctx_struct = format!("NovaCtx_{}", handler_id); // Plan 175 Ф.2-v2 (common closure-capture path — replaces the old // `#define {cap} (*_c->{cap})` textual-macro aliasing, [M-effect- // handler-body-record-literal] fix-direction): handler-literal // captures now use EXACTLY the mechanism `emit_lambda` uses for // closures (mirrors it 1:1, see emit_lambda's env-struct/var_boxed // comments a few thousand lines below in this file): // * MUTABLE captures: escaping handlers (returned as `Effect[X]` // from a factory fn) heap-promote to a `T*` box (reusing an // already-boxed var if a prior closure/handler in this fn // already promoted it — `self.var_boxed`) and register it in the // OUTER `var_boxed` so `ExprKind::Ident` transparently derefs // `(*box)` afterward — exactly like a closure capture. INLINE // handlers (`with X = effect {…} { body }`) instead take // `&cap_name` directly (no box, no outer `var_boxed` write) — // `emit_with` wraps handler-construction in its own nested C // block, so a box's C VARIABLE DECLARATION would itself go out // of scope at the end of that `with`, corrupting any later read // of `cap_name` ([M-175-handler-lit-boxed-var-c-scope-leak]); // `&cap_name` is a bare expression, not a new scoped variable, // and is always sound since `cap_name` was already in scope // before the handler literal. See the per-capture code below for // the exact split. // * IMMUTABLE captures (incl. already-pointer heap refs, e.g. a // captured `Nova_Foo*` local) are a plain BY-VALUE snapshot — // copying a pointer by value already preserves shared-object // mutation visibility; only REASSIGNMENT of the local binding // needs the box, which is exactly what `var_mutable` gates. // Field names are MANGLED (`_nv_fv_<handler_id>_<name>`, matching // emit_lambda's `mangled_field`) so a nested closure/handler literal // built INSIDE an op body never collides with a struct-member token // sharing a captured var's bare name (the exact class of bug the // macro approach required a separate mangling workaround for). let free_var_is_mut: Vec<bool> = all_captures.iter() .map(|(name, _)| self.var_mutable.contains(name)) .collect(); let mangled_field = |name: &str| format!("_nv_fv_{}_{}", handler_id, name); let capture_ptr_tys: Vec<String> = all_captures.iter() .zip(free_var_is_mut.iter()) .map(|((_, cap_ty), &is_mut)| { if is_mut { format!("{}*", cap_ty) } else { cap_ty.clone() } }).collect(); self.line(&format!("typedef struct {{")); for ((cap_name, _), ptr_ty) in all_captures.iter().zip(capture_ptr_tys.iter()) { self.line(&format!(" {} {};", ptr_ty, mangled_field(cap_name))); } if all_captures.is_empty() { self.line(" char _dummy;"); } self.line(&format!("}} {};", ctx_struct)); // ---- Emit context struct typedef into deferred_impls (file scope) ---- // The impl functions (file-scope) also need to know the ctx struct type. let _ = writeln!(self.deferred_impls, "typedef struct {{"); for ((cap_name, _), ptr_ty) in all_captures.iter().zip(capture_ptr_tys.iter()) { let _ = writeln!(self.deferred_impls, " {} {};", ptr_ty, mangled_field(cap_name)); } if all_captures.is_empty() { let _ = writeln!(self.deferred_impls, " char _dummy;"); } let _ = writeln!(self.deferred_impls, "}} {};", ctx_struct); // ---- Emit heap-allocated vtable and context ---- // Heap allocation ensures handlers can be returned from functions safely. let vtable_var = format!("{}_vtable", handler_id); let ctx_var = format!("{}_ctx", handler_id); self.line(&format!( "NovaVtable_{eff}* {vt} = (NovaVtable_{eff}*)nova_alloc(sizeof(NovaVtable_{eff}));", eff = eff, vt = vtable_var )); self.line(&format!( "{ctx_ty}* {ctx} = ({ctx_ty}*)nova_alloc(sizeof({ctx_ty}));", ctx_ty = ctx_struct, ctx = ctx_var )); for ((cap_name, cap_ty), &is_mut) in all_captures.iter().zip(free_var_is_mut.iter()) { let field = mangled_field(cap_name); if is_mut { // Mutable capture. Two cases ([M-175-handler-lit-boxed-var- // c-scope-leak] — found via spec_tests/conformance/repro_ // matrix.nv's two-level nested-handler-capture fixture): // // (a) ESCAPING handler — the literal is the return value of a // factory fn (`fn f() -> Effect[X] { … effect X {…} }`). // `&stack_local` would dangle once the factory returns, so // UNCONDITIONALLY heap-promote (mirrors emit_lambda's // closure-capture box logic) — reuse an existing box if // `cap_name` was already promoted by an earlier closure/ // handler in this same enclosing fn (`self.var_boxed`), // else allocate one now, and register it in the OUTER // `var_boxed` so later reads/writes of `cap_name` in the // enclosing fn stay synced with the handler's mutations // (exactly like a closure capture). // // (b) INLINE handler (`with X = effect X {…} { body }`, NOT // returned) — `&cap_name` directly, NO box. This matters // even though the box's ALLOCATION would be heap-durable: // `emit_with` wraps handler-construction in its OWN // nested C block (the interrupt-frame machinery for // `with`), so a NEW `_box_<cap>` C VARIABLE declared // there is itself block-scoped and goes out of C scope // once that `with` closes — but `cap_name`'s Nova-level // `mut` binding (and any use of it AFTER the `with` // block, or by a SIBLING `with` at the same level) must // keep working. `&cap_name` sidesteps this entirely: it's // just an address-of expression, no new scoped variable, // and it is always sound because `cap_name`'s OWN `mut` // declaration necessarily lives in a C scope that // outlives this `with` (Nova's binding rules — a name // can only be captured if it was already in scope before // the handler literal). Do NOT touch the OUTER // `var_boxed` here: `cap_name` remains a plain local for // the rest of the enclosing fn — nothing about its // storage changed. let handler_escapes = self.current_fn_return_ty.as_deref() .map_or(false, |t| t.starts_with("NovaVtable_")); let field_val = if handler_escapes { if let Some(existing) = self.var_boxed.get(cap_name) { existing.clone() } else { let bv = format!("_box_{}", cap_name); self.line(&format!( "{ty}* {bv} = ({ty}*)nova_alloc(sizeof({ty}));", ty = cap_ty, bv = bv)); self.line(&format!("*{bv} = {cap};", bv = bv, cap = cap_name)); self.var_boxed.insert(cap_name.clone(), bv.clone()); bv } } else { format!("&{}", cap_name) }; self.line(&format!("{ctx}->{field} = {field_val};", ctx = ctx_var, field = field, field_val = field_val)); } else { // Immutable capture (incl. already-pointer heap refs): plain // by-value snapshot. `cap_name` here can never itself be // var_boxed (only mutable vars are ever registered there), // so the bare read is always the correct current value. self.line(&format!("{ctx}->{field} = {cap};", ctx = ctx_var, field = field, cap = cap_name)); } } // Patch vtable at runtime — use mangled field name for overloaded ops self.line(&format!("{vt}->ctx = {ctx};", vt = vtable_var, ctx = ctx_var)); for m in methods { let fn_name = format!("{}_impl_{}_{}", handler_id, eff, m.name); // [M-exp-promotion-blockers: retry E_UNUSED_PREFIX_TYPEVAR] // guaranteed-correct forward decl for THIS use site, written to // `lambda_forward_decls` (file-scope buffer, flushed right // before the CURRENT fn/test/mono'd-method body — see // `emit_test`/`emit_monomorphized_method`), NOT `deferred_impls` // (spliced ONCE, after every function — too late for a use // inside the very function that needs it) and NOT an inline // block-scope decl (C forbids `static` storage class on a // function declared at block scope). // // The module pre-scan (`emit_handler_forward_decls`) predicts // exactly ONE handler_counter slot per SOURCE `with Fail[...]` // occurrence — sound for non-generic code, but a handler literal // inside a GENERIC method body (e.g. `RetryPolicy@execute[T,E]`'s // `with Fail[E] = |e| interrupt Err(e) {...}`) is emitted ONCE // PER MONOMORPHIZATION (once per distinct (T,E) call-site), each // allocating a NEW handler_counter id — only the FIRST instance // (by coincidence of counter alignment) lands on the pre-scan's // single reserved forward-decl; every subsequent instance // (`_nova_handler_lit_6_impl_Fail_fail` for retry's SECOND // (T,E) pair) had no forward decl at all → CC-FAIL "undeclared // identifier". A redundant extra forward-decl here (C allows // repeated compatible declarations) is unconditionally correct // regardless of whether the pre-scan already covered this one. { let (param_types, ret_ty) = Self::schema_lookup(&schema, &m.name) .cloned() .unwrap_or_else(|| (vec![], "nova_unit".into())); let mut decl_params = vec!["void* _ctx".to_string()]; for (i, p) in m.params.iter().enumerate() { let ty = param_types.get(i).cloned().unwrap_or_else(|| "nova_int".into()); decl_params.push(format!("{} {}", ty, p.name)); } let _ = writeln!(self.lambda_forward_decls, "{storage}{ret} {fn}({params});", storage = self.top_level_storage(), ret = ret_ty, fn = fn_name, params = decl_params.join(", ") ); } // Resolve mangled vtable field: look up by plain name in schema, // find the matching key (mangled or plain). let field = { let schema_snap = self.effect_schemas.get(&eff).cloned().unwrap_or_default(); // Find the schema key whose prefix matches m.name let mangled_key = schema_snap.keys() .find(|k| *k == &m.name || k.starts_with(&format!("{}__", m.name))) .cloned() .unwrap_or_else(|| m.name.clone()); mangled_key }; // Plan 175 Ф.3 (D316 typed retype): `NovaVtable_Time`'s // sleep/now/now_monotonic slots are raw-int64-nanos WIRE // (effects.h — hand-written struct can't name a per-CU typed // value-record), while THIS handler-literal's op body // (`fn_name`) is schema-driven typed (`NovaValue_Duration`/ // `Timestamp`/`Monotonic`, same as any user handler-literal // body). Installing `fn_name` DIRECTLY into the vtable slot // would be a function-pointer-signature mismatch (by-value // struct param/return vs raw int64 — different, no // ABI-compatible reinterpretation). Generate a thin static // marshalling THUNK with the WIRE signature that wraps/unwraps // at the one call boundary, and install THAT instead — mirror // image of the dispatch-fn-side marshalling in // `emit_effect_type`. let install_fn = if eff == "Time" && matches!(field.as_str(), "sleep" | "now" | "now_monotonic") { let thunk_name = format!("{}_time_wire_{}", handler_id, m.name); if field == "sleep" { let _ = writeln!(self.lambda_forward_decls, "{storage}nova_unit {thunk}(void* _ctx, int64_t nanos);", storage = self.top_level_storage(), thunk = thunk_name); let _ = writeln!(self.deferred_impls, "{storage}nova_unit {thunk}(void* _ctx, int64_t nanos) {{ \ NovaValue_Duration _nv_d = {{ .nanos = nanos }}; return {fn}(_ctx, _nv_d); }}", storage = self.top_level_storage(), thunk = thunk_name, fn = fn_name); } else { let ret_ty = if field == "now" { "NovaValue_Timestamp" } else { "NovaValue_Monotonic" }; let _ = writeln!(self.lambda_forward_decls, "{storage}int64_t {thunk}(void* _ctx);", storage = self.top_level_storage(), thunk = thunk_name); let _ = writeln!(self.deferred_impls, "{storage}int64_t {thunk}(void* _ctx) {{ {ret} _nv_r = {fn}(_ctx); return _nv_r.nanos; }}", storage = self.top_level_storage(), thunk = thunk_name, ret = ret_ty, fn = fn_name); } thunk_name } else { fn_name.clone() }; self.line(&format!("{vt}->{field} = {install_fn};", vt = vtable_var, field = field, install_fn = install_fn)); } // Plan 20 Ф.8 (4): vtable->prev initialized to NULL here. // Будет перезаписан в `with X = h { ... }` codegen перед install'ом // (см. emit_with: `h->prev = _nova_handler_X; _nova_handler_X = h;`). // Для эффектов БЕЗ `prev` поля (не Fail-shaped) это noop — мы // эмитим `prev = NULL` только для встроенных Fail-like vtables. // Bootstrap-stage: hardcoded на эффект "Fail" — единственный с // prev в runtime. if eff == "Fail" { self.line(&format!("{vt}->prev = NULL;", vt = vtable_var)); // Plan 61 followup #1: owner_iframe initialized в NULL; emit_with // перезапишет (`{hv}->owner_iframe = &iframe`) после iframe // allocation. handler-arm `interrupt v` использует этот frame. self.line(&format!("{vt}->owner_iframe = NULL;", vt = vtable_var)); } // ---- Emit forward declarations into deferred_impls (file scope) ---- for m in methods { let (param_types, ret_ty) = Self::schema_lookup(&schema, &m.name) .cloned() .unwrap_or_else(|| (vec![], "nova_unit".into())); let mut fn_params = vec!["void* _ctx".to_string()]; for (i, p) in m.params.iter().enumerate() { let ty = param_types.get(i).cloned().unwrap_or_else(|| "nova_int".into()); fn_params.push(format!("{} {}", ty, p.name)); } let fn_name = format!("{}_impl_{}_{}", handler_id, eff, m.name); let _ = writeln!(self.deferred_impls, "{storage}{ret} {fn}({params});", storage = self.top_level_storage(), ret = ret_ty, fn = fn_name, params = fn_params.join(", ") ); } // Return pointer to vtable (caller installs it) let result_ptr = self.fresh_tmp(); self.line(&format!( "NovaVtable_{eff}* {res} = {vt};", eff = eff, res = result_ptr, vt = vtable_var )); // ---- Emit impl function bodies into DEFERRED file-scope buffer ---- // We need to temporarily redirect emit_expr output to the deferred buffer. // Strategy: swap out/indent, emit, swap back. let saved_out = std::mem::take(&mut self.out); let saved_indent = self.indent; self.indent = 0; for m in methods { let (param_types, ret_ty) = schema.get(&m.name) .cloned() .unwrap_or_else(|| (vec![], "nova_unit".into())); let mut fn_params = vec!["void* _ctx".to_string()]; let mut method_param_types: Vec<(String, String)> = Vec::new(); // Plan 65 Ф.1: track params whose user-annotated type differs from // the effect schema — handler body needs a reinterpret-cast bridge // so `d.nanos` (Nova source) emits `((Nova_Duration*)d)->nanos`. // // Restrictions: // * Skip Fail effect — handled by the existing `fail_e_map` // mechanism below (Plan 61 Ф.3) which uses fail-frame payload // instead of a wire cast. // * Only apply when the schema wire type is a scalar pointer-able // primitive (`nova_int`) so the `(intptr_t)` round-trip is // well-defined; struct wire types (e.g. `nova_str`) can't be // casted via intptr_t. let mut annotation_bridges: Vec<(String, String, String)> = Vec::new(); for (i, p) in m.params.iter().enumerate() { let schema_ty = param_types.get(i).cloned() .unwrap_or_else(|| "nova_int".into()); let bridge_eligible = eff != "Fail" && schema_ty == "nova_int"; let ty = if bridge_eligible { if let Some(annot) = &p.ty { match self.type_ref_to_c(annot) { Ok(annot_c) if annot_c != schema_ty && (annot_c.ends_with('*') || annot_c.starts_with("Nova_")) => { annotation_bridges.push(( p.name.clone(), schema_ty.clone(), annot_c.clone(), )); annot_c } _ => schema_ty.clone(), } } else { schema_ty.clone() } } else { schema_ty.clone() }; let (wire_ty, wire_name) = if annotation_bridges.iter().any(|(n, _, _)| n == &p.name) { (schema_ty.clone(), format!("{}_wire", p.name)) } else { (ty.clone(), p.name.clone()) }; fn_params.push(format!("{} {}", wire_ty, wire_name)); method_param_types.push((p.name.clone(), ty)); } let fn_name = format!("{}_impl_{}_{}", handler_id, eff, m.name); // Emit function signature + ctx unpacking into self.out (which we'll move to deferred) self.line(&format!( "{storage}{ret} {fn}({params}) {{", storage = self.top_level_storage(), ret = ret_ty, fn = fn_name, params = fn_params.join(", ") )); self.indent += 1; // Register method params in var_types so infer_expr_c_type works inside the body let saved_params: Vec<(String, Option<String>)> = method_param_types.iter() .map(|(n, t)| (n.clone(), self.var_types.insert(n.clone(), t.clone()))) .collect(); // Plan 61 Ф.3: typed Fail[E] handler-arm population. Если effect // is Fail и handler-arm param has explicit annotation `e: E` где // E ≠ nova_str (resolved C-type), populate fail_e_map для всех // Ident'ов name → resolve через typed payload в fail-frame. // // Detection: m.params[i].ty (typed via ClosureFull) — explicit // annotation. Schema-resolved type для Fail всегда "nova_str" // (hardcoded в effects.h NovaVtable_Fail). User annotation // отменяет schema — это intent typed handling. let mut saved_fail_e_entries: Vec<(String, Option<String>)> = Vec::new(); if eff == "Fail" { for (i, p) in m.params.iter().enumerate() { if let Some(annotated_ty) = &p.ty { if let Ok(annot_c) = self.type_ref_to_c(annotated_ty) { if annot_c != "nova_str" { // Register typed binding. let pname = p.name.clone(); let prev = self.fail_e_map.insert( pname.clone(), annot_c.clone()); saved_fail_e_entries.push((pname.clone(), prev)); // Также update var_types для infer_expr_c_type // (метод-param сейчас зарегистрирован как // nova_str из schema; перерегистрируем как E_c). self.var_types.insert(pname, annot_c); let _ = i; /* подавляем clippy */ } } } } } // Plan 175 Ф.2-v2: this op body is its own C function — isolate // `var_boxed` (mirrors emit_lambda / emit_monomorphized_method's // `[M-mono-method-var-boxed-leak]` fix) so an unrelated boxed // name from the enclosing fn or a PREVIOUS op's body iteration // never leaks in, and so THIS op's captures don't leak to the // next op's body. Restored below alongside var_types/fail_e_map. let saved_var_boxed = std::mem::take(&mut self.var_boxed); // Unpack context: expose captured variables so body code can use // them directly — common closure-capture path (no `#define`): // mutable captures register `_c->field` in `var_boxed` so // `ExprKind::Ident` auto-derefs; immutable captures get a plain // local copy under the bare name (fresh local, own C-function // scope — no outer macro can be active to corrupt it). self.line(&format!("{ctx}* _c = ({ctx}*)_ctx;", ctx = ctx_struct)); for ((cap_name, cap_ty), &is_mut) in all_captures.iter().zip(free_var_is_mut.iter()) { let field = mangled_field(cap_name); if is_mut { self.var_boxed.insert(cap_name.clone(), format!("_c->{}", field)); } else { self.line(&format!("{} {} = _c->{};", cap_ty, cap_name, field)); } } // Plan 65 Ф.1: rebind annotation-bridged handler params from the // wire-typed C arg (schema int) to the user-annotated record // pointer. Body code refers to the param by name, so we shadow // the wire-arg with a same-name pointer alias. // // Why this matters: Time effect schema has `sleep(int)`, but // mock handlers want to receive `Duration`. The call site emits // the Duration pointer cast to int (intptr_t), so we reverse // the cast in the body. for (pname, _wire_ty, annot_ty) in &annotation_bridges { self.line(&format!( "{annot} {pname} = ({annot})(intptr_t)({pname}_wire);", annot = annot_ty, pname = pname )); } // Plan 175 handler-annot («один канал»): оп-тело — отдельная C-функция // со СВОЕЙ сигнатурой (ret_ty из effect-схемы = той же разметки, что // видел чекер), но до этого фикса эмитилось с type-контекстом ВНЕШНЕЙ // функции: `expected_record_type`/`current_fn_return_ty` оставались от // enclosing fn. Анонимный record-литерал (D55, `make() => { x: 1 }`) // в теле опа падал «anonymous record literal without spread not // supported in codegen» (задокументировано в D316-amend Ф.2), а // `return X` coercion читал чужой return-тип. Подводим ТОТ ЖЕ канал, // что и emit_fn_body (:20826) / lambda (:15807) / protocol-method // (:21619) — потребитель один, инференция не дублируется. // `contracts_post_label` гасим: `return` в оп-теле не должен emit'ить // `goto` на ensures-лейбл внешней функции (лейбл вне этой C-функции). let saved_op_ret_ty = std::mem::replace( &mut self.current_fn_return_ty, Some(ret_ty.clone())); let saved_op_expected = std::mem::replace( &mut self.expected_record_type, Self::debt_struct_name_from_c_type(&ret_ty)); let saved_op_post_label = self.contracts_post_label.take(); match &m.body { HandlerMethodBody::Expr(e) => { let v = self.emit_expr(e)?; if ret_ty == "nova_unit" { self.line(&format!("(void)({}); return NOVA_UNIT;", v)); } else if v == "NOVA_UNIT" { // Body was interrupt/throw (unreachable); emit a type-correct zero. let zero = Self::zero_literal_for_type(&ret_ty); self.line(&format!("return {};", zero)); } else { self.line(&format!("return {};", v)); } } HandlerMethodBody::Block(b) => { let hm_block_id = self.enter_defer_scope(b, false); for stmt in &b.stmts { self.emit_stmt(stmt)?; } let last_is_return = b.stmts.last() .map(|s| matches!(s, Stmt::Return { .. })) .unwrap_or(false); if let Some(trailing) = &b.trailing { let v = self.emit_expr(trailing)?; self.leave_defer_scope(hm_block_id); if ret_ty == "nova_unit" { self.line(&format!("(void)({}); return NOVA_UNIT;", v)); } else if v == "NOVA_UNIT" { // Trailing was interrupt/throw (unreachable); emit type-correct zero. let zero = Self::zero_literal_for_type(&ret_ty); self.line(&format!("return {};", zero)); } else { self.line(&format!("return {};", v)); } } else if last_is_return { self.leave_defer_scope(hm_block_id); // Explicit return already emitted — no additional return needed. } else if ret_ty == "nova_unit" { self.leave_defer_scope(hm_block_id); self.line("return NOVA_UNIT;"); } else { self.leave_defer_scope(hm_block_id); // No trailing expr: body likely ended with interrupt/throw (unreachable). // Emit a zero return to satisfy the C type checker. let zero = Self::zero_literal_for_type(&ret_ty); self.line(&format!("return {};", zero)); } } } // Plan 175 handler-annot: restore enclosing-fn type context. self.current_fn_return_ty = saved_op_ret_ty; self.expected_record_type = saved_op_expected; self.contracts_post_label = saved_op_post_label; // Restore var_boxed (per-op isolation — see take() above; no // `#undef` needed, common closure-capture path uses `var_boxed` // rewriting rather than macros). self.var_boxed = saved_var_boxed; // Restore var_types state for method params for (name, prev) in saved_params { match prev { Some(old) => { self.var_types.insert(name, old); } None => { self.var_types.remove(&name); } } } // Plan 61 Ф.3: restore fail_e_map после handler body emit. for (name, prev) in saved_fail_e_entries { match prev { Some(old) => { self.fail_e_map.insert(name, old); } None => { self.fail_e_map.remove(&name); } } } self.indent -= 1; self.line("}"); self.line(""); } // Move the emitted impl functions into deferred_impls let impl_code = std::mem::replace(&mut self.out, saved_out); self.deferred_impls.push_str(&impl_code); self.indent = saved_indent; Ok(result_ptr) } /// Plan 97.1 Ф.2 (D142): codegen для protocol-литерала /// `protocol Name { method-impl* }`. /// /// Подход A (synthetic concrete + Plan 56 D122 box-pattern): /// 1. Allocate synthetic ctx struct `NovaCtx_<lit_id>` с capture-полями /// (idents, упоминаемые в method bodies но не входящие в method-params). /// 2. Эмитить vtable struct `NovaVtable_<Proto>` через расширенный /// `emit_protocol_box_typedef` (Ф.1; работает с empty type_args). /// 3. Heap-allocate `NovaVtable_<Proto>*` instance + patch field-указатели /// на synthetic free fn methods. /// 4. Эмитить method-bodies как `static <ret> _proto_lit_<id>_impl_<m>(void* _ctx, args) { ... }` /// в `deferred_impls` (file-scope), unpacking captures через `_c->cap`. /// 5. Возврат `NovaBox_<Proto>` fat-pointer `{ .data = ctx, .vtable = vt }`. /// /// Method dispatch на полученный value (`box.method(args)`) уже работает /// через Plan 72 P3-B path (см. emit_call / Member-dispatch). fn emit_protocol_lit( &mut self, proto_path: &[String], methods: &[HandlerMethod], ) -> Result<String, String> { let proto = proto_path.join("_"); let lit_id = self.protocol_lit_counter; self.protocol_lit_counter += 1; let lit_name = format!("_nova_proto_lit_{}", lit_id); let ctx_struct = format!("NovaCtx_{}", lit_name); // ---- Эмитить vtable struct + box typedef через расширенный helper ---- let box_c_type = self.emit_protocol_box_typedef(&proto, &[]) .ok_or_else(|| format!( "protocol `{}` not registered in protocol_method_registry — \ cannot emit literal (Plan 97.1 Ф.2)", proto))?; let vtable_struct = format!("NovaVtable_{}", proto); // ---- Получить method signatures из protocol_method_registry ---- let (_type_params, proto_methods) = self.protocol_method_registry.get(&proto) .ok_or_else(|| format!("protocol `{}` not in registry", proto))? .clone(); // ---- Captures (free vars not in method params; mirrors emit_handler_lit) ---- let mut all_captures: Vec<(String, String)> = Vec::new(); for m in methods { let method_param_names: std::collections::HashSet<String> = m.params.iter().map(|p| p.name.clone()).collect(); let refs = Self::collect_idents_in_handler_method(m); for name in refs { if method_param_names.contains(&name) { continue; } if all_captures.iter().any(|(n, _)| n == &name) { continue; } if let Some(ty) = self.var_types.get(&name).cloned() { all_captures.push((name, ty)); } } } // Plan 97.1 hardening (D142): capture-mode разделение — // * pointer-types (already heap) → by-pointer (alias). // * mutable scalar (`let mut`) → by-pointer (mutation visible). // * immutable scalar (function param / `let`) → **by-value** // snapshot. Это критично для **factory pattern** где literal // возвращается за пределы fn — stack-local pointer бы dangling. // capture_modes[i] == true → by-pointer; false → by-value. let capture_modes: Vec<bool> = all_captures.iter().map(|(name, cap_ty)| { if cap_ty.ends_with('*') { return true; } self.var_mutable.contains(name) }).collect(); let capture_ptr_tys: Vec<String> = all_captures.iter().zip(capture_modes.iter()) .map(|((_, cap_ty), &by_ptr)| { if by_ptr { if cap_ty.ends_with('*') { cap_ty.clone() } else { format!("{}*", cap_ty) } } else { cap_ty.clone() // by-value, storage = cap_ty directly } }).collect(); // ---- Эмитить ctx struct typedef в lambda_forward_decls // (file-scope, splice'ится ДО fn definitions). Это делает ctx struct // visible и в test fn body (где аллокация), и в impl fn body // (file-scope, в deferred_impls после fn). let _ = writeln!(self.lambda_forward_decls, "typedef struct {{"); for ((cap_name, _), ptr_ty) in all_captures.iter().zip(capture_ptr_tys.iter()) { let _ = writeln!(self.lambda_forward_decls, " {} {};", ptr_ty, cap_name); } if all_captures.is_empty() { let _ = writeln!(self.lambda_forward_decls, " char _dummy;"); } let _ = writeln!(self.lambda_forward_decls, "}} {};", ctx_struct); // ---- Heap-allocate vtable + ctx ---- let vtable_var = format!("{}_vtable", lit_name); let ctx_var = format!("{}_ctx", lit_name); self.line(&format!( "{vts}* {vt} = ({vts}*)nova_alloc(sizeof({vts}));", vts = vtable_struct, vt = vtable_var )); self.line(&format!( "{ctx_ty}* {ctx} = ({ctx_ty}*)nova_alloc(sizeof({ctx_ty}));", ctx_ty = ctx_struct, ctx = ctx_var )); for (((cap_name, _), _ptr_ty), &by_ptr) in all_captures.iter() .zip(capture_ptr_tys.iter()).zip(capture_modes.iter()) { if by_ptr { // Pointer-type (heap obj) или mutable scalar — by-pointer. // Для scalar: storage `T*` хранит `&local`. let cap_ty = &all_captures.iter().find(|(n, _)| n == cap_name).unwrap().1; if cap_ty.ends_with('*') { self.line(&format!("{ctx}->{cap} = {cap};", ctx = ctx_var, cap = cap_name)); } else { self.line(&format!("{ctx}->{cap} = &{cap};", ctx = ctx_var, cap = cap_name)); } } else { // By-value snapshot: copy current value. self.line(&format!("{ctx}->{cap} = {cap};", ctx = ctx_var, cap = cap_name)); } } // Patch vtable fields → impl functions. for m in methods { let fn_name = format!("{}_impl_{}", lit_name, m.name); self.line(&format!("{vt}->{m} = {fn};", vt = vtable_var, m = m.name, fn = fn_name)); } // ---- Forward decls of impl functions в lambda_forward_decls ---- // (file-scope, до fn definitions; необходимо чтобы test fn body // могла ссылаться на `_proto_lit_N_impl_m` в vt-patching). for m in methods { // Найти proto method signature для return type + param types. let proto_m = proto_methods.iter().find(|pm| pm.name == m.name); let ret_c = proto_m .and_then(|pm| pm.return_type.as_ref()) .and_then(|rt| self.type_ref_to_c(rt).ok()) .unwrap_or_else(|| "nova_unit".to_string()); let mut fn_params = vec!["void* _ctx".to_string()]; for (i, p) in m.params.iter().enumerate() { let pty = proto_m .and_then(|pm| pm.params.get(i)) .and_then(|pp| self.type_ref_to_c(&pp.ty).ok()) .unwrap_or_else(|| "nova_int".into()); fn_params.push(format!("{} {}", pty, p.name)); } let fn_name = format!("{}_impl_{}", lit_name, m.name); let _ = writeln!(self.lambda_forward_decls, "{storage}{ret} {fn}({params});", storage = self.top_level_storage(), ret = ret_c, fn = fn_name, params = fn_params.join(", ") ); } // ---- Return box value ---- let box_var = self.fresh_tmp(); self.line(&format!( "{box_ty} {box_v} = ({box_ty}){{ .data = (void*){ctx}, .vtable = {vt} }};", box_ty = box_c_type, box_v = box_var, ctx = ctx_var, vt = vtable_var )); // ---- Emit impl function bodies в deferred_impls ---- // Strategy (mirrors emit_handler_lit): swap out, emit, swap back. let saved_out = std::mem::take(&mut self.out); let saved_indent = self.indent; self.indent = 0; for m in methods { let proto_m = proto_methods.iter().find(|pm| pm.name == m.name); let ret_c = proto_m .and_then(|pm| pm.return_type.as_ref()) .and_then(|rt| self.type_ref_to_c(rt).ok()) .unwrap_or_else(|| "nova_unit".to_string()); let mut fn_params = vec!["void* _ctx".to_string()]; let mut method_param_types: Vec<(String, String)> = Vec::new(); for (i, p) in m.params.iter().enumerate() { let pty = proto_m .and_then(|pm| pm.params.get(i)) .and_then(|pp| self.type_ref_to_c(&pp.ty).ok()) .unwrap_or_else(|| "nova_int".into()); fn_params.push(format!("{} {}", pty, p.name)); method_param_types.push((p.name.clone(), pty)); } let fn_name = format!("{}_impl_{}", lit_name, m.name); self.line(&format!( "{storage}{ret} {fn}({params}) {{", storage = self.top_level_storage(), ret = ret_c, fn = fn_name, params = fn_params.join(", ") )); self.indent += 1; // Register method params in var_types для infer_expr_c_type. let saved_params: Vec<(String, Option<String>)> = method_param_types.iter() .map(|(n, t)| (n.clone(), self.var_types.insert(n.clone(), t.clone()))) .collect(); // Unpack context: macros для capture-доступа. // Plan 97.1 hardening: capture_modes управляет deref'ом — // by-pointer (heap obj / mut scalar) — pointer access; // by-value (immutable scalar snapshot) — direct field access. self.line(&format!("{ctx}* _c = ({ctx}*)_ctx;", ctx = ctx_struct)); for ((cap_name, cap_ty), &by_ptr) in all_captures.iter().zip(capture_modes.iter()) { if by_ptr { if cap_ty.ends_with('*') { self.line(&format!("#define {cap} (_c->{cap})", cap = cap_name)); } else { self.line(&format!("#define {cap} (*_c->{cap})", cap = cap_name)); } } else { // By-value snapshot: direct field access (no deref). self.line(&format!("#define {cap} (_c->{cap})", cap = cap_name)); } } match &m.body { HandlerMethodBody::Expr(e) => { let v = self.emit_expr(e)?; if ret_c == "nova_unit" { self.line(&format!("(void)({}); return NOVA_UNIT;", v)); } else if v == "NOVA_UNIT" { let zero = Self::zero_literal_for_type(&ret_c); self.line(&format!("return {};", zero)); } else { self.line(&format!("return {};", v)); } } HandlerMethodBody::Block(b) => { let pm_block_id = self.enter_defer_scope(b, false); for stmt in &b.stmts { self.emit_stmt(stmt)?; } let last_is_return = b.stmts.last() .map(|s| matches!(s, Stmt::Return { .. })) .unwrap_or(false); if let Some(trailing) = &b.trailing { let v = self.emit_expr(trailing)?; self.leave_defer_scope(pm_block_id); if ret_c == "nova_unit" { self.line(&format!("(void)({}); return NOVA_UNIT;", v)); } else if v == "NOVA_UNIT" { let zero = Self::zero_literal_for_type(&ret_c); self.line(&format!("return {};", zero)); } else { self.line(&format!("return {};", v)); } } else if last_is_return { self.leave_defer_scope(pm_block_id); } else if ret_c == "nova_unit" { self.leave_defer_scope(pm_block_id); self.line("return NOVA_UNIT;"); } else { self.leave_defer_scope(pm_block_id); let zero = Self::zero_literal_for_type(&ret_c); self.line(&format!("return {};", zero)); } } } // Undef capture macros. for (cap_name, _) in &all_captures { self.line(&format!("#undef {}", cap_name)); } // Restore var_types для method params. for (name, prev) in saved_params { match prev { Some(old) => { self.var_types.insert(name, old); } None => { self.var_types.remove(&name); } } } self.indent -= 1; self.line("}"); self.line(""); } // Move emitted impl functions в deferred_impls (file-scope). let impl_code = std::mem::replace(&mut self.out, saved_out); self.deferred_impls.push_str(&impl_code); self.indent = saved_indent; Ok(box_var) } // ---- spawn ---- /// Plan 173.1 Ф.2: how an element of C type `elem_c` rides the (mono /// nova_int-slotted) channel in the `parallel for → []T` collection /// lowering. ONE policy shared by the send side (`emit_spawn` parfor mode) /// and the recv side (`emit_parfor_drain_fiber`) — they MUST agree. fn parfor_chan_repr(elem_c: &str) -> ParforChanRepr { if elem_c.ends_with('*') { // Heap pointer (record / sum / nested Vec / boxed tuple / closure): // by reference — cast through intptr_t (D71: «heap — ссылкой»). ParforChanRepr::Pointer } else if matches!(elem_c, "nova_int" | "nova_bool" | "nova_byte" | "nova_char" | "nova_i8" | "nova_i16" | "nova_i32" | "nova_i64" | "nova_u8" | "nova_u16" | "nova_u32" | "nova_u64") { // Integer scalar ≤ 64 bit: value fits the nova_int slot directly. ParforChanRepr::IntScalar } else { // Everything else — f32/f64 (bit pattern would not survive an // arithmetic (nova_int) cast), nova_str (struct), value-records // (NovaValue_*), anonymous/named tuples, NovaOpt_/NovaRes_ payloads // — is copied into a GC-heap box; the box pointer is sent // (D71: «value-типы — копией»; the copy IS the boxed snapshot). ParforChanRepr::Boxed } } // ── [221.1 №431 остаток] Fiber-exit anchor — the three-part protocol ── // // A coroutine dies ONLY by returning from its entry function; `mco_yield` // merely suspends it, and a suspended-forever fiber is a silent CPU hang // (measured and rejected by the p431 window). So "end exactly this fiber // and leave the process alone" needs a return point established at fiber // entry that any point inside the body can jump to. The runtime jumps // here from `_nova_cancel_no_handler` (nova_rt/effects.c) when a cancel // arrives with no fail-frame left to catch it — the D75 "token unbound or // scope already ended" race, which used to end the whole process. // // All THREE codegen fiber-entry emitters must use all three helpers, in // order: the layout field (`…_anchor_field`), the arm+setjmp opening // (`…_anchor_arm`) as the entry's first act, and the close+disarm // (`…_anchor_close`) immediately before the entry's epilogue. Those are // `emit_spawn`, `emit_parfor_drain_fiber` (both below) and `emit_detach` // (emit_detach.rs). Getting the set wrong is FATAL in the same way the // `schedlink` mirror is: the runtime writes an anchor pointer through // `NovaSpawnCtxBase`, so a layout without the field corrupts the first // user-capture field instead. /// Layout mirror of `NovaSpawnCtxBase::_nova_fiber_anchor` (fibers.h) — /// the LAST base field, after `schedlink`, before any user capture. fn emit_spawn_ctx_anchor_field(&mut self) { let _ = writeln!(self.lambda_forward_decls, " NovaFiberAnchor* _nova_fiber_anchor;"); } /// Arm the anchor and open the region it guards. Must be emitted right /// after `_c` is fetched and BEFORE anything else — including the /// prologue safepoint, which can itself deliver a cancel at a moment when /// the fiber's root fail-frame has not been pushed yet. /// /// Only `_c` is read after the `setjmp`, and it is assigned once, before /// it — so no object this branch depends on is modified inside the /// guarded region (C11 7.13.2.1p3: otherwise its value after a longjmp /// would be indeterminate). fn emit_fiber_anchor_arm(&mut self) { self.line("NovaFiberAnchor _nv_anchor;"); self.line("_c->_nova_fiber_anchor = &_nv_anchor;"); self.line("if (setjmp(_nv_anchor.jmp) == 0) {"); self.indent += 1; } /// Close the guarded region and disarm. The `else` arm is the retirement /// landing pad, and NOTHING is reported to the parent scope — the /// precondition for landing here is that the scope is already gone, so a /// report would manufacture a failure out of a harmless race. Disarming /// before the epilogue is what stops a second late cancel, arriving while /// the epilogue runs, from jumping back into a body that has finished. /// /// The three TLS slots reset here are exactly the ones that hold pointers /// INTO the frames the jump just skipped past — the same hazard, and the /// same remedy, as `nova_runtime_reset` (fibers.h) applies after a panic /// caught by a test-frame. `_nova_fail_top` / `_nova_interrupt_top` are /// per-fiber save/restore'd by the scheduler and were NULL when this fiber /// started (`nova_fiber_spawn_into` seeds both slots), so NULL restores /// them exactly. `_nova_current_handler_iframe` is NOT per-fiber swapped — /// a stale value left behind by a dying fiber would outlive it on this OS /// thread and be dereferenced by the next `nova_interrupt` that reads it. /// The per-fiber error/trace bucket needs no reset: it dies with the /// fiber (`_nova_error_state_p`, effects.h). fn emit_fiber_anchor_close(&mut self) { self.indent -= 1; self.line("} else {"); self.indent += 1; self.line("/* [221.1 №431] late cancel with no handler — retire THIS fiber only */"); self.line("_nova_fail_top = NULL;"); self.line("_nova_interrupt_top = NULL;"); self.line("_nova_current_handler_iframe = NULL;"); self.indent -= 1; self.line("}"); self.line("_c->_nova_fiber_anchor = NULL;"); } fn emit_spawn(&mut self, body: &Expr) -> Result<String, String> { // D50/D71: spawn разрешён только внутри structured-scope. // В bootstrap-codegen — только supervised. Вне scope — compile error. if self.current_scope_queue.is_none() { return Err(format!( "spawn is only allowed inside `supervised`, `parallel for` or other structured-scope (D50). \ Wrap your code in `supervised {{ ... }}` to enable concurrent execution." )); } let spawn_id = format!("_nova_spawn_{}", self.spawn_counter); self.spawn_counter += 1; // Collect all identifiers referenced in the spawn body let mut refs: Vec<String> = Vec::new(); Self::collect_idents_expr(body, &mut refs); refs.sort(); refs.dedup(); // Collect all names *bound* inside the spawn body (let bindings, for patterns, match arms). // These are local to the spawn and must not be captured from the outer scope. let mut bound: std::collections::HashSet<String> = std::collections::HashSet::new(); Self::collect_bound_names_expr(body, &mut bound); // [M-parfor-capture-callee-name-collides-std-local]: names that resolve // (checker's `resolved_callees` channel, by exact Call-site `ExprId`) to a // genuine module-fn/method callee are NOT variables — never consult // `var_types` for them below. See `collect_resolved_call_target_names_expr` // doc for the full root-cause (flat, never-per-function-scoped `var_types` // leaking a same-named LOCAL from an unrelated CU function). let mut resolved_fn_call_names: std::collections::HashSet<String> = std::collections::HashSet::new(); self.collect_resolved_call_target_names_expr(body, &mut resolved_fn_call_names); // A name is a capture only if: it is in outer var_types AND not bound inside spawn. // Each capture is recorded with `by_value` flag: // - immutable (let, not let mut) → captured BY VALUE (snapshot at spawn site). // - mutable → BY POINTER (shared mutation). // Rationale: parallel-for / supervised holds spawns until end-of-scope; loop- // variables (`let cur = xs[i]`) are immutable and change ВНЕШНЕ across // iterations (the `for`-loop reuses the same C stack slot each turn). // Capturing by value snapshots them; by pointer would let all queued fibers // see the LAST iteration's value once they finally run. // // [M-parfor-loopvar-nonscalar-byref-capture] (Plan 173.1, 2026-07-13): this // used to additionally require `is_scalar` (type ∈ {nova_int, nova_bool, // nova_f64, nova_f32, nova_byte}) before granting by-value capture — ANY // non-scalar immutable loop variable (str, heap-record pointer, value-record, // tuple, sum) fell through to by-POINTER, aliasing the loop's shared stack // slot. Repro: `parallel for s in ["a","b","c"] { Report{source: s, ...} }` // — every child read the address of the SAME `s`, so all children observed // whatever `s` held once they actually ran (the loop had usually already // advanced past it) → duplicate/missing elements, silently wrong `source` // fields (`[M-parfor-record-result-miscompile]`). For a non-loop, single- // assignment immutable capture, by-value (struct/pointer copy) and by-pointer // (address of a stable slot) are behaviourally IDENTICAL — the value never // changes for the rest of the scope either way — so widening by-value capture // to every immutable type is a pure bugfix, not a behaviour change, outside // the loop-variable-reuse case it fixes. `by_value=true` non-scalar structs // (nova_str, tuples, value-records) get a plain C struct-copy ctx field // (`T cap;`, not `T* cap;`) — safe, GC-scanned via the ctx allocation. let mut captures: Vec<(String, String, bool)> = Vec::new(); for name in refs { if bound.contains(&name) { continue; } // [M-parfor-capture-callee-name-collides-std-local]: a resolved // call-target name (module-fn/method the checker statically picked // for THIS call-site) is never a captured variable — skip it BEFORE // the var_types lookup below, which has no per-function scoping and // would otherwise wrongly match a same-named LOCAL from a completely // unrelated function elsewhere in the CU. The call ITSELF is emitted // correctly regardless (mangled free-fn dispatch, `emit_call`); only // the spurious ctx-capture field is what this skip prevents. if resolved_fn_call_names.contains(&name) { continue; } // [M-spawn-module-const-capture] (Plan 173.1, 2026-07-09): a MODULE- // LEVEL const also sits in `var_types` (emit_const_decl registers // name→type for inference), but it is NOT a local — its C symbol is // the mangled file-scope global `Nova_const_<mod>_<NAME>`. Capturing // it emitted `ctx-><NAME> = <NAME>;` with the RAW source name at the // call site → undeclared C identifier (surfaced by consts referenced // inside `spawn` in nova_tests/concurrency/sleep_real_clock.nv). // Skip it: the body's Ident emission — with no capture-rewrite entry // — falls through to the `private_const_c_names` lookup and reads the // global directly (visible at file scope inside the fiber fn). if self.private_const_c_names .contains_key(&(body.span.file_id, name.clone())) { continue; } if let Some(ty) = self.var_types.get(&name).cloned() { let is_mut = self.var_mutable.contains(&name); let by_value = !is_mut; captures.push((name, ty, by_value)); } } // Ctx struct typedef is emitted ONLY into lambda_forward_decls (file scope). // We do NOT emit a duplicate typedef inside the current function: when this spawn // appears nested in another fiber's body, capture macros could tokenize-rewrite // the field declarations and break compilation. let ctx_ty = format!("NovaSpawnCtx_{}", &spawn_id[1..]); // strip leading _ // Heap-alloc the ctx — each spawn inside a loop iteration needs its own ctx, // and the queue holds them until scope-exit. nova_alloc returns zeroed memory. // // Plan 83.4.5.8 (2026-05-24): conditional allocation. Под armed M:N // используем nova_alloc_uncollectable — GC race на Windows fiber arena // приводит к worker-side zeros despite ctx_pins / arena root coverage // (см. Plan 83.4.5.8 §4). Uncollectable allocation persists до // явного nova_free_uncollectable в worker_main post-mco_destroy // (см. runtime.c::_worker_main). Под bootstrap (`_armed == false`) // используем regular nova_alloc — fiber лежит в q->fiber_ctx[] // (GC-managed array, scope on main stack → GC roots). let ctx_var = format!("{}_ctx", spawn_id); self.line(&format!("nova_bool _nova_is_init_{ctr} = nova_runtime_is_initialized();", ctr = self.spawn_counter - 1)); // Plan 83.6 (2026-05-24): под armed M:N — nova_spawn_pool_acquire // (per-worker free-list pool; обходит Boehm GC_malloc_uncollectable // global lock). Pool возвращает class-size buffer (≥ sizeof(ctx)) // зануляет память и sets base->_nova_pool_size. Под bootstrap // (single-thread) — regular nova_alloc как и было. self.line(&format!( "{ty}* {var} = ({ty}*)(_nova_is_init_{ctr} ? nova_spawn_pool_acquire(sizeof({ty})) : nova_alloc(sizeof({ty})));", ty = ctx_ty, var = ctx_var, ctr = self.spawn_counter - 1)); for (cap, _, by_value) in &captures { // If cap is itself a capture of the *enclosing* fiber, the outer ctx field // is either T (by-value) or T* (by-pointer). For inner spawn: // - inner by-value: copy the current value; if outer is by-pointer, deref. // - inner by-pointer: pass the same pointer; if outer is by-value, take address. let is_outer_cap = self.current_spawn_captures.as_ref() .map(|s| s.contains(cap)).unwrap_or(false); let outer_by_value = self.current_spawn_capture_by_value.as_ref() .map(|s| s.contains(cap)).unwrap_or(false); // [M-nv-spawn-ctx-capture-mut-param-ptr-mismatch] fix (222.7): // a "local var" outer capture is NOT always a plain by-value C // local — a `mut T` free-fn/method PARAMETER (Plan 184 R10 // in-out ABI) is ALREADY represented as `T*` in C, exactly like // an `is_outer_cap && !outer_by_value` capture field one level // up. The two branches below only special-cased the latter // (nested-spawn re-capture); a bare `mut`-param name fell // through to the `cap.clone()`/`&{cap}` "ordinary local" // arms, which assume `cap`'s C storage IS the value — so // `ctx->field = cap;` (by_value) assigned a `T*` into a `T` // field (clang: "assigning to 'T' from incompatible type // 'T *'"), and `ctx->field = ∩` (by-pointer) took the // address of a POINTER (`T**`) instead of passing it through. // `ref_params` (Plan 184) is the existing registry of exactly // these by-pointer-ABI names — same predicate every other // read/write site in this file already consults (see e.g. the // Ident-emission `ref_params` deref below). Treating a // `ref_params` name as "already a pointer" here mirrors that. let outer_is_ref_param = self.ref_params.contains(cap); let access_outer = if is_outer_cap { if outer_by_value { format!("_c->{}", cap) } // T value else { format!("(*_c->{})", cap) } // *T } else if outer_is_ref_param { format!("(*{})", cap) // by-pointer param → deref for the value } else { cap.clone() // local var }; let address_outer = if is_outer_cap { if outer_by_value { format!("&_c->{}", cap) } // address of T field else { format!("_c->{}", cap) } // already a pointer } else if outer_is_ref_param { cap.clone() // by-pointer param — already the address } else { format!("&{}", cap) // local var address }; if *by_value { // Copy current value into the new ctx (snapshot). self.line(&format!("{ctx}->{cap} = {acc};", ctx = ctx_var, cap = cap, acc = access_outer)); } else { // Store a pointer for shared mutation. self.line(&format!("{ctx}->{cap} = {addr};", ctx = ctx_var, cap = cap, addr = address_outer)); } } // [M-187-nested-spawn-scope-var-cc-fail]: a `spawn` LEXICALLY NESTED // inside this spawn's own body (same `supervised` scope, no // intervening nested `supervised`/`parallel for`) needs to register // its own fiber as a child of the SAME scope queue this spawn itself // registers into (`queue`, below) — but `queue`'s C name is a // codegen-internal local (`_nova_scope_q_N`), never an AST `Ident`, // so the ordinary `captures` scan above (`collect_idents_expr`) can // never discover it. Without this, a nested `emit_spawn` call reads // `self.current_scope_queue` unchanged (still the OUTER scope's bare // local name) while emitting into a DIFFERENT C function (this // spawn's own fiber fn, whose only local is `_c`) — CC-FAIL "use of // undeclared identifier". Fix: always thread the active scope queue // through the ctx (one extra pointer field, unconditionally — cheap, // and correct whether or not this spawn's body actually contains a // nested spawn) and repoint `current_scope_queue` at the captured // field for the duration of body emission below, mirroring the // `is_outer_cap` capture-macro treatment user captures already get. // (`queue` is normally computed further down, right before the fiber // is pushed into it — hoisted here, unchanged value, so this capture // assignment can use it too.) let queue = self.current_scope_queue.clone().expect("scope queue must be active"); let queue_cap_field = "_nova_captured_scope_q".to_string(); self.line(&format!("{ctx}->{field} = &{q};", ctx = ctx_var, field = queue_cap_field, q = queue)); // D71 / Plan 173.1 Ф.2 `parallel for → []T`: clone the parent Sender // into the child's ctx AT THE SPAWN SITE — the clone is created in the // PARENT (refcount++ happens strictly before the parent tx close that // follows the enqueue loop), then MOVEd into the child (ownership = // child; the child closes it on every fiber-exit path — see the close // emission after the fail-frame below). Plan 173.1 §2.3: «клон создан // в родителе (момент spawn → refcount++) и MOVE'нут в ребёнка». let parfor_send = self.current_parfor_send.clone(); if let Some((tx_var, _)) = &parfor_send { self.line(&format!("{ctx}->_nova_par_tx = nova_chan_writer_clone({tx});", ctx = ctx_var, tx = tx_var)); } // Push the fiber into the scope queue. spawn returns unit (D50/D71): // results from concurrent execution come through mut-captures or // `parallel for` (homogeneous results), never from spawn itself. // (`queue` hoisted above, next to the new scope-queue capture field.) // Plan 44.5 Layer 5 fix: initialize _nova_worker_slot = -1 explicitly. // nova_alloc zero-initializes (slot=0), but 0 is a valid slot index — // -1 is required as "not yet set" sentinel for the worker loop restore logic. self.line(&format!("{ctx}->_nova_worker_slot = -1;", ctx = ctx_var)); // Plan 173.0 Ф.2 (A2.2): same sentinel discipline for the retention // slot — 0 is a valid child index too. nova_runtime_spawn_into // (runtime.c) overwrites this with the real // nova_scope_alloc_child_slot() index on the M:N remote path; // stays -1 on the single-thread/bootstrap path (nova_fiber_spawn_into // never touches it — that path doesn't use Ф.2 retention). self.line(&format!("{ctx}->_nova_parent_slot = -1;", ctx = ctx_var)); // Plan 44.5 Layer 5: implicit M:N — runtime initialized → push в worker // deque; иначе single-thread path unchanged. // Plan 83.2 Ф.1 примечание: чтобы supervised{spawn} попал в M:N // ветку без явного runtime.init, codegen основной программы // (emit_main_function) теперь вызывает _auto_arm_if_needed() в // самом начале main — runtime армится при старте программы, // is_initialized() возвращает true ниже, идём по M:N пути. // Hello-world без spawn остаётся 0 worker-потоков (пул лениво // материализуется только в _ensure_materialized из spawn-пути). // Plan 83.4.5.8: используем cached _nova_is_init_N для consistency // с allocation выбором (см. above). self.line(&format!("if (_nova_is_init_{ctr}) {{", ctr = self.spawn_counter - 1)); self.indent += 1; self.line(&format!("{ctx}->_nova_parent_scope = &{q};", ctx = ctx_var, q = queue)); // Plan 83.4.5.4 (2026-05-23): spawn-time TLS handler-snapshot capture // на parent thread'е — fiber inherit'ит outer `with X = h` биндинги. // Worker preamble adopts эту allocation в fiber_effect_snapshot[slot]. // Plan 83.4.5.8 (2026-05-24): snapshot — collectable (nova_alloc). // Reachable через ctx (uncollectable, scanned by GC) до preamble, // через scope->fiber_effect_snapshot[slot] (scope в worker struct, // GC-rooted via Plan 82 Ф.3) после preamble. GC reclaim'ит когда // slot reused либо worker shutdown. self.line(&format!("{ctx}->_nova_init_snapshot = (NovaEffectSnapshot*)nova_alloc(sizeof(NovaEffectSnapshot));", ctx = ctx_var)); self.line(&format!("nova_effect_snapshot_save({ctx}->_nova_init_snapshot);", ctx = ctx_var)); self.line(&format!("nova_runtime_spawn_into(&{q}, {id}, {ctx});", q = queue, id = spawn_id, ctx = ctx_var)); self.indent -= 1; self.line("} else {"); self.indent += 1; self.line(&format!("{ctx}->_nova_parent_scope = NULL;", ctx = ctx_var)); // Single-thread path: nova_fiber_spawn_into внутри себя save'ит // snapshot directly в q->fiber_effect_snapshot[count] (line 1009-1011 // в fibers.h). Init_snapshot тут не нужен. self.line(&format!("{ctx}->_nova_init_snapshot = NULL;", ctx = ctx_var)); self.line(&format!("nova_fiber_spawn_into(&{q}, {id}, {ctx});", q = queue, id = spawn_id, ctx = ctx_var)); self.indent -= 1; self.line("}"); // Emit the ctx-struct typedef into lambda_forward_decls — flushed before the // current function in `out`, so the typedef is visible at the spawn-instance // declaration site (and also for the entry fn body which lives in deferred_impls). // // Plan 44.5 Layer 5 fix: base fields MUST be FIRST so NovaSpawnCtxBase* cast // in runtime.c worker loop is safe (fixed offsets). User captures follow. let _ = writeln!(self.lambda_forward_decls, "typedef struct {{"); // Base fields first (NovaSpawnCtxBase layout — must match fibers.h). // Plan 44.5 Layer 5: parent scope для remote-spawn tracking. // NULL = single-thread (без runtime.init). Always present. let _ = writeln!(self.lambda_forward_decls, " NovaFiberQueue* _nova_parent_scope;"); // Plan 173.0 Ф.2 (A2.2): retention-slot index into // _nova_parent_scope's child_error[]/child_ctx[] arrays — MUST // immediately follow _nova_parent_scope (mirrors NovaSpawnCtxBase // field order in fibers.h exactly; the worker loop casts this ctx // to NovaSpawnCtxBase* by common-initial-sequence, so every base // field here must match fibers.h's order/type 1:1). -1 = unset. let _ = writeln!(self.lambda_forward_decls, " int _nova_parent_slot;"); // Plan 44.5 Layer 5 park/wake: slot index в worker scope. // Initialized to -1; set by preamble on first run. -1 = not yet set. let _ = writeln!(self.lambda_forward_decls, " int _nova_worker_slot;"); // Plan 44.5 Layer 5 fix: per-fiber fail/interrupt-top chain snapshot. // Worker saves/restores these around mco_resume to isolate fiber fail-stacks. let _ = writeln!(self.lambda_forward_decls, " NovaFailFrame* _nova_saved_fail_top;"); let _ = writeln!(self.lambda_forward_decls, " NovaInterruptFrame* _nova_saved_interrupt_top;"); // Plan 44.5 Layer 5 deadlock fix: home worker scope for work-stealing. // Set once in preamble; worker restores _nova_active_scope from this // before each mco_resume so channel ops always capture the correct scope. // NULL before preamble runs (_nova_worker_slot == -1 guards that path). let _ = writeln!(self.lambda_forward_decls, " NovaFiberQueue* _nova_fiber_scope;"); // Plan 83.4.5.4 (2026-05-23): spawn-time TLS handler-snapshot capture. // Allocated + saved BEFORE nova_runtime_spawn_into на parent thread'е // (где TLS handlers видимы). Worker preamble adopts его в // fiber_effect_snapshot[slot] чтобы fiber видел inherited handler-state // (Node AsyncLocalStorage / Kotlin CoroutineContext semantics). // NULL для single-thread spawn (nova_fiber_spawn_into сам save'ит // directly в scope's parallel array). let _ = writeln!(self.lambda_forward_decls, " NovaEffectSnapshot* _nova_init_snapshot;"); // Plan 83.4.5.7 (2026-05-23): atomic fiber state machine. nova_alloc // zero-init → starts as NOVA_FIBER_STATE_IDLE. Must match fibers.h // NovaSpawnCtxBase layout exactly — runtime.c worker loop cast'ает // user_data к NovaSpawnCtxBase* и читает _nova_fiber_state по // фиксированному offset. let _ = writeln!(self.lambda_forward_decls, " nova_atomic_int _nova_fiber_state;"); // Plan 83.6 (2026-05-24): allocation size — used by free path для // routing ctx обратно в P-local SpawnCtx pool. Set по nova_spawn_pool_acquire. let _ = writeln!(self.lambda_forward_decls, " size_t _nova_pool_size;"); // Plan 110.2.1.a (D188 R3) [M-110.x-cleanup-shield-deadline-underflow] // supervised(cancel:) fix (2026-06-05): cancel-shield mask + deadline // fields. MUST be в codegen layout — иначе runtime reads past struct // (Boehm GC garbage bytes) → mask=garbage > 0 → nv_shield_check_deadline // enters slow path → deadline=garbage triggers bogus watchdog-варн // (было: bogus CleanupTimeoutError, 720M+ ms "over budget" в 6s tests; // D192-ретракт Plan 173 Ф.5 п.2 заменил throw на one-shot варн). let _ = writeln!(self.lambda_forward_decls, " nova_atomic_int _nova_cancel_mask_count;"); let _ = writeln!(self.lambda_forward_decls, " int64_t _nova_cancel_deadline_ns;"); // Plan 83-go-cmn Ф.2: gopark/goready 4-state park-latch. MUST be in the // codegen layout (mirrors NovaSpawnCtxBase._nova_park_state in fibers.h) // and MUST be inserted BEFORE schedlink (which stays second-to-last, // ahead of the #431 fiber-anchor). Zero-init = NOVA_PARK_NIL. Omitting // it shifts schedlink onto a user capture field → silent corruption // (same FATAL as Ф.1a). let _ = writeln!(self.lambda_forward_decls, " nova_atomic_int _nova_park_state;"); // Plan 83-go-cmn Ф.1: intrusive overflow link — mirrors // NovaSpawnCtxBase.schedlink in fibers.h. Without it the overflow path // (nova_co_schedlink) would write onto the first user capture field → // silent corruption / heap overflow past the pool class. let _ = writeln!(self.lambda_forward_decls, " mco_coro* schedlink;"); // [221.1 №431 остаток] Fiber-exit anchor — LAST base field. Same // FATAL-if-omitted property as schedlink above. self.emit_spawn_ctx_anchor_field(); // User capture fields follow base fields. for (cap, ty, by_value) in &captures { if *by_value { let _ = writeln!(self.lambda_forward_decls, " {} {};", ty, cap); } else { let _ = writeln!(self.lambda_forward_decls, " {}* {};", ty, cap); } } // [M-187-nested-spawn-scope-var-cc-fail]: active scope queue, threaded // through unconditionally so a NESTED `spawn` inside this spawn's own // body can re-register into the same scope (see the assignment above). let _ = writeln!(self.lambda_forward_decls, " NovaFiberQueue* {};", queue_cap_field); // D71 / Plan 173.1 Ф.2 `parallel for → []T`: the child's owned Sender // clone (created in the parent at the spawn site, closed by the child // on fiber exit). Replaces the indexed-slot pair (_nova_par_idx / // _nova_par_result) of the pre-173.1 lowering — collection is dense // completion-order via the channel now, no index slots (§2.3). if parfor_send.is_some() { let _ = writeln!(self.lambda_forward_decls, " Nova_ChanWriter* _nova_par_tx;"); } // Note: no `_nova_result` field — spawn returns unit (D50/D71). let _ = writeln!(self.lambda_forward_decls, "}} {};", ctx_ty); // №379 fix (`disarm_outer_auto_cleanup_for_fiber_body`, emit_detach.rs) — MUST run before the `self.out` swap below. if let ExprKind::Block(b) = &body.kind { self.disarm_outer_auto_cleanup_for_fiber_body(b); } // Swap out to deferred_impls for body emission let saved_out = std::mem::take(&mut self.out); let saved_indent = self.indent; self.indent = 0; // Plan 175 Ф.2-v2 ([M-spawn-var-boxed-leak] class): a spawn-body is // its own C function reached ONLY via `_c->name` capture rewriting // (`current_spawn_captures`, checked further below in Ident // resolution) — NOT via `var_boxed` macro-free box-deref. But // `var_boxed` is a flat, un-scoped map: if an EARLIER closure or // handler-literal in the SAME enclosing fn boxed a mut var this // spawn ALSO captures (same name), the stale `var_boxed` entry is // checked FIRST in Ident resolution (before `current_spawn_ // captures`) and wrongly emits `(*_box_name)` — a C local that // lives in the CALLER's stack frame, invisible inside this spawn's // own function. Isolate (mirrors emit_lambda's identical fix for // its own body) so this spawn's captures are resolved ONLY via its // own ctx struct; restored below. let saved_var_boxed_spawn = std::mem::take(&mut self.var_boxed); // Plan 48 Ф.4 ([M-mono-spawn-fwd-decls]): pre-scan `scan_expr_fwd` // emits forward declarations for every spawn-body it sees in the // ORIGINAL module AST, before fn definitions. Monomorphized fn // bodies are synthesized during the mono-worklist drain, AFTER the // pre-scan ran — so spawn-bodies emitted from inside a mono'd fn // have no pre-scan'ed forward decl, and the mono'd fn would // reference `_nova_spawn_N` undefined-identifier. Detect mono // context via non-empty `current_type_subst` and push the missing // forward decl into `mono_fwd_decls`, which gets spliced into the // `/*__MONO_FWD_DECLS__*/` marker at the top of the C file (before // any fn definition). Idempotent: each spawn_id is unique per // counter, no risk of duplicates. if !self.current_type_subst.is_empty() { self.mono_fwd_decls.push_str(&format!( "{}void {}(mco_coro* _co);\n", self.top_level_storage(), spawn_id)); } self.line(&format!("{}void {}(mco_coro* _co) {{", self.top_level_storage(), spawn_id)); self.indent += 1; // Plan 44.5 Layer 5: _c всегда нужен — entry function reads // _c->_nova_parent_scope для remote-fiber cleanup (decrement // pending_remote + signal_main). Раньше для empty-capture spawn // было `(void)_co;` — теперь _c always. self.line(&format!("{ctx}* _c = ({ctx}*)mco_get_user_data(_co);", ctx = ctx_ty)); // [221.1 №431 остаток] Arm this fiber's exit anchor — see // `emit_fiber_anchor_arm` for the full rationale. FIRST act of the // entry, ahead of even the prologue safepoint below: that safepoint // can itself deliver a cancel, and at that moment the fiber's root // fail-frame (`_ff`, pushed further down) does not exist yet — which // is precisely the "nobody left to catch it" state this anchor // exists to survive. self.emit_fiber_anchor_arm(); // Plan 143.2: prologue safepoint — unconditional. This is a FIBER-ENTRY // function reached indirectly via the scheduler; its body is user Nova // code that may run straight-line work before the first loop/call. KEEP // conservatively (no source FnDecl key for a spawn-body block). self.emit_prologue_preempt_check_unconditional(); // Plan 44.5 Layer 5 park/wake: alloc slot in worker scope on first resume. // Required so _nova_active_slot >= 0 (D92 invariant) when fiber calls // Time.sleep / Channel.recv in worker context. // Single-thread path: _nova_parent_scope == NULL → skip. self.line("if (_c->_nova_parent_scope) {"); self.indent += 1; self.line("_nova_active_slot = nova_scope_alloc_slot(_nova_active_scope, _co);"); self.line("_c->_nova_worker_slot = _nova_active_slot;"); self.line("_c->_nova_fiber_scope = _nova_active_scope;"); // Plan 83.4.5.4 (2026-05-23): adopt spawn-time captured snapshot // в worker's home scope's fiber_effect_snapshot[slot]. Worker loop // restore'ит из него перед mco_resume → fiber видит parent's TLS // handler-state inherited. self.line("if (_c->_nova_init_snapshot && _nova_active_slot >= 0) {"); self.indent += 1; self.line("_nova_active_scope->fiber_effect_snapshot[_nova_active_slot] = _c->_nova_init_snapshot;"); self.line("_c->_nova_init_snapshot = NULL;"); /* ownership transferred */ self.indent -= 1; self.line("}"); self.indent -= 1; self.line("} else {"); self.indent += 1; self.line("_c->_nova_worker_slot = -1;"); self.indent -= 1; self.line("}"); // Activate capture rewriting: ExprKind::Ident → `(*_c->name)` or `_c->name`. let mut cap_set: HashSet<String> = HashSet::new(); let mut cap_by_value: HashSet<String> = HashSet::new(); for (cap, _, by_value) in &captures { cap_set.insert(cap.clone()); if *by_value { cap_by_value.insert(cap.clone()); } } let prev_caps = std::mem::replace(&mut self.current_spawn_captures, Some(cap_set)); let prev_by_value = std::mem::replace(&mut self.current_spawn_capture_by_value, Some(cap_by_value)); // [M-187-nested-spawn-scope-var-cc-fail]: repoint the active scope // queue at the captured ctx field (`&(*_c->_nova_captured_scope_q)` — // a valid `NovaFiberQueue*`-typed address expression, identical in // effect to the bare local it replaces) for the duration of THIS // spawn's own body emission, so a nested `emit_spawn` call picks up // a reference valid inside this fiber fn instead of the outer // function's now-unreachable local. let prev_scope_queue = std::mem::replace( &mut self.current_scope_queue, Some(format!("(*_c->{})", queue_cap_field)), ); // Wrap body in a fail-frame so that `throw` inside fiber is caught here // (longjmp lands on THIS fiber's stack — safe). After catch, report the // error to the active scope queue via nova_fiber_report_error, and let // the fiber finish cleanly. Scope-runner re-throws on main flow after // all fibers have been drained (nova_supervised_run). self.line("NovaFailFrame _ff;"); self.line("nova_fail_push(&_ff);"); self.line("if (setjmp(_ff.jmp) == 0) {"); self.indent += 1; // Inside the spawn body, the parfor-send mode belongs to THIS spawn — but // any *nested* spawn must not inherit it. Temporarily disable while // emitting the body. let saved_parfor = self.current_parfor_send.take(); // Emit body, discard its value (spawn returns unit) — UNLESS in parfor // mode, where the trailing expression's value is SENT into the collection // channel (Plan 173.1 Ф.2). Representation per `parfor_chan_repr`: // integer scalars ride the nova_int channel slot directly; heap pointers // are cast through intptr_t; everything else (f64, str, value-records, // tuples, Option/Result payloads) is boxed on the GC heap and the box // pointer is sent (the channel buffer is nova_alloc'd → conservatively // scanned → the box stays alive while in flight). let emit_parfor_send = |this: &mut Self, v: &str| { if let Some((_, elem_c)) = &saved_parfor { match Self::parfor_chan_repr(elem_c) { ParforChanRepr::IntScalar => { this.line(&format!( "(void)nova_chan_writer_send(_c->_nova_par_tx, (nova_int)({}));", v)); } ParforChanRepr::Pointer => { this.line(&format!( "(void)nova_chan_writer_send(_c->_nova_par_tx, (nova_int)(intptr_t)({}));", v)); } ParforChanRepr::Boxed => { this.line("{"); this.indent += 1; this.line(&format!( "{ty}* _nova_pf_box = ({ty}*)nova_alloc(sizeof({ty}));", ty = elem_c)); this.line(&format!("*_nova_pf_box = ({});", v)); this.line( "(void)nova_chan_writer_send(_c->_nova_par_tx, (nova_int)(intptr_t)_nova_pf_box);"); this.indent -= 1; this.line("}"); } } } else { this.line(&format!("(void)({});", v)); } }; match &body.kind { ExprKind::Block(b) => { // [M-217-spawn-body-toplevel-bare-consume-let-noop] BUGFIX: // this raw per-statement loop used to skip `enter_defer_scope`/ // `leave_defer_scope` entirely — unlike EVERY other block- // emission site (`emit_supervised`'s own body, match arms, `if`/ // `while` bodies, …), which all wrap their stmts with the // defer-scope prologue/epilogue (mirrors `emit_supervised` // just above, ~line 12630). A plain `defer` statement directly // at spawn-body top level silently never ran; worse, a BARE // auto-cleanup `consume x = e` (Plan 217 hybrid, no `{ … }` // scope-block) directly at spawn-body top level got its // `active_var`/`consume_policy` entry registered nowhere — // `enter_defer_scope`'s prologue is the ONLY place that scans // for `auto_cleanup_qualifies` lets and arms them — so the // resource's `@cleanup` never fired at all (found via a probe // that placed `consume r = mk(1)` directly in `spawn { }` with // no enclosing match/if: `on_resource_exit` never invoked, // `exit_calls` stayed 0 — a silent resource leak, not merely a // missing-symbol/link defect). Nested blocks (a `match`/`if` // arm INSIDE the spawn body) were unaffected — those go // through their own `enter_defer_scope` call in their own // emission function regardless of the outer spawn context, // which is why `examples/net/echo_server.nv`'s `consume s = // stream` (nested inside a `match … { Ok(consume stream) => // { … } }` arm) still got a dispatch call emitted (the // separate DCE-seeding defect this wave's `lints.rs` fix // addresses) — only a BARE top-level spawn-body consume-let // hit this second, independent gap. let block_id = self.enter_defer_scope(b, false); for stmt in &b.stmts { self.emit_stmt(stmt)?; } if let Some(trailing) = &b.trailing { let v = self.emit_expr(trailing)?; emit_parfor_send(self, &v); } self.leave_defer_scope(block_id); } _ => { let v = self.emit_expr(body)?; emit_parfor_send(self, &v); } } // Restore parfor-send mode so the surrounding emit_parallel_for can clear // it after the for-loop body has run. self.current_parfor_send = saved_parfor; self.line("nova_fail_pop();"); self.indent -= 1; self.line("} else {"); self.indent += 1; self.line("nova_fail_pop();"); // D61: distinguish real error from cross-mco-boundary `interrupt v` // (nova_interrupt sets the sentinel message "__nova_interrupt__" and // populates scope->interrupt_pending/interrupt_value). For interrupt // we DON'T report a fiber error — supervised_run will re-issue the // interrupt on main-flow after drain. self.line("if (_ff.error_msg.ptr && _ff.error_msg.len == 18 && memcmp(_ff.error_msg.ptr, \"__nova_interrupt__\", 18) == 0) {"); self.indent += 1; self.line("/* interrupt: scope state already set, fiber dies cleanly */"); self.indent -= 1; self.line("} else {"); self.indent += 1; // Plan 44.5 Layer 5: error reporting — remote vs local. // Plan 49 Ф.2 + Ф.5: kinded — пробрасываем _ff.error_kind / error_reason_ptr. // Local path: USER-precedence через nova_fiber_report_error_kinded. // Remote path: kinded atomic report (compare-kind CAS-loop — // CANCEL→USER overwrite, иначе keep). self.line("if (_c->_nova_parent_scope) {"); self.indent += 1; // Plan 83.10 (2026-05-25): fix [M-83.10-armed-user-throw-routing] — // typed throw payload + tid тоже propagate в atomic, чтобы main // re-throw мог dispatch через nova_throw_typed handler chain. // Plan 173.0 Ф.2 (A2.3): nova_fiber_report_child_kinded wraps the // existing atomic report (unchanged fast path) with a per-slot // write into _c->_nova_parent_scope->child_error[_c->_nova_parent_slot] // — replaces N-simultaneous-failure collapse with genuine per-child // retention. Takes `_c` (not `_c->_nova_parent_scope`) since it // needs both the parent scope AND this child's own slot index. self.line("nova_fiber_report_child_kinded(_c, _ff.error_msg.ptr, _ff.error_kind, _ff.error_reason_ptr, _ff.error_user_payload, _ff.error_user_type_id);"); self.indent -= 1; self.line("} else {"); self.indent += 1; self.line("nova_fiber_report_error_kinded(_ff.error_msg.ptr, _ff.error_kind, _ff.error_reason_ptr);"); self.indent -= 1; self.line("}"); self.indent -= 1; self.line("}"); self.indent -= 1; self.line("}"); // [221.1 №431 остаток] Close the anchor region and disarm. Everything // below is the epilogue, which every exit path — normal return, // caught throw, interrupt, AND late-cancel retirement — shares. self.emit_fiber_anchor_close(); // Plan 173.1 Ф.2: the child OWNS its Sender clone — close it on EVERY // fiber-exit path (this point is reached on success, throw, cancel and // interrupt alike — the fail-frame above never rethrows out of the // fiber). The last closed clone drives writer_count → 0 → channel // closed → the drain fiber's recv() returns None and the drain ends. // A child that threw never sent → its element is simply absent (dense // completion-order collection, no holes — §2.3/Stop semantics). if self.current_parfor_send.is_some() { self.line("nova_chan_writer_close(_c->_nova_par_tx);"); } // Plan 44.5 Layer 5: remote fiber post-completion cleanup. // (1) Free worker scope slot (alloc'd in preamble) — before decrement // so the slot is available for the next fiber immediately. // (2) Decrement parent's pending_remote (release ordering) — main // thread в supervised_run wait-loop увидит decrement через // nova_aint_load(acquire). signal_main wake'ом wake'ает main // thread'а из uv_run(UV_RUN_ONCE). self.line("if (_c->_nova_parent_scope) {"); self.indent += 1; self.line("if (_c->_nova_worker_slot >= 0) {"); self.indent += 1; self.line("nova_scope_free_slot(_nova_active_scope, _c->_nova_worker_slot);"); self.line("_nova_active_slot = -1;"); self.indent -= 1; self.line("}"); // [196.6 / D228 §6 class]: pending_sweeps++ strictly BEFORE the // pending_remote release-decrement (same thread, program order) — // the scope owner that observes pending_remote==0 therefore also // observes pending_sweeps>0 until the worker's post-mortem sweep // (nova_scope_sweep_dead_child) release-decrements it. Closes the // stack-scope use-after-return in the sweep (Plan 198 floating AV; // docs/plans/196.6-race-state-dump-notes.md). self.line("(void)nova_aint_inc(&_c->_nova_parent_scope->pending_sweeps);"); self.line("(void)nova_aint_fetch_sub_release(&_c->_nova_parent_scope->pending_remote);"); self.line("nova_runtime_signal_main();"); self.indent -= 1; self.line("}"); // Deactivate capture rewriting before emitting closing brace. self.current_spawn_captures = prev_caps; self.current_spawn_capture_by_value = prev_by_value; // [M-187-nested-spawn-scope-var-cc-fail]: restore the caller's scope // queue reference now that this spawn's own body is fully emitted. self.current_scope_queue = prev_scope_queue; self.indent -= 1; self.line("}"); self.line(""); let entry_code = std::mem::replace(&mut self.out, saved_out); self.deferred_impls.push_str(&entry_code); self.indent = saved_indent; self.var_boxed = saved_var_boxed_spawn; // spawn evaluates to unit. Ok("NOVA_UNIT".to_string()) } // ---- supervised scope ---- /// Emit `supervised { body }` — D50 structured-concurrency scope. /// All `spawn` inside the body push fibers into a local NovaFiberQueue; /// at scope-exit, nova_supervised_run drives them round-robin to completion. /// Emit `supervised { body }` / `supervised(cancel: tok) { body }` /// (D50 / D75 revised, Plan 47). /// /// Если `cancel` присутствует — токен (`NovaCancelToken*`) вычисляется /// при входе в scope, ПРИВЯЗЫВАЕТСЯ к scope-queue прямо перед /// `nova_supervised_run_cancel` (после эмиссии тела — так прямой `throw` /// в стейтменте тела не оставит висящий `bound_scope`), и ОТВЯЗЫВАЕТСЯ /// внутри `nova_supervised_run_cancel` на всех путях выхода (нормальный /// возврат + re-throw). fn emit_supervised( &mut self, body: &Block, cancel: Option<&Expr>, deadline: Option<&crate::ast::SupervisedDeadline>, on_timeout: Option<&Expr>, ) -> Result<String, String> { let id = self.supervised_counter; self.supervised_counter += 1; let queue_var = format!("_nova_scope_q_{}", id); let prev_scope_var = format!("_nova_prev_scope_{}", id); // Plan 173.1 Ф.1 (D-block «supervised — value-expression»): a `supervised` // block whose body ends in a trailing expression evaluates to that // expression's value AFTER all spawned children have joined (round-robin // drain via `nova_supervised_run`) — NOT before, and NOT discarded. // Previously (bootstrap stub) trailing was evaluated eagerly *before* the // scheduler ran and immediately `(void)`-discarded, so `supervised { … v }` // always yielded unit regardless of `v`. `parallel for`'s array-mode (Ф.2) // is the motivating caller: it needs the post-join accumulator. // // Mechanism: the result variable must be declared *outside* the wrapping // `{ … }` C block (this codegen has no GNU statement-expressions — see // `emit_parallel_for`'s identical constraint) so it stays alive past the // closing brace. `infer_expr_c_type` is a pure/static inference (no // codegen side-effects — same helper `emit_parallel_for` already calls // ahead of emission), safe to call before opening the block. // // [M-codegen-let-locals-overlay] (mirrors `emit_block_expr`'s identical // guard, ~line 35148): the trailing expression may reference the body's // OWN top-level `let`-locals (e.g. `supervised { ro inner = …; inner + 1 // }`), which aren't registered in `var_types` yet at this pre-block probe // point — `var_types` is populated when `body.stmts` are actually emitted, // further down. Without this overlay, the probe hits a stale/absent entry // (P67-LEGACY panic) or a same-named leaked binding from an unrelated // earlier function (`var_types` is not per-fn scoped). Transiently // register each top-level let's inferred type, probe, then restore — // the real emission below re-registers these locals authoritatively. // // [M-supervised-value-unit-trailing] (found testing Ф.1 against the // existing corpus): a trailing expression whose C type is `nova_unit` // is NOT a genuine value to defer — it's almost always the body's LAST // enqueueing statement written without a following statement (Nova's // "last expr in a block with no semicolon is the trailing expr" rule // means `supervised { spawn { … } }` parses `spawn { … }` as `body. // trailing`, not `body.stmts`!). `spawn` MUST run at its source // position (pre-join, to enqueue the fiber) — deferring it to // post-join would silently move it OUTSIDE the active scope, breaking // `current_scope_queue` (`emit_spawn` then sees no active scope: "spawn // is only allowed inside `supervised`…"). So: only unit-typed trailing // stays eager/pre-join/discarded (old behaviour, byte-identical); // non-unit trailing gets the new deferred/post-join value treatment. let mut saved_body_locals: Vec<(String, Option<String>)> = Vec::new(); for stmt in &body.stmts { if let Stmt::Let(d) = stmt { if let Pattern::Ident { name, .. } = &d.pattern { let ty = d.ty.as_ref() .and_then(|t| self.type_ref_to_c(t).ok()) .unwrap_or_else(|| self.infer_expr_c_type(&d.value)); saved_body_locals.push((name.clone(), self.var_types.insert(name.clone(), ty))); } } } let result_var_ty = body.trailing.as_ref().and_then(|t| { let ty = self.infer_expr_c_type(t); // Empty string is `infer_expr_c_type`'s own "unhandled ExprKind, caller // should degrade to nova_unit" convention (see its final wildcard arm) — // e.g. bare `ExprKind::Spawn` has no dedicated arm there. Treat the same // as an explicit "nova_unit" for this eager-vs-deferred decision. if ty == "nova_unit" || ty.is_empty() { None } else { let var = format!("_nova_sup_res_{}", id); Some((ty, var)) } }); for (name, old) in saved_body_locals.into_iter().rev() { match old { Some(t) => { self.var_types.insert(name, t); } None => { self.var_types.remove(&name); } } } // D449: keep the trailing's C type around (`result_c_ty`) — needed // below to cast the `on_timeout:` handler call to the SAME return // type as the body ("тип конструкции всегда равен типу тела"). // `None` here means the body is unit-typed; the handler call is // still emitted (for its side effects / to catch a handler that // itself misbehaves) but cast to `nova_unit` and discarded, exactly // like the pre-existing unit-typed-trailing path below. let result_c_ty = result_var_ty.as_ref().map(|(ty, _)| ty.clone()); let result_var = result_var_ty.map(|(ty, var)| { self.line(&format!("{} {};", ty, var)); var }); // Plan 266 (D455): `supervised(cancel: tok) { … }` WITHOUT // `timeout:`/`deadline:` returns `Outcome[T]`, not the body's plain // `T` — the `cancel:` + `timeout:`/`deadline:` combination is D455 // open question §3, deliberately left UNCHANGED here // (`deadline.is_none()` excludes it — byte-identical to before this // plan for that combination; see std/prelude/concurrency.nv // `Outcome` doc). `outcome_result_var` mirrors `result_var` just // above (same "must live outside the `{ … }` C block" constraint — // it is read after the block closes) but is ALWAYS declared for the // outcome form, even when the body has no trailing value: the wrap // applies regardless of body shape (D455 table — "тип результата // зависит от НАЛИЧИЯ cancel:, и только от него"). `outcome_body_c_ty` // reuses `result_c_ty` (`None` there already means "unit-typed // trailing / no trailing", same convention `on_timeout:`'s cast // above relies on) instead of re-probing — this scope's `T` is the // same `T` either consumer needs. Monomorphization is registered // directly (mirrors `try_infer_variant_mono_args`, ~22971) rather // than through a synthesized call — there is no Nova-AST arg to // infer FROM here (the trailing's own C value doesn't exist as a // real Nova expression at this codegen point, only as an already- // emitted C temp), so this reproduces that helper's OWN mono- // registration side effects (worklist + instance-info) by hand for // the ONE fixed `Outcome` template instead of the general variant- // call inference path. let is_outcome_form = cancel.is_some() && deadline.is_none(); let outcome_body_c_ty = result_c_ty.clone().unwrap_or_else(|| "nova_unit".to_string()); let outcome_mangled = if is_outcome_form { Some(self.outcome_mono_c_base(&outcome_body_c_ty)) } else { None }; let outcome_result_var = outcome_mangled.as_ref().map(|mangled| { let var = format!("_nova_sup_outcome_{}", id); self.line(&format!("{}* {};", mangled, var)); var }); // Wrap the scope in a C block so the queue is local. self.line("{"); self.indent += 1; self.line(&format!("NovaFiberQueue {} = {{0}};", queue_var)); self.line(&format!("nova_scope_init(&{});", queue_var)); // Plan 173.2 (supervision-as-effect): stamp supervisor-mode at scope // entry — on the scope's own thread, strictly BEFORE any spawn. An // ambient `with Supervisor = policy` handler flips the scope into // deferred-decision mode (fibers.h). Emitted only when the CU knows // the Supervisor effect (prelude present) — `#no_prelude` CUs and the // no-handler case stay byte-identical (field is false from init). if self.effect_schemas.contains_key("Supervisor") { self.line(&format!( "{q}.has_supervisor = (_nova_handler_Supervisor != NULL);", q = queue_var )); } // Plan 47: evaluate the cancel-token expr at scope entry — source // order, `(cancel: tok)` стоит до `{ body }`. Кладём в temp; // bind происходит ниже, после тела, перед run'ом. let cancel_tok_var = if let Some(cexpr) = cancel { let cval = self.emit_expr(cexpr)?; let tv = format!("_nova_cancel_tok_{}", id); self.line(&format!("NovaCancelToken* {} = {};", tv, cval)); // Plan 83.11 §11.4 Option A: pin cancel token in scope.ctx_pins so // it survives GC sweeps triggered by ctx_pins array doubling at // ~512 spawned fibers. Without this, conservative scan can miss // tok-in-register at GC trigger time → reallocation aliases tok // address with NovaSpawnCtxBase (structural overlap at offset +8 // — bound_scope vs _nova_parent_scope) → "token already bound to // a live scope" panic при последующем bind. ctx_pins[] is rooted // on the supervised scope's stack frame, so it tracks the token // as a GC root until scope-end. Closes [M-83.11-gc-cancel-token-alias]. self.line(&format!("nova_scope_pin_ctx(&{}, (void*){});", queue_var, tv)); Some(tv) } else { None }; // Plan 221.1 №169: `nova_scope_init`'s own inheritance // (`q->deadline_ns = _nova_active_scope ? ... : 0`, fibers.h) only // sees a deadline through the RUNTIME `_nova_active_scope` TLS chain // — which D439 (comment above, `prev_scope_var`) deliberately does // NOT repoint at a directly (non-`spawn`) entered enclosing // `supervised{}` block, since that block's own body statements // execute BEFORE `_nova_active_scope` would ever be assigned to it. // A LEXICALLY nested `supervised { supervised(timeout:) { … } }` // (no intervening `spawn`) therefore silently loses the outer // block's tightened deadline through that path — the inner block's // OWN `nova_deadline_combine` call below only ever sees whatever // ambient (usually 0, or an unrelated outer-outer) value // `nova_scope_init` picked up. Fix: combine explicitly against the // LEXICALLY enclosing queue variable's `deadline_ns`, known at // compile time (`self.current_scope_queue`, still the OUTER value // here — only replaced with our own `queue_var` further down). // No-op (both operands read as the SAME already-inherited value) // whenever the lexical parent is ALSO the runtime-ambient scope // (e.g. a `spawn`'d child's own nested `supervised{}` — there // `_nova_active_scope` already correctly equals the parent, per // `nova_supervised_step`'s wrap) — this only changes behaviour for // the genuinely-broken direct-nesting case. if let Some(enclosing_q) = self.current_scope_queue.clone() { self.line(&format!( "{q}.deadline_ns = nova_deadline_combine({q}.deadline_ns, ({p}).deadline_ns);", q = queue_var, p = enclosing_q )); } // Plan 174 (D349): scope deadline. `deadline:` = absolute Monotonic // point; `timeout:` = relative Duration (sugar for `Monotonic.now() + // d`). Both lower to an absolute monotonic-ns i64. Computed at scope // entry (source order, before body) so nested scopes inherit the // tightened point via nova_scope_init. nova_deadline_combine keeps the // earliest of the inherited (nova_scope_init) and this local point — // an inner scope can only TIGHTEN, never extend (план 173 §3a). // Emitted ONLY when a deadline/timeout is present → plain and cancel- // only scopes stay byte-identical. // The type checker validates the argument type (Monotonic for // `deadline:`, Duration for `timeout:`) and rejects everything else // with a structured diagnostic — see `check_supervised_deadline_type`. // Codegen therefore trusts the checker and extracts the single `i64 // nanos` field of the value-record (both are single-field `value` // records — `.nanos` is a plain struct access; if the checker were // bypassed the C compiler would reject `.nanos` on a non-record). if let Some(dl) = deadline { let v = self.emit_expr(&dl.expr)?; if dl.relative { // `timeout: <Duration>` — relative offset from now. self.line(&format!( "{}.deadline_ns = nova_deadline_combine({}.deadline_ns, \ time_monotonic_ns() + (int64_t)(({}).nanos));", queue_var, queue_var, v )); } else { // `deadline: <Monotonic>` — absolute point on the monotonic clock. self.line(&format!( "{}.deadline_ns = nova_deadline_combine({}.deadline_ns, \ (int64_t)(({}).nanos));", queue_var, queue_var, v )); } } // D449: evaluate the `on_timeout:` handler expr ONCE at scope entry // — source order, `on_timeout: h` stands before `{ body }`, same as // `cancel:`/`deadline:` above. Stored in a stable local (plain C // stack var — Boehm's conservative scan covers it, no TLS-pin // needed the way `with Fail`'s handler value requires) because the // catch-branch below reads it a second time; re-evaluating the // source expression there would re-run any side effects and could // observe a different closure value entirely. let on_timeout_var = if let Some(ohexpr) = on_timeout { // [M-on-timeout-closure-param-ty]: an UNANNOTATED closure literal // (`|_e| Err(ReadFailed)`, the D449-canonical form) would // otherwise have its param typed by `emit_lambda`'s OWN default // fallback chain — which, reached through the plain // `ExprKind::ClosureLight` dispatch (`context_param_tys: None`), // bottoms out at `nova_int` (Plan 70 PhaseA2 Cat D). The // call-site below invokes this closure through a RAW function- // pointer cast to `(ret)(*)(void*, Nova_TimeoutError*)` (no C- // level signature check on that cast — function-pointer casts // are unchecked in C); if the closure's OWN generated body // unpacks its param as `nova_int`, a real read of the handler's // `e.deadline_ns` would reinterpret the pointer bit-pattern as // an integer — silently wrong, not even a crash. Fix: emit the // closure directly via `emit_lambda` (bypassing the generic // `ExprKind::ClosureLight`/`ClosureFull` dispatch arms, which // pass `context_param_tys: None`) with `context_param_tys` // supplying `Nova_TimeoutError*` for its first param — mirrors // `desugar_handler_lambda`'s identical "inject inferred first- // param type" trick for `with Fail[E] = |e| …` (Plan 61 Ф.3). // Also supplies the expected return type (this scope's own // `result_c_ty`, or `nova_unit`) so the body infers/coerces // against the SAME type the construct itself must produce // (D449 "тип конструкции всегда равен типу тела"). A NON- // closure `on_timeout:` (a named `fn` reference, an existing // closure-typed variable, …) already carries its own correct, // independently-checked signature — falls through to the // ordinary `emit_expr` path unchanged. let ret_c_ty = result_c_ty.clone().unwrap_or_else(|| "nova_unit".to_string()); let ov = match &ohexpr.kind { ExprKind::ClosureLight { params, body } => { let legacy_params: Vec<LambdaParam> = params .iter() .map(|p| LambdaParam { name: p.name.clone(), ty: None, span: p.span }) .collect(); let body_expr: Expr = match body { crate::ast::ClosureBody::Expr(e) => (**e).clone(), crate::ast::ClosureBody::Block(b) => Expr::new( ExprKind::Block(b.clone()), b.span, ), }; let ctx = [("Nova_TimeoutError*".to_string(), ret_c_ty.clone())]; self.emit_lambda(&legacy_params, &body_expr, Some(&ctx), None, ohexpr.id)? } ExprKind::ClosureFull(sb) => { let legacy_params: Vec<LambdaParam> = sb.params .iter() .map(|p| LambdaParam { name: p.name.clone(), ty: Some(p.ty.clone()), span: p.span }) .collect(); let body_expr: Expr = match &sb.body { FnBody::Expr(e) => e.clone(), FnBody::Block(b) => Expr::new(ExprKind::Block(b.clone()), b.span), FnBody::External => return Err( "`on_timeout:` handler cannot be `external`".to_string() ), }; let ctx = [("Nova_TimeoutError*".to_string(), ret_c_ty.clone())]; self.emit_lambda(&legacy_params, &body_expr, Some(&ctx), sb.return_type.as_ref(), ohexpr.id)? } _ => self.emit_expr(ohexpr)?, }; let tv = format!("_nova_on_timeout_{}", id); self.line(&format!("void* {} = (void*)({});", tv, ov)); Some(tv) } else { None }; // Plan 221.1 №162 (D439): do NOT repoint _nova_active_scope/_slot at // `queue` — untouched, a direct body blocking op parks correctly on // this coroutine's real (scope,slot). fibers.h (nova_scope_free_slot) + D435. self.line(&format!("NovaFiberQueue* {} = _nova_active_scope;", prev_scope_var)); // D449: open an OUTER catch around everything from here through the // run/join call + active-scope restore — the two places THIS // scope's own TimeoutError can originate (the early-deadline timer, // armed just below, firing while a body statement is parked; or the // join-loop's own deadline gate inside `nova_supervised_run(_cancel)` // further down, D439/№165). A body-direct throw first hits the // INNER body-only guard (`guard_var`, below) which unconditionally // cleans up + `nova_rethrow_scope`s — that re-throw lands HERE // (this frame was pushed first, so it's next on the fail-frame // stack once the inner one pops itself). Only a TimeoutError whose // `error_user_type_id` matches THIS scope's own throw // (`NOVA_TID_USER_TimeoutError`) is handled by calling // `on_timeout`; anything else (a real body USER error, a nested // scope's own independent failure, a PANIC, …) is re-thrown // unchanged — this frame is transparent to everything except its // own scope's deadline. Gated on `on_timeout_var` — a plain // `supervised(timeout:)` (no `on_timeout:`) emits none of this, // byte-identical to before (D449 compatibility requirement). let on_timeout_frame = on_timeout_var.as_ref().map(|_| { let f = format!("_nova_ontimeout_frame_{}", id); self.line(&format!("NovaFailFrame {};", f)); self.line(&format!("nova_fail_push(&{});", f)); self.line(&format!("if (setjmp({}.jmp) == 0) {{", f)); self.indent += 1; f }); // Plan 221.1 №165: arm the early-deadline timer (fibers.h) NOW — // `queue_var.deadline_ns` has its final value (inherited via // nova_scope_init + the lexical combine above + this block's own // local `deadline:`/`timeout:` combine). Unconditional call: the // runtime function itself is a cheap guarded no-op whenever // `deadline_ns == 0` (the overwhelming common case — plain/ // cancel-only `supervised{}`), so this does not need Rust-side // gating. Closes the gap where `nova_supervised_run_impl`'s OWN // (later, join-loop) deadline gate cannot fire because the owner is // still executing body statements — see D439 amendment. let early_dl_var = format!("_nova_early_dl_{}", id); self.line(&format!( "void* {} = nova_scope_arm_early_deadline(&{});", early_dl_var, queue_var )); // Plan 221.1 №165: bind the cancel token EARLY — BEFORE body runs, // not after. Binding used to happen right before // `nova_supervised_run_cancel` (i.e. strictly AFTER every body // statement had already executed) specifically so a body that // throws directly wouldn't leave `tok` dangling-bound to a stack // frame `nova_supervised_run_cancel` never got a chance to unbind // from. But a DIRECT (non-`spawn`) blocking op in the body — D439's // whole point — parks WHILE executing a body statement; `tok.cancel()` // firing during that park read `t->bound_scope == NULL` (confirmed // empirically: [M-supervised-cancel-no-interrupt-parked-accept]) and // did nothing at all — cancel was structurally unreachable for the // entire body-execution window, matching the live serve() repro // byte for byte. Fix: bind here, and reinstate the original // dangling-bound_scope protection with a narrow, LOCAL try/finally // (below) around JUST the body-statement-execution window instead — // a body that throws directly now unbinds THERE before // propagating, so `tok` never outlives the frame it's bound to on // ANY path. Gated on `cancel_tok_var`/`deadline` — a plain // `supervised{}` (no cancel:, no deadline:) emits neither the bind // nor the guard, byte-identical to before. if let Some(tv) = &cancel_tok_var { self.line(&format!("nova_cancel_token_bind({}, &{});", tv, queue_var)); } let need_body_guard = cancel_tok_var.is_some() || deadline.is_some(); let guard_var = if need_body_guard { let g = format!("_nova_sup_guard_{}", id); self.line(&format!("NovaFailFrame {};", g)); self.line(&format!("nova_fail_push(&{});", g)); self.line(&format!("if (setjmp({}.jmp) == 0) {{", g)); self.indent += 1; Some(g) } else { None }; // Activate scope: spawn inside body routes into queue. let prev = std::mem::replace(&mut self.current_scope_queue, Some(queue_var.clone())); // Emit body statements with defer scope (supervised body can contain defer). // Plan 173.1 Ф.1: trailing expression is deferred — evaluated AFTER // `nova_supervised_run` below (post-join), not here. Statements still run // eagerly (they're what enqueue `spawn`s in the first place). let block_id = self.enter_defer_scope(body, false); for stmt in &body.stmts { self.emit_stmt(stmt)?; } // Unit-typed trailing (see [M-supervised-value-unit-trailing] above) stays // eager/pre-join — matches the OLD behaviour exactly (evaluate, discard). // `result_var` is `None` here, so nothing runs post-join for this case. if result_var.is_none() { if let Some(trailing) = &body.trailing { let v = self.emit_expr(trailing)?; self.line(&format!("(void)({});", v)); } } self.leave_defer_scope(block_id); // Restore scope state. self.current_scope_queue = prev; // Plan 221.1 №165: close the body-execution guard opened above. // Normal path: pop the fail-frame, fall through. Catch path: pop, // then release EXACTLY what a direct-body-throw would otherwise // leave dangling — the early-deadline timer (self-cleaning even // without this, see its own doc — this is belt-and-suspenders) and // the cancel-token bind (NOT self-cleaning — this IS the fix) — // then re-throw the SAME error (kind/msg/payload/suppressed // preserved via nova_rethrow_scope, the established Plan 201 // explicit-suppressed re-throw point) so this scope's own // behaviour on a genuine body error is otherwise unchanged. if let Some(g) = &guard_var { self.line(&format!("nova_fail_pop();")); self.indent -= 1; self.line("} else {"); self.indent += 1; self.line(&format!("nova_fail_pop();")); self.line(&format!("nova_scope_disarm_early_deadline({});", early_dl_var)); if let Some(tv) = &cancel_tok_var { self.line(&format!("nova_cancel_token_unbind({});", tv)); } self.line(&format!( "nova_rethrow_scope({g}.error_msg.ptr, {g}.error_kind, \ {g}.error_user_payload, {g}.error_user_type_id, {g}.error_suppressed);", g = g )); self.indent -= 1; self.line("}"); } // Plan 221.1 №165: disarm the early-deadline timer — body statements // (the only thing it exists to protect) are done; from here on // nova_supervised_run(_cancel)'s own join-loop deadline gate takes // over for any registered `spawn` children. Safe no-op if the timer // already fired (CAS-guarded in the runtime helper), was never // armed (deadline_ns == 0), or was already disarmed by the guard's // catch-arm above (normal path only reaches this line). self.line(&format!("nova_scope_disarm_early_deadline({});", early_dl_var)); // Run the scheduler: round-robin until all fibers in queue are dead. // Plan 47/№165: cancel-token bind now happens EARLY (above, before // body) — see that comment. `nova_supervised_run_cancel` still owns // unbind for ITS OWN exit paths (normal return / re-throw / // interrupt / timeout longjmp), unchanged. if let Some(tv) = &cancel_tok_var { self.line(&format!("nova_supervised_run_cancel(&{}, {});", queue_var, tv)); } else { self.line(&format!("nova_supervised_run(&{});", queue_var)); } // Restore previous active scope (may be NULL or outer scope). self.line(&format!("_nova_active_scope = {};", prev_scope_var)); // Plan 173.1 Ф.1: NOW (post-join — every spawned child has run to // completion or the scope re-threw on failure/cancel above) evaluate the // deferred trailing expression and store it into the outer-declared // result var. Mutations/results children produced (mut-captures, or — // for `parallel for`'s channel desugar, Ф.2 — a drain-fiber-populated // accumulator) are visible here. if let Some(rv) = &result_var { let trailing = body.trailing.as_ref() .expect("result_var is Some only when body.trailing is Some"); let v = self.emit_expr(trailing)?; self.line(&format!("{} = {};", rv, v)); } // D449: close the outer on_timeout catch opened above. Catch // branch: restore `_nova_active_scope` (this frame is standing in // for what a `with Fail[TimeoutError]` wrapping the whole scope // from outside would do — same longjmp-safety hazard the // `with`-block's own `active_scope_save` documents: skipping this // leaves `_nova_active_scope` dangling at this freed frame, and the // NEXT scope_init would inherit its garbage `deadline_ns`). Then // dispatch on the caught error's type: THIS scope's own // TimeoutError → call `on_timeout(e)`, cast to the body's own // return type (or `nova_unit` when the body has none — the call // still happens, for its side effects / so a misbehaving handler's // own throw is observable, just discarded); anything else → // re-throw unchanged via the same `nova_rethrow_scope` point the // inner body-guard already uses. if let Some(f) = &on_timeout_frame { self.line("nova_fail_pop();"); self.indent -= 1; self.line("} else {"); self.indent += 1; self.line("nova_fail_pop();"); self.line(&format!("_nova_active_scope = {};", prev_scope_var)); self.line(&format!( "if ({f}.error_user_type_id == NOVA_TID_USER_TimeoutError) {{", f = f )); self.indent += 1; let callee = on_timeout_var.as_ref() .expect("on_timeout_frame is Some only when on_timeout_var is Some"); let ret_for_call = result_c_ty.clone().unwrap_or_else(|| "nova_unit".to_string()); let call_expr = format!( "(({ret}(*)(void*, Nova_TimeoutError*))(((NovaClosBase*)({cb}))->fn))\ (((NovaClosBase*)({cb}))->env, (Nova_TimeoutError*)({f}.error_user_payload))", ret = ret_for_call, cb = callee, f = f ); match &result_var { Some(rv) => self.line(&format!("{} = {};", rv, call_expr)), None => self.line(&format!("(void)({});", call_expr)), } self.indent -= 1; self.line("} else {"); self.indent += 1; self.line(&format!( "nova_rethrow_scope({f}.error_msg.ptr, {f}.error_kind, \ {f}.error_user_payload, {f}.error_user_type_id, {f}.error_suppressed);", f = f )); self.indent -= 1; self.line("}"); self.indent -= 1; self.line("}"); } // Plan 266 (D455): wrap into `Outcome[T]` — `Aborted` if the token // was cancelled, `Finished(v)` otherwise. Deliberately checked here, // AFTER `result_var` (if any) is already filled just above: this // reuses the EXISTING unconditional post-join evaluation of the // trailing (unchanged timing/side-effects for every other form — // D449's `on_timeout:` cast, plain `T` forms, the cancel+timeout // combination — all byte-identical to before this plan) rather than // making trailing-evaluation itself conditional on cancellation. // Consequence, stated plainly: a non-unit trailing still runs even // when the scope was cancelled — its value is simply not the one // that reaches the caller (discarded in favor of `Aborted`). `tok` // (`cancel_tok_var`) is read via `nova_cancel_token_is_cancelled` // AFTER `nova_supervised_run_cancel` has drained every spawned // child — the same "ask after the scope" ordering D75's own doc // prescribes for the pre-266 manual `tok.is_cancelled()` idiom // (06-concurrency.md:1275) — so this is the FIRST point where the // answer is final. if let Some(mangled) = &outcome_mangled { let ov = outcome_result_var.as_ref() .expect("outcome_result_var is Some whenever outcome_mangled is Some"); let tok = cancel_tok_var.as_ref() .expect("outcome_mangled is Some only when is_outcome_form, which requires cancel: (cancel_tok_var Some)"); self.line(&format!("if (nova_cancel_token_is_cancelled({})) {{", tok)); self.indent += 1; self.line(&format!("{} = nova_make_{}_Aborted();", ov, mangled)); self.indent -= 1; self.line("} else {"); self.indent += 1; let payload = result_var.clone().unwrap_or_else(|| "NOVA_UNIT".to_string()); self.line(&format!("{} = nova_make_{}_Finished({});", ov, mangled, payload)); self.indent -= 1; self.line("}"); } self.indent -= 1; self.line("}"); // supervised expression evaluates to its trailing value (Ф.1) or unit // when the body has no trailing expression — UNLESS this is the // D455 `Outcome[T]`-wrapped form (Plan 266), which evaluates to the // wrapped `outcome_result_var` instead (`Finished(v)`/`Aborted`). Ok(outcome_result_var.or(result_var).unwrap_or_else(|| "NOVA_UNIT".to_string())) } /// Emit `parallel for x in iter { body }` — D14 fan-out via desugar to /// `supervised { for x in iter { spawn { body } } }`. Each iteration spawns /// a fiber capturing the loop-variable BY VALUE (immutable scalar) so all /// queued fibers see their own snapshot. /// /// D71 / Plan 173.1 Ф.2 `parallel for → []T`: when `body` has a trailing /// expression, the parallel-for evaluates to `[]T` (≡ `Vec[T]`, D239) for /// ANY element type T and ANY iterator — collection is CHANNEL-based /// (§2.3, sign-off 2026-06-21): /// /// 1. a bounded channel `K = min(len, CAP)` (CAP=16) is created in the /// scope (back-pressure buffer, NOT O(N)); /// 2. each iteration's spawn gets an OWNED `Sender` clone — cloned in the /// parent AT the spawn site (refcount++ strictly before the parent tx /// close), moved into the child, closed by the child on every /// fiber-exit path (see `emit_spawn`'s parfor-send mode); /// 3. the child SENDS its trailing value (int scalars direct, heap /// pointers via intptr, value types boxed — `parfor_chan_repr`); /// 4. the parent tx closes after the enqueue loop; a dedicated DRAIN /// fiber inside the same scope `recv()`s until `None` (= last child /// clone closed) and pushes into the result Vec — dense, /// completion-order, no index slots, no concurrent push (single /// drainer); /// 5. `nova_supervised_run` joins children + drain; the Vec is the /// expression value. A child that threw never sent → its element is /// absent (dense). Child failure cancels siblings + rethrows after /// drain (173.0 substrate) → the accumulator is not returned. /// /// No deadlock: the drain runs concurrently with the children inside the /// scope; producers parked on a full buffer are woken by the drain's recv. /// When body has no trailing (purely effectful), the form yields unit. fn emit_parallel_for( &mut self, pattern: &Pattern, iter: &Expr, body: &Block, expr_id: crate::ast::ExprId, ) -> Result<String, String> { use crate::diag::Span; let span = Span::dummy(); // Detect array-mode: body has a trailing expression that yields a value. // In that case we evaluate the trailing's type to size the result array // and route each spawn's value into result[idx]. let array_mode = body.trailing.is_some(); if !array_mode { // Plan 83.4.5.10 Ф.3 (2026-05-24): inline-threshold optimization. // Для коротких parallel-for (iter_count ≤ NOVA_PARALLEL_INLINE_THRESHOLD, // default 32) бежать кооперативно inline (regular for-loop в main // thread) вместо worker pool. Avoids spawn overhead (~5-10ms per // spawn) для batch'ей где overhead больше gain'а. // // Bench measurement (Plan 83.4.5.6 closure): 16 × fib(33) parallel // = ~518ms vs sequential = ~345ms. Worker pool overhead dominates // для коротких задач. Inline path даёт parallel ≈ sequential // (≥1× speedup acceptance MET). // // V1: только Range-iter поддерживает inline check (compute // iter_count = end - start at runtime). Non-Range (ArrayLit, Ident) // → fall back к spawn path (V2 followup). // // Cross-runtime parity: tokio rayon `join_context` adaptive // splitting / Java ForkJoinPool task granularity threshold. let supports_threshold = matches!(&iter.kind, ExprKind::Range { .. }); if supports_threshold { if let ExprKind::Range { start, end, inclusive } = &iter.kind { let start = start.as_deref().ok_or_else(|| "parallel for: open-ended Range without start bound (Plan 96)".to_string())?; let end = end.as_deref().ok_or_else(|| "parallel for: open-ended Range without end bound (Plan 96)".to_string())?; let s = self.emit_expr(start)?; let e = self.emit_expr(end)?; let plus_one = if *inclusive { " + 1" } else { "" }; self.line("{"); self.indent += 1; self.line(&format!("nova_int _nova_par_count = ({} - {}{});", e, s, plus_one)); self.line("if (_nova_par_count <= (nova_int)nova_runtime_parallel_inline_threshold()) {"); self.indent += 1; // Inline cooperative path — plain for-loop on caller thread. // НИКАКОГО spawn'а, supervised'а — body runs sequentially в // main thread'а. Под workloads с iter_count ≤ threshold // overhead spawn'ов превышает выгоду параллелизма. let plain_for = Expr::new( ExprKind::For { pattern: pattern.clone(), iter: Box::new(iter.clone()), body: body.clone(), elem_type: None, invariants: vec![], decreases: None, iter_consume: false, }, span, ); let plain_for_emit = self.emit_expr(&plain_for)?; self.line(&format!("(void)({});", plain_for_emit)); self.indent -= 1; self.line("} else {"); self.indent += 1; // Spawn path — existing desugar. let spawn_body_expr = Expr::new(ExprKind::Block(body.clone()), span); let spawn_expr = Expr::new(ExprKind::Spawn(Box::new(spawn_body_expr)), span); let for_body = Block { stmts: vec![Stmt::Expr(spawn_expr)], trailing: None, span, is_unsafe: false }; let for_expr = Expr::new( ExprKind::For { pattern: pattern.clone(), iter: Box::new(iter.clone()), body: for_body, elem_type: None, invariants: vec![], decreases: None, iter_consume: false }, span, ); let supervised_block = Block { stmts: vec![Stmt::Expr(for_expr)], trailing: None, span, is_unsafe: false }; self.emit_supervised(&supervised_block, None, None, None)?; self.indent -= 1; self.line("}"); self.indent -= 1; self.line("}"); return Ok("NOVA_UNIT".to_string()); } } // Statement-mode (non-Range): legacy desugar без threshold check. let spawn_body_expr = Expr::new(ExprKind::Block(body.clone()), span); let spawn_expr = Expr::new(ExprKind::Spawn(Box::new(spawn_body_expr)), span); let for_body = Block { stmts: vec![Stmt::Expr(spawn_expr)], trailing: None, span, is_unsafe: false }; let for_expr = Expr::new( ExprKind::For { pattern: pattern.clone(), iter: Box::new(iter.clone()), body: for_body, elem_type: None, invariants: vec![], decreases: None, iter_consume: false }, span, ); let supervised_block = Block { stmts: vec![Stmt::Expr(for_expr)], trailing: None, span, is_unsafe: false }; return self.emit_supervised(&supervised_block, None, None, None); } // ────────────────────────────────────────────────────────────────── // Array-mode — Plan 173.1 Ф.2: channel-collected `[]T` for ANY element // type and ANY iterator (closes [M-parfor-record-result-miscompile]). // ────────────────────────────────────────────────────────────────── let trailing = body.trailing.as_ref().unwrap(); let id = self.supervised_counter; self.supervised_counter += 1; // (1) Element C type. Preferred source: the checker's whole-expression // annotation (ParallelFor : Vec[T] — types/mod.rs f1 preamble, §0). // Fallback: probe the trailing with the loop variable transiently // bound to the iterator's element C type (mirrors the checker's own // body-scope extension). let ann_elem: Option<crate::types::ResolvedType> = if expr_id.is_set() { match self.resolved_types.get(&expr_id) { Some(crate::types::ResolvedType::Named { name, args, .. }) if name == "Vec" && args.len() == 1 => Some(args[0].clone()), _ => None, } } else { None }; let mut elem_c: Option<String> = ann_elem .and_then(|rt| self.resolved_type_to_c(&rt).ok()) .filter(|c| !c.is_empty() && !self.debt_is_generic_stub_c(c)); if elem_c.is_none() { let loop_var_c: Option<String> = match &iter.kind { ExprKind::Range { .. } => Some("nova_int".to_string()), _ => { let it_c = self.infer_expr_c_type(iter); self.vec_or_array_elem_c(&it_c) } }; if let (Pattern::Ident { name, .. }, Some(lc)) = (pattern, loop_var_c) { let old = self.var_types.insert(name.clone(), lc); let probed = self.infer_expr_c_type(trailing); match old { Some(t) => { self.var_types.insert(name.clone(), t); } None => { self.var_types.remove(name); } } if !probed.is_empty() && !self.debt_is_generic_stub_c(&probed) { elem_c = Some(probed); } } } let elem_c = elem_c.ok_or_else(|| { "parallel for → []T: element type could not be inferred \ (no checker Vec[T] annotation and the trailing probe failed) — \ annotate the loop variable (`parallel for x TYPE in …`, Plan 87)" .to_string() })?; // (2) Bounded buffer K = min(len, CAP), CAP = 16 (Ф.3 §2.3): the drain // is a single fast consumer — the buffer only has to absorb the burst // of simultaneously-active producers, NOT the whole input (memory // O(CAP), back-pressure via send-park on full). `len` is used only // when the source offers it for free (Range bounds / array-literal // arity / a Vec-typed local); lazy or opaque iterators go straight to // CAP. Range bounds are materialised ONCE into temps (reused by the // For loop below) so side-effectful bounds are not evaluated twice. const PARFOR_CHAN_CAP: i64 = 16; let mut for_iter: Expr = iter.clone(); let k_expr: String = match &iter.kind { ExprKind::Range { start: Some(s), end: Some(e), inclusive } => { let s_c = self.emit_expr(s)?; let e_c = self.emit_expr(e)?; let lo = format!("_nova_pf_lo_{}", id); let hi = format!("_nova_pf_hi_{}", id); self.line(&format!("nova_int {} = {};", lo, s_c)); self.line(&format!("nova_int {} = {};", hi, e_c)); self.var_types.insert(lo.clone(), "nova_int".to_string()); self.var_types.insert(hi.clone(), "nova_int".to_string()); for_iter = Expr::new( ExprKind::Range { start: Some(Box::new(Expr::new(ExprKind::Ident(lo.clone()), span))), end: Some(Box::new(Expr::new(ExprKind::Ident(hi.clone()), span))), inclusive: *inclusive, }, span, ); format!("({} - {}{})", hi, lo, if *inclusive { " + 1" } else { "" }) } ExprKind::ArrayLit(elems) if !elems.iter().any(|el| matches!(el, ArrayElem::Spread(_))) => { elems.len().to_string() } ExprKind::Ident(n) => { let it_c = self.var_types.get(n.as_str()).cloned().unwrap_or_default(); if self.vec_or_array_elem_c(&it_c).is_some() { // Pure variable read — safe to re-emit alongside the loop. let src_c = self.emit_expr(iter)?; format!("(({})->len)", src_c) } else { PARFOR_CHAN_CAP.to_string() } } _ => PARFOR_CHAN_CAP.to_string(), }; // (3) Result Vec — declared OUTSIDE the scope block (no GNU statement- // exprs) so it survives as the expression value. Built via the real // `Vec[T].new()` mono ctor; the drain fiber pushes into it (amortised // growth — the pre-sized slot buffer of the old lowering is gone with // the index slots). let (vec_mangled, vec_ctor, vec_push) = self.vec_mono_ctor_push(&elem_c)?; let acc_var = format!("_nova_par_res_{}", id); // [M-vec-new-cap-default-arg-backfill]: hand-formatted ctor call, no // Nova-AST Call node — see the sibling note in // `try_emit_typed_vec_literal` (the callee's real C signature always // takes `cap`; pass the literal default `0`). self.line(&format!("{ty}* {acc} = {ctor}(0);", ty = vec_mangled, acc = acc_var, ctor = vec_ctor)); self.var_types.insert(acc_var.clone(), format!("{}*", vec_mangled)); // Element-type tracking so `xs[i]` / for-in typing over the result works. self.array_element_types.insert(acc_var.clone(), elem_c.clone()); // (4) Scope block: queue + channel + active-scope swap. let queue_var = format!("_nova_scope_q_{}", id); let prev_scope_var = format!("_nova_prev_scope_{}", id); let tx_var = format!("_nova_pf_tx_{}", id); let rx_var = format!("_nova_pf_rx_{}", id); self.line("{"); self.indent += 1; self.line(&format!("NovaFiberQueue {} = {{0}};", queue_var)); self.line(&format!("nova_scope_init(&{});", queue_var)); // Plan 173.2: same supervisor-mode stamp as emit_supervised — the // array-mode `parallel for` scope is its own supervised scope. if self.effect_schemas.contains_key("Supervisor") { self.line(&format!( "{q}.has_supervisor = (_nova_handler_Supervisor != NULL);", q = queue_var )); } self.line(&format!("nova_int _nova_pf_k_{} = {};", id, k_expr)); self.line(&format!("if (_nova_pf_k_{id} > {cap}) _nova_pf_k_{id} = {cap};", id = id, cap = PARFOR_CHAN_CAP)); // Channel.new rejects cap <= 0; empty input still needs a live channel // (the drain must see closed→None, not a throw). self.line(&format!("if (_nova_pf_k_{id} < 1) _nova_pf_k_{id} = 1;", id = id)); self.line(&format!("Nova_ChannelPair _nova_pf_ch_{id} = nova_channel_new(_nova_pf_k_{id});", id = id)); self.line(&format!("Nova_ChanWriter* {} = _nova_pf_ch_{}.tx;", tx_var, id)); self.line(&format!("Nova_ChanReader* {} = _nova_pf_ch_{}.rx;", rx_var, id)); self.line(&format!("NovaFiberQueue* {} = _nova_active_scope;", prev_scope_var)); self.line(&format!("_nova_active_scope = &{};", queue_var)); let prev = std::mem::replace(&mut self.current_scope_queue, Some(queue_var.clone())); // (5) Enqueue loop — the ORDINARY `for` lowering over the (possibly // rebuilt) iterator handles ANY iterator shape and ANY pattern; the // body is `spawn { <body> }` with parfor-send mode active, so each // spawn site clones the Sender into its child (see emit_spawn). let saved_send = self.current_parfor_send .replace((tx_var.clone(), elem_c.clone())); let spawn_body_expr = Expr::new(ExprKind::Block(body.clone()), span); let spawn_expr = Expr::new(ExprKind::Spawn(Box::new(spawn_body_expr)), span); let for_body = Block { stmts: vec![Stmt::Expr(spawn_expr)], trailing: None, span, is_unsafe: false, }; let for_expr = Expr::new( ExprKind::For { pattern: pattern.clone(), iter: Box::new(for_iter), body: for_body, elem_type: None, invariants: vec![], decreases: None, iter_consume: false, }, span, ); let for_v = self.emit_expr(&for_expr)?; self.line(&format!("(void)({});", for_v)); self.current_parfor_send = saved_send; // (6) Parent tx close — AFTER the enqueue loop: every child clone was // already counted (clone-at-spawn in the parent), so writer_count can // only reach 0 once the LAST child closes its clone. Empty input → // 1→0 right here → channel closed → drain sees None immediately. self.line(&format!("nova_chan_writer_close({});", tx_var)); // (7) Dedicated drain fiber — the single consumer, inside the scope, // concurrent with the children (§2.3: «дренаж ВНУТРИ supervised»). self.emit_parfor_drain_fiber( id, &queue_var, &rx_var, &acc_var, &vec_mangled, &vec_push, &elem_c)?; // (8) Join children + drain; restore scope state. self.current_scope_queue = prev; self.line(&format!("nova_supervised_run(&{});", queue_var)); self.line(&format!("_nova_active_scope = {};", prev_scope_var)); self.indent -= 1; self.line("}"); // The expression value is the collected Vec[T]*. Ok(acc_var) } /// Plan 173.1 Ф.2: the parallel-for DRAIN fiber — the single consumer of /// the collection channel. Emits (a) the ctx typedef + entry forward-decl /// into `lambda_forward_decls` (file scope — same placement as /// `emit_detach`), (b) the spawn-into-scope call at the CURRENT emission /// point, and (c) the entry function into `deferred_impls`. /// /// The ctx base fields MUST mirror `NovaSpawnCtxBase` (fibers.h) 1:1 — /// identical discipline to `emit_spawn`/`emit_detach` (the worker loop /// casts the ctx by common-initial-sequence; a missing/misordered field /// silently corrupts the first payload field). /// /// Body: `for(;;) { recv → None? break : push(unrepr(value)) }` wrapped in /// the standard fiber fail-frame. `recv()` throws on scope cancel (child /// failure auto-cancels siblings — 173.0 substrate), which lands in the /// fail-frame and is reported kinded like any child error, so Escalate /// semantics hold and the drain never hangs a failing scope. #[allow(clippy::too_many_arguments)] fn emit_parfor_drain_fiber( &mut self, id: usize, queue_var: &str, rx_var: &str, acc_var: &str, vec_mangled: &str, vec_push: &str, elem_c: &str, ) -> Result<(), String> { let drain_id = format!("_nova_parfor_drain_{}", id); let ctx_ty = format!("NovaParforDrainCtx_{}", id); let ctx_var = format!("{}_ctx", drain_id); // ── (a) ctx typedef + entry forward-decl (file scope). Base fields // mirror NovaSpawnCtxBase exactly — see emit_spawn for the per-field // rationale (parent_scope/parent_slot/worker_slot/fail-tops/fiber_scope/ // init_snapshot/fiber_state/pool_size/cancel-shield pair/park_state/ // schedlink/#431 fiber-anchor — the anchor LAST). let _ = writeln!(self.lambda_forward_decls, "typedef struct {{"); let _ = writeln!(self.lambda_forward_decls, " NovaFiberQueue* _nova_parent_scope;"); let _ = writeln!(self.lambda_forward_decls, " int _nova_parent_slot;"); let _ = writeln!(self.lambda_forward_decls, " int _nova_worker_slot;"); let _ = writeln!(self.lambda_forward_decls, " NovaFailFrame* _nova_saved_fail_top;"); let _ = writeln!(self.lambda_forward_decls, " NovaInterruptFrame* _nova_saved_interrupt_top;"); let _ = writeln!(self.lambda_forward_decls, " NovaFiberQueue* _nova_fiber_scope;"); let _ = writeln!(self.lambda_forward_decls, " NovaEffectSnapshot* _nova_init_snapshot;"); let _ = writeln!(self.lambda_forward_decls, " nova_atomic_int _nova_fiber_state;"); let _ = writeln!(self.lambda_forward_decls, " size_t _nova_pool_size;"); let _ = writeln!(self.lambda_forward_decls, " nova_atomic_int _nova_cancel_mask_count;"); let _ = writeln!(self.lambda_forward_decls, " int64_t _nova_cancel_deadline_ns;"); let _ = writeln!(self.lambda_forward_decls, " nova_atomic_int _nova_park_state;"); let _ = writeln!(self.lambda_forward_decls, " mco_coro* schedlink;"); // [221.1 №431 остаток] Fiber-exit anchor — LAST base field. self.emit_spawn_ctx_anchor_field(); let _ = writeln!(self.lambda_forward_decls, " Nova_ChanReader* _nova_pf_rx;"); let _ = writeln!(self.lambda_forward_decls, " {}* _nova_pf_acc;", vec_mangled); let _ = writeln!(self.lambda_forward_decls, "}} {};", ctx_ty); let _ = writeln!(self.lambda_forward_decls, "{}void {}(mco_coro* _co);", self.top_level_storage(), drain_id); // ── (b) call site: alloc ctx + spawn into the scope (mirror of // emit_spawn's call-site protocol, incl. pool-acquire under armed M:N // and the spawn-time TLS handler snapshot). self.line(&format!( "nova_bool _nova_is_init_pfd_{} = nova_runtime_is_initialized();", id)); self.line(&format!( "{ty}* {var} = ({ty}*)(_nova_is_init_pfd_{id} ? nova_spawn_pool_acquire(sizeof({ty})) : nova_alloc(sizeof({ty})));", ty = ctx_ty, var = ctx_var, id = id)); self.line(&format!("{}->_nova_pf_rx = {};", ctx_var, rx_var)); self.line(&format!("{}->_nova_pf_acc = {};", ctx_var, acc_var)); self.line(&format!("{}->_nova_worker_slot = -1;", ctx_var)); self.line(&format!("{}->_nova_parent_slot = -1;", ctx_var)); self.line(&format!("if (_nova_is_init_pfd_{}) {{", id)); self.indent += 1; self.line(&format!("{}->_nova_parent_scope = &{};", ctx_var, queue_var)); self.line(&format!( "{}->_nova_init_snapshot = (NovaEffectSnapshot*)nova_alloc(sizeof(NovaEffectSnapshot));", ctx_var)); self.line(&format!("nova_effect_snapshot_save({}->_nova_init_snapshot);", ctx_var)); self.line(&format!("nova_runtime_spawn_into(&{}, {}, {});", queue_var, drain_id, ctx_var)); self.indent -= 1; self.line("} else {"); self.indent += 1; self.line(&format!("{}->_nova_parent_scope = NULL;", ctx_var)); self.line(&format!("{}->_nova_init_snapshot = NULL;", ctx_var)); self.line(&format!("nova_fiber_spawn_into(&{}, {}, {});", queue_var, drain_id, ctx_var)); self.indent -= 1; self.line("}"); // ── (c) entry function → deferred_impls (out-swap like emit_spawn). let saved_out = std::mem::take(&mut self.out); let saved_indent = self.indent; self.indent = 0; // [M-mono-spawn-fwd-decls] parity: inside a monomorphized fn body the // pre-pass never saw this drain — push the decl to the mono splice. if !self.current_type_subst.is_empty() { self.mono_fwd_decls.push_str(&format!( "{}void {}(mco_coro* _co);\n", self.top_level_storage(), drain_id)); } self.line(&format!("{}void {}(mco_coro* _co) {{", self.top_level_storage(), drain_id)); self.indent += 1; self.line(&format!("{ctx}* _c = ({ctx}*)mco_get_user_data(_co);", ctx = ctx_ty)); // [221.1 №431 остаток] Anchor first — see emit_spawn's identical // ordering note (the prologue safepoint below can deliver a cancel // before the root fail-frame exists). self.emit_fiber_anchor_arm(); self.emit_prologue_preempt_check_unconditional(); self.line("if (_c->_nova_parent_scope) {"); self.indent += 1; self.line("_nova_active_slot = nova_scope_alloc_slot(_nova_active_scope, _co);"); self.line("_c->_nova_worker_slot = _nova_active_slot;"); self.line("_c->_nova_fiber_scope = _nova_active_scope;"); self.line("if (_c->_nova_init_snapshot && _nova_active_slot >= 0) {"); self.indent += 1; self.line("_nova_active_scope->fiber_effect_snapshot[_nova_active_slot] = _c->_nova_init_snapshot;"); self.line("_c->_nova_init_snapshot = NULL;"); self.indent -= 1; self.line("}"); self.indent -= 1; self.line("} else {"); self.indent += 1; self.line("_c->_nova_worker_slot = -1;"); self.indent -= 1; self.line("}"); self.line("NovaFailFrame _ff;"); self.line("nova_fail_push(&_ff);"); self.line("if (setjmp(_ff.jmp) == 0) {"); self.indent += 1; self.line("for (;;) {"); self.indent += 1; self.line("NovaOpt_nova_int _r = nova_chan_reader_recv(_c->_nova_pf_rx);"); self.line("if (_r.tag != NOVA_TAG_Option_Some) break;"); // Un-transport per the SHARED repr policy (must mirror emit_spawn's // send side — see `parfor_chan_repr`). match Self::parfor_chan_repr(elem_c) { ParforChanRepr::IntScalar => { self.line(&format!( "(void){push}(_c->_nova_pf_acc, ({ty})_r.value);", push = vec_push, ty = elem_c)); } ParforChanRepr::Pointer => { self.line(&format!( "(void){push}(_c->_nova_pf_acc, ({ty})(intptr_t)_r.value);", push = vec_push, ty = elem_c)); } ParforChanRepr::Boxed => { self.line(&format!( "(void){push}(_c->_nova_pf_acc, *({ty}*)(intptr_t)_r.value);", push = vec_push, ty = elem_c)); } } self.indent -= 1; self.line("}"); self.line("nova_fail_pop();"); self.indent -= 1; self.line("} else {"); self.indent += 1; self.line("nova_fail_pop();"); // Identical error routing to emit_spawn: interrupt sentinel passes // through silently; real errors (incl. the CANCEL thrown by recv when // a failing child auto-cancels the scope) are reported kinded. self.line("if (_ff.error_msg.ptr && _ff.error_msg.len == 18 && memcmp(_ff.error_msg.ptr, \"__nova_interrupt__\", 18) == 0) {"); self.indent += 1; self.line("/* interrupt: scope state already set, fiber dies cleanly */"); self.indent -= 1; self.line("} else {"); self.indent += 1; self.line("if (_c->_nova_parent_scope) {"); self.indent += 1; self.line("nova_fiber_report_child_kinded(_c, _ff.error_msg.ptr, _ff.error_kind, _ff.error_reason_ptr, _ff.error_user_payload, _ff.error_user_type_id);"); self.indent -= 1; self.line("} else {"); self.indent += 1; self.line("nova_fiber_report_error_kinded(_ff.error_msg.ptr, _ff.error_kind, _ff.error_reason_ptr);"); self.indent -= 1; self.line("}"); self.indent -= 1; self.line("}"); self.indent -= 1; self.line("}"); // [221.1 №431 остаток] Close the anchor region + disarm before the // shared epilogue — identical protocol to emit_spawn. self.emit_fiber_anchor_close(); // Remote fiber post-completion cleanup — identical to emit_spawn. self.line("if (_c->_nova_parent_scope) {"); self.indent += 1; self.line("if (_c->_nova_worker_slot >= 0) {"); self.indent += 1; self.line("nova_scope_free_slot(_nova_active_scope, _c->_nova_worker_slot);"); self.line("_nova_active_slot = -1;"); self.indent -= 1; self.line("}"); // [196.6 / D228 §6 class]: pending_sweeps++ strictly BEFORE the // pending_remote release-decrement (same thread, program order) — // the scope owner that observes pending_remote==0 therefore also // observes pending_sweeps>0 until the worker's post-mortem sweep // (nova_scope_sweep_dead_child) release-decrements it. Closes the // stack-scope use-after-return in the sweep (Plan 198 floating AV; // docs/plans/196.6-race-state-dump-notes.md). self.line("(void)nova_aint_inc(&_c->_nova_parent_scope->pending_sweeps);"); self.line("(void)nova_aint_fetch_sub_release(&_c->_nova_parent_scope->pending_remote);"); self.line("nova_runtime_signal_main();"); self.indent -= 1; self.line("}"); self.indent -= 1; self.line("}"); self.line(""); let entry_code = std::mem::replace(&mut self.out, saved_out); self.deferred_impls.push_str(&entry_code); self.indent = saved_indent; Ok(()) } /// Emit `blocking { body }` — Plan 83.3 Ф.4.2 (D50): real threadpool /// offload. Block-extraction по образцу `emit_spawn`, но проще — тело /// не fiber и не scheduled, а обычная leaf-функция для libuv /// threadpool: /// 1. capture-анализ свободных переменных тела → ctx-struct; /// 2. сгенерированная `void _nova_blk_N(void*)` распаковывает ctx, /// выполняет тело, пишет результат в `_c->_nova_result`; /// 3. на месте `blocking { }`: stack-local ctx + context-sensitive /// ветка — в fiber'е `nova_blocking_offload` (worker свободен на /// время блокирующей работы), на main-потоке тело inline (нет /// worker'а пинить); /// 4. значение `blocking { }` = `ctx._nova_result` (тип тела). /// /// ctx — stack-local текущего C-кадра: `nova_blocking_offload` /// синхронен с точки зрения fiber'а (паркует его до завершения /// work_cb), стек-кадр fiber'а (mco-coroutine) переживает park — /// `&ctx`, переданный на threadpool-поток, остаётся валиден. /// /// V1-контракт (D50, [M-83.3-blocking-leaf-contract]): тело — leaf /// (FFI/syscall без GC-аллокации и без control-flow-escape наружу). fn emit_blocking(&mut self, body: &Block) -> Result<String, String> { let blk_id = format!("_nova_blk_{}", self.blocking_counter); self.blocking_counter += 1; // Тип результата = тип trailing expr тела (как у block-expr). let result_ty = body.trailing.as_ref() .map(|e| self.infer_expr_c_type(e)) .unwrap_or_else(|| "nova_unit".into()); let has_result = result_ty != "nova_unit"; // ─── capture-анализ (по образцу emit_spawn) ─── let mut refs: Vec<String> = Vec::new(); Self::collect_idents_block(body, &mut refs); refs.sort(); refs.dedup(); let mut bound: HashSet<String> = HashSet::new(); Self::collect_bound_names_block(body, &mut bound); // Захват: immutable → by-value (snapshot), mutable → by-pointer (shared // mutation видна после wake). Тот же критерий, что у spawn. // [M-parfor-loopvar-nonscalar-byref-capture] (2026-07-13): by-value gate // widened to ANY immutable capture (was scalar-only) — see emit_spawn's // capture-analysis comment for the full rationale. let mut captures: Vec<(String, String, bool)> = Vec::new(); for name in refs { if bound.contains(&name) { continue; } // [M-spawn-module-const-capture]: module-level const — resolves to // its mangled file-scope global, never a capture (see emit_spawn). if self.private_const_c_names .contains_key(&(body.span.file_id, name.clone())) { continue; } if let Some(ty) = self.var_types.get(&name).cloned() { let is_mut = self.var_mutable.contains(&name); let by_value = !is_mut; captures.push((name, ty, by_value)); } } let ctx_ty = format!("NovaBlkCtx_{}", &blk_id[1..]); // strip leading _ let ctx_var = format!("{}_ctx", blk_id); // ─── ctx-struct typedef + work-fn forward-decl → lambda_forward_decls ─── // (file scope, flushed before fn definitions — виден и на месте // blocking{}, и в deferred_impls'е с телом _nova_blk_N). let _ = writeln!(self.lambda_forward_decls, "typedef struct {{"); for (cap, ty, by_value) in &captures { if *by_value { let _ = writeln!(self.lambda_forward_decls, " {} {};", ty, cap); } else { let _ = writeln!(self.lambda_forward_decls, " {}* {};", ty, cap); } } if has_result { let _ = writeln!(self.lambda_forward_decls, " {} _nova_result;", result_ty); } let _ = writeln!(self.lambda_forward_decls, "}} {};", ctx_ty); let _ = writeln!(self.lambda_forward_decls, "{}void {}(void* _blk_arg);", self.top_level_storage(), blk_id); // ─── ctx-инстанс на стеке текущего кадра + заполнение захватов ─── self.line(&format!("{} {};", ctx_ty, ctx_var)); for (cap, _, by_value) in &captures { // Если cap сам — захват enclosing-fiber'а (spawn/blocking), // поле outer-ctx это T (by-value) либо T* (by-pointer). let is_outer_cap = self.current_spawn_captures.as_ref() .map(|s| s.contains(cap)).unwrap_or(false); let outer_by_value = self.current_spawn_capture_by_value.as_ref() .map(|s| s.contains(cap)).unwrap_or(false); let access_outer = if is_outer_cap { if outer_by_value { format!("_c->{}", cap) } else { format!("(*_c->{})", cap) } } else { cap.clone() }; let address_outer = if is_outer_cap { if outer_by_value { format!("&_c->{}", cap) } else { format!("_c->{}", cap) } } else { format!("&{}", cap) }; if *by_value { self.line(&format!("{}.{} = {};", ctx_var, cap, access_outer)); } else { self.line(&format!("{}.{} = {};", ctx_var, cap, address_outer)); } } // ─── context-sensitive вызов ─── // В fiber'е (mco_running) — offload на libuv threadpool: fiber // паркуется, worker свободен. На main-потоке нет worker'а пинить // → тело выполняется inline тем же _nova_blk_N. self.line("if (mco_running()) {"); self.indent += 1; self.line(&format!( "nova_blocking_offload(_nova_active_scope, _nova_active_slot, {}, &{});", blk_id, ctx_var)); self.indent -= 1; self.line("} else {"); self.indent += 1; self.line(&format!("{}(&{});", blk_id, ctx_var)); self.indent -= 1; self.line("}"); // ─── эмиссия тела work-функции в deferred_impls ─── let saved_out = std::mem::take(&mut self.out); let saved_indent = self.indent; self.indent = 0; // Plan 175 Ф.2-v2 ([M-spawn-var-boxed-leak] class, mirrors emit_spawn): // isolate `var_boxed` — this work-fn body resolves captures via // `current_spawn_captures`, not `var_boxed`. let saved_var_boxed_blk = std::mem::take(&mut self.var_boxed); self.line(&format!("{}void {}(void* _blk_arg) {{", self.top_level_storage(), blk_id)); self.indent += 1; self.line(&format!("{}* _c = ({}*)_blk_arg;", ctx_ty, ctx_ty)); // _c может быть unused (нет захватов и нет результата) — глушим. self.line("(void)_c;"); // Активировать capture-rewriting: ExprKind::Ident → `_c->name` // (by-value) либо `(*_c->name)` (by-pointer). Переиспользуем тот // же механизм, что у spawn-тел. let mut cap_set: HashSet<String> = HashSet::new(); let mut cap_by_value: HashSet<String> = HashSet::new(); for (cap, _, by_value) in &captures { cap_set.insert(cap.clone()); if *by_value { cap_by_value.insert(cap.clone()); } } let prev_caps = std::mem::replace(&mut self.current_spawn_captures, Some(cap_set)); let prev_by_value = std::mem::replace( &mut self.current_spawn_capture_by_value, Some(cap_by_value)); // Plan 103.6: set in_blocking flag for blocking body — park-ing sync // calls are forbidden inside blocking{} (E_BLOCKING_SYNC_PARK). let prev_in_blocking = self.in_blocking; self.in_blocking = true; let blk_block_id = self.enter_defer_scope(body, false); for stmt in &body.stmts { self.emit_stmt(stmt)?; } if let Some(trailing) = &body.trailing { let mut v = self.emit_expr(trailing)?; if has_result { let rty = result_ty.clone(); // [M-http-props-mut-chain-argpos-value-ptr-mismatch] fix // (222.7): see `emit_assign_typed`'s own doc comment — deref // a value-record fluent `-> @` chain tail before assigning // into the (value-typed) `_c->_nova_result` field. if self.is_fluent_value_ptr_for_target(trailing, &rty) { v = format!("(*({}))", v); } Self::emit_assign_typed(self, "_c->_nova_result", &rty, &v); } else { self.line(&format!("(void)({});", v)); } } self.leave_defer_scope(blk_block_id); self.in_blocking = prev_in_blocking; self.current_spawn_captures = prev_caps; self.current_spawn_capture_by_value = prev_by_value; self.indent -= 1; self.line("}"); self.line(""); let blk_code = std::mem::replace(&mut self.out, saved_out); self.deferred_impls.push_str(&blk_code); self.indent = saved_indent; self.var_boxed = saved_var_boxed_blk; if has_result { Ok(format!("{}._nova_result", ctx_var)) } else { Ok("NOVA_UNIT".to_string()) } } /// Plan 113 (D172): emit a `#blocking fn` call-site offload. /// /// For `#blocking fn foo(a int, b str) -> int`, a call `foo(x, y)` becomes: /// /// ```c /// typedef struct { nova_int a; nova_str b; nova_int _nova_result; } NovaBlkFnCtx_nova_blkfn_N; /// static void _nova_blkfn_N(void* _blk_arg) { /// NovaBlkFnCtx_nova_blkfn_N* _c = ...; /// _c->_nova_result = nova_fn_foo(_c->a, _c->b); /// } /// // call site: /// NovaBlkFnCtx_nova_blkfn_N _nova_blkfn_N_ctx; /// _nova_blkfn_N_ctx.a = x; /// _nova_blkfn_N_ctx.b = y; /// if (mco_running()) { /// nova_blocking_offload(..., _nova_blkfn_N, &_nova_blkfn_N_ctx); /// } else { _nova_blkfn_N(&_nova_blkfn_N_ctx); } /// // result: _nova_blkfn_N_ctx._nova_result /// ``` fn emit_blocking_fn_call( &mut self, fn_name: &str, args: &[crate::ast::CallArg], ) -> Result<String, String> { // Unique id using blocking_counter (same pool as blocking{} ids). let blk_id = format!("_nova_blkfn_{}", self.blocking_counter); self.blocking_counter += 1; // Look up FnDecl to get param types and return type. let fn_decl = self.mono_fn_decls.get(fn_name) .ok_or_else(|| format!("[E_BLOCKING_FN_DECL_MISSING] #blocking fn `{}` not in mono_fn_decls", fn_name))? .clone(); // Collect param C types (parallel to args). // // Plan 248 (wave 3 fallout, real bug found by the integrator's // mega-CU gate, [M-blocking-fn-call-mut-param-value-ptr-mismatch]): // this used the param's BARE declared C type — never accounting for // `mut x T` (Plan 184 §Р10 by-pointer in-out ABI: a `mut` param of a // value/primitive type is `T*` in the callee's REAL C signature, the // callee receives the caller's storage address). Every OTHER call // path (ordinary free-fn calls via `synthesize_inout_refargs`, // `emit_fn_forward_decl`'s own signature emission) already applies // this; `#blocking fn` calls have their OWN dedicated ctx-struct // codegen path (thread-offload machinery) that independently (and, // until now, incompletely) recomputes param types — never learned // about Р10. Invisible before Plan 248 wave 3: for the old pointer- // newtype `Atomic*`/`Mutex` shapes, the bare value type WAS already // a pointer (`Nova_AtomicI64*`), so the missing `mut`-adjustment // never changed anything; now that `AtomicI64` etc. are genuine // value records (`NovaValue_AtomicI64`, `#no_copy`), the ctx struct // field ended up VALUE-typed while the offloaded call site still // needed to pass the ADDRESS — "assigning to 'NovaValue_AtomicI64' // from incompatible type 'NovaValue_AtomicI64 *'" (field decl) / // "passing 'NovaValue_AtomicI64' to parameter of incompatible type // 'NovaValue_AtomicI64 *'" (the `_c->d172_a` call-through) — found // via `d172_realtime_blocking_attrs.nv`'s `#blocking fn // d172_blk_fetch_add(mut d172_a AtomicI64, delta int)`. // `bool` alongside each type: is this param's ctx field an inout-ptr // (Р10) — needed again below when filling the field (address-of the // argument, not the bare value) and when building the in-work-fn // call (`_c->field` already IS the pointer the callee expects — no // further change needed there). let param_c_types: Vec<(String, bool)> = fn_decl.params.iter() .map(|p| { let base = self.type_ref_to_c(&p.ty).unwrap_or_else(|_| "nova_int".into()); if Self::param_is_inout_ptr(p, &base) { (format!("{}*", base), true) } else { (base, false) } }) .collect(); let result_ty = self.return_type_c(&fn_decl)?; let has_result = result_ty != "nova_unit"; let ctx_ty = format!("NovaBlkFnCtx{}", &blk_id); // NovaBlkFnCtx_nova_blkfn_N let ctx_var = format!("{}_ctx", blk_id); // _nova_blkfn_N_ctx // ─── ctx-struct typedef + work-fn forward decl → lambda_forward_decls ─── let _ = writeln!(self.lambda_forward_decls, "typedef struct {{"); for (i, (ty, _)) in param_c_types.iter().enumerate() { let field = fn_decl.params.get(i) .map(|p| p.name.as_str()) .unwrap_or("_arg"); let _ = writeln!(self.lambda_forward_decls, " {} {};", ty, field); } if has_result { let _ = writeln!(self.lambda_forward_decls, " {} _nova_result;", result_ty); } let _ = writeln!(self.lambda_forward_decls, "}} {};", ctx_ty); let _ = writeln!(self.lambda_forward_decls, "{}void {}(void* _blk_arg);", self.top_level_storage(), blk_id); // ─── ctx instance on stack + fill args ─── // Note: `args` has already passed through `emit_call`'s // `synthesize_inout_refargs` (Р10, upstream of the `#blocking fn` // dispatch that routes here) — a `mut x T` positional arg arrives // pre-wrapped as `RefArg(place)`, and `self.emit_expr` on that node // already emits the address (`&(place)`). No further address-of // needed here — `arg_c` is already the right C expression for // EITHER field shape (bare value or, per the `param_c_types` fix // just above, `T*`). self.line(&format!("{} {};", ctx_ty, ctx_var)); for (i, arg) in args.iter().enumerate() { let field = fn_decl.params.get(i) .map(|p| p.name.clone()) .unwrap_or_else(|| format!("_arg{}", i)); let arg_c = self.emit_expr(arg.expr())?; self.line(&format!("{}.{} = {};", ctx_var, field, arg_c)); } // ─── context-sensitive dispatch (same pattern as emit_blocking) ─── self.line("if (mco_running()) {"); self.indent += 1; self.line(&format!( "nova_blocking_offload(_nova_active_scope, _nova_active_slot, {}, &{});", blk_id, ctx_var)); self.indent -= 1; self.line("} else {"); self.indent += 1; self.line(&format!("{}(&{});", blk_id, ctx_var)); self.indent -= 1; self.line("}"); // ─── work-fn body → deferred_impls ─── let c_fn_name = self.free_fn_c_name(fn_name); let saved_out = std::mem::take(&mut self.out); let saved_indent = self.indent; self.indent = 0; // Plan 175 Ф.2-v2 ([M-spawn-var-boxed-leak] class, mirrors emit_spawn): // isolate `var_boxed` — this work-fn body's captures (if any go // through Ident resolution rather than the plain `_c->param` args // built below) must not see a stale outer box entry. let saved_var_boxed_blk = std::mem::take(&mut self.var_boxed); self.line(&format!("{}void {}(void* _blk_arg) {{", self.top_level_storage(), blk_id)); self.indent += 1; self.line(&format!("{}* _c = ({}*)_blk_arg;", ctx_ty, ctx_ty)); self.line("(void)_c;"); // Build call: nova_fn_foo(_c->a, _c->b, ...) let arg_exprs: String = fn_decl.params.iter() .map(|p| format!("_c->{}", p.name)) .collect::<Vec<_>>() .join(", "); if has_result { self.line(&format!("_c->_nova_result = {}({});", c_fn_name, arg_exprs)); } else { self.line(&format!("{}({});", c_fn_name, arg_exprs)); } self.indent -= 1; self.line("}"); self.line(""); let blk_code = std::mem::replace(&mut self.out, saved_out); self.deferred_impls.push_str(&blk_code); self.indent = saved_indent; self.var_boxed = saved_var_boxed_blk; if has_result { Ok(format!("{}._nova_result", ctx_var)) } else { Ok("NOVA_UNIT".to_string()) } } /// Pre-scan the module for HandlerLit and Spawn nodes; emit file-scope forward decls. fn emit_handler_forward_decls(&mut self, module: &Module) -> Result<(), String> { let mut h_ctr = 0usize; // handler_counter let mut s_ctr = 0usize; // spawn_counter // Scan order MUST match actual emit order: Fn (step 4) → Test (step 5) → Bench (step 5b). // Mixed order caused handler_counter desync when Tests precede Fns in module.items. let dead = compute_dead_decls(module); for item in &module.items { if let Item::Fn(f) = item { // Mirror the DCE skips from step 4. if f.receiver.is_none() && f.generics.is_empty() && !matches!(f.body, crate::ast::FnBody::External) && dead.dead_fns.contains(&f.name) { continue; } if let Some(r) = &f.receiver { if f.generics.is_empty() && !matches!(f.body, crate::ast::FnBody::External) && dead.dead_method_keys.contains(&(r.type_name.clone(), f.name.clone())) { continue; } } self.scan_fn_fwd(f, &mut h_ctr, &mut s_ctr)?; } } for item in &module.items { if let Item::Test(t) = item { self.scan_block_fwd(&t.body, &mut h_ctr, &mut s_ctr)?; } } for item in &module.items { match item { Item::Fn(_) | Item::Test(_) => {} // already processed above // Plan 83.4.5.6 Ф.4 (2026-05-24): pre-scan bench bodies для // spawn-fwd decls. Без этого `bench "..." { measure { supervised // { parallel for ... } } }` падает на CC-FAIL — `_nova_spawn_N` // undeclared (тело функции в deferred_impls после call site'а). // // CRITICAL: emit_bench эмитит `measure_body` **ТРИ РАЗА** // (warmup loop + calibration + sample collection — см. // emit_c.rs::emit_bench). Каждый emit инкрементит // self.spawn_counter → 1 spawn в source даёт 3 spawn_ids в // generated C. Pre-scan ДОЛЖЕН отражать ту же 3× expansion, // иначе s_ctr/spawn_counter рассинхронизируются и fwd-decls // не покрывают позднейшие spawn_ids. Setup/teardown // эмитятся один раз — single scan каждый. Item::Bench(b) => { for stmt in &b.setup { self.scan_stmt_fwd(stmt, &mut h_ctr, &mut s_ctr)?; } // 3× measure_body — emit_bench warmup + calibration + sample. // Plan 83.4.5.6 V1 limitation: triple-scan может вызвать // pre-existing type-cache mishaps в record/Option codegen // (см. bench/micro/gc.nv — Nova_Node.next тип теряется // если scan повторяется). V2: либо emit_bench refactor // на 1×measure, либо scan-skip помечать generics. for _ in 0..3 { self.scan_block_fwd(&b.measure_body, &mut h_ctr, &mut s_ctr)?; } for stmt in &b.teardown { self.scan_stmt_fwd(stmt, &mut h_ctr, &mut s_ctr)?; } for group in &b.groups { for case in &group.cases { for stmt in &case.setup { self.scan_stmt_fwd(stmt, &mut h_ctr, &mut s_ctr)?; } // 3× case.measure_body — same rationale. for _ in 0..3 { self.scan_block_fwd(&case.measure_body, &mut h_ctr, &mut s_ctr)?; } for stmt in &case.teardown { self.scan_stmt_fwd(stmt, &mut h_ctr, &mut s_ctr)?; } } } } _ => {} } } Ok(()) } fn scan_fn_fwd(&mut self, f: &FnDecl, h: &mut usize, s: &mut usize) -> Result<(), String> { match &f.body { FnBody::Expr(e) => self.scan_expr_fwd(e, h, s), FnBody::Block(b) => self.scan_block_fwd(b, h, s), // D82: external fn — тела нет, scan'ить нечего. FnBody::External => Ok(()), } } fn scan_expr_fwd(&mut self, expr: &Expr, h: &mut usize, s: &mut usize) -> Result<(), String> { match &expr.kind { ExprKind::HandlerLit { effect_name, methods } => { let eff = effect_name.join("_"); let handler_id = format!("_nova_handler_lit_{}", *h); *h += 1; let schema = self.effect_schemas.get(&eff).cloned().unwrap_or_default(); for m in methods { let (param_types, ret_ty) = Self::schema_lookup(&schema, &m.name) .cloned() .unwrap_or_else(|| (vec![], "nova_unit".into())); let mut fn_params = vec!["void* _ctx".to_string()]; for (i, p) in m.params.iter().enumerate() { let ty = param_types.get(i).cloned().unwrap_or_else(|| "nova_int".into()); fn_params.push(format!("{} {}", ty, p.name)); } let fn_name = format!("{}_impl_{}_{}", handler_id, eff, m.name); self.line(&format!( "{storage}{ret} {fn}({params});", storage = self.top_level_storage(), ret = ret_ty, fn = fn_name, params = fn_params.join(", ") )); // Recurse в method body — могут содержать nested // HandlerLit/Spawn. match &m.body { HandlerMethodBody::Expr(e) => self.scan_expr_fwd(e, h, s)?, HandlerMethodBody::Block(b) => self.scan_block_fwd(b, h, s)?, } } } // Plan 97.1 final hardening (D142): ProtocolLit emission // не использует handler_counter (отдельный protocol_lit_counter) // — здесь только **recurse** в method bodies для catch nested // HandlerLit/Spawn. Forward-decl самих impl-функций эмитится // прямо в emit_protocol_lit в lambda_forward_decls (не нужен // pre-scan, IDs локально-monotonic). ExprKind::ProtocolLit { methods, .. } => { for m in methods { match &m.body { HandlerMethodBody::Expr(e) => self.scan_expr_fwd(e, h, s)?, HandlerMethodBody::Block(b) => self.scan_block_fwd(b, h, s)?, } } } ExprKind::Spawn(body) => { let spawn_id = format!("_nova_spawn_{}", *s); *s += 1; self.line(&format!("{}void {}(mco_coro* _co);", self.top_level_storage(), spawn_id)); // Plan 47: рекурсия в тело spawn'а — вложенные spawn'ы // (`spawn { supervised { spawn {...} } }`) тоже нуждаются в // [INV-TODO: №523] forward-decl, и `*s` counter обязан совпадать с emit'овским // [INV-TODO: №523] — совпадение держится тем, что обе стороны // читают один и тот же счётчик, но проверки на расхождение нет. // (emit_spawn инкрементит spawn_counter, затем эмитит тело → // depth-first). Без рекурсии scan/emit рассинхронизировались // и вложенные entry-функции оставались undeclared. self.scan_expr_fwd(body, h, s)?; } ExprKind::Block(b) => self.scan_block_fwd(b, h, s)?, ExprKind::If { cond, then, else_ } => { self.scan_expr_fwd(cond, h, s)?; self.scan_block_fwd(then, h, s)?; match else_.as_ref() { Some(ElseBranch::Block(b)) => self.scan_block_fwd(b, h, s)?, Some(ElseBranch::If(e)) => self.scan_expr_fwd(e, h, s)?, None => {} } } ExprKind::With { bindings, body } => { for b in bindings { // Plan 19, C8 codegen (D31-rev): handler-лямбда // должна быть desugar'ена на pre-scan в синтетический // HandlerLit, иначе h-counter rassinch'нется // и forward-decls не совпадут с emit_with-side. if let Some((_, lit_expr)) = self.desugar_handler_lambda(&b.effect, &b.handler)? { self.scan_expr_fwd(&lit_expr, h, s)?; } else { self.scan_expr_fwd(&b.handler, h, s)?; } } self.scan_block_fwd(body, h, s)?; } ExprKind::Call { func, args, .. } => { self.scan_expr_fwd(func, h, s)?; for a in args { self.scan_expr_fwd(a.expr(), h, s)?; } } ExprKind::Binary { left, right, .. } => { self.scan_expr_fwd(left, h, s)?; self.scan_expr_fwd(right, h, s)?; } ExprKind::While { cond, body, .. } => { self.scan_expr_fwd(cond, h, s)?; self.scan_block_fwd(body, h, s)?; } ExprKind::For { iter, body, .. } => { self.scan_expr_fwd(iter, h, s)?; self.scan_block_fwd(body, h, s)?; } ExprKind::ParallelFor { iter, body, .. } => { // Desugar mirrors supervised { for x in iter { spawn { body } } } // — pre-scan reserves a spawn id for the implicit spawn. self.scan_expr_fwd(iter, h, s)?; self.scan_block_fwd(body, h, s)?; let spawn_id = format!("_nova_spawn_{}", *s); *s += 1; self.line(&format!("{}void {}(mco_coro* _co);", self.top_level_storage(), spawn_id)); } ExprKind::Loop { body, .. } => { self.scan_block_fwd(body, h, s)?; } ExprKind::Match { scrutinee, arms } => { self.scan_expr_fwd(scrutinee, h, s)?; for arm in arms { match &arm.body { MatchArmBody::Expr(e) => self.scan_expr_fwd(e, h, s)?, MatchArmBody::Block(b) => self.scan_block_fwd(b, h, s)?, } } } ExprKind::Unary { operand, .. } => self.scan_expr_fwd(operand, h, s)?, ExprKind::Supervised { body, .. } => self.scan_block_fwd(body, h, s)?, ExprKind::Detach(b) | ExprKind::Blocking(b) => self.scan_block_fwd(b, h, s)?, _ => {} } Ok(()) } fn scan_block_fwd(&mut self, block: &Block, h: &mut usize, s: &mut usize) -> Result<(), String> { for stmt in &block.stmts { self.scan_stmt_fwd(stmt, h, s)?; } if let Some(t) = &block.trailing { self.scan_expr_fwd(t, h, s)?; } Ok(()) } fn scan_stmt_fwd(&mut self, stmt: &Stmt, h: &mut usize, s: &mut usize) -> Result<(), String> { match stmt { Stmt::Let(d) => self.scan_expr_fwd(&d.value, h, s), Stmt::Expr(e) => self.scan_expr_fwd(e, h, s), Stmt::Assign { value, .. } => self.scan_expr_fwd(value, h, s), Stmt::Return { value: Some(v), .. } => self.scan_expr_fwd(v, h, s), Stmt::Throw { value, .. } => self.scan_expr_fwd(value, h, s), _ => Ok(()), } } /// Collect all identifier names referenced inside a handler method body. fn collect_idents_in_handler_method(m: &HandlerMethod) -> Vec<String> { let mut names = Vec::new(); match &m.body { HandlerMethodBody::Expr(e) => Self::collect_idents_expr(e, &mut names), HandlerMethodBody::Block(b) => { for stmt in &b.stmts { Self::collect_idents_stmt(stmt, &mut names); } if let Some(t) = &b.trailing { Self::collect_idents_expr(t, &mut names); } } } names.sort(); names.dedup(); names } /// Collect all names *introduced* (bound) inside an expression: /// let-bindings, for-pattern, match-arm patterns, if-let, while-let. /// These names are local to the spawn body and must not be treated as captures. fn collect_bound_names_expr(expr: &Expr, out: &mut std::collections::HashSet<String>) { match &expr.kind { ExprKind::Block(b) => Self::collect_bound_names_block(b, out), ExprKind::If { then, else_, .. } => { Self::collect_bound_names_block(then, out); match else_.as_ref() { Some(ElseBranch::Block(b)) => Self::collect_bound_names_block(b, out), Some(ElseBranch::If(e)) => Self::collect_bound_names_expr(e, out), None => {} } } ExprKind::IfLet { pattern, then, else_, .. } => { Self::collect_bound_names_pattern(pattern, out); Self::collect_bound_names_block(then, out); match else_.as_ref() { Some(ElseBranch::Block(b)) => Self::collect_bound_names_block(b, out), Some(ElseBranch::If(e)) => Self::collect_bound_names_expr(e, out), None => {} } } ExprKind::Match { scrutinee, arms } => { Self::collect_bound_names_expr(scrutinee, out); for arm in arms { Self::collect_bound_names_pattern(&arm.pattern, out); match &arm.body { MatchArmBody::Expr(e) => Self::collect_bound_names_expr(e, out), MatchArmBody::Block(b) => Self::collect_bound_names_block(b, out), } } } ExprKind::For { pattern, iter, body, .. } | ExprKind::ParallelFor { pattern, iter, body, .. } => { Self::collect_bound_names_expr(iter, out); Self::collect_bound_names_pattern(pattern, out); Self::collect_bound_names_block(body, out); } ExprKind::While { body, .. } => Self::collect_bound_names_block(body, out), ExprKind::WhileLet { pattern, body, .. } => { Self::collect_bound_names_pattern(pattern, out); Self::collect_bound_names_block(body, out); } ExprKind::Loop { body, .. } => Self::collect_bound_names_block(body, out), ExprKind::With { body, .. } => Self::collect_bound_names_block(body, out), ExprKind::Supervised { body, .. } => Self::collect_bound_names_block(body, out), ExprKind::Detach(body) | ExprKind::Blocking(body) => Self::collect_bound_names_block(body, out), ExprKind::Select { arms } => { for arm in arms { if let SelectOp::Recv { binding: Some(b), .. } = &arm.op { out.insert(b.clone()); } Self::collect_bound_names_block(&arm.body, out); } } _ => {} } } fn collect_bound_names_block(block: &Block, out: &mut std::collections::HashSet<String>) { for stmt in &block.stmts { match stmt { Stmt::Let(d) => { Self::collect_bound_names_pattern(&d.pattern, out); Self::collect_bound_names_expr(&d.value, out); } Stmt::Expr(e) => Self::collect_bound_names_expr(e, out), Stmt::Assign { target, value, .. } => { Self::collect_bound_names_expr(target, out); Self::collect_bound_names_expr(value, out); } // Plan 173.3 Ф.3 (D415 §4): `spawn consume c = e { body }` // desugars to Spawn(Block[ConsumeScope]) — the scope's // `binding` is bound INSIDE the spawn (must not be captured // from the outer fn), and its body may bind further names. Stmt::ConsumeScope { binding, init, body, .. } => { // Re-give form `spawn consume x { … }` desugars with // init = Ident(x): the OUTER `x` must remain capturable // into the spawn ctx (child-side `#define` shadows body // references) — marking it bound here loses the capture → // C `use of undeclared identifier`. let regive = matches!(&init.kind, ExprKind::Ident(n) if n == binding); if !regive { out.insert(binding.clone()); } Self::collect_bound_names_expr(init, out); Self::collect_bound_names_block(body, out); } // [M-nv-defer-captured-var-in-detach-undeclared] fix (221.1 // Ф.2 #22): companion to the `collect_idents_stmt` fix above // (same marker) — `defer(o ScopeOutcome) { … }`'s optional // outcome-binding `o` is materialized FRESH per exit-path // (`emit_defer_body_with_outcome`'s `#define`), never an // outer capture; without this arm it fell through to `_ => // {}` same as the ident-collection gap, so a body reading // `o` would have been misclassified as a free outer // reference (spuriously "captured") once the ident-side gap // above is fixed. Also scans the body itself for any of ITS // OWN nested bindings (match arms, closures, …), same as // every other arm's sub-expression scan. Stmt::Defer { outcome_binding, body, .. } => { if let Some(name) = outcome_binding { out.insert(name.clone()); } Self::collect_bound_names_expr(body, out); } _ => {} } } if let Some(t) = &block.trailing { Self::collect_bound_names_expr(t, out); } } fn collect_bound_names_pattern(pat: &Pattern, out: &mut std::collections::HashSet<String>) { match pat { Pattern::Ident { name, .. } => { out.insert(name.clone()); } Pattern::Binding { name, inner, .. } => { out.insert(name.clone()); Self::collect_bound_names_pattern(inner, out); } Pattern::Variant { kind, .. } => { if let VariantPatternKind::Tuple { patterns, .. } = kind { for p in patterns { Self::collect_bound_names_pattern(p, out); } } } Pattern::Record { fields, .. } => { for f in fields { if let Some(p) = &f.pattern { Self::collect_bound_names_pattern(p, out); } else { out.insert(f.name.clone()); } } } Pattern::Array { elems, .. } => { for e in elems { match e { ArrayPatternElem::Item(p) => Self::collect_bound_names_pattern(p, out), ArrayPatternElem::RestBind(name) => { out.insert(name.clone()); } ArrayPatternElem::Rest => {} } } } Pattern::Tuple(pats, _) => { for p in pats { Self::collect_bound_names_pattern(p, out); } } Pattern::Or { alternatives, .. } => { // Используем bindings из первого альтернатива (canonical). if let Some(first) = alternatives.first() { Self::collect_bound_names_pattern(first, out); } } Pattern::Wildcard(_) | Pattern::Literal(_, _) => {} } } fn collect_idents_expr(expr: &Expr, out: &mut Vec<String>) { match &expr.kind { ExprKind::Ident(name) => out.push(name.clone()), ExprKind::Binary { left, right, .. } => { Self::collect_idents_expr(left, out); Self::collect_idents_expr(right, out); } ExprKind::Unary { operand, .. } => Self::collect_idents_expr(operand, out), ExprKind::Call { func, args, trailing, .. } => { Self::collect_idents_expr(func, out); for a in args { Self::collect_idents_expr(a.expr(), out); } // Plan 103.5: also recurse into trailing blocks so spawn-body capture // analysis picks up variables used in nested trailing closures // (e.g. `once.call_once() { counter.fetch_add(1) }` inside parallel for). if let Some(t) = trailing { match t { crate::ast::Trailing::Block(b) => Self::collect_idents_block(b, out), crate::ast::Trailing::Fn(f) => match &f.body { crate::ast::FnBody::Block(b) => Self::collect_idents_block(b, out), crate::ast::FnBody::Expr(e) => Self::collect_idents_expr(e, out), crate::ast::FnBody::External => {} }, crate::ast::Trailing::LegacyBlockWithParams(tb) => Self::collect_idents_block(&tb.body, out), } } } ExprKind::Member { obj, .. } => Self::collect_idents_expr(obj, out), ExprKind::Index { obj, index } => { Self::collect_idents_expr(obj, out); Self::collect_idents_expr(index, out); } ExprKind::If { cond, then, else_ } => { Self::collect_idents_expr(cond, out); Self::collect_idents_block(then, out); if let Some(ElseBranch::Block(b)) = else_.as_ref() { Self::collect_idents_block(b, out); } if let Some(ElseBranch::If(e)) = else_.as_ref() { Self::collect_idents_expr(e, out); } } ExprKind::IfLet { scrutinee, then, else_, .. } => { Self::collect_idents_expr(scrutinee, out); Self::collect_idents_block(then, out); if let Some(ElseBranch::Block(b)) = else_.as_ref() { Self::collect_idents_block(b, out); } if let Some(ElseBranch::If(e)) = else_.as_ref() { Self::collect_idents_expr(e, out); } } ExprKind::While { cond, body, .. } => { Self::collect_idents_expr(cond, out); Self::collect_idents_block(body, out); } ExprKind::WhileLet { scrutinee, body, .. } => { Self::collect_idents_expr(scrutinee, out); Self::collect_idents_block(body, out); } ExprKind::For { iter, body, .. } | ExprKind::ParallelFor { iter, body, .. } => { Self::collect_idents_expr(iter, out); Self::collect_idents_block(body, out); } ExprKind::Loop { body, .. } => Self::collect_idents_block(body, out), ExprKind::Match { scrutinee, arms } => { Self::collect_idents_expr(scrutinee, out); for arm in arms { if let Some(g) = &arm.guard { Self::collect_idents_expr(g, out); } match &arm.body { MatchArmBody::Expr(e) => Self::collect_idents_expr(e, out), MatchArmBody::Block(b) => Self::collect_idents_block(b, out), } } } ExprKind::Range { start, end, .. } => { if let Some(s) = start { Self::collect_idents_expr(s, out); } if let Some(e) = end { Self::collect_idents_expr(e, out); } } ExprKind::Lambda { body, .. } => Self::collect_idents_expr(body, out), ExprKind::TupleLit(elems) => { for e in elems { Self::collect_idents_expr(e, out); } } ExprKind::ArrayLit(elems) => { for elem in elems { match elem { ArrayElem::Item(x) | ArrayElem::Spread(x) => Self::collect_idents_expr(x, out), } } } ExprKind::MapLit { elems, .. } => { for me in elems { match me { crate::ast::MapElem::Pair(k, v) => { Self::collect_idents_expr(k, out); Self::collect_idents_expr(v, out); } crate::ast::MapElem::Spread(e) => { Self::collect_idents_expr(e, out); } } } } ExprKind::RecordLit { fields, .. } => { for f in fields { if let Some(v) = &f.value { Self::collect_idents_expr(v, out); } } } ExprKind::Spawn(body) => Self::collect_idents_expr(body, out), ExprKind::With { bindings, body } => { for b in bindings { Self::collect_idents_expr(&b.handler, out); } Self::collect_idents_block(body, out); } ExprKind::Coalesce(l, r) => { Self::collect_idents_expr(l, out); Self::collect_idents_expr(r, out); } ExprKind::Try(e) | ExprKind::Bang(e) | ExprKind::As(e, _) | ExprKind::Is(e, _) => { Self::collect_idents_expr(e, out); } ExprKind::Interrupt(Some(v)) => Self::collect_idents_expr(v, out), ExprKind::Block(b) => Self::collect_idents_block(b, out), ExprKind::Supervised { body, cancel, deadline, on_timeout } => { Self::collect_idents_block(body, out); if let Some(c) = cancel { Self::collect_idents_expr(c, out); } if let Some(dl) = deadline { Self::collect_idents_expr(&dl.expr, out); } if let Some(oh) = on_timeout { Self::collect_idents_expr(oh, out); } } ExprKind::Detach(b) | ExprKind::Blocking(b) => Self::collect_idents_block(b, out), ExprKind::Select { arms } => { for arm in arms { match &arm.op { SelectOp::Recv { chan, .. } => Self::collect_idents_expr(chan, out), SelectOp::Send { chan, value } => { Self::collect_idents_expr(chan, out); Self::collect_idents_expr(value, out); } SelectOp::Default => {} } if let Some(g) = &arm.guard { Self::collect_idents_expr(g, out); } Self::collect_idents_block(&arm.body, out); } } // Владелец 2026-07-21 (найдено при str-concat-lint канонизации, // [M-str-interp-closure-capture-miss]): `InterpolatedStr` не // обходился здесь — идентификатор, упомянутый ТОЛЬКО внутри // `${expr}` (напр. `|host| "${host}:${port}"` где `port` — outer // captured var, ЕСЛИ он больше нигде в теле closure'а не // используется), не попадал в capture-list. Симптом: C // codegen `use of undeclared identifier 'port'` — closure struct // не резервировал под него поле. Репро: `resolve_addr` (examples/ // flagship/aggregator/src/main.nv) после канонизации `host + ":" // + port.to_str()` → `"${host}:${port}"`. ExprKind::InterpolatedStr { parts } => { for p in parts { if let crate::ast::InterpStrPart::Expr { expr, .. } = p { Self::collect_idents_expr(expr, out); } } } _ => {} } } fn collect_idents_block(block: &Block, out: &mut Vec<String>) { for stmt in &block.stmts { Self::collect_idents_stmt(stmt, out); } if let Some(t) = &block.trailing { Self::collect_idents_expr(t, out); } } /// [M-parfor-capture-callee-name-collides-std-local] fix: a spawn-ctx /// capture pre-pass companion to `collect_idents_expr` — hunts for /// `Call{func: Ident(name), ..}` nodes whose call-site the checker /// resolved to a genuine `FnDecl` (`self.resolved_callees`, Plan 172.1 /// U.3.4 channel — populated in `types/mod.rs::f1_check_call` ONLY for /// an unambiguously-resolved free-fn/method call, NEVER for a dynamic /// closure-variable invocation `f(x)`). /// /// `emit_spawn`'s capture loop subtracts these names from the free- /// identifier set BY RESOLVE, not by a per-name blacklist — same disease /// class as [M-callnorm-free-fn-name-collision] / 196.6: `var_types` is a /// flat `HashMap<String,String>` that is NEVER per-function-scoped /// (`emit_fn_scoped_inner` inserts params into it but never restores the /// prior state at function-exit, unlike `var_mutable`) — an unrelated /// function elsewhere in the CU with a LOCAL of the same bare name /// (e.g. `uint64_t probe` inside std's float-format engine) leaves a /// stale entry that a module-fn CALLEE of the same spelling (`probe()` /// called from inside a `parallel for`) wrongly matched, emitting /// `ctx->probe = probe;` with no such C identifier in scope — CC-FAIL /// "use of undeclared identifier 'probe'". /// /// Mirrors `collect_idents_expr`'s full traversal (every nested position /// a `Call` can hide in — match arms, loop/if bodies, trailing closures, /// record-lit fields, etc.) so a resolved callee buried anywhere in the /// spawn body is found. Unlike it, a bare `Ident` leaf is a no-op here — /// this pass exists ONLY to name resolved call-targets, never ordinary /// variable reads. An UNRESOLVED call target (a captured closure /// variable invoked as `f(x)`) is intentionally never recorded — the /// checker never puts such a call in `resolved_callees`, so the name /// falls straight through `collect_idents_expr`'s ordinary collection /// and stays capture-eligible exactly as before this fix. fn collect_resolved_call_target_names_expr( &self, expr: &Expr, out: &mut std::collections::HashSet<String>, ) { let rce = |this: &Self, e: &Expr, out: &mut std::collections::HashSet<String>| { this.collect_resolved_call_target_names_expr(e, out) }; match &expr.kind { ExprKind::Binary { left, right, .. } => { rce(self, left, out); rce(self, right, out); } ExprKind::Unary { operand, .. } => rce(self, operand, out), ExprKind::Call { func, args, trailing } => { if let ExprKind::Ident(name) = &func.kind { if self.resolved_callees.contains_key(&expr.id) { out.insert(name.clone()); } } rce(self, func, out); for a in args { rce(self, a.expr(), out); } if let Some(t) = trailing { match t { crate::ast::Trailing::Block(b) => self.collect_resolved_call_target_names_block(b, out), crate::ast::Trailing::Fn(f) => match &f.body { crate::ast::FnBody::Block(b) => self.collect_resolved_call_target_names_block(b, out), crate::ast::FnBody::Expr(e) => rce(self, e, out), crate::ast::FnBody::External => {} }, crate::ast::Trailing::LegacyBlockWithParams(tb) => self.collect_resolved_call_target_names_block(&tb.body, out), } } } ExprKind::Member { obj, .. } => rce(self, obj, out), ExprKind::Index { obj, index } => { rce(self, obj, out); rce(self, index, out); } ExprKind::If { cond, then, else_ } => { rce(self, cond, out); self.collect_resolved_call_target_names_block(then, out); if let Some(ElseBranch::Block(b)) = else_.as_ref() { self.collect_resolved_call_target_names_block(b, out); } if let Some(ElseBranch::If(e)) = else_.as_ref() { rce(self, e, out); } } ExprKind::IfLet { scrutinee, then, else_, .. } => { rce(self, scrutinee, out); self.collect_resolved_call_target_names_block(then, out); if let Some(ElseBranch::Block(b)) = else_.as_ref() { self.collect_resolved_call_target_names_block(b, out); } if let Some(ElseBranch::If(e)) = else_.as_ref() { rce(self, e, out); } } ExprKind::While { cond, body, .. } => { rce(self, cond, out); self.collect_resolved_call_target_names_block(body, out); } ExprKind::WhileLet { scrutinee, body, .. } => { rce(self, scrutinee, out); self.collect_resolved_call_target_names_block(body, out); } ExprKind::For { iter, body, .. } | ExprKind::ParallelFor { iter, body, .. } => { rce(self, iter, out); self.collect_resolved_call_target_names_block(body, out); } ExprKind::Loop { body, .. } => self.collect_resolved_call_target_names_block(body, out), ExprKind::Match { scrutinee, arms } => { rce(self, scrutinee, out); for arm in arms { if let Some(g) = &arm.guard { rce(self, g, out); } match &arm.body { MatchArmBody::Expr(e) => rce(self, e, out), MatchArmBody::Block(b) => self.collect_resolved_call_target_names_block(b, out), } } } ExprKind::Range { start, end, .. } => { if let Some(s) = start { rce(self, s, out); } if let Some(e) = end { rce(self, e, out); } } ExprKind::Lambda { body, .. } => rce(self, body, out), ExprKind::TupleLit(elems) => { for e in elems { rce(self, e, out); } } ExprKind::ArrayLit(elems) => { for elem in elems { match elem { ArrayElem::Item(x) | ArrayElem::Spread(x) => rce(self, x, out), } } } ExprKind::MapLit { elems, .. } => { for me in elems { match me { crate::ast::MapElem::Pair(k, v) => { rce(self, k, out); rce(self, v, out); } crate::ast::MapElem::Spread(e) => { rce(self, e, out); } } } } ExprKind::RecordLit { fields, .. } => { for f in fields { if let Some(v) = &f.value { rce(self, v, out); } } } ExprKind::Spawn(body) => rce(self, body, out), ExprKind::With { bindings, body } => { for b in bindings { rce(self, &b.handler, out); } self.collect_resolved_call_target_names_block(body, out); } ExprKind::Coalesce(l, r) => { rce(self, l, out); rce(self, r, out); } ExprKind::Try(e) | ExprKind::Bang(e) | ExprKind::As(e, _) | ExprKind::Is(e, _) => { rce(self, e, out); } ExprKind::Interrupt(Some(v)) => rce(self, v, out), ExprKind::Block(b) => self.collect_resolved_call_target_names_block(b, out), ExprKind::Supervised { body, cancel, deadline, on_timeout } => { self.collect_resolved_call_target_names_block(body, out); if let Some(c) = cancel { rce(self, c, out); } if let Some(dl) = deadline { rce(self, &dl.expr, out); } if let Some(oh) = on_timeout { rce(self, oh, out); } } ExprKind::Detach(b) | ExprKind::Blocking(b) => self.collect_resolved_call_target_names_block(b, out), ExprKind::Select { arms } => { for arm in arms { match &arm.op { SelectOp::Recv { chan, .. } => rce(self, chan, out), SelectOp::Send { chan, value } => { rce(self, chan, out); rce(self, value, out); } SelectOp::Default => {} } if let Some(g) = &arm.guard { rce(self, g, out); } self.collect_resolved_call_target_names_block(&arm.body, out); } } _ => {} } } fn collect_resolved_call_target_names_block( &self, block: &Block, out: &mut std::collections::HashSet<String>, ) { for stmt in &block.stmts { self.collect_resolved_call_target_names_stmt(stmt, out); } if let Some(t) = &block.trailing { self.collect_resolved_call_target_names_expr(t, out); } } fn collect_resolved_call_target_names_stmt( &self, stmt: &Stmt, out: &mut std::collections::HashSet<String>, ) { match stmt { Stmt::Let(d) => self.collect_resolved_call_target_names_expr(&d.value, out), Stmt::Expr(e) => self.collect_resolved_call_target_names_expr(e, out), Stmt::Assign { target, value, .. } => { self.collect_resolved_call_target_names_expr(target, out); self.collect_resolved_call_target_names_expr(value, out); } Stmt::Return { value: Some(v), .. } => self.collect_resolved_call_target_names_expr(v, out), Stmt::Throw { value, .. } => self.collect_resolved_call_target_names_expr(value, out), Stmt::ConsumeScope { init, body, .. } => { self.collect_resolved_call_target_names_expr(init, out); for s in &body.stmts { self.collect_resolved_call_target_names_stmt(s, out); } if let Some(t) = &body.trailing { self.collect_resolved_call_target_names_expr(t, out); } } _ => {} } } fn collect_idents_stmt(stmt: &Stmt, out: &mut Vec<String>) { match stmt { Stmt::Let(d) => Self::collect_idents_expr(&d.value, out), Stmt::Expr(e) => Self::collect_idents_expr(e, out), Stmt::Assign { target, value, .. } => { Self::collect_idents_expr(target, out); Self::collect_idents_expr(value, out); } Stmt::Return { value: Some(v), .. } => Self::collect_idents_expr(v, out), Stmt::Throw { value, .. } => Self::collect_idents_expr(value, out), // Plan 173.3 Ф.3 (D415 §4): `spawn consume c = e { body }` desugar — // idents referenced by the scope's init AND body must count as // spawn-body references (else an outer `tx` used inside the // consume-scope body is never captured into the spawn ctx → // C `use of undeclared identifier`). The scope's own `binding` // is excluded via collect_bound_names_block's ConsumeScope arm. Stmt::ConsumeScope { init, body, .. } => { Self::collect_idents_expr(init, out); for s in &body.stmts { Self::collect_idents_stmt(s, out); } if let Some(t) = &body.trailing { Self::collect_idents_expr(t, out); } } // [M-nv-defer-captured-var-in-detach-undeclared] fix (221.1 Ф.2 // #22): a bare `defer EXPR` statement's body was never scanned // for free identifiers here — the ONLY caller of // `collect_idents_stmt` (via `collect_idents_block`) that feeds // `emit_spawn`/`emit_detach`'s capture-collection pass // (`refs`/`captures`, see there). A variable referenced ONLY // inside a `defer` statement (never anywhere else in the SAME // spawn/detach body) was therefore invisible to that pass — // no ctx-struct field ever got created for it, and the body's // own emission (via `emit_defer_body_void`/`emit_expr`, which DOES // correctly consult `current_spawn_captures` when a name IS a // known capture) fell through to the bare-identifier case // instead — `CC-FAIL "use of undeclared identifier '<name>'"` // (the ctx struct genuinely has no such field; this isn't a // capture-access rewrite bug like markers #9/#16, it's a // capture-DISCOVERY bug one step upstream). Every OTHER // reference to the same variable elsewhere in the block (e.g. a // plain statement using it, or the checker's own D415 capture // analysis) was already correctly discovered — only the // `defer`-only-reference shape hit this gap, matching the "other // uses of the same variable resolve correctly" observation in // the original report. Mirrors `Stmt::ConsumeScope`'s identical // "scan a statement-carried sub-expression for free idents" // shape immediately above. Stmt::Defer { body, .. } => Self::collect_idents_expr(body, out), _ => {} } } // ---- forward declarations ---- fn emit_fn_forward_decl(&mut self, f: &FnDecl) -> Result<(), String> { // [M-sync-crossmodule…] (D381): lower this fn's signature in the context // of its OWN declaring file, so a colliding param/return type (`ErrorKind` // in std.io's `IoError.of`) resolves to its module-qualified base rather // than a bare `Nova_ErrorKind`. GATED on a collision existing in this CU: // when none, `current_emit_file_id` stays exactly as the legacy passes // left it (None here) so file-private fn-name resolution — and thus every // emitted byte — is unchanged. The next fn/body pass re-sets it, and an // `?`-error aborts compilation, so no explicit per-return restore. if self.any_type_file_collision() { self.current_emit_file_id = Some(f.span.file_id); } // Plan 184 Р10: record which positional params of this FREE fn use the // by-pointer in-out ABI (value/primitive `mut x T`), so call-sites can // inject the address-of (`synthesize_inout_refargs`). Only when at least // one param qualifies (keeps the map sparse). if f.receiver.is_none() { let flags: Vec<bool> = f.params.iter().map(|p| { let ty_c = if let Some((box_ty, _, _)) = self.protocol_box_c_type_for(&p.ty) { box_ty } else { self.type_ref_to_c(&p.ty).unwrap_or_default() }; Self::param_is_inout_ptr(p, &ty_c) }).collect(); if flags.iter().any(|b| *b) { self.free_fn_inout_params.insert(f.name.clone(), flags); } } // D82: external fn — forward decl не нужен (реализация в nova_rt/*.h // уже включена через preamble #include). // Plan 91.10 (D163 retracted): branch для D163 external fn удалён. // needs_caps всегда empty после retract; non-stdlib external fn // отвергаются type-checker'ом раньше. if f.is_external { // Plan 172.1 U.1.3b Gap B: index an EXTERN method's return C-type by its // declaration `Span` into `fn_ret_by_span` — but ONLY when that return is a // context-independent PRIMITIVE (`is_primitive_lowerable`, the shared gate that // the checker also consumes, §0/§3). extern fns get NO forward decl (the real // impl is a C trampoline / `nova_rt` header), so they skip the MAIN span-index // at the bottom of this fn (line ~9579); without THIS, an INLINED library body // calling `self.<extern>()` — e.g. the Nova-body `Once.start()` calling // `self.start_won() -> bool` — finds NO channel entry at the U.4.5(a) flip // site and re-derives the return in legacy, which guesses `nova_int` (§0/§1 // re-derive bug → `if condition must be bool, got nova_int`). The PRIMITIVE gate // is the safety: a primitive's C name is the SAME regardless of mono/tuple/Result // context, so the bare span→C-name index is sound. tuple / `Result` / effect / // record / generic returns (net `split→(R,W)`, `recv_from→Result[(str,Addr)]`) // need codegen's mono/tuple-registration context the channel cannot reproduce // (naive `return_type_c` registration → `_NovaTuple_2_8` CC-FAIL = the U.4.3 // tuple-mono hazard) → they are NOT indexed here and stay on the legacy path // (full fix = U.4.3, typed IR). Gated to CONCRETE-receiver methods (`self`/`@` // is what Gap A resolves into `resolved_callees`; a generic receiver takes the // mono dispatch path, mirroring the concrete-callee invariant at line ~9579). // For the gated set this is a §1 FIX, NOT byte-identical — see the relaxed // U.4.5a-xcheck at the flip site. if let Some(recv) = &f.receiver { if recv.generics.is_empty() { if let Some(ret_tr) = &f.return_type { let rt = crate::types::ResolvedType::from_type_ref(ret_tr); // Index an extern method's return in the channel when its C-type is // CONTEXT-INDEPENDENT (same regardless of mono / call-site): // (1) primitives (`is_primitive_lowerable`) — original Gap B gate. // (2) a RUNTIME_DEFINED_TYPES Named type (args empty) — C-header-backed // guard structs etc. lower to a context-independent `Nova_X*` // (final guard below). Gating on RUNTIME_DEFINED_TYPES (NOT a // structural value-record check — `value_record_names` is NOT yet // populated at fn-forward-decl time) excludes user value-records // like `SocketAddr` → no `type_ref_to_c` side-effect on them, so // their structural `==` is untouched. Class (2) fixes the Option- // erasure: `Some(self.<extern>())` where the extern returns e.g. // `OnceGuard` (`Nova_OnceGuard*`) — without the channel the extern // return erased to `nova_int` → `NovaOpt_nova_int` vs declared // `NovaOpt_Nova_OnceGuard_p` (sync `Once.start`). let channel_safe = rt.is_primitive_lowerable() || matches!( rt.peel_view(), crate::types::ResolvedType::Named { name, args, .. } if args.is_empty() && RUNTIME_DEFINED_TYPES.contains(&name.as_str()) ); if channel_safe { if let Ok(ret_c) = self.type_ref_to_c(ret_tr) { if rt.is_primitive_lowerable() || self.debt_is_bare_nova_ptr(&ret_c) { self.fn_ret_by_span.insert(f.span, ret_c); } } } } } } return Ok(()); } if f.name == "main" { return Ok(()); } // Plan 95 Ф.3: Nova-body метод на builtin sum-типе // (`Option`/`Result`) эмитится **только** через method-only // mono channel (per-T в `register_mono_method_instance` / // `emit_monomorphized_method`). Регулярный fwd-decl эмитил бы // erased `Nova_Option*` (incomplete type — Plan 93 Ф.0 root // cause). Skip — call-site через перехват `NovaOpt_` (#6) / // `is_result_like` (#7) роутит в `DeclaredBody`-ветку. if let Some(recv) = &f.receiver { if matches!(recv.type_name.as_str(), "Option" | "Result") { return Ok(()); } } // Plan 11 Follow-up (2026-05-17): Methods на receiver-generic типах // (e.g. `Container[T].new(...)`) регистрируем в **отдельной** карте // self_method_decls для Self.method() fast-path (mono enrollment). // Не reuse mono_method_decls — он наблюдается другими code paths и // получает new entries → false-trigger других mono routing'ов // (e.g. HashMap.contains misroute). if let Some(recv) = &f.receiver { if !recv.generics.is_empty() && !recv.type_name.starts_with("[]") { self.self_method_decls.insert( (recv.type_name.clone(), f.name.clone()), f.clone(), ); } } // Plan 48: Generic free functions → store for monomorphization; no erased forward decl. // №129: insert-time ошибка на одноимённость ОТКАЧЕНА приёмкой — красила // легальные module-private одноимённые fn (check_ok ×2 в мега-CU). // Дефект last-wins жив: [M-mono-fn-decls-module-qualified-key]. if !f.generics.is_empty() && f.receiver.is_none() { self.mono_fn_decls.insert(f.name.clone(), f.clone()); // Track tuple return arity so call sites can populate tuple_element_types if let Some(TypeRef::Tuple(elems, _)) = &f.return_type { self.generic_fn_tuple_arity.insert(f.name.clone(), elems.len()); } return Ok(()); } // Plan 103.6 / Plan 113: Non-generic free functions annotated with #parks/#wakes/#realtime // or #blocking are stored in mono_fn_decls so emit_call can check sync_class / blocking_attr // at call sites (E_REALTIME_NESTED_SYNC_VIA_FN / E_BLOCKING_SYNC_PARK / blocking offload). if (f.sync_class.is_some() || f.blocking_attr) && f.generics.is_empty() && f.receiver.is_none() && !f.is_external { self.mono_fn_decls.entry(f.name.clone()).or_insert_with(|| f.clone()); } // Plan 48: Generic methods with own type params → store for monomorphization. // Plan 101.1: array extension methods ([]T receivers) ALSO go to mono // pipeline когда T — это fn-prefix-generic (`fn[T] []T @method`). // Без этого dispatch на non-int receivers (e.g., `[]str`) ломается // — body эмитится один раз с default nova_int, и call-site // dispatch ищет несуществующую `NovaArray_nova_str_method_<m>` функцию. if !f.generics.is_empty() { if let Some(recv) = &f.receiver { // Регистрируем все generic methods (включая `[]T`-ext) в mono_method_decls. // №129: коллизия ОДИНАКОВОЙ формы = ошибка; легальный D84-overload // проходит и вставляется прежним last-wins (mono_method_registry.rs). let key = (recv.type_name.clone(), f.name.clone()); super::mono_method_registry::check_mono_method_decl_collision( self.mono_method_decls.get(&key), f.span, &recv.type_name, &f.name, f, )?; self.mono_method_decls.insert(key, f.clone()); self.mono_method_decls_by_span.insert(f.span, f.clone()); // №130 // Register sentinel MethodSig so call sites can find and mono-route this method. let sentinel_name = format!("__mono_method__{}__{}", recv.type_name, f.name); let sig = MethodSig { param_c_types: vec![], return_c_type: "void*".to_string(), is_instance: !matches!(f.receiver.as_ref().map(|r| &r.kind), Some(crate::ast::ReceiverKind::Static)), is_external: false, is_delegated: false, c_name: sentinel_name, variadic_last: false, param_defaults: vec![], // Plan 128 Ф.1: mono-sentinel — concrete sig is generated on // demand. Inherit from FnDecl recv.mutable; concrete mono'd // emission will use this when registering the real sig. recv_mutable: recv.mutable, // Plan 184 (Р13/Р14): generic mono-sentinel — carry the source // param modes so a concrete mono method can still be mode-matched. param_modes: Self::fn_param_modes(f), // U.4.3 c2.2: generic mono-sentinel is a routing placeholder, not a // concrete dispatch target — channel records only non-generic callees. fn_span: None, }; let key = (recv.type_name.clone(), f.name.clone()); self.register_method_overload(key, sig); return Ok(()); } else { // Free generic fn: already handled above. return Ok(()); } } if let Some(recv) = &f.receiver { if !recv.generics.is_empty() { // Generic method: emit erased forward decl let type_params: HashSet<String> = recv.generics.iter().filter_map(|tr| { if let TypeRef::Named { path, .. } = tr { path.first().cloned() } else { None } }).collect(); let is_instance = matches!(recv.kind, ReceiverKind::Instance); let mangled = self.mangle_fn(f); // Set receiver type so `Self` in return type AND param types // resolves to `Nova_{RecvType}*` instead of `Nova_Self*`. // Plan 11 Follow-up (2026-05-17): keep receiver set до конца // forward decl emit (включая param iteration), иначе params типа // `other Self` emit'ятся как `Nova_Self*`. let prev_recv = self.current_receiver_type.replace(recv.type_name.clone()); self.sync_receiver_rt(); let ret_c = self.erased_type_ref_c(&f.return_type, &type_params); let mut parts = if is_instance { // Plan 128 Ф.1: thread recv.mutable (Ф.2 consumes). vec![format!("{} nova_self", self.receiver_c_type(&recv.type_name, recv.mutable))] } else { vec![] }; for p in &f.params { let p_c = self.erased_type_ref_c(&Some(p.ty.clone()), &type_params); parts.push(format!("{} {}", p_c, p.name)); } self.current_receiver_type = prev_recv; self.sync_receiver_rt(); let params_s = if parts.is_empty() { "void".into() } else { parts.join(", ") }; self.var_types.insert(format!("fn_ret_{}", f.name), ret_c.clone()); // Plan 152.4.3: type-qualified return key (disambiguates same-named // methods across types in the inference fallback). self.var_types.insert(format!("fn_ret_{}_{}", recv.type_name, f.name), ret_c.clone()); self.line(&format!("{}{} {}({});", self.top_level_storage(), ret_c, mangled, params_s)); return Ok(()); } } // Set receiver type for Self resolution if let Some(recv) = &f.receiver { self.current_receiver_type = Some(recv.type_name.clone()); self.sync_receiver_rt(); // [M-static-selfreturn-value-mangle-conflict] (Plan 172.13): see // field doc — forward-decl side of the static/instance distinction. self.current_receiver_is_static = matches!(recv.kind, ReceiverKind::Static); } else { self.current_receiver_type = None; self.sync_receiver_rt(); self.current_receiver_is_static = false; } // Seed parameter types into var_types BEFORE inferring the return type // from a bodyless `-> T` (return_type_c infers from the trailing expr // when no annotation). At forward-declaration time the params are not // yet in var_types, so `cv.notify_one()` saw `cv` as the nova_int // fallback → method lookup missed → return mistyped nova_int (then the // bodyless fn was declared `nova_int` and assigned a unit value → CC-FAIL). // Restore afterwards so we don't leak param types across declarations. let mut ret_seed_saved: Vec<(String, Option<String>)> = Vec::new(); if f.return_type.is_none() { for p in &f.params { if let Ok(pc) = self.type_ref_to_c(&p.ty) { if !pc.is_empty() { ret_seed_saved.push((p.name.clone(), self.var_types.get(&p.name).cloned())); self.var_types.insert(p.name.clone(), pc); } } } } let mut ret = self.return_type_c(f)?; for (n, prev) in ret_seed_saved { match prev { Some(pp) => { self.var_types.insert(n, pp); } None => { self.var_types.remove(&n); } } } // Plan 72 P3-B return: a protocol return type (`-> Iter[int]`) lowers to // a `NovaBox_*` fat pointer — the C function physically returns the // 16-byte { data, vtable } struct. Emit the typedef up front so the // forward declaration that uses it is valid. if let Some((proto, type_args)) = self.protocol_box_return_type_info(f) { if let Some(box_ty) = self.emit_protocol_box_typedef(&proto, &type_args) { ret = box_ty; } } let params = self.params_c(f)?; let mangled = self.mangle_fn(f); // Register return type so call sites can infer print helper self.var_types.insert(format!("fn_ret_{}", f.name), ret.clone()); // Plan 172.1 U.4.3: index THIS callee's return C-type by its declaration // `Span` — codegen's OWN view of the callee the `resolved_callees` channel // points at (the checker recorded `callee.span` on unambiguous resolution). // Stage (a) free fns / (b) STATIC methods / (c1) INSTANCE methods (`@method`, // single-overload) — the channel records each in `f1_check_call`. This is the // MAIN registration path, reached ONLY by CONCRETE (non-mono) callees: generic // fns / generic-own-param methods / receiver-generic methods all returned early // above (mono_fn_decls / sentinel / erased forward-decl) and stay codegen-mono'd // at the call site (legitimate lowering — stage d). So indexing every callee that // reaches here is exactly "non-generic concrete callees" — no receiver-kind guard // needed. Same `ret` the name-keyed `fn_ret_<name>` / `fn_ret_<recv>_<name>` // lookups return → the equivalence-assert proves the channel selects the identical // callee codegen would derive (§0/§7.7). self.fn_ret_by_span.insert(f.span, ret.clone()); // Plan 152.4.3: also register a TYPE-QUALIFIED return key for methods — // the name-only key above is last-wins across types, so same-named methods // on different receivers collide (e.g. `CharsIter.next -> Option[char]` vs // `GraphemesView.next -> Option[str]`); the inference fallback prefers this. if let Some(recv) = &f.receiver { self.var_types.insert(format!("fn_ret_{}_{}", recv.type_name, f.name), ret.clone()); } // Plan 72 P2-A: register Result[T,E] return params so `let r = f(...)` // (call RHS, no annotation) populates `result_type_params[r]` correctly // instead of falling back to `(nova_int, nova_str)`. if let Some(rt) = &f.return_type { if let Some(rparams) = self.extract_result_type_params(rt) { let key = match &f.receiver { Some(recv) => format!("{}.{}", recv.type_name, f.name), None => f.name.clone(), }; self.fn_result_type_params.insert(key, rparams); } } // Plan 72 P3-B: protocol-typed parameters lower to `NovaBox_*` fat // pointers (so the callee can dispatch via the vtable). Emit the // vtable/box typedef for each, and register the per-parameter protocol // info (keyed `fn_name` for free fns, `Type.method` for methods) so // call sites can box concrete arguments — for both free fns and methods. { let mut param_protos: Vec<Option<(String, Vec<String>)>> = Vec::new(); let mut any_proto = false; for p in &f.params { if let Some((proto, type_args)) = self.protocol_type_args(&p.ty) { self.emit_protocol_box_typedef(&proto, &type_args); param_protos.push(Some((proto, type_args))); any_proto = true; } else { param_protos.push(None); } } if any_proto { let key = match &f.receiver { Some(recv) => format!("{}.{}", recv.type_name, f.name), None => f.name.clone(), }; self.fn_protocol_params.insert(key, param_protos); } } // Plan 174.3 (D53): register `any`-typed parameters so call sites can // implicitly box concrete arguments (upcast `T → any`). Keyed like // `fn_protocol_params`. { let any_flags: Vec<bool> = f.params.iter() .map(|p| matches!(&p.ty, TypeRef::Named { path, generics, .. } if generics.is_empty() && path.len() == 1 && path[0] == "any")) .collect(); if any_flags.iter().any(|&b| b) { let key = match &f.receiver { Some(recv) => format!("{}.{}", recv.type_name, f.name), None => f.name.clone(), }; self.fn_any_params.insert(key, any_flags); } } // Register fn-typed return signature for closure binding propagation // [fix M-fn-newtype-return-position-broken, №53]: `resolve_fn_typeref` // ALSO recognizes a newtype-over-fn/alias-of-fn RETURN type here (e.g. // `fn make() -> Handler`), not just a literal `fn(...) -> ...` return — // the pre-existing guard below only matched the literal `Func` shape, // so `fn_returns_fn_sig` (consulted by the let-binding propagation at // ~line 30058 AND by the chain-call dispatch in `emit_call`/ // `infer_call_ret_c` added by this fix) never got an entry for a // fn-newtype-returning fn — root cause of forms (а)/(б)/(в). if let Some(TypeRef::Func { params: fp, return_type, .. }) = f.return_type.as_ref() .and_then(|rt| self.resolve_fn_typeref(rt)) { // Plan 70 PhaseA1.2: strict mode — return-fn signature lowering. // fn-returning-fn (HOF that returns closure): translate params + return // through type_ref_to_c. Fail = invalid generic/missing type = compiler bug. let ptys: Vec<String> = fp.iter() .map(|t| self.type_ref_to_c(t).map_err(|e| self.err_no_int_fallback( &format!("fn-returning-fn `{}`: returned-closure param type", f.name), &e, ))) .collect::<Result<Vec<_>, _>>()?; let rty = match return_type.as_ref() { Some(rt) => self.type_ref_to_c(rt).map_err(|e| self.err_no_int_fallback( &format!("fn-returning-fn `{}`: returned-closure return type", f.name), &e, ))?, None => "nova_unit".to_string(), }; self.fn_returns_fn_sig.insert(f.name.clone(), (ptys, rty)); } // Plan 14 Ф.3: регистрируем сигнатуру free fn в user_fn_sigs для // emit free-fn-as-value (`let f = inc`, `xs.map(inc)`). // Только для top-level fn без receiver'а (не методы) и не для // generic fn (мономорфизация по call-site, sig зависит от инстанциации). if f.receiver.is_none() && f.generics.is_empty() { // Plan 70 PhaseA1.2: strict mode — top-level non-generic fn signature // (user_fn_sigs registration). All concrete param types must translate. let param_c_tys: Vec<String> = f.params.iter() .map(|p| self.type_ref_to_c(&p.ty).map_err(|e| self.err_no_int_fallback( &format!("free fn `{}` parameter `{}`", f.name, p.name), &e, ))) .collect::<Result<Vec<_>, _>>()?; self.user_fn_sigs.insert(f.name.clone(), (param_c_tys, ret.clone())); // [M-196-freefn-arity-overload-default-ret-mismatch] fix: additively // record THIS overload's own arity alongside the last-wins entry // above (see `free_fn_ret_by_arity`'s doc) — every overload sharing // `f.name` pushes its own `(params.len(), ret)` instead of the // single slot overwriting. self.free_fn_ret_by_arity.entry(f.name.clone()) .or_default() .push((f.params.len(), ret.clone())); // Bidirectional inference: for each fn-typed parameter, record the // inner closure signature so ClosureLight call-site args can infer // their parameter types without explicit annotations. for (idx, p) in f.params.iter().enumerate() { if let Some(_ft) = self.resolve_fn_typeref(&p.ty) { let TypeRef::Func { params: fp, return_type, .. } = &_ft else { unreachable!() }; // Plan 70 PhaseA1.2: strict mode — HOF param signature (inner closure). let inner_ptys: Vec<String> = fp.iter() .map(|t| self.type_ref_to_c(t).map_err(|e| self.err_no_int_fallback( &format!("HOF param `{}` в fn `{}`: inner closure param type", p.name, f.name), &e, ))) .collect::<Result<Vec<_>, _>>()?; let inner_rty = match return_type.as_ref() { Some(rt) => self.type_ref_to_c(rt).map_err(|e| self.err_no_int_fallback( &format!("HOF param `{}` в fn `{}`: inner closure return type", p.name, f.name), &e, ))?, None => "nova_unit".to_string(), }; self.hof_param_fn_sigs.insert( (f.name.clone(), idx), (inner_ptys, inner_rty), ); } } // Plan 14 Ф.6: регистрация variadic-флага. if f.params.last().map(|p| p.is_variadic).unwrap_or(false) { self.user_fn_variadic.insert(f.name.clone()); } } self.line(&format!("{}{} {}({});", self.top_level_storage(), ret, mangled, params)); // Plan 172.14 (sret/_out §2): forward-decl `__sret`-варианта для // sret-eligible методов (та же классификация, что в emit_fn). if f.receiver.is_some() && self.sret_fn_eligible(f, &ret) { let params_sret = if params == "void" { format!("{} _out", ret) } else { format!("{}, {} _out", params, ret) }; self.line(&format!("{}{} {}__sret({});", self.top_level_storage(), ret, mangled, params_sret)); } Ok(()) } // ---- type declarations ---- /// Plan 52.2 Ф.2: forward-declare mono'd struct-type для использования /// в const-decl до того как mono pass эмитит full struct definition. /// /// Для `HashMap[str, int]` эмитит: /// ```c /// typedef struct Nova_HashMap____nova_str__nova_int Nova_HashMap____nova_str__nova_int; /// ``` /// /// Это даёт C-compiler'у знать имя struct (opaque), достаточно для /// pointer-type declaration в const-decl. Полная struct-definition /// эмитится позже через mono pass; pointer-type работает с opaque /// forward-decl. fn forward_declare_generic_type(&mut self, ty: &TypeRef) { let TypeRef::Named { path, generics, .. } = ty else { return; }; if generics.is_empty() { return; } // Только для record/sum-типов (не primitive Option/Array) let base_name = path.join("_"); if matches!(base_name.as_str(), "Option" | "Array" | "int" | "str" | "bool" | "f64" | "f32" | "i32" | "i64" | "u32" | "u64" | "i8" | "i16" | "u8" | "u16" | "char") { return; } // Вычислить C-имена type-args let type_args_c: Vec<String> = generics.iter() .filter_map(|g| self.type_ref_to_c(g).ok()) .collect(); if type_args_c.len() != generics.len() { return; } // Plan 56 followup: skip placeholder forward decl (Nova_K* / Nova_V* // — generic param placeholders, не concrete). Это soft-guard для // recursive generic calls в @clone() body etc. — placeholder instance // не должна emit'иться вообще (нет concrete struct definition для // placeholder type → references на forward decl ломают C compile). if let Some(template) = self.generic_type_templates.get(&base_name) { let generic_names: std::collections::HashSet<String> = template.generics.iter() .map(|g| g.name.clone()).collect(); let has_placeholder = type_args_c.iter() .any(|c| self.debt_type_arg_is_bare_placeholder(c, &generic_names)); if has_placeholder { return; } } let mono_name = Self::compute_generic_type_c_name(&base_name, &type_args_c); // Forward-declare через typedef. Idempotent если повторно вызван. self.line(&format!("typedef struct {0} {0};", mono_name)); } fn emit_type_decl(&mut self, t: &TypeDecl) -> Result<(), String> { // Plan 124.8 V2 (D226 §«codegen» V2 production): value-record // (`type X value { ... }`) emits as inline C struct (value type) // через emit_value_record_type — symmetric с NamedTuple path // (Plan 120 D215). Branch by t.allocation enum в Record dispatch // ниже в this function. // // V2 capabilities: // - `typedef struct NovaValue_X NovaValue_X;` inline value type. // - Field access via `.` (struct member, not `->` pointer member). // - Pass-by-value на parameter passes (C handles natively). // - Stack init для record literal (no nova_alloc heap allocation). // - is_value_type recognizes NovaValue_ prefix for member-access path. // // V2.1 advanced (further followups): // - `[]NovaValue_X` array inline storage (currently boxes elements). // - Escape analysis (auto-promote to heap if value escapes scope). // - Cross-module generic instantiation для value-records. // Plan 62.D.bis (D126): `external type X` — opaque, no emission. // Struct definition lives in runtime header (`nova_rt/<name>.h`), // never emit'нем locally. Forward-decl skip уже handled через // BUILTIN_RUNTIME_TYPES (emit_c.rs:1212-1218); это early-return // — defensive double-protection, делает intent explicit. // // Plan 100.5 (D163): Exception — user-defined `external type X consume` // for FFI opaque resource handles. These are NOT stdlib types (no // `nova_rt/<name>.h`), so we emit a minimal opaque struct definition: // typedef struct Nova_File { void* _nv_handle; } Nova_File; // This lets the C compiler see the struct layout (pointer-sized) while // keeping the type opaque (no user-accessible fields). The `_nv_handle` // is never accessed directly by generated code — the external fn wrappers // cast/coerce as needed. BUILTIN_RUNTIME_TYPES (stdlib opaque types) are // NOT matched here because they're filtered before emit_type_decl is // called (via the BUILTIN_RUNTIME_TYPES early-continue in emit_module). if matches!(t.kind, TypeDeclKind::Opaque) { if t.consume { // Plan 100.5 (D163): User-defined consume opaque type (FFI handle). // Emit a minimal opaque struct definition so the C compiler knows // the type. Without this, `Nova_File*` in generated function signatures // is an "unknown type name". The `_nv_handle` field is never accessed // by generated code — external fns receive/return pointer-sized values. // Stdlib opaque types (pre-filtered by BUILTIN_RUNTIME_TYPES check // before emit_type_decl is called) are NOT affected by this branch. self.user_type_fwd_decls.push_str(&format!( "typedef struct Nova_{0} {{ void* _nv_handle; }} Nova_{0};\n", t.name )); // Register in opaque_ffi_types so debt_is_generic_stub_c treats // `Nova_File*` as a concrete type (not an erased generic stub). // This makes Result[File, IoErr] monomorphize correctly to // `NovaRes_Nova_File_p_Nova_IoErr*` rather than the erased fallback. self.opaque_ffi_types.insert(t.name.clone()); } return Ok(()); } // Plan 62.A: skip emission of types pre-defined в nova_rt/*.h. // Когда `std/prelude/core.nv` declares `type Option[T]` / // `type Result[T, E]` / `type Error`, codegen НЕ должен заново // emit'ить typedef struct + constructors — они уже в nova_rt/ // array.h (`Nova_Option`, `Nova_Result`, `Nova_Error`, // `nova_make_Result_Ok`, etc.). Conflict resolved через skip // emission на основе name lookup. Schema registration уже сделана // pre-populated (emit_module §954-985). // // Не путать с BUILTIN_TYPE_NAMES (forward-decl skip) — это // полный skip emit_type_decl на самом раннем этапе. // RUNTIME_DEFINED_TYPES — module-level const (§0 single source, top of file). if RUNTIME_DEFINED_TYPES.contains(&t.name.as_str()) { // Plan 62.A: skip emission — C struct + constructors живут в // nova_rt/*.h. Но Plan 78 Ф.2 (2026-05-22): для runtime- // defined **sum-типов** (RuntimeError, MemOrdering) codegen'у // всё равно нужна sum-schema (payload-типы вариантов) для // pattern-matching. Регистрируем её ИЗ ДЕКЛАРАЦИИ — // убирает хардкод-зеркало pre-populate в `emit_module`. // (Option/Result schema — через mono-типы, не сюда.) // // Plan 103.1 Ф.3: MemOrdering также регистрируется в // sum_schema_registry для find_variant_compat — иначе // pattern matching через sum_schema_registry.find_variant_compat // не найдёт варианты MemOrdering. if let TypeDeclKind::Sum(variants) = &t.kind { if !variants.is_empty() && !self.sum_schemas.contains_key(&t.name) { let mut schema: HashMap<String, Vec<String>> = HashMap::new(); let mut variant_order: Vec<String> = Vec::new(); for v in variants { let field_types: Vec<String> = match &v.kind { SumVariantKind::Unit => Vec::new(), SumVariantKind::Tuple(types) => types.iter() .map(|ty| self.type_ref_to_c(ty) .unwrap_or_else(|_| "void*".into())) .collect(), SumVariantKind::Record(fields) => { let mut fts = Vec::new(); for f in fields { let c_ty = self.type_ref_to_c(&f.ty) .unwrap_or_else(|_| "void*".into()); self.record_variant_field_types.insert( format!("{}::{}::{}", t.name, v.name, f.name), c_ty.clone()); fts.push(c_ty); } self.record_variant_field_order.insert( format!("{}::{}", t.name, v.name), fields.iter().map(|f| f.name.clone()).collect()); fts } }; variant_order.push(v.name.clone()); schema.insert(v.name.clone(), field_types); } // Register in sum_schema_registry so find_variant_compat // can resolve variant names for pattern matching. let c_name = format!("Nova_{}", t.name); self.sum_schema_registry.register_user_sum( &t.name, &schema, &c_name, super::sum_schema_registry::SumAbi::PointerErrorLike, &variant_order, ); self.sum_schemas.insert(t.name.clone(), schema); } } // Plan 175 Ф.1 (D316 — единый источник схемы): built-in effect // vtables (Time / TimerMetrics) объявлены в .nv, а их C-vtable/ // dispatch живут в nova_rt/*.h (direct-C, RUNTIME_DEFINED_TYPES + // BUILTIN_VTABLE_NAMES skip). Здесь строим `effect_schemas[name]` // ИЗ ДЕКЛАРАЦИИ (симметрично sum-schema выше) — убирает хардкод- // зеркало в emit_module. НЕ вызываем emit_effect_type (он бы // сгенерировал конфликтующий typedef + dispatcher'ы). Guard // `!contains_key` сохраняет ранее pre-registered эффекты // (Fail — его хардкод остаётся источником в Ф.1; "Mem" больше // не эффект — D76 amend, не проходит через эту ветку вовсе). if let TypeDeclKind::Effect(methods) = &t.kind { if !self.effect_schemas.contains_key(&t.name) { // Param C-types per method (нужны для mangle_op — как в // emit_effect_type; для уникальных опов mangle == имя). let mut method_param_c: Vec<(String, Vec<String>)> = Vec::new(); for m in methods { let mut ptypes: Vec<String> = Vec::new(); for p in &m.params { ptypes.push(self.type_ref_to_c(&p.ty)?); } method_param_c.push((m.name.clone(), ptypes)); } let all_method_pairs: Vec<(&str, &[String])> = method_param_c.iter() .map(|(n, p)| (n.as_str(), p.as_slice())) .collect(); let mut schema: HashMap<String, (Vec<String>, String)> = HashMap::new(); for (m, (_, param_c_types)) in methods.iter().zip(method_param_c.iter()) { let ret = match &m.return_type { None => "nova_unit".to_string(), Some(tr) => self.type_ref_to_c(tr)?, }; let mangled = Self::mangle_op(&m.name, param_c_types, &all_method_pairs); schema.insert(mangled, (param_c_types.clone(), ret)); } self.effect_schemas.insert(t.name.clone(), schema); } } // Plan 207: RUNTIME_DEFINED_TYPES named-tuple registration — // mirrors Sum/Effect above. Needed for hand-written value structs // (e.g. CasRaw* CAS-witness carriers) pre-declared directly in a // nova_rt header as `NovaTuple_<Name>`. Struct-body emission is // skipped (already defined in the header), but codegen still // needs the field schema / `NovaTuple_<Name>` C-type alias to // lower field access (`.ok`/`.witness`) and local-var/return // types — without this, an unregistered NamedTuple name falls // back to the generic unknown-type `Nova_<Name>*` pointer-record // convention (wrong ABI vs the value-struct actually declared in // the header). if let TypeDeclKind::NamedTuple(fields) = &t.kind { if !self.record_schemas.contains_key(&t.name) { let mut schema = HashMap::new(); let mut nt_field_c_tys: Vec<String> = Vec::new(); for f in fields { let ty_c = self.type_ref_to_c(&f.ty)?; schema.insert(f.name.clone(), ty_c.clone()); nt_field_c_tys.push(ty_c.clone()); } self.record_schemas.insert(t.name.clone(), schema); self.record_field_order.insert( t.name.clone(), fields.iter().map(|f| f.name.clone()).collect(), ); self.value_struct_field_tys .insert(format!("NovaTuple_{}", t.name), nt_field_c_tys); let field_defaults: Vec<(String, Option<crate::ast::Expr>)> = fields.iter() .map(|f| (f.name.clone(), f.default.clone())) .collect(); self.named_tuple_field_defaults.insert(t.name.clone(), field_defaults); self.type_aliases.insert(t.name.clone(), format!("NovaTuple_{}", t.name)); } } // Plan 248 (wave 3, D447 #no_copy): RUNTIME_DEFINED_TYPES // value-record registration — mirrors the NamedTuple branch just // above. The 11 `Atomic*` types moved from a pointer-newtype // (`type X(*())`) to a value-inside record (`type X value priv // { v T }`) whose C struct is hand-written directly in // sync_primitives.h, named `NovaValue_<Name>` — the SAME prefix // convention every OTHER value-record in the language uses // (§0 single source: `is_value_struct`/receiver-ABI/generic // Option-Result wrapping all key off this exact prefix; the // header struct is named to match rather than the codegen // learning a second, unrecognized "value but not NovaValue_- // prefixed" case). Struct-body emission stays skipped (the // header owns it, RUNTIME_DEFINED_TYPES gate above); only the // schema/alias registration an ordinary value-record gets from // `emit_value_record_type` is missing without this branch — the // generic-fallback `Nova_<Name>*` heap-pointer convention // (`resolved_named_to_c`'s catch-all) would apply instead, wrong // ABI for a stack value (surfaced as a C type-mismatch: a // `mut c = AtomicInt.new(0)` local declared `Nova_AtomicInt*` // while the hand-written ctor now returns a bare // `NovaValue_AtomicInt` struct by value). // [M-str-record-schemas-revives-legacy-from-dispatch] (found by // differential bisect, 2026-08-06 — regression from THIS same // wave, `83788f7a9`, root-caused post-hoc, not by the wave // itself): `str` is ALSO `RUNTIME_DEFINED_TYPES` (lang-item, // hand-written `nova_str` typedef — see the const list above) // AND declared `TypeDeclKind::Record` + `AllocKind::Value` // (Plan 139.1) — so it falls into this branch exactly like the // 11 atomics it was written for, and would get a // `record_schemas["str"]` entry. That specific registration // was ALREADY diagnosed and reverted once before, for an // UNRELATED reason (`[M-open-range-len-source-hardcoded]`, see // the Ф.1-REVERTED comment ~6330 above): putting `"str"` into // `record_schemas` at all — regardless of WHICH code path does // it — revives a dormant legacy `.from`-dispatch fallback keyed // on "is this name in record_schemas", independent of the // schema's actual content. Symptom: `str.from(5)` (retracted, // D410 — must be a compile error, `neg_str_from_retracted`) // silently compiles again, codegen emitting a call to the // still-present-but-unreachable-by-design `Nova_str_static_from` // runtime helper. `str` needs NONE of this branch's registration // (`type_aliases`/`value_record_names`/`value_struct_field_tys`) // — its C type is the hand-written `nova_str` (not // `NovaValue_str`), wired through its own dedicated paths // elsewhere, same reasoning as the original revert. Excluded by // name, not by weakening the shared Value+Record gate (the 11 // atomics — and any future RUNTIME_DEFINED_TYPES value-record — // still need it). if let TypeDeclKind::Record(fields) = &t.kind { if t.name != "str" && matches!(t.allocation, crate::ast::AllocKind::Value) && !self.record_schemas.contains_key(&t.name) { let mut schema = HashMap::new(); let mut field_c_tys: Vec<String> = Vec::new(); for f in fields { let ty_c = self.type_ref_to_c(&f.ty)?; schema.insert(f.name.clone(), ty_c.clone()); field_c_tys.push(ty_c); } self.record_schemas.insert(t.name.clone(), schema); self.record_field_order.insert( t.name.clone(), fields.iter().map(|f| f.name.clone()).collect(), ); self.value_struct_field_tys .insert(format!("NovaValue_{}", t.name), field_c_tys); self.type_aliases.insert(t.name.clone(), format!("NovaValue_{}", t.name)); self.value_record_names.insert(t.name.clone()); } } return Ok(()); } // Plan 48 Ф.3: generic types are emitted in erased form (void* for type-param fields) // for bootstrap erasure mode. Monomorphized instances are emitted lazily by // drain_generic_type_worklist when explicit type args are provided. // Templates already stored in 1a pre-pass; here we emit the erased fallback only. if !t.generics.is_empty() { if let TypeDeclKind::Record(fields) = &t.kind { let type_params: HashSet<String> = t.generics.iter().map(|g| g.name.clone()).collect(); let mut schema = HashMap::new(); self.line(&format!("typedef struct Nova_{0} Nova_{0};", t.name)); self.line(&format!("struct Nova_{} {{", t.name)); self.indent += 1; for f in fields { let c_ty = match &f.ty { TypeRef::Named { path, .. } if path.len() == 1 && type_params.contains(&path[0]) => "void*".to_string(), TypeRef::Array(inner, _) if matches!(inner.as_ref(), TypeRef::Named { path, .. } if path.len() == 1 && type_params.contains(&path[0])) => "NovaArray_nova_int*".to_string(), // Named generic type with type-param args (e.g. HashMap[K, V]) → void* // in erased form. Array fields ([]Slot[K,V]) fall through and get // NovaArray_nova_int* from type_ref_to_c's Array arm. TypeRef::Named { path, generics, .. } if path.len() >= 1 && !generics.is_empty() && generics.iter().any(|g| Self::type_ref_uses_any_type_param(g, &type_params)) => "void*".to_string(), // Plan 70 PhaseA1: strict mode — concrete field types must // translate successfully. Earlier arms catch generic-param erasure // patterns (void* / NovaArray_nova_int*); this fall-through is for // non-generic-param types where translation fail = compiler bug. // **Plan 147 Ф.3 (D246, 3-axis L3):** the D33 §2 // binding-mut promotion is REMOVED — pointee-mutability // comes from the TYPE (`*mut T`), never inherited from // the field's `mut` binding (L1). A `mut`-bound bare // `*T` field is now a `mut`-reassignable handle to a // *ro* pointee (`const T*`); a writable buffer must be // declared `*mut T` explicitly (as `Vec.data` is). The // field type is emitted verbatim. _ => self.type_ref_to_c(&f.ty).map_err(|e| self.err_no_int_fallback( &format!("field `{}` в type `{}`", f.name, t.name), &e, ))?, }; let mangled = Self::mangle_field_name(&f.name); self.line(&format!("{} {};", c_ty, mangled)); schema.insert(f.name.clone(), c_ty); } self.indent -= 1; self.line("};"); self.line(""); self.record_schemas.insert(t.name.clone(), schema); } // Generic sum types: emit erased form so that erased method bodies // (emit_generic_method_erased) can access ->tag and ->payload. if let TypeDeclKind::Sum(variants) = &t.kind { // Plan 72 P1-B: generic empty sum — same int64_t ABI as non-generic. if variants.is_empty() { self.line(&format!("typedef int64_t Nova_{};", t.name)); self.line(""); self.sum_schemas.insert(t.name.clone(), HashMap::new()); return Ok(()); } let type_params: HashSet<String> = t.generics.iter().map(|g| g.name.clone()).collect(); // Tag enum self.line("typedef enum {"); self.indent += 1; for v in variants { // №296: explicit discriminants (`= N`, incl. negative) // must reach the C tag enum — C auto-increments an // entry with no `= N` from the PREVIOUS listed value // (spec 02-types.md rule 2), same rule Nova specifies, // so omitting `=` for `None` variants is correct as-is. match v.discriminant { Some(d) => self.line(&format!("NOVA_TAG_{}_{} = {},", t.name, v.name, d)), None => self.line(&format!("NOVA_TAG_{}_{},", t.name, v.name)), } } self.indent -= 1; self.line(&format!("}} Nova_{}_Tag;", t.name)); self.line(&format!("typedef struct Nova_{0} Nova_{0};", t.name)); self.line(&format!("struct Nova_{} {{", t.name)); self.indent += 1; self.line(&format!("Nova_{}_Tag tag;", t.name)); self.line("union {"); self.indent += 1; let mut sum_schema: HashMap<String, Vec<String>> = HashMap::new(); let has_payload = variants.iter().any(|v| !matches!(v.kind, SumVariantKind::Unit)); if !has_payload { self.line("char _dummy;"); } for v in variants { match &v.kind { SumVariantKind::Unit => { sum_schema.insert(v.name.clone(), vec![]); } SumVariantKind::Tuple(types) => { let mut field_types = Vec::new(); self.line("struct {"); self.indent += 1; for (i, ty) in types.iter().enumerate() { let c_ty = if let TypeRef::Named { path, generics, .. } = ty { if path.len() == 1 && type_params.contains(&path[0]) { "void*".to_string() } else if !generics.is_empty() && generics.iter().any(|g| Self::type_ref_uses_any_type_param(g, &type_params)) { "void*".to_string() } else { self.type_ref_to_c(ty).unwrap_or_else(|_| "void*".into()) } } else { self.type_ref_to_c(ty).unwrap_or_else(|_| "void*".into()) }; field_types.push(c_ty.clone()); self.line(&format!("{} _{};", c_ty, i)); } self.indent -= 1; self.line(&format!("}} {};", v.name)); sum_schema.insert(v.name.clone(), field_types); } SumVariantKind::Record(fields) => { let mut field_types = Vec::new(); self.line("struct {"); self.indent += 1; for f in fields { let c_ty = if let TypeRef::Named { path, generics, .. } = &f.ty { if path.len() == 1 && type_params.contains(&path[0]) { "void*".to_string() } else if !generics.is_empty() && generics.iter().any(|g| Self::type_ref_uses_any_type_param(g, &type_params)) { "void*".to_string() } else { self.type_ref_to_c(&f.ty).unwrap_or_else(|_| "void*".into()) } } else { self.type_ref_to_c(&f.ty).unwrap_or_else(|_| "void*".into()) }; field_types.push(c_ty.clone()); let mf = Self::mangle_field_name(&f.name); self.line(&format!("{} {};", c_ty, mf)); let key = format!("{}::{}::{}", t.name, v.name, f.name); self.record_variant_field_types.insert(key, c_ty); } let order_key = format!("{}::{}", t.name, v.name); let field_names: Vec<String> = fields.iter().map(|f| f.name.clone()).collect(); self.record_variant_field_order.insert(order_key, field_names); self.indent -= 1; self.line(&format!("}} {};", v.name)); sum_schema.insert(v.name.clone(), field_types); } } } self.indent -= 1; self.line("} payload;"); self.indent -= 1; self.line("};"); self.line(""); // Emit erased constructor functions (name with Nova_ prefix for C) let type_name = t.name.clone(); let cname = format!("Nova_{}", type_name); let sum_schema_clone = sum_schema.clone(); for v in variants { let field_types = sum_schema_clone.get(&v.name).cloned().unwrap_or_default(); let params: String = field_types.iter().enumerate() .map(|(i, ty)| format!("{} _{}", ty, i)) .collect::<Vec<_>>() .join(", "); let params_str = if params.is_empty() { "void".to_string() } else { params }; self.line(&format!( "{storage}{cname}* nova_make_{tname}_{var}({params}) {{", storage = self.top_level_storage(), cname = cname, tname = type_name, var = v.name, params = params_str )); self.indent += 1; self.line(&format!( "{cname}* _r = ({cname}*)nova_alloc(sizeof({cname}));", cname = cname )); self.line(&format!("_r->tag = NOVA_TAG_{name}_{var};", name = type_name, var = v.name)); match &v.kind { SumVariantKind::Unit => {} SumVariantKind::Tuple(_) => { for (i, _) in field_types.iter().enumerate() { self.line(&format!("_r->payload.{var}._{i} = _{i};", var = v.name, i = i)); } } SumVariantKind::Record(fields) => { for (i, f) in fields.iter().enumerate() { let mf = Self::mangle_field_name(&f.name); self.line(&format!("_r->payload.{var}.{fname} = _{i};", var = v.name, fname = mf, i = i)); } } } self.line("return _r;"); self.indent -= 1; self.line("}"); self.line(""); } // Plan 62.A.bis Ф.2.1: mirror legacy insert в registry. // Generic sum types — heap-pointer ABI (PointerErrorLike). let variant_order: Vec<String> = variants.iter() .map(|v| v.name.clone()).collect(); let c_name = format!("Nova_{}", t.name); self.sum_schema_registry.register_user_sum( &t.name, &sum_schema, &c_name, super::sum_schema_registry::SumAbi::PointerErrorLike, &variant_order, ); self.sum_schemas.insert(t.name.clone(), sum_schema); } // Plan 91.12 V2 followup (2026-06-02) — generic Newtype: // `type X[T](ptr)` за исключением runtime-backed sync types // (OnceCell/Lazy/Condvar — обрабатываются в emit_generic_type_instance // через emit_oncecell_instance/emit_lazy_instance). // // Для произвольного generic newtype над ptr (user FFI handle): // - T параметр — type-system fiction, C-level value identical // - Emit single typedef `typedef nova_ptr Nova_X;` — все // monomorphizations X[int]/X[str]/etc. share C representation // - Register в type_aliases для constructor `MyHandle[T](v)` // identity-cast intercept (emit_call) // Inner non-ptr types (e.g. `type Wrap[T](int)`) — followup // [M-91.12-generic-newtype-non-ptr-inner]. if let TypeDeclKind::Newtype(inner) = &t.kind { // Plan 173.3 (D415 §2): extended with the pointer-handle sync // primitives that got a `#share`-carrying `type X(*())` decl // (their C struct/typedef already lives hand-written in // nova_rt/sync_*.h — same "codegen must not also emit its own // conflicting typedef" reasoning as Condvar). Atomics: `.new()` // returns `Nova_AtomicX*` (heap ptr) despite the VALUE struct // typedef in sync_primitives.h — same pointer-handle ABI shape. if !Self::debt_is_runtime_backed_newtype(t.name.as_str()) { // Use type_ref_to_c — для inner = *() это даст "void*"; // для других pointer types — "*T" form. Primitives work too. // Generic type params (T) in inner — fail; гард: emit // только если inner_c не пустой. if let Ok(inner_c) = self.type_ref_to_c(inner) { if !inner_c.is_empty() { // Plan 134: inner *() → "void*"; inner *T → "const T*"; etc. self.line(&format!( "typedef {} Nova_{};", inner_c, t.name )); self.type_aliases.insert(t.name.clone(), inner_c); } } } } return Ok(()); } // [M-sync-crossmodule…] (D381): make the type's own defining file the // current context so field-type references to colliding types resolve // (byte-identical when no collision — `current_emit_file_id` only steers // `ref_type_base`, a no-op for non-colliding names). `def_base` is the // module-qualified struct/tag base for a colliding struct-like type, else // the bare `t.name`. Restored after the match (errors abort compilation). let saved_emit_file = self.current_emit_file_id; if self.any_type_file_collision() { self.current_emit_file_id = Some(t.span.file_id); } let def_base = self.def_type_base(&t.name, t.span.file_id); match &t.kind { TypeDeclKind::Record(fields) => { // Plan 124.8 V2 (D226): branch by allocation contract. // Heap (default) → emit_record_type (pointer-based, GC). // Value → emit_value_record_type (inline struct, stack). use crate::ast::AllocKind; match t.allocation { AllocKind::Heap => self.emit_record_type(&def_base, fields)?, AllocKind::Value => self.emit_value_record_type(&def_base, fields)?, // Plan 127 V1: ValueHeapPromoted lives только на per-binding // slots, не на TypeDecl. Type declaration аллокация всегда // {Heap, Value}. Per-binding promotion обрабатывается на // record-lit / method-recv level (Ф.3). Unreachable here. AllocKind::ValueHeapPromoted => unreachable!( "AllocKind::ValueHeapPromoted invalid on TypeDecl `{}` — \ promotion is per-binding, not per-type (Plan 127 V1)", t.name ), } } TypeDeclKind::Sum(variants) => { self.emit_sum_type(&def_base, variants)?; } TypeDeclKind::Newtype(inner) => { // Plan 91.12 V2 (D126 retract — sync types migration): // runtime-backed newtype declarations skip emit. Their C struct // lives either in nova_rt/*.h (non-generic: Condvar in // sync_condvar.h) or is emitted per-T by emit_generic_type_instance // (generic: OnceCell/Lazy via emit_oncecell_instance / // emit_lazy_instance). Emitting `typedef void* Nova_X` here // would conflict with the actual struct typedef. // // List parallels Plan 91.12 V2 §«D126 retract — sync types»: // ровно те 3 типа, что мигрировали с `external type` (Opaque) // на `type X(*())` / `type X[T](*())`. // Plan 173.3 (D415 §2): extended with the pointer-handle sync // primitives that got a `#share`-carrying `type X(*())` decl // (their C struct/typedef already lives hand-written in // nova_rt/sync_*.h — same "codegen must not also emit its own // conflicting typedef" reasoning as Condvar). Atomics: `.new()` // returns `Nova_AtomicX*` (heap ptr) despite the VALUE struct // typedef in sync_primitives.h — same pointer-handle ABI shape. if Self::debt_is_runtime_backed_newtype(t.name.as_str()) { // Skip typedef emit — runtime / per-T mono handles it. // Register alias так чтобы type_ref_to_c для bare `OnceCell` // (без mono context) не падал. Generic refs резолвятся // через per-T mono mangling separately. return Ok(()); } let inner_c = self.type_ref_to_c(inner)?; // [fix №154 форма (б)] Emit the typedef under `def_base` (the // collision-aware qualified base — see the `record_def`/ // `qualifiable` doc above), not the bare `t.name`: a colliding // Newtype simple name (`Sign` vs std's `runtime.fmt_buf.Sign`) // otherwise emits a second `typedef ... Nova_Sign;` for a // DIFFERENT underlying C type → CC-FAIL `typedef redefinition // with different types`. Byte-identical when non-colliding // (`def_base` ≡ `t.name`). `type_aliases` stays keyed by the // bare SOURCE name — same convention as `sum_schemas`/ // `record_schemas` (reference sites resolve the alias by source // name; only the emitted C text needs the qualified base). // // Emit into user_type_fwd_decls (spliced before value-record defs // and tuple typedefs) so that value-record fields of newtype can // reference this typedef without forward-declaration issues. self.user_type_fwd_decls.push_str(&format!( "typedef {} Nova_{};\n", inner_c, def_base)); // Newtypes are typedef'd scalars — use inner type directly (no pointer indirection) self.type_aliases.insert(t.name.clone(), inner_c); } TypeDeclKind::Alias(inner) => { let inner_c = self.type_ref_to_c(inner)?; self.line(&format!("typedef {} Nova_{};", inner_c, t.name)); // Register alias so type_ref_to_c returns inner type directly (no extra *) self.type_aliases.insert(t.name.clone(), inner_c); } TypeDeclKind::Effect(methods) => { self.emit_effect_type(&t.name, methods)?; } // Plan 15 D53 strict: protocols — compile-time-only // структурные контракты (D72 bound checking). Vtable не // нужен — нет runtime-dispatch'а. Skip emission. // Бонус: попутно фиксит pre-existing codegen-bug, где // Self в protocol-методе ломал vtable (Nova_Self* // undefined). Без vtable type_ref_to_c для protocol-методов // вообще не вызывается. TypeDeclKind::Protocol { .. } => {} // Plan 172.3 (D310): type-set — compile-time-only generic bound // (D72 amended). Нет runtime-типа/vtable — skip emission, как protocol. TypeDeclKind::TypeSet(_) => {} // Plan 120 (D215): named tuple — value-type struct with named fields. TypeDeclKind::NamedTuple(fields) => { self.emit_named_tuple_type(&def_base, fields)?; } // Plan 62.D.bis (D126): unreachable — early-return on top // of emit_type_decl уже отфильтровал Opaque kind. Branch // present для exhaustiveness; semantically meaningful no-op. TypeDeclKind::Opaque => {} } // [M-sync-crossmodule…] restore prior emission-file context. self.current_emit_file_id = saved_emit_file; // Plan 114.4.1 (D200): emit associated constants как top-level // `static const T Type_NAME = literal;` в .rodata. Generic // T-dependent assoc consts (sizeof(T) etc) — followup Ф.3 // [M-114.4.1-generic-per-mono]; non-generic + T-independent // обрабатываются здесь. // // [M-d200-assoc-const-composite-value] (2026-07-23): this loop MUST // run AFTER the `match &t.kind { .. }` above (record/value-record // struct body + `record_schemas`/`type_aliases` registration for // THIS type) — a composite value referencing the type's OWN shape // (`const OK StatusCode = { code: 200 }` inside `type StatusCode`) // needs both (a) `record_schemas[t.name]` populated so // `emit_const_record_lit` can resolve field C-types, and (b) the // `NovaValue_<Name>`/`Nova_<Name>` struct typedef textually emitted // BEFORE this `static const` initialiser references the complete // type (a self-referential const emitted where the old scalar-only // loop ran, ABOVE the struct body, would be an incomplete-type C // error the moment a composite value was attempted). Scalar assoc // consts are unaffected — they never depended on struct layout. for ac in &t.assoc_consts { // Plan 157: `ro Type.NAME` — NOT constexpr-required, handled by // `emit_assoc_ro_lazy_globals` (assoc_ro.rs) instead of the // strict-constexpr path below. if ac.is_lazy_ro { continue; } let ty_c = if let Some(ty) = &ac.ty { self.type_ref_to_c(ty)? } else { self.infer_expr_c_type(&ac.value) }; let val = self.emit_const_expr_typed(&ac.value, Some(&ty_c)) .map_err(|e| format!( "assoc const `{}.{}` codegen failed: {}", t.name, ac.name, e ))?; let symbol = format!("{}_{}", t.name, ac.name); self.line(&format!("{}const {} {} = {};", self.top_level_storage(), ty_c, symbol, val)); self.var_types.insert(symbol, ty_c); } // Plan 124.8 [M-124.8-zero-on-move] (2026-06-03): emit per-type // `Nova_T_zero_storage` helper для types помеченных #zero_on_move. // V1: helper доступен явному вызову; auto-injection в consume-call // sites deferred → V2 followup [M-124.8-zero-on-move-auto-inject]. if t.zero_on_move { self.emit_zero_on_move_helper(t)?; } Ok(()) } /// Plan 124.8 [M-124.8-zero-on-move] (2026-06-03): emit per-type zero /// helper. For value records / named tuples / newtypes — `memset` of the /// underlying storage. For heap records — `memset` of the pointee. /// Generated form: /// static inline void Nova_T_zero_storage(<C_type> *p) { /// if (p) memset(p, 0, sizeof(*p)); /// } /// Callable from Nova через external decl или будущий auto-inject hook. fn emit_zero_on_move_helper(&mut self, t: &TypeDecl) -> Result<(), String> { use crate::ast::AllocKind; let target_c_type = match &t.kind { TypeDeclKind::Record(_) => { match t.allocation { AllocKind::Heap => format!("Nova_{}", t.name), AllocKind::Value => format!("NovaValue_{}", t.name), // Plan 127 V1: ValueHeapPromoted invalid на TypeDecl. AllocKind::ValueHeapPromoted => unreachable!( "AllocKind::ValueHeapPromoted invalid on TypeDecl `{}` \ (Plan 127 V1: per-binding only)", t.name ), } } TypeDeclKind::NamedTuple(_) => format!("NovaTuple_{}", t.name), TypeDeclKind::Newtype(_) => format!("Nova_{}", t.name), _ => return Ok(()), }; self.line(&format!( "{}void Nova_{}_zero_storage({}* p) {{", self.top_level_storage_inline(), t.name, target_c_type )); self.indent += 1; self.line("if (p) memset((void*)p, 0, sizeof(*p));"); self.indent -= 1; self.line("}"); Ok(()) } fn emit_effect_type(&mut self, name: &str, methods: &[EffectMethod]) -> Result<(), String> { // Pre-compute C param types for all methods (needed for mangle_op). let mut method_param_c: Vec<(String, Vec<String>)> = Vec::new(); for m in methods { let mut ptypes: Vec<String> = Vec::new(); for p in &m.params { ptypes.push(self.type_ref_to_c(&p.ty)?); } method_param_c.push((m.name.clone(), ptypes)); } // Build the name+param pairs needed by mangle_op. let all_method_pairs: Vec<(&str, &[String])> = method_param_c.iter() .map(|(n, p)| (n.as_str(), p.as_slice())) .collect(); // Plan 175 Ф.2-v3: "Time" is the ONE narrow exception — its // `NovaVtable_Time` struct + `_nova_handler_Time` TLS slot stay // hand-declared in nova_rt/effects.h/.c (see the doc comment there): // hand-written C consumers OUTSIDE codegen (nova_rt/channels.h // `ChanReader.close_after` mock-time path, nova_rt/runtime.c // worker-thread TLS registration) dereference `_nova_handler_Time`'s // fields directly and are compiled ONCE (not per-CU), so they need a // single stable named type — an anonymous struct emitted fresh into // each generated CU can't serve that (and would typedef-redefinition // conflict with effects.h's). Steps 1+2 below are skipped for Time; // step 3 (dispatch functions) stays fully generic either way. let emit_struct_and_slot = name != "Time"; // 1. Vtable struct: one fn ptr per method, plus void* ctx if emit_struct_and_slot { self.line(&format!("typedef struct {{")); self.indent += 1; self.line("void* ctx;"); } let mut schema: HashMap<String, (Vec<String>, String)> = HashMap::new(); for (m, (_, param_c_types)) in methods.iter().zip(method_param_c.iter()) { let ret = match &m.return_type { None => "nova_unit".to_string(), Some(t) => self.type_ref_to_c(t)?, }; let mangled = Self::mangle_op(&m.name, param_c_types, &all_method_pairs); if emit_struct_and_slot { let mut param_types_with_ctx = vec!["void*".to_string()]; // ctx first param_types_with_ctx.extend(param_c_types.iter().cloned()); let params_sig = param_types_with_ctx.join(", "); self.line(&format!("{} (*{})({}); ", ret, mangled, params_sig)); } schema.insert(mangled.clone(), (param_c_types.clone(), ret)); } if emit_struct_and_slot { self.indent -= 1; self.line(&format!("}} NovaVtable_{};", name)); self.line(""); // 2. Thread-local handler slot. self.line("#ifdef _MSC_VER"); self.line(&format!( "__declspec(thread) NovaVtable_{name}* _nova_handler_{name} = NULL;", name = name )); self.line("#else"); self.line(&format!( "__thread NovaVtable_{name}* _nova_handler_{name} = NULL;", name = name )); self.line("#endif"); self.line(""); } // Plan 175 Ф.2-v3 [ordering-fix]: `emit_effect_type` runs during the // TYPE-decl emission pass, which happens EARLY in the generated // file — well before the `#default_handler` ctor free-fn's own // forward declaration (emitted later, in the fn-decl pass). The // ensure-default check below calls the ctor by mangled name; without // an explicit prototype HERE first, C falls back to an implicit // `int fn()` declaration at the call site, which then conflicts with // the REAL `NovaVtable_X*`-returning prototype emitted downstream // ("conflicting types for ..."). Emit a matching forward decl now — // harmless if a compatible one appears again later (repeat // prototypes are fine in C; only *conflicting* ones error). if let Some(fn_name) = self.default_handler_fns.get(name).cloned() { let ctor_c_name = self.free_fn_c_name(&fn_name); self.line(&format!( "{storage}NovaVtable_{name}* {ctor}(void);", storage = self.top_level_storage(), name = name, ctor = ctor_c_name, )); } // 3. Dispatch helpers: Nova_Effect_method() calls through vtable for (m, (_, param_c_types)) in methods.iter().zip(method_param_c.iter()) { let mangled = Self::mangle_op(&m.name, param_c_types, &all_method_pairs); let (_, ret) = schema.get(&mangled).unwrap(); let ret = ret.clone(); let mut fn_params: Vec<String> = Vec::new(); let mut call_args: Vec<String> = vec![ format!("_nova_handler_{name}->ctx", name = name) ]; for (p, ty) in m.params.iter().zip(param_c_types.iter()) { fn_params.push(format!("{} {}", ty, p.name)); call_args.push(p.name.clone()); } let fn_params_str = if fn_params.is_empty() { "void".to_string() } else { fn_params.join(", ") }; let call_args_str = call_args.join(", "); self.line(&format!( "{storage}{ret} Nova_{name}_{method}({params}) {{", storage = self.top_level_storage_inline(), ret = ret, name = name, method = mangled, params = fn_params_str )); self.indent += 1; // Plan 175 Ф.2-v2 (`#default_handler`, GENERIC — any effect may opt // in): if this effect has a registered default-handler factory, // lazily construct + install it the first time ANY op dispatches // with no `with X = …` in scope for this thread — mirrors a // closure's lazy-box-promotion pattern (compute once, memoized in // the TLS slot itself; a real `with` still overrides normally, // since `emit_with` always overwrites `_nova_handler_X` on entry // and restores the PRIOR value — NULL or the default — on exit). // No registered default → falls through to the null-check + // `nv_panic` guard below (№158) — controlled panic, not NULL-deref. if let Some(fn_name) = self.default_handler_fns.get(name).cloned() { let ctor_c_name = self.free_fn_c_name(&fn_name); self.line(&format!( "if (!_nova_handler_{name}) {{ _nova_handler_{name} = {ctor}(); }}", name = name, ctor = ctor_c_name, )); } // Plan 110.9.3 V1.1 [M-110.9.3-register-finalizer-lifo]: // intercept Application.register_finalizer dispatcher — push к // active LIFO stack БЕЗ handler-vtable call. User handler impl // ignored (V1 design: runtime-managed LIFO; per-handler observer // logic moves к Cleanup effect Plan 110.4.4). if name == "Application" && m.name == "register_finalizer" { // Plan 110.9.3 V1.1: fn arg — NovaClosBase* (fn + env) // wrapping Nova fn type. Extract closure parts через // nova_clos_base_fn / _env macros (defined в nova_rt.h). // Push pair to TLS LIFO stack. let fn_arg = m.params.first() .map(|p| p.name.clone()) .unwrap_or_else(|| "f".to_string()); self.line(&format!( "NovaClosBase* _nv_clos = (NovaClosBase*)({});", fn_arg )); self.line( "nova_finalizer_push(_nova_active_finalizer_stack, \ _nv_clos ? _nv_clos->fn : NULL, \ _nv_clos ? _nv_clos->env : NULL);" ); self.line("nova_unit _u = { 0 };"); self.line("return _u;"); } else if name == "Time" && matches!(mangled.as_str(), "sleep" | "now" | "now_monotonic" | "local_offset_sec") { // Plan 175 Ф.3 (D316 typed retype): the hand-written // `NovaVtable_Time` slots (effects.h) stay raw-int64-nanos // WIRE (they can't name a per-CU `NovaValue_Duration`/ // `Timestamp`/`Monotonic` type — see effects.h doc comment), // but THIS dispatch fn's own signature is schema-driven // (typed `NovaValue_Duration`/`Timestamp`/`Monotonic`, same // as any other effect op) — it DOES know the complete type // (emitted after Фаза 2's reordering), so the marshalling // between typed surface and raw wire happens right here, // once, at the one chokepoint every call funnels through. // // Per-FIELD null-check + real-clock fallback (mirrors the // pre-Ф.2-v3 `now_monotonic_ns`/`local_offset_sec` // backward-compat pattern, extended here to ALL FOUR ops // defensively): a handler LITERAL is allowed to be partial // (implement only SOME of Time's ops — e.g. // nova_tests/plan83_10/handler_isolation_per_fiber.nv's // handler defines only `now()`) — C99 designated-init // zero-fills the rest of the heap-allocated vtable, so an // unimplemented slot is a NULL fn pointer. Calling through // a NULL fn pointer unconditionally (as the naive // `_nova_handler_Time->field(...)` one-liner would) is a // regression vs the hand-written dispatcher this replaces; // falling back to the real-clock primitive instead keeps // the exact same behavior partial-handler fixtures relied on. match mangled.as_str() { "sleep" => { // param `d` is `NovaValue_Duration` (by value) — // extract `.nanos` for the int64-wire slot. let arg_name = m.params.first().map(|p| p.name.clone()) .unwrap_or_else(|| "d".to_string()); self.line(&format!( "if (_nova_handler_Time->sleep) {{ return _nova_handler_Time->sleep(_nova_handler_Time->ctx, {arg}.nanos); }}", arg = arg_name )); self.line(&format!( "return time_sleep_ms((nova_int)(({arg}.nanos + 999999) / 1000000));", arg = arg_name )); } "now" => { self.line("if (_nova_handler_Time->now) { int64_t _nv_w = _nova_handler_Time->now(_nova_handler_Time->ctx); return (NovaValue_Timestamp){ .nanos = _nv_w }; }"); self.line(&format!( "return ({ret}){{ .nanos = time_wall_unix_ms() * (int64_t)1000000 }};", ret = ret )); } "now_monotonic" => { self.line("if (_nova_handler_Time->now_monotonic) { int64_t _nv_w = _nova_handler_Time->now_monotonic(_nova_handler_Time->ctx); return (NovaValue_Monotonic){ .nanos = _nv_w }; }"); self.line(&format!( "return ({ret}){{ .nanos = time_monotonic_ns() }};", ret = ret )); } _ /* local_offset_sec */ => { self.line("if (_nova_handler_Time->local_offset_sec) { return _nova_handler_Time->local_offset_sec(_nova_handler_Time->ctx); }"); self.line("return time_local_offset_sec();"); } } } else { // №158: null handler → `nv_panic` (D62 "runtime fail (panic)"), не NULL-deref. self.line(&format!( "if (!_nova_handler_{name}) {{ nv_panic(nova_str_from_cstr(\"unhandled effect `{name}.{op}`: no active handler (missing `with {name} = …` around this call)\")); return ({ret}){{0}}; }} return _nova_handler_{name}->{field}({args});", name = name, op = m.name, ret = ret, field = mangled, args = call_args_str )); } self.indent -= 1; self.line("}"); self.line(""); } self.effect_schemas.insert(name.to_string(), schema); Ok(()) } /// D39 / Plan 11 Ф.9: эмитит auto-proxy методы для wrapper-типов с /// embed'ами. Для каждого Delegated MethodSig в `method_overloads` /// генерирует C-функцию которая делегирует на embedded-объекта /// через `nova_self->field`. /// /// Override-precedence: если есть Own MethodSig с тем же ключом и /// param_c_types, Delegated пропускается (skip — собственный метод /// уже эмитен в emit_fn). fn emit_embed_proxies(&mut self) -> Result<(), String> { // Snapshot ключей чтобы избежать borrow-конфликта. let wrapper_types: Vec<String> = self.embed_fields.keys().cloned().collect(); for wrapper_type in wrapper_types { let embeds = self.embed_fields.get(&wrapper_type).cloned().unwrap_or_default(); // Collect все методы wrapper'а и разделить на Own / Delegated. // Pair (method_name, param_types) → ключ для override-detection. let all_overloads: Vec<((String, String), MethodSig)> = self.method_overloads.iter() .filter(|((t, _), _)| t == &wrapper_type) .flat_map(|(k, sigs)| sigs.iter().map(move |s| (k.clone(), s.clone()))) .collect(); for ((_, method_name), sig) in &all_overloads { if !sig.is_delegated { continue; } // Plan 11 Ф.9.3: override-precedence. Если есть Own с тем же // method_name и param_c_types — пропустить delegated. let has_own_override = all_overloads.iter().any(|((_, mn), s)| mn == method_name && !s.is_delegated && s.param_c_types == sig.param_c_types); if has_own_override { // Plan 11 Ф.9.5: lint warning "possible infinite recursion" // если own-method вызывает себя без явного base-call'а // (anonymous embed не даёт имени). Накапливаем в warnings // (не eprintln!) — test runner направит в captured_stderr. if embeds.iter().any(|(_, _, anon)| *anon) { self.warnings.borrow_mut().push(format!( "warning: type `{}` overrides delegated method `{}({})`; \ anonymous embed has no name for explicit base-call — \ possible infinite recursion", wrapper_type, method_name, sig.param_c_types.join(", "))); } continue; } // Найти подходящий embed: тот в котором этот method есть // как Own (не Delegated). let mut target_field: Option<(String, String)> = None; for (fname, embedded_ty, _) in &embeds { let found = self.method_overloads.get(&(embedded_ty.clone(), method_name.clone())) .map(|sigs| sigs.iter().any(|s| !s.is_delegated && s.param_c_types == sig.param_c_types)) .unwrap_or(false); if found { target_field = Some((fname.clone(), embedded_ty.clone())); break; } } let (field_name, embedded_ty) = match target_field { Some(t) => t, None => continue, // не нашли base — skip }; // Найти base-method's c_name (mangled). let base_c_name = self.method_overloads.get(&(embedded_ty.clone(), method_name.clone())) .and_then(|sigs| sigs.iter() .find(|s| !s.is_delegated && s.param_c_types == sig.param_c_types) .map(|s| s.c_name.clone())); let base_c_name = match base_c_name { Some(n) => n, None => continue, }; // Build params + arg names. let mut param_decls: Vec<String> = vec![format!("Nova_{}* nova_self", wrapper_type)]; let mut arg_names: Vec<String> = Vec::new(); for (i, ty) in sig.param_c_types.iter().enumerate() { param_decls.push(format!("{} arg{}", ty, i)); arg_names.push(format!("arg{}", i)); } // Forward decl + body. self.line(&format!("{}{} {}({});", self.top_level_storage(), sig.return_c_type, sig.c_name, param_decls.join(", "))); self.line(&format!("{}{} {}({}) {{", self.top_level_storage(), sig.return_c_type, sig.c_name, param_decls.join(", "))); self.indent += 1; let field_mangled = Self::mangle_field_name(&field_name); let mut call_args = vec![format!("nova_self->{}", field_mangled)]; call_args.extend(arg_names); if sig.return_c_type == "nova_unit" { self.line(&format!("{}({});", base_c_name, call_args.join(", "))); self.line("return NOVA_UNIT;"); } else { self.line(&format!("return {}({});", base_c_name, call_args.join(", "))); } self.indent -= 1; self.line("}"); self.line(""); } } Ok(()) } /// Plan 124.8 V2 (D226 §«codegen» V2): emit value-record as inline /// C struct (value type). Mirror NamedTuple emission pattern with /// `NovaValue_<Name>` prefix to distinguish from heap-record path. /// /// Registers in `record_schemas` (для field access lookup) + /// `type_aliases` (для type_ref_to_c returning value type, not pointer). /// `is_value_type` уже distinguishes `NovaValue_` prefix. fn emit_value_record_type(&mut self, name: &str, fields: &[RecordField]) -> Result<(), String> { let mut schema = HashMap::new(); // [реестр 221.1 №139 Round 3 — unified value-type topo-sort] This // record's rendered text is no longer appended directly to // `value_record_defs_buf` (a FIXED early marker position). Instead it // is captured (below, `self.pending_value_nodes.push(...)`) together // with its field C-types, and rendered later as part of ONE global // topological sort spanning EVERY value-type category (user value- // records, generic-instances, both heap and value form) — see // `render_unified_value_types` for the full rationale (this is the // Round-3 fix for the class of bug rounds 1/2 whack-a-moled: a fixed // marker order can satisfy only ONE direction of a dependency that, // in the real corpus, goes BOTH ways — a value-record can embed a // generic-instance by value (round 1: `Bundle` needs `Wrap[int]`) // AND a generic-instance can embed a user value-record by value // (round 3: nova-http's Header wrapper needs `HeaderName`/ // `HeaderValue`) — only a real dependency-respecting sort over the // UNION of both categories is correct in general). // // Round-1/2's `hoist_pending_generic_type_defs` (force-drain + // relocate) is now UNNECESSARY and REMOVED: this record's position // moves relative to OTHER value-types (generic-instances/tuples) // via the unified topo-sort itself, not a manual hoist. // // The NovaOpt `opt_delta` hoist below, however, is STILL NEEDED — // this record renders at the EARLY unified position // (`__VALUE_RECORD_DEFS__`, still the earliest safe marker: a PLAIN // value-record used elsewhere as an ordinary `Option[T]`/`Result[T,E]` // payload — e.g. std prelude's `Utf8Error` — must be available // BEFORE `__NOVAOPT_TYPEDEFS__`/`__NOVARES_TYPEDEFS__`, ruling out // moving value-records to a later position), which is BEFORE those // NORMAL (non-late) NovaOpt/NovaRes markers — so a record whose OWN // field is `Option[AnotherValueRecord]` (a plain, non-mono payload — // e.g. flagship's `ExtDto.opt_rec Option[ValRec]`) needs that // SPECIFIC `NovaOpt_NovaValue_ValRec` wrapper struct complete // BEFORE this record's own struct — but it would otherwise only be // declared at the later, non-late-payload marker position. (A late // VALUE-payload Option/Result whose payload contains "____" — a // MONO'd generic value-record — is a DIFFERENT case, already routed // by `register_novaopt_decl_forced` to the separate, even-later // `__NOVAOPT_VR_TYPEDEFS__`/`__NOVARES_VR_TYPEDEFS__` markers, // untouched here.) // // A by-POINTER field to a mono'd generic struct (`Nova_X____…*`) / // value-record / named-tuple still only needs a forward typedef // ahead of THIS record's own struct (the pointee's full body may // legitimately sort to EITHER side in the unified order) — // collected into `fwd_decls`, unchanged. let opt_snap = self.novaopt_typedefs_buf.borrow().len(); let mut fwd_decls = String::new(); // Plan 172.12 A8: by-value field C-types, for the pre-registered // NovaOpt block relocation below. let mut field_c_tys: Vec<String> = Vec::new(); let saved_out = std::mem::take(&mut self.out); self.line(&format!("typedef struct NovaValue_{0} NovaValue_{0};", name)); self.line(&format!("struct NovaValue_{} {{", name)); self.indent += 1; if fields.is_empty() { self.line("char _empty_value_record_marker;"); } for f in fields { let ty_c = self.type_ref_to_c(&f.ty)?; field_c_tys.push(ty_c.clone()); // By-pointer field to a late-emitted struct → early forward typedef. // Restrict to guaranteed struct tags (mono `____`, NovaValue_, // NovaTuple_) so we never shadow a non-struct newtype alias typedef. if let Some(pointee) = ty_c.strip_suffix('*') { let pointee = pointee.trim(); if self.debt_is_guaranteed_struct_tag(pointee) { let fwd = format!("typedef struct {p} {p};\n", p = pointee); // [Round 3] dedup is now WITHIN this record's own // `fwd_decls` only (was ALSO cross-checked against the // shared `value_record_defs_buf` accumulator — that // buffer no longer accumulates cross-record text, see // fn doc). A duplicate `typedef struct X X;` across // DIFFERENT nodes' captured text is harmless — C11 §6.7 // permits identical redeclaration. if !fwd_decls.contains(&fwd) { fwd_decls.push_str(&fwd); } } } schema.insert(f.name.clone(), ty_c.clone()); let mangled = Self::mangle_field_name(&f.name); self.line(&format!("{} {};", ty_c, mangled)); // Plan 14 Ф.4: fn-typed field sig registry mirror. if let TypeRef::Func { params: fp, return_type, .. } = &f.ty { let ptys: Vec<String> = fp.iter() .map(|t| self.type_ref_to_c(t).map_err(|e| self.err_no_int_fallback( &format!("value-record `{}` fn-typed field `{}` param", name, f.name), &e, ))) .collect::<Result<Vec<_>, _>>()?; let rty = match return_type.as_ref() { Some(rt) => self.type_ref_to_c(rt).map_err(|e| self.err_no_int_fallback( &format!("value-record `{}` fn-typed field `{}` return", name, f.name), &e, ))?, None => "nova_unit".to_string(), }; self.record_field_fn_sigs.insert( (name.to_string(), f.name.clone()), (ptys, rty), ); } } self.indent -= 1; self.line("};"); self.line(""); // Plan 172.14 Ф.1: запомнить упорядоченные C-типы полей — точный // источник C-размера структуры для auto-by-ref классификации. // Пустой record эмитит char-маркер (size 1) — фиксируем его же. self.value_struct_field_tys.insert( format!("NovaValue_{}", name), if fields.is_empty() { vec!["nova_byte".to_string()] } else { field_c_tys.clone() }, ); let struct_def = std::mem::replace(&mut self.out, saved_out); // Restore the NovaOpt typedefs this record's by-value fields // registered (delta since `opt_snap`) into THIS node's own text; // truncate the shared buffer back so the later // `/*__NOVAOPT_TYPEDEFS__*/` splice does not re-emit them. // Registration order (innermost-first) is preserved, so nested // `NovaOpt_NovaOpt_…` stays topologically valid. let mut opt_delta = { let mut buf = self.novaopt_typedefs_buf.borrow_mut(); let d = buf[opt_snap..].to_string(); buf.truncate(opt_snap); d }; // Plan 172.12 A8 (приёмка, ExtDto-class): the delta only covers Opts // registered FIRST during THIS record's field lowering. An Opt that // was PRE-registered by an earlier consumer (e.g. a protocol // vtable slot lowering the same `Option[[]str]`) is seen-dedup'd → // its FULL typedef stays at the later `/*__NOVAOPT_TYPEDEFS__*/` // splice, AFTER this struct → "unknown type" on the by-value // field. MOVE such a block (typedef line + its `nova_opt_eq_*` fn) // from the shared buffer into this node's own text — same // reasoning the fresh-registration case already gets, just via an // explicit lookup instead of relying on delta-since-snapshot. for fty in &field_c_tys { if !fty.starts_with("NovaOpt_") || fty.ends_with('*') { continue; } let typedef_needle = format!("typedef struct {} {{", fty); if opt_delta.contains(&typedef_needle) { continue; // already moved into this node's own text } let mut buf = self.novaopt_typedefs_buf.borrow_mut(); let Some(td_start) = buf.find(&typedef_needle) else { continue; // runtime-predeclared (array.h) or not registered here }; let td_end = match buf[td_start..].find('\n') { Some(rel) => td_start + rel + 1, None => buf.len(), }; let typedef_line = buf[td_start..td_end].to_string(); buf.replace_range(td_start..td_end, ""); // The eq fn block: from its header line through the first `\n}\n` // (all register_novaopt_decl eq bodies are flat — a single return / // tag-checks, no nested braces at column 0). ONLY move a // DEFINITION (header line ending `{`): for some Opts the buffer // holds a mere PROTOTYPE (`…);` — the definition lives in the late // [M-172.1-option-eq-record-structural] channel); cutting from a // prototype through the next `\n}\n` would swallow innocent // neighbouring blocks. A prototype may safely stay at the late // splice — eq call-sites live in fn bodies emitted after it. let sani = &fty["NovaOpt_".len()..]; // Plan 209 Ф.1: needle must track the actual emitted storage-class // prefix — `top_level_storage_inline()` drops "static inline " // under multi-TU (nova_opt_eq_* bodies get promoted to a single // external part; see recon-notes.md §4), so a hardcoded needle // would silently stop matching and this hoist would no-op. let eq_needle = format!("{}nova_bool nova_opt_eq_{}(", self.top_level_storage_inline(), sani); let eq_block = if let Some(eq_start) = buf.find(&eq_needle) { let header_end = buf[eq_start..].find('\n') .map(|r| eq_start + r) .unwrap_or(buf.len()); let is_definition = buf[eq_start..header_end].trim_end().ends_with('{'); if is_definition { let close_rel = buf[eq_start..].find("\n}\n") .map(|r| eq_start + r + "\n}\n".len()); match close_rel { Some(eq_end) => { let b = buf[eq_start..eq_end].to_string(); buf.replace_range(eq_start..eq_end, ""); b } None => String::new(), } } else { String::new() } } else { String::new() }; drop(buf); // Forward-declare the payload's mono struct name (pointer payload // `Nova_X____…*` inside the moved typedef) ahead of everything. if let Some(open) = typedef_line.find('{') { if let Some(val_pos) = typedef_line.find(" value;") { let payload = typedef_line[open + 1..val_pos].trim(); if let Some(pointee) = payload.strip_suffix('*') { let pointee = pointee.trim(); if pointee.starts_with("Nova") && pointee.contains("____") { let fwd = format!("typedef struct {p} {p};\n", p = pointee); if !fwd_decls.contains(&fwd) { fwd_decls.push_str(&fwd); } } } } } opt_delta.push_str(&typedef_line); opt_delta.push_str(&eq_block); } // [Round 3] Capture (name, field-C-types, rendered text) instead of // appending directly — `render_unified_value_types` (called once, // at true finalize) topologically sorts EVERY value-type node // (this one included) and renders them in dependency order. See the // fn-level doc above for why the round-1/2 hoists are gone (the // NovaOpt `opt_delta` hoist just above is NOT one of them — it's // orthogonal, still needed, see its own doc). let mut combined = String::new(); combined.push_str(&fwd_decls); combined.push_str(&opt_delta); combined.push_str(&struct_def); self.pending_value_nodes.push(( format!("NovaValue_{}", name), field_c_tys.clone(), combined, )); self.record_schemas.insert(name.to_string(), schema); // Value-type alias: Named{Vec3} → "NovaValue_Vec3" (no pointer). self.type_aliases.insert(name.to_string(), format!("NovaValue_{}", name)); // Mark as value-record for emit_record_lit detection. self.value_record_names.insert(name.to_string()); Ok(()) } fn emit_record_type(&mut self, name: &str, fields: &[RecordField]) -> Result<(), String> { let mut schema = HashMap::new(); // [M-option-self-recursive-record-mono]: mark this record concrete WHILE // its OWN fields are lowered — mirrors `being_defined_sum_types` in // `emit_sum_type` — so a self-referential field (`next Option[Self]`) is // recognised as the concrete `Nova_<Name>*` heap type instead of an // unresolved generic-param stub (`rt_named_is_stub` consults this). // MUST be set before ANY `type_ref_to_c` call on this record's own // fields — including the forward-decl pre-pass right below, which // calls it too (a self-referential `Option[Self]` field's structural- // eq registration side effect must see this guard on its FIRST call, // or the deferral fix in `register_novaopt_decl` never engages). self.being_defined_record_types.insert(name.to_string()); // [реестр 221.1 №139 Round 3 — NOT unified, deliberately] Unlike // `emit_value_record_type`/Record-kind generic-instances, a HEAP // record's text stays a DIRECT `self.out` write at the CURRENT // type-decl-loop position (unchanged from before rounds 1/2 ever // touched this file). It doesn't need to join the unified topo-sort // (`render_unified_value_types`, spliced at the EARLY // `__VALUE_RECORD_DEFS__` marker): a heap record's direct `self.out` // position is ALREADY, structurally, AFTER the ENTIRE `emit_preamble` // marker skeleton (every early marker's TEXT is written during the // `emit_preamble()` call, which returns before the type-decl loop — // the loop that calls this fn — even starts), so it is already safe // to depend on ANYTHING the unified section (or NovaOpt/NovaRes/ // tuple/fixarr) provides — no repositioning ever required. This was // tried in an earlier pass of this same window (capturing heap // records into `pending_value_nodes` too, moving them EARLY to // position 6) and caused a NEW regression: a heap record's OWN // ordinary `Option[T]`/`Result[T,E]` field (e.g. `Nova_FmtCtx.align // Option[Align]` in std prelude) relies on the NORMAL, non-late // NovaOpt/NovaRes splice already being available BEFORE it — which // is true at heap records' ORIGINAL late position, but was made // FALSE by force-moving them to the early unified section. Nothing // in the unified section ever needs a heap record complete (heap // types are NEVER embedded by value anywhere, only via `Nova_X*`), // so leaving heap records at their original, later position loses // no coverage. // [M-toml-sum-variant-mono-field-hoist] (Plan 186, recursive-mono): // mirror of the SAME pre-pass in `emit_sum_type` (see its doc) — a // plain record field whose C type is a pointer to a MONO'D GENERIC // instance (e.g. `TomlParser { mut root HashMap[str, TomlValue] }`) // needs a forward `typedef struct X X;` BEFORE this record's own // `struct Nova_{name} {` opens. A C typedef statement cannot appear // inside another struct's braces, so this must run as its own pass // before the struct is opened below. { let mut fwd_seen: std::collections::HashSet<String> = std::collections::HashSet::new(); for f in fields { if let Ok(tc) = self.type_ref_to_c(&f.ty) { if let Some(base) = tc.strip_suffix('*') { let base = base.trim_end_matches('*').trim(); // [M-atomicint-record-field-typedef-collision] fix // (2026-07-11): a runtime-backed sync-primitive field // (`Mutex`/`Atomic*`/…, D415 §2) already has its real // typedef hand-written + `#include`d from // `nova_rt/sync_*.h` (a DIFFERENT underlying shape — // often an anonymous-struct typedef, not `struct // Nova_<X>`). Forward-declaring `typedef struct // Nova_<X> Nova_<X>;` here collides with it // ("typedef redefinition with different types"). // Skip the pre-pass entirely for these names — the // runtime header's complete typedef is already // visible by the time this record's fields reference // it (no forward-decl needed at all). if base.starts_with("Nova_") && !Self::debt_is_runtime_backed_newtype( base.trim_start_matches("Nova_"), ) && fwd_seen.insert(base.to_string()) { self.line(&format!("typedef struct {0} {0};", base)); } } } } } self.line(&format!("typedef struct Nova_{0} Nova_{0};", name)); self.line(&format!("struct Nova_{} {{", name)); self.indent += 1; // Plan 82 followup: пустой record (`type Marker { }`) — C // стандарт требует хотя бы одного именованного члена; GCC/Clang // допускают пустой struct как extension, MSVC отвергает (C2016). // Эмитим dummy-field для нулевых fields — паритет с empty-sum // (`char _dummy;` в union, см. emit_sum_type). if fields.is_empty() { self.line("char _empty_record_marker;"); } for f in fields { let ty_c = self.type_ref_to_c(&f.ty)?; schema.insert(f.name.clone(), ty_c.clone()); // Mangle если коллизия с C reserved keyword. let mangled = Self::mangle_field_name(&f.name); self.line(&format!("{} {};", ty_c, mangled)); // Plan 14 Ф.4: записываем fn-typed поля в реестр sig'ов // record-полей. Использует Member-call routing для эмита // closure-call (`obj.f(x)` → NOVA_CLOS_CALL_*). if let TypeRef::Func { params: fp, return_type, .. } = &f.ty { // Plan 70 PhaseA2.3: strict — record fn-typed field sig. let ptys: Vec<String> = fp.iter() .map(|t| self.type_ref_to_c(t).map_err(|e| self.err_no_int_fallback( &format!("record `{}` fn-typed field `{}` param", name, f.name), &e, ))) .collect::<Result<Vec<_>, _>>()?; let rty = match return_type.as_ref() { Some(rt) => self.type_ref_to_c(rt).map_err(|e| self.err_no_int_fallback( &format!("record `{}` fn-typed field `{}` return", name, f.name), &e, ))?, None => "nova_unit".to_string(), }; self.record_field_fn_sigs.insert( (name.to_string(), f.name.clone()), (ptys, rty), ); } } // [M-option-self-recursive-record-mono]: fields lowered — the type is // about to be registered for real; drop the mid-emission concreteness // guard (mirrors `emit_sum_type`'s `being_defined_sum_types.remove`). self.being_defined_record_types.remove(name); // Plan 173 Ф.5 (#8, D188 R2): hidden runtime exactly-once counter for // heap records with a USER `consume @cleanup` method. Zero-initialized // by the nova_alloc contract (alloc.c: "MUST return zeroed memory"); // checked+incremented by the `Nova_<T>_consume_cleanup` prologue. // Trailing field — designated initializers leave it 0; not part of // the Nova-visible record schema. if self.consume_cleanup_types.contains(name) { self.line("int _consume_ccount; /* Plan 173 Ф.5 D188 R2 exactly-once */"); self.consume_ccount_structs.insert(name.to_string()); } self.indent -= 1; self.line("};"); self.line(""); self.record_schemas.insert(name.to_string(), schema); // [M-172.1-option-eq-record-structural]: stable field order for the // structural eq recursion (record_schemas is an unordered HashMap). self.record_field_order.insert( name.to_string(), fields.iter().map(|f| f.name.clone()).collect(), ); Ok(()) } /// Plan 120 (D215): emit a named-tuple type as a value-type C struct. /// `type Point(x f64, y f64)` → `typedef struct NovaTuple_Point { double x; double y; } NovaTuple_Point;` /// Registered in `type_aliases` so `type_ref_to_c(Named{Point})` returns `NovaTuple_Point` (no pointer). /// /// [реестр 221.1 №139 Round 4] Captured into `pending_value_nodes` — see /// `render_unified_value_types` doc. Round 2 audited this category and /// found it safe UN-touched ("ordinary `Item::Type` declaration in the /// type-decl loop, not a deferred category") — TRUE at the time /// (`__VALUE_RECORD_DEFS__` sat at its OLD early position and named /// tuples render directly, in-loop, at essentially the SAME early /// position — no ordering gap existed between the two). Round 3 then /// moved the unified section to become the FIRST thing in the type-decl /// loop's output — meaning anything captured into it now renders /// BEFORE named tuples, which still render at their OLD in-loop /// position. Integrator's mega-CU caught the resulting gap directly /// (`a_q3_println_debug_record`: a value node needing `NovaTuple_ /// D414pfPair`/`NovaTuple_Vec36` by value, now positioned ahead of /// them). Folding named tuples into the SAME graph — instead of /// special-casing their position again — is what actually closes this: /// whichever of {named tuple, value-record, generic-instance, mono /// tuple/fixarr} needs another BY VALUE, Kahn's sort now sees it /// regardless of which category either side belongs to. fn emit_named_tuple_type(&mut self, name: &str, fields: &[NamedTupleField]) -> Result<(), String> { // [реестр 221.1 №139 Round 4] `opt_snap` mirrors // `emit_value_record_type`'s per-node NovaOpt cut-and-move — a // named tuple's OWN `Option[T]`/`Result[T,E]` field needs that // specific wrapper struct available before THIS node too, now that // named tuples share the early unified position. (A global // "NovaOpt/NovaRes as graph nodes" version was tried and reverted // this same round — see the note on the `opt_snap` declaration in // `drain_generic_type_worklist` for why.) let opt_snap = self.novaopt_typedefs_buf.borrow().len(); let mut fwd_decls = String::new(); let mut nt_field_c_tys: Vec<String> = Vec::new(); let saved_out = std::mem::take(&mut self.out); self.line(&format!("typedef struct NovaTuple_{0} NovaTuple_{0};", name)); self.line(&format!("struct NovaTuple_{} {{", name)); self.indent += 1; let mut schema = HashMap::new(); for f in fields { let ty_c = self.type_ref_to_c(&f.ty)?; // By-pointer field to a late-emitted struct → forward typedef // into THIS node's own text (mirrors `emit_value_record_type`'s // `fwd_decls` — named tuples never had this before because they // always rendered in-place, ahead of anything the unified graph // now might sort them against). if let Some(pointee) = ty_c.strip_suffix('*') { let pointee = pointee.trim(); if self.debt_is_guaranteed_struct_tag(pointee) { let fwd = format!("typedef struct {p} {p};\n", p = pointee); if !fwd_decls.contains(&fwd) { fwd_decls.push_str(&fwd); } } } schema.insert(f.name.clone(), ty_c.clone()); nt_field_c_tys.push(ty_c.clone()); let mangled = Self::mangle_field_name(&f.name); self.line(&format!("{} {};", ty_c, mangled)); } self.indent -= 1; self.line("};"); self.line(""); let struct_def = std::mem::replace(&mut self.out, saved_out); // Same NovaOpt cut-and-move as `emit_value_record_type` — a named // tuple's OWN `Option[T]`/`Result[T,E]` field needs that specific // wrapper struct available before THIS node too, now that named // tuples share the early unified position. let mut opt_delta = { let mut buf = self.novaopt_typedefs_buf.borrow_mut(); let d = buf[opt_snap..].to_string(); buf.truncate(opt_snap); d }; for fty in &nt_field_c_tys { if !fty.starts_with("NovaOpt_") || fty.ends_with('*') { continue; } let typedef_needle = format!("typedef struct {} {{", fty); if opt_delta.contains(&typedef_needle) { continue; } let mut buf = self.novaopt_typedefs_buf.borrow_mut(); let Some(td_start) = buf.find(&typedef_needle) else { continue; }; let td_end = match buf[td_start..].find('\n') { Some(rel) => td_start + rel + 1, None => buf.len(), }; let typedef_line = buf[td_start..td_end].to_string(); buf.replace_range(td_start..td_end, ""); let sani = &fty["NovaOpt_".len()..]; let eq_needle = format!("{}nova_bool nova_opt_eq_{}(", self.top_level_storage_inline(), sani); let eq_block = if let Some(eq_start) = buf.find(&eq_needle) { let header_end = buf[eq_start..].find('\n') .map(|r| eq_start + r) .unwrap_or(buf.len()); let is_definition = buf[eq_start..header_end].trim_end().ends_with('{'); if is_definition { let close_rel = buf[eq_start..].find("\n}\n") .map(|r| eq_start + r + "\n}\n".len()); match close_rel { Some(eq_end) => { let b = buf[eq_start..eq_end].to_string(); buf.replace_range(eq_start..eq_end, ""); b } None => String::new(), } } else { String::new() } } else { String::new() }; drop(buf); opt_delta.push_str(&typedef_line); opt_delta.push_str(&eq_block); } let mut combined = String::new(); combined.push_str(&fwd_decls); combined.push_str(&opt_delta); combined.push_str(&struct_def); self.pending_value_nodes.push(( format!("NovaTuple_{}", name), nt_field_c_tys.clone(), combined, )); self.record_schemas.insert(name.to_string(), schema); // Plan 172.14 Ф.1: упорядоченные C-типы полей для C-размера (см. // симметричный захват в emit_value_record_type). self.value_struct_field_tys .insert(format!("NovaTuple_{}", name), nt_field_c_tys); // D215 amend: register field defaults for constructor call emission. let field_defaults: Vec<(String, Option<crate::ast::Expr>)> = fields.iter() .map(|f| (f.name.clone(), f.default.clone())) .collect(); self.named_tuple_field_defaults.insert(name.to_string(), field_defaults); // Value type: Named{Point} → "NovaTuple_Point" (no pointer, like type alias). self.type_aliases.insert(name.to_string(), format!("NovaTuple_{}", name)); Ok(()) } fn emit_sum_type(&mut self, name: &str, variants: &[SumVariant]) -> Result<(), String> { // Plan 72 P1-B: empty sum type (0 variants) — bottom / uninhabited type. // `type RuntimeNoneError` etc. (empty sum). C does not support empty // enums, so emit as `typedef int64_t Nova_X;` (ABI placeholder). // No constructors emitted — the type is uninhabited by definition. if variants.is_empty() { self.line(&format!("typedef int64_t Nova_{};", name)); self.line(""); // Register empty schema so type-ref lookups don't fall to void*. self.sum_schemas.insert(name.to_string(), HashMap::new()); return Ok(()); } // Tag enum self.line("typedef enum {"); self.indent += 1; for v in variants { // №296: thread explicit discriminant (`= N`) into the C tag // enum; C auto-increments from the previous listed value for // entries with no `= N`, matching spec 02-types.md rule 2. match v.discriminant { Some(d) => self.line(&format!("NOVA_TAG_{}_{} = {},", name, v.name, d)), None => self.line(&format!("NOVA_TAG_{}_{},", name, v.name)), } } self.indent -= 1; self.line(&format!("}} Nova_{}_Tag;", name)); // Collect schema while building union let mut sum_schema: HashMap<String, Vec<String>> = HashMap::new(); // [M-172.1-self-ref-slice-variant-erasure]: mark this type concrete WHILE its // variant payload fields are lowered, so a self-referential `[]Self` slice // element (`type T | Node([]T)`) resolves to `Nova_Vec____Nova_T_p*` instead // of the erased `Nova_Vec____nova_int*` (`debt_is_generic_stub_c` consults // this). MUST be set before ANY `type_ref_to_c` call on this sum's own // variant fields — including the forward-decl pre-pass right below. self.being_defined_sum_types.insert(name.to_string()); // [M-toml-sum-variant-mono-field-hoist] (Plan 186, recursive-mono): // pre-emit forward `typedef struct X X;` decls for any Tuple/Record // variant payload field whose C type is a pointer to a MONO'D GENERIC // instance (e.g. `TomlTable(HashMap[str, TomlValue])` — the field's // `Nova_HashMap____nova_str__Nova_TomlValue_p*` typedef is only // emitted later, when `drain_generic_type_worklist` runs — long // after this plain (non-generic) sum type's OWN struct body, which // references it as a pointer field right here). A pointer field only // needs the type NAME declared, not the full struct body, so a // forward-decl emitted BEFORE this struct opens is sufficient — but // it must be emitted before `struct Nova_{name} {` starts (a C // typedef statement cannot appear inside another struct's braces), // hence this dedicated pre-pass over ALL variants (mirrors the // analogous pre-pass `emit_generic_type_instance`'s Record arm // already does for its OWN fields, one level up the generic-instance // stack — this closes the SAME gap for a plain sum's fields). { let mut fwd_seen: std::collections::HashSet<String> = std::collections::HashSet::new(); for v in variants { let field_tys: Vec<&crate::ast::TypeRef> = match &v.kind { SumVariantKind::Unit => Vec::new(), SumVariantKind::Tuple(types) => types.iter().collect(), SumVariantKind::Record(fields) => fields.iter().map(|f| &f.ty).collect(), }; for ty in field_tys { if let Ok(tc) = self.type_ref_to_c(ty) { if let Some(base) = tc.strip_suffix('*') { let base = base.trim_end_matches('*').trim(); // [M-atomicint-record-field-typedef-collision] // fix (2026-07-11) — same guard as // `emit_record_type`'s pre-pass: runtime-backed // sync primitives already have a complete // `#include`d typedef, forward-declaring here // collides with it. if base.starts_with("Nova_") && !Self::debt_is_runtime_backed_newtype( base.trim_start_matches("Nova_"), ) && fwd_seen.insert(base.to_string()) { self.line(&format!("typedef struct {0} {0};", base)); } } } } } } // Union payload self.line(&format!("typedef struct Nova_{0} Nova_{0};", name)); self.line(&format!("struct Nova_{} {{", name)); self.indent += 1; self.line(&format!("Nova_{}_Tag tag;", name)); self.line("union {"); self.indent += 1; // Check if any variant has payload — MSVC requires at least one member let has_payload = variants.iter().any(|v| !matches!(v.kind, SumVariantKind::Unit)); if !has_payload { self.line("char _dummy;"); } for v in variants { match &v.kind { SumVariantKind::Unit => { sum_schema.insert(v.name.clone(), vec![]); } SumVariantKind::Tuple(types) => { let mut field_types = Vec::new(); self.line("struct {"); self.indent += 1; for (i, ty) in types.iter().enumerate() { let tc = self.type_ref_to_c(ty)?; field_types.push(tc.clone()); self.line(&format!("{} _{};", tc, i)); } self.indent -= 1; self.line(&format!("}} {};", v.name)); sum_schema.insert(v.name.clone(), field_types); } SumVariantKind::Record(fields) => { let mut field_types = Vec::new(); let mut field_names_ordered: Vec<String> = Vec::new(); self.line("struct {"); self.indent += 1; for f in fields { let tc = self.type_ref_to_c(&f.ty)?; field_types.push(tc.clone()); field_names_ordered.push(f.name.clone()); // Mangle если коллизия с C-keyword. let mfn = Self::mangle_field_name(&f.name); self.line(&format!("{} {};", tc, mfn)); let key = format!("{}::{}::{}", name, v.name, f.name); self.record_variant_field_types.insert(key, tc); } let order_key = format!("{}::{}", name, v.name); self.record_variant_field_order.insert(order_key, field_names_ordered); self.indent -= 1; self.line(&format!("}} {};", v.name)); sum_schema.insert(v.name.clone(), field_types); } } } self.indent -= 1; self.line("} payload;"); self.indent -= 1; self.line("};"); self.line(""); // [M-172.1-self-ref-slice-variant-erasure]: payload fields lowered — the type // is about to be registered; drop the mid-emission concreteness guard. self.being_defined_sum_types.remove(name); // Constructor functions: Nova_Shape* nova_make_Shape_Circle(nova_f64 _0) { ... } for v in variants { let field_types = sum_schema.get(&v.name).cloned().unwrap_or_default(); let params: String = field_types.iter().enumerate() .map(|(i, t)| format!("{} _{}", t, i)) .collect::<Vec<_>>() .join(", "); let params_str = if params.is_empty() { "void".to_string() } else { params }; self.line(&format!( "{storage}Nova_{name}* nova_make_{name}_{var}({params}) {{", storage = self.top_level_storage(), name = name, var = v.name, params = params_str )); self.indent += 1; self.line(&format!( "Nova_{name}* _r = (Nova_{name}*)nova_alloc(sizeof(Nova_{name}));", name = name )); self.line(&format!("_r->tag = NOVA_TAG_{name}_{var};", name = name, var = v.name)); match &v.kind { SumVariantKind::Unit => {} SumVariantKind::Tuple(_) => { for (i, _) in field_types.iter().enumerate() { self.line(&format!("_r->payload.{var}._{i} = _{i};", var = v.name, i = i)); } } SumVariantKind::Record(fields) => { // Named fields — assign by field name, not positional index. // Mangle field if collides with C-keyword. for (i, f) in fields.iter().enumerate() { let mfn = Self::mangle_field_name(&f.name); self.line(&format!("_r->payload.{var}.{fname} = _{i};", var = v.name, fname = mfn, i = i)); } } } self.line("return _r;"); self.indent -= 1; self.line("}"); self.line(""); } // Plan 62.A.bis Ф.2.1: mirror legacy insert в registry. // emit_sum_type — heap-pointer ABI (PointerErrorLike). let variant_order: Vec<String> = variants.iter() .map(|v| v.name.clone()).collect(); let c_name = format!("Nova_{}", name); self.sum_schema_registry.register_user_sum( name, &sum_schema, &c_name, super::sum_schema_registry::SumAbi::PointerErrorLike, &variant_order, ); self.sum_schemas.insert(name.to_string(), sum_schema); Ok(()) } /// Если field-name коллизирует с C reserved-keyword'ом — добавим /// префикс `nv_`. Применяется ко **всем** field-emission точкам: /// struct decl, record-literal, member-access, pattern match. /// Plan 96 Ф.1 — bounds-checked array element access. /// /// Wraps `(obj)->data[idx]` в GNU statement-expression с runtime /// bounds-check: при `idx < 0 || idx >= len` вызывается /// `nv_panic_index_oob` (defined в array.h). Сохраняет lvalue-семантику — /// результат может быть как rvalue, так и target присваивания. /// /// До Plan 96 codegen эмитил `(obj)->data[idx]` без проверки — /// controlled buffer overflow на запись, UB на чтение (D27 §1632 drift). /// /// `obj_expr` и `idx_expr` — уже emit'нутые подвыражения; `storage_elem_ty` /// — C-тип ФИЗИЧЕСКОГО элемента буфера (`a->data[i]`), напр. `nova_int` для /// erased-хранилища (self-field, array-of-arrays) или конкретный тип для /// типизированного NovaArray. Вызывающий при необходимости кастует результат /// к «реальному» типу (см. call-sites: `(({real})({bchk}))`). /// /// Plan 145 — portable форма (MSVC C2059 fix). Раньше использовался GNU /// statement-expression + `__typeof__` (cl.exe не поддерживает). Теперь — /// `*(T*)nova_idx_chk((void*)(arr), (i), sizeof(T))` (хелпер в array.h): /// `void*`-параметр лаундерит тип (strict-aliasing safe), bounds-check + /// single-eval обоих подвыражений внутри хелпера, а `*(T*)…` — валидный /// lvalue в любом компиляторе (для `arr[i] = v` / `&arr[i]` / `arr[i].m()`). fn emit_bchk_array_access( storage_elem_ty: &str, obj_expr: &str, idx_expr: &str, ) -> String { format!( "(*({elem}*)nova_idx_chk((void*)({o}), ({i}), sizeof({elem})))", elem = storage_elem_ty, o = obj_expr, i = idx_expr, ) } /// Plan 96 Ф.1 — bounds-checked array access for arr[i][j] double-indexing. /// Bounds-check on outer (idx_outer) AND inner (idx_inner). Inner array /// is reached via cast `(inner_arr_ty)(outer->data[idx_outer])` — /// element-type erasure (nested arrays stored as nova_int). /// /// Plan 145 — portable форма (MSVC C2059 fix): два вложенных /// `*(T*)nova_idx_chk(...)` вместо GNU statement-expression. Внешний буфер /// хранит inner-array-указатели как erased `nova_int`; читаем его как /// `nova_int`, кастуем к `inner_arr_ty`, затем индексируем внутренний по /// его storage-типу (`strip(inner_arr_ty)`, обычно `nova_int`). Каждое /// подвыражение вычисляется один раз; результат — lvalue (`*(T*)…`). fn emit_bchk_double_array_access( inner_arr_ty: &str, outer_expr: &str, outer_idx_expr: &str, inner_idx_expr: &str, ) -> String { // Inner element storage type: physical type held in inner->data[]. // For erased nested arrays this is nova_int. let inner_storage = Self::debt_novaarray_elem_storage(inner_arr_ty); // Outer buffer stores inner-array pointers erased as nova_int. let outer_access = format!( "(*(nova_int*)nova_idx_chk((void*)({oe}), ({oi}), sizeof(nova_int)))", oe = outer_expr, oi = outer_idx_expr, ); let inner_arr = format!("(({ity})({oa}))", ity = inner_arr_ty, oa = outer_access); format!( "(*({is}*)nova_idx_chk((void*)({ia}), ({ii}), sizeof({is})))", is = inner_storage, ia = inner_arr, ii = inner_idx_expr, ) } /// Иначе генерируется invalid C (`nova_int char;`). /// /// `n` это C-keyword? Список — стандартный C99/C11 + popular extensions. /// /// [M-c-keyword-ident-collision] (Plan 172.13): та же функция используется /// НЕ только для полей (`mangle_field_name`'s исходное имя — historical), /// но и как единый канал маскирования ЛЮБОГО user-identifier'а Nova, /// который становится C-токеном (local var decl/read/write, fn-параметр, /// match-arm binding). Nova сам не резервирует эти слова — пользователь /// не должен знать о зарезервированных словах ЦЕЛЕВОГО языка кодогена. fn mangle_field_name(name: &str) -> String { if Self::is_c_keyword(name) { format!("nv_{}", name) } else { name.to_string() } } fn is_c_keyword(name: &str) -> bool { matches!(name, "auto" | "break" | "case" | "char" | "const" | "continue" | "default" | "do" | "double" | "else" | "enum" | "extern" | "float" | "for" | "goto" | "if" | "inline" | "int" | "long" | "register" | "restrict" | "return" | "short" | "signed" | "sizeof" | "static" | "struct" | "switch" | "typedef" | "union" | "unsigned" | "void" | "volatile" | "while" | "_Bool" | "_Atomic" | "_Complex" | "_Imaginary" | "_Generic" | "_Thread_local" | "_Static_assert" | "_Noreturn" | "_Alignas" | "_Alignof" | "asm" | "fortran" ) } // ---- function emission ---- /// Plan 135 Ф.2: detect whether a receiver expression is mutable at the /// call-site, so we can tiebreak `__mut` vs `__ro` overloads. /// /// - `Ident(name)` → check `var_mutable` set (covers `let mut x`). /// - `SelfAccess` → use `current_receiver_is_mut` (set when entering the /// enclosing method body). /// - anything else → `false` (conservative / no tiebreak). fn is_obj_mutable(&self, obj: &Expr) -> bool { match &obj.kind { ExprKind::Ident(name) => self.var_mutable.contains(name.as_str()), ExprKind::SelfAccess => self.current_receiver_is_mut, _ => false, } } /// Plan 184 (Р14): is `e` a mutable, addressable PLACE — a binding declared /// `mut`, a field path / index whose root is such a place, or the `mut @` /// receiver? Used to decide whether a `mut`-mode overload is preferred for /// the argument (the argument-binding-mutability dispatch rule, mirror of the /// receiver-mutability tiebreak of Plan 135). fn is_place_mutable(&self, e: &Expr) -> bool { match &e.kind { ExprKind::Ident(name) => self.var_mutable.contains(name.as_str()), ExprKind::SelfAccess => self.current_receiver_is_mut, ExprKind::Member { obj, .. } => self.is_place_mutable(obj), ExprKind::Index { obj, .. } => self.is_place_mutable(obj), _ => false, } } /// Plan 184 (Р14): is `e` an owned rvalue/temporary with no addressable /// backing store — a call result, literal, arithmetic, record/collection /// literal, etc.? Such an argument is unconditionally last-use, so a /// `consume`-mode overload participates for it (no live binding is silently /// consumed). Named places (Ident/Member/Index/`@`) are NOT temporaries. fn is_rvalue_temp(e: &Expr) -> bool { !matches!( &e.kind, ExprKind::Ident(_) | ExprKind::SelfAccess | ExprKind::Member { .. } | ExprKind::Index { .. } ) } /// Plan 184 (Р13/Р14): among overload candidates already matched by param /// C-types (or arity), narrow by the parameter MODE axis {ro,mut,consume} /// against each argument's binding capability. Selection rule (D84 amendment): /// - `ro` param — accepts any argument (specificity 0); /// - `mut` param — eligible only when the argument is a mutable place /// (specificity 2, preferred over `ro` — mirror of the receiver-mut rule); /// - `consume` param — eligible only when the argument is an owned rvalue / /// last-use temporary (specificity 3, most specific). /// The most-specific eligible candidate wins. `mut` and `consume` eligibility /// are mutually exclusive by argument class (a place is not a temporary), so a /// unique winner exists by construction (the axes are orthogonal — no /// ambiguity). Returns the input unchanged when the candidates do NOT form a /// mode-overload set (identical modes) or when none is eligible (fall through /// to the pre-184 selection). fn narrow_by_param_mode(&self, pool: Vec<MethodSig>, args: &[CallArg]) -> Vec<MethodSig> { if pool.len() < 2 { return pool; } // Must be a real mode-overload set: all candidates carry a param_modes // vector of the call's arity, and at least two modes differ. let arity = args.len(); if pool.iter().any(|s| s.param_modes.len() != arity) { return pool; } let first = &pool[0].param_modes; if pool.iter().all(|s| &s.param_modes == first) { return pool; } let mut best = i32::MIN; let mut scored: Vec<(i32, MethodSig)> = Vec::new(); 'cand: for s in pool.iter() { let mut score = 0i32; for (i, &m) in s.param_modes.iter().enumerate() { let Some(arg) = args.get(i).map(|a| a.expr()) else { continue; }; match m { 1 => { if self.is_place_mutable(arg) { score += 2; } else { continue 'cand; } } 2 => { if Self::is_rvalue_temp(arg) { score += 3; } else { continue 'cand; } } _ => {} } } if score > best { best = score; } scored.push((score, s.clone())); } let winners: Vec<MethodSig> = scored.into_iter() .filter(|(sc, _)| *sc == best) .map(|(_, s)| s) .collect(); if winners.is_empty() { pool } else { winners } } /// Plan 138.4 Ф.2 (G-A): compute the param C-type vector used to match a /// method `FnDecl` against its registered overload signatures, replicating /// the registration pre-pass (~2507-2539). For generic-receiver types /// (`generic_types` membership) and `[]T` array-extension receivers, params /// are erased via `erased_type_ref_c` (bare type-param → `void*`) exactly as /// stored in `method_overloads`; for concrete receivers it is the strict /// `type_ref_to_c` translation. Keeping these two computations in lockstep is /// what lets multiple same-name overloads on a generic type resolve to their /// distinct mangled C symbols. fn mangle_want_params(&self, recv: &crate::ast::Receiver, f: &FnDecl) -> Vec<String> { let is_array_ext = recv.type_name.starts_with("[]"); let is_generic_recv = self.generic_types.contains(&recv.type_name) || is_array_ext; if is_generic_recv { let recv_type_params: HashSet<String> = { let from_recv = recv.generics.iter().filter_map(|tr| { if let TypeRef::Named { path, .. } = tr { path.first().cloned() } else { None } }); let from_fn = if is_array_ext { f.generics.iter().map(|g| g.name.clone()).collect::<Vec<_>>() } else { Vec::new() }; from_recv.chain(from_fn.into_iter()).collect() }; f.params.iter() .map(|p| self.erased_type_ref_c(&Some(p.ty.clone()), &recv_type_params)) .collect() } else { f.params.iter() .map(|p| self.type_ref_to_c(&p.ty).unwrap_or_else(|_| "nova_int".into())) .collect() } } fn mangle_fn(&self, f: &FnDecl) -> String { if let Some(recv) = &f.receiver { // Plan 11 Ф.3: если есть multi-overload registry для (type, name), // ищем по сигнатуре и берём её c_name (mangled). Иначе — старый mangling. let key = (recv.type_name.clone(), f.name.clone()); if let Some(overloads) = self.method_overloads.get(&key) { if overloads.len() > 1 { // Резолвим по param C-типам этого FnDecl'а. // // Plan 138.4 Ф.2 (G-A): for generic-receiver types (Vec[T], // HashMap[K,V], `[]T` array-ext) the overload registry stored // `param_c_types` via `erased_type_ref_c` (type-param → `void*`), // see the pre-pass at ~2530. `type_ref_to_c` would lower a bare // type-param `T` to the `nova_int` fallback instead, so a write // overload `mut @index(i int, val T)` whose registered sig is // `[nova_int, void*]` failed to match `[nova_int, nova_int]` and // fell through to the un-suffixed base name — colliding in C with // the read `@index(i int) -> T`. Mirror the registration erasure // here so each full signature resolves to its distinct C symbol. let want_params = self.mangle_want_params(recv, f); // Plan 135 Ф.1: tiebreak по recv_mutable когда params совпадают. let want_recv_mut = recv.mutable; // Plan 184 (Р13/Р14): tiebreak by parameter modes too, so a // mode-overload (`fn T @m(x H)` vs `fn T mut @m(mut x H)`) emits // its OWN body under its OWN mangled symbol. let want_modes = Self::fn_param_modes(f); // Pass 0: exact match on params + recv_mutable + param_modes. for sig in overloads.iter() { if sig.param_c_types == want_params && sig.recv_mutable == want_recv_mut && sig.param_modes == want_modes { return sig.c_name.clone(); } } // First pass: exact match on both params + recv_mutable. for sig in overloads.iter() { if sig.param_c_types == want_params && sig.recv_mutable == want_recv_mut { return sig.c_name.clone(); } } // Second pass (fallback): params only (for non-mut-overload case). for sig in overloads.iter() { if sig.param_c_types == want_params { return sig.c_name.clone(); } } } } let safe_type = Self::receiver_type_c_ident(&recv.type_name); // Plan 100.6 (D164): consume-bit in method mangling (fallback path). // Must match the base_c_name formula in pre-pass registration (~line 2067). match recv.kind { ReceiverKind::Instance => { if recv.consume { format!("Nova_{}_consume_{}", safe_type, f.name) } else { format!("Nova_{}_method_{}", safe_type, f.name) } } ReceiverKind::Static => format!("Nova_{}_static_{}", safe_type, f.name), } } else { // Plan 170 (D307): file-private free fn — the C symbol is file- // discriminated by the DECLARING file (`f.span.file_id`), so the // definition header AND the forward-decl always resolve to the same // per-file name regardless of `current_emit_file_id`. Take priority // over the shared overload registry (file-private fns are never in // it — see emit_fn_forward_decl skip). // // [M-exp-promotion-blockers: uuid_namespace] (батч 3 follow-up к // 88a2ffe75): the batch-2 cross-module same-name dedup routes // COLLIDING plain (non-`priv(file)`) free fns through this same // per-(file_id, name) map — but this lookup was gated on // `f.file_private`, so the FORWARD DECLARATION still emitted the // unqualified name (`nova_fn_rotl32`, twice) while the definition // and call sites used the qualified one → first call hit C's // implicit-declaration rule → "conflicting types" at the // definition. Consult the map for EVERY non-external free fn: it // only contains entries D307 or the dedup deliberately inserted, // keyed by the DECLARING file, so non-colliding names miss it and // stay byte-identical. if !f.is_external { if let Some(mangled) = self .file_priv_fn_c_names .get(&(f.span.file_id, f.name.clone())) { return mangled.clone(); } } // D84: free-function — тот же путь через registry с sentinel-key // ("", name). Если несколько overloads — резолвим по param C-типам // этого FnDecl'а и возвращаем mangled c_name. let key = ("".to_string(), f.name.clone()); if let Some(overloads) = self.method_overloads.get(&key) { if overloads.len() > 1 { let want_params: Vec<String> = f.params.iter() .map(|p| self.type_ref_to_c(&p.ty) .unwrap_or_else(|_| "nova_int".into())) .collect(); // Plan 184 (Р13/Р14): mode tiebreak first — a free-fn // mode-overload (`f(x H)` vs `f(mut x H)`) shares param C-types. let want_modes = Self::fn_param_modes(f); for sig in overloads { if sig.param_c_types == want_params && sig.param_modes == want_modes { return sig.c_name.clone(); } } for sig in overloads { if sig.param_c_types == want_params { return sig.c_name.clone(); } } } } self.free_fn_c_name(&f.name) } } // Plan 172.1 U.2.5 (§0): dead `resolve_overload` removed — был `#[allow(dead_code)]` // c 0 callers (emit_call использует inline overload-резолв; перенос резолва в чекер — U.3). /// Mangle an effect op name with its param C-types for vtable field naming. /// Single overload (no collision): returns plain name. /// Overloaded: returns `name__type1_type2` (e.g. `balance__nova_int`). /// `all_methods` is the full list of methods in this effect. fn mangle_op(name: &str, param_c_types: &[String], all_methods: &[(&str, &[String])]) -> String { let same_name_count = all_methods.iter().filter(|(n, _)| *n == name).count(); if same_name_count <= 1 { return name.to_string(); } // Mangle: replace pointer stars and spaces with underscores for valid C identifier let suffix: String = param_c_types.iter() .map(|t| t.replace("* ", "_ptr_").replace('*', "_ptr").replace(' ', "_")) .collect::<Vec<_>>() .join("_"); if suffix.is_empty() { format!("{}_void", name) } else { format!("{}__{}", name, suffix) } } /// Lookup in effect schema by op name, supporting both mangled and plain keys. /// Used at call-sites where only the plain method name is known. fn schema_lookup<'s>( schema: &'s HashMap<String, (Vec<String>, String)>, method_name: &str, ) -> Option<&'s (Vec<String>, String)> { if let Some(v) = schema.get(method_name) { return Some(v); } // Mangled key search: find first key that is `method_name` or starts with `method_name__` let prefix = format!("{}__", method_name); schema.iter() .find(|(k, _)| k.as_str() == method_name || k.starts_with(&prefix)) .map(|(_, v)| v) } /// Recursively collect type names from a TypeRef. /// `out` — regular struct names (Nova_X). `vtable_out` — effect vtable names (NovaVtable_X) /// from Handler[X] generics and Func effects. fn collect_typeref_names( ty: &crate::ast::TypeRef, out: &mut HashSet<String>, vtable_out: &mut HashSet<String>, ) { use crate::ast::TypeRef; match ty { TypeRef::Named { path, generics, .. } => { let name = path.last().cloned(); if let Some(n) = &name { // Plan 97 Ф.3 (D142): `Handler` → `Effect`. // `Effect[X]` → X is a vtable name, not a struct name. if n == "Effect" { if let Some(TypeRef::Named { path: gpath, .. }) = generics.first() { if let Some(eff) = gpath.last() { vtable_out.insert(eff.clone()); } } return; } out.insert(n.clone()); } for g in generics { Self::collect_typeref_names(g, out, vtable_out); } } TypeRef::Array(inner, _) | TypeRef::FixedArray(_, inner, _) => { Self::collect_typeref_names(inner, out, vtable_out); } TypeRef::Tuple(items, _) => { for item in items { Self::collect_typeref_names(item, out, vtable_out); } } TypeRef::Func { params, effects, return_type, .. } => { for p in params { Self::collect_typeref_names(p, out, vtable_out); } // Effects in fn type → vtable names for e in effects { if let TypeRef::Named { path, .. } = e { if let Some(n) = path.last() { vtable_out.insert(n.clone()); } } } if let Some(r) = return_type { Self::collect_typeref_names(r, out, vtable_out); } } // Plan 97 Ф.2 (D142): анонимный protocol-тип не вводит // именованных type-зависимостей — методы внутри ссылаются // на `Self` и тип-параметры окружения; собственного // C-struct'а у protocol нет (void*). TypeRef::Protocol { methods, .. } => { for m in methods { for p in &m.params { Self::collect_typeref_names(&p.ty, out, vtable_out); } if let Some(rt) = &m.return_type { Self::collect_typeref_names(rt, out, vtable_out); } } } TypeRef::Unit(_) => {} // D176 (Plan 108): readonly T — transparent. TypeRef::Readonly(inner, _) => Self::collect_typeref_names(inner, out, vtable_out), // Plan 118 D216: typed pointer `*T` — recurse on inner для // dependency collection (pointee type must be declared). // Plan 118.5: Mut/Unsafe are transparent wrappers. TypeRef::Pointer(inner, _) | TypeRef::Mut(inner, _) | TypeRef::Uninit(inner, _) | TypeRef::Ref(inner, _) => Self::collect_typeref_names(inner, out, vtable_out), } } /// Plan 48 Ф.3: check if TypeRef contains any of the given type params (directly or in generics). /// Used to detect when erased struct fields should be void* rather than a mangled generic name. /// /// Plan 91.12 V2 followup #3 (2026-06-02): thin wrapper над /// `TypeRef::uses_any_type_param` (ast::mod.rs) — extracted common /// helper, removed duplication с types/mod.rs's parallel impl. #[inline] fn type_ref_uses_any_type_param(ty: &crate::ast::TypeRef, type_params: &HashSet<String>) -> bool { // Plan 91.12 V2 followup #3: delegate to TypeRef::uses_any_type_param // (single source of truth in ast/mod.rs). Plan 118 D216 added Pointer // recursion to the upstream method, so this thin wrapper supports // `*T` family automatically. ty.uses_any_type_param(type_params) } /// Plan 153.2 gap A: does this TypeRef contain a function type anywhere? /// Used to detect closure-holding generic-type fields (`f fn(T)->U`, /// `step fn()->Option[T]`) whose erased-method body produces invalid C /// (closure cast through literal type-param placeholders). Recurses through /// the type-constructor wrappers so a wrapped/nested function is also caught. fn type_ref_contains_func(ty: &crate::ast::TypeRef) -> bool { use crate::ast::TypeRef; match ty { TypeRef::Func { .. } => true, TypeRef::Named { generics, .. } => generics.iter().any(Self::type_ref_contains_func), TypeRef::Array(inner, _) | TypeRef::FixedArray(_, inner, _) | TypeRef::Pointer(inner, _) | TypeRef::Mut(inner, _) | TypeRef::Uninit(inner, _) | TypeRef::Readonly(inner, _) => Self::type_ref_contains_func(inner), TypeRef::Tuple(elems, _) => elems.iter().any(Self::type_ref_contains_func), _ => false, } } /// Collect type names referenced in a type declaration's fields/variants. fn collect_typeref_names_in_typedecl( t: &crate::ast::TypeDecl, out: &mut HashSet<String>, vtable_out: &mut HashSet<String>, ) { use crate::ast::{TypeDeclKind, SumVariantKind}; match &t.kind { TypeDeclKind::Record(fields) => { for f in fields { Self::collect_typeref_names(&f.ty, out, vtable_out); } } TypeDeclKind::Sum(variants) => { for v in variants { match &v.kind { SumVariantKind::Unit => {} SumVariantKind::Tuple(tys) => { for ty in tys { Self::collect_typeref_names(ty, out, vtable_out); } } SumVariantKind::Record(fields) => { for f in fields { Self::collect_typeref_names(&f.ty, out, vtable_out); } } } } } _ => {} } } /// Plan 138.1 Ф.1 (D239): collect the element `TypeRef` of every `[]T` /// occurrence (recursively) so the corresponding `Vec[T]` mono instance /// can be forward-declared (`typedef struct Nova_Vec____<elem> ...;`) /// before any record/sum struct that embeds `[]T` as a field. Mirrors /// `collect_typeref_names` but yields the array element TypeRefs. /// /// Excludes `[]fn(...)` (closure-array — stays NovaArray_void_p*, out of /// scope per [M-138.1-closure-array]) and `[N]T` FixedArray (separate /// built-in). For nested `[][]T` it pushes BOTH the inner `[]T` and the /// outer element so the recursive Vec[Vec[T]] forward-decl is complete. fn collect_array_elem_typerefs(ty: &crate::ast::TypeRef, out: &mut Vec<crate::ast::TypeRef>) { use crate::ast::TypeRef; match ty { TypeRef::Array(inner, _) => { // Closure-array stays NovaArray — do NOT treat as Vec. if matches!(inner.as_ref(), TypeRef::Func { .. }) { return; } out.push((**inner).clone()); // Recurse into the element to catch nested `[][]T`. Self::collect_array_elem_typerefs(inner, out); } // FixedArray `[N]T` is a separate built-in — out of scope. TypeRef::FixedArray(_, inner, _) => { Self::collect_array_elem_typerefs(inner, out); } TypeRef::Named { path, generics, .. } => { // Plan 152.8: `Vec[T]` written as a Named generic (not `[]T` sugar) // must also trigger a forward-decl for the Vec mono instance. // Push the element TypeRef just like the Array arm does. if path.last().map(|s| s == "Vec").unwrap_or(false) { if let Some(elem) = generics.first() { if !matches!(elem, TypeRef::Func { .. }) { out.push(elem.clone()); // Recurse to handle nested Vec[Vec[T]] etc. Self::collect_array_elem_typerefs(elem, out); } } } for g in generics { Self::collect_array_elem_typerefs(g, out); } } TypeRef::Tuple(items, _) => { for it in items { Self::collect_array_elem_typerefs(it, out); } } TypeRef::Func { params, return_type, .. } => { for p in params { Self::collect_array_elem_typerefs(p, out); } if let Some(r) = return_type { Self::collect_array_elem_typerefs(r, out); } } TypeRef::Protocol { methods, .. } => { for m in methods { for p in &m.params { Self::collect_array_elem_typerefs(&p.ty, out); } if let Some(rt) = &m.return_type { Self::collect_array_elem_typerefs(rt, out); } } } TypeRef::Readonly(inner, _) | TypeRef::Pointer(inner, _) | TypeRef::Mut(inner, _) | TypeRef::Uninit(inner, _) | TypeRef::Ref(inner, _) => Self::collect_array_elem_typerefs(inner, out), TypeRef::Unit(_) => {} } } /// Plan 138.1 Ф.1: `[]T` element TypeRefs referenced in a type /// declaration's fields/variants (for Vec mono forward-decl). fn collect_array_elem_typerefs_in_typedecl( t: &crate::ast::TypeDecl, out: &mut Vec<crate::ast::TypeRef>, ) { use crate::ast::{TypeDeclKind, SumVariantKind}; match &t.kind { TypeDeclKind::Record(fields) => { for f in fields { Self::collect_array_elem_typerefs(&f.ty, out); } } TypeDeclKind::Sum(variants) => { for v in variants { match &v.kind { SumVariantKind::Unit => {} SumVariantKind::Tuple(tys) => { for ty in tys { Self::collect_array_elem_typerefs(ty, out); } } SumVariantKind::Record(fields) => { for f in fields { Self::collect_array_elem_typerefs(&f.ty, out); } } } } } _ => {} } } /// Plan 168 (D300): collect Vec[T] element TypeRefs from a function body /// so that body-only Vec instantiations (local vars, TurboFish calls) /// get forward-declared in the global preamble, just like signature/field /// sites. Scans Let type annotations and TurboFish type_args recursively. /// Lambda / closure bodies are NOT entered — they are handled by their own /// mono-pass context and do not produce Vec-local vars without a /// surrounding signature context. fn collect_array_elem_typerefs_in_fnbody( body: &crate::ast::FnBody, out: &mut Vec<crate::ast::TypeRef>, ) { use crate::ast::FnBody; match body { FnBody::Block(block) => Self::collect_array_elem_typerefs_in_block(block, out), FnBody::Expr(expr) => Self::collect_array_elem_typerefs_in_expr(expr, out), FnBody::External => {} } } fn collect_array_elem_typerefs_in_block( block: &crate::ast::Block, out: &mut Vec<crate::ast::TypeRef>, ) { for stmt in &block.stmts { Self::collect_array_elem_typerefs_in_stmt(stmt, out); } if let Some(trailing) = &block.trailing { Self::collect_array_elem_typerefs_in_expr(trailing, out); } } fn collect_array_elem_typerefs_in_stmt( stmt: &crate::ast::Stmt, out: &mut Vec<crate::ast::TypeRef>, ) { use crate::ast::Stmt; match stmt { Stmt::Let(ld) => { if let Some(ty) = &ld.ty { Self::collect_array_elem_typerefs(ty, out); } Self::collect_array_elem_typerefs_in_expr(&ld.value, out); } Stmt::Const(cd) => { if let Some(ty) = &cd.ty { Self::collect_array_elem_typerefs(ty, out); } Self::collect_array_elem_typerefs_in_expr(&cd.value, out); } Stmt::Expr(e) => Self::collect_array_elem_typerefs_in_expr(e, out), Stmt::Assign { target, value, .. } => { Self::collect_array_elem_typerefs_in_expr(target, out); Self::collect_array_elem_typerefs_in_expr(value, out); } Stmt::TupleAssign { lhs, rhs, .. } => { for e in lhs { Self::collect_array_elem_typerefs_in_expr(e, out); } for e in rhs { Self::collect_array_elem_typerefs_in_expr(e, out); } } Stmt::Return { value: Some(e), .. } | Stmt::Throw { value: e, .. } | Stmt::Defer { body: e, .. } => { Self::collect_array_elem_typerefs_in_expr(e, out); } Stmt::ConsumeScope { type_annot, init, body, .. } => { if let Some(ty) = type_annot { Self::collect_array_elem_typerefs(ty, out); } Self::collect_array_elem_typerefs_in_expr(init, out); Self::collect_array_elem_typerefs_in_block(body, out); } // Ghost / proof statements — no runtime TypeRefs Stmt::Return { value: None, .. } | Stmt::Break(_) | Stmt::Continue(_) | Stmt::AssertStatic { .. } | Stmt::Assume { .. } | Stmt::Apply { .. } | Stmt::Calc { .. } | Stmt::Reveal { .. } => {} } } fn collect_array_elem_typerefs_in_expr( expr: &crate::ast::Expr, out: &mut Vec<crate::ast::TypeRef>, ) { use crate::ast::ExprKind; match &expr.kind { // Plan 172.5 (D326): call-site `ref <place>` — transparent; recurse // the place for array-elem typeref collection. ExprKind::RefArg(inner) => { Self::collect_array_elem_typerefs_in_expr(inner, out); } // Key sites: TurboFish carries explicit type_args (e.g. Vec[u32].with_capacity(n)) ExprKind::TurboFish { base, type_args } => { // If this is `Vec[T]` (or `[]T` sugar), add T directly as a Vec element // so the forward-decl pre-pass picks up body-only Vec instantiations. let base_is_vec = matches!(&base.kind, ExprKind::Ident(n) if n == "Vec"); if base_is_vec { for ta in type_args { // Push element type directly — mirrors Array arm of // collect_array_elem_typerefs which pushes inner. if !matches!(ta, crate::ast::TypeRef::Func { .. }) { out.push(ta.clone()); } Self::collect_array_elem_typerefs(ta, out); } } else { for ta in type_args { Self::collect_array_elem_typerefs(ta, out); } } Self::collect_array_elem_typerefs_in_expr(base, out); } ExprKind::As(e, ty) | ExprKind::Is(e, ty) => { Self::collect_array_elem_typerefs(ty, out); Self::collect_array_elem_typerefs_in_expr(e, out); } ExprKind::RecordLit { .. } => { // RecordLit does not carry a TypeRef directly in ExprKind; // its type_name is a path (Vec<String>), not a TypeRef. // No TypeRef to collect here. } ExprKind::Call { func, args, trailing } => { Self::collect_array_elem_typerefs_in_expr(func, out); for a in args { use crate::ast::CallArg; match a { CallArg::Item(e) | CallArg::Spread(e) => { Self::collect_array_elem_typerefs_in_expr(e, out); } CallArg::Named { value, .. } => { Self::collect_array_elem_typerefs_in_expr(value, out); } } } if let Some(t) = trailing { use crate::ast::Trailing; match t { Trailing::Block(block) => { Self::collect_array_elem_typerefs_in_block(block, out); } // Fn / LegacyBlockWithParams are closures — skip (separate mono context) Trailing::Fn(_) | Trailing::LegacyBlockWithParams(_) => {} } } } ExprKind::Member { obj, .. } | ExprKind::Spawn(obj) | ExprKind::Try(obj) | ExprKind::Bang(obj) | ExprKind::Throw(obj) | ExprKind::Interrupt(Some(obj)) // [E_COALESCE_RETURN_FALLBACK]: checker always rejects `X ?? return R` // before codegen; walked defensively like `Interrupt`. | ExprKind::CoalesceReturnFallback(Some(obj)) => { Self::collect_array_elem_typerefs_in_expr(obj, out); } ExprKind::Index { obj, index } => { Self::collect_array_elem_typerefs_in_expr(obj, out); Self::collect_array_elem_typerefs_in_expr(index, out); } ExprKind::Unary { operand, .. } => { Self::collect_array_elem_typerefs_in_expr(operand, out); } ExprKind::Binary { left, right, .. } | ExprKind::Coalesce(left, right) => { Self::collect_array_elem_typerefs_in_expr(left, out); Self::collect_array_elem_typerefs_in_expr(right, out); } ExprKind::Block(block) => { Self::collect_array_elem_typerefs_in_block(block, out); } ExprKind::If { cond, then, else_, .. } => { Self::collect_array_elem_typerefs_in_expr(cond, out); Self::collect_array_elem_typerefs_in_block(then, out); if let Some(eb) = else_ { use crate::ast::ElseBranch; match eb { ElseBranch::Block(b) => { Self::collect_array_elem_typerefs_in_block(b, out); } ElseBranch::If(e) => { Self::collect_array_elem_typerefs_in_expr(e, out); } } } } ExprKind::IfLet { scrutinee, then, else_, .. } => { Self::collect_array_elem_typerefs_in_expr(scrutinee, out); Self::collect_array_elem_typerefs_in_block(then, out); if let Some(eb) = else_ { use crate::ast::ElseBranch; match eb { ElseBranch::Block(b) => { Self::collect_array_elem_typerefs_in_block(b, out); } ElseBranch::If(e) => { Self::collect_array_elem_typerefs_in_expr(e, out); } } } } ExprKind::Match { scrutinee, arms } => { Self::collect_array_elem_typerefs_in_expr(scrutinee, out); for arm in arms { use crate::ast::MatchArmBody; match &arm.body { MatchArmBody::Expr(e) => Self::collect_array_elem_typerefs_in_expr(e, out), MatchArmBody::Block(b) => Self::collect_array_elem_typerefs_in_block(b, out), } } } ExprKind::For { iter, body, .. } | ExprKind::ParallelFor { iter, body, .. } => { Self::collect_array_elem_typerefs_in_expr(iter, out); Self::collect_array_elem_typerefs_in_block(body, out); } ExprKind::While { cond, body, .. } => { Self::collect_array_elem_typerefs_in_expr(cond, out); Self::collect_array_elem_typerefs_in_block(body, out); } ExprKind::WhileLet { scrutinee, body, .. } => { Self::collect_array_elem_typerefs_in_expr(scrutinee, out); Self::collect_array_elem_typerefs_in_block(body, out); } ExprKind::Loop { body, .. } => { Self::collect_array_elem_typerefs_in_block(body, out); } ExprKind::With { body, .. } | ExprKind::Supervised { body, .. } | ExprKind::Detach(body) | ExprKind::Blocking(body) | ExprKind::Forbid { body, .. } | ExprKind::Realtime { body, .. } => { Self::collect_array_elem_typerefs_in_block(body, out); } ExprKind::TupleLit(items) => { for e in items { Self::collect_array_elem_typerefs_in_expr(e, out); } } ExprKind::ArrayLit(elems) => { use crate::ast::ArrayElem; for e in elems { match e { ArrayElem::Item(ex) | ArrayElem::Spread(ex) => { Self::collect_array_elem_typerefs_in_expr(ex, out); } } } } ExprKind::MapLit { elems, inferred_key, inferred_value, .. } => { if let Some(k) = inferred_key { Self::collect_array_elem_typerefs(k, out); } if let Some(v) = inferred_value { Self::collect_array_elem_typerefs(v, out); } use crate::ast::MapElem; for e in elems { match e { MapElem::Pair(k, v) => { Self::collect_array_elem_typerefs_in_expr(k, out); Self::collect_array_elem_typerefs_in_expr(v, out); } MapElem::Spread(e) => { Self::collect_array_elem_typerefs_in_expr(e, out); } } } } ExprKind::TaggedTemplate { tag, args, .. } => { Self::collect_array_elem_typerefs_in_expr(tag, out); for e in args { Self::collect_array_elem_typerefs_in_expr(e, out); } } ExprKind::Select { arms } => { for arm in arms { Self::collect_array_elem_typerefs_in_block(&arm.body, out); } } ExprKind::Forall { range, body, .. } | ExprKind::Exists { range, body, .. } => { Self::collect_array_elem_typerefs_in_expr(range, out); Self::collect_array_elem_typerefs_in_expr(body, out); } ExprKind::Range { start, end, .. } => { if let Some(e) = start { Self::collect_array_elem_typerefs_in_expr(e, out); } if let Some(e) = end { Self::collect_array_elem_typerefs_in_expr(e, out); } } // Atoms and lambda/closure bodies — no TypeRefs to collect here ExprKind::IntLit(_) | ExprKind::FloatLit(_) | ExprKind::StrLit(_) | ExprKind::BoolLit(_) | ExprKind::UnitLit | ExprKind::CharLit(_) | ExprKind::HexBlobLit(_) | ExprKind::NullPtrLit | ExprKind::Ident(_) | ExprKind::Path(_) | ExprKind::SelfAccess | ExprKind::InterpolatedStr { .. } | ExprKind::Interrupt(None) | ExprKind::CoalesceReturnFallback(None) | ExprKind::Lambda { .. } | ExprKind::ClosureLight { .. } | ExprKind::ClosureFull(_) | ExprKind::HandlerLit { .. } | ExprKind::ProtocolLit { .. } => {} } } /// Plan 95 Ф.2.1: C type приёмника для builtin sum-типов /// (`Option`/`Result`), участвующих в method-mono через /// «method-only»-канал. Резолвится через `current_type_subst` (заполнен /// `register_mono_method_instance` / `emit_monomorphized_method` перед /// `receiver_c_type` call'ом) + сохранённые в Ф.1.1 /// `builtin_sum_type_params` имена type-параметров типа. /// /// `NovaOpt_<sani(T)>` для Option — **value-тип** (без `*`); приёмник /// идёт по значению, как `NovaOpt_` повсюду в codegen'е. /// /// `NovaRes_<ok>_<err>*` для Result — **pointer**; согласовано с /// `is_result_like` / трамплинами `Nova_Result_method_*(NovaRes_<n>* r)`. /// /// Fallback (subst отсутствует — не должно случиться вне mono-контекста, /// defensive): legacy `Nova_<T>*` → CC-fail loud (Plan 79: no silent /// fallback). fn builtin_sum_receiver_c_type(&self, type_name: &str) -> String { let params = match self.builtin_sum_type_params.get(type_name) { Some(p) => p, None => return format!("Nova_{}*", type_name), }; match (type_name, params.len()) { ("Option", 1) => { let t_c = match self.subst_c(¶ms[0]) { Some(t) => t, None => return format!("Nova_{}*", type_name), }; format!("NovaOpt_{}", Self::sanitize_for_novaopt(&t_c)) } ("Result", 2) => { let ok_c = match self.subst_c(¶ms[0]) { Some(t) => t, None => return format!("Nova_{}*", type_name), }; let err_c = match self.subst_c(¶ms[1]) { Some(t) => t, None => return format!("Nova_{}*", type_name), }; format!("NovaRes_{}_{}*", Self::sanitize_for_novaopt(&ok_c), Self::sanitize_for_novaopt(&err_c)) } _ => format!("Nova_{}*", type_name), } } /// №170: bare-typevar `recv_type` ("T") -> конкретное Nova-имя из /// `current_type_subst`, ТОЛЬКО для примитивно-скалярных подстановок. /// Полное обоснование (включая пойманную и исключённую ABI-регрессию /// SplitIter/VecIter для protocol-bound blanket'ов) — mono_method_registry.rs §№170. fn resolve_mono_recv_nova_name(&self, recv_type: &str) -> String { if recv_type.len() <= 2 && recv_type.chars().all(|c| c.is_ascii_uppercase()) { if let Some(c_ty) = self.subst_c(recv_type) { if !c_ty.contains("Nova") && !c_ty.ends_with('*') { return Self::debt_nova_type_name_from_c(&c_ty); } } } recv_type.to_string() } /// C type for receiver-typed parameter (D35 v2: receiver may be a primitive). /// Returns the C type to use for `nova_self`. /// /// Plan 128 Ф.1: `recv_mutable` carries the AST `Receiver.mutable` flag /// (`fn Type mut @method` vs `fn Type @method`). /// /// Plan 124.8 §2.7 + Plan 128 Ф.2 (D215 amend): NamedTuple receiver ABI /// now branches on `recv_mutable` — `mut` methods take `NovaTuple_X*` /// (pointer to caller's stack slot, in-place @field mutation propagates); /// `ro` methods keep value ABI (`NovaTuple_X`, immutable copy). Mirrors /// the NovaValue_X* pattern (D226). /// /// Owner fix 2026-08-09 (closes №468, R5 `02-types.md:16101`): primitives /// are NO LONGER unconditionally by-value. `mut @` ≡ `mut ref @` is /// ALWAYS by-pointer per R5, "any size" including scalars — this was the /// one receiver family where codegen silently dropped the mutation /// (E_PRIMITIVE_MUT_METHOD existed to forbid it instead of fixing the /// ABI). `recv_mutable` now selects the pointer form for primitives too; /// `ro @` keeps the existing by-value form unchanged (R5: ro is /// size-discretionary/invisible, no observable-mutation contract to honour). fn receiver_c_type(&self, type_name: &str, recv_mutable: bool) -> String { match type_name { // Plan 172.1-K1 (int-de-collapse): a primitive receiver lowers through the SINGLE // scalar source `primitive_name_to_c` — the SAME leaf `resolved_type_to_c` uses // (§0: `resolved_type_to_c → int_name → primitive_name_to_c`), carrying width+sign. // NO collapse to `nova_int` (§0/§10/D368/D130): `int`/`i64` stay `nova_int` (D129 // alias), but `uint`→nova_uint, `u8`→nova_byte, `u16`→uint16_t, `u32`→uint32_t, // `u64`→uint64_t, `i8`→int8_t, `i16`→int16_t, `i32`→int32_t — now DISTINCT (fixes // signed-compare-of-unsigned, e.g. `Nova_uint_method_compare`). `receiver_c_type` // itself is retired into a receiver-aware `resolved_type_to_c` in the U.4.5/FIN // endgame — this routes its scalar case to the single source NOW (behavioural fix, // NOT a redone patch: the mapping is byte-identical to what the unified lowering emits). "int" | "i8" | "i16" | "i32" | "i64" | "u8" | "u16" | "u32" | "u64" | "uint" => { let base = Self::primitive_name_to_c(type_name) .expect("int-family primitive always present in primitive_name_to_c"); if recv_mutable { format!("{}*", base) } else { base.to_string() } } // `size` (usize-like) has NO `primitive_name_to_c` entry — its width/sign model is // unpinned (separate concern, not in the carrier). Preserved as `nova_int` pending that. "size" => if recv_mutable { "nova_int*".to_string() } else { "nova_int".to_string() }, "f32" => if recv_mutable { "nova_f32*".to_string() } else { "nova_f32".to_string() }, "f64" => if recv_mutable { "nova_f64*".to_string() } else { "nova_f64".to_string() }, "bool" => if recv_mutable { "nova_bool*".to_string() } else { "nova_bool".to_string() }, "char" => if recv_mutable { "nova_char*".to_string() } else { "nova_char".to_string() }, "str" => if recv_mutable { "nova_str*".to_string() } else { "nova_str".to_string() }, // Plan 95 Ф.2.1: builtin sum-types (`Option`/`Result`) с // method-mono каналом — value-тип `NovaOpt_<T>` / pointer // `NovaRes_<ok>_<err>*` (см. `builtin_sum_receiver_c_type`). // Без этого спец-кейса `format!("Nova_{}*", "Option")` = // `Nova_Option*` (incomplete type) — корень Plan 93 Ф.0 CC-fail'а. "Option" | "Result" => self.builtin_sum_receiver_c_type(type_name), other => { // Plan 248 (wave 3, D447 #no_copy): the 11 value-inside // atomics — hardcoded, order-independent (mirrors the // value-generic-mono check just below, same reasoning): // does not depend on `type_aliases` having already been // populated by an earlier pass over sync.nv's own module. if matches!(other, "AtomicI64" | "AtomicI32" | "AtomicI16" | "AtomicI8" | "AtomicU64" | "AtomicU32" | "AtomicU16" | "AtomicU8" | "AtomicInt" | "AtomicUint" | "AtomicBool") { return format!("NovaValue_{}*", other); } // Plan 153.2 Ф.1 (STAGE 1 — by-value generic value-records): // a `value` generic mono receiver (`BoxIter____nova_int`) is a // D226 value-record — its receiver C-type is a POINTER to a // stack slot (`NovaValue_<short>*`), so `@field`-mutating `mut` // methods propagate to the caller, EXACTLY as the non-generic // value-record path does (~line 10917). Order-independent: does // not need `type_aliases` populated first. if let Some(short) = self.debt_value_generic_mono_short(other) { return format!("NovaValue_{}*", short); } // Plan 101.1: bare typevar receiver (`fn[T] T @method`) — // resolve T via current_type_subst при mono-emission. // Без этого receiver_c_type("T") → "Nova_T*" (placeholder). // Plan 161: blanket protocol-receiver methods (`fn[I Next[T]] I @m`): // when the substituted C type is a heap struct (`Nova_X` without `*`), // add `*` so the function signature is `Nova_X* nova_self` (pointer), // not `Nova_X nova_self` (value). Struct receivers are always passed // by pointer in Nova's C backend. if other.len() <= 2 && other.chars().all(|c| c.is_ascii_uppercase()) { if let Some(c_ty) = self.subst_c(other) { // Heap struct (`Nova_X`) must become a pointer (`Nova_X*`). if c_ty.starts_with("Nova_") && !c_ty.ends_with('*') { return format!("{}*", c_ty); } return c_ty; } } // Plan 153.5 (D263) / [M-153.5-flatten-nested-receiver]: a // NESTED slice receiver (`[][]T`, `[][][]T`, …, depth >= 2) is // NOT a single-level array. Reconstruct the structured // `Array(Array(...Named))` type and lower it via `type_ref_to_c` // (which, under D239, mono's each level to `Nova_Vec____<elem>*`) // — this yields the correct multi-level receiver C type with // `current_type_subst` (T=int) in scope. The single-level `[]T` // case below is UNCHANGED (legacy `NovaArray_<elem>*`), so flat // dispatch stays byte-identical. if other.starts_with("[][]") { let nested_ty = Self::slice_str_to_typeref(other); if let Ok(c) = self.type_ref_to_c(&nested_ty) { return c; } } // Extension methods on array types: []T, []str, []int, etc. // Plan 172.12 A7 (Vec-canon flip, owner decision 2026-07-08): the // single-level `[]T` receiver was the LAST wide NovaArray_ emitter — // it now mirrors `resolved_array_to_c`'s Vec-flip (D239 `[]T ≡ // Vec[T]`), mangling + registering the `Vec[elem]` instance instead // of building a legacy `NovaArray_<elem>*` name. `uint` closes // [M-uint-legacy-array-uint64-until-a4] here: it now maps to the // canonical `nova_uint` (not the old `uint64_t` NovaArray slot), // matching the scalar receiver arm above. if let Some(elem_ty) = other.strip_prefix("[]") { let c_elem_owned: String; let c_elem: &str = match elem_ty { "str" => "nova_str", "bool" => "nova_bool", "f64" => "nova_f64", "f32" => "nova_f32", "u8" => "nova_byte", "char" => "nova_char", // Plan 70.4 Ф.2: sized-int distinct packed storage. "i32" => "int32_t", "i16" => "int16_t", "i8" => "int8_t", "u32" => "uint32_t", "u16" => "uint16_t", "u64" => "uint64_t", // uint → nova_uint (Vec-flip canon; [M-uint-legacy-array-uint64-until-a4] CLOSED). "uint" => "nova_uint", // Plan 101.1: T (generic typevar) — check current_type_subst // для mono'd context (`fn[T] []T @method` body). // Без этого fallback на nova_int → wrong type для // non-int T при mono per-T emission. _ => { if let Some(c_ty) = self.subst_c(elem_ty) { let trimmed = c_ty.trim_end_matches('*').trim(); c_elem_owned = trimmed.to_string(); &c_elem_owned } else if elem_ty.len() <= 2 && elem_ty.chars().all(|c| c.is_ascii_uppercase()) { // Genuine unresolved generic typevar shape (`T`/`U`/`K`/…, // no `current_type_subst` entry — erased-generic-method // body context, mirrors the bare-receiver typevar gate // above, ~16902) — erasure fallback, unchanged. "nova_int" // int/i64/erased T fallback } else { // [M-slice-ext-receiver-for-in-elem-type] (codegen half, // 2026-07-19): `elem_ty` is a CONCRETE named element (a // real record/sum, e.g. `[]TaskResult`) — NOT a typevar. // The generic-typevar arm above (§Plan 101.1) assumed any // non-primitive `elem_ty` absent from `current_type_subst` // was an unresolved generic, defaulting straight to the // `nova_int` erasure truth — wrong here: no slice-extension // anywhere in the corpus used a concrete NAMED record // element before, so this branch was unreachable-but-wrong // (silently mono'd `nova_self` as `Vec[int]` regardless of // the receiver's real element type — the actual root of // the reported `for r in @` CC-FAIL; the checker-side // `scope["@"]` fix alone was NOT sufficient, THIS is where // the C receiver type is actually decided). Resolve through // the SAME canonical single-source lowering every other // Named type uses (`resolved_type_to_c` → `resolved_named_to_c`), // identically to how `resolved_array_to_c` (the WORKING // path for `[]TaskResult` as an ordinary field/param, ~3744) // derives its Vec-mono element arg — so the mangled // `Vec[TaskResult]` instance name this receiver builds is // byte-identical to the one the call-site's own `[]TaskResult` // value already registered (no second, diverging mangle). let named = crate::types::ResolvedType::Named { name: elem_ty.to_string(), module: Vec::new(), args: Vec::new(), }; c_elem_owned = self.resolved_type_to_c(&named) .unwrap_or_else(|_| "nova_int".to_string()); &c_elem_owned } } }; let type_args_c = vec![c_elem.to_string()]; let mangled = Self::compute_generic_type_c_name("Vec", &type_args_c); if !self.emitted_generic_type_instances.contains(&mangled) { let mut wl = self.generic_type_worklist.borrow_mut(); if !wl.iter().any(|(_, _, m)| m == &mangled) { wl.push(("Vec".to_string(), Self::args_lift(&type_args_c), mangled.clone())); } } self.generic_type_instance_info .borrow_mut() .entry(mangled.clone()) .or_insert_with(|| ("Vec".to_string(), Self::args_lift(&type_args_c))); return format!("{}*", mangled); } // Plan 101 [M-fn-prefix-int-only-mono] fix: если other уже // имеет форму полного NovaArray_<elem> (как при mono'd emit // for fn[T] []T @method где rt передан в register_mono_method_instance), // не префиксуем повторно "Nova_" — просто добавляем `*`. if other.starts_with("NovaArray_") || other.starts_with("Nova_Vec____") { return format!("{}*", other); } // Plan 120 (D215): named tuple receiver — value type, no pointer. // Plan 124.8 V2 (D226): value-record receiver — pointer to stack // slot (NovaValue_X*) so @field mutations propagate to caller. // Plan 124.8 §2.7 + Plan 128 Ф.2: NamedTuple `mut` receiver — // pointer (NovaTuple_X*) so @field mutations через `mut` методы // propagate to caller; `ro` receiver stays by-value (immutable copy). if let Some(c_ty) = self.type_aliases.get(other) { // 172.4 A6 (§0 ЕДИНЫЙ путь): value-record И named-tuple — // pointer-carrier receiver-ABI ВСЕГДА (mut И ro, D226-модель). // NT как ЗНАЧЕНИЕ (поле/параметр/return-не-Self/элемент) // остаётся by-value через type_ref_to_c — меняется ТОЛЬКО // receiver-ABI. Один предикат вместо двух параллельных веток. if Self::is_value_struct(c_ty) { return format!("{}*", c_ty); } } // [M-sync-crossmodule…] (D381): a method receiver whose type is a // colliding sum/record resolves to its module-qualified base (byte- // identical for non-colliding — `ref_type_base` ≡ id). format!("Nova_{}*", self.ref_type_base(other, &[])) } } } /// Plan 91 Ф.2.6 (D178): convert a simple literal Expr to its C representation. /// Used to emit default parameter values in Nova-body method dispatch. /// Only handles simple compile-time constants; returns None for anything complex. fn simple_literal_c(expr: &Expr) -> Option<String> { match &expr.kind { ExprKind::IntLit(n) => Some(format!("{}LL", n)), ExprKind::BoolLit(b) => Some(if *b { "1LL".to_string() } else { "0LL".to_string() }), ExprKind::FloatLit(f) => Some(f.to_string()), ExprKind::Unary { op: UnOp::Neg, operand } => { if let ExprKind::IntLit(n) = &operand.kind { Some(format!("-{}LL", n)) } else { None } } _ => None, } } /// Plan 153.5 (D263) / [M-153.5-flatten-nested-receiver]: reconstruct the /// structured `Array(Array(...Named))` TypeRef from a slice-receiver string /// `"[]"*N + innermost` (`"[][]T"` → `Array(Array(Named T))`). Used to lower /// a nested slice receiver via the canonical `type_ref_to_c` path. fn slice_str_to_typeref(s: &str) -> crate::ast::TypeRef { use crate::ast::TypeRef; let mut depth = 0usize; let mut rest = s; while let Some(r) = rest.strip_prefix("[]") { depth += 1; rest = r; } let dummy = Span::default(); let mut ty = TypeRef::Named { path: vec![rest.to_string()], generics: Vec::new(), span: dummy, }; for _ in 0..depth { ty = TypeRef::Array(Box::new(ty), dummy); } ty } /// Convert a receiver type name to a valid C identifier component. /// []T → "NovaArray_nova_int", []str → "NovaArray_nova_str", etc. /// Plan 153.5: a NESTED slice receiver (`[][]T`, depth >= 2) mangles to /// `NovaArray_` repeated per level so distinct nesting depths get distinct /// C identifiers (avoids arity/symbol collision the spec warns about). /// Other names are returned unchanged (already valid C identifiers). fn receiver_type_c_ident(type_name: &str) -> String { if type_name.starts_with("[][]") { // depth >= 2: strip ALL leading "[]" levels, mangle the innermost // element once, and prefix one "NovaArray_" per level so each depth // is a distinct, valid C identifier. let mut depth = 0usize; let mut rest = type_name; while let Some(r) = rest.strip_prefix("[]") { depth += 1; rest = r; } let inner_ident = Self::receiver_type_c_ident(&format!("[]{}", rest)); // inner_ident is already "NovaArray_<elem>"; add (depth-1) more. return format!("{}{}", "NovaArray_".repeat(depth - 1), inner_ident); } if let Some(elem_ty) = type_name.strip_prefix("[]") { let c_elem = match elem_ty { "str" => "nova_str", "bool" => "nova_bool", "f64" => "nova_f64", // Plan 70.4: f32 distinct (4 vs 8 bytes ABI). "f32" => "nova_f32", "u8" => "nova_byte", // Plan 70.3: char distinct. "char" => "nova_char", // Plan 70.4 Ф.2: sized-int distinct packed storage. "i32" => "int32_t", "i16" => "int16_t", "i8" => "int8_t", "u32" => "uint32_t", "u16" => "uint16_t", "u64" => "uint64_t", // uint → uint64_t (runtime-backed NovaArray). [M-uint-legacy-array-uint64-until-a4] "uint" => "uint64_t", // int/i64 + unknown erased T → nova_int. _ => "nova_int", }; return format!("NovaArray_{}", c_elem); } type_name.to_string() } fn params_c(&self, f: &FnDecl) -> Result<String, String> { let mut parts = Vec::new(); // Instance methods receive the receiver as the first parameter. // Primitives by value (D35 v2), records/sums by pointer. if let Some(recv) = &f.receiver { if matches!(recv.kind, ReceiverKind::Instance) { // Plan 128 Ф.1: thread recv.mutable (Ф.2 consumes). parts.push(format!("{} nova_self", self.receiver_c_type(&recv.type_name, recv.mutable))); } } for (p_idx, p) in f.params.iter().enumerate() { // Plan 72 P3-B: a protocol-typed parameter (`x Iter[int]`) lowers // to the `NovaBox_*` fat pointer so the callee can dispatch via the // vtable — mirrors the protocol-return-type lowering. let mut ty_c = if let Some((box_ty, _, _)) = self.protocol_box_c_type_for(&p.ty) { box_ty } else { self.type_ref_to_c(&p.ty)? }; // Plan 172.5 (D326 R5): a `mut ref` parameter is lowered to a C // pointer (`T*`) — the callee receives the caller's storage address // so writes land in the caller. Body uses auto-deref (`ref_params`). // Consistent between forward-decl and definition (both go through // this helper). `ro ref` is NOT lowered here — its zero-copy // placement is the size-driven auto mechanism of Plan 172.4 (R3), // NOT duplicated; explicit `ro ref` is a semantic annotation that // otherwise passes like a normal value parameter. // Plan 184 Р10: a value/primitive `mut x T` param is lowered to // `T*` (by-pointer in-out). (Legacy `mut ref` form removed — // заход-5 п.7: `ParamRefMode` больше нет.) if Self::param_is_inout_ptr(p, &ty_c) { ty_c.push('*'); } else if f.receiver.is_none() && self.free_fn_byref_flag(&f.name, p_idx) { // Plan 172.14 Ф.1: большой read-only value-struct параметр // free-fn — by-ref (`T*`). Тело auto-deref через `ref_params`, // call-site оборачивает аргумент в RefArg (см. // `build_free_fn_byref_map`). ty_c.push('*'); } else if let Some(recv) = &f.receiver { // [M-172.14-methods-byref]: то же для METHOD value-параметра // (НЕ receiver'а — тот уже by-ptr отдельным путём выше). if self.method_byref_flag(&recv.type_name, &f.name, p_idx) { ty_c.push('*'); } } // [M-c-keyword-ident-collision]: a Nova param named like a C // keyword (`long`, `int`, ...) must not appear bare in the C // signature — mangle the OUTPUT text only (internal bookkeeping // — var_types/var_mutable/etc. — stays keyed by the raw Nova // name `p.name`, unaffected). parts.push(format!("{} {}", ty_c, Self::mangle_field_name(&p.name))); } if parts.is_empty() { Ok("void".into()) } else { Ok(parts.join(", ")) } } /// Emit a type-erased version of a generic free function. /// Convert a TypeRef to C, erasing type parameters (in type_params set) to void*. /// Used for generic method emission where T→void*, []T→void*, Option[T]→NovaOpt_nova_int. fn erased_type_ref_c(&self, ty_opt: &Option<TypeRef>, type_params: &HashSet<String>) -> String { let ty = match ty_opt { None => return "nova_unit".into(), Some(t) => t, }; match ty { TypeRef::Named { path, generics, .. } => { let name = path.join("_"); if type_params.contains(&name) { return "void*".into(); } // Option[T] where T is a type param → NovaOpt_nova_int // Plan 62.A.bis Ф.2.4: c_name source = registry entry для // "Option" (hardcoded baseline: "NovaOpt_nova_int"). Fallback // на literal если registry почему-то пуст (defensive). if name == "Option" { if let Some(g) = generics.first() { if let TypeRef::Named { path: gp, .. } = g { if type_params.contains(&gp.join("_")) { let c_name = self.sum_schema_registry .lookup_sum_schema("Option") .map(|e| e.c_name.clone()) .unwrap_or_else(|| "NovaOpt_nova_int".into()); return c_name; } } } } // [M-generic-value-self-protocol-wrapper-mono] (221.1 Ф.2 // #25, ОКНО-3 2026-07-23): the two bare-typevar guards above // (`type_params.contains(&name)` direct-Self, and the // "Option[<bare T>]" hardcoded check just above) both miss a // NESTED still-generic inner — `Result[Wrap[T], E]` / // `Option[Wrap[T]]` (`Self` = the receiver's OWN generic // type, e.g. a protocol method `.m(...) -> Result[Self, E]` // on `Wrap[T] value {...}` — `Self` substitutes to // `Wrap[T]`, a `Named` whose OWN generics still mention `T`, // not itself a bare type-param). Falling through to // `type_ref_to_c` below recursively mono's `Wrap[T]` using // the UNSUBSTITUTED literal `T` — `NovaValue_Wrap____ // Nova_T_p` — and registers a NovaOpt/NovaRes wrapper struct // for that bogus name (`unknown type name`, CC-FAIL) — same // class of hazard the `generic_type_templates`-gated // `Pair[B,A]` guard below already handles for a BARE generic // return, generalized here to `Option`/`Result`'s FIRST // (Ok/Some) type-arg specifically, RECURSIVELY // (`type_ref_uses_any_type_param`, same helper the `Pair` // guard uses) so a nested `Wrap[T]` is caught too, not just a // direct bare `T`. Scoped to `Option`/`Result` (the two // protocol-conformance-bearing containers actually reported // live) — the erased body is provably unreachable for this // shape (mirrors every other stub-routed case in this // function/`emit_generic_static_method_stub`): the caller- // side mono pass emits the correct concrete instance per // call site. if (name == "Option" || name == "Result") && !generics.is_empty() { let inner_still_generic = generics.first() .map_or(false, |g| Self::type_ref_uses_any_type_param(g, type_params)); if inner_still_generic { if name == "Option" { let c_name = self.sum_schema_registry .lookup_sum_schema("Option") .map(|e| e.c_name.clone()) .unwrap_or_else(|| "NovaOpt_nova_int".into()); return c_name; } // Result[X, E] erased placeholder — a real (never- // dereferenced, per the stub-routing rationale above) // pointer type is always valid C regardless of `X`. return "void*".into(); } } // Plan 48 Ф.3: generic type with type-param args (e.g. Pair[B, A] in erased context) // must NOT be monomorphized — return erased base pointer to avoid spurious instances. // Plan 153.2 Ф.2 (STAGE 2): the param check is RECURSIVE // (`uses_any_type_param`), not just direct-arg. A generic-over- // source adapter return `FilterIter[MapIter[I, U]]` has a DIRECT // arg `MapIter[I,U]` that is not itself a bare param, but it // NESTS the unresolved `I`/`U`. The old shallow check let it fall // through to `type_ref_to_c`, mono'ing it to a placeholder-laden // name (`…____Nova_I_p__Nova_U_p`) whose struct is correctly // suppressed by the drain guard — leaving the erased STUB's // signature referencing an undefined type. Recursing here returns // the erased base pointer `Nova_FilterIter*` for the stub (never // called; the mono call-site emits the concrete instance). if !generics.is_empty() && self.generic_type_templates.contains_key(&name) { let any_param = generics.iter() .any(|g| Self::type_ref_uses_any_type_param(g, type_params)); if any_param { return format!("Nova_{}*", name); } } // Plan 70 Cat B (intentional erasure): erased_type_ref_c is whole-fn // erasure path для emit_generic_* contexts. type_ref_to_c fallback к // nova_int legitimately handles type-params не in subst (mono pending). // Strict-mode здесь = breaking erased dispatch. Documented Cat B. self.type_ref_to_c(ty).unwrap_or_else(|_| "nova_int".into()) } TypeRef::Array(inner, _) => { if let TypeRef::Named { path, .. } = inner.as_ref() { if type_params.contains(&path.join("_")) { return "NovaArray_nova_int*".into(); } } // Plan 70 Cat B (intentional): array element fallback to void* // (preserves pointer-stomping для erased generic arrays). self.type_ref_to_c(ty).unwrap_or_else(|_| "void*".into()) } TypeRef::Unit(_) => "nova_unit".into(), // Plan 70 Cat B (intentional): erased_type_ref_c whole-fn erasure default. _ => self.type_ref_to_c(ty).unwrap_or_else(|_| "nova_int".into()), } } /// Emit a minimal stub body for a method on a generic type. /// Used for static constructors and instance methods with void*-field bodies /// that would generate invalid C. The stub body matches the forward-decl signature. fn emit_generic_static_method_stub(&mut self, f: &FnDecl) -> Result<(), String> { let recv = f.receiver.as_ref().unwrap(); let is_instance = matches!(recv.kind, ReceiverKind::Instance); let type_params: HashSet<String> = recv.generics.iter().filter_map(|tr| { if let TypeRef::Named { path, .. } = tr { path.first().cloned() } else { None } }).collect(); let mangled = self.mangle_fn(f); // Set receiver type so `Self` in return type resolves correctly. let prev_recv = self.current_receiver_type.replace(recv.type_name.clone()); self.sync_receiver_rt(); let ret_c = self.erased_type_ref_c(&f.return_type, &type_params); self.current_receiver_type = prev_recv; self.sync_receiver_rt(); // Plan 128 Ф.1: thread recv.mutable (Ф.2 consumes). let recv_c = self.receiver_c_type(&recv.type_name, recv.mutable); // Match the same signature as the forward declaration in emit_fn_decl let mut parts: Vec<String> = if is_instance { vec![format!("{} nova_self", recv_c)] } else { vec![] }; for p in &f.params { let p_c = self.erased_type_ref_c(&Some(p.ty.clone()), &type_params); parts.push(format!("{} {}", p_c, p.name)); } let params_s = if parts.is_empty() { "void".into() } else { parts.join(", ") }; self.line(&format!("{}{} {}({}) {{", self.top_level_storage(), ret_c, mangled, params_s)); self.indent += 1; if is_instance { self.line("(void)nova_self;"); } for p in &f.params { self.line(&format!("(void){};", p.name)); } if ret_c == "nova_unit" { self.line("return NOVA_UNIT;"); } else if ret_c.ends_with('*') { self.line("return NULL;"); } else { self.line(&format!("return ({}){{0}};", ret_c)); } self.indent -= 1; self.line("}"); self.line(""); Ok(()) } /// Emit a type-erased version of a generic method (instance or static). /// Type params in recv.generics map to void*. fn emit_generic_method_erased(&mut self, f: &FnDecl) -> Result<(), String> { // [race-198 class-closure]: см. override_maps_scope_enter doc. let ovr_saved = self.override_maps_scope_enter(); let r = self.emit_generic_method_erased_scoped_inner(f); self.override_maps_scope_exit(ovr_saved, r.is_ok()); r } fn emit_generic_method_erased_scoped_inner(&mut self, f: &FnDecl) -> Result<(), String> { let recv = f.receiver.as_ref().unwrap(); let is_instance = matches!(recv.kind, ReceiverKind::Instance); let type_params: HashSet<String> = recv.generics.iter().filter_map(|tr| { if let TypeRef::Named { path, .. } = tr { path.first().cloned() } else { None } }).collect(); // If the receiver type has fields that become void* in erased form (Named generic types // with type-param args, same condition as emit_type_decl), the erased body would try // to access sub-fields on void* — generate a stub instead. let has_void_ptr_fields = if let Some(template) = self.generic_type_templates.get(&recv.type_name).cloned() { use crate::ast::TypeDeclKind; if let TypeDeclKind::Record(fields) = &template.kind { fields.iter().any(|fld| { // Plan 131 Ф.3: typed-pointer field `*mut T` / `*T` over a // type param (e.g. `Vec[T] { data *mut T }`). The erased // body lowers `*(@data + @n)` к broken pointer arithmetic on // an erased element type — emit a stub instead; the // caller-side mono pass produces a correct concrete instance // (`Nova_Vec____nova_int` with `nova_int* data`). if matches!(&fld.ty, TypeRef::Pointer(..) | TypeRef::Mut(..) | TypeRef::Uninit(..)) && Self::type_ref_uses_any_type_param(&fld.ty, &type_params) { return true; } // Plan 153.2 gap A: function-typed field over type params // (e.g. lazy-iterator adapters `MapIt[I,T,U] { f fn(T)->U }` // / `BoxIter[T] { step fn()->Option[T] }`). The erased body // lowers `(@f)(x)` to a closure cast through `Nova_U*(*)(...)` // (literal `U`/`T` placeholders) and dispatches `@field.method()` // on the erased self → invalid C. The caller-side mono pass // (drain_generic_type_worklist → emit_monomorphized_method) // produces a correct concrete instance with a populated // current_type_subst, so the erased base is never legitimately // called for these chains — route it to a safe NULL stub. if Self::type_ref_uses_any_type_param(&fld.ty, &type_params) && Self::type_ref_contains_func(&fld.ty) { return true; } // Same condition as emit_type_decl erased field → void* arm (lines 3666-3670) if let TypeRef::Named { path, generics, .. } = &fld.ty { if path.len() == 1 && generics.is_empty() && type_params.contains(&path[0]) { // Plan 162 fix: bare type-param field (e.g. `src I` in // `EnumerateIter[I, T]`). In erased form this becomes `void*`, // and the erased body would call methods on that void* (e.g. // `@src.next()`) and produce wrong return-type casts. // Route to stub — the monomorphized path generates correct code. return true; } path.len() >= 1 && !generics.is_empty() && generics.iter().any(|g| Self::type_ref_uses_any_type_param(g, &type_params)) } else if let TypeRef::Array(inner, _) = &fld.ty { // Plan 56 followup: Array field с generic inner type // (e.g. HashMap.buckets: []Slot[K, V]) — также triggers // erased body issues. Stub safe choice — caller-side // mono pass генерирует proper concrete instance. if let TypeRef::Named { generics, .. } = inner.as_ref() { !generics.is_empty() && generics.iter().any(|g| Self::type_ref_uses_any_type_param(g, &type_params)) } else { false } } else { false } }) } else { false } } else { false }; if has_void_ptr_fields { return self.emit_generic_static_method_stub(f); } // [M-generic-value-self-protocol-wrapper-mono] (221.1 Ф.2 #25, ОКНО-3 // 2026-07-23): a protocol-conformance method whose return type is // `Result[X, E]` / `Option[X]` with `X` STILL mentioning the // receiver's own generic type-param recursively (e.g. `Self` on // `Wrap[T] value {...}` substituting to `Wrap[T]`) — same family as // `has_void_ptr_fields` above (erased body provably unreachable for // a still-generic value-kind receiver; the caller-side mono pass // emits the correct concrete instance per call site). Route to the // stub — `erased_type_ref_c` (just fixed alongside this) now returns // a syntactically-safe placeholder for this exact shape instead of // falling through to a bogus bare-typevar mono registration, but the // FULL erased body (this function, not the stub) would still try to // construct/return a REAL Result/Option value into that placeholder // type — invalid. The stub skips body construction entirely (zeroed // dummy return), matching this file's established pattern. let ret_wraps_still_generic_inner = f.return_type.as_ref().map_or(false, |rt| { if let TypeRef::Named { path, generics: rt_generics, .. } = rt { matches!(path.last().map(String::as_str), Some("Result") | Some("Option")) && rt_generics.first().map_or(false, |inner| Self::type_ref_uses_any_type_param(inner, &type_params)) } else { false } }); if ret_wraps_still_generic_inner { return self.emit_generic_static_method_stub(f); } // D109: Methods whose params use bare type params (e.g. find_slot(key K)) generate // broken erased code when the body calls methods on those params (key.hash(), // key.eq()). Stub only when the receiver type has Array fields with type-param // element types (collection types like HashMap). Simple generic types (Result2, // Option, Wrapper) just pass/return bare type-param params and work correctly in // erased form — their erased body compiles to valid C. let has_type_param_params = f.params.iter().any(|p| { if let TypeRef::Named { path, generics, .. } = &p.ty { generics.is_empty() && path.len() == 1 && type_params.contains(&path[0]) } else { false } }); let has_array_fields_with_type_params = if has_type_param_params { if let Some(template) = self.generic_type_templates.get(&recv.type_name).cloned() { use crate::ast::TypeDeclKind; if let TypeDeclKind::Record(fields) = &template.kind { fields.iter().any(|fld| { if let TypeRef::Array(inner, _) = &fld.ty { if let TypeRef::Named { generics, .. } = inner.as_ref() { !generics.is_empty() && generics.iter().any(|g| { Self::type_ref_uses_any_type_param(g, &type_params) }) } else { false } } else { false } }) } else { false } } else { false } } else { false }; if has_type_param_params && has_array_fields_with_type_params { return self.emit_generic_static_method_stub(f); } let mangled = self.mangle_fn(f); // Plan 11 Follow-up (2026-05-17): keep current_receiver_type set до конца // emit (включая param iteration), чтобы `Self` в param-position // (e.g. `other Self`) тоже резолвилось правильно. let prev_recv = self.current_receiver_type.replace(recv.type_name.clone()); self.sync_receiver_rt(); let ret_c = self.erased_type_ref_c(&f.return_type, &type_params); // Plan 128 Ф.1: thread recv.mutable (Ф.2 consumes). let recv_c = self.receiver_c_type(&recv.type_name, recv.mutable); // Static methods don't get nova_self; instance methods do. let mut parts: Vec<String> = if is_instance { vec![format!("{} nova_self", recv_c)] } else { vec![] }; for p in &f.params { let p_c = self.erased_type_ref_c(&Some(p.ty.clone()), &type_params); parts.push(format!("{} {}", p_c, p.name)); } // Plan 11 Follow-up (2026-05-17): НЕ восстанавливаем current_receiver_type // здесь — оставляем set, чтобы body emit (включая var_types для params // на line ~5089 и match-expr inferral'ы) видел правильный recv для Self. // Restored в конце body emit (line ~5165: current_receiver_type = None). let _ = prev_recv; let params_s = if parts.is_empty() { "void".into() } else { parts.join(", ") }; // Plan 47: буферизуем тело — см. emit_generic_fn_erased (тот же баг: // spawn в generic-методе → ctx-typedef после использования). let saved_out = std::mem::take(&mut self.out); let saved_indent = self.indent; self.indent = 0; self.line(&format!("{}{} {}({}) {{", self.top_level_storage(), ret_c, mangled, params_s)); self.indent += 1; // Plan 143.2: prologue safepoint. Erased generic method body inherits // the SOURCE template (`f`) KEEP-status — a recursive generic method // keeps a safepoint in its erased form too. self.emit_prologue_preempt_check(f); // Register nova_self only for instance methods let mut saved_array_elem_keys: Vec<String> = Vec::new(); if is_instance { self.var_types.insert("nova_self".into(), recv_c.clone()); // Pre-populate array_element_types for array fields of the receiver type // so that @field[idx] emits the right cast in the erased body. // Key: the C expression "(nova_self->{field})" that emit_expr(Member) produces. if let Some(template) = self.generic_type_templates.get(&recv.type_name).cloned() { use crate::ast::TypeDeclKind; if let TypeDeclKind::Record(fields) = &template.kind { for fld in fields { if let crate::ast::TypeRef::Array(elem_ty, _) = &fld.ty { let elem_c = self.erased_type_ref_c(&Some(*elem_ty.clone()), &type_params); // Only register if the element type is a pointer (sum types, records). // nova_int elements don't need a cast. if elem_c.ends_with('*') && elem_c != "nova_int*" { let field_c = Self::mangle_field_name(&fld.name); // Use the same C-expression format that emit_expr(Member) produces. let key = format!("(nova_self->{})", field_c); self.array_element_types.insert(key.clone(), elem_c); saved_array_elem_keys.push(key); } } } } } } let saved: Vec<(String, Option<String>)> = f.params.iter().map(|p| { let p_c = self.erased_type_ref_c(&Some(p.ty.clone()), &type_params); (p.name.clone(), self.var_types.insert(p.name.clone(), p_c)) }).collect(); // Register fn-typed param signatures so that calls like `pred(h)` inside the erased // body know the return type (e.g. `bool` for `fn(T) -> bool` — without this, the // call infers `nova_int` by default and an `if pred(h)` body fails the strict-bool check. let saved_fn_sigs: Vec<(String, Option<(Vec<String>, String)>)> = f.params.iter() .filter_map(|p| { if let Some(_ft) = self.resolve_fn_typeref(&p.ty) { let TypeRef::Func { params: fp, return_type, .. } = &_ft else { unreachable!() }; let erase_unk = |c: String| -> String { self.debt_erase_unknown_nova_ptr(c) }; // Plan 70 Cat B (intentional erasure): erase_unk нормализует // unknown→nova_int для consistent pointer-stomping в emit_generic_* // (erased generics). type_ref_to_c fail здесь = type-param ещё не // mono'd, erase_unk применяет к "nova_int" → tracks эту erasure. // Strict-mode false-positive — это namespace squat для erased dispatch. let param_c_tys: Vec<String> = fp.iter() .map(|t| erase_unk(self.type_ref_to_c(t).unwrap_or_else(|_| "nova_int".into()))) .collect(); let ret_c = match return_type { Some(rt) => erase_unk(self.type_ref_to_c(rt).unwrap_or_else(|_| "nova_int".into())), None => "nova_unit".into(), }; let prev = self.fn_param_sigs.insert(p.name.clone(), (param_c_tys, ret_c)); Some((p.name.clone(), prev)) } else { None } }) .collect(); // [fix M-nested-fn-newtype-bind-then-call-broken, реестр 221.1 №78, // форма 2]: same sibling addition as the other `resolve_fn_typeref` // param-registration sites — see `nested_fn_return_sig` doc. let saved_fn_returns_sigs: Vec<(String, Option<(Vec<String>, String)>)> = f.params.iter() .filter_map(|p| { let _ft = self.resolve_fn_typeref(&p.ty)?; let sig = self.nested_fn_return_sig(&_ft)?; let prev = self.fn_returns_fn_sig.insert(p.name.clone(), sig); Some((p.name.clone(), prev)) }) .collect(); self.current_receiver_type = Some(recv.type_name.clone()); self.sync_receiver_rt(); // Emit body let saved_expected = self.expected_record_type.clone(); self.expected_record_type = Self::debt_struct_name_from_c_type(&ret_c); match &f.body { FnBody::Expr(e) => { self.emit_source_annotation_for_expr(e); let val = self.emit_expr(e)?; if ret_c == "nova_unit" { self.line(&format!("{};", val)); self.line("return NOVA_UNIT;"); } else { self.line(&format!("return {};", val)); } } FnBody::Block(block) => { self.emit_block_stmts(block, &ret_c)?; } // D82: external fn — body заэмичен в nova_rt; здесь игнорируем // (вызов будет диспатчен через external dispatch table). FnBody::External => {} } self.expected_record_type = saved_expected; // Restore params for (name, prev) in saved { match prev { Some(old) => { self.var_types.insert(name, old); } None => { self.var_types.remove(&name); } } } // Restore fn-typed param sigs for (name, prev) in saved_fn_sigs { match prev { Some(old) => { self.fn_param_sigs.insert(name, old); } None => { self.fn_param_sigs.remove(&name); } } } // [fix M-nested-fn-newtype-bind-then-call-broken, №78] restore paired // with the `nested_fn_return_sig` save loop above. for (name, prev) in saved_fn_returns_sigs { match prev { Some(old) => { self.fn_returns_fn_sig.insert(name, old); } None => { self.fn_returns_fn_sig.remove(&name); } } } if is_instance { self.var_types.remove("nova_self"); } // Restore array_element_types entries added for this erased body for key in &saved_array_elem_keys { self.array_element_types.remove(key); } self.current_receiver_type = None; self.sync_receiver_rt(); self.flush_boxed_vars(); self.indent -= 1; self.line("}"); self.line(""); // Plan 47: restore + flush spawn-ctx typedefs / lambda impls before body. let fn_body = std::mem::replace(&mut self.out, saved_out); self.indent = saved_indent; if !self.lambda_forward_decls.is_empty() { self.out.push_str(&std::mem::take(&mut self.lambda_forward_decls)); } if !self.lambda_impls.is_empty() { self.out.push_str(&std::mem::take(&mut self.lambda_impls)); } self.out.push_str(&fn_body); Ok(()) } // ---- Plan 48: monomorphization helpers ---- /// Plan 48: sanitize a C type string to a valid C identifier component. /// "nova_int" → "nova_int", "Nova_Box*" → "Nova_Box_p", "NovaArray_nova_int*" → "NovaArray_nova_int_p" fn sanitize_c_for_ident(c_type: &str) -> String { c_type .replace("* ", "_p_") .replace('*', "_p") .replace(' ', "_") .replace('[', "_arr_") .replace(']', "") .replace('-', "_") } /// Plan 59: partial inverse of `sanitize_c_for_ident` — restores `*` /// from `_p` suffix. Idempotent для не-pointer types (`nova_int`). /// Используется при decode'е element types из length-prefixed mangle. fn desanitize_c_from_ident(s: &str) -> String { if let Some(stem) = s.strip_suffix("_p") { format!("{}*", stem) } else { s.to_string() } } /// Plan 59 Phase 5: parse mono'd tuple mangled name → element C types. /// Length-prefixed format: `_NovaTuple_<arity>_<L1>_<T1>_<L2>_<T2>...` /// — каждое `<Ln>` десятичная длина следующего element name. Это /// unambiguous для **любой** глубины nesting (раньше split-by-`__` /// ломался когда element сам — `_NovaTuple____...`). /// Returns None если name не валидный mono'd tuple format (legacy /// `_NovaTupleN` тоже не matches — distinguishable по `_` после /// `NovaTuple`). fn parse_mono_tuple_elements(mangled: &str) -> Option<Vec<String>> { let rest = mangled.strip_prefix("_NovaTuple_")?; // Arity prefix. let (arity_str, after_arity) = Self::take_digits(rest); if arity_str.is_empty() { return None; } let arity: usize = arity_str.parse().ok()?; let mut cursor = after_arity; let mut elems: Vec<String> = Vec::with_capacity(arity); for _ in 0..arity { cursor = cursor.strip_prefix('_')?; let (len_str, after_len) = Self::take_digits(cursor); if len_str.is_empty() { return None; } let len: usize = len_str.parse().ok()?; let body = after_len.strip_prefix('_')?; if body.len() < len { return None; } let raw_elem = &body[..len]; elems.push(Self::desanitize_c_from_ident(raw_elem)); cursor = &body[len..]; } if !cursor.is_empty() { return None; } Some(elems) } /// Plan 63 Fix F+ [M-result-erased-no-mono]: try to determine the boxed /// inner C type для Result Ok payload of an expression. Production-grade /// closure of Fix F's gap — supports propagation через function calls /// (where boxing happens inside callee), not just direct boxing at let /// scope. Returns Some("_NovaTuple_..._mono*") or struct-pointer string /// when expr's static type is Result[T, E] with T such что it gets /// boxed (Tuple, struct). None otherwise (legacy nova_int slot path). fn try_get_result_ok_inner_type_for_expr(&self, expr: &crate::ast::Expr) -> Option<String> { use crate::ast::ExprKind; match &expr.kind { ExprKind::Ident(name) => { self.result_ok_inner_types.get(name.as_str()).cloned() } ExprKind::Call { func, .. } => { let fn_c_name = self.debt_call_target_c_name(func); self.fn_result_ok_inner_types.get(&fn_c_name).cloned() } ExprKind::Block(b) => { b.trailing.as_ref().and_then(|t| self.try_get_result_ok_inner_type_for_expr(t)) } _ => None, } } /// [Facet-B D307 §1/§3] True when the CALLING file (`caller_fid`) itself /// declares a `priv(file)` CONCRETE (non-generic) overload of `name` — /// per D84, a file-visible concrete overload beats a same-named generic /// that happens to live (as `priv(file)`) in an unrelated peer file. /// Consulted BEFORE routing a call to `name` through the bare-name /// (file-oblivious) `generic_fns`/`mono_fn_decls` generic-mono /// machinery, which would otherwise hijack EVERY call to `name` in the /// whole folder-CU the moment ANY peer file declares a same-named /// `priv(file)` generic (byte-identical when no such collision exists). fn facetb_file_local_concrete_overload(&self, caller_fid: crate::diag::FileId, name: &str) -> bool { self.file_priv_free_fn_decls .get(&(caller_fid, name.to_string())) .map(|d| d.generics.is_empty()) .unwrap_or(false) } /// [Facet-B D307 §1/§3] The FnDecl to actually monomorphize for a same- /// named generic call: prefer the CALLER's own file-local `priv(file)` /// generic (the only `priv(file)` decl a caller may legally reference) /// over the bare-name `mono_fn_decls` entry, which is last-registration- /// wins across ALL peer files and can silently hand back an UNRELATED /// peer's same-named generic FnDecl (wrong body / wrong declaring file /// for mono-name purposes). Falls back to the legacy global lookup when /// the caller's file has no local candidate — the normal cross-file /// exported-generic case, untouched (byte-identical). fn facetb_mono_fn_decl_for_call( &self, caller_fid: crate::diag::FileId, name: &str, ) -> Option<crate::ast::FnDecl> { self.file_priv_free_fn_decls .get(&(caller_fid, name.to_string())) .filter(|d| !d.generics.is_empty()) .cloned() .or_else(|| self.mono_fn_decls.get(name).cloned()) } /// Plan 63 Fix F+: get C-name of callee for fn_result_ok_inner_types lookup. /// Plan 81 Ф.6: C-имя символа пользовательской свободной функции. /// /// Единая точка построения имени — Ф.6.1 централизует ~15 /// разбросанных `format!("nova_fn_{}", ...)`; Ф.6.2 заменит тело на /// mangling с путём модуля (`nova_<modpath>_<name>`). Перегрузки /// добавляют param-type-суффикс на стороне `method_overloads`; /// синтетика (`nova_fn_main_impl`, closure-адаптеры `nova_fn_vi` /// и т.п.) сюда **не** идёт — она exempt. fn free_fn_c_name(&self, name: &str) -> String { // Plan 91.12 Ф.-1 (D282): `extern "C" fn` — literal C name, no prefix. // Check FIRST: c_literal_extern_fns takes priority over registry and module map. if self.c_literal_extern_fns.contains(name) { return name.to_string(); } // Plan 170 (D307): file-private free fn — resolve to the file-discriminated // C symbol for the CURRENT emission file. Checked before fn_module_map so a // file-private helper shadows a same-named module symbol within its own file. // (current_emit_file_id is set during the function-definition emit and during // call-site lowering; both share the declaring file for a file-local helper.) if let Some(fid) = self.current_emit_file_id { if let Some(mangled) = self.file_priv_fn_c_names.get(&(fid, name.to_string())) { return mangled.clone(); } } // Plan 103.1 Ф.6: ExternalRegistry builtins (fence, etc.) always // use nova_fn_<name> — they live in nova_rt/*.h, not user modules. // Check BEFORE fn_module_map to prevent mangled names when test // files import std.runtime.sync (which would otherwise add fence // to fn_module_map with path ["runtime","sync"]). let ext_free_key = (String::new(), name.to_string()); if self.external_registry.by_key.contains_key(&ext_free_key) { return format!("nova_fn_{}", name); } match self.fn_module_map.get(name) { Some(modpath) if !modpath.is_empty() => { Self::mangle_free_fn(modpath, name) } // Не пользовательская функция (runtime/builtin/синтетика) // либо peer_files не заполнены — legacy-имя (без коллизий // в single-crate bootstrap, см. D134). _ => format!("nova_fn_{}", name), } } /// Plan 81 Ф.6.2 (D134): mangled C-имя свободной функции — /// `nova_fn_` + length-prefixed сегменты пути модуля + length-prefixed /// имя функции. Length-prefix однозначен: Nova-идентификаторы не /// начинаются с цифры, поэтому граница «число длины ↔ идентификатор» /// чёткая. Префикс `nova_fn_` зарезервирован — не коллидирует с /// runtime-символами (`nova_str_*`, `nova_int_*`, ...). fn mangle_free_fn(modpath: &[String], name: &str) -> String { let mut s = String::from("nova_fn_"); for seg in modpath { s.push_str(&seg.len().to_string()); s.push_str(seg); } s.push_str(&name.len().to_string()); s.push_str(name); // Лимит длины C-идентификатора: при превышении — усечение + // hash-суффикс (сохраняет уникальность). if s.len() > 240 { let h = Self::ident_hash(&s); let head: String = s.chars().take(216).collect(); format!("{}_h{:08x}", head, h) } else { s } } /// FNV-1a 32-bit — hash-суффикс для усечённых mangled-имён. fn ident_hash(s: &str) -> u32 { let mut h: u32 = 2166136261; for b in s.bytes() { h ^= b as u32; h = h.wrapping_mul(16777619); } h } /// Plan 103.1 Ф.3: Convert a Nova MemOrdering literal expression to the /// corresponding `__ATOMIC_*` C constant string. /// /// Used in Plan 103.2+ for `atomic.load(MemOrdering.X)` / `atomic.store(v, MemOrdering.X)` /// codegen — emits the literal constant directly instead of dispatching through /// the `nova_fn_fence` switch. Returns None for runtime-value orderings (requires /// fallback to the switch-dispatch path or a runtime assert). /// /// Tag values (coordinated with sync_primitives.h NOVA_TAG_MemOrdering_*): /// Relaxed=0 → __ATOMIC_RELAXED /// Acquire=1 → __ATOMIC_ACQUIRE /// Release=2 → __ATOMIC_RELEASE /// AcqRel=3 → __ATOMIC_ACQ_REL /// SeqCst=4 → __ATOMIC_SEQ_CST pub fn nova_mem_ordering_to_atomic(ord_expr: &crate::ast::Expr) -> Option<&'static str> { use crate::ast::ExprKind; // MemOrdering.Variant is parsed as Path(["MemOrdering", "Variant"]). if let ExprKind::Path(parts) = &ord_expr.kind { if parts.len() == 2 && parts[0] == "MemOrdering" { return match parts[1].as_str() { "Relaxed" => Some("__ATOMIC_RELAXED"), "Acquire" => Some("__ATOMIC_ACQUIRE"), "Release" => Some("__ATOMIC_RELEASE"), "AcqRel" => Some("__ATOMIC_ACQ_REL"), "SeqCst" => Some("__ATOMIC_SEQ_CST"), _ => None, }; } } // Runtime value (variable / call result) — caller handles fallback. None } /// Mirrors `emit_call`'s callee name resolution для Ident/Path/Member cases. fn debt_call_target_c_name(&self, func: &crate::ast::Expr) -> String { use crate::ast::ExprKind; match &func.kind { ExprKind::Ident(name) => self.free_fn_c_name(name), ExprKind::Path(parts) if !parts.is_empty() => { // Static-method-style `Type.method` → "Nova_Type_method_<method>" // (Plan 11: instance methods all use _method_ mangling). if parts.len() == 2 { format!("Nova_{}_method_{}", parts[0], parts[1]) } else { self.free_fn_c_name(&parts.join("_")) } } ExprKind::Member { obj, name } => { // Instance method-call `<obj>.method(...)` — receiver type // resolves через var_types. Mangled name follows // `Nova_<RecvType>_method_<name>` convention. let obj_ty = match &obj.kind { ExprKind::Ident(n) => self.var_types.get(n.as_str()).cloned(), _ => None, }; if let Some(ty) = obj_ty { // Strip leading "Nova_" prefix and trailing pointer/`*`. let core = ty.trim_end_matches('*') .trim_start_matches("Nova_") .to_string(); format!("Nova_{}_method_{}", core, name) } else { String::new() } } _ => String::new(), } } /// Plan 72 P2-A: derive the `fn_result_type_params` lookup key for a call's /// callee expression. `Ident` → free-fn name; `Path[T, m]` or /// `Member{Type, m}` (Type is a known record/sum) → `Type.method` static /// form. Returns `None` for forms not tracked (instance-method calls etc.). /// /// Plan 196.3 (result-repr triple) verdict: RETAINED-AS-LEGACY-FALLBACK. /// Both current callers (`infer_result_type_params` Call-arm and the /// `let r = f(...)` decl-arm, both in this file) now try /// `channel_result_type_params_c` FIRST and only reach this name-keyed /// deriver on a channel miss — free-fn/static-method calls whose /// checker-resolved return isn't (yet) a concrete `Named{Result,[T,E]}` /// in `resolved_types` (Stage-1a/1b don't cover every producer). The /// function itself is a pure AST-shape→String mapper with no inferred /// type to migrate — nothing left to convert here; it's the fallback /// key, not a re-derivation of something the channel already knows. fn call_result_type_params_key(&self, func: &crate::ast::Expr) -> Option<String> { use crate::ast::ExprKind; match &func.kind { ExprKind::Ident(name) => Some(name.clone()), ExprKind::Path(parts) if parts.len() == 2 => { Some(format!("{}.{}", parts[0], parts[1])) } ExprKind::Member { obj, name } => { if let ExprKind::Ident(t) = &obj.kind { if self.record_schemas.contains_key(t.as_str()) || self.sum_schemas.contains_key(t.as_str()) { return Some(format!("{}.{}", t, name)); } } None } _ => None, } } /// Plan 72 P3-B [M-protocol-param-free-fn-only]: derive the /// `fn_protocol_params` key for a call's callee. Free fn (`Ident`) → name; /// static method (`Path[T,m]` / `Member{Type,m}`) → `Type.method`; /// instance method (`Member{var,m}`) → `<RecvType>.method` resolved via /// the receiver's inferred type. Covers free fns AND methods uniformly. fn debt_call_protocol_params_key(&self, func: &crate::ast::Expr) -> Option<String> { use crate::ast::ExprKind; match &func.kind { ExprKind::Ident(name) => Some(name.clone()), ExprKind::Path(parts) if parts.len() == 2 => { Some(format!("{}.{}", parts[0], parts[1])) } ExprKind::Member { obj, name } => { if let ExprKind::Ident(t) = &obj.kind { if self.record_schemas.contains_key(t.as_str()) || self.sum_schemas.contains_key(t.as_str()) { // Static-method form `Type.method`. return Some(format!("{}.{}", t, name)); } } // Instance call `obj.method(...)` — resolve the receiver type // (`Nova_Foo*` → `Foo`) so the key matches the `Type.method` // registration form. // [M-172.1-d174-sync-consume-registry] phase-safety: Ident-ресивер // без типа (модульный путь `runtime.init(2)` — модуль не значение; // либо pre-pass до регистрации локала) → этот protocol-key // side-channel неприменим (None), не re-derive через P67-пробу. let recv_c = match &obj.kind { ExprKind::Ident(n) => self .var_types .get(n) .cloned() .or_else(|| { if obj.id.is_set() { self.resolved_types .get(&obj.id) .and_then(|rt| self.resolved_type_to_c(rt).ok()) } else { None } })?, _ => self.infer_expr_c_type(obj), }; let recv = recv_c.trim_end_matches('*').trim() .strip_prefix("Nova_")?; if recv.is_empty() { return None; } Some(format!("{}.{}", recv, name)) } _ => None, } } /// Plan 196.3 wave-2 (D30/D85, one-window): channel-first `(ok_c, err_c)` /// for a Call-expr whose checker-resolved return type is `Result[T,E]`. /// Reads `resolved_types[expr.id]` — 196.4 Stage-1a extended /// `resolve_return_channel` to bind method-level generics from call /// arguments (e.g. `some_option.ok_or(e) -> Result[T,E]` where `T` comes /// from the `Option[T]` receiver), so a call whose Result payload depends /// on generic instantiation now lands in the channel as a CONCRETE /// `Named{Result,[T,E]}` instead of a residual `TypeParam` — and lowers /// both args through the SINGLE checker→C path (`resolved_type_to_c`), /// mirroring the `channel_array_elem_c` (D239) precedent, instead of /// re-deriving `(ok_c, err_c)` from the codegen-local /// `fn_result_type_params` name-keyed registry (populated at `emit_fn` /// from the callee's DECLARED — possibly still-generic — `f.return_type` /// AST, blind to the call site's own generic instantiation). `None` = /// channel miss (call un-annotated, or the resolved type isn't /// `Named{Result,[T,E]}`, or a leg is still an unsubstituted generic stub) /// → caller falls back to `call_result_type_params_key`/ /// `fn_result_type_params` (legacy name-keyed path — still needed since /// Stage-1a/1b don't cover every producer yet, e.g. free-fn generic /// returns land here too until Stage-1b). fn channel_result_type_params_c(&self, expr: &crate::ast::Expr) -> Option<(String, String)> { if !expr.id.is_set() { return None; } match self.resolved_types.get(&expr.id) { Some(crate::types::ResolvedType::Named { name, args, .. }) if name == "Result" && args.len() == 2 => { let ok_c = self.resolved_type_to_c(&args[0]).ok()?; let err_c = self.resolved_type_to_c(&args[1]).ok()?; if self.debt_is_generic_stub_c(&ok_c) || self.debt_is_generic_stub_c(&err_c) { return None; } Some((ok_c, err_c)) } _ => None, } } /// Plan 59 Ф.7.5-lite: выводит `(ok_c, err_c)` для произвольного /// Result-выражения, включая **inline** (без let-биндинга) — закрывает /// блокер `[M-result-method-named-var-only]`. /// /// - `Ident` → `result_type_params` (tracked named var); /// - `Call(fn)` → channel-first `channel_result_type_params_c` (196.3 /// wave-2 D30/D85); fallback fn объявлена с return `Result[T,E]` → /// `fn_result_type_params` (инфра Plan 72 P2-A); /// - `.map`/`.map_err` цепочка → рекурсия по receiver'у + closure /// return-type (`typed_closure_c_sig`). /// /// `None` — тип не выводится (caller фоллбэчится на `(nova_int, nova_str)`). /// /// Plan 196.3 Q9 (D16/D53/D72 inventory; this fn's own D30/D85 Result-channel /// twin): wraps [`Self::infer_result_type_params_legacy`] with a byte-identity /// guarded read of [`Self::infer_result_type_params_channel`] (checker /// `resolved_types[expr.id]`, Stage-1a `resolve_return_channel` 196.4). Legacy /// wins on disagreement (safety — mirrors `subst_map_adopt_rt`'s guard); the /// channel ONLY fills a gap the legacy local-registry inference missed. The /// channel structurally CANNOT cover every case: the checker's per-`Call` /// annotation (`types/mod.rs` `f1_expr_inner`, `ExprKind::Call` arm) writes /// `resolved_types_buf[e.id]` only for method-level-generic method calls /// (`infer_method_call_channel_type`), TurboFish static ctors, `size_of`/ /// `align_of`, and `Some(x)` — Result ctors (`Ok`/`Err`) and free-fn generic /// calls with a declared `-> Result[T,E]` are explicitly NOT channeled /// (`Result needs BOTH Ok+Err → contextual`, `types/mod.rs` ~7910) — a real, /// current architectural gap (Tier-2 per the 196.3 Q5 finding), not a bug /// here. So the legacy Ident/Call/`.map`/`.map_err` inference below remains /// load-bearing for the OVERWHELMING majority of call sites. fn infer_result_type_params(&self, expr: &crate::ast::Expr) -> Option<(String, String)> { let legacy = self.infer_result_type_params_legacy(expr); match (&legacy, self.infer_result_type_params_channel(expr)) { (Some(_), _) => legacy, (None, Some(c)) => Some(c), (None, None) => None, } } /// Plan 196.3 Q9: channel-first half of [`Self::infer_result_type_params`] — /// reads `resolved_types[expr.id]` (checker channel, D315) and, when it /// resolves to a concrete `Result[Ok,Err]` (peeling any view wrapper), lowers /// both arms via [`Self::resolved_type_to_c`]. `None` on channel-miss OR a /// non-`Result`/residual (`TypeParam`/`Any`/`Raw`) shape — the caller then /// relies purely on the legacy local-registry inference. fn infer_result_type_params_channel(&self, expr: &crate::ast::Expr) -> Option<(String, String)> { use crate::types::ResolvedType as R; let rt = self.channel_arg_rt(expr)?; let rt = rt.peel_view(); let R::Named { name, args, .. } = &rt else { return None }; if name != "Result" || args.len() != 2 { return None; } let ok = self.resolved_type_to_c(&args[0]).ok()?; let err = self.resolved_type_to_c(&args[1]).ok()?; if ok.is_empty() || err.is_empty() || ok == "void*" || err == "void*" { return None; } Some((ok, err)) } fn infer_result_type_params_legacy(&self, expr: &crate::ast::Expr) -> Option<(String, String)> { use crate::ast::ExprKind; match &expr.kind { ExprKind::TurboFish { base, .. } => self.infer_result_type_params(base), ExprKind::Ident(v) => self.result_type_params.get(v.as_str()).cloned(), ExprKind::Call { func, args, .. } => { // Channel-first (196.3 wave-2, D30/D85): the checker may have // already materialized this call's CONCRETE Result[T,E] // return — including method-level-generic instantiations // (e.g. `opt.ok_or(e)`) that 196.4 Stage-1a now resolves — // into `resolved_types[expr.id]`. Prefer it over the // name-keyed AST re-derivation below. if let Some(p) = self.channel_result_type_params_c(expr) { return Some(p); } // fn-call с declared return `Result[T,E]` (legacy name-keyed // fallback — still reached for producers Stage-1a/1b don't // cover yet). if let Some(k) = self.call_result_type_params_key(func) { if let Some(p) = self.fn_result_type_params.get(&k) { return Some(p.clone()); } } // Result method-chain: obj.map(f) → Result[U,E]; // obj.map_err(f) → Result[T,F]. if let ExprKind::Member { obj, name } = &func.kind { match name.as_str() { "map" => { let (ok, err) = self.infer_result_type_params(obj)?; let u = args.first() .and_then(|a| self.typed_closure_c_sig(a.expr())) .map(|(_, ret)| ret) .unwrap_or(ok); return Some((u, err)); } "map_err" => { let (ok, err) = self.infer_result_type_params(obj)?; let f = args.first() .and_then(|a| self.typed_closure_c_sig(a.expr())) .map(|(_, ret)| ret) .unwrap_or(err); return Some((ok, f)); } _ => {} } } None } _ => None, } } /// Plan 63 Fix F+: register fn's Result Ok payload boxed mono'd type /// при emit_fn. Если return type — `Result[Tuple(<concrete>), E]` или /// `Result[<user_struct>, E]` (boxed-as-pointer), populate /// fn_result_ok_inner_types[fn_c_name] = "<mono_ty>*". Subsequent /// `try_get_result_ok_inner_type_for_expr` для Call-к-этой-fn returns /// Some(...) → пропускается в `result_ok_inner_types` для destructure. fn register_fn_result_ok_inner_type(&mut self, fn_c_name: &str, ret_type: &crate::ast::TypeRef) { use crate::ast::TypeRef; if let TypeRef::Named { path, generics, .. } = ret_type { let is_result = path.last().map(|s| s.as_str()) == Some("Result") && generics.len() == 2; if !is_result { return; } let ok_ty = &generics[0]; // Resolve Tuple — все элементы concrete (no placeholders). if let TypeRef::Tuple(elems, _) = ok_ty { if elems.is_empty() { return; } let mut elem_cs: Vec<String> = Vec::with_capacity(elems.len()); for e in elems { if let Some(c) = Self::apply_type_subst_to_ref(e, &[]) { elem_cs.push(c); } else { return; // unresolved } } let mono = Self::compute_mono_tuple_c_name(&elem_cs); self.register_mono_tuple(&elem_cs); self.fn_result_ok_inner_types .insert(fn_c_name.to_string(), format!("{}*", mono)); } // (Struct user-types в Ok могут быть добавлены аналогично; Fix F // pending mechanism уже handles direct cases, registry — для // function-return propagation.) } } /// Plan 59 Phase 5: peel leading decimal digits. fn take_digits(s: &str) -> (&str, &str) { let split_at = s.bytes().take_while(|b| b.is_ascii_digit()).count(); (&s[..split_at], &s[split_at..]) } /// Plan 59: detect mono'd tuple return-type mismatch. Returns true if /// `ret_ty` and `actual_ty` are both mono'd `_NovaTuple_<arity>_...` /// names but different (element types disagree). Such struct-to-struct /// assignment fails C type-check; caller emits field-wise copy instead. /// `_NovaTuple_` prefix (с `_`) различает new mono'd format от legacy /// `_NovaTuple1`/`_NovaTuple2`/... (без `_`). fn needs_tuple_field_copy(ret_ty: &str, actual_ty: &str) -> bool { ret_ty.starts_with("_NovaTuple_") && actual_ty.starts_with("_NovaTuple_") && ret_ty != actual_ty } /// Plan 59: emit field-wise copy for mono'd tuple return value. Если /// types match — simple `T tmp = val;`. Если mismatch (mono'd tuples /// of same arity, different element types) — emit per-field assignment /// with cast: `T tmp; tmp.f0 = (E1)val.f0; ...`. Bit-compatible value /// types (nova_int, void*, fn pointer) round-trip transparently. fn emit_tuple_return_stash(&mut self, ret_ty: &str, tmp: &str, val: &str, actual_ty: &str) { if !Self::needs_tuple_field_copy(ret_ty, actual_ty) { self.line(&format!("{} {} = {};", ret_ty, tmp, val)); return; } if let Some(elem_tys) = Self::parse_mono_tuple_elements(ret_ty) { self.line(&format!("{} {};", ret_ty, tmp)); for (i, ety) in elem_tys.iter().enumerate() { self.line(&format!("{}.f{} = ({}){}.f{};", tmp, i, ety, val, i)); } } else { self.line(&format!("{} {} = {};", ret_ty, tmp, val)); } } /// Plan 141: structural equality field-by-field, dispatched **by C-type**. /// Returns a C boolean expression that is true iff the two operands (given /// as already-emitted C l-value/expression strings `l` and `r`) are equal. /// /// Replaces the prior `memcmp`-based tuple equality (unsound for float /// `-0.0`/`NaN`, struct padding, nested composites) and extends the /// str-only sum-payload dispatch. /// /// Dispatch: /// - scalar / int-like / float (`nova_int`/`bool`/`byte`/`char`/sized /// ints/`nova_f32`/`nova_f64`) → `(l == r)`. For float this is the /// correct IEEE comparison (`-0.0 == +0.0`, `NaN != NaN`), unlike /// `memcmp`. /// - raw pointer (`void*`/`nova_ptr` and other `*`-suffixed C pointers that /// are NOT a single `Nova_X*` value) → `(l == r)` (identity is the only /// meaningful comparison for an opaque pointer). /// - `nova_str` → `nova_str_eq(l, r)`. /// - mono'd tuple `_NovaTuple_<arity>_...` → recursive field-by-field over /// the parsed element C-types on `(l).f{i}` / `(r).f{i}`, joined with /// `&&`. /// - legacy `_NovaTupleN` (arities 1..=8, all `nova_int` slots) → per-slot /// `.f{i} == .f{i}` (all slots scalar — sound). /// - single `Nova_X*` value (record or sum): structural — reuse the /// user/derived `@equal`/`@eq` method if present, else `@compare == 0`, /// else recurse over the sum tag + payload via `sum_schemas`. Never a /// pointer `==` (that would compare identity, not structure). /// /// `depth` guards against unbounded recursion on (unsupported) cyclic /// composite schemas — see R2 in the plan (record-cycle eq is out-of-scope /// for V1). On hitting the cap we fall back to `(l == r)` so codegen always /// terminates and produces *some* expression; non-cyclic schemas are finite /// and never reach the cap. fn emit_field_eq(&self, c_type: &str, l: &str, r: &str, depth: usize) -> String { const MAX_EQ_DEPTH: usize = 32; let cty = c_type.trim(); // Recursion guard (R2): cyclic record schema → bail to identity. if depth >= MAX_EQ_DEPTH { return format!("(({}) == ({}))", l, r); } // nova_str — struct {ptr,len}; needs the runtime helper. if cty == "nova_str" { return format!("nova_str_eq({}, {})", l, r); } // nova_unit / void / empty — vacuously equal. Unit carries no data and // its C type is a zero/opaque struct, so `==` on it is invalid C // (regression: Set[T] = HashMap[T, ()] payload eq hit this). All unit // values are equal: evaluate both operands (avoid unused) and yield 1. if cty == "nova_unit" || cty == "void" || cty.is_empty() { return format!("((void)({}), (void)({}), 1)", l, r); } // Scalar / int-like / float → C `==`. Float `==` is the intended IEEE // semantics (per the plan); do NOT memcmp. if matches!( cty, "nova_int" | "nova_bool" | "bool" | "nova_byte" | "nova_char" | "nova_i8" | "nova_i16" | "nova_i32" | "nova_i64" | "nova_u8" | "nova_u16" | "nova_u32" | "nova_u64" | "nova_f32" | "nova_f64" // Plan 175 Ф.1b: value-record field schemas store RAW C scalar // types (`int64_t` etc), not the `nova_*` aliases heap records // use — recognise them so value-record structural `==` emits a // scalar `(l).f == (r).f` (not `memcmp(&rvalue.f, …)` → address- // of-rvalue CC-FAIL). `==` on scalars is sound (IEEE for floats). | "int64_t" | "int32_t" | "int16_t" | "int8_t" | "uint64_t" | "uint32_t" | "uint16_t" | "uint8_t" | "intptr_t" | "uintptr_t" | "int" | "unsigned" | "size_t" | "ptrdiff_t" | "double" | "float" | "char" ) { return format!("(({}) == ({}))", l, r); } // Nested Option payload (`NovaOpt_<inner>`, a by-value struct) — // delegate to its generated `nova_opt_eq_<sani>` helper rather than // a struct `==` (C error) or memcmp (float/padding unsoundness on the // inner payload). The eq fn for a struct `NovaOpt_<innerSani>` is named // `nova_opt_eq_<innerSani>`, where `innerSani` = // `sanitize_for_novaopt(inner_c_ty)`. The C-type `cty` IS the struct // name `NovaOpt_<innerSani>`, so the suffix after the `NovaOpt_` prefix // is exactly `innerSani` — use it directly. NB: `NovaOpt_` does not // start with the `Nova_` prefix used for the sum/record branch below, // so this is order-safe. if let Some(inner_sani) = cty.strip_prefix("NovaOpt_") { if !cty.ends_with('*') { return format!("nova_opt_eq_{}({}, {})", inner_sani, l, r); } } // Plan 172.4 Ф.2 (D-block: value-record `==` STRUCTURAL): a value-record // `NovaValue_<Name>` is a BY-VALUE struct (not a `Nova_X*` heap pointer), so a raw // C `==` on it is invalid (the §2 acceptance CC-FAIL). Route to STRUCTURAL field-by- // field eq — the SAME §0 single dispatcher used for sum/heap-record/tuple, just with // BY-VALUE access `(l).field` (not `(*l)->field`). Prefer a user/derived `@equal` // (the c_name from the method registry), else recurse over `record_schemas` (the // value-record carries the SAME field schema — only placement differs). A NovaValue_ // POINTER (`NovaValue_X*`, a `*T` over a value-record) is NOT this case → excluded by // the `*`-suffix guard, falling to the pointer arms below. `NovaValue_` does not start // with the `Nova_` prefix of the single-`Nova_X*` arm, so this is order-safe. if let Some(type_name) = cty.strip_prefix("NovaValue_") { if !cty.ends_with('*') { // (1) user/derived @equal (D237 Equal.equal / Plan 126) — by c_name. let key = (type_name.to_string(), "equal".to_string()); if let Some(sigs) = self.method_overloads.get(&key) { if let Some(sig) = sigs.iter().find(|s| s.is_instance && s.param_c_types.len() == 1) { // 172.4 Ф.3 A3: user @equal имеет receiver-ABI NovaValue_X* // (D226), а l/r — by-value выражения (возможны rvalue → & // напрямую нельзя). Per-type обёртка by-value → & (единый // источник §0; late-emission через novaopt_eq_fns_buf, // тот же механизм, что nova_opt_eq_*). let arg_is_ptr = sig // №274: also consult method_byref_flag .param_c_types .first() .map(|p| p.ends_with('*')) .unwrap_or(false) || self.method_byref_flag(type_name, "equal", 0); let wrap = format!("nova_vr_ueq_{}", type_name); { let mut buf = self.novaopt_eq_fns_buf.borrow_mut(); let probe = format!("{}(", wrap); if !buf.contains(&probe) { let (proto, def) = super::operator_dispatch::emit_vr_wrapper( self.top_level_storage(), "nova_bool", &wrap, cty, cty, &sig.c_name, arg_is_ptr, ); self.vr_ueq_protos_buf.borrow_mut().push_str(&proto); buf.push_str(&def); } } return format!("{}({}, {})", wrap, l, r); } } // (2) structural field-by-field over `record_schemas` (BY-VALUE access). if let Some(schema) = self.record_schemas.get(type_name) { if !schema.is_empty() { let schema = schema.clone(); let order: Vec<String> = self .record_field_order .get(type_name) .cloned() .unwrap_or_else(|| { let mut ks: Vec<String> = schema.keys().cloned().collect(); ks.sort(); ks }); let conds: Vec<String> = order .iter() .filter_map(|fname| { schema.get(fname).map(|fty| { let mfn = Self::mangle_field_name(fname); let li = format!("({}).{}", l, mfn); let ri = format!("({}).{}", r, mfn); self.emit_field_eq(fty, &li, &ri, depth + 1) }) }) .collect(); if !conds.is_empty() { return format!("({})", conds.join(" && ")); } } } // No usable schema → fall through to the fallback identity below. } } // 172.4 Ф.3 A4: named-tuple `NovaTuple_X` (by-value struct) — зеркало // NovaValue_-арма (D328): (1) user @equal (NT ro-receiver ABI by-value — // прямой вызов с учётом param-ABI); (2) structural field-by-field по // record_schemas (NT-схема регистрируется тем же каналом полей). if let Some(type_name) = cty.strip_prefix("NovaTuple_") { if !cty.ends_with('*') { let key = (type_name.to_string(), "equal".to_string()); if let Some(sigs) = self.method_overloads.get(&key) { if let Some(sig) = sigs.iter().find(|s| s.is_instance && s.param_c_types.len() == 1) { // [M-static-selfreturn-value-mangle-conflict] (Plan // 172.13): the named-tuple instance-method receiver // ABI is ALWAYS a pointer (`NovaTuple_X*`, D215/128) — // `l` here is always an addressable field-access chain // (a plain eq-wrapper's own by-value param, or a // recursive `(...).fN`/`.field` built from one — never // an arbitrary rvalue expression, unlike the binary/ // unary operator-dispatch call sites), so `&(l)` is // always legal C — no hoist-to-temp needed here. return format!("{}(&({}), {})", sig.c_name, l, r); } } if let Some(schema) = self.record_schemas.get(type_name) { if !schema.is_empty() { let schema = schema.clone(); let order: Vec<String> = self .record_field_order .get(type_name) .cloned() .unwrap_or_else(|| { let mut ks: Vec<String> = schema.keys().cloned().collect(); ks.sort(); ks }); let conds: Vec<String> = order .iter() .filter_map(|fname| { schema.get(fname).map(|fty| { let mfn = Self::mangle_field_name(fname); let li = format!("({}).{}", l, mfn); let ri = format!("({}).{}", r, mfn); self.emit_field_eq(fty, &li, &ri, depth + 1) }) }) .collect(); if !conds.is_empty() { return format!("({})", conds.join(" && ")); } } } } } // mono'd tuple — recurse field-by-field over parsed element C-types. if let Some(elems) = Self::parse_mono_tuple_elements(cty) { if elems.is_empty() { // Unit-like tuple — vacuously equal. return "(1)".to_string(); } let conds: Vec<String> = elems .iter() .enumerate() .map(|(i, ety)| { let li = format!("({}).f{}", l, i); let ri = format!("({}).f{}", r, i); self.emit_field_eq(ety, &li, &ri, depth + 1) }) .collect(); return format!("({})", conds.join(" && ")); } // legacy _NovaTupleN (no `_` after NovaTuple) — arities 1..=8, all slots // are `nova_int`; per-slot scalar `==` is sound. if cty.starts_with("_NovaTuple") { let arity_str = &cty["_NovaTuple".len()..]; if let Ok(arity) = arity_str.parse::<usize>() { if arity == 0 { return "(1)".to_string(); } let conds: Vec<String> = (0..arity) .map(|i| format!("(({}).f{} == ({}).f{})", l, i, r, i)) .collect(); return format!("({})", conds.join(" && ")); } } // Single `Nova_X*` value (record or sum) — structural eq. let is_single_nova_ptr = cty.starts_with("Nova_") && cty.ends_with('*') && !cty.ends_with("**"); if is_single_nova_ptr { // [M-156-bare-unit-variant-eq-invalid-cast] (221.1, rationale in // NOTES.md / commit b5c4a689e): re-cast to `cty` — no-op for a // real pointer, round-trip for the D109 erased-scalar ctor form. let l = format!("(({})({}))", cty, l); let r = format!("(({})({}))", cty, r); let l = l.as_str(); let r = r.as_str(); let type_name = Self::debt_strip_nova_prefix_or_empty(cty) .trim_end_matches('*') .to_string(); // [M-172.1-option-container-eq-structural] + [M-172.1-option-hashmap-eq-structural]: // a mono'd CONTAINER (`Vec[T]` → `Nova_Vec____<elem>*`, `HashMap[K,V]` / // `Set[T]` → `Nova_HashMap____<k>__<v>*`) nested as an Option payload / // sum-variant / Result / tuple field needs ELEMENT-WISE structural eq via // the MONO `<container>_method_equal` Nova-body (Vec: vec/protocols.nv; // HashMap: hashmap.nv, order-independent) — NOT the erased generic // `Nova_Vec_method_equal` (conflicting types / wrong erased element compare // → SEGV), and NOT a struct-field recurse into the opaque `data`/buckets // header (the record branch below would compare the heap pointer by // identity). `[]T` ≡ `Vec[T]`. The direct `Container==Container` operator is // handled in emit_binary (`&mut`); HERE we are `&self`, so we cannot trigger // the `&mut` mono instantiation directly — record the request in // `pending_container_eq_monos` (drained post-emission, register_container_eq_mono). // Under `novaopt_early_gen` (early opt path, before the mono fn-fwd-decls) the // mono proto isn't visible → bail to identity; the structural-LATE opt path // (debt_opt_payload_needs_structural_eq) reaches us with early_gen off and splices // the eq fn AFTER the mono fwd-decls. if type_name.starts_with("Vec____") || type_name.starts_with("HashMap____") { if *self.novaopt_early_gen.borrow() { return format!("(({}) == ({}))", l, r); } let cont_c = cty.trim_end_matches('*').to_string(); // Nova_<container>____<args> if self.container_eq_requested.borrow_mut().insert(cont_c.clone()) { self.pending_container_eq_monos.borrow_mut().push(cont_c); } return format!("{}_method_equal({}, {})", type_name, l, r); } // (1) user/derived @equal (D237 Equal.equal / Plan 126). for method_name in &["equal"] { let key = (type_name.clone(), method_name.to_string()); if let Some(sigs) = self.method_overloads.get(&key) { if let Some(sig) = sigs .iter() .find(|s| s.is_instance && s.param_c_types.len() == 1) { return format!("{}({}, {})", sig.c_name, l, r); } } } // (2) synthesis via @compare (Equal.equal default = compare == 0). if self .all_methods .contains(&(type_name.clone(), "compare".to_string())) { return format!( "(Nova_{}_method_compare({}, {}) == 0)", Self::sanitize_c_for_ident(&type_name), l, r ); } // (3) sum-type recursion over tag + payload via sum_schemas, or (4) // record-field recursion via record_schemas. Records are NOT auto- // `@equal`'d, so a record compared only structurally (e.g. via // `Option[Rec]==` or as a sum/Result field) reaches the record branch // below ([M-172.1-option-eq-record-structural] L2). // // Guard: when novaopt_early_gen is set we are inside the EARLY opt path // (emits into novaopt_typedefs_buf — BEFORE sum/record struct bodies). // Accessing ->tag / ->payload / ->field here causes clang "incomplete // type" errors. Fall back to pointer identity (the function calling us // is in an early opt_eq context where struct members are unavailable). // The structural-late opt path (debt_opt_payload_needs_structural_eq) does // NOT set early_gen, so it reaches the real recursion below. if *self.novaopt_early_gen.borrow() && (self.sum_schemas.contains_key(&type_name) || self.record_schemas.contains_key(&type_name)) { return format!("(({}) == ({}))", l, r); } // [M-result-direct-recursive-enum] / [M-option-self-recursive-record-mono] // (Plan 186, recursive-mono): a genuine cycle (self- or mutually- // recursive heap type ALREADY being expanded up this call chain) is // NOT re-inlined — route through a named per-type function instead // (see `struct_eq_stack` field doc + `emit_named_struct_eq_call`). // Non-cyclic types fall straight through to `structural_eq_body_for_ptr` // unaffected (byte-identical inline expansion, same as before this fix). if self.struct_eq_stack.borrow().contains(&type_name) { return self.emit_named_struct_eq_call(&type_name, l, r); } self.struct_eq_stack.borrow_mut().push(type_name.clone()); let __struct_eq_result = self.structural_eq_body_for_ptr(&type_name, cty, l, r, depth); self.struct_eq_stack.borrow_mut().pop(); return __struct_eq_result; } // Plan 153.3 fix: Result is a *special* heap-pointer ABI `NovaRes_<n>*` // (NOT a `Nova_X*` sum — it carries extra typed-error fields), so it // failed the `Nova_`-prefix sum test above and fell through to pointer // identity (`Ok(1) == Ok(1)` → false). Compare tag + the active // variant's payload structurally. `novares_ok_err` yields the concrete // `(ok, err)` C-types; every mono'd Result instance shares the generic // tags `NOVA_TAG_Result_Ok`/`_Err`. if cty.starts_with("NovaRes_") && cty.ends_with('*') { if let Some((ok_c, err_c)) = self.novares_ok_err(cty) { let ok_eq = self.emit_field_eq( &ok_c, &format!("({})->payload.Ok._0", l), &format!("({})->payload.Ok._0", r), depth + 1, ); let err_eq = self.emit_field_eq( &err_c, &format!("({})->payload.Err._0", l), &format!("({})->payload.Err._0", r), depth + 1, ); return format!( "((({l})->tag == ({r})->tag) && (({l})->tag != NOVA_TAG_Result_Ok \ || ({ok})) && (({l})->tag != NOVA_TAG_Result_Err || ({err})))", l = l, r = r, ok = ok_eq, err = err_eq ); } } // Any other pointer (void*/nova_ptr/double-ptr/typed raw ptr) — opaque, // identity comparison is the only meaningful equality. if cty.ends_with('*') { return format!("(({}) == ({}))", l, r); } // Unknown by-value composite C-type with no structural schema // (e.g. NovaRes_<..>, NovaArray_<..> by value, runtime structs). A // struct `==` is a C error, so fall back to `memcmp` — exactly the // prior behavior for these. This is NOT one of the unsound float/ // padding cases that Plan 141 targets (tuple/sum/record/str/Option are // all handled structurally above); it only triggers for value-structs // we have no field schema for, where byte-compare is the best available // and keeps codegen total. format!("(memcmp(&{}, &{}, sizeof({})) == 0)", l, r, cty) } /// [M-result-direct-recursive-enum] / [M-option-self-recursive-record-mono] /// (Plan 186): the tag+payload / field-by-field structural-eq body for a /// single `Nova_<type_name>*` heap sum or record, EXTRACTED verbatim from /// `emit_field_eq`'s former inline body (behavior unchanged for the /// non-cyclic case — this is a pure extract-method refactor). Callers are /// responsible for `struct_eq_stack` push/pop (cycle bookkeeping lives at /// the call sites: the direct `emit_field_eq` entry, and /// `emit_named_struct_eq_call` when synthesizing a named function body). fn structural_eq_body_for_ptr(&self, type_name: &str, cty: &str, l: &str, r: &str, depth: usize) -> String { if let Some(variants) = self.sum_schemas.get(type_name).cloned() { // Empty-sum (0 variants, unit-type like CharTryFromError): emitted as // `typedef int64_t Nova_X` — no struct tag/payload. All instances are // vacuously equal (uninhabited / single-value unit); dereference as scalar. if variants.is_empty() { return format!("((void)({}), (void)({}), 1)", l, r); } let mut field_conds: Vec<String> = Vec::new(); // [M-codegen-emission-nondeterminism] fix (2026-07-20): `variants` is a // `HashMap<String, Vec<String>>` (legacy `sum_schemas` value) — Rust's // default per-process-random hasher gives a DIFFERENT iteration order on // every `nova build` invocation, reordering the `&&`-chained tag conjuncts // below run-to-run (semantically inert — every conjunct is a pure, // side-effect-free comparison and `&&` is commutative here — but the // emitted C text is not reproducible). Sort by variant name (stable, // meaningful key: the C `NOVA_TAG_<ty>_<v>` identity) before iterating. let mut variant_names: Vec<&String> = variants.keys().collect(); variant_names.sort(); for var_name in variant_names { let field_types = &variants[var_name]; if field_types.is_empty() { continue; } // Record-style variant (`V { a, b }`) uses NAMED C fields, not // positional `._i`; tuple-style (`V(T,U)`) uses `._i`. Recover the // field names from `record_variant_field_order` (same index order as // `field_types`). Without this, a record-variant payload generated // `payload.V._0` → C "no member named '_0'" (e.g. ParseJsonError / // ReadBufferError in `Option[<sum>]==`). let rec_order = self.record_variant_field_order .get(&format!("{}::{}", type_name, var_name)).cloned(); let var_fields: Vec<String> = field_types .iter() .enumerate() .map(|(i, fty)| { let (li, ri) = match rec_order.as_ref().and_then(|o| o.get(i)) { Some(fname) => { let mfn = Self::mangle_field_name(fname); (format!("({})->payload.{}.{}", l, var_name, mfn), format!("({})->payload.{}.{}", r, var_name, mfn)) } None => (format!("({})->payload.{}._{}", l, var_name, i), format!("({})->payload.{}._{}", r, var_name, i)), }; self.emit_field_eq(fty, &li, &ri, depth + 1) }) .collect(); field_conds.push(format!( "(({l})->tag != NOVA_TAG_{ty}_{v} || ({fields}))", l = l, ty = type_name, v = var_name, fields = var_fields.join(" && ") )); } let tag_eq = format!("(({l})->tag == ({r})->tag)", l = l, r = r); return if field_conds.is_empty() { tag_eq } else { format!("({} && {})", tag_eq, field_conds.join(" && ")) }; } // [M-172.1-option-eq-record-structural] (L2): a heap user RECORD without // an `@equal` (records are NOT auto-`@equal`'d) reaches here when compared // structurally — `Option[Rec]==`, a sum/Result field, or a direct `Rec==Rec`. // Recurse field-by-field over `record_schemas` — the SINGLE per-type eq // dispatcher (§0 per-type операции), mirroring the sum recursion above — // instead of degrading to pointer identity. Field order from // `record_field_order` (the schema HashMap is unordered → non-deterministic // codegen otherwise); a builtin record registered without an order list // falls back to sorted keys. EMPTY-schema records (opaque builtin runtime // structs like StringBuilder) are skipped → identity below (sound for // opaque handles; matches the debt_opt_payload_needs_structural_eq exclusion). // CONTAINERS (`Vec____`/`HashMap____`) are in `record_schemas` only for // field-access lowering, but carry an opaque heap `data`/buckets buffer // where field-recursion would compare the POINTER by identity // (`Some([1,2])==Some([1,2])` false). Vec is intercepted earlier (mono // `@equal` element-wise); HashMap has no `@equal` Nova-body yet → skip here, // falling to identity (HashMap-eq = separate follow-up). Mono user-generic // value-records (`Box____<T>`) DO recurse correctly — only the two known // container prefixes are excluded. if let Some(schema) = self.record_schemas.get(type_name) { let is_container = type_name.starts_with("Vec____") || type_name.starts_with("HashMap____"); if !schema.is_empty() && !is_container { let schema = schema.clone(); let order: Vec<String> = self.record_field_order .get(type_name).cloned() .unwrap_or_else(|| { let mut ks: Vec<String> = schema.keys().cloned().collect(); ks.sort(); ks }); let conds: Vec<String> = order.iter().filter_map(|fname| { schema.get(fname).map(|fty| { let mfn = Self::mangle_field_name(fname); let li = format!("({})->{}", l, mfn); let ri = format!("({})->{}", r, mfn); self.emit_field_eq(fty, &li, &ri, depth + 1) }) }).collect(); if !conds.is_empty() { return format!("({})", conds.join(" && ")); } // all fields filtered out (shouldn't happen for non-empty schema) // — fall through to identity below. } } // Plan 153.3 fix: mono'd generic sum whose legacy `sum_schemas` // entry is absent under the mono'd key (generic sums register the // schema under the GENERIC name; the mono'd key may be unregistered // here) — structural `==` otherwise silently degraded to pointer // identity (`Foo[int].A(1) == A(1)` → false). Reconstruct the // substituted variant schema from the generic template + recorded // mono type-args and emit the same tag+payload recursion. Mono'd // sum C tags use the FULL mangled prefix `Nova_<mono>` (the mono // constructor emits `NOVA_TAG_Nova_<mono>_<V>`), unlike non-generic // sums (which strip `Nova_`), so the tag prefix is the full c-type // name (sans `*`). Strictly a fallback BEFORE pointer-identity — it // only fires for sums currently broken, never changing a sum that // already resolved via `sum_schemas`/@equal/@compare above. { let full_c = cty.trim_end_matches('*'); if let Some(recon) = self.reconstruct_mono_sum_schema(full_c) { let mut field_conds: Vec<String> = Vec::new(); for (var_name, field_types) in &recon { if field_types.is_empty() { continue; } let var_fields: Vec<String> = field_types .iter() .enumerate() .map(|(i, fty)| { let li = format!("({})->payload.{}._{}", l, var_name, i); let ri = format!("({})->payload.{}._{}", r, var_name, i); self.emit_field_eq(fty, &li, &ri, depth + 1) }) .collect(); field_conds.push(format!( "(({l})->tag != NOVA_TAG_{ty}_{v} || ({fields}))", l = l, ty = full_c, v = var_name, fields = var_fields.join(" && ") )); } let tag_eq = format!("(({l})->tag == ({r})->tag)", l = l, r = r); return if field_conds.is_empty() { tag_eq } else { format!("({} && {})", tag_eq, field_conds.join(" && ")) }; } } // No structural info available — fall back to pointer identity. format!("(({}) == ({}))", l, r) } /// [M-result-direct-recursive-enum] / [M-option-self-recursive-record-mono] /// (Plan 186): the cycle-breaking half of the fix — called by /// `emit_field_eq` when `type_name` is already on `struct_eq_stack` (a /// genuine self- or mutually-recursive heap type). Ensures a named /// `nova_struct_eq_<T>` function exists (idempotent via /// `struct_eq_fn_requested`) and returns a CALL to it instead of inlining /// the comparison again. The function body is built via /// `structural_eq_body_for_ptr` — the SAME logic used for the non-cyclic /// case — so a self-referential field inside the body re-enters this exact /// path, sees the type already in `struct_eq_fn_requested`, and short- /// circuits to a plain call: the C function recurses on itself at RUNTIME /// (bounded by the actual, finite, heap structure) instead of being /// unrolled at COMPILE time (which is what previously produced /// `branching^depth` string growth up to `MAX_EQ_DEPTH`, multiple GB / /// hang on a real recursive enum — see `struct_eq_stack` field doc). /// Spliced into `novaopt_eq_fns_buf` (marker `/*__NOVAOPT_EQ_FNS__*/`, /// placed AFTER struct bodies and method forward-decls — safe for /// `->tag`/`->payload`/field derefs and `@equal` method calls). fn emit_named_struct_eq_call(&self, type_name: &str, l: &str, r: &str) -> String { let fn_name = format!("nova_struct_eq_{}", Self::sanitize_c_for_ident(type_name)); let newly_requested = self.struct_eq_fn_requested .borrow_mut() .insert(type_name.to_string()); if newly_requested { // Proto FIRST (before building the body) — a mutual cycle (A calls // named-B, B calls named-A) needs A's own prototype visible before // B's definition references it, regardless of which of the two // finishes building (and thus gets spliced) first. self.struct_eq_protos_buf.borrow_mut().push_str(&format!( "{s}nova_bool {fname}(Nova_{t}* a, Nova_{t}* b);\n", s = self.top_level_storage(), fname = fn_name, t = type_name, )); let cty = format!("Nova_{}*", type_name); self.struct_eq_stack.borrow_mut().push(type_name.to_string()); let body = self.structural_eq_body_for_ptr(type_name, &cty, "a", "b", 0); self.struct_eq_stack.borrow_mut().pop(); self.novaopt_eq_fns_buf.borrow_mut().push_str(&format!( "{s}nova_bool {fname}(Nova_{t}* a, Nova_{t}* b) {{ return {body}; }}\n", s = self.top_level_storage(), fname = fn_name, t = type_name, body = body, )); } format!("{}({}, {})", fn_name, l, r) } /// Plan 153.3: reconstruct the *substituted* variant schema of a mono'd /// generic sum (`Nova_Foo____nova_int` → `[("A", ["nova_int"]), ("B", [])]`) /// from the generic template + the recorded mono type-args. Used by /// `emit_field_eq` when the legacy `sum_schemas` lacks the mono'd key, so /// structural `==` works instead of degrading to pointer identity. /// /// `full_c_name` is the full mangled name WITH the `Nova_` prefix (the key /// of `generic_type_instance_info`, e.g. `"Nova_Foo____nova_int"`). Returns /// variants in template declaration order. Pure `&self` / read-only: /// substitutes a payload that is a *bare* generic type-param by position; /// any other payload slot reuses the generic schema's stored C-type /// (concrete for non-param payloads, `void*` for the rare nested-generic /// case — degraded to identity for that slot only, never wrong for the /// common scalar/record/str/nested-mono'd-sum payloads). fn reconstruct_mono_sum_schema(&self, full_c_name: &str) -> Option<Vec<(String, Vec<String>)>> { let (base, type_args_c) = { let info = self.generic_type_instance_info.borrow(); info.get(full_c_name)?.clone() }; let template = self.generic_type_templates.get(&base)?; let variants = match &template.kind { TypeDeclKind::Sum(vs) => vs, _ => return None, }; // generic param name → concrete C type, by declaration position. let param_map: HashMap<String, String> = template .generics .iter() .enumerate() .filter_map(|(i, g)| type_args_c.get(i).map(|c| (g.name.clone(), self.arg_c(c)))) .collect(); let generic_schema = self.sum_schemas.get(&base); let mut out: Vec<(String, Vec<String>)> = Vec::new(); for v in variants { let tyrefs: Vec<&TypeRef> = match &v.kind { SumVariantKind::Unit => Vec::new(), SumVariantKind::Tuple(types) => types.iter().collect(), SumVariantKind::Record(fields) => fields.iter().map(|f| &f.ty).collect(), }; let mut slot_c: Vec<String> = Vec::new(); for (i, tr) in tyrefs.iter().enumerate() { let c = match tr { TypeRef::Named { path, generics, .. } if path.len() == 1 && generics.is_empty() && param_map.contains_key(&path[0]) => { param_map.get(&path[0]).cloned().unwrap() } _ => generic_schema .and_then(|gs| gs.get(&v.name)) .and_then(|slots| slots.get(i)) .cloned() .unwrap_or_else(|| "void*".to_string()), }; slot_c.push(c); } out.push((v.name.clone(), slot_c)); } Some(out) } /// Plan 49 Ф.6 cross-type cascade helper: C-type string → Nova type name /// (для lookup в from_targets и user-friendly diagnostic messages). /// "nova_int" → "int" /// "nova_str" → "str" /// "nova_bool" → "bool" /// "Nova_Foo*" → "Foo" /// (Generic mono'd names like "Nova_X____Y" остаются как есть.) fn debt_c_type_to_nova_name(c_ty: &str) -> String { let trimmed = c_ty.trim_end_matches('*').trim(); match trimmed { "nova_int" => "int".to_string(), "nova_str" => "str".to_string(), "nova_bool" => "bool".to_string(), "nova_f64" => "f64".to_string(), "nova_f32" => "f32".to_string(), "nova_byte" => "u8".to_string(), other => other.strip_prefix("Nova_").unwrap_or(other).to_string(), } } /// Plan 48 Ф.3: compute the mangled C name for a generic type instance. /// compute_generic_type_c_name("HashMap", ["nova_str", "nova_int"]) → "Nova_HashMap____nova_str__nova_int" fn compute_generic_type_c_name(base_name: &str, type_args_c: &[String]) -> String { if type_args_c.is_empty() { return format!("Nova_{}", base_name); } let args: String = type_args_c.iter() .map(|c_ty| Self::sanitize_c_for_ident(c_ty)) .collect::<Vec<_>>() .join("__"); format!("Nova_{}____{}", base_name, args) } /// Plan 266 (D455): compute — and idempotently register — the /// `Outcome[T]` mono instance's mangled C struct base name (WITHOUT the /// trailing `*`) for a given body-`T` C type. Shared by /// `infer_expr_c_type`'s `Supervised` probe arm (a `supervised(cancel: /// …)` expression's declared C type, e.g. for a `ro out = …` LET's own /// variable, which is probed BEFORE `emit_supervised` itself runs) and /// `emit_supervised`'s real emission — both need the identical mangled /// name and the identical idempotent worklist registration, and both /// only have `&self` here (interior mutability — /// `generic_type_worklist`/`generic_type_instance_info` are /// `RefCell`-backed — makes registering from a `&self` probe safe; /// mirrors `try_infer_variant_mono_args`, same pattern, ~22971). fn outcome_mono_c_base(&self, body_c_ty: &str) -> String { let mangled = Self::compute_generic_type_c_name("Outcome", &[body_c_ty.to_string()]); if !self.emitted_generic_type_instances.contains(&mangled) { let mut wl = self.generic_type_worklist.borrow_mut(); if !wl.iter().any(|(_, _, m)| m == &mangled) { wl.push(( "Outcome".to_string(), Self::args_lift(&[body_c_ty.to_string()]), mangled.clone(), )); } } self.generic_type_instance_info.borrow_mut() .entry(mangled.clone()) .or_insert_with(|| ("Outcome".to_string(), Self::args_lift(&[body_c_ty.to_string()]))); mangled } /// Plan 153.2 Ф.1 (STAGE 1 — by-value generic value-records): the /// **short** mono name for a generic instance — i.e. the heap-form /// mangled name with the `Nova_` prefix dropped /// (`Nova_BoxIter____nova_int` → `BoxIter____nova_int`). This short name /// is the key used by `value_record_names` / `record_schemas` / /// `type_aliases` (mirroring the NON-generic value-record path, which /// keys those tables by the bare type name). The worklist and /// `generic_type_instance_info` keep the full `Nova_`-prefixed mangled /// name unchanged — only the *C-type surface* of a `value` template /// switches to the `NovaValue_<short>` by-value form. fn debt_mono_short_name(mangled_nova: &str) -> &str { mangled_nova.strip_prefix("Nova_").unwrap_or(mangled_nova) } /// Plan 153.2 Ф.2 (STAGE 2): split a mono type-args string into its /// TOP-LEVEL args, depth-aware so a NESTED generic instance (which carries /// its own `____` base-args separator) is NOT torn apart. The naive /// `args_str.split("__")` mis-parses a generic-over-source arg like /// `Nova_VecIter____nova_int_p__nova_int` (the args of /// `FilterIter[VecIter[int], int]`) into `["Nova_VecIter", "nova_int_p", /// "nova_int"]` (3 args) instead of `["Nova_VecIter____nova_int_p", /// "nova_int"]` (2 args) — because the nested `____` reads as two empty- /// straddling `__` separators. This walks the string and treats a `____` /// (4+ underscores) run as a nested-instance opener that consumes the /// following `__`-delimited segments until the arg count balances: each /// `____` opens one nested arg-list, and the segment immediately AFTER a /// nested instance's own args rejoins at the parent level. Concretely we /// re-join on the structural rule "a `__` boundary that is NOT part of a /// `____`+ run is a separator UNLESS it directly follows a `Nova_`/`NovaValue_` /// nested-instance segment that has not yet been closed". /// /// Implementation: split on every maximal underscore run; a run of length /// `>= 4` (`____`) is the nested base→args separator (keep it joined), a run /// of exactly `2` (`__`) is an arg separator at the CURRENT depth. We track /// depth by counting, for each token, whether it OPENS a nested instance /// (token starts with `Nova_`/`NovaValue_` AND the next run is `____`). /// Because the encoding is not self-delimiting, the robust source of truth /// is the `generic_type_instance_info` registry; this string splitter is the /// fallback used only when the instance is not (yet) registered. fn split_top_level_mono_args(args_str: &str) -> Vec<String> { // Tokenize into (segment, following_underscore_run_len). let mut segs: Vec<(String, usize)> = Vec::new(); let bytes = args_str.as_bytes(); let mut i = 0usize; while i < bytes.len() { // Read a non-underscore segment. let seg_start = i; while i < bytes.len() && bytes[i] != b'_' { i += 1; } let seg = args_str[seg_start..i].to_string(); // Read the following underscore run. let und_start = i; while i < bytes.len() && bytes[i] == b'_' { i += 1; } let run = i - und_start; segs.push((seg, run)); } // Re-assemble. A `____` (run>=4) joins a nested base to its args; a `__` // (run==2) at depth 0 separates top-level args. Depth increases by 1 on // each `____` opener and we close ONE level on each subsequent `__` // until depth returns to 0 (a nested instance with N args needs N-1 // inner `__` separators plus its `____` opener). This mirrors the // emit-side mangle `compute_generic_type_c_name` (base `____` arg-join, // `__` between args) recursively applied. let mut out: Vec<String> = Vec::new(); let mut cur = String::new(); let mut depth: i32 = 0; for (idx, (seg, run)) in segs.iter().enumerate() { cur.push_str(seg); let is_last = idx + 1 == segs.len(); if is_last { break; } // Reconstruct the underscore run we consumed. if *run >= 4 { // Nested base→args opener: stays inside the current arg, deepen. cur.push_str(&"_".repeat(*run)); depth += 1; } else if *run == 2 { if depth > 0 { // Separator INSIDE a nested instance: keep joined, close one // nesting level (this nested arg-list yields one more arg). cur.push_str("__"); depth -= 1; } else { // Top-level arg boundary. out.push(std::mem::take(&mut cur)); } } else { // A single `_` (or `_p` sanitized pointer, run==1 leading into // `p`) is part of an identifier (`nova_int`, `_p`); keep joined. cur.push_str(&"_".repeat(*run)); } } if !cur.is_empty() { out.push(cur); } out.into_iter().filter(|s| !s.is_empty()).collect() } /// Plan 153.2 Ф.2 (STAGE 2): the TOP-LEVEL mono type-args of a receiver /// C-type, preferring the exact `generic_type_instance_info` registry and /// falling back to the depth-aware string splitter. `obj_ty` is a receiver /// surface type (`Nova_X____…*` / `NovaValue_X____…` / bare). Returns the /// args in declaration order, or an empty Vec if the name is not a mono /// instance. This is the value-record-safe replacement for the naive /// `args_str.split("__")` parse that mis-tears nested generic-over-source /// args. fn debt_mono_type_args_of(&self, obj_ty: &str) -> Vec<String> { let stripped = obj_ty .strip_prefix("NovaValue_") .or_else(|| obj_ty.strip_prefix("Nova_")) .unwrap_or(obj_ty) .trim_end_matches('*'); // Registry is authoritative — key is the `Nova_`-prefixed short name. let nova_key = format!("Nova_{}", stripped); // A1‴: registry args carry `ResolvedType` — clone out, drop the borrow, then lower // each to its C-name (`Raw` verbatim; no re-borrow while the registry Ref is alive). let args_rt = self.generic_type_instance_info.borrow().get(&nova_key).map(|(_, a)| a.clone()); if let Some(args) = args_rt { return args.iter().map(|a| self.arg_c(a)).collect(); } // Fallback: depth-aware split of the args substring after the base `____`. if let Some(sep_pos) = stripped.find("____") { return Self::split_top_level_mono_args(&stripped[sep_pos + 4..]); } Vec::new() } /// Plan 153.2 Ф.1 (STAGE 1): is this generic base a `value` record /// template (`type X[T] value { … }`, `AllocKind::Value`)? Gate for the /// by-value monomorphization split. Returns `false` for heap records, /// sums, newtypes, and any non-generic / unknown name — so every /// existing generic (all heap) and every existing value-record (all /// non-generic, never in `generic_type_templates`) is untouched. fn is_value_generic_template(&self, base_name: &str) -> bool { use crate::ast::{AllocKind, TypeDeclKind}; self.generic_type_templates.get(base_name) .map(|t| matches!(t.kind, TypeDeclKind::Record(_)) && t.allocation == AllocKind::Value) .unwrap_or(false) } /// Plan 153.2 Ф.1 (STAGE 1): is `name` — in ANY surface form (base /// `BoxIter`, short mono `BoxIter____nova_int`, `Nova_`-prefixed mono /// `Nova_BoxIter____nova_int`, or already-by-value `NovaValue_BoxIter…`) /// — an instance of a `value` generic record template? Returns the /// SHORT mono name (`BoxIter____nova_int`) if so, else `None`. /// /// This is ORDER-INDEPENDENT: it does not rely on `type_aliases` / /// `value_record_names` having been populated yet (those are filled by /// `emit_generic_type_instance`, which may run AFTER a method fwd-decl / /// body that needs the receiver type). It re-derives value-ness straight /// from `generic_type_instance_info` (mangled → base) + the template's /// `AllocKind`. For a bare base name it never fires (no `____`), which is /// correct — a bare `BoxIter` is never a concrete carrier. fn debt_value_generic_mono_short(&self, name: &str) -> Option<String> { let trimmed = name.trim_end_matches('*').trim(); // Normalize to the short mono name (drop NovaValue_/Nova_ prefix). let short = trimmed .strip_prefix("NovaValue_") .or_else(|| trimmed.strip_prefix("Nova_")) .unwrap_or(trimmed); // A concrete mono instance always carries the `____` arg separator. if !short.contains("____") { return None; } // Resolve base via instance-info (keyed by the `Nova_`-prefixed name). let nova_key = format!("Nova_{}", short); let base = { let info = self.generic_type_instance_info.borrow(); info.get(&nova_key).map(|(b, _)| b.clone()) }; // Fallback: base = substring before the first `____` (instance-info // may not be registered yet for this exact name). let base = base.unwrap_or_else(|| { short.split("____").next().unwrap_or(short).to_string() }); if self.is_value_generic_template(&base) { Some(short.to_string()) } else { None } } /// Plan 153.2 Ф.1 (STAGE 1): normalize a generic-instance C-type string to /// its correct by-value/heap surface. `Nova_<short>*` (heap pointer form /// emitted by `compute_generic_type_c_name` / `apply_type_subst_to_ref`, /// both static and value-blind) is rewritten to `NovaValue_<short>` (no /// `*`, by value) when `<short>` names a `value` generic record instance. /// All other strings (heap generics, primitives, NovaOpt/NovaRes, tuples, /// already-NovaValue forms) pass through unchanged. Apply this on any /// return-type / let-binding inference that may produce a generic instance, /// so the local-var type matches the by-value method ABI. /// /// [196.3 audit, 2026-07-12] NOT a redundant re-derivation of the /// checker-resolved-type channel: `resolved_named_to_c`'s parallel /// `is_value_generic_template(&full)` check (`Ok(format!("NovaValue_{}", /// ..))` arm, ~line 3807) makes this SAME value-vs-heap decision for /// `ResolvedType`-typed inputs inside `resolved_type_to_c` — the /// checker-first channel. This function's callers instead all sit on /// the SEPARATE, still-legacy `TypeRef` + string-subst inference path /// (`apply_type_subst_to_ref`, `value_aware_subst_to_ref`, /// `register_generic_instances_in_typeref`, `debt_bind_self_for_mono_ /// recv`, `infer_mono_method_ret_with_args`), which `infer_expr_c_type` /// falls back to only AFTER Channel 1/2 (`resolved_callees`/ /// `resolved_types` → `resolved_type_to_c`) have already missed or /// yielded an erased/generic stub (see the `// Channel N:` cascade at /// ~48751). Since no `ResolvedType` is available at those call sites, /// there is no checker-resolved value to duplicate — this is the /// legitimate value-category → C-representation lowering step for that /// fallback path and stays as-is. It should be retired only when the /// `apply_type_subst_to_ref`/`infer_mono_method_ret_with_args` family /// itself migrates from `TypeRef` onto `ResolvedType` (out of scope /// here — that is the broader 172.x/196.4 channel migration). fn value_aware_generic_c_type(&self, c_ty: &str) -> String { if let Some(short) = self.debt_value_generic_mono_short(c_ty) { format!("NovaValue_{}", short) } else { c_ty.to_string() } } /// Plan 153.2 Ф.2 (STAGE 2 — generic-over-source adapters): a `&self`, /// value-AWARE mirror of the static `apply_type_subst_to_ref`. For the /// generic-user-type arm it resolves each nested arg, then normalizes it /// through `value_aware_generic_c_type` so a `value` generic-instance arg /// embeds the `NovaValue_<short>` prefix (NOT the value-blind `Nova_<short>*` /// the static path would bake in). This makes the mangled name AGREE with /// the `type_ref_to_c` path (which is already value-aware via the recursive /// `type_ref_to_c(g)` arg resolution) so a nested chain /// `FilterIter[MapIter[VecIter[int], int]]` produces ONE mangled name on /// both the enqueue (registration) side and the type-decl / field side — /// otherwise the worklist emits two divergent instances /// (`…____Nova_MapIter…_p` vs `…____NovaValue_MapIter…`) and the field type /// references a struct that was never defined ("unknown type name"). /// /// The OUTER result is returned by VALUE (no trailing `*`) when the outer /// template is itself a `value` record, mirroring `value_aware_generic_ /// c_type`. All non-generic-user-type forms delegate to the static method /// unchanged (primitives, Option/Result, arrays, tuples, pointers). fn value_aware_subst_to_ref( &self, ty: &crate::ast::TypeRef, subst: &[(String, Option<String>)], ) -> Option<String> { use crate::ast::TypeRef; if let TypeRef::Named { path, generics, .. } = ty { let base = path.last().cloned().unwrap_or_default(); if !generics.is_empty() && base != "Option" && base != "Result" && self.generic_type_templates.contains_key(&base) { // Resolve each arg value-aware (recurse), then normalize its // surface so a `value` arg embeds `NovaValue_<short>`. let mut args_c: Vec<String> = Vec::with_capacity(generics.len()); for g in generics { let c = self.value_aware_subst_to_ref(g, subst)?; args_c.push(self.value_aware_generic_c_type(&c)); } let mangled = Self::compute_generic_type_c_name(&base, &args_c); // Outer surface: by-value for a `value` template, else heap ptr. return Some(self.value_aware_generic_c_type(&format!("{}*", mangled))); } } // All other shapes: defer to the value-blind static resolver, then run // a final outer normalization (covers the simple `BoxIter[T]` case the // static path mangles heap-side). Self::apply_type_subst_to_ref(ty, subst) .map(|c| self.value_aware_generic_c_type(&c)) } /// Plan 180 [M-180-namespace-static-generic-mono followup]: resolve a /// generic free-fn RETURN type of shape `Result[Ok, Err]` / `Option[Inner]` /// whose type-params are bound in `subst`. `value_aware_subst_to_ref` / /// `apply_type_subst_to_ref` deliberately SKIP `Result`/`Option` (they carry /// the special `NovaRes_`/`NovaOpt_` mangling, not the generic-type-template /// mangling), so a turbofish call like `json_decode[User](s) -> Result[T, /// DeError]` inferred `None` and the `?`/`!!` degenerated. Construct the /// `NovaRes`/`NovaOpt` C type directly and register its decl. Returns None /// for non-Result/Option or when an arg is unresolvable. /// /// [M-196-gen] (Plan 196 "ONE TRUTH", GEN-agent wave): the two callers of /// this fn (`infer_call_ret_c`'s `B06a_method_overload_sentinel_mono` — /// METHOD-level-generic sentinel, and `B10j_generic_fn_value_aware_return` /// — free-fn/static-ctor generic return) both sit INSIDE the frozen /// wave-1 zone (`infer_call_ret_c`, 196.2) — per docs/plans/196.4-call- /// resolvedtype-channel.md §9, Tier-2 helpers living inside that zone are /// torn down "вместе с кластером или раньше через `panic!`-detach, если /// ветвь отделима без правки замороженного диапазона". The two callers /// are NOT separable from in here (this fn has no expr-id / call-site /// identity to distinguish them), and the CH map (docs/plans/wip/196-ch- /// result-notes.md §6) marks the METHOD-generic class (B06a's kind) as /// producer-incomplete (Producer B / `resolve_return_channel` method- /// level widen, out of THIS wave's scope) — so this fn's Result/Option /// derivation logic stays LIVE (unconditionally, in every build /// profile — release included) as the correctness fallback for both /// call sites. What DID move out (this wave): the `register_novares_ /// decl`/`register_novaopt_decl` + name-format side-effect, previously /// duplicated inline here, now funnels through the SAME canonical sink /// the channel-producer path uses (`result_repr_c_type` / /// `opt_repr_c_type`, both `~48486` — "LEGIT-LOWERING, canonical sink" /// verdict) — one engine for "Result/Option ResolvedType-pair → C + /// typedef", not two byte-identical copies. `icr_trace` markers below /// are the DEBUG-only (`cfg(debug_assertions)`, zero release overhead, /// same convention as every other `infer_call_ret_c` bucket) hooks for /// the NEXT wave's reachability proof — `NOVA_TRACE_ICR=1` over the full /// corpus (this wave measured 0 hits over d85/d30/d408/d30_result_ /// option_ret_generic/d88_default_generic_params/m196_facetc_generic_ /// static_typaram + targeted std/collections+time+encoding sweep — see /// docs/plans/wip/196-gen-notes.md; not exhaustive enough over the WHOLE /// corpus/flagship/nova_tests to justify an unconditional panic yet). fn resolve_result_option_ret( &self, ty: &crate::ast::TypeRef, subst: &[(String, Option<String>)], ) -> Option<String> { use crate::ast::TypeRef; let resolve_arg = |g: &TypeRef| -> Option<String> { if let TypeRef::Named { path: gp, generics: gg, .. } = g { if gg.is_empty() { if let Some(nm) = gp.last() { if let Some((_, Some(c))) = subst.iter().find(|(n, _)| n == nm) { return Some(c.clone()); } } } } self.type_ref_to_c(g).ok().filter(|c| !c.is_empty() && c != "void*") }; if let TypeRef::Named { path, generics, .. } = ty { match (path.last().map(|s| s.as_str()), generics.len()) { (Some("Result"), 2) => { let ok = resolve_arg(&generics[0])?; let err = resolve_arg(&generics[1])?; self.icr_trace("GEN196_legacy_resolve_result_option_ret_RESULT"); return Some(self.result_repr_c_type(&ok, &err)); } (Some("Option"), 1) => { let inner = resolve_arg(&generics[0])?; self.icr_trace("GEN196_legacy_resolve_result_option_ret_OPTION"); return Some(self.opt_repr_c_type(&inner)); } _ => {} } } None } /// Plan 59 Phase 5: compute mono'd tuple struct C name. /// **Length-prefixed encoding** (Itanium ABI analog): self-describing /// для любой глубины nesting, parseable без ambiguity. /// Format: `_NovaTuple_<arity>_<L1>_<T1>_<L2>_<T2>_..._<LN>_<TN>` /// — где `<Ln>` decimal byte length следующего sanitized element. /// Примеры: /// `(nova_str, nova_int)` → `_NovaTuple_2_8_nova_str_8_nova_int` /// `(Nova_X*, nova_int)` → `_NovaTuple_2_10_Nova_X_p_8_nova_int` /// `((int,int), int)` → /// `_NovaTuple_2_33__NovaTuple_2_8_nova_int_8_nova_int_8_nova_int` /// Distinguishable от legacy `_NovaTupleN` по `_` после `NovaTuple`. /// Empty tuple → unreachable (Tuple(elems) where elems.is_empty() === Unit). pub fn compute_mono_tuple_c_name(elem_c_tys: &[String]) -> String { let mut out = String::from("_NovaTuple_"); out.push_str(&elem_c_tys.len().to_string()); for c_ty in elem_c_tys { let sanitized = Self::sanitize_c_for_ident(c_ty); out.push('_'); out.push_str(&sanitized.len().to_string()); out.push('_'); out.push_str(&sanitized); } out } /// Plan 148 Ф.4 [M-codegen-unify-tuple-repr]: record that a LEGACY /// all-`nova_int` `_NovaTupleN` of the given arity is referenced, so the /// finalize pass emits its typedef on demand (the blanket `_NovaTuple1..8` /// pre-decl is retired). Returns the legacy C struct name `_NovaTupleN`. /// Idempotent — the registry is a `BTreeSet`. fn register_legacy_tuple(&self, arity: usize) -> String { self.legacy_tuple_arities.borrow_mut().insert(arity); format!("_NovaTuple{}", arity) } /// Plan 59: register mono'd tuple для emit при finalize. Returns the /// mono'd C struct name (e.g. "_NovaTuple____nova_str__nova_int"). /// Safe to call multiple times — deduplicated через HashSet. fn register_mono_tuple(&self, elem_c_tys: &[String]) -> String { let key: Vec<String> = elem_c_tys.to_vec(); let inserted = self.mono_tuple_instances.borrow_mut().insert(key); // Plan 59 Ф.7.3: sizeof estimation — warning для big tuples (>5 // elements OR >128 bytes estimated). Warns пользователя что // record-тип может быть более clear/efficient + предупреждает // cache-line stuffing. Estimate-only — actual struct sizes // depend на C alignment, conservative upper bound. if inserted { let mut est_bytes: usize = 0; for c_ty in elem_c_tys { est_bytes += Self::estimate_c_type_size_bytes(c_ty); } let too_many = elem_c_tys.len() > 5; let too_big = est_bytes > 128; if too_many || too_big { let mangled = Self::compute_mono_tuple_c_name(elem_c_tys); self.warnings.borrow_mut().push(format!( "warning: large mono'd tuple `{}` ({} элементов, ~{} bytes) — \ consider using record type для clarity и stable ABI; \ big tuples могут вызвать cache-line stuffing.", mangled, elem_c_tys.len(), est_bytes)); } } Self::compute_mono_tuple_c_name(elem_c_tys) } /// [M-fixed-array-value-semantics] (2026-07-10, D27-амендмент): compute the mono'd /// `[N]T` INLINE struct C name. Length-prefixed encoding (mirrors /// `compute_mono_tuple_c_name` — self-describing, unambiguous for any nesting depth, /// e.g. `[4][8]int`'s inner `[8]int` element name embeds cleanly): /// `_NovaFixArr_<N>_<Llen>_<elem>` — `<Llen>` = decimal byte length of the /// (sanitized) element type name that follows. Distinguishable from the mono tuple /// name by the `NovaFixArr` stem (no collision risk with `_NovaTuple_`/`_NovaTupleN`). pub fn compute_mono_fixed_array_c_name(n: usize, elem_c_ty: &str) -> String { let sanitized = Self::sanitize_c_for_ident(elem_c_ty); format!("_NovaFixArr_{}_{}_{}", n, sanitized.len(), sanitized) } /// Inverse of `compute_mono_fixed_array_c_name` — `(N, elem_c_ty)` from a mangled /// name, or `None` if `mangled` isn't a `_NovaFixArr_...` name. fn parse_mono_fixed_array_name(mangled: &str) -> Option<(usize, String)> { let rest = mangled.strip_prefix("_NovaFixArr_")?; let (n_str, after_n) = Self::take_digits(rest); if n_str.is_empty() { return None; } let n: usize = n_str.parse().ok()?; let cursor = after_n.strip_prefix('_')?; let (len_str, after_len) = Self::take_digits(cursor); if len_str.is_empty() { return None; } let len: usize = len_str.parse().ok()?; let body = after_len.strip_prefix('_')?; if body.len() != len { return None; } Some((n, Self::desanitize_c_from_ident(body))) } /// [M-fixed-array-value-semantics]: register a mono'd `[N]T` INLINE struct type for /// finalize-pass typedef emission (`typedef struct { T data[N]; } _NovaFixArr_<N>_<L>_<T>;`). /// Idempotent — dedup by `(N, elem_c_ty)`, mirrors `register_mono_tuple`. Returns the /// mono'd C struct name. fn register_mono_fixed_array(&self, n: usize, elem_c_ty: &str) -> String { self.mono_fixed_array_instances.borrow_mut().insert((n, elem_c_ty.to_string())); Self::compute_mono_fixed_array_c_name(n, elem_c_ty) } /// Plan 59 Ф.7.3: estimate sizeof for a C type string — conservative /// upper bound для sizeof warning'а. Pointer types = 8 (x64). Scalar /// types known sizes. Mono'd nested types recursive sum + alignment /// padding (8-byte aligned). Unknown types default 8. fn estimate_c_type_size_bytes(c_ty: &str) -> usize { // Pointer types — 8 bytes (x64). if c_ty.ends_with('*') || c_ty.ends_with("_p") { return 8; } match c_ty { "nova_int" | "nova_f64" | "uint64_t" | "int64_t" => 8, "nova_str" => 16, // struct { ptr, len } "nova_bool" | "nova_byte" | "uint8_t" | "int8_t" => 1, "nova_f32" | "uint32_t" | "int32_t" => 4, "uint16_t" | "int16_t" => 2, "nova_unit" => 1, other if other.starts_with("_NovaTuple_") => { // Recurse через parse_mono_tuple_elements if let Some(elems) = Self::parse_mono_tuple_elements(other) { let mut total: usize = 0; for e in &elems { total += Self::estimate_c_type_size_bytes(e); } total } else { 8 // unknown nested format } } _ => 8, // unknown user struct — conservative } } /// Plan 59: walk TypeRef recursively, register mono'd tuples encountered /// (после применения subst). Returns true если any tuple registered. /// Used после `apply_type_subst_to_ref` returns Some(...) — caller knows /// resolved type's full tuple structure and registers descendants. fn register_tuples_in_typeref(&self, ty: &crate::ast::TypeRef, subst: &[(String, Option<String>)]) { use crate::ast::TypeRef; match ty { TypeRef::Tuple(elems, _) if !elems.is_empty() => { let mut elem_cs: Vec<String> = Vec::with_capacity(elems.len()); for e in elems { if let Some(c) = Self::apply_type_subst_to_ref(e, subst) { elem_cs.push(c); // Recurse into nested types — tuple of tuples, // Option[(T, U)], Array of tuples, etc. self.register_tuples_in_typeref(e, subst); } else { return; // Cannot fully resolve — abort registration. } } self.register_mono_tuple(&elem_cs); } TypeRef::Array(inner, _) => self.register_tuples_in_typeref(inner, subst), TypeRef::Named { generics, .. } => { for g in generics { self.register_tuples_in_typeref(g, subst); } } _ => {} } } /// Plan 153.2 gap A: eagerly register a generic-TYPE instance (and any nested /// generic instances) found in a TypeRef, given a substitution for type-params. /// /// This closes the registration-timing hole: when a generic-type method's /// RETURN type is itself a generic instance (e.g. `@vmap -> MapIt[VecIter[T],T,U]`, /// `@bmap -> BoxIter[U]`), the codegen previously registered only the METHOD /// instance — the return TYPE's instance got registered only LATER, when the /// mono'd method body was drained. A chained call on the result /// (`....vmap(f).collect()`) runs the emit-side generic-instance method dispatch /// ("5b") which keys EXCLUSIVELY off `generic_type_instance_info`; at that moment /// the key is ABSENT, so 5b is skipped and the call falls through to the ERASED /// base method, whose body is emitted with EMPTY current_type_subst → `Vec[U].new()` /// mangles `U` literally → `Nova_Vec____Nova_U_p` corruption. /// /// Modeled on `register_tuples_in_typeref` (abort-on-unresolved) + the canonical /// enqueue+register block in `try_infer_variant_mono_args` (12236-12246). Inner /// instances are registered FIRST (depth-first) so forward-typedef ordering in /// `generic_type_defs_buf` stays valid (mirrors the depth-loop in drain). fn register_generic_instances_in_typeref( &self, ty: &crate::ast::TypeRef, subst: &[(String, Option<String>)], ) { use crate::ast::TypeRef; match ty { TypeRef::Named { path, generics, .. } if !generics.is_empty() => { let base = path.last().cloned().unwrap_or_default(); // Recurse into nested generic args FIRST (depth-first). for g in generics { self.register_generic_instances_in_typeref(g, subst); } // Option/Result are builtin sum-types (NovaOpt_/NovaRes_), NOT // user generic-type templates — their inner instances were already // handled by the recursion above; nothing to register for the // outer Option/Result wrapper itself. if base == "Option" || base == "Result" { return; } // Only register when the base is a user-defined generic template. if !self.generic_type_templates.contains_key(&base) { return; } // Resolve every generic arg under the subst; abort if any unresolved // (avoids emitting `Nova_..._Nova_U_p` placeholder instances — // mirrors register_tuples_in_typeref's abort and drain's guard). // Plan 153.2 Ф.2 (STAGE 2): resolve args value-AWARE so a nested // `value` generic-instance arg embeds `NovaValue_<short>` and the // enqueued mangled name matches the `type_ref_to_c` / field-side // name. Value-blind `apply_type_subst_to_ref` here would enqueue a // divergent `…____Nova_<arg>_p` instance and leave the value-form // field referencing an undefined struct. let mut args_c: Vec<String> = Vec::with_capacity(generics.len()); for g in generics { match self.value_aware_subst_to_ref(g, subst) { Some(c) => args_c.push(self.value_aware_generic_c_type(&c)), None => return, } } let mangled = Self::compute_generic_type_c_name(&base, &args_c); if !self.emitted_generic_type_instances.contains(&mangled) { let mut wl = self.generic_type_worklist.borrow_mut(); if !wl.iter().any(|(_, _, m)| m == &mangled) { wl.push((base.clone(), Self::args_lift(&args_c), mangled.clone())); } } self.generic_type_instance_info.borrow_mut() .entry(mangled) .or_insert_with(|| (base, Self::args_lift(&args_c))); } TypeRef::Array(inner, _) => { self.register_generic_instances_in_typeref(inner, subst); } // Option[T]/Result[T,E] arrive as Named with generics — covered above. TypeRef::Tuple(elems, _) => { for e in elems { self.register_generic_instances_in_typeref(e, subst); } } _ => {} } } /// [M-153.1-append-as-slice-ccfail] Span-free structural key for a `TypeRef`, /// used to distinguish method OVERLOADS by their parameter types (`TypeRef` /// derives neither `PartialEq` nor a span-free form; its span fields make a /// naive comparison useless). Only needs to be injective enough to tell /// `tag(int)` from `tag(str)` — surface path + nesting, no spans. fn type_ref_overload_key(t: &crate::ast::TypeRef) -> String { use crate::ast::TypeRef as T; match t { T::Named { path, generics, .. } => { let mut s = path.join("."); if !generics.is_empty() { s.push('['); s.push_str(&generics.iter().map(Self::type_ref_overload_key) .collect::<Vec<_>>().join(",")); s.push(']'); } s } T::Array(inner, _) => format!("[]{}", Self::type_ref_overload_key(inner)), T::FixedArray(n, inner, _) => format!("[{}]{}", n, Self::type_ref_overload_key(inner)), T::Tuple(elems, _) => format!("({})", elems.iter().map(Self::type_ref_overload_key).collect::<Vec<_>>().join(",")), T::Func { params, return_type, .. } => format!("fn({})->{}", params.iter().map(Self::type_ref_overload_key).collect::<Vec<_>>().join(","), return_type.as_ref().map(|r| Self::type_ref_overload_key(r)).unwrap_or_default()), T::Protocol { .. } => "protocol".to_string(), T::Unit(_) => "()".to_string(), T::Readonly(inner, _) => format!("ro {}", Self::type_ref_overload_key(inner)), T::Mut(inner, _) => format!("mut {}", Self::type_ref_overload_key(inner)), // §10a rename (Plan 174.5, 2026-07-11): `Uninit` wrapping `Func` // keeps the `unsafe` spelling (D216 §10 legacy fn-pointer // shape); any other payload renders as `uninit` (the renamed // possibly-uninit data modifier). T::Uninit(inner, _) => { let kw = if matches!(inner.as_ref(), T::Func { .. }) { "unsafe" } else { "uninit" }; format!("{} {}", kw, Self::type_ref_overload_key(inner)) } T::Pointer(inner, _) => format!("*{}", Self::type_ref_overload_key(inner)), // Plan 184: `ref T` distinct overload key (Р13/Р14 mode axis — the // ref-ness of the target participates in structural distinction). T::Ref(inner, _) => format!("ref {}", Self::type_ref_overload_key(inner)), } } /// Plan 48: compute the monomorphized C name. /// compute_mono_name("nova_fn_within", [("T","nova_int")]) → "nova_fn_within____nova_int" fn compute_mono_name(base_c_name: &str, type_subst: &[(String, String)]) -> String { if type_subst.is_empty() { return base_c_name.to_string(); } let args: String = type_subst.iter() .map(|(_, c_ty)| Self::sanitize_c_for_ident(c_ty)) .collect::<Vec<_>>() .join("__"); format!("{}____{}", base_c_name, args) } /// [M-176-generic-wrapper-mono-inference] Does the generic type `type_name` /// have any field whose erased form becomes `void*` — the exact condition /// (`has_void_ptr_fields`, `emit_generic_method_erased`) under which the /// erased base method is emitted as a NULL/`{0}` STUB (`emit_generic_static /// _method_stub`). Used to gate inference-based ctor monomorphization to the /// stub-only case (types whose erased dispatch works are left byte-identical). fn generic_type_has_voidptr_fields(&self, type_name: &str) -> bool { let template = match self.generic_type_templates.get(type_name) { Some(t) => t, None => return false, }; use crate::ast::{TypeDeclKind, TypeRef}; let fields = match &template.kind { TypeDeclKind::Record(fields) => fields, _ => return false, }; let type_params: HashSet<String> = template.generics.iter().map(|g| g.name.clone()).collect(); fields.iter().any(|fld| { if matches!(&fld.ty, TypeRef::Pointer(..) | TypeRef::Mut(..) | TypeRef::Uninit(..)) && Self::type_ref_uses_any_type_param(&fld.ty, &type_params) { return true; } if Self::type_ref_uses_any_type_param(&fld.ty, &type_params) && Self::type_ref_contains_func(&fld.ty) { return true; } if let TypeRef::Named { path, generics, .. } = &fld.ty { if path.len() == 1 && generics.is_empty() && type_params.contains(&path[0]) { return true; } !generics.is_empty() && generics.iter().any(|g| Self::type_ref_uses_any_type_param(g, &type_params)) } else if let TypeRef::Array(inner, _) = &fld.ty { if let TypeRef::Named { generics, .. } = inner.as_ref() { !generics.is_empty() && generics.iter().any(|g| Self::type_ref_uses_any_type_param(g, &type_params)) } else { false } } else { false } }) } /// [M-176-generic-wrapper-mono-inference] Constructor-argument type-arg /// inference for a generic wrapper type. When a generic type's static method /// is called WITHOUT explicit turbofish (`Wrap.new(x)`, `BufWriter.new(w)`), /// the erased dispatch (`Nova_Wrap_static_new(void*)`) is a NULL stub for any /// type with void-ptr fields → runtime crash / CC-FAIL. Mirror the turbofish /// static path (~27242): infer the concrete type-args from the argument C /// types, register the mono instance + worklist, and dispatch to the /// monomorphized static method. Gated on the stub case only /// (`generic_type_has_voidptr_fields`) so non-stub generic types keep their /// existing (byte-identical) erased dispatch. Returns `Ok(None)` when the /// type is non-stub, has no matching static method, or the type-args cannot /// be fully inferred from the args — the caller then keeps the legacy path. fn try_generic_static_ctor_mono( &mut self, base_name: &str, method: &str, args: &[crate::ast::CallArg], ) -> Result<Option<String>, String> { // Gate: only intervene where the erased base is a NULL stub. if !self.generic_type_has_voidptr_fields(base_name) { return Ok(None); } let template = match self.generic_type_templates.get(base_name).cloned() { Some(t) => t, None => return Ok(None), }; // The static method decl to monomorphize. let fn_decl = match self.generic_type_methods.get(base_name) .and_then(|ms| ms.iter().find(|m| m.name == *method && !matches!(m.receiver.as_ref().map(|r| &r.kind), Some(crate::ast::ReceiverKind::Instance)))) .cloned() { Some(f) => f, None => return Ok(None), }; // Infer each template type-param from the argument C types. let mut subst_pending: Vec<(String, Option<String>)> = template.generics.iter().map(|g| (g.name.clone(), None)).collect(); for (param, arg) in fn_decl.params.iter().zip(args.iter()) { let arg_c = self.infer_expr_c_type(arg.expr()); self.infer_type_param_binding(¶m.ty, &arg_c, &mut subst_pending); } // [M-property-testing-rot] (Plan 172.13 батч 3): TYPE-level bound // structural inference — emit twin of `infer_generic_static_ctor_ret`'s // pass: `type ArrayGen[G Generator[T], T]` ctor binds `G` from the arg, // `T` only through `G`'s bound (concrete return of the resolved `G`'s // `generate()`). if subst_pending.iter().any(|(_, c)| c.is_none()) { for gp in &template.generics { let resolved_c = match subst_pending.iter().find(|(n, _)| n == &gp.name) { Some((_, Some(c))) => c.clone(), _ => continue, }; for bound_ref in &gp.bounds { if let crate::ast::TypeRef::Named { path, generics: bgens, .. } = bound_ref { if bgens.is_empty() { continue; } let proto_name = path.join("_"); if self.protocol_method_registry.contains_key(&proto_name) { self.infer_protocol_structural_binding( &proto_name, bgens, &resolved_c, &mut subst_pending, ); } } } } } // Require every template generic to be inferable; otherwise fall back. if subst_pending.iter().any(|(_, c)| c.is_none()) { return Ok(None); } let type_args_c: Vec<String> = subst_pending.iter() .map(|(_, c)| c.clone().unwrap()) .collect(); if type_args_c.iter().any(|c| c.is_empty() || c == "void*") { return Ok(None); } let type_subst: Vec<(String, String)> = subst_pending.into_iter() .map(|(n, c)| (n, c.unwrap())) .collect(); let mangled = Self::compute_generic_type_c_name(base_name, &type_args_c); self.generic_type_instance_info.borrow_mut() .entry(mangled.clone()) .or_insert_with(|| (base_name.to_string(), Self::args_lift(&type_args_c))); if !self.emitted_generic_type_instances.contains(&mangled) { let mut wl = self.generic_type_worklist.borrow_mut(); if !wl.iter().any(|(_, _, m)| m == &mangled) { wl.push((base_name.to_string(), Self::args_lift(&type_args_c), mangled.clone())); } } let method_c_name = format!("{}_static_{}", mangled, method); // Emit args with the type-substitution active (array-literal element // types flow through), mirroring the turbofish static path. // Plan 172.12 A1″: seed with structural RT from the channel where it lowers // byte-identically (else Raw debt) — `args` carry `ExprId`, so their // `resolved_types` RT unifies against the template param `TypeRef`s. let slot_names: Vec<String> = template.generics.iter().map(|g| g.name.clone()).collect(); let rt_slots = self.rt_slots_from_args( fn_decl.params.iter().map(|p| &p.ty), args, &slot_names); let seeded = self.subst_map_adopt_rt(&type_subst, &rt_slots); let saved_subst = std::mem::replace(&mut self.current_type_subst, seeded); let mut arg_strs = Vec::new(); for (i, a) in args.iter().enumerate() { let arg_expr = a.expr(); let target_c = if matches!(&arg_expr.kind, crate::ast::ExprKind::ArrayLit(_)) { fn_decl.params.get(i) .and_then(|p| self.type_ref_to_c(&p.ty).ok()) .filter(|c| !c.is_empty() && c != "void*") } else { None }; match target_c { Some(tc) => arg_strs.push(self.emit_expr_with_target_type(arg_expr, &tc)?), None => arg_strs.push(self.emit_expr(arg_expr)?), } } self.current_type_subst = saved_subst; // Pre-register any generic instances referenced by the return type. if let Some(rt) = fn_decl.return_type.as_ref() { let subst_opt: Vec<(String, Option<String>)> = type_subst.iter() .map(|(n, c)| (n.clone(), Some(c.clone()))) .collect(); self.register_generic_instances_in_typeref(rt, &subst_opt); } let recv_type_stripped = Self::debt_strip_nova_prefix(&mangled).to_string(); self.register_mono_method_instance( &fn_decl, type_subst, &method_c_name, &recv_type_stripped); Ok(Some(format!("{}({})", method_c_name, arg_strs.join(", ")))) } /// [M-176-generic-wrapper-mono-inference] Inference-side twin of /// `try_generic_static_ctor_mono`: for `Wrap.new(x)` / `BufWriter.new(w)` /// (a stub-only generic wrapper ctor without turbofish), return the /// monomorphized instance C type (`Nova_Wrap____nova_str*`) so the LHS local /// is declared with the mono type and its instance methods dispatch to the /// mono bodies (not the erased NULL stubs). Immutable — no registration. /// `None` when the type is non-stub, has no matching static method, or the /// type-args cannot be fully inferred from the args. fn infer_generic_static_ctor_ret( &self, base_name: &str, method: &str, args: &[crate::ast::CallArg], ) -> Option<String> { if !self.generic_type_has_voidptr_fields(base_name) { return None; } let tmpl = self.generic_type_templates.get(base_name)?; let fn_decl = self.generic_type_methods.get(base_name)? .iter() .find(|m| m.name == *method && !matches!(m.receiver.as_ref().map(|r| &r.kind), Some(crate::ast::ReceiverKind::Instance)))?; let mut pend: Vec<(String, Option<String>)> = tmpl.generics.iter().map(|g| (g.name.clone(), None)).collect(); for (param, arg) in fn_decl.params.iter().zip(args.iter()) { let arg_c = self.infer_expr_c_type(arg.expr()); self.infer_type_param_binding(¶m.ty, &arg_c, &mut pend); } // [M-property-testing-rot] (Plan 172.13 батч 3): TYPE-level bound // structural inference — `type ArrayGen[G Generator[T], T]` with ctor // `ArrayGen[G, T].default(elem G)`: `G` binds from the arg, `T` only // through `G`'s bound (`Generator[T]` ⇒ `T` = concrete return of the // resolved `G`'s `generate()`). Mirror of resolve_mono_type_args' // Source 2e-bis for the static-ctor channel. if pend.iter().any(|(_, c)| c.is_none()) { for gp in &tmpl.generics { let resolved_c = match pend.iter().find(|(n, _)| n == &gp.name) { Some((_, Some(c))) => c.clone(), _ => continue, }; for bound_ref in &gp.bounds { if let crate::ast::TypeRef::Named { path, generics: bgens, .. } = bound_ref { if bgens.is_empty() { continue; } let proto_name = path.join("_"); if self.protocol_method_registry.contains_key(&proto_name) { self.infer_protocol_structural_binding( &proto_name, bgens, &resolved_c, &mut pend, ); } } } } } if pend.iter().any(|(_, c)| c.is_none()) { return None; } let type_args_c: Vec<String> = pend.iter().map(|(_, c)| c.clone().unwrap()).collect(); if type_args_c.iter().any(|c| c.is_empty() || c == "void*") { return None; } let mangled = Self::compute_generic_type_c_name(base_name, &type_args_c); // [M-property-testing-rot] (Plan 172.13 батч 3): register the inferred // instance's metadata so downstream structural inference // (`infer_protocol_structural_binding` Case A) can recover // (base, type-args) from the mono name — a `ro gen = ArrayGen.default(..)` // binding's type flows into `property(gen, ...)`'s bound-based `T` // resolution. Metadata-only (idempotent); worklist emission stays with // the emit-side channels. self.generic_type_instance_info.borrow_mut() .entry(mangled.clone()) .or_insert_with(|| (base_name.to_string(), Self::args_lift(&type_args_c))); let mono_ptr = format!("{}*", mangled); // Resolve the declared return type under the substitution. A ctor // returning `Self` erases to `Nova_<base>*`; remap that to the mono ptr. if let Some(rt) = fn_decl.return_type.as_ref() { if let Some(c_ty) = Self::apply_type_subst_to_ref(rt, &pend) { if !c_ty.is_empty() && c_ty != "void*" { if c_ty == format!("Nova_{}*", base_name) { return Some(mono_ptr); } return Some(c_ty); } } } Some(mono_ptr) } /// Plan 48 Ф.7.4 (partial): try to infer mono type-args for a bare variant /// constructor call like `Ok2(42)` where the parent sum-type is generic. /// Returns (parent_type_name, mangled_instance_c_name, type_args_c) when: /// - the variant's parent type is a generic template /// - the variant is tuple-shaped with at least one positional arg /// - every generic param can be inferred from a corresponding arg's C type /// On success, also enqueues the instance for mono emission. /// /// Returns None for unit variants (`Err2`) or when inference is incomplete — /// caller falls back to the erased emit path. Unit-variant inference would /// need usage-context propagation (deferred to V2). fn try_infer_variant_mono_args( &self, variant_name: &str, args: &[crate::ast::CallArg], ) -> Option<(String, String, Vec<String>)> { use crate::ast::{TypeDeclKind, SumVariantKind}; // 1. Find parent sum-type by variant name. // Plan 62.A.bis Ф.2.2: registry-driven variant resolution. let (parent_type, _) = self.sum_schema_registry.find_variant_compat(variant_name)?; // 2. Must be a generic template (has type params). let template = self.generic_type_templates.get(&parent_type)?.clone(); if template.generics.is_empty() { return None; } // 3. Must be Sum with the variant present and tuple-shaped. let variants = match &template.kind { TypeDeclKind::Sum(vs) => vs, _ => return None, }; let variant = variants.iter().find(|v| v.name == variant_name)?; let field_types = match &variant.kind { SumVariantKind::Tuple(tys) => tys.clone(), // Unit / record variants are out of scope for this helper. _ => return None, }; if field_types.is_empty() || args.is_empty() { return None; } // 4. Infer subst[T] from each (declared field type, arg C type). let mut subst: Vec<(String, Option<String>)> = template.generics.iter() .map(|g| (g.name.clone(), None)) .collect(); for (field_ty, arg) in field_types.iter().zip(args.iter()) { let arg_c = self.infer_expr_c_type(arg.expr()); self.infer_type_param_binding(field_ty, &arg_c, &mut subst); } // 5. Require every generic param resolved. let type_args_c: Vec<String> = subst.iter() .map(|(_, opt)| opt.clone()) .collect::<Option<Vec<String>>>()?; // 6. Compute mangled instance name and enqueue for mono emit. let mangled = Self::compute_generic_type_c_name(&parent_type, &type_args_c); if !self.emitted_generic_type_instances.contains(&mangled) { let mut wl = self.generic_type_worklist.borrow_mut(); if !wl.iter().any(|(_, _, m)| m == &mangled) { wl.push((parent_type.clone(), Self::args_lift(&type_args_c), mangled.clone())); } } self.generic_type_instance_info.borrow_mut() .entry(mangled.clone()) .or_insert_with(|| (parent_type.clone(), Self::args_lift(&type_args_c))); Some((parent_type, mangled, type_args_c)) } /// реестр 221.1 №502: twin of `try_infer_variant_mono_args` just above, for a /// bare GENERIC named-tuple constructor call (`Ent(k, v)` where `type Ent[K, V] /// (key K, value V)`), no turbofish. The checker type-checks the generic /// DECLARATION exactly once (never re-runs per `Box2[str, int]` mono instance), /// so there is no per-instance `resolved_types` entry to read here — infer /// type-args STRUCTURALLY from (declared field type, arg C type) pairs, same /// as the sum-variant twin. On success also enqueues the instance into /// `generic_type_worklist` (idempotent) so `emit_generic_type_instance_body`'s /// `TypeDeclKind::NamedTuple` arm emits its struct body + `nova_ctor_<short>` /// constructor function — see that arm's doc for why a NAMED TUPLE /// constructor call needs routing here at all (the non-generic sibling never /// did: `type_aliases.get(name)` — checked by the caller BEFORE this — only /// ever gets a "NovaTuple_" entry from the non-generic `emit_named_tuple_type` /// path, never from a generic template). fn try_infer_named_tuple_mono_args( &self, type_name: &str, args: &[crate::ast::CallArg], ) -> Option<(String, Vec<String>)> { use crate::ast::TypeDeclKind; let template = self.generic_type_templates.get(type_name)?.clone(); if template.generics.is_empty() { return None; } let fields = match &template.kind { TypeDeclKind::NamedTuple(fs) => fs.clone(), _ => return None, }; if fields.is_empty() || args.is_empty() { return None; } let mut subst: Vec<(String, Option<String>)> = template.generics.iter() .map(|g| (g.name.clone(), None)) .collect(); for (i, arg) in args.iter().enumerate() { let field_and_val: Option<(&crate::ast::TypeRef, &crate::ast::Expr)> = match arg { crate::ast::CallArg::Named { name: fname, value } => fields.iter().find(|f| &f.name == fname).map(|f| (&f.ty, value)), crate::ast::CallArg::Item(e) => fields.get(i).map(|f| (&f.ty, e)), crate::ast::CallArg::Spread(_) => None, }; if let Some((field_ty, val_expr)) = field_and_val { let arg_c = self.infer_expr_c_type(val_expr); self.infer_type_param_binding(field_ty, &arg_c, &mut subst); } } let type_args_c: Vec<String> = subst.iter() .map(|(_, opt)| opt.clone()) .collect::<Option<Vec<String>>>()?; if type_args_c.iter().any(|c| c.is_empty() || c == "void*") { return None; } let mangled = Self::compute_generic_type_c_name(type_name, &type_args_c); if !self.emitted_generic_type_instances.contains(&mangled) { let mut wl = self.generic_type_worklist.borrow_mut(); if !wl.iter().any(|(_, _, m)| m == &mangled) { wl.push((type_name.to_string(), Self::args_lift(&type_args_c), mangled.clone())); } } self.generic_type_instance_info.borrow_mut() .entry(mangled.clone()) .or_insert_with(|| (type_name.to_string(), Self::args_lift(&type_args_c))); Some((mangled, type_args_c)) } /// Plan 48 Ф.0: resolve concrete type args for a generic fn call. /// Returns Vec<(param_name, c_type)> or Err with a helpful message (R5). /// Priority: turbofish > arg-type inference > return-type context. /// /// **[Q10, Plan 196.3 wave-2, 2026-07-12] Судьба: ВТОРОЕ ОКНО, Tier-2 /// СТРУКТУРНО ЗАБЛОКИРОВАНО (не мигрирована в этом окне).** Это не /// "mono-lowering" (механическая подстановка уже-решённого) — тело /// делает РЕАЛЬНУЮ unification (param `TypeRef` ⋂ arg C-type → T), /// т.е. повторяет инференс, который чекер обязан выполнить для /// type-check'а вызова generic-функции (`f1_check_call`, /// `types/mod.rs` ~10242), но НЕ сохраняет: guard `typeref_mentions_any` /// (`types/mod.rs` ~10478 — ВНУТРИ forbidden-зоны Q10, маркер 10452 /// лежит буквально в теле той же `f1_check_call`) явно пропускает /// generic-возврат («they'd need type-subst, not available here»). /// rustc-аналогия «mono = подстановка, не инференс» здесь НЕ применима /// буквально: у rustc typeck уже пишет `node_substs`; у нас channel для /// per-call generic type-args ОТСУТСТВУЕТ — то, что происходит здесь /// СЕЙЧАС, суть повтор инференса под видом «lowering» (ранняя карта /// `196.wave2-progress.md` строка `resolve_mono_type_args` называла это /// «✅ остаётся (lowering-subst)» — уточнено этим заходом: не lowering). /// Подтверждает Tier-2-находку Q5 (`196.3-wave2-d-driven.md` §«СТРУКТУРНАЯ /// НАХОДКА») для «mono-резолверов» конкретным кодовым доказательством. /// Постройка недостающего канала (per-call `ExprId` → `Vec<ResolvedType>`, /// аналог `node_substs`) требует правки `f1_check_call`/instance-method /// сиблинга — запрещённой для Q10 зоны; решение о постройке — за /// владельцем (см. Q5 «Развилка (A)/(B)»). Легаси НЕ трогается. См. также /// `resolve_method_level_subst` (~19355) — идентичный вердикт; три /// независимых hand-duplicated инференс-движка (эта функция + /// `resolve_method_level_subst` + инлайн instance-method dispatch) /// исторически чинились СИНХРОННО вручную (см. Source 4 ниже, /// «[M-exp-promotion-blockers: retry]»), что и есть симптом второго окна. /// /// **[Plan 196.5 Stage-B1 update]** канал построен (Stage-A, `node_substs`) — /// владелец санкционировал развилку (A). Этот легаси-body ОСТАЁТСЯ нетронутым /// (fallback + Stage-B propose-then-verify verifier); два из трёх вызывающих /// сайтов (вне замороженной зоны волны-1) теперь идут через channel-first /// `resolve_mono_type_args_ch` (ниже, ~19315), которая вызывает ЭТУ функцию как /// легаси-сторону гейта. Смотри `resolve_mono_type_args_ch` doc — там же метрика /// `NOVA_NODE_SUBSTS_TRACE` hit/fallback. fn resolve_mono_type_args( &self, fn_decl: &crate::ast::FnDecl, turbofish_refs: &[crate::ast::TypeRef], args: &[crate::ast::CallArg], ) -> Result<Vec<(String, String)>, String> { let type_params: Vec<String> = fn_decl.generics.iter().map(|g| g.name.clone()).collect(); if type_params.is_empty() { return Ok(vec![]); } // Initialize with None slots let mut subst: Vec<(String, Option<String>)> = type_params.iter() .map(|n| (n.clone(), None)) .collect(); // Source 1: turbofish (highest priority) for (i, tr) in turbofish_refs.iter().enumerate() { if i < subst.len() { // [M-vec-of-fn-newtype-codegen] (реестр 221.1 №47): a BARE turbofish name // (`alloc_buf[T](n)` called from inside `Vec[T]`'s own mono'd body, reusing // the ENCLOSING instantiation's own type-param letter) that is a DIRECT hit // in `type_subst_overrides`/`current_type_subst` is a REAL, final binding — // resolved through the very first check `resolved_named_to_c` makes (before // any protocol/generic-template/erasure fallback can produce a placeholder // "void*"). Checked BEFORE (and bypassing) the general `c_ty != "void*"` // guard below: that guard exists to reject a GENUINELY erased/placeholder // result (e.g. an unresolved protocol-generic argument), which this direct // subst-lookup structurally cannot be — so its "void*" (the true, permanent // C representation of a closure/fn-newtype element, T=QH) must not be // thrown away here the way an actually-ambiguous "void*" would be. if let crate::ast::TypeRef::Named { path, generics, .. } = tr { if generics.is_empty() { if let Some(name) = path.last() { let direct_hit = self.type_subst_overrides.borrow().get(name.as_str()).cloned() .or_else(|| self.subst_c(name.as_str())); if let Some(c) = direct_hit { if !c.is_empty() { subst[i].1 = Some(c); continue; } } } } } if let Ok(c_ty) = self.type_ref_to_c(tr) { if !c_ty.is_empty() && c_ty != "void*" { subst[i].1 = Some(c_ty); } } } } // Source 2: infer from actual arg types. // D109 Ф.7.7: two-pass — non-array params first so that a direct `target K` binding // (e.g. K = Nova_GrmPoint*) is set before the array param `items []K` would infer // K = nova_int from the erased NovaArray_nova_int* runtime representation. for (param, arg) in fn_decl.params.iter().zip(args.iter()) { if !matches!(param.ty, crate::ast::TypeRef::Array(..)) { let arg_c = self.infer_expr_c_type(arg.expr()); self.infer_type_param_binding(¶m.ty, &arg_c, &mut subst); } } for (param, arg) in fn_decl.params.iter().zip(args.iter()) { if matches!(param.ty, crate::ast::TypeRef::Array(..)) { let arg_c = self.infer_expr_c_type(arg.expr()); self.infer_type_param_binding(¶m.ty, &arg_c, &mut subst); } } // Source 2-rt [M-vec-of-fn-newtype-codegen] (реестр 221.1 №47): STRUCTURAL // fallback for a type-param that appears ONLY under a raw/typed pointer // (`*T`/`*mut T`) whose STRING C-type is opaque (`void*`/empty — e.g. // `RawMem.copy_n_nonoverlapping[T](src *T, dst *mut T, ..)` called with // `Vec[QH]`'s `@data`/growth-buffer args, QH a fn-newtype whose C repr genuinely // IS `void*`). Source 2 above (string `infer_type_param_binding`) legitimately // declines on `void*`/empty — it cannot tell "opaque-by-design closure repr" // apart from "erased/unresolved" at the string layer, and the latter really // must stay rejected. The checker channel (`resolved_types[ExprId]`, D315) keeps // the FULL structural type (`TypedPtr(_, Named{QH})`) regardless of how it // C-erases — unify the callee's declared param TypeRefs against that RT // (`infer_type_param_binding_rt`'s Pointer arm) and, for any name this // additionally resolves, lower the bound RT back to a C-string and adopt it. // Gated to fill ONLY still-`None` slots — a slot Source 2 already bound (from a // genuinely concrete arg) is never touched, so the existing Vec[int]/Vec[struct] // path is byte-identical (no arg there ever hits the RT path: `channel_arg_rt` // only fires when the slot is still unresolved, which never happens for them). if subst.iter().any(|(_, v)| v.is_none()) { let rt_slots = self.rt_slots_from_args( fn_decl.params.iter().map(|p| &p.ty), args, &type_params, ); for (name, rt_opt) in rt_slots { let Some(rt) = rt_opt else { continue }; if let Some(slot) = subst.iter_mut().find(|(n, v)| n == &name && v.is_none()) { if let Ok(c) = self.resolved_type_to_c(&rt) { if !c.is_empty() { slot.1 = Some(c); } } } } } // Source 2b: for fn-typed params, infer return type T from closure arg body. // Handles `body fn() Fail[E] -> T` where arg is `|| 42` → T = nova_int. for (param, arg) in fn_decl.params.iter().zip(args.iter()) { if let crate::ast::TypeRef::Func { return_type: Some(ret_ty_ref), .. } = ¶m.ty { let closure_ret_c = match &arg.expr().kind { ExprKind::ClosureLight { body, .. } => match body { crate::ast::ClosureBody::Expr(e) => { let t = self.infer_expr_c_type(e); if t.is_empty() || t == "void*" { String::new() } else { t } } crate::ast::ClosureBody::Block(b) => b.trailing.as_ref() .map(|e| self.infer_expr_c_type(e)) .filter(|t| !t.is_empty() && t != "void*") .unwrap_or_default(), }, ExprKind::ClosureFull(sb) => sb.return_type.as_ref() .and_then(|rt| self.type_ref_to_c(rt).ok()) .filter(|t| !t.is_empty() && t != "void*") .unwrap_or_default(), _ => String::new(), }; if !closure_ret_c.is_empty() { self.infer_type_param_binding(ret_ty_ref.as_ref(), &closure_ret_c, &mut subst); } } } // Plan 55 Ф.1 (Source 2b-array): for `[]fn(P...) -> T` params, infer T // from the array literal's first closure element. Without this, the // `[]T` Source 2 above sees concrete=`NovaArray_void_p*` and would // bind T = "void_p", which is wrong. This source overrides with the // actual closure return type. for (param, arg) in fn_decl.params.iter().zip(args.iter()) { if let crate::ast::TypeRef::Array(inner, _) = ¶m.ty { if let crate::ast::TypeRef::Func { return_type: Some(ret_ty_ref), .. } = inner.as_ref() { // Find first closure-literal element of the array arg. let closure_ret_c: String = if let ExprKind::ArrayLit(elems) = &arg.expr().kind { elems.iter().find_map(|e| { let ArrayElem::Item(expr) = e else { return None; }; match &expr.kind { ExprKind::ClosureLight { body, .. } => match body { crate::ast::ClosureBody::Expr(ce) => { let t = self.infer_expr_c_type(ce); if t.is_empty() || t == "void*" { None } else { Some(t) } } crate::ast::ClosureBody::Block(b) => b.trailing.as_ref() .map(|ce| self.infer_expr_c_type(ce)) .filter(|t| !t.is_empty() && t != "void*"), }, ExprKind::ClosureFull(sb) => sb.return_type.as_ref() .and_then(|rt| self.type_ref_to_c(rt).ok()) .filter(|t| !t.is_empty() && t != "void*"), _ => None, } }).unwrap_or_default() } else if let ExprKind::Ident(name) = &arg.expr().kind { // Variable holding a []fn(...) -> T value — look up element sig. self.array_param_fn_sigs.get(name) .map(|(_, r)| r.clone()) .filter(|t| !t.is_empty() && t != "void*" && t != "nova_unit") .unwrap_or_default() } else { String::new() }; if !closure_ret_c.is_empty() { // Clear any prior "void_p" binding before re-binding to the real T. if let Some(name) = match ret_ty_ref.as_ref() { crate::ast::TypeRef::Named { path, generics, .. } if generics.is_empty() => Some(path.join("_")), _ => None, } { if let Some(slot) = subst.iter_mut().find(|(n, _)| n == &name) { if slot.1.as_deref() == Some("void_p") { slot.1 = None; } } } self.infer_type_param_binding(ret_ty_ref.as_ref(), &closure_ret_c, &mut subst); } } } } // Plan 54 Ф.5 (Source 2d): для fn-typed param когда arg — // variable reference (не closure literal). Если var_types/ // fn_param_sigs знают return-type of variable's closure, можем // infer T. Пример: nested generic call `with_timeout[T] calls // within(ms, body)` — body's return type из fn_param_sigs уже // substituted (если we внутри mono'd with_timeout body), используем // его чтобы infer within's T. for (param, arg) in fn_decl.params.iter().zip(args.iter()) { if let crate::ast::TypeRef::Func { return_type: Some(ret_ty_ref), .. } = ¶m.ty { if let ExprKind::Ident(name) = &arg.expr().kind { if let Some((_, ret_c)) = self.fn_param_sigs.get(name) { if !ret_c.is_empty() && ret_c != "void*" && ret_c != "nova_unit" { self.infer_type_param_binding(ret_ty_ref.as_ref(), ret_c, &mut subst); } } } } } // Source 2f ([M-property-testing-rot], Plan 172.13 батч 3): nested // generic call inside a MONO'd generic body forwarding the enclosing // fn's params (`property[T]` body → `property_with(gen, body, cfg)`). // C-type inference is useless for a protocol-typed param (erased // `void*`); unify the callee's declared param TypeRefs against the // CALLER's declared param TypeRefs instead, lowering leaves through // `current_type_subst` (T_caller → concrete C). for (param, arg) in fn_decl.params.iter().zip(args.iter()) { if subst.iter().all(|(_, v)| v.is_some()) { break; } if let ExprKind::Ident(name) = &arg.expr().kind { if let Some(decl_ref) = self.current_fn_param_typerefs.get(name).cloned() { self.infer_type_param_binding_from_ref(¶m.ty, &decl_ref, &mut subst); } } } // Source 2c: for generic-type params (e.g. `box_get[T](b Box[T])`), extract T from // monomorphized instance info. After Ф.3, Box[int] arg has C type Nova_Box____nova_int* // and generic_type_instance_info maps "Nova_Box____nova_int" → ("Box", ["nova_int"]). for (param, arg) in fn_decl.params.iter().zip(args.iter()) { if let crate::ast::TypeRef::Named { generics, .. } = ¶m.ty { if generics.is_empty() { continue; } let arg_c = self.infer_expr_c_type(arg.expr()); let key = arg_c.trim_end_matches('*').trim().to_string(); let instance_args: Option<Vec<crate::types::ResolvedType>> = self.generic_type_instance_info .borrow() .get(&key) .map(|(_, args)| args.clone()); if let Some(iargs) = instance_args { if iargs.len() == generics.len() { for (gen_ty, c_ty) in generics.iter().zip(iargs.iter()) { // A1‴: registry arg is `ResolvedType` — lower to C-name. self.infer_type_param_binding(gen_ty, &self.arg_c(c_ty), &mut subst); } } } } } // Source 2e (Plan 72 P2-B): structural bound inference. // For `fn foo[U, T Iter[U]](it T)` when T is resolved but U isn't: // iterate over method_overloads for the concrete T type and try to extract // the bound-generic values from method return types. // Handles the `Iter[U]` pattern: T is bound by `Iter[U]`, T is resolved to // `Nova_IntCounter*`, look at `next() -> NovaOpt_nova_int*` → U = nova_int. { let still_unresolved: std::collections::HashSet<String> = subst.iter() .filter(|(_, v)| v.is_none()) .map(|(n, _)| n.clone()) .collect(); if !still_unresolved.is_empty() { // Collect candidates: (bound_generic_name, candidate_c_type) let mut candidates: Vec<(String, String)> = Vec::new(); for gp in &fn_decl.generics { // Only look at resolved type params that have a bound let resolved_c = match subst.iter().find(|(n, _)| n == &gp.name) { Some((_, Some(c))) => c.clone(), _ => continue, }; // Plan 101.3: multi-bound — для codegen mono-dispatch // используем первый bound (typical use). Дополнительные // bounds участвуют только в type-check satisfaction. let bound_ref = match gp.bounds.first() { Some(b) => b, None => continue, }; // Extract Nova type name from resolved C type let nova_name = { let t = resolved_c.trim_end_matches('*').trim(); Self::debt_strip_nova_prefix_or_empty(t).to_string() }; if nova_name.is_empty() { continue; } // Get the bound's generic params (e.g., `Iter[U]` → generics=[U]) let bound_generics = match bound_ref { crate::ast::TypeRef::Named { generics, .. } => generics, _ => continue, }; // Find which positions in bound_generics are unresolved type params let mut unresolved_at: Vec<(usize, String)> = Vec::new(); for (i, bg) in bound_generics.iter().enumerate() { if let crate::ast::TypeRef::Named { path, generics, .. } = bg { if generics.is_empty() { let name = path.join("_"); if still_unresolved.contains(&name) { unresolved_at.push((i, name)); } } } } if unresolved_at.is_empty() { continue; } // Gather all instance methods of the concrete type let methods: Vec<(String, String)> = self.method_overloads.iter() .filter(|((t, _), _)| t == &nova_name) .flat_map(|((_, m), sigs)| { sigs.iter() .filter(|s| s.is_instance) .map(move |s| (m.clone(), s.return_c_type.clone())) }) .collect(); // For each unresolved-at position, try extracting from method return types for (pos, u_name) in &unresolved_at { for (_, ret_c) in &methods { if ret_c.is_empty() || ret_c == "void*" { continue; } // Position 0 in bound (e.g., Iter[U]): the return type wraps U // in Option → `NovaOpt_X*` or directly `Nova_X*`. if *pos == 0 { if let Some(rest) = ret_c.strip_prefix("NovaOpt_") { // `next() -> Option[U]` lowers to the value // type `NovaOpt_<U>` (no `*`); a boxed form // `NovaOpt_<U>*` may also occur. Accept both. let inner = rest.strip_suffix('*').unwrap_or(rest); if !inner.is_empty() && inner != "void" { candidates.push((u_name.clone(), inner.to_string())); break; } } } } } } // Apply candidates (do not overwrite already-resolved slots) for (name, c_ty) in candidates { if let Some(slot) = subst.iter_mut().find(|(n, v)| n == &name && v.is_none()) { slot.1 = Some(c_ty); } } } } // Source 2e-bis ([M-property-testing-rot], Plan 172.13 батч 3): GENERAL // structural bound inference — supersedes 2e's narrow `Iter[U].next() // -> Option[U]` shape. For `fn property[G Generator[T], T](gen G, ...)` // with `G` resolved to a concrete generator (mono instance like // `ArrayGen[IntGen, int]` OR a plain type like `IntGen`): walk the // BOUND protocol's method signatures against the concrete type's // same-named methods (`infer_protocol_structural_binding`) and bind // the bound's generic positions (e.g. `T` from // `@generate() -> T` ⇒ concrete return of the resolved `G`). if subst.iter().any(|(_, v)| v.is_none()) { for gp in &fn_decl.generics { let resolved_c = match subst.iter().find(|(n, _)| n == &gp.name) { Some((_, Some(c))) => c.clone(), _ => continue, }; for bound_ref in &gp.bounds { if let crate::ast::TypeRef::Named { path, generics: bgens, .. } = bound_ref { if bgens.is_empty() { continue; } let proto_name = path.join("_"); if self.protocol_method_registry.contains_key(&proto_name) { self.infer_protocol_structural_binding( &proto_name, bgens, &resolved_c, &mut subst, ); } } } } } // Source 3: infer from current_fn_return_ty vs fn return type if let Some(ref ret_ty) = fn_decl.return_type { if let Some(ref actual_ret) = self.current_fn_return_ty { self.infer_type_param_binding(ret_ty, actual_ret, &mut subst); } } // Source 4 [M-exp-promotion-blockers: retry E_UNUSED_PREFIX_TYPEVAR]: // infer effect-clause type-params (`Fail[E]`) from a closure ARG's // thrown value. `E` in `body fn() Fail[E] -> T` appears ONLY inside // the param's own effect-clause — none of Sources 1-3 above (which // all bind from param/return/current_fn C types) can see it. Mirror // of the identical fix in `resolve_method_level_subst` (Step 3) and // the inline instance-method dispatch block — three independent, // hand-duplicated inference engines all needed the same fix. for (param, arg) in fn_decl.params.iter().zip(args.iter()) { let effects: &[crate::ast::TypeRef] = match ¶m.ty { crate::ast::TypeRef::Func { effects, .. } => effects, _ => continue, }; if effects.is_empty() { continue; } let body_expr: Option<Expr> = match &arg.expr().kind { ExprKind::ClosureLight { body, .. } => Some(match body { crate::ast::ClosureBody::Expr(e) => (**e).clone(), crate::ast::ClosureBody::Block(b) => Expr::new( ExprKind::Block(b.clone()), b.span), }), ExprKind::ClosureFull(fsb) => match &fsb.body { crate::ast::FnBody::Expr(e) => Some(e.clone()), crate::ast::FnBody::Block(b) => Some(Expr::new( ExprKind::Block(b.clone()), fsb.span)), crate::ast::FnBody::External => None, }, _ => None, }; let Some(body_expr) = body_expr else { continue }; let Some(thrown) = Self::find_first_throw_value(&body_expr) else { continue }; let thrown_c = self.infer_expr_c_type(thrown); if thrown_c.is_empty() || thrown_c == "void*" { continue; } for eff in effects { if let crate::ast::TypeRef::Named { path, generics, .. } = eff { if path.last().map(String::as_str) == Some("Fail") && generics.len() == 1 { self.infer_type_param_binding(&generics[0], &thrown_c, &mut subst); } } } } // Collect results — error on unresolved let mut result = Vec::new(); for (name, resolved) in subst { match resolved { Some(c_ty) => result.push((name, c_ty)), None => { // Plan 48 Ф.7.5: указать в каком параметре T встречается. // Помогает LLM/user понять, какой argument добавить или // как явно дать turbofish. let positions: Vec<String> = fn_decl.params.iter().enumerate() .filter_map(|(i, p)| { let mut found = false; let mut names = std::collections::HashSet::new(); Self::collect_typeref_names(&p.ty, &mut names, &mut std::collections::HashSet::new()); if names.contains(&name) { found = true; } if found { Some(format!("param `{}` (#{i})", p.name)) } else { None } }) .collect(); // [M-exp-promotion-blockers: retry] narrow, explicit check — // does `name` appear ONLY inside a `Fail[name]`-shaped // effect-clause on some param (never in a directly // type-checkable position)? Source 4 above already tried // binding such an effect-only typevar from a closure's // thrown value; reaching here means the callback // genuinely never throws (dead `Fail[E]` branch, no // ambient handler needed) — any concrete C type is // functionally safe there (the slot it types never gets // constructed with real data on that path). Deliberately // NARROW (not "positions.is_empty()", which would also // swallow the pre-existing genuine "returned only, needs // turbofish" case) — only THIS specific effects-only // shape gets a default; everything else still loud-errors. let effect_only = positions.is_empty() && fn_decl.params.iter().any(|p| { matches!(&p.ty, crate::ast::TypeRef::Func { effects, .. } if effects.iter().any(|e| matches!(e, crate::ast::TypeRef::Named { path, generics, .. } if path.last().map(String::as_str) == Some("Fail") && generics.len() == 1 && matches!(&generics[0], crate::ast::TypeRef::Named { path: gp, generics: gg, .. } if gg.is_empty() && gp.last() == Some(&name))))) }); if effect_only { result.push((name, "nova_str".to_string())); continue; } let where_used = if positions.is_empty() { " (returned only — turbofish required)".to_string() } else { format!(" — appears in {}", positions.join(", ")) }; return Err(format!( "cannot infer type argument `{name}` for generic function `{}`{}; \ use turbofish: `{}[{name}](...)`", fn_decl.name, where_used, fn_decl.name )); } } } Ok(result) } /// Plan 196.5 Stage-B1 — channel-first preamble for `resolve_mono_type_args` (Q10 /// engine #1, see the doc block above `resolve_mono_type_args` itself). `call_id` is /// the SAME call-site key `resolved_types`/`resolved_callees`/`node_substs` all use /// (`emit_call`'s own parameter — every caller of this fn is `⊂ emit_call`, §4 196.5). /// /// Propose-then-verify (mirrors the `subst_map_adopt_rt` byte-identity guard, §6.3): /// the checker channel (`node_substs[call_id]`, written by `f1_check_call` / /// `resolve_return_channel`, Stage-A, ADDITIVE) is the PROPOSAL; the unchanged legacy /// body (`resolve_mono_type_args`, ~400 lines of ad-hoc re-unification) is both the /// FALLBACK and, during Stage-B, the VERIFIER. The channel is adopted (`hit`) ONLY when /// it is present, COMPLETE (`ordered.len() == fn_decl.generics.len()` — a residual/ /// erased-body param means the channel legitimately stayed unwritten, §7), and lowers /// (`resolved_type_to_c`, D315) to the EXACT SAME C-string as legacy for every name /// (name-for-name, not positional, in case a future producer reorders). A miss /// (no entry / incomplete entry) or a mismatch degrades to the legacy value — this /// function's return is therefore byte-identical to calling `resolve_mono_type_args` /// directly, BY CONSTRUCTION, regardless of hit/fallback (zero behavior-change risk; /// the hit/fallback split is a coverage metric, not a correctness fork). Once the /// corpus proves hit-only for a given D-form, Stage-C detaches the legacy body for /// that form (§8/§9) — this function is the propose-then-verify scaffolding that gate /// rests on. `NOVA_NODE_SUBSTS_TRACE` (same env var as the Stage-A producer trace) /// tallies `hit` / `fallback=miss|incomplete|mismatch` per call-site for the Stage-B /// acceptance metric ("channel-subst ≡ legacy-subst, no site regresses"). /// /// NOT wired at the frozen-zone call-site (`infer_call_ret_c`, `46293`–`48883` per the /// 196.5 plan `wave-1` freeze — that site keeps calling `resolve_mono_type_args` /// directly, unchanged) — only the two out-of-zone `emit_call` call-sites are flipped /// to this entry point (§8 point 1: "resolve_mono_type_args MIXED → detach вне-зонных /// сайтов, зонный ждёт волну-1"). fn resolve_mono_type_args_ch( &self, fn_decl: &crate::ast::FnDecl, turbofish_refs: &[crate::ast::TypeRef], args: &[crate::ast::CallArg], call_id: crate::ast::ExprId, ) -> Result<Vec<(String, String)>, String> { let legacy = self.resolve_mono_type_args(fn_decl, turbofish_refs, args); let trace = std::env::var_os("NOVA_NODE_SUBSTS_TRACE").is_some(); let Ok(legacy_pairs) = legacy else { // Legacy already loud-errors (turbofish-required diagnostics, §7) — Stage-B // does not paper over an Err with an unverified channel guess. return legacy; }; let channel = self.node_substs.get(&call_id); // [M-196.5-stage-c2] Plan 196.5 Stage-C2 — before degrading to legacy on a // channel miss/incomplete/mismatch, try the composition described in // `docs/plans/196.5-stage-c-notes.md` §2 Class R1: the checker channel // structurally CANNOT write an entry when a call's turbofish re-spells the // ENCLOSING generic body's OWN (residual, pre-mono) type-param (e.g. // `alloc_buf[T](n)` called from inside `Vec[T]`'s own body — `T` is bound // only post-mono). At EMIT time, though, `current_type_subst` (populated by // the mono-wrapper before this body is emitted) already carries exactly // that binding — `type_ref_to_c`/`resolved_type_to_c` already consult it // (`resolved_named_to_c` ~3809, `TypeParam` arm ~3384). This composes TWO // already-existing truths (channel-per-name ∪ turbofish-lowered-through- // current_type_subst) — no new inference engine — and is trusted as a // "hit" ONLY when it reproduces `legacy_pairs` byte-for-byte (propose- // then-verify, unchanged discipline). if let Some(composed) = self.compose_mono_type_args_ch(fn_decl, turbofish_refs, channel) { if composed == legacy_pairs { if trace { eprintln!( "[NODE_SUBSTS] consumer=mono_type_args call_id={:?} hit-composed n={}", call_id, composed.len() ); } return Ok(composed); } } let Some(channel) = channel else { if trace { eprintln!( "[NODE_SUBSTS] consumer=mono_type_args call_id={:?} fallback=miss", call_id ); } return Ok(legacy_pairs); }; if channel.len() != fn_decl.generics.len() { if trace { eprintln!( "[NODE_SUBSTS] consumer=mono_type_args call_id={:?} fallback=incomplete \ channel_n={} want_n={}", call_id, channel.len(), fn_decl.generics.len() ); } return Ok(legacy_pairs); } // [196-wave2-b, Plan 196 wave-2] REMOVED (was: a per-name loop rebuilding `lowered` from // `channel` and returning `Ok(lowered)` on full agreement — the historical direct, non- // composed "hit" path). `compose_mono_type_args_ch` above is tried FIRST and, for every // `fn_decl.generics` name, ALSO looks up `channel`-by-name through the SAME // `resolved_type_to_c` lowering before falling back to a positional turbofish ref — so // whenever this loop's per-name checks would ALL have passed (every name present in // `channel`, every lowering byte-identical to legacy), `compose_mono_type_args_ch` must // already have produced the identical `composed == legacy_pairs` match and returned above // as `hit-composed`. Detach+panic verified 0/1900+ `NOVA_NODE_SUBSTS_TRACE` events across // std/{collections,time,encoding} + spec_tests/conformance/standalone (88 files) + // examples/flagship/aggregator (`--strict-effects`) — see docs/plans/wip/196-wave2-b-notes.md // §2/§7/§8 for the full census + byte-construction argument. Falling straight through to the // legacy value here (rather than re-deriving a value already proven to equal it) is // byte-identical by the SAME argument that gated every other branch in this function. Kept // its own trace label (rather than silently merging into `fallback=miss`/`incomplete` above) // so a future producer change that DOES make this reachable shows up distinctly in the // `NOVA_NODE_SUBSTS_TRACE` census instead of vanishing. if trace { eprintln!( "[NODE_SUBSTS] consumer=mono_type_args call_id={:?} fallback=post-compose-miss", call_id ); } Ok(legacy_pairs) } /// [M-196.5-stage-c2] Plan 196.5 Stage-C2 — POST-mono composition helper for /// `resolve_mono_type_args_ch` (B1). For each of `fn_decl`'s declared generics, /// prefer a per-name `node_substs[call_id]` entry (lowered via /// `resolved_type_to_c`, which ITSELF composes any residual `TypeParam` through /// `current_type_subst` — so a channel entry that mentions the ENCLOSING body's /// own type-param still lowers correctly here); where the channel has no entry /// for a name, fall back to the positional turbofish ref (`resolve_mono_type_ /// args`'s own Source-1) lowered the SAME way. Returns `None` (never a partial /// vec) unless EVERY generic resolves to a concrete (non-empty, non-`void*`) /// C-name — the caller then gates adoption on byte-identity with the legacy /// result, so this function inventing nothing new is provable, not assumed. fn compose_mono_type_args_ch( &self, fn_decl: &crate::ast::FnDecl, turbofish_refs: &[crate::ast::TypeRef], channel: Option<&Vec<(String, crate::types::ResolvedType)>>, ) -> Option<Vec<(String, String)>> { if fn_decl.generics.is_empty() { return None; } let mut out = Vec::with_capacity(fn_decl.generics.len()); for (i, g) in fn_decl.generics.iter().enumerate() { let from_channel = channel .and_then(|c| c.iter().find(|(n, _)| n == &g.name)) .and_then(|(_, rt)| self.resolved_type_to_c(rt).ok()) .filter(|c| !c.is_empty() && c != "void*"); let resolved = match from_channel { Some(c) => c, None => { let tr = turbofish_refs.get(i)?; let c = self.type_ref_to_c(tr).ok()?; if c.is_empty() || c == "void*" { return None; } c } }; out.push((g.name.clone(), resolved)); } Some(out) } /// Plan 153.5 (D263) / [M-153.5-flatten-nested-receiver]: count the /// slice/Vec NESTING DEPTH of a (possibly nested) Vec-mono receiver C type /// `rt` (the `Nova_`-stripped, `*`-stripped form). `Vec____nova_int` → 1, /// `Vec____Nova_Vec____nova_int_p` → 2, `Vec____Nova_Vec____Nova_Vec____nova_int_p_p` /// → 3, … by walking `generic_type_instance_info` (which records each level's /// un-mangled element). A non-Vec `rt` → 0. Used to pick the matching /// `"[]"*depth + "T"` slice-receiver sentinel key so `[][]T`/`[][][]T` /// extension methods route to the real monomorphized function rather than /// the hardcoded single-level `"[]T"` key. fn vec_nesting_depth(&self, rt: &str) -> usize { let mut depth = 0usize; let mut cur = rt.trim_end_matches('*').trim().to_string(); loop { if cur.starts_with("NovaArray_") { depth += 1; // Legacy array elem is the suffix after the prefix. cur = cur.trim_start_matches("NovaArray_").trim_end_matches('*').to_string(); continue; } if cur.starts_with("Vec____") || cur.starts_with("Nova_Vec____") { depth += 1; let key = if cur.starts_with("Nova_") { cur.clone() } else { format!("Nova_{}", cur) }; let elem = self.generic_type_instance_info.borrow() .get(&key) .and_then(|(base, args)| { if base == "Vec" { args.first().cloned() } else { None } }); match elem { // A1‴: registry arg is `ResolvedType` — lower to C-name. Some(e) => { cur = self.arg_c(&e).trim_end_matches('*').trim().to_string(); } None => break, } continue; } break; } depth } /// Plan 153.5 (D263): the slice-receiver sentinel key `"[]"*depth + "T"` for a /// Vec-mono receiver C type `rt`, or `None` if `rt` is not a Vec/array mono. fn slice_sentinel_key_for_rt(&self, rt: &str) -> Option<String> { let depth = self.vec_nesting_depth(rt); if depth == 0 { None } else { Some(format!("{}T", "[]".repeat(depth))) } } /// Plan 153.5 (D263) / [M-153.5-flatten-nested-receiver]: collect every /// free typevar (short all-uppercase single-segment `Named`) in a structured /// receiver type, recursively, in first-seen order. Used to seed the /// structural receiver-typevar re-binding when the method's typevars are /// carrier-declared (and thus NOT listed in `fn_decl.generics`). fn collect_receiver_typevars(ty: &crate::ast::TypeRef, out: &mut Vec<String>) { use crate::ast::TypeRef; match ty { TypeRef::Named { path, generics, .. } => { if path.len() == 1 && generics.is_empty() { let n = &path[0]; if !n.is_empty() && n.len() <= 2 && n.chars().all(|c| c.is_ascii_uppercase()) && !out.contains(n) { out.push(n.clone()); } } for g in generics { Self::collect_receiver_typevars(g, out); } } TypeRef::Array(inner, _) | TypeRef::FixedArray(_, inner, _) => { Self::collect_receiver_typevars(inner, out); } TypeRef::Tuple(items, _) => { for it in items { Self::collect_receiver_typevars(it, out); } } TypeRef::Readonly(inner, _) | TypeRef::Mut(inner, _) | TypeRef::Uninit(inner, _) | TypeRef::Pointer(inner, _) => Self::collect_receiver_typevars(inner, out), _ => {} } } /// Plan 153.5 (D263) / [M-153.5-flatten-nested-receiver]: a structured /// receiver type is NESTED (needs structural typevar re-binding) iff the /// top-level type's IMMEDIATE generic arg / array element is itself a /// compound type rather than a bare typevar / concrete Named. For a FLAT /// receiver (`Vec[T]` → immediate generic `Named T`; `[]T` → element /// `Named T`) the template-derived shallow binding already equals the /// structural one, so no override is needed. For `Vec[Vec[T]]` / `[][]T` /// the immediate arg is itself an `Array`/generic-`Named`, so the method's /// receiver typevar binds DEEPER than the template's and must be overridden. fn receiver_ty_is_nested(ty: &crate::ast::TypeRef) -> bool { use crate::ast::TypeRef; match ty { TypeRef::Array(inner, _) | TypeRef::FixedArray(_, inner, _) => { matches!(inner.as_ref(), TypeRef::Array(..) | TypeRef::FixedArray(..)) || matches!(inner.as_ref(), TypeRef::Named { generics, .. } if !generics.is_empty()) } TypeRef::Named { generics, .. } => { generics.iter().any(|g| matches!(g, TypeRef::Array(..) | TypeRef::FixedArray(..)) || matches!(g, TypeRef::Named { generics: gg, .. } if !gg.is_empty())) } _ => false, } } /// [M-nested-generic-receiver-method-mono] (реестр 221.1 №247, /// 2026-08-01): a NESTED receiver pattern (`Option[Result[T, E]] /// @transpose`, the carrier's own generic slot filled by a COMPOUND /// type rather than a bare typevar) has its method-introduced typevars /// (`T`/`E`, extracted structurally from decomposing that compound — /// checker-side, see the `E_DUPLICATE_GENERIC_DECL` avoidance for this /// receiver shape) COLLIDE BY NAME with the receiver type's OWN /// declared carrier param whenever the carrier is a BUILTIN sum type /// (`Option`/`Result`'s prelude decl also spells its own slot "T"/"E"). /// The various call sites that seed `type_subst` for these builtin /// carriers (`Nova_Option_method_*`/`Nova_Result_method_*` dispatch, /// Plan 95/99.1) do a SHALLOW single-slot bind — Option's OWN "T" ↦ the /// WHOLE compound element (e.g. the `Result[int,str]` mono's C name) — /// correct only for a FLAT receiver (`Option[T] @flat_map`, where that /// "T" genuinely IS the method's own). For a nested receiver this /// leaves the method's REAL `T` bound to the wrong (compound) value and /// its `E` unbound entirely — the return type (`Result[Option[T], E]` /// for `transpose`) then mono's with a garbage/missing subst, dropping /// the `Option` wrapper around the payload in the emitted C. /// /// Call this AFTER `recv_c` (the receiver's own C type) has been /// computed from the shallow seed (so `@`'s own destructuring stays /// correct — `recv_c` itself is not recomputed) but BEFORE the return /// type / body are lowered — it REBINDS `current_type_subst` in place /// for just the typevars the receiver pattern structurally carries, /// via full unification of the DECLARED receiver TypeRef against the /// now-final `recv_c` (reusing `infer_type_param_binding`, which /// already natively decomposes `Option[T]`/`Result[T,E]` C-name shapes, /// ~L23570/23584). Any OTHER already-bound slot (e.g. a method-level /// generic like `map[U]`'s `U`, resolved earlier by /// `resolve_method_level_subst` and already in `current_type_subst`) is /// left untouched — only slots the structural walk itself resolves are /// overridden. No-op for a flat receiver (`receiver_ty_is_nested` /// false) — mirrors Plan 153.5 [M-153.5-flatten-nested-receiver]'s /// structural receiver re-bind (applied at the CALL SITE for USER /// generic types, ~L42810); this is the same primitive applied /// centrally so it also covers BUILTIN Option/Result dispatch. fn debt_rebind_nested_receiver_typevars( &mut self, fn_decl: &crate::ast::FnDecl, recv_c: &str, ) { let recv_ty = match fn_decl.receiver.as_ref().and_then(|r| r.receiver_ty.as_ref()) { Some(t) if Self::receiver_ty_is_nested(t) => t, _ => return, }; let mut tvars: Vec<String> = Vec::new(); Self::collect_receiver_typevars(recv_ty, &mut tvars); if tvars.is_empty() { return; } let mut pend: Vec<(String, Option<String>)> = tvars.iter().map(|n| (n.clone(), None)).collect(); self.infer_type_param_binding(recv_ty, recv_c, &mut pend); for (n, c) in pend { if let Some(c) = c { self.current_type_subst.insert(n, Self::lift_c_name(c)); } } } /// Plan 48 Ф.0 / Plan 98 Ф.1: match param_typeref against concrete_c, /// bind type params in subst. /// /// **Plan 98 Ф.1 (2026-05-23):** конвертирован из associated `fn` в /// метод `&self` — нужен доступ к `novaopt_value_types` / /// `novares_value_types` / `generic_type_instance_info` для recovery /// реальных C-типов из mangled/sanitized форм. Добавлены три новые /// ветки рекурсии: Option[T], Result[T,E], user-generic Box[T]/ /// HashMap[K,V]/etc. Раньше эти param-формы молча игнорировались → /// каждый generic-helper, принимающий generic-тип, **требовал /// turbofish** (`check[int](a)` вместо естественного `check(a)`). /// Теперь inference recurses сквозь generic-параметризованные типы — /// паритет с Rust/Go-style unification. /// [M-property-testing-rot] (Plan 172.13 батч 3): structural protocol /// unification for generic-fn type-arg inference. Given a protocol-typed /// param `p Proto[G...]` (where `G...` are TypeRefs that mention the FN's /// generics) matched against a concrete generic instance `CBase[cargs...]`, /// walk the protocol's method signatures: each protocol position that /// mentions a protocol generic `X_i` corresponds (after `X_i ↦ G_i`) to the /// CONCRETE type's same-named method position resolved under /// `CBase`'s own generics ↦ `cargs`. Recursing into /// [`Self::infer_type_param_binding`] with (substituted protocol position, /// concrete C type) binds the fn generics through ARBITRARY structure — /// e.g. `ArrayGen[U] @generate() -> []U` implementing `Generator[[]U]` /// binds `T = []int` for `gen Generator[T]` ← `ArrayGen[int]` (the naive /// positional zip would wrongly bind `T = int`). /// [M-property-testing-rot] (Plan 172.13 батч 3): TypeRef-level structural /// unification for Source 2f of [`Self::resolve_mono_type_args`]. Binds the /// callee's still-unresolved generic slots by matching its declared param /// TypeRef against the CALLER's declared TypeRef for the forwarded arg — /// e.g. callee `gen Generator[T_callee]` vs caller `gen Generator[T_caller]` /// recurses into the generics and binds `T_callee := /// type_ref_to_c(T_caller)`, which lowers through `current_type_subst` /// (the caller's own mono substitution). fn infer_type_param_binding_from_ref( &self, param_ty: &crate::ast::TypeRef, concrete_ref: &crate::ast::TypeRef, subst: &mut Vec<(String, Option<String>)>, ) { use crate::ast::TypeRef as TR; // A bare Named that IS one of the callee's generic slots → bind it to // the C lowering of the caller-side position (under current_type_subst). if let TR::Named { path, generics, .. } = param_ty { if generics.is_empty() && path.len() == 1 && subst.iter().any(|(n, _)| n == &path[0]) { if let Ok(c) = self.type_ref_to_c(concrete_ref) { if !c.is_empty() && c != "void*" { if let Some(slot) = subst .iter_mut() .find(|(n, v)| n == &path[0] && v.is_none()) { slot.1 = Some(c); } } } return; } } match (param_ty, concrete_ref) { (TR::Array(pi, _), TR::Array(ci, _)) => { self.infer_type_param_binding_from_ref(pi, ci, subst); } ( TR::Named { path: pp, generics: pg, .. }, TR::Named { path: cp, generics: cg, .. }, ) if pp.last() == cp.last() && pg.len() == cg.len() => { for (p, c) in pg.iter().zip(cg.iter()) { self.infer_type_param_binding_from_ref(p, c, subst); } } ( TR::Func { params: pfp, return_type: pr, .. }, TR::Func { params: cfp, return_type: cr, .. }, ) if pfp.len() == cfp.len() => { for (p, c) in pfp.iter().zip(cfp.iter()) { self.infer_type_param_binding_from_ref(p, c, subst); } if let (Some(p), Some(c)) = (pr.as_ref(), cr.as_ref()) { self.infer_type_param_binding_from_ref(p, c, subst); } } (TR::Readonly(pi, _), _) | (TR::Mut(pi, _), _) => { self.infer_type_param_binding_from_ref(pi, concrete_ref, subst); } (_, TR::Readonly(ci, _)) | (_, TR::Mut(ci, _)) => { self.infer_type_param_binding_from_ref(param_ty, ci, subst); } _ => {} } } fn infer_protocol_structural_binding( &self, proto_name: &str, proto_args: &[crate::ast::TypeRef], concrete_c: &str, subst: &mut Vec<(String, Option<String>)>, ) { // Depth guard: protocol positions may mention protocols (incl. // themselves), so the walk is bounded (see `proto_unify_depth`). if self.proto_unify_depth.get() >= 4 { return; } self.proto_unify_depth.set(self.proto_unify_depth.get() + 1); let _depth_reset = { struct DepthReset<'a>(&'a std::cell::Cell<u8>); impl<'a> Drop for DepthReset<'a> { fn drop(&mut self) { self.0.set(self.0.get().saturating_sub(1)); } } DepthReset(&self.proto_unify_depth) }; let Some((proto_params, proto_methods)) = self.protocol_method_registry.get(proto_name).cloned() else { return }; if proto_params.len() != proto_args.len() { return; } // proto's own generic name → the param-position TypeRef (mentions fn generics). let proto_subst: HashMap<String, crate::ast::TypeRef> = proto_params .iter() .cloned() .zip(proto_args.iter().cloned()) .collect(); // ── Case A: concrete arg is a GENERIC mono instance // (`Nova_ArrayGen____nova_int*`) — walk the concrete TEMPLATE's method // declarations under its own generics ↦ instance-args substitution. let stripped = concrete_c.trim_end_matches('*').trim(); let lookup_key = stripped .strip_prefix("NovaValue_") .map(|s| format!("Nova_{}", s)) .unwrap_or_else(|| stripped.to_string()); let generic_instance: Option<(String, Vec<crate::types::ResolvedType>)> = self .generic_type_instance_info .borrow() .get(lookup_key.as_str()) .cloned(); if let Some((concrete_base, concrete_args)) = generic_instance { let Some(ctemplate) = self.generic_type_templates.get(&concrete_base) else { return }; if ctemplate.generics.len() != concrete_args.len() { return; } let csubst: HashMap<String, String> = ctemplate .generics .iter() .map(|g| g.name.clone()) .zip(concrete_args.iter().map(|a| self.arg_c(a))) .collect(); let Some(cmethods) = self.generic_type_methods.get(&concrete_base) else { return }; for pm in &proto_methods { let pm_name = pm.name.trim_start_matches('@'); let Some(cm) = cmethods .iter() .find(|m| m.name.trim_start_matches('@') == pm_name) else { continue }; // Resolve one (protocol position, concrete position) pair. let mut unify_pos = |proto_pos: &crate::ast::TypeRef, concrete_pos: &crate::ast::TypeRef, subst: &mut Vec<(String, Option<String>)>| { let target = crate::const_fn_trampoline::subst_type_ref_pub(proto_pos, &proto_subst); // Lower the concrete position under csubst via the RefCell // override channel (this fn is `&self`). let saved = self.type_subst_overrides.replace(csubst.clone()); let concrete_pos_c = self.type_ref_to_c(concrete_pos).ok(); self.type_subst_overrides.replace(saved); if let Some(c) = concrete_pos_c { if !c.is_empty() && c != "void*" { self.infer_type_param_binding(&target, &c, subst); } } }; if let (Some(pr), Some(cr)) = (pm.return_type.as_ref(), cm.return_type.as_ref()) { unify_pos(pr, cr, subst); } for (pp, cp) in pm.params.iter().zip(cm.params.iter()) { unify_pos(&pp.ty, &cp.ty, subst); } } return; } // ── Case B: concrete arg is a NON-generic type (`Nova_IntGen*`) — its // method signatures are already registered at the C level in // `method_overloads`; unify protocol positions against those directly // (e.g. `IntGen @generate() -> int` implementing `Generator[int]`). let concrete_base = Self::debt_nova_type_name_from_c(concrete_c); if concrete_base.is_empty() { return; } // GUARD: a GENERIC concrete type that reached here WITHOUT instance // info (an ERASED `Nova_ArrayGen*` value) must NOT bind through // `method_overloads` — the registered sig for a generic type's method // is the ERASED one (its own type-params defaulted, e.g. // `@generate() -> []T` registered as returning `NovaArray_nova_int*`), // which would silently bind the protocol position to a LIE. if self.generic_types.contains(&concrete_base) { return; } for pm in &proto_methods { let pm_name = pm.name.trim_start_matches('@'); let key = (concrete_base.clone(), pm_name.to_string()); let Some(sig) = self .method_overloads .get(&key) .and_then(|sigs| sigs.first()) .cloned() else { continue }; if let Some(pr) = pm.return_type.as_ref() { if !sig.return_c_type.is_empty() && sig.return_c_type != "void*" { let target = crate::const_fn_trampoline::subst_type_ref_pub(pr, &proto_subst); self.infer_type_param_binding(&target, &sig.return_c_type, subst); } } for (pp, c) in pm.params.iter().zip(sig.param_c_types.iter()) { if !c.is_empty() && c != "void*" { let target = crate::const_fn_trampoline::subst_type_ref_pub(&pp.ty, &proto_subst); self.infer_type_param_binding(&target, c, subst); } } } } /// [M-method-value-arg-in-generic-combinator-infer] True when `ty` is a /// bare (no own generics) `Named` TypeRef whose name is one of /// `generics` — a reference to one of THIS callee's OWN not-yet- /// resolved method-level type-params (`U` in `map[U]`), as opposed to /// an ALREADY-CONCRETE position (the receiver's own class-level /// generic `T`, or a genuinely concrete named type) that /// `type_ref_to_c` can safely resolve right now via the active /// `current_type_subst`. Used to gate the method-value-arg receiver/ /// param MISMATCH check (Step 2m, all 3 sibling sites) to positions /// that are structurally guaranteed already-bound — never a method's /// own still-unresolved type-param (which would otherwise erase to a /// bogus `Nova_<name>*` literal-fallback C-type and false-positive). fn typeref_is_bare_own_generic( ty: &crate::ast::TypeRef, generics: &[crate::ast::GenericParam], ) -> bool { if let crate::ast::TypeRef::Named { path, generics: g, .. } = ty { g.is_empty() && generics.iter().any(|gp| path.join("_") == gp.name) } else { false } } fn infer_type_param_binding( &self, param_ty: &crate::ast::TypeRef, concrete_c: &str, subst: &mut Vec<(String, Option<String>)>, ) { if concrete_c.is_empty() || concrete_c == "void*" { return; } match param_ty { // Bare T → bind T = concrete_c crate::ast::TypeRef::Named { path, generics, .. } if generics.is_empty() => { let name = path.join("_"); if let Some(slot) = subst.iter_mut().find(|(n, _)| n == &name) { if slot.1.is_none() { slot.1 = Some(concrete_c.to_string()); } } } // []T → extract element type from the receiver's concrete C type. crate::ast::TypeRef::Array(inner, _) => { // Legacy spelling: `NovaArray_<elem>*` — strip prefix + `*`. if let Some(inner_c) = Self::debt_strip_novaarray_prefix_opt(concrete_c) .and_then(|s| s.strip_suffix('*')) { self.infer_type_param_binding(inner, inner_c, subst); return; } // Plan 138.2 / D239 flip: `[]T` ≡ `Vec[T]`, so the concrete is // the mono'd `Nova_Vec____<sani_elem>*`. Recover the REAL element // C-type from `generic_type_instance_info` (which records the // un-mangled `type_args_c`) and recurse — this lets a structural // receiver bind (`[][]T` against `Nova_Vec____Nova_Vec____nova_int_p*`) // descend through EVERY level. Plan 153.5 (D263). let stripped = concrete_c.trim_end_matches('*').trim(); if stripped.starts_with("Nova_Vec____") || stripped.starts_with("Vec____") { let key = if stripped.starts_with("Nova_") { stripped.to_string() } else { format!("Nova_{}", stripped) }; let elem = self.generic_type_instance_info.borrow() .get(&key) .and_then(|(base, args)| { if base == "Vec" { args.first().cloned() } else { None } }); if let Some(elem_c) = elem { // A1‴: registry arg is `ResolvedType` — lower to C-name. self.infer_type_param_binding(inner, &self.arg_c(&elem_c), subst); } } } // Plan 98 Ф.1: Option[T] / Result[T,E] / user-generic types // в позиции param — извлекаем concrete type-args из // mangled/sanitized form concrete_c и рекурсивно bind'аем. crate::ast::TypeRef::Named { path, generics, .. } if !generics.is_empty() => { let nova_name = path.join("_"); match nova_name.as_str() { // Option[T]: concrete_c = "NovaOpt_<sani>" (или "NovaOpt_<sani>*") "Option" if generics.len() == 1 => { let stripped = concrete_c.trim_end_matches('*').trim(); if let Some(sani) = stripped.strip_prefix("NovaOpt_") { // Recovery: novaopt_value_types map'ит sanitized // в реальный c_ty (`Nova_Foo_p` → `Nova_Foo*`). // Для примитивов (`nova_int`/etc) sanitized == // real, fallback на сам sanitized — корректно. let real_t = self.novaopt_value_types.borrow() .get(sani).cloned() .unwrap_or_else(|| sani.to_string()); self.infer_type_param_binding(&generics[0], &real_t, subst); } } // Result[T,E]: concrete_c = "NovaRes_<ok>_<err>*" "Result" if generics.len() == 2 => { if let Some((ok_c, err_c)) = self.novares_ok_err(concrete_c) { self.infer_type_param_binding(&generics[0], &ok_c, subst); self.infer_type_param_binding(&generics[1], &err_c, subst); } } // User generic types (Box[T], HashMap[K,V], etc): // concrete_c = "Nova_<base>____<arg1>__<arg2>...*" // (mangled через compute_generic_type_c_name). Lookup в // generic_type_instance_info → (base, type_args_c). // Plan 153.2 [M-153.2-flat-map-inner-option]: value-generic // types (BoxIter[U]) have surface type `NovaValue_<short>` // (no trailing `*`, no `Nova_` prefix) but // generic_type_instance_info keys on `Nova_<short>`. // Normalize before lookup so BoxIter[U] in return position // of a closure (`f fn(T)->BoxIter[U]`) correctly binds U. _ => { let stripped = concrete_c.trim_end_matches('*').trim(); let lookup_key = stripped .strip_prefix("NovaValue_") .map(|s| format!("Nova_{}", s)) .unwrap_or_else(|| stripped.to_string()); let instance_info = self.generic_type_instance_info.borrow(); let positional_hit = match instance_info.get(lookup_key.as_str()) { // Sanity: base должен соответствовать // nova_name (защита от cross-type match'а // при коллизии mangled-имени). Some((base, type_args)) if base == &nova_name && type_args.len() == generics.len() => { Some(type_args.clone()) } _ => None, }; drop(instance_info); if let Some(type_args_owned) = positional_hit { for (g_ty, arg_c) in generics.iter().zip(type_args_owned.iter()) { // A1‴: registry arg is `ResolvedType` — lower to C-name. self.infer_type_param_binding(g_ty, &self.arg_c(arg_c), subst); } } else if self.protocol_method_registry.contains_key(&nova_name) { // [M-property-testing-rot] (Plan 172.13 батч 3): // STRUCTURAL protocol unification. The param is // protocol-typed (`gen Generator[T]`) and the arg is a // CONCRETE type — either a generic mono instance // (`ArrayGen[int]`, mono `Nova_ArrayGen____nova_int*`) // or a plain type (`IntGen`, `Nova_IntGen*`). The // positional zip above cannot apply (base mismatch / // no instance info) — and positionally it would be // WRONG anyway: the protocol's generics relate to the // concrete type's through its structural // implementation, e.g. `ArrayGen[U] @generate() -> []U` // implements `Generator[[]U]`, so `T` in // `gen Generator[T]` must unify with `[]int`, not // `int`. Walk the protocol's method signatures against // the concrete type's same-named methods to bind the // fn generics through arbitrary structure. self.infer_protocol_structural_binding( &nova_name, generics, concrete_c, subst, ); } } } } // [M-rawmem-typed-copy-wrappers] `*T` / `*mut T` param → strip one // pointer level off the concrete C arg type and recurse into the // pointee. Mirror of the substitution-direction unwrap already done // in `apply_type_subst_to_ref` (Pointer wraps Mut/Unsafe for the // postfix `*mut T`/`*unsafe T` spelling, D216/Plan 138.5); without // this arm a generic fn whose type param appears ONLY under a raw // pointer (e.g. `RawMem.copy_n_nonoverlapping[T](src *T, dst *mut T, ...)`) // can never bind T from call-site args — every call falls through to // the `cannot infer` error (or a caller-side silent fallback). crate::ast::TypeRef::Pointer(inner, _) => { let base = match inner.as_ref() { crate::ast::TypeRef::Mut(ti, _) | crate::ast::TypeRef::Uninit(ti, _) => ti.as_ref(), other => other, }; // Strip exactly ONE pointer level (`strip_suffix`, not // `trim_end_matches` — the pointee itself may be a pointer, // e.g. an element type `Nova_U*` inside a `Vec[U*]` buffer // gives concrete_c = "Nova_U**"; trimming ALL trailing `*` // would over-strip to bare "Nova_U" and bind T to an // incomplete/erased stub type, not the real one-level pointee). if let Some(stripped) = concrete_c.strip_prefix("const ").unwrap_or(concrete_c).strip_suffix('*') { self.infer_type_param_binding(base, stripped.trim(), subst); } } crate::ast::TypeRef::Mut(inner, _) | crate::ast::TypeRef::Uninit(inner, _) => { if let crate::ast::TypeRef::Pointer(p_inner, _) = inner.as_ref() { if let Some(stripped) = concrete_c.strip_prefix("const ").unwrap_or(concrete_c).strip_suffix('*') { self.infer_type_param_binding(p_inner, stripped.trim(), subst); } } else { self.infer_type_param_binding(inner, concrete_c, subst); } } // fn(..)->T: skip (closure C type doesn't encode return type directly) _ => {} } } /// Plan 196.5 §W1-instance addendum (W1.5, вариант ii) — W1-i.A /// (extract+SHADOW). ЕДИНЫЙ POST-mono резолвер, консолидирующий per- /// instance subst-реконструкцию, дублированную вручную в 6 legacy- /// армах `infer_call_ret_c` (B05/B06a/B07/B07r/B11c/B11x, все — внутри /// wave-1 замороженной зоны, RESUME-4/`AGENT_HEARTBEAT.txt`) — все шесть /// решают один и тот же предикат: «дан структурный signature каллиси /// (`callee`) + конкретный ресивер-инстанс (`recv_instance_rt` — тот же /// `rt`/`stripped`, что армы уже держат: base+args mono-имя БЕЗ /// `Nova_`-префикса, БЕЗ trailing `*`) + args → собрать subst каллиси /// (carrier ++ method-level)». /// /// rustc-эталон (196.5-node-substs-channel.md §W1.3): per-instance subst /// = `node_args.instantiate(Instance.args)`; здесь `Instance.args` — /// уже материализованный `current_type_subst`/конкретный ресивер- /// инстанс, НЕ checker-side `node_substs`-канал (эти сайты — residual /// ПО ПОСТРОЕНИЮ: RESUME-4 инструментировал вход всех 6 армов — 850/850 /// `node_substs.get(&expr.id)` = NONE, множества «канал покрыл» и «арм /// жив» дизъюнктны по гейту §7). /// /// W1-i.A: ЧИСТО-READ, БЕЗ side-effects (typedef/instance-регистрацию /// по-прежнему делает вызывающий арм — напр. B07r остаётся /// `register_generic_instances_in_typeref` на месте). Потребляется /// ТОЛЬКО SHADOW-сверкой (см. `w1_shadow_probe`) — легаси-армы остаются /// единственным авторитетным источником возврата до W1-i.B (flip). /// /// Возвращает `None`, если у `callee` нет receiver (не instance-метод). /// `_call_id` не используется в самой реконструкции (эти сайты node_ /// substs-residual) — параметр зарезервирован под probe-dedup/W1-i.B. /// /// `turbofish_args` — explicit method-level type-args from `obj.method[T, /// ...](...)` call syntax (already extracted by the caller — `infer_call_ /// ret_c`'s `TurboFish{base:Member}` unwrap at its top, mirroring `emit_ /// call`'s `current_method_turbofish` stash at `~31860`). **[M-196.5-w1i- /// turbofish-regression fix]** The W1-i.B flip (`resolve_mono_type_args`/ /// hand-rolled subst → this resolver, 196.5 Stage-D) dropped the m176 fix /// ([M-codegen-method-return-turbofish], backlog-followups.md — CLOSED /// 2026-07-06) for a METHOD-LEVEL generic that appears ONLY in RETURN /// position with NO args/closures to structurally bind it from (e.g. `fn /// Res consume @into[T]() -> Option[T]`): this resolver's carrier-bind /// (step 1) and arg/closure-bind (step 2) both had nothing to see, so an /// explicit `r.into[str]()` turbofish silently erased to the unbound /// generic-stub `NovaOpt_Nova_T_p` — CC-FAIL (`nova_tests/plan176_holes/ /// m176_method_return_turbofish.nv`, regression caught outside the 4- /// corpus census measurement). Fix: seed unbound method-level slots from /// `turbofish_args` positionally (declaration order of `callee.generics`) /// BEFORE the arg/closure derivation below — mirrors `resolve_method_ /// level_subst`'s `explicit_tf` seeding (`~20477`), same "explicit user /// annotation wins, `infer_type_param_binding` never overwrites a bound /// slot" precedence. Empty slice at call sites with no turbofish in scope /// (chained-receiver dispatch, `~45297`) is a no-op — byte-identical there. fn resolve_instance_call_subst( &self, _call_id: ExprId, callee: &FnDecl, recv_instance_rt: &str, args: &[CallArg], turbofish_args: &[TypeRef], ) -> Option<Vec<(String, Option<String>)>> { // 1. Carrier-биндинг — СТРУКТУРНО через уже-существующий // `infer_type_param_binding` (нет нового инференса): собрать // typevar-имена, встречающиеся в receiver_ty, матчить против // конкретного mono-имени ресивера. Два кандидата конкретной // C-строки — с "Nova_"-префиксом (generic_type_templates-конвенция: // Vec/Pair/пользовательские шаблоны) и без (builtin `NovaArray_ // <elem>`, без registry-записи) — `infer_type_param_binding` // заполняет ТОЛЬКО пустые слоты, так что пробовать оба кандидата // по очереди безопасно (мирроринг B05 47788-47800 fallback-пути). // // Receiver МОЖЕТ отсутствовать (`callee.receiver`/`receiver_ty == // None`) — некоторые `mono_method_decls`-sentinel-записи (напр. // `Mutex.with_lock` B06a) хранят FnDecl без заполненного receiver, // хотя return у них зависит ТОЛЬКО от method-level generics (не от // carrier). Не бейлимся — carrier-шаг просто no-op, метод-level // (шаг 2) всё ещё может резолвить return. let recv_ty = callee.receiver.as_ref().and_then(|r| r.receiver_ty.as_ref()); let mut subst: Vec<(String, Option<String>)> = Vec::new(); let recv_prefixed = format!("Nova_{}*", recv_instance_rt); if let Some(recv_ty) = recv_ty { let mut tvars: Vec<String> = Vec::new(); Self::collect_receiver_typevars(recv_ty, &mut tvars); subst = tvars.iter().map(|n| (n.clone(), None)).collect(); self.infer_type_param_binding(recv_ty, &recv_prefixed, &mut subst); if subst.iter().any(|(_, c)| c.is_none()) { let recv_bare = format!("{}*", recv_instance_rt); self.infer_type_param_binding(recv_ty, &recv_bare, &mut subst); } } // Registry-enrichment: остаточные carrier-слоты — из structurally- // typed `generic_type_instance_info` (A1‴), для форм, которые // структурный матч выше не видит (напр. receiver_ty == bare `Self`, // или receiver отсутствует вовсе — enrichment всё равно пробует // registry по `recv_instance_rt`, добавляя НОВЫЕ слоты). if recv_ty.is_none() || subst.iter().any(|(_, c)| c.is_none()) { let instance = self.generic_type_instance_info.borrow() .get(&format!("Nova_{}", recv_instance_rt)).cloned(); if let Some((base_name, type_args_rt)) = instance { if let Some(tmpl) = self.generic_type_templates.get(&base_name) { for (g, rt) in tmpl.generics.iter().zip(type_args_rt.iter()) { let c = self.arg_c(rt); match subst.iter_mut().find(|(n, _)| n == &g.name) { Some(slot) if slot.1.is_none() => slot.1 = Some(c), None => subst.push((g.name.clone(), Some(c))), _ => {} } } } } } if !subst.iter().any(|(n, _)| n == "Self") { subst.push(("Self".to_string(), Some(recv_prefixed.clone()))); } // 2. Method-level биндинг — из конкретных args + closure-return-тел // (тот же двух-шаговый паттерн, что `resolve_method_level_subst` // Steps 1/2 и `infer_mono_method_ret_with_args`). for g in &callee.generics { if !subst.iter().any(|(n, _)| n == &g.name) { subst.push((g.name.clone(), None)); } } // [M-196.5-w1i-turbofish-regression fix] Seed method-level slots from // an explicit turbofish BEFORE structural arg/closure derivation — // the ONLY source for a type-param appearing solely in return // position with no param/closure to bind it from (m176 precedent). // Positional: `callee.generics[i]` <- `turbofish_args[i]`. Only fills // still-`None` slots (never overwrites a carrier/Self binding from // step 1 above — disjoint namespace in practice, but the guard is // cheap insurance). if !turbofish_args.is_empty() { for (g, tr) in callee.generics.iter().zip(turbofish_args.iter()) { if let Ok(c) = self.type_ref_to_c(tr) { if !c.is_empty() && c != "void*" { if let Some(slot) = subst.iter_mut().find(|(n, _)| n == &g.name) { if slot.1.is_none() { slot.1 = Some(c); } } } } } } if !callee.generics.is_empty() { let receiver_subst_map: HashMap<String, String> = subst.iter() .filter_map(|(n, c)| c.clone().map(|c| (n.clone(), c))) .collect(); let saved = self.type_subst_overrides.replace(receiver_subst_map); for (param, arg) in callee.params.iter().zip(args.iter()) { let arg_c = self.infer_expr_c_type(arg.expr()); self.infer_type_param_binding(¶m.ty, &arg_c, &mut subst); if let TypeRef::Func { params: fp, return_type: Some(ret_ty_ref), .. } = ¶m.ty { if let ExprKind::ClosureLight { params: cl_params, body } = &arg.expr().kind { let fp_tys: Vec<String> = fp.iter() .map(|t| self.type_ref_to_c(t).unwrap_or_else(|_| "nova_int".into())) .collect(); let mut ovr = self.closure_param_type_overrides.borrow_mut(); let saved_cl: Vec<(String, Option<String>)> = cl_params.iter().zip(fp_tys.iter()) .map(|(p, ty)| (p.name.clone(), ovr.insert(p.name.clone(), ty.clone()))) .collect(); drop(ovr); let ret_c = match body { ClosureBody::Expr(e) => self.infer_expr_c_type(e), ClosureBody::Block(b) => b.trailing.as_ref() .map(|e| self.infer_expr_c_type(e)).unwrap_or_default(), }; let mut ovr = self.closure_param_type_overrides.borrow_mut(); for (name, prev) in saved_cl { match prev { Some(old) => { ovr.insert(name, old); } None => { ovr.remove(&name); } } } drop(ovr); if !ret_c.is_empty() && ret_c != "void*" { self.infer_type_param_binding(ret_ty_ref.as_ref(), &ret_c, &mut subst); } } else if let ExprKind::Member { obj: mv_obj, name: mv_name_full } = &arg.expr().kind { // [M-method-value-arg-in-generic-combinator-infer]: // mirror of the ClosureLight branch above, but for an // UNBOUND method-value arg (`Type.@method`, D35) — // this function computes the CALL EXPR's own return // C-type (for the enclosing `ro r = ...` declared var, // via `infer_call_ret_c`'s B06a sentinel-mono path), // a DIFFERENT consumer from `resolve_method_level_ // subst` (which only feeds the callee's mono symbol/ // C-name at emission) — both needed the same method- // value-arg source or the callee mono'd correctly // while the caller's `let` got the WRONG declared // type (found empirically: `NovaOpt_Nova_MvInferNum` // instead of `NovaOpt_nova_str` for `a.map(T.@to_str)` // — every downstream `match`/field-access on the // wrongly-typed local then mis-emits). Best-effort // (`Ok` only; a lookup miss silently leaves the slot // unresolved here, same permissive style as every // other source in this function — this function // returns `Option`, not a hard-error `Result`). if let Some(mv_name) = mv_name_full.strip_prefix('@') { if let Ok((_, mv_is_unbound, mv_recv_c_ty, mv_sig)) = self.method_value_lookup_sig(mv_obj, mv_name, None) { if mv_is_unbound { self.icr_trace("MVI_ricsub_step2m_entered"); let mv_param_tys: Vec<String> = std::iter::once(mv_recv_c_ty) .chain(mv_sig.param_c_types.iter().cloned()) .collect(); // [M-method-value-arg-in-generic-combinator-infer] // neg-case guard (mirrors `resolve_method_ // level_subst`'s Step 2m — see its doc): a // mismatch at an already-concrete position // means an INCOMPATIBLE method-value; the // authoritative hard error comes from // `resolve_method_level_subst` at actual // emission — this best-effort function just // declines to bind (leaves the slot // unresolved) rather than silently adopting // a wrong type for the `let`-binding. // NOTE: resolves against the LOCAL `subst` // accumulator (carrier-bound so far), NOT // `self.type_ref_to_c` — this function // does not maintain `self.current_type_ // subst` (unlike `resolve_method_level_ // subst`), so a `type_ref_to_c` call here // would resolve against whatever context // happens to be active at the CALL site, // not this receiver's own carrier binding // — confirmed by a real regression: it // false-positived on `Option[MvInferBox] // .flat_map(MvInferBox.@unwrap)` (T bound // fine via `subst`, but unrelated via // `current_type_subst`), silently // dropping a valid bind. let mismatch = fp.iter().zip(mv_param_tys.iter()).any(|(fp_ty, arg_c)| { !Self::typeref_is_bare_own_generic(fp_ty, &callee.generics) && Self::apply_type_subst_to_ref(fp_ty, &subst) .map(|c| !c.is_empty() && c != "void*" && &c != arg_c).unwrap_or(false) }); if !mismatch { for (fp_ty, arg_c) in fp.iter().zip(mv_param_tys.iter()) { self.infer_type_param_binding(fp_ty, arg_c, &mut subst); } let ret_c = &mv_sig.return_c_type; if !ret_c.is_empty() && ret_c != "void*" { self.infer_type_param_binding(ret_ty_ref.as_ref(), ret_c, &mut subst); } } } } } } } } self.type_subst_overrides.replace(saved); } Some(subst) } /// Plan 99.1 Ф.1: resolve method-level type-param substitutions для /// generic method (с собственными `[U]`/`[F]`/`[E]` после receiver T). /// /// Extract'нут из Plan 48 method-param mono логики (была inline в /// user-generic dispatch). Используется как user-generic'ом, так и /// builtin Option/Result `DeclaredBody`-dispatch (Plan 99.1 Ф.2/Ф.3). /// /// Algorithm (двухстадийный): /// 1. **Non-closure args** — standard `infer_type_param_binding` /// (Plan 98 расширенный) для каждого param/arg pair. /// 2. **ClosureLight args** — pre-infer closure return type /// с typed `var_types` для closure params (substituted через /// `current_type_subst`). Без этого `x: T` в `|x| ...` defaults /// к `nova_int` и method-level U не resolved. /// /// Loud-error при unresolved method-level type-params (Plan 48 Ф.9 / /// D119) с указанием на param-position для diagnostic. /// /// `current_type_subst` save'ится/restore'ится локально (вход с /// `receiver_subst`, выход — оригинальное состояние). Возвращает /// `Vec<(name, c_ty)>` отфильтрованный (пустые / `void*` отброшены). /// /// **[Q10, Plan 196.3 wave-2, 2026-07-12] Судьба: было ВТОРОЕ ОКНО, /// Tier-2 СТРУКТУРНО ЗАБЛОКИРОВАНО** (тот же вердикт, что у twin-функции /// `resolve_mono_type_args`, ~18421) — эта функция была собственным /// unification-движком (Steps 1/2/2f/3 ниже), дублирующим инференс, /// который `f1_check_call`/`resolve_return_channel` (`types/mod.rs`) /// обязаны выполнять для type-check'а вызова метода с method-level /// generics, но раньше не экспортировали. **Plan 196.5 Stage-B2: /// разблокировано** — тот самый канал (`node_substs: ExprId → /// Vec<(имя, ResolvedType)>`, Stage-A) теперь читается CHANNEL-FIRST /// ниже; Steps 1/2/2f/3 остаются легаси-fallback для промаха/residual /// (переходный период, Stage-C снимает fallback per-D после ICR-трейса). /// [M-196.5-node-substs] fn resolve_method_level_subst( &mut self, fn_decl: &crate::ast::FnDecl, args: &[CallArg], receiver_subst: &[(String, String)], diag_context: &str, call_id: crate::ast::ExprId, ) -> Result<Vec<(String, String)>, String> { if fn_decl.generics.is_empty() { return Ok(Vec::new()); } // [M-196.5-node-substs] Plan 196.5 Stage-B2: consume any pending // method-level turbofish (`obj.method[U,...](...)`) UP FRONT, // REGARDLESS of which path below resolves the subst — mirrors the // legacy seed's `mem::take` (a stashed OUTER turbofish must never // leak into a NESTED `resolve_method_level_subst` call triggered // while THIS call's own args/closures get emitted further down in // `emit_call`). The channel-first path below doesn't need the // VALUE — the checker already folded any explicit turbofish into // the subst it wrote to `node_substs` at this SAME `call_id` (same // AST node, same source text) — only the CLEARING side-effect; // the taken value itself is now dead (196 GEN-final snos below, // 2026-07-20) — the clear MUST still run unconditionally so an // outer turbofish never leaks into a nested call. let _explicit_tf = std::mem::take(&mut self.current_method_turbofish); // Channel-first: the checker (`f1_check_call` / `resolve_return_ // channel`, 196.5 §6.2) already solved this call-site's FULL subst // (receiver-carrier ++ method-level, in declaration order) into // `node_substs[call_id]` — pull just THIS method's OWN declared // names out of it by name, instead of re-running the Steps 1/2/2f/3 // inference below. Byte-identity gate: a name only "wins" when its // channel `ResolvedType` lowers (`resolved_type_to_c`, D315) to a // non-empty, non-`void*` C-string — the EXACT completeness bar the // legacy result is filtered through at the bottom of this function // (`.filter(|(_, c)| !c.is_empty() && c != "void*")`) — AND only // when EVERY method-generic name clears that bar (a partial hit is // never adopted: it would silently drop a name Step 2f/3 might // still resolve from forwarding/effect-clause sources the channel // doesn't cover). Any miss degrades the WHOLE call-site to the // untouched legacy fallback below — never a partial channel // adoption. `NOVA_NODE_SUBSTS_TRACE` tallies hit vs fallback // (mirrors the producer-side trace, 196.5 Stage-A). let trace_ns = std::env::var_os("NOVA_NODE_SUBSTS_TRACE").is_some(); if let Some(channel) = self.node_substs.get(&call_id) { let mut ordered: Vec<(String, String)> = Vec::with_capacity(fn_decl.generics.len()); let mut complete = true; for g in &fn_decl.generics { let lowered = channel.iter() .find(|(n, _)| n == &g.name) .and_then(|(_, rt)| self.resolved_type_to_c(rt).ok()) .filter(|c| !c.is_empty() && c != "void*"); match lowered { Some(c) => ordered.push((g.name.clone(), c)), None => { complete = false; break; } } } if complete { self.icr_trace("MVI_rmls_channel_hit"); if trace_ns { eprintln!( "[NODE_SUBSTS] consumer=resolve_method_level_subst call_id={:?} ctx={} hit n={}", call_id, diag_context, ordered.len(), ); } return Ok(ordered); } if trace_ns { eprintln!( "[NODE_SUBSTS] consumer=resolve_method_level_subst call_id={:?} ctx={} fallback reason=partial", call_id, diag_context, ); } } else if trace_ns { eprintln!( "[NODE_SUBSTS] consumer=resolve_method_level_subst call_id={:?} ctx={} fallback reason=miss", call_id, diag_context, ); } // ---- legacy fallback (Steps 1/2/2f/3, unchanged) ---- // Set receiver-only subst как окружающий контекст для // type_ref_to_c / infer_expr_c_type вызовов внутри. let saved_outer = std::mem::replace( &mut self.current_type_subst, Self::subst_map_from_c_pairs(receiver_subst.iter().cloned()), ); // Type-param slots, initially None. let mut subst_slots: Vec<(String, Option<String>)> = fn_decl.generics.iter() .map(|g| (g.name.clone(), None)) .collect(); // [M-196.5-stage-c2] Pre-seed any names the channel DID resolve, even // though the whole-map completeness gate above rejected the entry (a // partial channel hit still safely narrows what Step 1 below needs to // (re)derive — `infer_type_param_binding` never overwrites a bound // slot, mirroring the `ordered`/`complete` per-name lowering above). if let Some(channel) = self.node_substs.get(&call_id) { for (name, rt) in channel { if let Some(slot) = subst_slots.iter_mut().find(|(n, _)| n == name) { if slot.1.is_none() { if let Ok(c) = self.resolved_type_to_c(rt) { if !c.is_empty() && c != "void*" { slot.1 = Some(c); } } } } } } // [M-91.1-method-turbofish-dispatch] REMOVED (Plan 196 GEN-final, // 2026-07-20, docs/plans/wip/196-prodb-notes.md §9 + // docs/plans/wip/196-gen-final-notes.md). Former: seed slots from // explicit method-level type-args (`obj.method[U,...]`), // positionally onto `fn_decl.generics`, for names the channel-first // block above (~21276-21309) missed/left partial. Producer B // (196-prodb-notes.md §3-6) now writes `node_substs[call_id]` for // explicit turbofish on INSTANCE-method calls via // `resolve_return_channel`'s `explicit_method_type_args` overlay, // so the channel-first block is complete for this call class. // debug-only panic-detach (`[M-196-gen-final-detach]`) ran clean // (0 fires) across: the two REAL corpus call-sites of this AST // shape repo-wide (`standalone/m176_method_return_turbofish.nv`'s // `r.empty[str]()`/`r.into[str]()`, confirmed by a fresh repo-wide // grep — same N=2 Producer B found), the 5 byte-parity gate // fixtures (d119_option_result_method_level_generic, // d122_bound_method_mono_dispatch, d122_generic_bound_forwarding, // d30_try_op_unwrap_pair, d408_option_chain_sized_width), // m196_facetc_instance_collision_and_method_generic_default, the // std/src/{collections,time,encoding} standalone corpus (104 // files), and a 262-file partial sweep of spec_tests/conformance. // NO-HIT ⟹ structurally unreachable (§5) ⟹ removed outright (not // left as a live panic — the repo-wide grep bounds the AST shape // that could ever reach here to the 2 already-verified sites). // Step 1: non-closure args через standard inference (Plan 98 + // user-generic existing behavior). for (param, arg) in fn_decl.params.iter().zip(args.iter()) { let is_closure = matches!(arg.expr().kind, ExprKind::ClosureLight { .. } | ExprKind::ClosureFull(_)); if !is_closure { let arg_c = self.infer_expr_c_type(arg.expr()); self.infer_type_param_binding(¶m.ty, &arg_c, &mut subst_slots); } } // [M-196.5-stage-c2] Composed-hit early exit (Plan 196.5 Stage-C2, Class // R2 — docs/plans/196.5-stage-c-notes.md §2): if the channel pre-seed + // explicit turbofish + Step 1 (non-closure structural arg binding) alone // already resolved EVERY method-level generic, Steps 2/2f/3 below are // no-ops (each only fills a still-`None` slot) and the tail diagnostics // never fire — returning here is BYTE-IDENTICAL to letting the function // run to completion, just skipping the redundant closure/effect-clause // work. R2's dominant shape (`fn Vec[T] mut @append[S AsSlice[T]](other // S)`) binds `S` ONLY from `other`'s arg C-type — residual at CHECK time // (the checker's `node_substs` producer structurally can't write it // inside another generic body, §7 erased-body boundary) but ALREADY // concrete at EMIT time (`infer_expr_c_type` resolves through THIS mono // clone's `var_types`/`current_type_subst`) — composing the channel's // silence with that already-concrete arg C-type, not a new inference path. if subst_slots.iter().all(|(_, v)| v.is_some()) { self.current_type_subst = saved_outer; let result: Vec<(String, String)> = subst_slots.into_iter() .filter_map(|(n, c)| c.map(|c| (n, c))) .filter(|(_, c)| !c.is_empty() && c != "void*") .collect(); if trace_ns { eprintln!( "[NODE_SUBSTS] consumer=resolve_method_level_subst call_id={:?} ctx={} \ hit-composed n={}", call_id, diag_context, result.len(), ); } return Ok(result); } // Step 2: closure args (`ClosureLight` `|x| ...` или `ClosureFull` // `fn(x int) -> int => ...`) — pre-infer closure return type. // Для ClosureFull return-type explicit, не нужен var_types-binding. // Для ClosureLight — bind closure params в `var_types` для // typed body inference (substituted через current_type_subst). for (param, arg) in fn_decl.params.iter().zip(args.iter()) { let (fp, ret_ty_ref) = if let crate::ast::TypeRef::Func { params: fp, return_type: Some(rt), .. } = ¶m.ty { (fp.clone(), rt.clone()) } else { continue }; // ClosureFull — explicit return-type, можно использовать напрямую. if let ExprKind::ClosureFull(fsb) = &arg.expr().kind { // Bind param types если method-level в `fp` (fn(T) -> U). if let Ok(closure_param_tys) = fsb.params.iter() .map(|p| self.type_ref_to_c(&p.ty)) .collect::<Result<Vec<_>, _>>() { for (fp_ty, arg_c) in fp.iter().zip(closure_param_tys.iter()) { self.infer_type_param_binding(fp_ty, arg_c, &mut subst_slots); } } if let Some(ret_ty) = &fsb.return_type { if let Ok(ret_c) = self.type_ref_to_c(ret_ty) { if !ret_c.is_empty() && ret_c != "void*" { self.infer_type_param_binding( &ret_ty_ref, &ret_c, &mut subst_slots); } } } continue; } // ClosureLight — нужен tmp var_types binding для body inference. let (closure_params, body_expr) = match &arg.expr().kind { ExprKind::ClosureLight { params, body } => { let body_e = match body { crate::ast::ClosureBody::Expr(e) => (**e).clone(), crate::ast::ClosureBody::Block(b) => Expr::new( ExprKind::Block(b.clone()), b.span, ), }; (params.clone(), body_e) } _ => continue, }; // Tmp var_types: bind closure params к substituted fn-param types. let inner_ptys: Vec<String> = fp.iter() .map(|t| self.type_ref_to_c(t).map_err(|e| self.err_no_int_fallback( "closure param type binding", &e, ))) .collect::<Result<Vec<_>, _>>()?; let saved_var_types: Vec<(String, Option<String>)> = closure_params.iter().zip(inner_ptys.iter()) .map(|(cp, c_ty)| (cp.name.clone(), self.var_types.insert(cp.name.clone(), c_ty.clone()))) .collect(); let closure_ret_c = self.infer_expr_c_type(&body_expr); // Restore var_types. for (name, prev) in saved_var_types { match prev { Some(old) => { self.var_types.insert(name, old); } None => { self.var_types.remove(&name); } } } if !closure_ret_c.is_empty() && closure_ret_c != "void*" { self.infer_type_param_binding( &ret_ty_ref, &closure_ret_c, &mut subst_slots); } } // Step 2m [M-method-value-arg-in-generic-combinator-infer]: a // method-value arg (`Type.@method`, D35 UNBOUND method-value — bound // `obj.@method` was removed at check-time, E_BOUND_METHOD_REMOVED, // Plan 132, so only the unbound form can reach codegen here) is not // a closure literal (Step 2 above skips it) and its own C-type // (`infer_expr_c_type`) is an erased `void*` carrying no // `fn(T)->U`-shaped structure for Step 1 to bind against — a // method-level type-param appearing ONLY in a method-value arg's // return position (`map[U](f fn(T)->U)` fed `int.@to_str`) falls // through both Steps 1 and 2 unbound. The method-value's fn- // signature is knowable STATICALLY from the method's own // declaration (no body to infer) — mirror Step 2's ClosureFull // branch, sourcing param/return C-types from the method-value // registry lookup (`method_value_lookup_sig`, shared with actual // emission — see its doc) instead of an explicit `-> T` annotation. // Arg-bearing method-values (e.g. `int.@clamp` needing lo/hi) stay // out of scope: a method-value binds NO call args of its own, so // this only ever lifts an ARG-LESS method reference — that shape // still requires a closure (`a.map(|v| v.clamp(lo, hi))`). for (param, arg) in fn_decl.params.iter().zip(args.iter()) { let (fp, ret_ty_ref) = if let crate::ast::TypeRef::Func { params: fp, return_type: Some(rt), .. } = ¶m.ty { (fp.clone(), rt.clone()) } else { continue }; let (mv_obj, mv_name) = match &arg.expr().kind { ExprKind::Member { obj, name } => match name.strip_prefix('@') { Some(m) => (obj, m), None => continue, }, _ => continue, }; // Same selection `emit_method_value_typed` uses at actual // emission time (`target_sig: None` ⇒ single/first overload) — // whatever gets inferred here is guaranteed consistent with // what gets emitted later for this SAME arg expr. let (_mv_type_name, mv_is_unbound, mv_recv_c_ty, mv_sig) = self.method_value_lookup_sig(mv_obj, mv_name, None)?; if !mv_is_unbound { // Bound `obj.@method` — removed at check-time // (E_BOUND_METHOD_REMOVED, Plan 132); shouldn't reach here, // but skip rather than mis-infer if it somehow does. continue; } self.icr_trace("MVI_rmls_step2m_entered"); let mv_param_tys: Vec<String> = std::iter::once(mv_recv_c_ty) .chain(mv_sig.param_c_types.iter().cloned()) .collect(); for (fp_ty, arg_c) in fp.iter().zip(mv_param_tys.iter()) { // [M-method-value-arg-in-generic-combinator-infer] neg-case // guard: `fp_ty` positions that are NOT one of THIS // method's own type-params (the receiver's class-level // generic `T`, or a genuinely concrete type) are already // resolvable via the active receiver-only `current_type_ // subst` — a mismatch there means a genuinely INCOMPATIBLE // method-value (wrong receiver/param type), not an // inference gap. Silently proceeding would let a bogus // call compile (confirmed empirically: `Option[int].map // (str.@byte_len)` — receiver mismatch int vs str — // compiled clean pre-guard and SEGFAULTED at runtime, a // real C receiver-type mismatch, not a diagnosable Nova // error). Erroring here beats silent memory corruption. if !Self::typeref_is_bare_own_generic(fp_ty, &fn_decl.generics) { if let Ok(expected_c) = self.type_ref_to_c(fp_ty) { if !expected_c.is_empty() && expected_c != "void*" && &expected_c != arg_c { return Err(format!( "method-value argument type mismatch for `{diag_context}` \ — expected `{expected_c}`, method-value's signature has `{arg_c}`", )); } } } self.infer_type_param_binding(fp_ty, arg_c, &mut subst_slots); } let ret_c = &mv_sig.return_c_type; if !ret_c.is_empty() && ret_c != "void*" { self.infer_type_param_binding(&ret_ty_ref, ret_c, &mut subst_slots); } } // Step 2f [M-generic-method-self-recursive-return] (Plan 186, // recursive-mono, mirrors `resolve_mono_type_args` Source 2f / // [M-property-testing-rot]): a fn-typed arg passed as a bare // IDENTIFIER (not a closure literal) — e.g. `t.map(f)` inside // `@map[U]`'s OWN body, forwarding its OWN param `f: fn(T)->U` // straight through to a recursive call on itself. Steps 1/2 above // cannot infer `U` here: Step 1 explicitly skips fn-typed args (a // closure's C type is an erased `void*`, encoding no type info) and // Step 2 only pre-infers CLOSURE LITERALS, not identifier references. // Unify the callee's declared param TypeRef against the CALLER's own // declared TypeRef for that identifier (`current_fn_param_typerefs`, // now populated by `emit_monomorphized_method` too — see its doc) // instead of a C-type comparison; `type_ref_to_c` on the caller's // side must lower through the ENCLOSING method's FULL type-arg subst // (`saved_outer` — both receiver AND method-level params, e.g. T AND // U), NOT the receiver-only view this function activated above // (line ~18953: `current_type_subst` = `receiver_subst` only, for // Steps 1-3's OWN lookups) — a bare method-level typaram reference // like "U" has no receiver-subst entry and would otherwise resolve to // the erroneous literal-name fallback `Nova_U*` (a bogus "user type" // named U) instead of the enclosing scope's already-resolved binding. // Swap in `saved_outer` only for this block's own resolution calls. { let saved_receiver_only = std::mem::replace( &mut self.current_type_subst, saved_outer.clone()); for (param, arg) in fn_decl.params.iter().zip(args.iter()) { if subst_slots.iter().all(|(_, v)| v.is_some()) { break; } if let ExprKind::Ident(name) = &arg.expr().kind { if let Some(decl_ref) = self.current_fn_param_typerefs.get(name).cloned() { self.infer_type_param_binding_from_ref(¶m.ty, &decl_ref, &mut subst_slots); } } } self.current_type_subst = saved_receiver_only; } // Step 3 [M-exp-promotion-blockers: retry E_UNUSED_PREFIX_TYPEVAR]: // infer effect-clause type-params (`Fail[E]`) from a closure ARG's // thrown value. `E` in `body fn() Fail[E] -> T` appears ONLY inside // the param's own effect-clause — never in a directly-inspectable // value position — so Steps 1/2 (which bind from param/return C // types) can never see it. The one place its concrete type IS // observable is the closure body's `throw <expr>`. for (param, arg) in fn_decl.params.iter().zip(args.iter()) { let effects: &[crate::ast::TypeRef] = match ¶m.ty { crate::ast::TypeRef::Func { effects, .. } => effects, _ => continue, }; if effects.is_empty() { continue; } let body_expr: Option<Expr> = match &arg.expr().kind { ExprKind::ClosureLight { body, .. } => Some(match body { crate::ast::ClosureBody::Expr(e) => (**e).clone(), crate::ast::ClosureBody::Block(b) => Expr::new( ExprKind::Block(b.clone()), b.span), }), ExprKind::ClosureFull(fsb) => match &fsb.body { crate::ast::FnBody::Expr(e) => Some(e.clone()), crate::ast::FnBody::Block(b) => Some(Expr::new( ExprKind::Block(b.clone()), fsb.span)), crate::ast::FnBody::External => None, }, _ => None, }; let Some(body_expr) = body_expr else { continue }; let Some(thrown) = Self::find_first_throw_value(&body_expr) else { continue }; let thrown_c = self.infer_expr_c_type(thrown); if thrown_c.is_empty() || thrown_c == "void*" { continue; } for eff in effects { if let crate::ast::TypeRef::Named { path, generics, .. } = eff { if path.last().map(String::as_str) == Some("Fail") && generics.len() == 1 { self.infer_type_param_binding(&generics[0], &thrown_c, &mut subst_slots); } } } } // Restore outer subst — все inference-эффекты завершены. self.current_type_subst = saved_outer; // Plan 48 Ф.9 / D119: diagnose unresolved method-level type-params // ДО return — silent drop'a быть не должно. for (name, resolved) in &mut subst_slots { if resolved.is_none() { let positions: Vec<String> = fn_decl.params.iter() .enumerate() .filter_map(|(i, p)| { let mut names = std::collections::HashSet::new(); Self::collect_typeref_names(&p.ty, &mut names, &mut std::collections::HashSet::new()); if names.contains(name) { Some(format!("param `{}` (#{i})", p.name)) } else { None } }) .collect(); if positions.is_empty() { // [M-exp-promotion-blockers: retry] `collect_typeref_names` // deliberately does not descend into a `Fail[E]` effect's // OWN generic arg (it routes the effect NAME to the // vtable-name set, not the value-type set) — so an empty // `positions` here means the typevar is referenced ONLY // inside an effect-clause (`Fail[E]`), not "nowhere"/"only // in return type" as the pre-existing message assumed. // Step 3 above already tried to bind it from a thrown // value; reaching here means the closure body genuinely // never throws (e.g. `policy.execute(|| 42)` with no // `Fail` handler in scope) — the effect branch is // unreachable dead code for THIS call, so any concrete // C type is functionally safe to pick (the `Option[E]` // last-error slot never gets constructed with real data). // Default to `nova_str` (the overwhelmingly common // Nova error-payload shape) rather than a hard error — // requiring an explicit turbofish on every non-throwing // callback would be an ergonomics regression for a // dead-code-only ambiguity, not a real inference failure. *resolved = Some("nova_str".to_string()); continue; } return Err(format!( "cannot infer method-level type argument `{name}` for `{diag_context}` \ — appears in {}; provide a closure/arg whose type fixes `{name}`", positions.join(", "), )); } } Ok(subst_slots.into_iter() .filter_map(|(n, c)| c.map(|c| (n, c))) .filter(|(_, c)| !c.is_empty() && c != "void*") .collect()) } /// [M-exp-promotion-blockers: retry] Find the first `throw <expr>` reachable /// from `expr` by walking straight-line control flow (`if`/`if let`/`match` /// bodies) without crossing into nested closures/loops (a `throw` inside a /// nested `for`/`while` body doesn't necessarily execute — conservative: /// only descends where a value is unconditionally observable on some path). /// Used to infer a `Fail[E]`-effect's `E` from a closure argument's body /// when no other source binds it (see Step 3 above). fn find_first_throw_value(expr: &Expr) -> Option<&Expr> { match &expr.kind { ExprKind::Block(b) => Self::find_first_throw_in_block(b), ExprKind::If { then, else_, .. } => { Self::find_first_throw_in_block(then).or_else(|| match else_ { Some(crate::ast::ElseBranch::Block(b)) => Self::find_first_throw_in_block(b), Some(crate::ast::ElseBranch::If(e)) => Self::find_first_throw_value(e), None => None, }) } ExprKind::IfLet { then, else_, .. } => { Self::find_first_throw_in_block(then).or_else(|| match else_ { Some(crate::ast::ElseBranch::Block(b)) => Self::find_first_throw_in_block(b), Some(crate::ast::ElseBranch::If(e)) => Self::find_first_throw_value(e), None => None, }) } ExprKind::Match { arms, .. } => arms.iter().find_map(|arm| match &arm.body { crate::ast::MatchArmBody::Expr(e) => Self::find_first_throw_value(e), crate::ast::MatchArmBody::Block(b) => Self::find_first_throw_in_block(b), }), _ => None, } } fn find_first_throw_in_block(b: &Block) -> Option<&Expr> { for s in &b.stmts { match s { Stmt::Throw { value, .. } => return Some(value), Stmt::Expr(e) => { if let Some(t) = Self::find_first_throw_value(e) { return Some(t); } } _ => {} } } b.trailing.as_deref().and_then(Self::find_first_throw_value) } /// Plan 56 followup: compute real array element type для произвольной /// глубины field-access chains (`obj.f1.f2.f3.field[i]` где `field` — /// array of mono'd struct pointers). /// /// Возвращает Some(elem_c_ty) если obj indicates struct AND field is /// Array field в struct schema/template. None — fallback на caller /// default. /// /// Worker для emit_expr/infer_expr_c_type Index path: cast cast'ит /// `data[i]` в proper struct pointer вместо nova_int default. fn debt_compute_field_array_elem_type(&self, obj: &Expr, field: &str) -> Option<String> { let obj_ty = self.infer_expr_c_type(obj); // Strip Nova_ prefix и trailing *. let struct_name = obj_ty .strip_prefix("Nova_") .unwrap_or("") .trim_end_matches('*').trim(); if struct_name.is_empty() { return None; } // Try concrete mono schema first. if let Some(schema) = self.record_schemas.get(struct_name) { if let Some(field_c_ty) = schema.get(field) { if let Some(elem_raw) = field_c_ty.strip_prefix("NovaArray_") { let elem = elem_raw.trim_end_matches('*').trim(); if !elem.is_empty() && elem != "nova_int" { return Some(if field_c_ty.ends_with("**") { format!("{}*", elem) } else { elem.to_string() }); } } } } // Try template + subst (mono'd name like "HashMap____<K>__<V>"). let base_name: &str = struct_name.split("____").next().unwrap_or(struct_name); if base_name.len() < struct_name.len() { // Plan 153.2 Ф.2 (STAGE 2): depth-AWARE, registry-backed args (see // the field-type resolver's twin fix) — a nested generic-over-source // arg must not be torn at its inner `____`. let type_args: Vec<String> = self.debt_mono_type_args_of(struct_name); if let Some(template) = self.generic_type_templates.get(base_name).cloned() { if let crate::ast::TypeDeclKind::Record(fields) = &template.kind { if let Some(field_decl) = fields.iter().find(|f| f.name == field) { if let crate::ast::TypeRef::Array(inner, _) = &field_decl.ty { // Compute concrete element type via subst. let subst: Vec<(String, Option<String>)> = template.generics.iter() .zip(type_args.iter()) .map(|(g, c)| (g.name.clone(), Some(c.clone()))) .collect(); if let Some(elem_c) = Self::apply_type_subst_to_ref(inner, &subst) { return Some(elem_c); } } } } } } None } /// Plan 56 followup: deep version — компонует chain `obj.f1.f2.field[i]` /// рекурсивно вызывая debt_compute_field_array_elem_type на каждом уровне. /// Возвращает element type для **самого внутреннего** array. /// /// Plan 196.3 wave-2 (D239, one-window) status, re-verified: the stale claim /// this doc comment used to carry — a direct caller inside `infer_call_ret_c`'s /// `"get"` arm (frozen wave-1 zone) — no longer matches the code (grepped: 0 /// hits inside the current frozen-zone line range). Whatever caller that /// referred to is gone (removed by wave-1's own sweep, or the coordinates /// drifted as the file grew — either way it's not there now). The **sole** /// remaining caller is the Channel-6k fallback inside `infer_expr_c_type` /// (`channel_array_elem_c` miss → this fn), itself OUTSIDE `infer_call_ret_c` — /// so this fn is SEP, not SHARED, as of this verification. NOT yet proven /// 0-hit at that one call site (deep non-self field-access chain, /// `obj.f1.f2.field[i]`, is a real remaining gap in `channel_array_elem_c`'s /// coverage — it only reads `resolved_types[obj.id]` for the immediate `obj`, /// not a recursive field chain) — kept as a live fallback, not pruned. fn compute_array_elem_type_for_obj(&self, obj: &Expr) -> Option<String> { match &obj.kind { ExprKind::Ident(n) => self.array_element_types.get(n).cloned(), ExprKind::Member { obj: inner, name } => { self.debt_compute_field_array_elem_type(inner, name) } // [M-91.8c-direct-index-method]: @[j] in a generic []T method body // (ExprKind::SelfAccess). emit_monomorphized_method registers the // concrete element C type under "nova_self" in array_element_types // so that infer_expr_c_type for `@[j].compare(key)` resolves the // correct pointer type (e.g. "Nova_Point*") instead of falling back // to the NovaArray_ strip path which loses the trailing '*'. ExprKind::SelfAccess => self.array_element_types.get("nova_self").cloned(), _ => None, } } /// Plan 196.3 wave-2 (D239, one-window): channel-first element type for a /// `[]T`/`Vec[T]`-typed `obj` expression. Reads the checker's resolved type /// for `obj` (`resolved_types[obj.id]`, populated by `f1_expr_inner`'s /// Ident/Member/SelfAccess probes — types/mod.rs ~7249-7325, D239/D315 §0: /// `[]T`/`Vec[T]` canonicalize to `Named{Vec,[elem]}` in the channel) and /// lowers the Vec element arg through the SINGLE checker→C path /// (`resolved_type_to_c`) — mirrors the precedent at `emit_parallel_for` /// (~11289-11300) — instead of re-deriving it from the codegen-local /// `array_element_types` side-table / `debt_compute_field_array_elem_type` /// AST-walk that `compute_array_elem_type_for_obj` performs. `None` = channel /// miss (obj un-annotated, or its resolved type isn't a `Named{Vec,[elem]}` — /// e.g. a bare unsubstituted generic-level `T`) → caller falls back to /// `compute_array_elem_type_for_obj` (legacy). fn channel_array_elem_c(&self, obj: &Expr) -> Option<String> { if !obj.id.is_set() { return None; } match self.resolved_types.get(&obj.id) { Some(crate::types::ResolvedType::Named { name, args, .. }) if name == "Vec" && args.len() == 1 => { self.resolved_type_to_c(&args[0]) .ok() .filter(|c| !c.is_empty() && !self.debt_is_generic_stub_c(c)) } _ => None, } } /// [M-91.1-composite-array-storage] Plan 91 Ф.1: if a generic array-ext /// method (`fn[T] []T @map[U](...) -> []U`) returns `[]U` and U substitutes /// to a COMPOSITE pointer C-type (record/sum `Nova_<Name>*`, mono'd /// `Nova_X____...*`), register that element type in `array_element_types` /// keyed by the emitted call string `result_str`. The let-binding /// propagation (search `array_element_types.get(&val)`) then copies it onto /// the result variable, so `result[i].field`, `for x in result`, and /// `result.get(i)` cast the int64-erased slot back to the real element /// pointer instead of failing on `nova_int`. Primitive U is left alone /// (handled by the C-type-name path). No-op when U is unresolved/erased. fn register_array_result_elem( &mut self, fn_decl: &crate::ast::FnDecl, type_subst: &[(String, String)], result_str: &str, recv_elem: Option<&str>, ) { if let Some(crate::ast::TypeRef::Array(inner, _)) = &fn_decl.return_type { if let crate::ast::TypeRef::Named { path, generics, .. } = inner.as_ref() { if path.len() == 1 && generics.is_empty() { let uname = &path[0]; // Resolve the result element C-type. `map`→`[]U`: U comes // from closure-return inference (e.g. "Nova_Wrap*"). But // `filter`→`[]T` returns the RECEIVER element type-param, // which type_subst resolves to the ERASED "nova_int" (the // receiver C-name carries no composite info). In that case // prefer the receiver's real element type (recv_elem from // array_element_types) so filtered `[]Record` keeps casting. let mut elem = type_subst.iter() .find(|(n, _)| n == uname) .map(|(_, c)| c.clone()); if elem.as_deref() == Some("nova_int") { if let Some(re) = recv_elem { if re.ends_with('*') && re != "nova_int*" { elem = Some(re.to_string()); } } } if let Some(c) = elem { if !Self::is_primitive_array_elem_c(&c) && c.ends_with('*') { self.array_element_types.insert(result_str.to_string(), c); } } } } } } /// Plan 48: apply a type param substitution to a TypeRef, returning a C type string. /// Used in infer_expr_c_type for resolving the return type of generic fn calls. /// Returns None if type cannot be resolved from the subst alone (e.g. non-named types). /// Plan 85.4: true если `ty` упоминает любое имя из `names` (имена /// generic-параметров функции). Используется чтобы отличить fully- /// concrete return-тип (резолвится через `type_ref_to_c`) от типа с /// неразрешёнными type-params (erased → void*). fn type_ref_mentions_name(ty: &crate::ast::TypeRef, names: &[String]) -> bool { use crate::ast::TypeRef; match ty { TypeRef::Named { path, generics, .. } => { let n = path.join("_"); names.iter().any(|x| x == &n) || generics.iter().any(|g| Self::type_ref_mentions_name(g, names)) } TypeRef::Array(inner, _) | TypeRef::FixedArray(_, inner, _) => Self::type_ref_mentions_name(inner, names), TypeRef::Tuple(elems, _) => elems.iter().any(|e| Self::type_ref_mentions_name(e, names)), TypeRef::Func { params, return_type, .. } => params.iter().any(|p| Self::type_ref_mentions_name(p, names)) || return_type.as_ref() .map(|r| Self::type_ref_mentions_name(r, names)) .unwrap_or(false), // Plan 97 Ф.2: methods могут ссылаться на type-параметры // окружения (например, `[T protocol { @lt(other Self) -> bool }]` // не упоминает T в protocol-теле, но методы более сложных // inline-protocol'ов могут — рекурсивно проверяем). TypeRef::Protocol { methods, .. } => methods.iter().any(|m| { m.params.iter().any(|p| Self::type_ref_mentions_name(&p.ty, names)) || m.return_type.as_ref() .map(|r| Self::type_ref_mentions_name(r, names)) .unwrap_or(false) }), TypeRef::Unit(_) => false, // D176 (Plan 108): readonly T — transparent. TypeRef::Readonly(inner, _) => Self::type_ref_mentions_name(inner, names), // Plan 118 D216: typed pointer `*T` — recurse on inner. // Plan 118.5: Mut/Unsafe are transparent wrappers. TypeRef::Pointer(inner, _) | TypeRef::Mut(inner, _) | TypeRef::Uninit(inner, _) | TypeRef::Ref(inner, _) => Self::type_ref_mentions_name(inner, names), } } fn apply_type_subst_to_ref( ty: &crate::ast::TypeRef, subst: &[(String, Option<String>)], ) -> Option<String> { match ty { crate::ast::TypeRef::Named { path, generics, .. } if generics.is_empty() => { let name = path.join("_"); // Check if this is a type param if let Some((_, Some(c_ty))) = subst.iter().find(|(n, _)| n == &name) { return Some(c_ty.clone()); } // U.6.1.b: common primitives via the SINGLE shared `primitive_name_to_c` // (no third hand-copied table — the missing u32 there drifted and broke // Vec[u32] mangling, Plan 152.8). if let Some(c) = Self::primitive_name_to_c(&name) { return Some(c.to_string()); } // `byte` alias (≡ `u8` → nova_byte) is apply-only — `type_ref_to_c` has // no bare `byte` arm — so it stays here, NOT in the shared table. if name == "byte" { return Some("nova_byte".to_string()); } // Concrete user-types (NOT type-params) резолвятся caller'ом через // `type_ref_to_c` — здесь нельзя отличить `Ordering` (user-тип) от // unresolved type-param `U` без registry, поэтому только примитивы. None } // Option[T] → NovaOpt_<T_c> crate::ast::TypeRef::Named { path, generics, .. } if path.last().map(|s| s.as_str()) == Some("Option") && generics.len() == 1 => { let inner_c = Self::apply_type_subst_to_ref(&generics[0], subst)?; let sanitized = Self::sanitize_for_novaopt(&inner_c); Some(format!("NovaOpt_{}", sanitized)) } // Plan 88 Ф.2.1: Result[T, E] → `NovaRes_<ok>_<err>*` — mono'd // Result-репрезентация (Plan 59 Ф.7.5). Зеркало `type_ref_to_c` // ветки "Result" и `apply_type_subst_to_ref` ветки Option. Без // этого `Result[T,E]` уходила в generic-user-type ветку ниже и // мангл'илась как `Nova_Result____<ok>__<err>` — имя, которое // нигде не объявлено (CC-FAIL «unknown type name»). Typedef // `NovaRes_<n>` регистрируется при emit'е самой mono'd функции // (её return-тип идёт через `type_ref_to_c` → `result_repr_c_type`). crate::ast::TypeRef::Named { path, generics, .. } if path.last().map(|s| s.as_str()) == Some("Result") && generics.len() == 2 => { let ok_c = Self::apply_type_subst_to_ref(&generics[0], subst)?; let err_c = Self::apply_type_subst_to_ref(&generics[1], subst)?; let ok_s = Self::sanitize_for_novaopt(&ok_c); let err_s = Self::sanitize_for_novaopt(&err_c); Some(format!("NovaRes_{}_{}*", ok_s, err_s)) } crate::ast::TypeRef::Array(inner, _) => { // [M-91.1-composite-array-storage] Plan 91 Ф.1 lesson: this // call-site MUST NOT invent type names the emit passes never // declare, or mono fwd-decls CC-FAIL with "unknown type name". // Plan 172.12 A8: primitive elements switch from the retired // `NovaArray_<prim>*` (deleted with array.h's DECL/IMPL snос) // to the Vec[T]-mangled name — the body lowering // (`resolved_array_to_c`) registers exactly that instance, so // the name always exists. NON-primitive elements keep the // int64-slot `NovaArray_nova_int*` erasure sentinel unchanged // (pre-A8 behavior): an UNSUBSTITUTED placeholder element // (`Nova_T*` inside an erased-generic template) would otherwise // mangle to a never-emitted `Nova_Vec____Nova_T_p` (empirically // hit by std/collections/vec_seq), and the sentinel struct is // still unconditionally declared in array.h. let inner_c = Self::apply_type_subst_to_ref(inner, subst)?; if Self::is_primitive_array_elem_c(&inner_c) { Some(format!("{}*", Self::compute_generic_type_c_name("Vec", &[inner_c]))) } else { Some("NovaArray_nova_int*".to_string()) } } // Generic user-defined type e.g. Pair[B, A] → Nova_Pair____T1__T2* crate::ast::TypeRef::Named { path, generics, .. } if !generics.is_empty() && path.last().map(|s| s.as_str()) != Some("Option") => { let mut resolved = Vec::new(); for g in generics { if let Some(c) = Self::apply_type_subst_to_ref(g, subst) { resolved.push(c); } else { return None; } } let base = path.last().cloned().unwrap_or_default(); let mangled = Self::compute_generic_type_c_name(&base, &resolved); Some(format!("{}*", mangled)) } crate::ast::TypeRef::Unit(_) => Some("nova_unit".to_string()), crate::ast::TypeRef::Tuple(elems, _) if elems.is_empty() => { // () zero-tuple is nova_unit, same as TypeRef::Unit. Some("nova_unit".to_string()) } crate::ast::TypeRef::Tuple(elems, _) => { // Plan 59: mono'd tuple struct. Compute element C types via // recursive subst, then mangled struct name. Returns struct // value type (без `*`) — stored direct в NovaOpt_<...>.value // and other containers без heap boxing. // Caller responsible для registering struct emit via // mono_tuple_instances (use `register_tuples_in_typeref` helper). // let mut elem_cs: Vec<String> = Vec::with_capacity(elems.len()); for e in elems { let c = Self::apply_type_subst_to_ref(e, subst)?; elem_cs.push(c); } Some(Self::compute_mono_tuple_c_name(&elem_cs)) } // Plan 131 Ф.3: typed pointer family `*T` / `*mut T` / `*unsafe T` // (parsed as `Pointer(inner)` where inner may itself be `Mut`/ // `Unsafe`). Resolve the pointee via subst и append `*` so a // generic free fn returning `*mut T` (e.g. `alloc_buf[T] -> *mut T`) // infers a concrete `nova_int*` / `NovaOpt_nova_int*` / `Nova_Pt**` // at the call site, не erased `void*`. Mirror of `type_ref_to_c` // `Pointer`/`Mut`/`Unsafe` arms (sans const-ness, irrelevant for the // inferred binding C type). // Plan 134: *() = pointer-to-unit = void* (replaces `ptr` builtin). crate::ast::TypeRef::Pointer(inner, _) => { let base = match inner.as_ref() { crate::ast::TypeRef::Mut(ti, _) | crate::ast::TypeRef::Uninit(ti, _) => ti.as_ref(), other => other, }; // *() = void*. if matches!(base, crate::ast::TypeRef::Unit(_)) { return Some("void*".to_string()); } let inner_c = Self::apply_type_subst_to_ref(base, subst)?; Some(format!("{}*", inner_c)) } crate::ast::TypeRef::Mut(inner, _) | crate::ast::TypeRef::Uninit(inner, _) => { if let crate::ast::TypeRef::Pointer(p_inner, _) = inner.as_ref() { // *mut () = void*. if matches!(p_inner.as_ref(), crate::ast::TypeRef::Unit(_)) { return Some("void*".to_string()); } let inner_c = Self::apply_type_subst_to_ref(p_inner, subst)?; Some(format!("{}*", inner_c)) } else { Self::apply_type_subst_to_ref(inner, subst) } } _ => None, } } // Plan 172.1 U.6.1.a: `simple_type_ref_to_c` (a lossy static mirror of `type_ref_to_c` // that drifted — the missing u32 broke Vec[u32] mangling, Plan 152.8) was DELETED; the // one TurboFish-member caller (infer_expr_c_type) now delegates to the single instance // `type_ref_to_c`. One type->C path (§0/§2). NOT byte-identical: the smart resolver // resolves nested mono type-params to concrete (more precise) — owner-approved 2026-06-20. /// Plan 48 V1 fallback: register/emit a void*-erased version of a generic fn on demand. /// Called when type argument inference fails (e.g. generic record params). /// Idempotent — body is only emitted once (guarded by mono_instantiated). fn register_erased_instance(&mut self, fn_decl: &crate::ast::FnDecl) { let erased_name = self.free_fn_c_name(&fn_decl.name); if self.mono_instantiated.contains(&erased_name) { return; // Already registered (either erased or mono base name) } self.mono_instantiated.insert(erased_name.clone()); // Build erased signature: type params → void*, other params → void* let type_params: HashSet<String> = fn_decl.generics.iter().map(|g| g.name.clone()).collect(); let params_str = if fn_decl.params.is_empty() { "void".to_string() } else { fn_decl.params.iter().map(|p| { match &p.ty { TypeRef::Named { path, generics, .. } => { let nm = path.join("_"); if type_params.contains(&nm) { format!("void* {}", p.name) } else if !generics.is_empty() && self.record_schemas.contains_key(&nm) { format!("Nova_{}* {}", nm, p.name) } else { format!("void* {}", p.name) } } _ => format!("void* {}", p.name), } }).collect::<Vec<_>>().join(", ") }; // Emit forward decl into mono_fwd_decls buffer self.mono_fwd_decls.push_str(&format!("{}void* {}({});\n", self.top_level_storage(), erased_name, params_str)); // Register var_types for erased return (legacy) self.var_types.insert(format!("fn_ret_{}", fn_decl.name), "void*".into()); // Enqueue in worklist with special marker: empty type_subst = erased mode // We store the ERASED_SENTINEL as fn_name with special prefix to distinguish from mono self.mono_worklist.push(( format!("__erased__{}", fn_decl.name), vec![], erased_name, )); } /// Plan 48: register a monomorphized fn instance (add forward decl + worklist entry). /// Idempotent — safe to call multiple times with the same mono_name. fn register_mono_instance( &mut self, fn_decl: &crate::ast::FnDecl, type_subst: Vec<(String, String)>, mono_name: &str, ) { if self.mono_instantiated.contains(mono_name) { return; } self.mono_instantiated.insert(mono_name.to_string()); // Compute param and return C types with substitution applied let saved_subst = std::mem::replace( &mut self.current_type_subst, Self::subst_map_from_c_pairs(type_subst.iter().cloned()), ); // Plan 70 PhaseB1 (session 2): cascade-blocked site (register_mono_instance — // no return type, caller chain change requires массивный refactor). // Strict mode: record E7001 в strict_errors; emit_module finalization // fails build если non-empty. См. record_strict_error doc. // [M-generic-static-method-value-arg-addr-mismatch] fix (221.1 Ф.2 // #26): same gap as `register_mono_method_instance`'s identical fix // (this file) — this is the FREE-FUNCTION twin. Without consulting // `free_fn_byref_flag` here, a mono'd GENERIC free fn with a large // `ro` value-struct param (Plan 172.14) declared it BY VALUE while // any call site passing that same value-struct to it (or, as in the // live repro, a generic free fn forwarding its own by-value param // straight into a flagged callee) produced a value/pointer mismatch. let param_c_tys: Vec<String> = fn_decl.params.iter().enumerate() .map(|(p_idx, p)| { let mut ty_c = self.type_ref_to_c(&p.ty).unwrap_or_else(|e| self.record_strict_error( &format!("register_mono_instance `{}` param `{}`", fn_decl.name, p.name), &e, )); if self.free_fn_byref_flag(&fn_decl.name, p_idx) { ty_c.push('*'); } ty_c }) .collect(); // Plan 70 PhaseB2 (session 2): cascade-blocked site (register_mono_instance). // Outer None = no return_type declared (void fn — legitimate unit). // Inner type_ref_to_c failure = silent miscompilation → E7001 via // record_strict_error. Placeholder "nova_unit" returned, finalization fails. let ret_c = fn_decl.return_type.as_ref() .map(|t| self.type_ref_to_c(t).unwrap_or_else(|e| self.record_strict_error( &format!("register_mono_instance `{}` return type", fn_decl.name), &e, ))) .unwrap_or_else(|| "nova_unit".into()); self.current_type_subst = saved_subst; let params_str = if fn_decl.params.is_empty() { "void".to_string() } else { fn_decl.params.iter().zip(¶m_c_tys) .map(|(p, ty)| format!("{} {}", ty, p.name)) .collect::<Vec<_>>() .join(", ") }; // Emit forward decl into buffer self.mono_fwd_decls.push_str(&format!( "{}{} {}({});\n", self.top_level_storage(), ret_c, mono_name, params_str )); // Enqueue for body emission (A1‴: carrier is RT-typed; lift string subst at the boundary) self.mono_worklist.push((fn_decl.name.clone(), Self::subst_vec_from_c_pairs(&type_subst), mono_name.to_string())); } /// Plan 48: register a monomorphized METHOD instance (add forward decl + worklist entry). /// Like register_mono_instance but prepends `nova_self` receiver param. fn register_mono_method_instance( &mut self, fn_decl: &crate::ast::FnDecl, type_subst: Vec<(String, String)>, mono_name: &str, recv_type: &str, ) { if self.mono_instantiated.contains(mono_name) { return; } // Plan 55 followup ([M-erased-generic-method-dispatch]): skip mono'd // emit когда type_subst содержит placeholder (Nova_X* для X из // fn_decl.generics). Это случается когда @clone()/@something в generic // body делает рекурсивный call на Self type (e.g. HashMap[K, V].with_ // capacity внутри @clone) — placeholder становится "concrete" subst, // что приводит к broken `key->hash()` C-syntax для bound K methods. // Real mono triggers только с фактически concrete types. let generic_names: std::collections::HashSet<String> = fn_decl.generics.iter() .map(|g| g.name.clone()).collect(); if !generic_names.is_empty() { let has_placeholder = type_subst.iter() .any(|(_, c)| self.debt_type_arg_is_bare_placeholder(c, &generic_names)); if has_placeholder { return; } } self.mono_instantiated.insert(mono_name.to_string()); // Compute param and return C types with substitution applied. // Plan 11 Follow-up (2026-05-17): current_receiver_type set BEFORE // computing param_c_tys, чтобы `Self` в param-position (e.g. // `other Self`) тоже резолвилось правильно. Раньше set только // перед ret_c — params получали Nova_Self fallback. let saved_subst = std::mem::replace( &mut self.current_type_subst, Self::subst_map_from_c_pairs(type_subst.iter().cloned()), ); let prev_recv_for_ret = self.current_receiver_type.replace(recv_type.to_string()); self.sync_receiver_rt(); // [M-138.2-self-in-param]: bind `Self` for nested type-arg `Self` so the // FORWARD DECL matches the body (emit_monomorphized_method does the same). // See the detailed rationale there. Both fwd-decl and body must produce // the SAME value-aware mono or C reports conflicting types. self.debt_bind_self_for_mono_recv(recv_type); // Plan 128 Ф.1: thread recv.mutable from fn_decl AST (Ф.2 consumes). let recv_mutable = fn_decl.receiver.as_ref().map(|r| r.mutable).unwrap_or(false); let recv_c = self.receiver_c_type(recv_type, recv_mutable); // [M-nested-generic-receiver-method-mono] (реестр 221.1 №247): see // `debt_rebind_nested_receiver_typevars` doc — rebinds a builtin // Option/Result carrier's colliding "T"/"E" to the method's OWN // (deep) view for the fwd-decl's own return-type computation below. self.debt_rebind_nested_receiver_typevars(fn_decl, &recv_c); // Plan 70 PhaseB1 (session 2): cascade-blocked site (register_mono_method_instance — // no return type). Strict mode: record E7001 в strict_errors. // [M-generic-static-method-value-arg-addr-mismatch] fix (221.1 Ф.2 // #26): mirrors the identical fix in `emit_monomorphized_method_ // scoped_inner`'s own `param_c_tys` (same file, this instance's // DEFINITION) — this is the FORWARD-DECL twin, and without this same // `method_byref_flag` check the forward decl kept declaring the // param BY VALUE even after the definition was fixed to `T*`, // itself now a forward-decl/definition C conflict. See the // definition-side comment for the full root-cause note. let param_c_tys: Vec<String> = fn_decl.params.iter().enumerate() .map(|(p_idx, p)| { let mut ty_c = self.type_ref_to_c(&p.ty).unwrap_or_else(|e| self.record_strict_error( &format!("register_mono_method_instance `{}.{}` param `{}`", recv_type, fn_decl.name, p.name), &e, )); if let Some(recv) = &fn_decl.receiver { if self.method_byref_flag(&recv.type_name, &fn_decl.name, p_idx) { ty_c.push('*'); } } ty_c }) .collect(); // Plan 70 PhaseB2 (session 2): cascade-blocked site (register_mono_method_instance). let ret_c = fn_decl.return_type.as_ref() .map(|t| self.type_ref_to_c(t).unwrap_or_else(|e| self.record_strict_error( &format!("register_mono_method_instance `{}.{}` return type", recv_type, fn_decl.name), &e, ))) .unwrap_or_else(|| "nova_unit".into()); self.current_receiver_type = prev_recv_for_ret; self.sync_receiver_rt(); self.current_type_subst = saved_subst; // Plan 48 Ф.7.2: static-методы без nova_self. let is_instance = matches!(fn_decl.receiver.as_ref().map(|r| &r.kind), Some(crate::ast::ReceiverKind::Instance)); let mut parts: Vec<String> = if is_instance { vec![format!("{} nova_self", recv_c)] } else { Vec::new() }; for (p, ty) in fn_decl.params.iter().zip(¶m_c_tys) { parts.push(format!("{} {}", ty, p.name)); } let params_str = if parts.is_empty() { "void".to_string() } else { parts.join(", ") }; // Forward decl // // Plan 95 Ф.2.4: для builtin sum-типов (`Option`/`Result`) сигнатура // содержит `NovaOpt_<T>` (by-value) / `NovaRes_<n>*` (pointer) — // routing fwd-decl в `builtin_sum_method_fwd_decls` чтобы splice'ить // его ПОСЛЕ NovaOpt/NovaRes typedef placeholder'ов (file order: // typedefs Y < fwd-decl Z < body P). Иначе CC-fail `incomplete type`. let fwd_decl = format!("{}{} {}({});\n", self.top_level_storage(), ret_c, mono_name, params_str); if matches!(recv_type, "Option" | "Result") { self.builtin_sum_method_fwd_decls.push_str(&fwd_decl); } else { self.mono_fwd_decls.push_str(&fwd_decl); } // [M-serde-encode-pointer-op-regression] fix: register the SAME // Plan-152.4.3 type-qualified return key (`fn_ret_<recv>_<method>`) // the plain (non-generic-own-param) forward-decl path already writes // (~13908 above) — but keyed by THIS instantiation's CONCRETE C // receiver type (`recv_c`, stripped of any `Nova_`/`NovaValue_`/ // `NovaTuple_` prefix and trailing `*`, mirroring `infer_call_ret_c`'s // own read-side normalization of a call site's `obj_ty`), not // present anywhere for a receiver-own-generic method ("bare-T // blanket", `fn[T] T @method()`, e.g. the primitive `@to_str()` // fallback in `std/src/runtime/string/core.nv`): that declaration is // routed entirely through `mono_method_decls`/`method_overloads` // sentinels (~13764 above, gated on `!f.generics.is_empty()`, which // fires FIRST and returns early) and never reaches the ordinary // forward-decl registration at all — so `infer_call_ret_c`'s // type-qualified lookup (`B11ae_type_qualified_fn_ret`) ALWAYS // missed for a primitive receiver relying on this blanket, silently // falling through to the receiver-BLIND name-only fallback // (`B11af_fn_ret_method_nameonly`, last-registered-wins across every // unrelated type sharing the method name). Concretely: `int.to_str()` // / `char.to_str()` (both routed through this exact blanket) picked // up whichever OTHER same-named method registered `fn_ret_to_str` // last in the compile unit — e.g. a co-present `[]u8 @to_str() -> // Result[str, Utf8Error]` — mistyping the primitive's `str` result as // a `Result`-shaped (i.e. raw-pointer-shaped) value; a later `+` // string-concatenation on that mistyped operand then tripped Plan // 70's strict-propagation `E_POINTER_OP_USE_METHOD` guard (the // guard caught the fallout, not the cause). This is the DEFINITION // side of the SAME registry `register_mono_method_instance` already // computes `ret_c` for (per-concrete-instantiation, correctly // substituted) — publishing it here closes the gap directly, with no // change to the read side. { let bare_recv_c = recv_c.trim_end_matches('*'); let key_tn = Self::debt_strip_value_nova_tuple_prefix(bare_recv_c); if !key_tn.is_empty() { self.var_types.insert(format!("fn_ret_{}_{}", key_tn, fn_decl.name), ret_c.clone()); } } // Enqueue for body emission — prefix __method__TYPE::name so worklist drain can route let worklist_key = format!("__method__{}::{}", recv_type, fn_decl.name); self.mono_worklist.push((worklist_key.clone(), Self::subst_vec_from_c_pairs(&type_subst), mono_name.to_string())); // [M-138.2-generic-method-overload-mono] Record the EXACT chosen overload // for this final mono name so the drain emits its body (not a bare-name // first-wins re-lookup). Keyed on the suffixed `mono_name`, so getter and // setter (distinct mono names) each map to their own FnDecl. self.mono_method_fndecl_for_name.insert(mono_name.to_string(), fn_decl.clone()); // Plan 172.14 (sret/_out §2): sret-eligible mono-метод — регистрируем // classic-имя в реестре, эмитим второй fwd-decl `__sret` (доп. параметр // `S* _out`) и ставим второе тело в тот же worklist (emit_monomorphized_ // method детектит суффикс имени). НЕ для Option/Result — это не отбор // по типу возврата (тот целиком в `sret_fn_eligible`), а ограничение // КАНАЛА эмиссии: методы этих двух встроенных сумм форвард-декларируются // отдельным путём (`builtin_sum_receiver_c_type`, :19601), в который // второй `__sret`-fwd-decl не встаёт. Plan 172.15 Ф.1: остаток названного // типа, но по РЕСИВЕРУ и по причине из другой машинерии; прежнее // обоснование «ret_c гейт Nova_Vec____ их и так исключает» с уходом // гейта по имени типа устарело — оговорка держится сама. if !mono_name.ends_with("__sret") && !matches!(recv_type, "Option" | "Result") && self.sret_fn_eligible(fn_decl, &ret_c) { self.sret_fns.insert( mono_name.to_string(), ret_c.trim_end_matches('*').trim().to_string()); let sret_name = format!("{}__sret", mono_name); let params_sret = if params_str == "void" { format!("{} _out", ret_c) } else { format!("{}, {} _out", params_str, ret_c) }; self.mono_fwd_decls.push_str( &format!("{}{} {}({});\n", self.top_level_storage(), ret_c, sret_name, params_sret)); self.mono_worklist.push(( worklist_key, Self::subst_vec_from_c_pairs(&type_subst), sret_name.clone())); self.mono_method_fndecl_for_name.insert(sret_name, fn_decl.clone()); } } /// Plan 48: emit a monomorphized METHOD body (instance method variant of emit_monomorphized_fn). fn emit_monomorphized_method( &mut self, fn_decl: &crate::ast::FnDecl, type_subst: Vec<(String, crate::types::ResolvedType)>, mono_name: &str, recv_type: &str, ) -> Result<(), String> { // [race-198 class-closure]: см. override_maps_scope_enter doc. Mono- // эмиссия дренится ЛЕНИВО из середины чужих тел — scoping обязателен. let ovr_saved = self.override_maps_scope_enter(); let r = self.emit_monomorphized_method_scoped_inner( fn_decl, type_subst, mono_name, recv_type); self.override_maps_scope_exit(ovr_saved, r.is_ok()); r } fn emit_monomorphized_method_scoped_inner( &mut self, fn_decl: &crate::ast::FnDecl, type_subst: Vec<(String, crate::types::ResolvedType)>, mono_name: &str, recv_type: &str, ) -> Result<(), String> { use crate::ast::FnBody; // [M-sync-crossmodule…] (D381): a monomorphized method body references // colliding types (`ErrorKind.WriteZero` in `BufWriter[W].flush`) — resolve // them under the METHOD's declaring file so `ref_type_base` qualifies the // ctor/tag to match the qualified definition. GATED (byte-identical for // non-colliding CUs). Restored with the type-subst at the end. let saved_emit_file_id_mono = self.current_emit_file_id; if self.any_type_file_collision() { self.current_emit_file_id = Some(fn_decl.span.file_id); } // Set type substitution (A1‴: worklist carries RT — seed current_type_subst directly) let saved_subst = std::mem::replace( &mut self.current_type_subst, type_subst.iter().cloned().collect(), ); // Plan 11 Follow-up (2026-05-17): current_receiver_type set BEFORE // computing param_c_tys + ret_c, чтобы `Self` в param/return position // (e.g. `other Self`, `-> Self`) резолвилось в concrete mono'd type. // Раньше set только перед ret_c — params получали Nova_Self fallback. // №170: self-call диспетч читает current_receiver_type как ключ — // сырой typevar попадал в плейсхолдер (mono_method_registry.rs §№170). let resolved_recv_name = self.resolve_mono_recv_nova_name(recv_type); let prev_recv_for_emit = self.current_receiver_type.replace(resolved_recv_name.clone()); self.sync_receiver_rt(); // [M-138.2-self-in-param]: bind `Self` so a nested type-arg `Self` // (e.g. `-> FiltIt[Self, U]`, or `other Self` in a param) resolves to // the SAME value-aware receiver mono the call-site uses (the call-site // return-inference at emit_c.rs:34750-34754 binds `Self` to the mono // pointer then value-aware-normalizes it). Without this, the nested // `Self` misses `current_type_subst["Self"]` (type_ref_to_c:5265) and // falls to the `"Self"` arm → `receiver_c_type` POINTER form, which // adds a spurious trailing `*` → `_p` in the mangled type-arg → the // method's emitted return/param mono diverges from the call-site temp // → C "initializing ... from incompatible type". Only for generic // mono instances (recv_type carries `____`); value_aware_generic_c_type // leaves heap-generic / non-value forms unchanged (heap `-> Self` and // primitive receivers are unaffected). `.or_insert` so any pre-existing // `Self` binding (e.g. from type_subst) wins — no clobber. self.debt_bind_self_for_mono_recv(recv_type); // Plan 128 Ф.1: thread recv.mutable from fn_decl AST (Ф.2 consumes). let recv_mutable = fn_decl.receiver.as_ref().map(|r| r.mutable).unwrap_or(false); let recv_c = self.receiver_c_type(recv_type, recv_mutable); // [M-nested-generic-receiver-method-mono] (реестр 221.1 №247): see // `debt_rebind_nested_receiver_typevars` doc — rebinds a builtin // Option/Result carrier's colliding "T"/"E" to the method's OWN // (deep) view for the param/return-type + body computed below. self.debt_rebind_nested_receiver_typevars(fn_decl, &recv_c); // Plan 70 PhaseA3: strict — emit_monomorphized_method param/return. // [M-generic-static-method-value-arg-addr-mismatch] fix (221.1 Ф.2 // #26): this signature computation used to be a bare `type_ref_to_c` // per param — unlike `params_c` (the non-generic method/free-fn // signature path, ~L18819-18833), it NEVER consulted // `method_byref_flag` (Plan 172.14: a large — >16B — `ro` value- // struct param is auto-by-ref, `T*` in C). `build_method_byref_map`'s // pre-pass registers the flag under the SOURCE-level (erased) type // name (e.g. "Path"/"Wrap") from `fn_decl.receiver.type_name` — the // SAME name this function already has via `fn_decl` — so the two // paths were never actually looking at different keys; ONE of them // (this one) simply never looked at all. Call-site codegen // (`synthesize_method_byref_args`) DOES consult the same map at every // static/instance method call and wraps a flagged arg in `&(...)` — // so a generic static method's mono'd C signature declared the param // BY VALUE while every call site passed a POINTER: CC-FAIL // "passing 'T *' to parameter of incompatible type 'T'". Live repro: // nova-http's `Path[T Deserialize].from_request(req ServerRequest)` // (`ServerRequest` is an 8-field value record, past the 16B // threshold) called as `Path[Concrete].from_request(req)` — reported // by a downstream package worktree, blocking Plan 222.3 Ф.3 // extractors end-to-end. `mut`/in-out params intentionally NOT // touched here (`param_is_inout_ptr`) — out of this marker's narrow // scope, no live repro exercises it, and it would be a separate, // unverified behavior change for generic-method mono instances. let param_c_tys: Vec<String> = fn_decl.params.iter().enumerate() .map(|(p_idx, p)| { let mut ty_c = self.type_ref_to_c(&p.ty).map_err(|e| self.err_no_int_fallback( &format!("mono'd method `{}.{}` param `{}`", recv_type, fn_decl.name, p.name), &e, ))?; if let Some(recv) = &fn_decl.receiver { if self.method_byref_flag(&recv.type_name, &fn_decl.name, p_idx) { ty_c.push('*'); } } Ok(ty_c) }) .collect::<Result<Vec<_>, String>>()?; let ret_c = match fn_decl.return_type.as_ref() { Some(t) => self.type_ref_to_c(t).map_err(|e| self.err_no_int_fallback( &format!("mono'd method `{}.{}` return", recv_type, fn_decl.name), &e, ))?, None => "nova_unit".to_string(), }; // НЕ восстанавливаем здесь — оставляем set для body emit (var_types для // params, match infers и пр.). Restore в самом конце вместе с saved_subst. let _ = prev_recv_for_emit; // Plan 48 Ф.7.2: static-методы без nova_self. let is_instance = matches!(fn_decl.receiver.as_ref().map(|r| &r.kind), Some(crate::ast::ReceiverKind::Instance)); let mut parts: Vec<String> = if is_instance { vec![format!("{} nova_self", recv_c)] } else { Vec::new() }; for (p, ty) in fn_decl.params.iter().zip(¶m_c_tys) { parts.push(format!("{} {}", ty, p.name)); } // Plan 172.14 (sret/_out §2): mono `__sret`-вариант (детект по суффиксу // имени, поставлен register_mono_method_instance) — доп. параметр // `S* _out` + активный sret_fn_out на тело (восстановление в конце). let is_sret_variant = mono_name.ends_with("__sret"); if is_sret_variant { parts.push(format!("{} _out", ret_c)); } let saved_sret_fn_out = if is_sret_variant { std::mem::replace(&mut self.sret_fn_out, Some("_out".to_string())) } else { self.sret_fn_out.take() }; let params_str = if parts.is_empty() { "void".to_string() } else { parts.join(", ") }; // Buffer body let saved_out = std::mem::take(&mut self.out); let saved_indent = self.indent; // Plan 91-stdmvp followup 2026-05-30 (root-cause fix for // [M-mono-method-var-boxed-leak]): mono'd method bodies emitted via // worklist drain inherit caller's `var_boxed` map (heap-promotion // for closure captures). Without isolation, identifier `count` в // mono'd body может резолвиться через caller'скую (*_box_count) → // CC-FAIL «undeclared _box_count». Reset for clean emission; // restore at function end. Same pattern as `emit_lambda_body` // (line ~23846). let saved_var_boxed = std::mem::take(&mut self.var_boxed); self.indent = 0; self.line(&format!("{}{} {}({}) {{", self.top_level_storage(), ret_c, mono_name, params_str)); self.indent += 1; // Plan 143.2: prologue safepoint. The monomorphized instance inherits // the KEEP-status of its SOURCE template (`fn_decl`), so a recursive // generic method keeps a safepoint in every instantiation. self.emit_prologue_preempt_check(fn_decl); // Register nova_self and params in var_types (только если instance method) let prev_self = if is_instance { self.var_types.insert("nova_self".to_string(), recv_c.clone()) } else { None }; // [M-91.8c-direct-index-method] Register the concrete element C type for // array_element_types["nova_self"] so that @[j].method() in generic []T // method bodies resolves the element type via compute_array_elem_type_for_obj // (SelfAccess arm) instead of falling back to the NovaArray_ strip path // which loses the trailing '*' for struct pointer elements. // Covers two receiver families: // Nova_Vec____<elem>* — look up generic_type_instance_info for the // mono'd Vec type; fall back to current_type_subst for the first typevar // when the info entry is not yet present. // NovaArray_<elem>* — strip prefix and restore '*' for struct pointers. let prev_self_elem: Option<Option<String>> = if is_instance { let elem_ty_opt: Option<String> = if recv_c.starts_with("Nova_Vec____") { let mangled = recv_c.trim_end_matches('*').trim().to_string(); self.generic_type_instance_info.borrow() // A1‴: registry arg is `ResolvedType` — lower to C-name (`Raw` verbatim). .get(&mangled).and_then(|(_, a)| a.first().map(|rt| self.arg_c(rt))) .or_else(|| { // Fallback: derive from current_type_subst for the generic param. fn_decl.generics.iter() .find(|g| !g.name.is_empty()) .and_then(|g| self.subst_c(&g.name)) }) } else if let Some(after_prefix) = Self::debt_strip_novaarray_prefix_opt(&recv_c) { // NovaArray_<elem>* — element is after the prefix, re-add '*' for structs. let elem = after_prefix.trim_end_matches('*').trim(); if elem.starts_with("Nova_") { Some(format!("{}*", elem)) } else { Some(elem.to_string()) } } else { None }; elem_ty_opt.map(|e| self.array_element_types.insert("nova_self".to_string(), e)) } else { None }; let saved_var_types: Vec<(String, Option<String>)> = fn_decl.params.iter() .zip(¶m_c_tys) .map(|(p, ty)| (p.name.clone(), self.var_types.insert(p.name.clone(), ty.clone()))) .collect(); // Register function-typed params in fn_param_sigs with concrete types // Plan 70 PhaseA3: strict — mono'd fn-typed param signature. let mut saved_fn_sigs: Vec<(String, Option<(Vec<String>, String)>)> = Vec::new(); // [fix M-nested-fn-newtype-bind-then-call-broken, реестр 221.1 №78, // форма 2]: mirrors the top-level-fn primary site's sibling addition — // see `nested_fn_return_sig` doc. Scoped (save/restore) here, matching // this site's own `fn_param_sigs` scoping discipline. let mut saved_fn_returns_sigs: Vec<(String, Option<(Vec<String>, String)>)> = Vec::new(); for (p, _c_ty) in fn_decl.params.iter().zip(¶m_c_tys) { if let Some(_ft) = self.resolve_fn_typeref(&p.ty) { let crate::ast::TypeRef::Func { params: fp, return_type, .. } = &_ft else { unreachable!() }; let inner_ptys: Vec<String> = fp.iter() .map(|t| self.type_ref_to_c(t).map_err(|e| self.err_no_int_fallback( &format!("mono'd fn-typed param `{}` element", p.name), &e, ))) .collect::<Result<Vec<_>, _>>()?; let inner_ret = match return_type.as_ref() { Some(t) => self.type_ref_to_c(t).map_err(|e| self.err_no_int_fallback( &format!("mono'd fn-typed param `{}` return", p.name), &e, ))?, None => "nova_unit".to_string(), }; let prev = self.fn_param_sigs.insert(p.name.clone(), (inner_ptys, inner_ret)); saved_fn_sigs.push((p.name.clone(), prev)); if let Some(sig) = self.nested_fn_return_sig(&_ft) { let prev2 = self.fn_returns_fn_sig.insert(p.name.clone(), sig); saved_fn_returns_sigs.push((p.name.clone(), prev2)); } } } // Pre-populate tuple_element_types for array-of-tuple parameters in mono context. // Enables `for (k, v) in pairs` to extract typed fields without losing the // concrete K/V → nova_str/nova_int substitution stored in current_type_subst. let mut saved_tuple_elem_keys: Vec<String> = Vec::new(); // Plan 63 Fix E: ALSO save/restore array_element_types для fn params, чтобы // не подцепить leak'нувшую запись из caller'а (e.g. test fn body создал // `let pairs = [...]` с array_element_types["pairs"], и static method // тоже принимает param "pairs" — без save/restore for-loop читает // caller'скую запись с неправильным storage type). let mut saved_array_elem_for_params: Vec<(String, Option<String>)> = Vec::new(); for p in &fn_decl.params { if let crate::ast::TypeRef::Array(inner, _) = &p.ty { if let crate::ast::TypeRef::Tuple(elems, _) = inner.as_ref() { if !elems.is_empty() { // Plan 63 Fix E: для mono'd tuple field types кладём raw // C type (без `*`). Mono'd tuple struct хранит values // напрямую (no pointer-stomping), так что destructure'у // нужны raw types для direct `.fN` access. Boxing-wrap // оставлен только для legacy _NovaTupleN path'ов где // mono не выходит концлуч концом (e.g. iter-method case). let tuple_c_for_mono_check = self.type_ref_to_c(inner) .unwrap_or_else(|_| String::new()); let is_mono_tuple = tuple_c_for_mono_check.starts_with("_NovaTuple_"); let field_tys: Vec<String> = elems.iter() .map(|e| { let c_ty = self.type_ref_to_c(e) .unwrap_or_else(|_| "nova_int".to_string()); if is_mono_tuple { c_ty } else { // Legacy _NovaTupleN path: fields boxed. let needs_heap = c_ty.starts_with("_NovaTuple") || c_ty.starts_with("NovaOpt_") || c_ty == "nova_str" || c_ty == "nova_unit"; if needs_heap && !c_ty.ends_with('*') { format!("{}*", c_ty) } else { c_ty } } }) .collect(); self.tuple_element_types.insert(p.name.clone(), field_tys); saved_tuple_elem_keys.push(p.name.clone()); // Plan 63 Fix E: register array_element_types для param, чтобы // for-loop emit видел typed pointer storage `_NovaTuple_<mono>*`. // Без этого elem_ty fallback → "nova_int" (через arr_ty) // ИЛИ подхватывает leak'нувшую запись из caller'а. let tuple_c = self.type_ref_to_c(inner) .unwrap_or_else(|_| self.register_legacy_tuple(elems.len())); let stored_ty = if !tuple_c.ends_with('*') { format!("{}*", tuple_c) } else { tuple_c }; let prev = self.array_element_types.insert(p.name.clone(), stored_ty); saved_array_elem_for_params.push((p.name.clone(), prev)); } } } } // D109: Pre-populate array_element_types for pointer-stomped array fields of the // receiver type, so @buckets[idx] casts to the concrete element type. // (current_type_subst is already set, so type_ref_to_c resolves K/V correctly.) let mut saved_mono_array_elem_keys: Vec<String> = Vec::new(); if is_instance { let base_opt = self.generic_type_instance_info.borrow() .get(&format!("Nova_{}", recv_type)).map(|(b, _)| b.clone()); if let Some(base_name) = base_opt { if let Some(template) = self.generic_type_templates.get(&base_name).cloned() { use crate::ast::TypeDeclKind; if let TypeDeclKind::Record(fields) = &template.kind { for fld in fields { if let crate::ast::TypeRef::Array(elem_ty, _) = &fld.ty { if let Ok(elem_c) = self.type_ref_to_c(elem_ty) { if elem_c.ends_with('*') && elem_c != "nova_int*" { let field_c = Self::mangle_field_name(&fld.name); let key = format!("(nova_self->{})", field_c); self.array_element_types.insert(key.clone(), elem_c); saved_mono_array_elem_keys.push(key); } } } } } } } } // Drain any generic type instances enqueued during array_element_types setup above. // Without this, sum_schemas / record_variant_field_types for types like // Slot____nova_int__nova_unit are not yet populated when pattern_bind_typed // runs during body emission — causing field-type lookups to fall back to the // erased base type (e.g. "Slot") whose fields are typed void* (the erased form). if !self.generic_type_worklist.borrow().is_empty() { self.drain_generic_type_worklist()?; } // Set receiver type so @field and Self resolve correctly. // №170: то же резолвленное имя — ЭТО присваивание действует при эмиссии тела. let saved_recv = std::mem::replace(&mut self.current_receiver_type, Some(resolved_recv_name)); self.sync_receiver_rt(); let saved_ret_ty = std::mem::replace(&mut self.current_fn_return_ty, Some(ret_c.clone())); // [M-generic-method-self-recursive-return] (Plan 186): remember this // mono'd method's own name so a self-recursive call (`t.map(f)` inside // `@map`'s own body) can be recognised in `infer_expr_c_type`'s fallback. let saved_fn_name = std::mem::replace(&mut self.current_fn_name, Some(fn_decl.name.clone())); // [M-generic-method-self-recursive-return] (Plan 186): mirror of the // free-fn `emit_generic_fn_erased` path ([M-property-testing-rot]) — // expose this mono'd METHOD body's own declared param TypeRefs so a // nested generic call forwarding one of its params (e.g. `t.map(f)` // passing the method's OWN `f: fn(T)->U` straight through to the // recursive call) can unify the callee's method-level type-args at the // TypeRef level (`resolve_method_level_subst`'s Source 2f below) — // `f`'s C type alone is an erased `void*`/closure-struct pointer, // useless for inferring `U`. Was ONLY set for free generic fns before // this fix, never for generic METHOD mono — the exact gap that made a // self-recursive generic method (`LinkedList[T]@map[U]` calling // `t.map(f)` on itself) unable to infer its own `U`. let saved_param_typerefs = std::mem::replace( &mut self.current_fn_param_typerefs, fn_decl.params.iter() .map(|p| (p.name.clone(), p.ty.clone())) .collect(), ); let saved_expected = std::mem::replace( &mut self.expected_record_type, fn_decl.return_type.as_ref().and_then(|t| { if let crate::ast::TypeRef::Named { path, generics, .. } = t { if generics.is_empty() { Some(path.join("_")) } else { None } } else { None } }), ); // Plan 140 cgfix ([M-138.2-flip-erased-base-body-mono]): emit the // `requires`/`ensures` contract pre/postamble in the MONO'd generic- // method body too. Previously the mono path skipped contract codegen // entirely (only `emit_fn`'s non-generic path emitted it), so a // contract violation on a generic-type method (e.g. `Vec[T] @index` // OOB called via dispatch, `Bag[int] @at` OOB) went silently UNCAUGHT // — a soundness hole. This ports the exact logic from `emit_fn` // (lines ~14555+): Z3-proven contracts elide via `proven_contracts`, // quantifiers skip. Plan 194 A4 (ретракт `#unchecked`): per-fn/module // opt-out убран — requires/ensures gate только по `contracts_elided_for` // (константно `false` до будущей mode-based элизии, A3+). // №172: `#unverified` гасит только SMT-верификацию, НЕ runtime-emit. let mono_verifiable = !fn_decl.contracts.is_empty(); let mono_has_contracts = mono_verifiable && !self.contracts_elided_for(ContractKind::Requires); let mono_has_ensures = mono_verifiable && !self.contracts_elided_for(ContractKind::Ensures) && fn_decl.contracts.iter().any(|c| matches!(c.kind, ContractKind::Ensures)); // emit requires checks (enforce-with-elision; unproven stay in release). if mono_has_contracts { for c in &fn_decl.contracts { if matches!(c.kind, ContractKind::Requires) { if c.debug_only && self.mode_erases_debug() { continue; } if self.proven_contracts.contains(&(fn_decl.name.clone(), c.span.start)) { continue; } if matches!(c.expr.kind, ExprKind::Forall { .. } | ExprKind::Exists { .. }) { continue; } let expr_c = self.emit_expr(&c.expr)?; let expr_src = Self::expr_to_display(&c.expr); let (file_lit, line) = self.loc_for_span(c.span.start); self.emit_contract_check( &expr_c, "NOVA_CONTRACT_PRE", &fn_decl.name, &expr_src, &file_lit, line, &c.message, &c.message_expr, )?; } } } // Ф.7: Erased Array runtime always returns NovaOpt_nova_int; emit a bridge wrapper // for methods whose concrete return type is NovaOpt_T (T != nova_int). // Bridge only works for pointer types: scalars (nova_str, nova_bool, nova_f64, nova_byte) // are stored by value in typed arrays — the erased version reads them as nova_int // (wrong element size), so those must get a proper monomorphized body instead. let bridge_emitted = if let Some(inner_t) = ret_c.strip_prefix("NovaOpt_") { if inner_t != "nova_int" && is_instance && inner_t.ends_with('*') { let info = self.generic_type_instance_info.borrow(); let base_opt = info.get(&format!("Nova_{}", recv_type)).map(|(b, _)| b.clone()); drop(info); if let Some(base_name) = base_opt { let erased_method = format!("Nova_{}_method_{}", base_name, fn_decl.name); let base_recv_ty = format!("Nova_{}*", base_name); let value_convert = format!("({})(intptr_t)(_erased.value)", inner_t); self.line(&format!( "NovaOpt_nova_int _erased = {}(({})nova_self);", erased_method, base_recv_ty )); // Plan 118 Ф.5: _erased is NovaOpt_nova_int (erased form, // never NPO) — tag-check stays. Return constructors use // ret_c which может быть NPO-eligible (NovaOpt_Nova_X_p). self.line("if (_erased.tag == NOVA_TAG_Option_None) {"); self.indent += 1; let ret_sani = ret_c.strip_prefix("NovaOpt_").unwrap_or(&ret_c); self.line(&format!("return {};", self.option_none_expr(ret_sani))); self.indent -= 1; self.line("}"); self.line(&format!("return {};", self.option_some_expr(ret_sani, &value_convert))); true } else { false } } else { false } } else { false }; // Emit body let body_clone = fn_decl.body.clone(); if !bridge_emitted { match &body_clone { FnBody::Expr(e) => { self.emit_source_annotation_for_expr(e); let val = self.emit_expr(e)?; if ret_c == "nova_unit" { self.line(&format!("{};", val)); // Plan 140 cgfix: ensures on a unit-return expr body. if mono_has_ensures { self.emit_ensures_checks(fn_decl)?; } self.line("return NOVA_UNIT;"); } else if mono_has_ensures { // Plan 140 cgfix: collect into `_nova_result`, register // `result` for the ensures expression, then check + return. self.line(&format!("{} _nova_result = {};", ret_c, val)); self.var_types.insert("result".into(), ret_c.clone()); self.emit_ensures_checks(fn_decl)?; self.var_types.remove("result"); self.line("return _nova_result;"); } else { self.line(&format!("return {};", val)); } } FnBody::Block(block) if mono_has_ensures => { // Plan 140 cgfix: block-body + ensures — route ALL returns // (incl. early-return) through a post-label that collects the // value into `_nova_result`, then run ensures-checks + final // return. Mirrors `emit_fn`'s production-grade path; the // `Stmt::Return` handler already honours `contracts_post_label`. let post_label = format!("_nova_contract_post_{}", mono_name); if ret_c != "nova_unit" { self.line(&format!("{} _nova_result;", ret_c)); } let block_id = self.enter_defer_scope(block, false); let saved_label = self.contracts_post_label.take(); self.contracts_post_label = Some(post_label.clone()); self.var_types.insert("result".into(), ret_c.clone()); for stmt in &block.stmts { self.emit_stmt(stmt)?; } if let Some(trailing) = &block.trailing { self.emit_source_annotation_for_expr(trailing); let trailing_ty = self.infer_expr_c_type(trailing); let val = self.emit_expr(trailing)?; if ret_c == "nova_unit" { self.line(&format!("{};", val)); } else if trailing_ty == "nova_unit" && ret_c != "nova_unit" { // Divergent trailing (e.g. loop {} with internal returns): // no fallthrough value — the post-label is reached only // via the routed returns. Emit side-effect only. self.line(&format!("{};", val)); } else if Self::needs_tuple_field_copy(&ret_c, &trailing_ty) { let tmp = self.fresh_tmp(); self.emit_tuple_return_stash(&ret_c, &tmp, &val, &trailing_ty); self.line(&format!("_nova_result = {};", tmp)); } else { self.line(&format!("_nova_result = {};", val)); } } self.leave_defer_scope(block_id); // Post-label (C label syntax: 0-indent). let saved_indent_lbl = self.indent; self.indent = 0; self.line(&format!("{}:;", post_label)); self.indent = saved_indent_lbl; self.emit_ensures_checks(fn_decl)?; self.var_types.remove("result"); self.contracts_post_label = saved_label; if ret_c == "nova_unit" { self.line("return NOVA_UNIT;"); } else { self.line("return _nova_result;"); } } FnBody::Block(block) => { let block_id = self.enter_defer_scope(block, false); for stmt in &block.stmts { self.emit_stmt(stmt)?; } if let Some(trailing) = &block.trailing { self.emit_source_annotation_for_expr(trailing); let trailing_ty = self.infer_expr_c_type(trailing); // Plan 172.1 [M-172.1-some-target-coerce]: a `NovaOpt_<X>` / typed-integer // return coerces the trailing TO the return type so a context-typed payload // literal lowers to X — `Some(<int-literal>) -> Option[uint]` builds // `NovaOpt_nova_uint`, not the literal-default `NovaOpt_nova_int` (named- // priority int-collapse → CC-FAIL, surfaced by d86_coalesce_width). Gated on // the NovaOpt_/typed-int ret surface so the common path keeps `emit_expr`. // [M-d55-str-literal-coercion-name-gated] fix: a `[]u8` // return type ALSO needs target-typed routing (D55 amend // str-literal→[]u8 — the arm inside `emit_expr_with_ // target_type` rewrites a bare `StrLit` trailing return // into `.bytes()`; the plain `emit_expr` fallback below // does not know about this coercion at all). // [M-callnorm-free-fn-name-collision] followup (closure-megacu // regression, 2026-07-19): `_NovaTuple_` target added — a tuple // RETURN type whose trailing literal embeds a `_NovaFixArr_` // element (e.g. `(int, [N]T)`) needs the SAME target-typed // routing as a bare fixarr return, or the nested array-literal // inside the tuple literal gets emitted with NO element-type // hint and panics `[P67] nova_int collapse` in the legacy // array-literal path (`current_array_elem_hint` never set). // `emit_expr_with_target_type`'s `TupleLit` arm already handles // this correctly (decodes `_NovaTuple_` elem types, recurses // per element) — it just wasn't reached from here. See // docs/plans/wip/closure-megacu-fix-notes.md. let val = if ret_c.starts_with("NovaOpt_") || ret_c.starts_with("_NovaFixArr_") || ret_c.starts_with("_NovaTuple_") || Self::is_typed_integer(&ret_c) || Self::is_bytes_slice_c_ty(&ret_c) { self.emit_expr_with_target_type(trailing, &ret_c)? } else { self.emit_expr(trailing)? }; self.leave_defer_scope(block_id); if ret_c == "nova_unit" { self.line(&format!("{};", val)); self.line("return NOVA_UNIT;"); } else if trailing_ty == "nova_unit" && ret_c != "nova_unit" { // Trailing is unit (e.g. infinite loop with internal returns) // but fn returns non-unit. Emit side-effect + dummy unreachable return. self.line(&format!("{};", val)); self.line(&format!("return ({})0; /* unreachable */", ret_c)); } else if Self::needs_tuple_field_copy(&ret_c, &trailing_ty) { // Plan 59: mono'd tuple return type mismatch — field-wise. let tmp = self.fresh_tmp(); self.emit_tuple_return_stash(&ret_c, &tmp, &val, &trailing_ty); self.line(&format!("return {};", tmp)); } else { self.line(&format!("return {};", val)); } } else { // [M-196-mono-block-notrailing-ret-ignored] (dormant-fixes): // `block.trailing == None` means the block's last item was // an explicit `Stmt::Return` (or another terminator), NOT an // implicit-return expression — `emit_stmt` above already // emitted the real `return <value>;`. Unconditionally // appending `return NOVA_UNIT;` here (regardless of `ret_c`) // used to follow that real return with a SECOND, wrongly-typed // one: `nova_unit` doesn't convert to a non-unit `ret_c` // (pointer/scalar/struct), so clang/MSVC rejected the // otherwise-correct generic-static method body outright // (`returning 'nova_unit' from a function with incompatible // result type ...`). Mirrors the canonical non-mono path // (`emit_block_stmts`, ~24407-24416): only synthesize the // fallthrough `return NOVA_UNIT;` when the function's OWN // return type is unit; otherwise the explicit return already // covers it (a truly missing return path stays a *legitimate* // C "control reaches end of non-void function" diagnostic, // not a silently-fabricated wrong-type one). self.leave_defer_scope(block_id); if ret_c == "nova_unit" { self.line("return NOVA_UNIT;"); } } } FnBody::External => {} } } // end: match body, if !bridge_emitted // Restore for (name, prev) in saved_var_types { match prev { Some(old) => { self.var_types.insert(name, old); } None => { self.var_types.remove(&name); } } } if is_instance { match prev_self { Some(old) => { self.var_types.insert("nova_self".to_string(), old); } None => { self.var_types.remove("nova_self"); } } } else { let _ = prev_self; } // [M-91.8c-direct-index-method] Restore array_element_types["nova_self"] // (paired with the registration above after var_types["nova_self"] set). if let Some(prev) = prev_self_elem { match prev { Some(old) => { self.array_element_types.insert("nova_self".to_string(), old); } None => { self.array_element_types.remove("nova_self"); } } } for (name, prev) in saved_fn_sigs { match prev { Some(old) => { self.fn_param_sigs.insert(name, old); } None => { self.fn_param_sigs.remove(&name); } } } // [fix M-nested-fn-newtype-bind-then-call-broken, №78] restore paired // with the `nested_fn_return_sig` save loop above. for (name, prev) in saved_fn_returns_sigs { match prev { Some(old) => { self.fn_returns_fn_sig.insert(name, old); } None => { self.fn_returns_fn_sig.remove(&name); } } } self.current_receiver_type = saved_recv; self.sync_receiver_rt(); self.current_fn_return_ty = saved_ret_ty; self.current_fn_name = saved_fn_name; self.current_fn_param_typerefs = saved_param_typerefs; self.expected_record_type = saved_expected; for key in &saved_mono_array_elem_keys { self.array_element_types.remove(key); } for key in &saved_tuple_elem_keys { self.tuple_element_types.remove(key); } // Plan 63 Fix E: restore caller-scope array_element_types для params, чтобы // не утекали наши mono'd substituted entries в outer fn body. for (name, prev) in saved_array_elem_for_params { match prev { Some(old) => { self.array_element_types.insert(name, old); } None => { self.array_element_types.remove(&name); } } } self.flush_boxed_vars(); self.indent -= 1; self.line("}"); self.line(""); // Plan 91-stdmvp followup 2026-05-30: restore caller-scope var_boxed // (paired with `take` at line ~11055). Same pattern as // `emit_lambda_body` (line ~23890). self.var_boxed = saved_var_boxed; let fn_body = std::mem::replace(&mut self.out, saved_out); self.indent = saved_indent; if !self.lambda_forward_decls.is_empty() { self.out.push_str(&std::mem::take(&mut self.lambda_forward_decls)); } if !self.lambda_impls.is_empty() { self.out.push_str(&std::mem::take(&mut self.lambda_impls)); } self.out.push_str(&fn_body); self.current_type_subst = saved_subst; self.current_emit_file_id = saved_emit_file_id_mono; // [M-sync-crossmodule…] D381 // Plan 172.14 (sret/_out §2): restore sret-режима тела. self.sret_fn_out = saved_sret_fn_out; // Plan 11 Follow-up: restore current_receiver_type теперь, когда body // полностью emitted (включая все Self-resolution context'ы). self.current_receiver_type = None; self.sync_receiver_rt(); Ok(()) } /// [реестр 221.1 №139 Round 3 — единая топосортированная секция /// value-типов] Rounds 1/2 tried to fix this class of bug by HOISTING /// specific categories (generic-instances, then their tuple/NovaOpt/ /// NovaRes dependencies) ahead of a FIXED marker position. Round 3's /// integrator-caught regression (`main.c: field has incomplete type /// 'NovaValue_HeaderName'`) proved that approach fundamentally cannot /// generalize: the real dependency graph between value-record types, /// generic-instance types, tuples, etc. goes in BOTH directions in the /// actual corpus (a value-record can embed a generic-instance by value /// — round 1/0's `Bundle`/`Wrap[int]` — AND a generic-instance can embed /// a user value-record by value — round 3's nova-http `Header`/ /// `HeaderName`) — no FIXED two-marker (or N-marker) order can satisfy /// both directions for every pair simultaneously. Only a real per-CU /// topological sort over the union of nodes is correct in general — /// exactly the diagnosis+fix the coordinator specified. /// /// Nodes: every entry `emit_value_record_type` (user value-records), /// `emit_record_type` (heap records), and `drain_generic_type_worklist` /// (Record-kind generic instances only — see `last_generic_record_instance` /// doc for why other `TypeDeclKind`s are out of scope for now) pushed /// into `pending_value_nodes`, carrying (typedef tag name, field C-types, /// ALREADY-RENDERED text). Rendering itself is UNCHANGED — this fn never /// re-implements struct-body emission (far too much accumulated special- /// case logic — self-referential guards, cross-file collision handling, /// consume-cleanup counters, protocol/vtable wiring — to safely /// duplicate); it only decides in what ORDER the pre-rendered blocks may /// appear. /// /// Edges: node A has an edge to node B when one of A's field C-types, /// with any trailing `*` stripped, equals B's exact tag name — i.e. A /// embeds B BY VALUE (a pointer field only ever needs B forward-declared, /// which `fwd_decls`/the pointer pre-pass in each renderer already /// handles independently of this sort, so pointer fields are NOT edges /// here — unlike `render_value_composite_typedefs`'s tuple/fixarr sort, /// which is over-conservative about this and treats pointer refs as /// edges too; that's fine for tuples in practice but WOULD produce /// false cycles for the much more pointer-heavy value-record/generic- /// instance world, so this sort is deliberately precise about it). /// Standard Kahn's-algorithm topological sort (mirrors /// `render_value_composite_typedefs`'s shape) — dependencies first. /// Whatever a node ALSO needs from an EARLIER-marker category (tuple/ /// fixarr/non-late NovaOpt/NovaRes — all spliced before this unified /// section already, unaffected by this change) is simply assumed /// satisfied — it is, by construction of the surrounding marker order. /// /// A genuine cycle (A embeds B by value AND B embeds A by value) is a /// C-impossible, infinite-size type — if Kahn's algorithm ever makes no /// progress with nodes still remaining, that is a compiler defect ONE /// LEVEL UP (the checker should have rejected the recursive-value-type /// declaration before codegen), not something this pass can paper over; /// it emits the leftover nodes in their original order (so the build /// fails with an ordinary C error instead of hanging) and — deliberately /// — always logs via `eprintln!` when this happens, cycle or not, so it /// is never silently swallowed the way a `continue`-only fallback would. fn render_unified_value_types(&mut self) -> String { let mut nodes = std::mem::take(&mut self.pending_value_nodes); // [реестр 221.1 №139 Round 3] Mono tuple/fixed-array instances are // FOLDED IN as ordinary nodes too (was: a separate, position-fixed // section via the now-removed `render_value_composite_typedefs`, // spliced at its own `__MONO_TUPLE_TYPEDEFS__` marker). Required // because a NORMAL (non-late) `Option[Utf8Error]`-style payload // needs the plain user value-record available BEFORE // `__NOVAOPT_TYPEDEFS__` — i.e. at THIS early position — but a // generic-instance can ALSO need a tuple by value (round 1's // `Vec[(str,str)]`) — so tuples must sit in the SAME graph as // value-records/generic-instances, not off to one side pretending // the dependency only ever goes one way. `.borrow().iter().cloned()` // is a non-destructive read (unlike `pending_value_nodes`, these // registries are never consumed elsewhere), so this is safe to // build unconditionally on every call — there is only one call, at // true finalize, so no double-render risk. { let mut tuple_instances: Vec<Vec<String>> = self.mono_tuple_instances .borrow().iter().cloned().collect(); tuple_instances.sort(); // deterministic tie-breaking for elems in tuple_instances { let mangled = Self::compute_mono_tuple_c_name(&elems); let mut text = String::new(); // Forward-declare pointer-to-heap-generic elements ahead of // THIS tuple's own body (mirrors the removed function's // pre-pass, scoped per-node now — a duplicate forward-decl // across different tuples' text is harmless, C11 §6.7). for elem in &elems { let base = elem.trim_end_matches('*'); if base.len() == elem.len() { continue; } // not a pointer if base.starts_with("Nova_") && base.contains("__") { let fwd = format!("typedef struct {0} {0};\n", base); if !self.user_type_fwd_decls.contains(&fwd) { text.push_str(&fwd); } } } let fields: String = elems.iter().enumerate() .map(|(i, c)| format!("{} f{}; ", c, i)) .collect(); text.push_str(&format!("#ifndef NOVA_TUPLE_TYPEDEF_{}\n", mangled)); text.push_str(&format!("#define NOVA_TUPLE_TYPEDEF_{}\n", mangled)); text.push_str(&format!( "typedef struct {} {{ {}}} {};\n", mangled, fields, mangled)); text.push_str("#endif\n"); nodes.push((mangled, elems, text)); } let mut fixarr_instances: Vec<(usize, String)> = self.mono_fixed_array_instances .borrow().iter().cloned().collect(); fixarr_instances.sort(); for (n, elem) in fixarr_instances { let mangled = Self::compute_mono_fixed_array_c_name(n, &elem); let text = format!( "#ifndef NOVA_FIXARR_TYPEDEF_{m}\n#define NOVA_FIXARR_TYPEDEF_{m}\n\ typedef struct {m} {{ {e} data[{n}]; }} {m};\n#endif\n", m = mangled, e = elem, n = n); nodes.push((mangled, vec![elem], text)); } } if nodes.is_empty() { return String::new(); } let known_names: std::collections::HashSet<String> = nodes.iter().map(|(name, _, _)| name.clone()).collect(); let mut remaining: Vec<(String, Vec<String>, String)> = nodes; let mut emitted: std::collections::HashSet<String> = std::collections::HashSet::new(); let mut sorted: Vec<(String, Vec<String>, String)> = Vec::with_capacity(remaining.len()); loop { if remaining.is_empty() { break; } let mut next_remaining = Vec::new(); let mut progress = false; for node in remaining.drain(..) { let deps_satisfied = node.1.iter().all(|elem| { if elem.ends_with('*') { return true; } // pointer — fwd-decl only, no edge // Mirrors the retired `render_value_composite_typedefs`'s // defensive `_p`-suffix strip for tuple/fixarr element // strings (a mangled-arg convention elsewhere in this // file sometimes substitutes `_p` for a literal `*`) — // carried over for byte-behavior parity on that sub-case; // a real struct tag name coincidentally ending in `_p` // is not a pattern used anywhere in this codebase's // naming scheme. let core = elem.trim_end_matches("_p"); !known_names.contains(core) || emitted.contains(core) }); if deps_satisfied { emitted.insert(node.0.clone()); sorted.push(node); progress = true; } else { next_remaining.push(node); } } if !progress { eprintln!( "[render_unified_value_types] no progress with {} node(s) remaining \ (real by-value cycle — C-impossible, should have been rejected earlier \ — or a mono-name mismatch bug): {:?}. Emitting in original order; \ expect a downstream C compile error, not a hang.", next_remaining.len(), next_remaining.iter().map(|n| n.0.clone()).collect::<Vec<_>>(), ); sorted.extend(next_remaining); break; } remaining = next_remaining; } let mut out = String::new(); for (_, _, text) in sorted { out.push_str(&text); } out } /// Plan 48 Ф.2: emit a monomorphized function body. /// This is like emit_fn but with `current_type_subst` set for concrete type resolution. /// Plan 48 Ф.3: drain the generic type instance worklist. /// Emits concrete struct/sum definitions for each queued instance into /// `generic_type_defs_buf` (spliced before fn definitions via marker). /// May enqueue further instances (nested generics), so loops until empty. fn drain_generic_type_worklist(&mut self) -> Result<(), String> { let mut depth = 0usize; loop { if self.generic_type_worklist.borrow().is_empty() { break; } let batch: Vec<(String, Vec<crate::types::ResolvedType>, String)> = self.generic_type_worklist.borrow_mut().drain(..).collect(); for (base_name, type_args_c, mangled) in batch { if self.emitted_generic_type_instances.contains(&mangled) { continue; } // Plan 55 followup ([M-erased-generic-method-dispatch]): skip // type-instance emit когда type-args содержат placeholder // (Nova_X* для X из template.generics). Это случается когда // generic method (например @clone) рекурсивно instantiates // Self type с unresolved K/V — даёт broken C-emit для // bound-method calls (`key->hash()` на Nova_K incomplete type). if let Some(template) = self.generic_type_templates.get(&base_name) { let generic_names: std::collections::HashSet<String> = template.generics.iter() .map(|g| g.name.clone()).collect(); // Plan 153.2 Ф.2 (STAGE 2): the NESTED-placeholder skip is // gated on the OUTER template being a `value` record. A HEAP // generic placeholder instance (`Vec[Slot[K,V]]` in the erased // HashMap base) is INTENTIONALLY emitted as a pointer-field // carrier — its fields are `Nova_Slot____Nova_K_p__Nova_V_p** // data` (incomplete-type POINTERS, valid C), and its forward- // typedef is what makes the erased base methods that reference // `Nova_Slot____Nova_K_p__Nova_V_p*` compile. Suppressing it // (as an over-eager nested-placeholder skip would) leaves those // references "unknown type name". Only a VALUE template would // embed the nested placeholder BY VALUE (an undefined inline // struct), which is the case Stage 2 must skip. let outer_is_value = matches!(template.allocation, crate::ast::AllocKind::Value); let has_placeholder = type_args_c.iter() .any(|c| self.debt_worklist_arg_is_placeholder(c, &generic_names, outer_is_value)); if has_placeholder { continue; } } self.emitted_generic_type_instances.insert(mangled.clone()); // Register instance info for Source 2c in resolve_mono_type_args: // "Nova_Box____nova_int" → ("Box", ["nova_int"]) self.generic_type_instance_info.borrow_mut() .entry(mangled.clone()) .or_insert_with(|| (base_name.clone(), type_args_c.clone())); let template = match self.generic_type_templates.get(&base_name).cloned() { Some(t) => t, None => continue, }; let type_subst: Vec<(String, String)> = template.generics.iter() .zip(type_args_c.iter()) .map(|(g, c)| (g.name.clone(), self.arg_c(c))) .collect(); // Redirect output to generic_type_defs_buf so instances appear // before fn definitions in the final C output (via marker splice). // Plan 168 (D300): also reset indent to 0 so the struct definition // is emitted at global scope even when drain is called from inside // a function body. Restores after emit. let saved_out = std::mem::take(&mut self.out); let saved_indent = self.indent; self.indent = 0; // Plan 168 (D300): emit forward-decl into user_type_fwd_decls so // that tuple-typedefs emitted before this drain call can reference // the type without "unknown type name". The struct definition itself // goes into generic_type_defs_buf (spliced at the marker, before fns). if base_name == "Vec" { let fwd = format!("typedef struct {0} {0};\n", mangled); if !self.user_type_fwd_decls.contains(&fwd) { self.user_type_fwd_decls.push_str(&fwd); } } // [реестр 221.1 №139 Round 3] Snapshot BEFORE emitting — // mirrors the `opt_snap` in `emit_value_record_type`: this // instance's OWN field lowering (inside the call below) may // register a NORMAL (non-late-payload) NovaOpt/NovaRes // (e.g. an `Option[SomeType]` field) whose wrapper struct // this instance's body needs BY VALUE — but that wrapper's // marker (`__NOVAOPT_TYPEDEFS__`/`__NOVARES_TYPEDEFS__`) // sits AFTER the unified section this instance (if // Record-kind) is about to join. Cut-and-move the delta // into this node's own text, same technique, so it doesn't // matter which side of the unified/NovaOpt marker boundary // this instance ends up sorted relative to ITS OWN // siblings. // // [Round 4 note] A GLOBAL "NovaOpt/NovaRes as proper unified // graph nodes" version was tried and REVERTED in this same // window — it broke a DIFFERENT, unrelated case // (`NovaOpt_NovaClos_ii_p`, a closure-pointer Option payload // registered through a separate code path this change // didn't account for) on the very corpus (`a_q3_println_ // debug_record`) this round is trying to fix. The per-node // cut here is proven correct on every test run this // session; its known narrow gap (documented in the round-4 // completeness audit) — two DIFFERENT unified nodes sharing // the IDENTICAL `Option[SameType]`/`Result[Same,Same]` as // their OWN field, where only the first to register wins // the only buffer copy — is not exercised anywhere in the // tested corpus; closing it properly needs a dedicated // follow-up window, not a rushed rewrite under this one's // time budget. let opt_snap = self.novaopt_typedefs_buf.borrow().len(); let res_snap = self.novares_typedefs_buf.borrow().len(); self.emit_generic_type_instance(&template.clone(), &type_subst, &mangled)?; let instance_code = std::mem::take(&mut self.out); // [реестр 221.1 №139 Round 3] Record-kind instances go into // the unified topo-sort (`pending_value_nodes`) instead of // the direct-append `generic_type_defs_buf` — see // `render_unified_value_types` doc. Everything else // (Sum/Newtype/etc. — side-channel left `None`) is // untouched, byte-identical to before — including these // NovaOpt/NovaRes snapshots, which only matter for the // Record-kind (unified-section) branch; a non-Record // instance keeps rendering at its ORIGINAL safely-late // `generic_type_defs_buf` position, same as always, so any // delta here is simply left in place (not cut) for it. match self.last_generic_record_instance.take() { Some((node_name, field_ctys)) => { let opt_delta = { let mut buf = self.novaopt_typedefs_buf.borrow_mut(); if buf.len() > opt_snap { buf.split_off(opt_snap) } else { String::new() } }; let res_delta = { let mut buf = self.novares_typedefs_buf.borrow_mut(); if buf.len() > res_snap { buf.split_off(res_snap) } else { String::new() } }; let mut combined = String::new(); combined.push_str(&opt_delta); combined.push_str(&res_delta); combined.push_str(&instance_code); self.pending_value_nodes.push((node_name, field_ctys, combined)); } None => { self.generic_type_defs_buf.push_str(&instance_code); } } self.out = saved_out; self.indent = saved_indent; } depth += 1; if depth > self.mono_depth_limit { return Err(format!( "generic type instantiation depth limit {} exceeded (possible recursive generic types); \ raise via --mono-depth=N CLI flag (or NOVA_MONO_DEPTH env var)", self.mono_depth_limit )); } } Ok(()) } /// Plan 48 Ф.3: emit a concrete C struct/union for one generic type instance. fn emit_generic_type_instance( &mut self, template: &crate::ast::TypeDecl, type_subst: &[(String, String)], mangled: &str, ) -> Result<(), String> { // [race-198 class-closure]: см. override_maps_scope_enter doc. let ovr_saved = self.override_maps_scope_enter(); let r = self.emit_generic_type_instance_scoped_inner(template, type_subst, mangled); self.override_maps_scope_exit(ovr_saved, r.is_ok()); r } fn emit_generic_type_instance_scoped_inner( &mut self, template: &crate::ast::TypeDecl, type_subst: &[(String, String)], mangled: &str, ) -> Result<(), String> { // [M-http-compress-errorkind-crosspkg-collision] fix (mirrors the // non-generic type-decl emission gate at emit_type_decl, ~15621): a // generic template's own fields/variant payloads are lowered here via // `type_ref_to_c` OUTSIDE any fn/test emission scope (this runs from // `drain_generic_type_worklist`, called both between module phases and // from mono-body drains — `current_emit_file_id` is whatever the LAST // fn/test/type left it as, often `None` at CU start). A field typed // with a COLLIDING simple name (e.g. a generic wrapper instantiated // over `HttpError`, whose OWN field `kind ErrorKind` gets re-lowered // when the wrapper's struct body references the inner type) then hits // `ref_type_base`'s bare-name fallthrough (`current_emit_file_id` is // None/stale, not the template's declaring file) — emitting an // unqualified `Nova_ErrorKind*` even though `ErrorKind` collides // between http/compress in this CU (D381 catches the TYPE's own // struct/tag emission, but not a re-lowering of the SAME field type // string from inside a generic instance body). GATED like the // existing type-decl block: byte-identical when this CU has no // cross-module/per-file type collision at all. let saved_emit_file_id_gti = self.current_emit_file_id; if self.any_type_file_collision() { self.current_emit_file_id = Some(template.span.file_id); } let r = self.emit_generic_type_instance_body(template, type_subst, mangled); self.current_emit_file_id = saved_emit_file_id_gti; r } fn emit_generic_type_instance_body( &mut self, template: &crate::ast::TypeDecl, type_subst: &[(String, String)], mangled: &str, ) -> Result<(), String> { use crate::ast::TypeDeclKind; use crate::ast::SumVariantKind; let saved_subst = std::mem::replace( &mut self.current_type_subst, Self::subst_map_from_c_pairs(type_subst.iter().cloned()), ); // [реестр 221.1 №139 Round 3] Side-channel for the Record arm below // to hand its (struct_c_name, field_c_tys) back to the caller // (`drain_generic_type_worklist`) without threading a new return // value through `emit_generic_type_instance`/`_scoped_inner` (both // shared, non-Record-specific plumbing). Reset here so a STALE // value from a PRIOR (unrelated) call never leaks through for a // non-Record kind (Sum/Newtype/etc., which leave it `None` — those // stay on the ORIGINAL direct-to-`generic_type_defs_buf` path, // unchanged; see `render_unified_value_types` doc for why only // Record-kind is unified for now). self.last_generic_record_instance = None; match template.kind.clone() { TypeDeclKind::Record(fields) => { let mut schema: HashMap<String, String> = HashMap::new(); // Pre-compute field types so we can emit forward decls for // pointer-to-struct fields before the struct definition. This // handles cases where a field references another generic instance // (e.g. `Nova_Lru____K__V` has `Nova_HashMap____K__V* store`) // that may be instantiated later in generic_type_defs_buf. // Plan 70 PhaseA3: strict — mono'd generic type instance fields. let field_ctys: Vec<String> = fields.iter() .map(|f| self.type_ref_to_c( // **Plan 147 Ф.3 (D246, 3-axis L3):** binding-mut // promotion REMOVED — pointee-mut is from the TYPE // (`*mut T`), not inherited from the field's `mut` // binding. `Vec.data` is declared `*mut T` explicitly, // so its pointee stays writable (`Nova_T*`) and the // forward-`typedef` still fires (a `*mut` pointee is // not `const`-qualified). &f.ty, ).map_err(|e| self.err_no_int_fallback( &format!("mono'd generic type instance field `{}`", f.name), &e, ))) .collect::<Result<Vec<_>, _>>()?; for c_ty in &field_ctys { if c_ty.ends_with('*') { // Plan 131 Ф.3: strip ALL trailing `*` so pointer-to- // pointer fields (`*mut T` over a record T → `Nova_Pt**`) // forward-declare the base struct (`Nova_Pt`), not the // invalid `typedef struct Nova_Pt* Nova_Pt*;`. let base = c_ty.trim_end_matches(|ch| ch == '*' || ch == ' '); // [M-atomicint-record-field-typedef-collision] fix // (2026-07-11) — same guard as the non-generic // `emit_record_type`/`emit_sum_type` pre-passes: a // generic instance field of a runtime-backed sync // primitive type must not get a competing forward // typedef (its real one is already `#include`d). // // [реестр 221.1 №139 Round 3] ALSO forward-declare a // pointer-to-positional-mono-tuple/fixarr // (`_NovaTuple_2_..`/`_NovaTupleN`/`_NovaFixArr_..`, // e.g. a Vec-mono's `_NovaTuple_..* data`) — these now // join the unified topo-sort too // (`render_unified_value_types`) and, being a // pointer reference, carry NO ordering edge, so the // pointee may legitimately sort AFTER this instance; // only a forward-decl (not the full body) is needed // here regardless of that order. if base.starts_with("Nova_") && !Self::debt_is_runtime_backed_newtype( base.trim_start_matches("Nova_"), ) { self.line(&format!("typedef struct {0} {0};", base)); } else if base.starts_with("_NovaTuple_") || base.starts_with("_NovaFixArr_") { // NOTE trailing underscore on `_NovaTuple_` — // excludes the legacy anonymous-struct form // `_NovaTupleN` (see `debt_is_guaranteed_struct_tag`'s // doc for the exact redefinition error this // avoids). self.line(&format!("typedef struct {0} {0};", base)); } } } // Plan 153.2 Ф.1 (STAGE 1 — by-value generic value-records): // a `value` template (`type X[T] value { … }`) monomorphizes to // an INLINE struct `NovaValue_<short>` registered in // `value_record_names` — exactly mirroring the non-generic // `emit_value_record_type` path. The record literal then takes // the `value_record_names` stack-init branch (`NovaValue_X t; // t.f = v;`) instead of `nova_alloc(sizeof(Nova_X))`, killing the // wrapper heap allocation. Heap templates keep `struct Nova_<…>` // (pointer ABI). `short` = mangled name minus the `Nova_` prefix, // the key the value-record machinery expects (parity with the // non-generic value-record which keys by bare type name). let short = Self::debt_mono_short_name(mangled).to_string(); let is_value = matches!(template.allocation, crate::ast::AllocKind::Value); let struct_c_name = if is_value { format!("NovaValue_{}", short) } else { mangled.to_string() }; // [реестр 221.1 №139 Round 3] Hand (name, deps) back to // `drain_generic_type_worklist` via the side-channel BEFORE // `field_ctys` is consumed by the `.zip` below. self.last_generic_record_instance = Some((struct_c_name.clone(), field_ctys.clone())); // Forward decl to handle circular/self-referential types self.line(&format!("typedef struct {0} {0};", struct_c_name)); self.line(&format!("struct {} {{", struct_c_name)); self.indent += 1; if is_value && fields.is_empty() { self.line("char _empty_value_record_marker;"); } for (f, c_ty) in fields.iter().zip(field_ctys) { let mf = Self::mangle_field_name(&f.name); self.line(&format!("{} {};", c_ty, mf)); schema.insert(f.name.clone(), c_ty); } self.indent -= 1; self.line("};"); self.line(""); // Schema is keyed by the short mono name for BOTH forms (heap // path also strips `Nova_`). self.record_schemas.insert(short.clone(), schema); if is_value { // Activate the D226 value-record ABI for this mono instance: // mark it value, and alias the short name → its by-value // C-type so any `type_ref_to_c`/`type_aliases` consumer that // keys by the short name resolves to `NovaValue_<short>`. self.value_record_names.insert(short.clone()); self.type_aliases.insert( short.clone(), format!("NovaValue_{}", short)); } } TypeDeclKind::Sum(variants) => { // Tag enum self.line("typedef enum {"); self.indent += 1; for v in &variants { // №296: thread explicit discriminant into the mono/ // generic-instance tag enum too — same rule as the // non-generic emit_sum_type path above. match v.discriminant { Some(d) => self.line(&format!("NOVA_TAG_{}_{} = {},", mangled, v.name, d)), None => self.line(&format!("NOVA_TAG_{}_{},", mangled, v.name)), } } self.indent -= 1; self.line(&format!("}} {}_Tag;", mangled)); let mut sum_schema: HashMap<String, Vec<String>> = HashMap::new(); // Forward decl self.line(&format!("typedef struct {0} {0};", mangled)); self.line(&format!("struct {} {{", mangled)); self.indent += 1; self.line(&format!("{}_Tag tag;", mangled)); self.line("union {"); self.indent += 1; let has_payload = variants.iter().any(|v| !matches!(v.kind, SumVariantKind::Unit)); if !has_payload { self.line("char _dummy;"); } for v in &variants { match &v.kind { SumVariantKind::Unit => { sum_schema.insert(v.name.clone(), vec![]); } SumVariantKind::Tuple(types) => { let mut field_types = Vec::new(); self.line("struct {"); self.indent += 1; for (i, ty) in types.iter().enumerate() { let tc = self.type_ref_to_c(ty)?; field_types.push(tc.clone()); self.line(&format!("{} _{};", tc, i)); } self.indent -= 1; self.line(&format!("}} {};", v.name)); sum_schema.insert(v.name.clone(), field_types); } SumVariantKind::Record(fields) => { let mut field_types = Vec::new(); self.line("struct {"); self.indent += 1; for f in fields { let tc = self.type_ref_to_c(&f.ty)?; field_types.push(tc.clone()); let mf = Self::mangle_field_name(&f.name); self.line(&format!("{} {};", tc, mf)); let schema_base = Self::debt_strip_nova_prefix(mangled); let key = format!("{}::{}::{}", schema_base, v.name, f.name); self.record_variant_field_types.insert(key, tc); } let schema_base = Self::debt_strip_nova_prefix(mangled); let order_key = format!("{}::{}", schema_base, v.name); let field_names: Vec<String> = fields.iter().map(|f| f.name.clone()).collect(); self.record_variant_field_order.insert(order_key, field_names); self.indent -= 1; self.line(&format!("}} {};", v.name)); sum_schema.insert(v.name.clone(), field_types); } } } self.indent -= 1; self.line("} payload;"); self.indent -= 1; self.line("};"); self.line(""); // Constructor functions for each variant let mangled_clone = mangled.to_string(); for v in &variants { let field_types = sum_schema.get(&v.name).cloned().unwrap_or_default(); let params: String = field_types.iter().enumerate() .map(|(i, t)| format!("{} _{}", t, i)) .collect::<Vec<_>>() .join(", "); let params_str = if params.is_empty() { "void".to_string() } else { params }; self.line(&format!( "{storage}{name}* nova_make_{name}_{var}({params}) {{", storage = self.top_level_storage(), name = mangled_clone, var = v.name, params = params_str )); self.indent += 1; self.line(&format!( "{name}* _r = ({name}*)nova_alloc(sizeof({name}));", name = mangled_clone )); self.line(&format!("_r->tag = NOVA_TAG_{name}_{var};", name = mangled_clone, var = v.name)); match &v.kind { SumVariantKind::Unit => {} SumVariantKind::Tuple(_) => { for (i, _) in field_types.iter().enumerate() { self.line(&format!("_r->payload.{var}._{i} = _{i};", var = v.name, i = i)); } } SumVariantKind::Record(fields) => { for (i, f) in fields.iter().enumerate() { let mf = Self::mangle_field_name(&f.name); self.line(&format!("_r->payload.{var}.{fname} = _{i};", var = v.name, fname = mf, i = i)); } } } self.line("return _r;"); self.indent -= 1; self.line("}"); self.line(""); } let sum_key = Self::debt_strip_nova_prefix(mangled); // Plan 62.A.bis Ф.2.1: mirror legacy insert в registry. // Generic mono'd sum types — heap-pointer ABI (PointerErrorLike). let variant_order: Vec<String> = variants.iter() .map(|v| v.name.clone()).collect(); self.sum_schema_registry.register_user_sum( sum_key, &sum_schema, mangled, super::sum_schema_registry::SumAbi::PointerErrorLike, &variant_order, ); self.sum_schemas.insert(sum_key.to_string(), sum_schema); } TypeDeclKind::Opaque => { // Plan 103.5: special runtime-backed generic types that need // per-T struct + methods emitted by codegen (not fully in // the runtime header, because T is generic). let base_name = template.name.clone(); match base_name.as_str() { "OnceCell" => { let t_cty = type_subst.iter() .find(|(k, _)| k == "T") .map(|(_, v)| v.clone()) .unwrap_or_else(|| "nova_int".to_string()); self.emit_oncecell_instance(mangled, &t_cty); } "Lazy" => { let t_cty = type_subst.iter() .find(|(k, _)| k == "T") .map(|(_, v)| v.clone()) .unwrap_or_else(|| "nova_int".to_string()); self.emit_lazy_instance(mangled, &t_cty); } _ => { // Other opaque types: C struct lives in runtime header, // no per-T emission needed. } } } // Plan 91.12 V2 (D126 retract — sync types migration): runtime-backed // generic newtypes need the same per-T struct + methods emission as // their predecessor `external type` (Opaque) form. After migration: // `external type OnceCell[T]` → `type OnceCell[T](ptr)` // `external type Lazy[T]` → `type Lazy[T](ptr)` // The Newtype declaration carries no body; per-T mono path routes to // the same emit_oncecell_instance / emit_lazy_instance helpers. // The Nova-level typedef declaration emission is suppressed for these // names (see emit_type_decl Newtype branch — RUNTIME_BACKED_NEWTYPES // list); per-T mono here defines the actual C struct. TypeDeclKind::Newtype(_) => { let base_name = template.name.clone(); match base_name.as_str() { "OnceCell" => { let t_cty = type_subst.iter() .find(|(k, _)| k == "T") .map(|(_, v)| v.clone()) .unwrap_or_else(|| "nova_int".to_string()); self.emit_oncecell_instance(mangled, &t_cty); } "Lazy" => { let t_cty = type_subst.iter() .find(|(k, _)| k == "T") .map(|(_, v)| v.clone()) .unwrap_or_else(|| "nova_int".to_string()); self.emit_lazy_instance(mangled, &t_cty); } _ => { // Regular newtype `type X(T)` — handled in emit_type_decl // (typedef T Nova_X). Generic newtype без runtime backing // currently не используется; per-T mono no-op для них. } } } // реестр 221.1 №502: a GENERIC named tuple (`type Ent[K, V](key K, value // V)`) template instance had NO arm here at all — it fell into the // catch-all `_ => {}` below (which the comment already labels "not // generic record/sum", but a NamedTuple genuinely IS one of the value // kinds this drain exists to emit). Consequence: the universal // registration in `resolved_named_to_c`'s generic-template arm still // pushes `(base_name, type_args_c, mangled)` onto `generic_type_worklist` // (so a `typedef struct <mangled> <mangled>;` forward-decl DOES appear, // from that same call site's caller) but NOTHING ever emitted the // `struct <mangled> { ... };` BODY — any `->field` access on the // instance hit clang's "incomplete definition of type" the moment the // forward-declared-only struct was dereferenced. // // Representation: `resolved_named_to_c`'s generic-template fallback // (`is_value_generic_template` tests ONLY `Record`) already decided // EVERY generic NamedTuple instance is heap-pointer ABI (`Nova_<mangled>*`, // not the non-generic sibling's `NovaTuple_<name>` by-value form) — this // arm keeps that pre-existing decision unchanged (narrow fix, scoped to // "fill in the missing body/ctor", not "reclassify value-vs-heap ABI for // generic named tuples", which would be a wider mono-model change). // // Schema is keyed by the SHORT mono name (`short`, `Nova_` prefix // stripped) — mirrors the Record arm above, so the checker-channel field // lookup (`resolved_type_to_c`'s `R::TypeParam` subst / any consumer // keying by short name) finds it. TypeDeclKind::NamedTuple(fields) => { let mut schema: HashMap<String, String> = HashMap::new(); let mut nt_field_c_tys: Vec<String> = Vec::with_capacity(fields.len()); for f in &fields { let ty_c = self.type_ref_to_c(&f.ty).map_err(|e| self.err_no_int_fallback( &format!("mono'd generic named-tuple instance field `{}`", f.name), &e, ))?; if let Some(pointee) = ty_c.strip_suffix('*') { let base = pointee.trim_end_matches(|ch| ch == '*' || ch == ' ').trim(); if base.starts_with("Nova_") && !Self::debt_is_runtime_backed_newtype(base.trim_start_matches("Nova_")) { self.line(&format!("typedef struct {0} {0};", base)); } } schema.insert(f.name.clone(), ty_c.clone()); nt_field_c_tys.push(ty_c); } self.line(&format!("typedef struct {0} {0};", mangled)); self.line(&format!("struct {} {{", mangled)); self.indent += 1; for (f, c_ty) in fields.iter().zip(nt_field_c_tys.iter()) { let mf = Self::mangle_field_name(&f.name); self.line(&format!("{} {};", c_ty, mf)); } self.indent -= 1; self.line("};"); self.line(""); let short = Self::debt_mono_short_name(mangled).to_string(); self.record_schemas.insert(short.clone(), schema); self.value_struct_field_tys.insert(mangled.to_string(), nt_field_c_tys.clone()); // Constructor: unlike the non-generic `emit_named_tuple_type` path // (an inline `((NovaTuple_X){.f=v})` compound literal — no function // needed, since a NON-generic type's C-type/size is fixed at compile // time and known at every call site), a GENERIC instance's size/ // layout is only known HERE, per-mono — so a real heap-allocating // constructor FUNCTION is emitted, named to match what the call-site // fix (`try_infer_named_tuple_mono_args`, `emit_call`'s // `ExprKind::Ident` branch) invokes. let ctor_name = format!("nova_ctor_{}", short); let params: Vec<String> = fields.iter().zip(nt_field_c_tys.iter()) .map(|(f, c_ty)| format!("{} {}", c_ty, Self::mangle_field_name(&f.name))) .collect(); self.line(&format!( "static {}* {}({}) {{", mangled, ctor_name, params.join(", "), )); self.indent += 1; self.line(&format!( "{0}* _nv_self = ({0}*)nova_alloc(sizeof({0}));", mangled, )); for f in &fields { let mf = Self::mangle_field_name(&f.name); self.line(&format!("_nv_self->{0} = {0};", mf)); } self.line("return _nv_self;"); self.indent -= 1; self.line("}"); self.line(""); } _ => { /* Protocol/effect/alias — not generic record/sum */ } } self.current_type_subst = saved_subst; Ok(()) } /// Plan 103.5: emit monomorphized OnceCell[T] struct + all methods. /// Called from emit_generic_type_instance for Opaque "OnceCell" instances. /// mangled = "Nova_OnceCell____nova_int", t_cty = "nova_int". fn emit_oncecell_instance(&mut self, mangled: &str, t_cty: &str) { let m = mangled; let opt_ty = format!("NovaOpt_{}", t_cty); // Emit struct self.line(&format!("/* ── OnceCell[{t}] — Plan 103.5 ───────────────────────── */", t = t_cty)); self.line(&format!("typedef struct {{")); self.indent += 1; self.line("nova_mutex_t mu;"); self.line("int state; /* 0=EMPTY 1=RUNNING 2=DONE */"); self.line("NovaOnceWaiter* waiters;"); self.line("nova_bool has_value;"); self.line(&format!("{t} value;", t = t_cty)); self.indent -= 1; self.line(&format!("}} {m};", m = m)); self.line(""); // static new() self.line(&format!("{s}{m}* {m}_static_new(void) {{", s = self.top_level_storage_inline(), m = m)); self.indent += 1; self.line(&format!("{m}* _c = ({m}*)nova_alloc(sizeof({m}));", m = m)); self.line("nova_mutex_init(&_c->mu);"); self.line("_c->state = 0; _c->waiters = NULL; _c->has_value = false;"); self.line("return _c;"); self.indent -= 1; self.line("}"); self.line(""); // get() -> Option[T] self.line(&format!("{s}{o} {m}_method_get({m}* _c) {{", s = self.top_level_storage_inline(), o = opt_ty, m = m)); self.indent += 1; // Plan 118 Ф.5: NPO-aware return constructors. let opt_sani = opt_ty.strip_prefix("NovaOpt_").unwrap_or(&opt_ty); let some_expr = self.option_some_expr(opt_sani, "_c->value"); let none_expr = self.option_none_expr(opt_sani); self.line("if (__atomic_load_n(&_c->has_value, __ATOMIC_ACQUIRE)) {"); self.indent += 1; self.line(&format!("return {};", some_expr)); self.indent -= 1; self.line("}"); self.line(&format!("return {};", none_expr)); self.indent -= 1; self.line("}"); self.line(""); // set(v T) -> bool self.line(&format!("{s}nova_bool {m}_method_set({m}* _c, {t} _v) {{", s = self.top_level_storage_inline(), m = m, t = t_cty)); self.indent += 1; self.line("nova_mutex_lock(&_c->mu);"); self.line("if (_c->has_value) { nova_mutex_unlock(&_c->mu); return false; }"); self.line("_c->value = _v;"); self.line("__atomic_store_n(&_c->has_value, true, __ATOMIC_RELEASE);"); self.line("_c->state = 2;"); self.line("nova_mutex_unlock(&_c->mu);"); self.line("return true;"); self.indent -= 1; self.line("}"); self.line(""); // get_or_init(init fn() -> T) -> T self.line(&format!("{s}{t} {m}_method_get_or_init({m}* _c, NovaClosBase* _init) {{", s = self.top_level_storage_inline(), t = t_cty, m = m)); self.indent += 1; // retry label for re-entry after init failure self.line("_oc_retry:;"); self.line("if (__atomic_load_n(&_c->has_value, __ATOMIC_ACQUIRE)) return _c->value;"); self.line("nova_mutex_lock(&_c->mu);"); self.line("if (_c->has_value) { nova_mutex_unlock(&_c->mu); return _c->value; }"); self.line("if (_c->state == 1 /* RUNNING */) {"); self.indent += 1; self.line("if (_nova_active_slot < 0) {"); self.indent += 1; self.line("nova_mutex_unlock(&_c->mu);"); self.line("for (;;) {"); self.indent += 1; self.line("_nova_cpu_yield();"); self.line("if (__atomic_load_n(&_c->has_value, __ATOMIC_ACQUIRE)) return _c->value;"); self.line("if (__atomic_load_n(&_c->state, __ATOMIC_ACQUIRE) == 0) goto _oc_retry;"); self.indent -= 1; self.line("}"); self.indent -= 1; self.line("}"); self.line("NovaOnceWaiter _oc_w; _oc_w.scope = _nova_active_scope;"); self.line("_oc_w.slot = _nova_active_slot; _oc_w.next = _c->waiters; _c->waiters = &_oc_w;"); self.line("nova_sched_park_with_unlock(_nova_active_scope, _nova_active_slot,"); self.line(" (void(*)(void*))nova_mutex_unlock, &_c->mu);"); self.line("if (__atomic_load_n(&_c->has_value, __ATOMIC_ACQUIRE)) return _c->value;"); self.line("goto _oc_retry; /* init failed — retry with our own closure */"); self.indent -= 1; self.line("}"); self.line("/* state == EMPTY: become runner */"); self.line("_c->state = 1;"); self.line("nova_mutex_unlock(&_c->mu);"); self.line("/* Run init with panic capture. */"); self.line("/* Plan 103.5: clear _nova_handler_Fail so throw→nova_throw→NOVA_TRY */"); self.line("NovaVtable_Fail* _oc_saved_fail = _nova_handler_Fail;"); self.line("_nova_handler_Fail = NULL;"); self.line("NovaFailFrame _oc_frame; nova_bool _oc_panicked = false; nova_str _oc_msg;"); self.line(&format!("{t} _oc_result;", t = t_cty)); self.line("if (NOVA_TRY(_oc_frame)) {"); self.indent += 1; self.line(&format!("_oc_result = (({t}(*)(void*))_init->fn)(_init->env);", t = t_cty)); self.line("nova_fail_pop();"); self.indent -= 1; self.line("} else {"); self.indent += 1; self.line("_oc_panicked = true; _oc_msg = NOVA_CATCH(_oc_frame);"); self.indent -= 1; self.line("}"); self.line("_nova_handler_Fail = _oc_saved_fail;"); self.line("nova_mutex_lock(&_c->mu);"); self.line("if (!_oc_panicked) {"); self.indent += 1; self.line("_c->value = _oc_result;"); self.line("__atomic_store_n(&_c->has_value, true, __ATOMIC_RELEASE);"); self.line("_c->state = 2; /* DONE */"); self.indent -= 1; self.line("} else { _c->state = 0; /* EMPTY — retry allowed */ }"); self.line("NovaOnceWaiter* _oc_wk = _c->waiters; _c->waiters = NULL;"); self.line("nova_mutex_unlock(&_c->mu);"); self.line("while (_oc_wk) { NovaOnceWaiter* _oc_nx = _oc_wk->next;"); self.line(" nova_sched_wake(_oc_wk->scope, _oc_wk->slot); _oc_wk = _oc_nx; }"); self.line("if (_oc_panicked) { Nova_Fail_fail(_oc_msg); nova_throw(_oc_msg); }"); self.line("return _c->value;"); self.indent -= 1; self.line("}"); self.line(""); // take() -> Option[T] self.line(&format!("{s}{o} {m}_method_take({m}* _c) {{", s = self.top_level_storage_inline(), o = opt_ty, m = m)); self.indent += 1; self.line("nova_mutex_lock(&_c->mu);"); // Plan 118 Ф.5: NPO-aware constructors. let opt_sani = opt_ty.strip_prefix("NovaOpt_").unwrap_or(&opt_ty); let none_expr = self.option_none_expr(opt_sani); let some_v_expr = self.option_some_expr(opt_sani, "_v"); self.line("if (!_c->has_value) {"); self.indent += 1; self.line("nova_mutex_unlock(&_c->mu);"); self.line(&format!("return {};", none_expr)); self.indent -= 1; self.line("}"); self.line(&format!("{t} _v = _c->value;", t = t_cty)); self.line("__atomic_store_n(&_c->has_value, false, __ATOMIC_RELEASE);"); self.line("_c->state = 0; /* EMPTY — re-initializable */"); self.line("nova_mutex_unlock(&_c->mu);"); self.line(&format!("return {};", some_v_expr)); self.indent -= 1; self.line("}"); self.line(""); // is_initialized() -> bool self.line(&format!("{s}nova_bool {m}_method_is_initialized({m}* _c) {{", s = self.top_level_storage_inline(), m = m)); self.indent += 1; self.line("return __atomic_load_n(&_c->has_value, __ATOMIC_ACQUIRE);"); self.indent -= 1; self.line("}"); self.line(""); } /// Plan 103.5: emit monomorphized Lazy[T] struct + all methods. /// Called from emit_generic_type_instance for Opaque "Lazy" instances. /// mangled = "Nova_Lazy____nova_int", t_cty = "nova_int". fn emit_lazy_instance(&mut self, mangled: &str, t_cty: &str) { let m = mangled; // Emit struct — includes a stored init closure (NovaClosBase*) // and a poisoned flag for Poisoned semantics (unlike OnceCell retry). self.line(&format!("/* ── Lazy[{t}] — Plan 103.5 ────────────────────────────── */", t = t_cty)); self.line(&format!("typedef struct {{")); self.indent += 1; self.line("nova_mutex_t mu;"); self.line("int state; /* 0=UNFORCED 1=RUNNING 2=FORCED */"); self.line("NovaOnceWaiter* waiters;"); self.line("nova_bool has_value;"); self.line(&format!("{t} value;", t = t_cty)); self.line("nova_bool poisoned;"); self.line("nova_str poison_msg;"); self.line("NovaClosBase* init_clos; /* fn() -> T, stored at new() */"); self.indent -= 1; self.line(&format!("}} {m};", m = m)); self.line(""); // static new(init fn() -> T) -> Self self.line(&format!("{s}{m}* {m}_static_new(NovaClosBase* _init) {{", s = self.top_level_storage_inline(), m = m)); self.indent += 1; self.line(&format!("{m}* _l = ({m}*)nova_alloc(sizeof({m}));", m = m)); self.line("nova_mutex_init(&_l->mu);"); self.line("_l->state = 0; _l->waiters = NULL; _l->has_value = false;"); self.line("_l->poisoned = false; _l->init_clos = _init;"); self.line("return _l;"); self.indent -= 1; self.line("}"); self.line(""); // force() -> T (Poisoned semantics — NOT retry like OnceCell) self.line(&format!("{s}{t} {m}_method_force({m}* _l) {{", s = self.top_level_storage_inline(), t = t_cty, m = m)); self.indent += 1; self.line("/* Fast path A: already forced. */"); self.line("if (__atomic_load_n(&_l->has_value, __ATOMIC_ACQUIRE)) return _l->value;"); self.line("/* Fast path B: poisoned — re-panic through effect system. */"); self.line("if (__atomic_load_n(&_l->poisoned, __ATOMIC_ACQUIRE)) { Nova_Fail_fail(_l->poison_msg); nova_throw(_l->poison_msg); }"); self.line("nova_mutex_lock(&_l->mu);"); self.line("if (_l->has_value) { nova_mutex_unlock(&_l->mu); return _l->value; }"); self.line("if (_l->poisoned) { nova_mutex_unlock(&_l->mu); Nova_Fail_fail(_l->poison_msg); nova_throw(_l->poison_msg); }"); self.line("if (_l->state == 1 /* RUNNING */) {"); self.indent += 1; self.line("if (_nova_active_slot < 0) {"); self.indent += 1; self.line("nova_mutex_unlock(&_l->mu);"); self.line("for (;;) {"); self.indent += 1; self.line("_nova_cpu_yield();"); self.line("if (__atomic_load_n(&_l->has_value, __ATOMIC_ACQUIRE)) return _l->value;"); self.line("if (__atomic_load_n(&_l->poisoned, __ATOMIC_ACQUIRE)) { Nova_Fail_fail(_l->poison_msg); nova_throw(_l->poison_msg); }"); self.indent -= 1; self.line("}"); self.indent -= 1; self.line("}"); self.line("NovaOnceWaiter _lz_w; _lz_w.scope = _nova_active_scope;"); self.line("_lz_w.slot = _nova_active_slot; _lz_w.next = _l->waiters; _l->waiters = &_lz_w;"); self.line("nova_sched_park_with_unlock(_nova_active_scope, _nova_active_slot,"); self.line(" (void(*)(void*))nova_mutex_unlock, &_l->mu);"); self.line("if (__atomic_load_n(&_l->has_value, __ATOMIC_ACQUIRE)) return _l->value;"); self.line("Nova_Fail_fail(_l->poison_msg); nova_throw(_l->poison_msg); /* woken after poison */"); self.line("return _l->value; /* unreachable, silence compiler */"); self.indent -= 1; self.line("}"); self.line("/* state == UNFORCED: we become runner */"); self.line("_l->state = 1;"); self.line("nova_mutex_unlock(&_l->mu);"); self.line("NovaClosBase* _lz_clos = _l->init_clos;"); self.line("/* Plan 103.5: clear _nova_handler_Fail so throw→nova_throw→NOVA_TRY */"); self.line("NovaVtable_Fail* _lz_saved_fail = _nova_handler_Fail;"); self.line("_nova_handler_Fail = NULL;"); self.line("NovaFailFrame _lz_frame; nova_bool _lz_panicked = false; nova_str _lz_msg;"); self.line(&format!("{t} _lz_result;", t = t_cty)); self.line("if (NOVA_TRY(_lz_frame)) {"); self.indent += 1; self.line(&format!("_lz_result = (({t}(*)(void*))_lz_clos->fn)(_lz_clos->env);", t = t_cty)); self.line("nova_fail_pop();"); self.indent -= 1; self.line("} else {"); self.indent += 1; self.line("_lz_panicked = true; _lz_msg = NOVA_CATCH(_lz_frame);"); self.indent -= 1; self.line("}"); self.line("_nova_handler_Fail = _lz_saved_fail;"); self.line("nova_mutex_lock(&_l->mu);"); self.line("if (!_lz_panicked) {"); self.indent += 1; self.line("_l->value = _lz_result;"); self.line("__atomic_store_n(&_l->has_value, true, __ATOMIC_RELEASE);"); self.line("_l->state = 2;"); self.indent -= 1; self.line("} else {"); self.indent += 1; self.line("_l->poison_msg = _lz_msg;"); self.line("__atomic_store_n(&_l->poisoned, true, __ATOMIC_RELEASE);"); self.line("_l->state = 3; /* poisoned terminal state */"); self.indent -= 1; self.line("}"); self.line("NovaOnceWaiter* _lz_wk = _l->waiters; _l->waiters = NULL;"); self.line("nova_mutex_unlock(&_l->mu);"); self.line("while (_lz_wk) { NovaOnceWaiter* _lz_nx = _lz_wk->next;"); self.line(" nova_sched_wake(_lz_wk->scope, _lz_wk->slot); _lz_wk = _lz_nx; }"); self.line("if (_lz_panicked) { Nova_Fail_fail(_lz_msg); nova_throw(_lz_msg); }"); self.line("return _l->value;"); self.indent -= 1; self.line("}"); self.line(""); // is_forced() -> bool self.line(&format!("{s}nova_bool {m}_method_is_forced({m}* _l) {{", s = self.top_level_storage_inline(), m = m)); self.indent += 1; self.line("return __atomic_load_n(&_l->has_value, __ATOMIC_ACQUIRE);"); self.indent -= 1; self.line("}"); self.line(""); } fn emit_monomorphized_fn( &mut self, fn_decl: &crate::ast::FnDecl, type_subst: Vec<(String, crate::types::ResolvedType)>, mono_name: &str, ) -> Result<(), String> { // [race-198 class-closure]: см. override_maps_scope_enter doc. let ovr_saved = self.override_maps_scope_enter(); let r = self.emit_monomorphized_fn_scoped_inner(fn_decl, type_subst, mono_name); self.override_maps_scope_exit(ovr_saved, r.is_ok()); r } fn emit_monomorphized_fn_scoped_inner( &mut self, fn_decl: &crate::ast::FnDecl, type_subst: Vec<(String, crate::types::ResolvedType)>, mono_name: &str, ) -> Result<(), String> { use crate::ast::FnBody; // [M-sync-crossmodule…] (D381): resolve colliding-type references in a // monomorphized free-fn body under its declaring file (gated; byte- // identical for non-colliding CUs). Restored with the type-subst. let saved_emit_file_id_mono = self.current_emit_file_id; if self.any_type_file_collision() { self.current_emit_file_id = Some(fn_decl.span.file_id); } // Set type substitution (A1‴: worklist carries RT — seed current_type_subst directly) let saved_subst = std::mem::replace( &mut self.current_type_subst, type_subst.iter().cloned().collect(), ); // Compute concrete param types // Plan 70 PhaseA3: strict — emit_monomorphized_fn param translation. // [M-generic-static-method-value-arg-addr-mismatch] fix (221.1 Ф.2 // #26): DEFINITION-side twin of `register_mono_instance`'s identical // fix (this file) — see there for the full root-cause note. let param_c_tys: Vec<String> = fn_decl.params.iter().enumerate() .map(|(p_idx, p)| { let mut ty_c = self.type_ref_to_c(&p.ty).map_err(|e| self.err_no_int_fallback( &format!("mono'd fn `{}` param `{}`", fn_decl.name, p.name), &e, ))?; if self.free_fn_byref_flag(&fn_decl.name, p_idx) { ty_c.push('*'); } Ok(ty_c) }) .collect::<Result<Vec<_>, String>>()?; // Plan 70 PhaseB2 (session 2): strict — emit_monomorphized_fn ret_c. let ret_c = match fn_decl.return_type.as_ref() { Some(t) => self.type_ref_to_c(t).map_err(|e| self.err_no_int_fallback( &format!("mono'd fn `{}` return type", fn_decl.name), &e, ))?, None => "nova_unit".into(), }; let params_str = if fn_decl.params.is_empty() { "void".to_string() } else { fn_decl.params.iter().zip(¶m_c_tys) .map(|(p, ty)| format!("{} {}", ty, p.name)) .collect::<Vec<_>>() .join(", ") }; // D109 Ф.7.7: pre-populate array_element_types for []K params where K is substituted // to a concrete pointer type. emit_for uses this to cast array elements correctly, // so `for it in items` gives `Nova_GrmPoint* it = (Nova_GrmPoint*)arr->data[i]` // instead of `nova_int it = arr->data[i]` when K = Nova_GrmPoint*. let mut added_array_elem_keys: Vec<String> = Vec::new(); for param in &fn_decl.params { if let crate::ast::TypeRef::Array(inner, _) = ¶m.ty { if let crate::ast::TypeRef::Named { path, generics, .. } = inner.as_ref() { if generics.is_empty() { let tparam_name = path.join("_"); if let Some(concrete) = self.subst_c(&tparam_name) { if concrete.ends_with('*') && concrete != "nova_int*" { self.array_element_types.insert(param.name.clone(), concrete); added_array_elem_keys.push(param.name.clone()); } } } } } } // Buffer body (same as emit_fn / emit_generic_fn_erased pattern) let saved_out = std::mem::take(&mut self.out); let saved_indent = self.indent; self.indent = 0; self.line(&format!("{}{} {}({}) {{", self.top_level_storage(), ret_c, mono_name, params_str)); self.indent += 1; // Plan 143.2: prologue safepoint. The monomorphized instance inherits // the KEEP-status of its SOURCE template (`fn_decl`), so a recursive // generic free fn keeps a safepoint in every instantiation. self.emit_prologue_preempt_check(fn_decl); // Register params in var_types with concrete C types let saved_var_types: Vec<(String, Option<String>)> = fn_decl.params.iter() .zip(¶m_c_tys) .map(|(p, ty)| (p.name.clone(), self.var_types.insert(p.name.clone(), ty.clone()))) .collect(); // [M-property-testing-rot]: expose the declared Nova TypeRefs of this // mono body's params so a NESTED generic call forwarding one of them // (`property_with(gen, body, ...)`) can unify its type args at the // TypeRef level (`resolve_mono_type_args` Source 2f) — the C type of a // protocol-typed param is an erased `void*`, useless for inference. let saved_param_typerefs = std::mem::replace( &mut self.current_fn_param_typerefs, fn_decl.params.iter() .map(|p| (p.name.clone(), p.ty.clone())) .collect(), ); // Register function-typed params in fn_param_sigs with concrete types // Plan 70 PhaseA3: strict — mono'd fn-typed param signature. let mut saved_fn_sigs: Vec<(String, Option<(Vec<String>, String)>)> = Vec::new(); // [fix M-nested-fn-newtype-bind-then-call-broken, реестр 221.1 №78, // форма 2]: mirrors the top-level-fn primary site's sibling addition — // see `nested_fn_return_sig` doc. Scoped (save/restore) here, matching // this site's own `fn_param_sigs` scoping discipline. let mut saved_fn_returns_sigs: Vec<(String, Option<(Vec<String>, String)>)> = Vec::new(); for (p, _c_ty) in fn_decl.params.iter().zip(¶m_c_tys) { if let Some(_ft) = self.resolve_fn_typeref(&p.ty) { let crate::ast::TypeRef::Func { params: fp, return_type, .. } = &_ft else { unreachable!() }; let inner_ptys: Vec<String> = fp.iter() .map(|t| self.type_ref_to_c(t).map_err(|e| self.err_no_int_fallback( &format!("mono'd fn-typed param `{}` element", p.name), &e, ))) .collect::<Result<Vec<_>, _>>()?; let inner_ret = match return_type.as_ref() { Some(t) => self.type_ref_to_c(t).map_err(|e| self.err_no_int_fallback( &format!("mono'd fn-typed param `{}` return", p.name), &e, ))?, None => "nova_unit".to_string(), }; let prev = self.fn_param_sigs.insert(p.name.clone(), (inner_ptys, inner_ret)); saved_fn_sigs.push((p.name.clone(), prev)); if let Some(sig) = self.nested_fn_return_sig(&_ft) { let prev2 = self.fn_returns_fn_sig.insert(p.name.clone(), sig); saved_fn_returns_sigs.push((p.name.clone(), prev2)); } } } // Set receiver type (None for free fns) let saved_recv = self.current_receiver_type.take(); self.sync_receiver_rt(); // Set return type let saved_ret_ty = std::mem::replace(&mut self.current_fn_return_ty, Some(ret_c.clone())); // Set expected_record_type from fn return type (for anonymous record literals) let saved_expected = std::mem::replace( &mut self.expected_record_type, fn_decl.return_type.as_ref().and_then(|t| { if let crate::ast::TypeRef::Named { path, generics, .. } = t { if generics.is_empty() { Some(path.join("_")) } else { None } } else { None } }), ); // Emit body let body_clone = fn_decl.body.clone(); match &body_clone { FnBody::Expr(e) => { self.emit_source_annotation_for_expr(e); let val = self.emit_expr(e)?; if ret_c == "nova_unit" { self.line(&format!("{};", val)); self.line("return NOVA_UNIT;"); } else { self.line(&format!("return {};", val)); } } FnBody::Block(block) => { let block_id = self.enter_defer_scope(block, false); for stmt in &block.stmts { self.emit_stmt(stmt)?; } if let Some(trailing) = &block.trailing { self.emit_source_annotation_for_expr(trailing); let trailing_ty = self.infer_expr_c_type(trailing); // Plan 172.1 [M-172.1-some-target-coerce]: a `NovaOpt_<X>` / typed-integer // return coerces the trailing TO the return type so a context-typed payload // literal lowers to X — `Some(<int-literal>) -> Option[uint]` builds // `NovaOpt_nova_uint`, not the literal-default `NovaOpt_nova_int` (named- // priority int-collapse → CC-FAIL, surfaced by d86_coalesce_width). Gated on // the NovaOpt_/typed-int ret surface so the common path keeps `emit_expr`. // [M-d55-str-literal-coercion-name-gated] fix: a `[]u8` // return type ALSO needs target-typed routing (D55 amend // str-literal→[]u8 — the arm inside `emit_expr_with_ // target_type` rewrites a bare `StrLit` trailing return // into `.bytes()`; the plain `emit_expr` fallback below // does not know about this coercion at all). // [M-callnorm-free-fn-name-collision] followup (closure-megacu // regression, 2026-07-19): `_NovaTuple_` target added — a tuple // RETURN type whose trailing literal embeds a `_NovaFixArr_` // element (e.g. `(int, [N]T)`) needs the SAME target-typed // routing as a bare fixarr return, or the nested array-literal // inside the tuple literal gets emitted with NO element-type // hint and panics `[P67] nova_int collapse` in the legacy // array-literal path (`current_array_elem_hint` never set). // `emit_expr_with_target_type`'s `TupleLit` arm already handles // this correctly (decodes `_NovaTuple_` elem types, recurses // per element) — it just wasn't reached from here. See // docs/plans/wip/closure-megacu-fix-notes.md. let val = if ret_c.starts_with("NovaOpt_") || ret_c.starts_with("_NovaFixArr_") || ret_c.starts_with("_NovaTuple_") || Self::is_typed_integer(&ret_c) || Self::is_bytes_slice_c_ty(&ret_c) { self.emit_expr_with_target_type(trailing, &ret_c)? } else { self.emit_expr(trailing)? }; self.leave_defer_scope(block_id); if ret_c == "nova_unit" { self.line(&format!("{};", val)); self.line("return NOVA_UNIT;"); } else if trailing_ty == "nova_unit" && ret_c != "nova_unit" { self.line(&format!("{};", val)); self.line(&format!("return ({})0; /* unreachable */", ret_c)); } else if Self::needs_tuple_field_copy(&ret_c, &trailing_ty) { // Plan 59: mono'd tuple return type mismatch — field-wise. let tmp = self.fresh_tmp(); self.emit_tuple_return_stash(&ret_c, &tmp, &val, &trailing_ty); self.line(&format!("return {};", tmp)); } else { self.line(&format!("return {};", val)); } } else { self.leave_defer_scope(block_id); self.line("return NOVA_UNIT;"); } } FnBody::External => {} } // Restore for (name, prev) in saved_var_types { match prev { Some(old) => { self.var_types.insert(name, old); } None => { self.var_types.remove(&name); } } } for (name, prev) in saved_fn_sigs { match prev { Some(old) => { self.fn_param_sigs.insert(name, old); } None => { self.fn_param_sigs.remove(&name); } } } // [fix M-nested-fn-newtype-bind-then-call-broken, №78] restore paired // with the `nested_fn_return_sig` save loop above. for (name, prev) in saved_fn_returns_sigs { match prev { Some(old) => { self.fn_returns_fn_sig.insert(name, old); } None => { self.fn_returns_fn_sig.remove(&name); } } } // D109 Ф.7.7: clean up array_element_types entries added for this call for key in added_array_elem_keys { self.array_element_types.remove(&key); } self.current_receiver_type = saved_recv; self.sync_receiver_rt(); self.current_fn_return_ty = saved_ret_ty; self.expected_record_type = saved_expected; self.flush_boxed_vars(); self.indent -= 1; self.line("}"); self.line(""); // Plan 47 pattern: flush lambda decls before body let fn_body = std::mem::replace(&mut self.out, saved_out); self.indent = saved_indent; if !self.lambda_forward_decls.is_empty() { self.out.push_str(&std::mem::take(&mut self.lambda_forward_decls)); } if !self.lambda_impls.is_empty() { self.out.push_str(&std::mem::take(&mut self.lambda_impls)); } self.out.push_str(&fn_body); // Restore type substitution self.current_type_subst = saved_subst; self.current_fn_param_typerefs = saved_param_typerefs; // [M-property-testing-rot] self.current_emit_file_id = saved_emit_file_id_mono; // [M-sync-crossmodule…] D381 Ok(()) } /// All type parameters map to void*. The body is emitted with type params erased. fn emit_generic_fn_erased(&mut self, f: &FnDecl) -> Result<(), String> { // [race-198 class-closure]: см. override_maps_scope_enter doc. let ovr_saved = self.override_maps_scope_enter(); let r = self.emit_generic_fn_erased_scoped_inner(f); self.override_maps_scope_exit(ovr_saved, r.is_ok()); r } fn emit_generic_fn_erased_scoped_inner(&mut self, f: &FnDecl) -> Result<(), String> { let mangled = self.mangle_fn(f); let type_params: HashSet<String> = f.generics.iter().map(|g| g.name.clone()).collect(); // Build param types: bare T → void*, generic record T[U] → Nova_T* let param_c_tys: Vec<String> = f.params.iter().map(|p| { match &p.ty { TypeRef::Named { path, generics, .. } => { let name = path.join("_"); if type_params.contains(&name) { "void*".into() } else if !generics.is_empty() && self.record_schemas.contains_key(&name) { // Generic record type like Box[T] → Nova_Box* format!("Nova_{}*", name) } else { "void*".into() } } _ => "void*".into(), } }).collect(); let params_str = if f.params.is_empty() { "void".to_string() } else { f.params.iter().zip(¶m_c_tys).map(|(p, ty)| format!("{} {}", ty, p.name)).collect::<Vec<_>>().join(", ") }; // Plan 47: буферизуем тело — spawn-ctx typedefs (lambda_forward_decls) // должны флашиться ПЕРЕД телом. Иначе `spawn` внутри generic-функции // (например stdlib `within`/`race`) → ctx-typedef оказывается ПОСЛЕ // использования → "undeclared NovaSpawnCtx_*". emit_fn/emit_test уже // делают так; emit_generic_fn_erased — нет (был баг). let saved_out = std::mem::take(&mut self.out); let saved_indent = self.indent; self.indent = 0; self.line(&format!("{}void* {}({}) {{", self.top_level_storage(), mangled, params_str)); self.indent += 1; // Plan 143.2: prologue safepoint. Erased generic free-fn body inherits // the SOURCE template (`f`) KEEP-status — a recursive generic fn keeps // a safepoint in its erased form too. self.emit_prologue_preempt_check(f); // Register params with their concrete (or erased) types let saved: Vec<(String, Option<String>)> = f.params.iter().zip(¶m_c_tys) .map(|(p, ty)| (p.name.clone(), self.var_types.insert(p.name.clone(), ty.clone()))) .collect(); // Emit body with type erasure — the first param is returned for identity-like fns let emit_erased_return = |this: &mut Self, val: &str, val_ty: &str| { // For struct types: heap-allocate and return pointer // For scalar/pointer types: cast via intptr_t if val_ty.starts_with("_NovaTuple") || val_ty.starts_with("Nova_") || val_ty == "nova_str" || val_ty.starts_with("NovaOpt_") { let heap_tmp = this.fresh_tmp(); this.line(&format!("{ty}* {tmp} = ({ty}*)nova_alloc(sizeof({ty}));", ty = val_ty, tmp = heap_tmp)); this.line(&format!("*{} = {};", heap_tmp, val)); this.line(&format!("return (void*){};", heap_tmp)); } else { this.line(&format!("return (void*)(intptr_t)({});", val)); } }; match &f.body { FnBody::Expr(e) => { self.emit_source_annotation_for_expr(e); let val_ty = self.infer_expr_c_type(e); let val = self.emit_expr(e)?; emit_erased_return(self, &val, &val_ty); } FnBody::Block(block) => { // Plan 20 Ф.8 follow-up: defer scope для generic-erased fn body. // Без этого defer/errdefer внутри generic fn panic'ит codegen // ("defer/errdefer outside defer scope"). let block_id = self.enter_defer_scope(block, false); for stmt in &block.stmts { self.emit_stmt(stmt)?; } if let Some(trailing) = &block.trailing { self.emit_source_annotation_for_expr(trailing); let val_ty = self.infer_expr_c_type(trailing); let val = self.emit_expr(trailing)?; // Cleanup ДО return (defer body не должен влиять на val). self.leave_defer_scope(block_id); emit_erased_return(self, &val, &val_ty); } else { self.leave_defer_scope(block_id); self.line("return NULL;"); } } // D82: external — wrapper-эмиттер не вызывается для external fn. FnBody::External => {} } // Restore param types for (name, prev) in saved { match prev { Some(old) => { self.var_types.insert(name, old); } None => { self.var_types.remove(&name); } } } self.flush_boxed_vars(); self.indent -= 1; self.line("}"); self.line(""); // Plan 47: restore + flush spawn-ctx typedefs / lambda impls before body. let fn_body = std::mem::replace(&mut self.out, saved_out); self.indent = saved_indent; if !self.lambda_forward_decls.is_empty() { self.out.push_str(&std::mem::take(&mut self.lambda_forward_decls)); } if !self.lambda_impls.is_empty() { self.out.push_str(&std::mem::take(&mut self.lambda_impls)); } self.out.push_str(&fn_body); Ok(()) } /// Plan 33.1 Ф.4 (D24): emit ensures-checks для функции `f`. /// Вызывается после вычисления body, до return'а. Перед вызовом /// `result` должна быть зарегистрирована в var_types и быть /// доступной как обычная C-переменная `_nova_result`. fn emit_ensures_checks(&mut self, f: &FnDecl) -> Result<(), String> { // Plan 140 Ф.1 (D24 amend): эмитим безусловно — НЕ под // `#ifdef NOVA_CONTRACTS_RUNTIME`. Недоказанные ensures проверяются // и в release (enforce-with-elision); Z3-proven уже элидируются ниже // через `continue` на codegen (zero-cost). Plan 194 A2.1/A4: legacy // build-level `--contracts=off` И per-fn/module `#unchecked` opt-out // оба retired — ensures теперь ВСЕГДА enforced. // Подставляем `result` → `_nova_result` при emit'е выражения. // emit_expr на Ident("result") вернёт "result" (она в var_types), // нам нужно "_nova_result". Используем post-process подмену. for c in &f.contracts { if matches!(c.kind, ContractKind::Ensures) { // Plan 194 A2.2 (D421 §3): `#debug ensures` erased outside `checked`. if c.debug_only && self.mode_erases_debug() { continue; } // Plan 33.3 Ф.9.9: skip emit для proven контрактов (zero-cost). if self.proven_contracts.contains(&(f.name.clone(), c.span.start)) { continue; } // D.1.3: квантор не может быть проверен в runtime — пропускаем. if matches!(c.expr.kind, ExprKind::Forall { .. } | ExprKind::Exists { .. }) { continue; } let expr_c = self.emit_expr(&c.expr)?; // Простая подмена идентификатора `result` на `_nova_result`. // Работает для случаев без collision (что справедливо в 33.1). let expr_c_subst = Self::substitute_result_var(&expr_c); let expr_src = Self::expr_to_display(&c.expr); // Plan 140.1 Ф.2 (D24 amend): location-first format // `<file>:<line>: ensures failed: [<msg> (]<expr>[)]`. let (file_lit, line) = self.loc_for_span(c.span.start); // Plan 140.3: ensures supports interpolated messages too. The helper // rewrites `result` → `_nova_result` in the emitted message build for // POST (mirror of the condition's substitute_result_var above), so // `ensures result > 0, "got ${result}"` interpolates the return value. self.emit_contract_check( &expr_c_subst, "NOVA_CONTRACT_POST", &f.name, &expr_src, &file_lit, line, &c.message, &c.message_expr, )?; } } Ok(()) } /// Простая текстовая подмена идентификатора `result` → `_nova_result` /// в C-коде. Используется при emit'е ensures-выражений. Работает /// потому что `result` — magic-имя, не может конфликтовать с /// пользовательскими (Ф.2 валидирует это). fn substitute_result_var(c: &str) -> String { // Word-boundary replace через простой parser. let mut out = String::with_capacity(c.len()); let bytes = c.as_bytes(); let target = b"result"; let mut i = 0; while i < bytes.len() { let b = bytes[i]; let is_word = b.is_ascii_alphanumeric() || b == b'_'; // Если стартует с `result` и не word-continuation вокруг — заменяем. if i + target.len() <= bytes.len() && &bytes[i..i + target.len()] == target && (i == 0 || !(bytes[i-1].is_ascii_alphanumeric() || bytes[i-1] == b'_')) && (i + target.len() == bytes.len() || !(bytes[i+target.len()].is_ascii_alphanumeric() || bytes[i+target.len()] == b'_')) { out.push_str("_nova_result"); i += target.len(); continue; } out.push(b as char); i += 1; let _ = is_word; } out } /// Plan 140.3: like `substitute_result_var` but **skips the contents of C /// string literals** so an interpolated message's literal text (e.g. /// `"result was "`) is preserved byte-for-byte (incl. UTF-8) — only `result` /// identifiers in CODE become `_nova_result`. Used to rewrite an `ensures` /// interpolated-message build so `${result}` reads the collected return var. fn substitute_result_var_in_code(c: &str) -> String { let bytes = c.as_bytes(); let mut out: Vec<u8> = Vec::with_capacity(bytes.len()); let target = b"result"; let mut i = 0; while i < bytes.len() { let b = bytes[i]; // Copy a whole `"..."` string literal verbatim (respecting `\"`). if b == b'"' { out.push(b); i += 1; while i < bytes.len() { let c2 = bytes[i]; out.push(c2); i += 1; if c2 == b'\\' && i < bytes.len() { out.push(bytes[i]); i += 1; continue; } if c2 == b'"' { break; } } continue; } if i + target.len() <= bytes.len() && &bytes[i..i + target.len()] == target && (i == 0 || !(bytes[i-1].is_ascii_alphanumeric() || bytes[i-1] == b'_')) && (i + target.len() == bytes.len() || !(bytes[i+target.len()].is_ascii_alphanumeric() || bytes[i+target.len()] == b'_')) { out.extend_from_slice(b"_nova_result"); i += target.len(); continue; } out.push(b); i += 1; } String::from_utf8(out).unwrap_or_else(|_| c.to_string()) } /// Plan 33.3 Ф.9.1: walks block для сбора `ghost let` имён. /// Используется в codegen чтобы runtime-check'и (assert_static/assume) /// читающие ghost-vars не emit'ились в C (ghost эрейзится). fn collect_ghost_vars_in_block(b: &Block, out: &mut std::collections::HashSet<String>) { for stmt in &b.stmts { if let Stmt::Let(decl) = stmt { if decl.is_ghost { if let Pattern::Ident { name, .. } = &decl.pattern { out.insert(name.clone()); } } } } } /// Plan 33.3 Ф.9.1: проверка, использует ли expr ghost-var. /// Используется для skip runtime check в codegen для spec-positions /// (assert_static/assume/loop invariants). fn expr_uses_ghost(e: &Expr, ghost_vars: &std::collections::HashSet<String>) -> bool { match &e.kind { ExprKind::Ident(n) => ghost_vars.contains(n), ExprKind::Binary { left, right, .. } => { Self::expr_uses_ghost(left, ghost_vars) || Self::expr_uses_ghost(right, ghost_vars) } ExprKind::Unary { operand, .. } => Self::expr_uses_ghost(operand, ghost_vars), ExprKind::Member { obj, .. } => Self::expr_uses_ghost(obj, ghost_vars), ExprKind::Index { obj, index } => { Self::expr_uses_ghost(obj, ghost_vars) || Self::expr_uses_ghost(index, ghost_vars) } ExprKind::Call { func, args, .. } => { if Self::expr_uses_ghost(func, ghost_vars) { return true; } args.iter().any(|a| Self::expr_uses_ghost(a.expr(), ghost_vars)) } ExprKind::As(inner, _) | ExprKind::Is(inner, _) | ExprKind::Try(inner) | ExprKind::Bang(inner) => Self::expr_uses_ghost(inner, ghost_vars), ExprKind::Coalesce(l, r) => { Self::expr_uses_ghost(l, ghost_vars) || Self::expr_uses_ghost(r, ghost_vars) } _ => false, } } // Plan 91.10 (D163 retracted, 2026-05-30): emit_d163_external_stub удалён // вместе с D163 capability machinery. Если в будущем понадобится stub // generation для test scaffolding non-stdlib external fn — вводить через // effect-based mechanism. // Plan 143.2 [M-opt-leaf-preempt-entry-elision]: emit the function-prologue // preemption safepoint (Plan 44.7) — the first statement of every emitted // Nova function — consulting the whole-program KEEP set. The check is // ELIDED only on a function the pre-pass proved leaf (not on a call-graph // cycle, no indirect/closure/fn-ptr call, no FFI/extern call, not // address-taken). CONSERVATIVE: when the pre-pass never ran // (`populated()==false`, e.g. a CEmitter built directly in a unit test) we // KEEP unconditionally. Used by emit_fn AND by every on-demand emit path // that lowers a *source* FnDecl (monomorphized / erased generic // fn+method) — the source-template key is the same one the pre-pass // registered, so KEEP-status is inherited by all instantiations (sound // over monomorphization). fn emit_prologue_preempt_check(&mut self, f: &FnDecl) { let keep_preempt = !self.preempt_keep.populated() || self .preempt_keep .must_keep(&crate::codegen::preempt_keep::fn_key(f)); if keep_preempt { self.line("nova_preempt_check();"); } else { self.line("/* preempt-check elided: provably-leaf (Plan 143.2) */"); } } // Plan 143.2: unconditional prologue safepoint for emitted code that has NO // source-level FnDecl in the pre-pass universe (lambdas / closures / // trailing-block bodies). These are reachable only through INDIRECT calls // (a fn-pointer / closure value), which the pre-pass already forces the // *caller* to KEEP — but the closure body itself is its own C function and // must carry a safepoint too (a closure stored and re-invoked could form an // unbounded cycle the source analysis cannot see). Conservative default // per acceptance criterion #6: never elide an unproven case. fn emit_prologue_preempt_check_unconditional(&mut self) { self.line("nova_preempt_check();"); } /// [race-198 class-closure 2026-07-13] Per-function scoping for the two /// shared inference-override maps (`closure_param_type_overrides`, /// `pattern_binding_overrides`). Both are consulted FIRST — before /// `var_types` — by `recv_c_type_materialized` / `infer_expr_c_type`, so /// any entry that survives past its intended insert/remove pair (early /// return between the pair, unbalanced nesting on same-named params, a /// `?`-propagated error that a caller recovers from, …) silently /// SHADOWS the correct `var_types` entry for every LATER function in the /// same CU that uses the same identifier. In a merged folder-module CU /// (~1000 files) this manifested as auto-derived `@debug`/`@display` /// bodies (`w: Write` → `Nova_StringBuilder*`) dispatching `w.write_str` /// to a FOREIGN receiver type (`Nova_TcpStream_method_write_str`) — /// reading a field at a foreign struct offset → composition-dependent /// access violation (the Plan 198 floating-AV blocker; full localization /// in docs/plans/196-race-state-dump-notes.md). Same defect class as the /// documented Plan 139.2 `var_types`-not-per-fn-scoped case /// (docs/dev/debugging-races.md §6.4) — closed HERE as a CLASS (playbook /// precedent), not by hunting the single unbalanced insert/remove site /// (~30 candidate pairs, and a new one could regress tomorrow). /// /// Usage: every top-level function-body emission entry point wraps its /// body in `enter`/`exit`. `enter` empties both maps (a fresh function /// body legitimately starts with NO ambient overrides — closure params / /// pattern bindings are strictly body-local) and returns the previous /// contents; `exit` restores them, so LAZY re-entrant emission (a mono /// drain or default-method synthesis triggered MID-body of an outer /// function) hands the outer context back exactly what it had. fn override_maps_scope_enter( &self, ) -> (HashMap<String, String>, HashMap<String, String>) { ( std::mem::take(&mut *self.closure_param_type_overrides.borrow_mut()), std::mem::take(&mut *self.pattern_binding_overrides.borrow_mut()), ) } /// Counterpart of `override_maps_scope_enter`. `emitted_ok` gates the /// leak point-probe: on a successful emission both maps MUST have /// drained back to empty (every insert paired with its remove) — a /// non-empty map here is precisely the cross-function leak this scoping /// contains, so surface it loudly in debug builds (release: contained /// and discarded by the restore below either way). On a failed emission /// (caller aborts or rolls back) intermediate entries are expected. fn override_maps_scope_exit( &self, saved: (HashMap<String, String>, HashMap<String, String>), emitted_ok: bool, ) { if emitted_ok { debug_assert!( self.closure_param_type_overrides.borrow().is_empty(), "closure_param_type_overrides leaked past a function-body \ emission (unbalanced insert/remove): {:?}", self.closure_param_type_overrides .borrow() .keys() .collect::<Vec<_>>() ); debug_assert!( self.pattern_binding_overrides.borrow().is_empty(), "pattern_binding_overrides leaked past a function-body \ emission (unbalanced insert/remove): {:?}", self.pattern_binding_overrides .borrow() .keys() .collect::<Vec<_>>() ); } *self.closure_param_type_overrides.borrow_mut() = saved.0; *self.pattern_binding_overrides.borrow_mut() = saved.1; } fn emit_fn(&mut self, f: &FnDecl) -> Result<(), String> { // [race-198 class-closure]: см. override_maps_scope_enter doc. let ovr_saved = self.override_maps_scope_enter(); // Реестр 221.1 №577/№592 (маркер [M-array-ext-static-erased-body-no- // generic-dispatch], CLOSED by №592): a STATIC array-ext method with // its OWN generic bound to the receiver's element (`fn[T Reflect] []T // .reflect() -> TypeShape => Arr(T.reflect())`) used to be emitted // ONCE, erased, under a name that LOOKED like a genuine `[]int` // instantiation (`receiver_type_c_ident`'s `_ => "nova_int"` catch- // all) but served every element — wrong for any non-int element. // `emit_fn_scoped_inner`'s top-of-fn dispatch now skips this shape's // erased emission entirely (both instance and static), in favor of a // real per-element mono driven by `array_ext_static_generic_fn` + // the `Path(["__array", elem])` call site (D38) — no seeding needed // here anymore, the seed used to matter only for the erased body this // function no longer reaches for this shape. let r = self.emit_fn_scoped_inner(f); self.override_maps_scope_exit(ovr_saved, r.is_ok()); r } fn emit_fn_scoped_inner(&mut self, f: &FnDecl) -> Result<(), String> { // D82: external fn — Nova body отсутствует, реализация в nova_rt/. // Skip emit'инг полностью: dispatch на C-функцию делается в emit_call. // Plan 91.10 (D163 retracted): D163 stub generation удалён. if f.is_external { return Ok(()); } if f.name == "main" { return self.emit_nova_main(f); } // Plan 95 Ф.3: parallel со skip в `emit_fn_forward_decl` — тело // Nova-body метода на builtin sum-типе эмитится **только** через // worklist drain (`emit_monomorphized_method`) per-T. Регулярный // emit использовал бы erased `Nova_Option*` (incomplete) и // конфликтовал бы с правильным mono-эмитом по C-имени. if let Some(recv) = &f.receiver { if matches!(recv.type_name.as_str(), "Option" | "Result") { return Ok(()); } } // Plan 33.3 Ф.9.1: collect ghost-var names в body для runtime-check // skip в assert_static/assume/loop-invariant. self.ghost_vars.clear(); if let FnBody::Block(b) = &f.body { Self::collect_ghost_vars_in_block(b, &mut self.ghost_vars); } // Plan 48: Generic free functions → monomorphized on demand; skip erased body. if !f.generics.is_empty() && f.receiver.is_none() { // FnDecl already stored in mono_fn_decls during forward-decl phase. return Ok(()); } // Plan 48: Generic methods with own type params → monomorphized on demand; skip body. // Exception: array extension methods ([]T receivers) never get monomorphized — fall // through to regular emit. fn_param_sigs registration below erases unknown type params. if !f.generics.is_empty() && f.receiver.is_some() { let is_array_ext = f.receiver.as_ref().map_or(false, |r| r.type_name.starts_with("[]")); if !is_array_ext { // FnDecl stored in mono_method_decls during forward-decl phase. return Ok(()); } // [196.5 ДЕФЕКТ-2, record_schemas-гэп] A PREFIX-generic slice-ext method // (`fn[T] []T @method` — the receiver ELEMENT is itself a declared // fn-prefix type-param) is mono-only since Plan 101.1: call-sites route // through the mono sentinel (see the mono_method_decls registration in // the forward-decl pass) and per-elem bodies come from // emit_monomorphized_method with `current_type_subst[T]` bound. The // legacy ERASED fall-through emission below additionally emitted the // body ONCE with NO subst — dead weight (nothing references the // `Nova_NovaArray_nova_int_method_<m>` erased symbol) that POISONS the // mono registries: `Vec[T].new()` inside such a body lowers `T` → // `Nova_T*`, registering an erased `Vec____Nova_T_p` generic-type // instance (refused by the type-drain guard → never in record_schemas) // PLUS an erased `..._static_new` METHOD instance (no equivalent // guard) whose body emission then fails loud: "expected struct // 'Vec____Nova_T_p' not in record_schemas". Skip the erased emission // for exactly this shape — mono is a PHASE, not a codegen side-effect // (rustc-collector analogy); generic templates are never emitted // erased. CONCRETE-receiver ext methods (`fn []int @max`, generics // empty) and method-own generics over a concrete slice receiver // (`fn[U] []int @m`) keep the legacy fall-through unchanged. // Реестр 221.1 №592 (was: "INSTANCE receivers only… STATIC // `fn[T] []T.m(...)` … has no mono routing yet"): it now does — // `array_ext_static_generic_fn` + the `Path(["__array", elem])` // call site (D38) monomorphize a fresh body per concrete element // via `register_mono_method_instance`/`emit_monomorphized_method`, // the SAME worklist-drain machinery this INSTANCE case already // used. Skip the erased single-shot emission for BOTH receiver // kinds now — a STATIC blanket left un-skipped would still emit // ONE dead `T`-defaulted-to-`nova_int` body no call site links // against anymore (harmless but wasted, and risks the exact same // mono-registry poisoning the instance case was already guarded // against above). let recv_elem = f.receiver.as_ref() .and_then(|r| r.type_name.strip_prefix("[]")) .unwrap_or(""); if f.generics.iter().any(|g| g.name == recv_elem) { return Ok(()); } // Fall through to regular emit path. } if let Some(recv) = &f.receiver { // Array extension methods ([]T, []str, etc.) are emitted directly — not erased. // They use a concrete NovaArray_nova_int receiver, not a generic struct. let is_array_ext = recv.type_name.starts_with("[]"); if !recv.generics.is_empty() && !is_array_ext { if matches!(recv.kind, ReceiverKind::Static) { // Static methods on generic types: emit minimal stub. // Static constructors (new, from, with_capacity) are always // called from concrete/monomorphized contexts. The erased body // for complex methods generates invalid C (tuple destructuring, // TurboFish calls to other generic methods with K/V unknowns). return self.emit_generic_static_method_stub(f); } // Instance methods on generic types. Concrete-instance code // is emitted via drain_generic_type_worklist → // emit_generic_type_instance whenever the type is monomorphized // (Plan 48 Ф.3). The erased emit below remains as the V1 // fallback for code paths the mono pipeline doesn't yet cover: // bare unit-variant references like `let r = Err2` where T // cannot be inferred from the constructor alone. Ф.7.4 is // therefore kept partial — full removal blocks on usage-context // inference for unit variants (tracked as V2 follow-up). return self.emit_generic_method_erased(f); } } // [M-sync-crossmodule…] (D381): establish THIS fn's declaring-file context // BEFORE the signature (return type + params) is lowered, so a colliding // param/return type (`ErrorKind` in std.io's `IoError.of`) resolves to its // module-qualified base. The legacy set happened AFTER the signature (~180 // lines below), so the body was fine but the SIGNATURE emitted a bare // `Nova_ErrorKind`. GATED on a collision existing so a non-colliding CU // keeps the original context (unset here) while the signature is lowered — // `ref_type_base` is a no-op then and the return-inference file-private fn // resolution stays exactly as before → byte-identical. `saved_emit_file_id` // captures the TRUE original for the end-of-body restore. let saved_emit_file_id = self.current_emit_file_id; if self.any_type_file_collision() { self.current_emit_file_id = Some(f.span.file_id); } // Set receiver type FIRST so Self resolves correctly in return_type_c/params_c if let Some(recv) = &f.receiver { self.current_receiver_type = Some(recv.type_name.clone()); self.sync_receiver_rt(); // Plan 135 Ф.2: track whether current method has a mut receiver // so that SelfAccess call-sites can tiebreak __mut/__ro overloads. // №462/№370: владеющий (`consume`) получатель может мутировать — // `mutable` и `consume` взаимоисключающие, поэтому одиночное чтение [INV-TODO: №462] // `.mutable` считало владельца за `ro`. self.current_receiver_is_mut = recv.mutable || recv.consume; // [M-static-selfreturn-value-mangle-conflict] (Plan 172.13): see // field doc — definition side of the static/instance distinction // (mirrors the forward-decl site above, ~11603). self.current_receiver_is_static = matches!(recv.kind, ReceiverKind::Static); // D215: pre-register nova_self type before return_type_c so that // SelfAccess in `=> expr` bodies (e.g. `@real() => @re`) resolves // the correct C type for field-access inference. Without this, // the first instance method on a named tuple gets nova_int for SelfAccess // because var_types["nova_self"] is only populated at the params loop // (line ~16191), after return_type_c is called. The definition then // conflicts with the forward decl if both used different paths. if matches!(recv.kind, ReceiverKind::Instance) { let recv_c = self.receiver_c_type(&recv.type_name, recv.mutable); self.var_types.insert("nova_self".into(), recv_c); } } else { self.current_receiver_type = None; self.sync_receiver_rt(); self.current_receiver_is_mut = false; self.current_receiver_is_static = false; } // Seed param types before inferring a bodyless `-> T` return type (same // reason as the forward-decl path ~9484): the params loop populates // var_types only AFTER return_type_c, so a trailing `cv.notify_one()` // would see `cv` as nova_int → method lookup miss → return mistyped // nova_int, conflicting with the unit-typed forward decl. Restore after. let mut ret_seed_saved: Vec<(String, Option<String>)> = Vec::new(); // D43: fn-typed params may have stale fn_param_sigs entries from a previously // emitted function with the same param name (e.g. `d43_run` sets `body → // nova_int`, then `d43_run_unit`'s return_type_c picks up the wrong entry via // legacy fn_param_sigs lookup). Seed the correct signature before return_type_c // and restore after — matches what the params loop computes at line ~17199. let mut fn_sigs_seed_saved: Vec<(String, Option<(Vec<String>, String)>)> = Vec::new(); if f.return_type.is_none() { for p in &f.params { if let Ok(pc) = self.type_ref_to_c(&p.ty) { if !pc.is_empty() { ret_seed_saved.push((p.name.clone(), self.var_types.get(&p.name).cloned())); self.var_types.insert(p.name.clone(), pc); } } if let Some(_ft) = self.resolve_fn_typeref(&p.ty) { let crate::ast::TypeRef::Func { params: fp, return_type: fn_ret, .. } = &_ft else { unreachable!() }; let param_c_tys: Vec<String> = fp.iter() .map(|t| self.type_ref_to_c(t).unwrap_or_else(|_| "nova_int".into())) .collect(); let ret_c = match fn_ret { Some(rt) => self.type_ref_to_c(rt).unwrap_or_else(|_| "nova_int".into()), None => "nova_unit".into(), }; fn_sigs_seed_saved.push((p.name.clone(), self.fn_param_sigs.get(&p.name).cloned())); self.fn_param_sigs.insert(p.name.clone(), (param_c_tys, ret_c)); } } } let mut ret = self.return_type_c(f)?; for (n, prev) in ret_seed_saved { match prev { Some(pp) => { self.var_types.insert(n, pp); } None => { self.var_types.remove(&n); } } } for (n, prev) in fn_sigs_seed_saved { match prev { Some(p) => { self.fn_param_sigs.insert(n, p); } None => { self.fn_param_sigs.remove(&n); } } } // Plan 72 P3-B return: protocol return type (`-> Iter[int]`) → the C // function returns a `NovaBox_*` fat pointer; trailing/explicit return // values are boxed (see `wrap_protocol_return`). let saved_fn_returns_protocol = self.current_fn_returns_protocol.take(); if let Some((proto, type_args)) = self.protocol_box_return_type_info(f) { if let Some(box_ty) = self.emit_protocol_box_typedef(&proto, &type_args) { ret = box_ty; self.current_fn_returns_protocol = Some((proto, type_args)); } } // Plan 174.3 (D53): `any` return type — concrete return values are boxed // (implicit upcast). `ret` is already `void*` via `return_type_c`. let saved_fn_returns_any = self.current_fn_returns_any; self.current_fn_returns_any = matches!(&f.return_type, Some(TypeRef::Named { path, generics, .. }) if generics.is_empty() && path.len() == 1 && path[0] == "any"); // Plan 55 Ф.4: save/restore current_fn_return_ty чтобы прошлый // ret не leak'ал при recursive emit (e.g. mono pass запускает // emit_fn для transitively'd dependencies из тела generic). let saved_ret_ty = std::mem::replace( &mut self.current_fn_return_ty, Some(ret.clone()), ); // Plan 63 Fix F+ [M-result-erased-no-mono]: pending_*_inner_type // используют scope-leaking pattern (set при boxing, consume при // next let). Если функция body содержит `Ok(...)` БЕЗ surrounding // let-binding (e.g. fn parse_kv() -> Result[..] { if cond { Ok((..)) } }), // pending leak'ает в caller'а: после emit_fn(parse_kv) pending // содержит inner type из последнего Ok(...) → следующий // `let r = parse_kv(...)` consumed бы это leaked значение вместо // правильного типа. Save/restore pending на fn body boundary. let saved_pending_result = self.pending_result_ok_inner_type.take(); let saved_pending_option = self.pending_option_inner_type.take(); // [M-176-conformance-cu-map-closure]: `var_mutable` was not scoped per // fn body (only test-body/spawn scopes saved it). A `mut f` local in one // fn therefore leaked its mutability into `var_mutable` and, when a LATER // fn's lambda captured a same-named but IMMUTABLE free var (e.g. the // fn-typed `f` param of `BoxIter[T].map`), `emit_lambda` mis-classified it // as a by-ref MUT capture (env field `T** f`, boxed by `&`, registered in // `var_boxed` with NO unpack local). The closure-CALL `f(x)` // (`NOVA_CLOS_CALL_*`) does not consult `var_boxed`, so it emitted a bare // `f` — undeclared in the body fn. The misfire only surfaced when another // module in the same CU (e.g. std.fs) contributed a `mut f`, which is why // it read as a CU-composition bug. Scope `var_mutable` per fn body: start // empty (this fn's own `mut` lets accumulate during emit, so genuine // by-ref captures like the map's `mut src` still classify correctly), // restore the caller's set at exit. Symmetric with test-body/spawn. let saved_var_mutable_fn = std::mem::take(&mut self.var_mutable); // Plan 172.5 (D326 R5): register this fn's `ro ref`/`mut ref` params so // body uses of their names auto-deref (`name` → `(*name)`). Scoped per // fn body (restored at exit) so a nested/sibling fn is unaffected. let saved_ref_params_fn = std::mem::take(&mut self.ref_params); // Plan 248 (wave 3): scoped/restored the same way, same reason — // see the field's own doc. let saved_mut_param_names_fn = std::mem::take(&mut self.mut_param_names); for (p_idx, p) in f.params.iter().enumerate() { if p.is_mut && !p.consume { // Plan 184 Р10: a value/primitive `mut x T` param is by-pointer // in-out; its body reads/writes auto-deref (`name` → `(*name)`). // Compute the param C type exactly as `params_c` does. let ty_c = if let Some((box_ty, _, _)) = self.protocol_box_c_type_for(&p.ty) { box_ty } else { self.type_ref_to_c(&p.ty).unwrap_or_default() }; if Self::param_is_inout_ptr(p, &ty_c) { self.ref_params.insert(p.name.clone()); } // Plan 248 (wave 3, third mega-CU regression, // [M-detach-capture-mut-param-not-in-var-mutable]): register in // the NARROW `mut_param_names` set (below), NOT the general // `var_mutable` — see that field's own doc for why: a broad // `var_mutable.insert` here was tried first and reverted, because // `var_mutable` also drives OTHER, unrelated capture decisions // (e.g. handler-literal capture, ~line 12395: "immutable scalar // (function param / let) → by-value snapshot... critical for // factory pattern where the literal escapes the fn — a stack- // local pointer would be dangling") that DELIBERATELY treat a // captured PARAM as by-value specifically to avoid a dangling // pointer once the function returns — flipping that for EVERY // `var_mutable` consumer risked trading the detach/mut-param bug // for a handler-literal dangling-pointer regression elsewhere. self.mut_param_names.insert(p.name.clone()); } else if f.receiver.is_none() && self.free_fn_byref_flag(&f.name, p_idx) { // Plan 172.14 Ф.1: большой ro value-struct параметр — by-ref // `T*`; чтения в теле auto-deref (`name` → `(*name)`), как у // Р10. Записей нет по построению (ro). self.ref_params.insert(p.name.clone()); } else if let Some(recv) = &f.receiver { // [M-172.14-methods-byref]: то же auto-deref для METHOD // value-параметра. if self.method_byref_flag(&recv.type_name, &f.name, p_idx) { self.ref_params.insert(p.name.clone()); } } } let params = self.params_c(f)?; // Plan 170 (D307): set the current emission file for the whole fn BODY so // call-sites to file-private helpers declared in THIS file resolve to the // right file-discriminated C symbol (free_fn_c_name reads this). Unconditional // (byte-identical with legacy); for a colliding CU it merely re-affirms the // gated top-of-fn set. Restored at the end via `saved_emit_file_id` // (captured as the TRUE original above, D381). self.current_emit_file_id = Some(f.span.file_id); let mangled = self.mangle_fn(f); // Plan 172.14 (sret/_out §1-2): классификация sret-eligible по форме. // Первая (classic) эмиссия регистрирует реестр; режим sret_variant_emit // (вторая эмиссия, взводится в конце этой fn) меняет имя на `__sret`, // добавляет параметр `S* _out` и активирует sret_fn_out на тело. let fn_sret_eligible = f.receiver.is_some() && self.sret_fn_eligible(f, &ret); if fn_sret_eligible && !self.sret_variant_emit { self.sret_fns.insert( mangled.clone(), ret.trim_end_matches('*').trim().to_string()); } let mangled = if self.sret_variant_emit { format!("{}__sret", mangled) } else { mangled }; // Plan 63 Fix F+ [M-result-erased-no-mono]: register fn's Result Ok // payload mono'd type. Enables call-site propagation (let r = parse_kv()) // и inline match без let-binding. if let Some(rt) = &f.return_type { self.register_fn_result_ok_inner_type(&mangled, rt); } // Register param types in var_types for match/infer // Plan 72 P0: save protocol_vars state before fn body (params may add protocol-typed entries) let saved_protocol_vars_fn = self.protocol_vars.clone(); // Plan 72 P1-C: save result_type_params state before fn body let saved_result_type_params_fn = self.result_type_params.clone(); // Plan 72 P3-B: save protocol_var_vtable state before fn body let saved_protocol_var_vtable_fn = self.protocol_var_vtable.clone(); if let Some(recv) = &f.receiver { if matches!(recv.kind, ReceiverKind::Instance) { // Plan 128 Ф.1: thread recv.mutable (Ф.2 consumes). self.var_types.insert("nova_self".into(), self.receiver_c_type(&recv.type_name, recv.mutable)); } } for p in &f.params { if let Ok(mut ty_c) = self.type_ref_to_c(&p.ty) { // Plan 72 P0 (E7201): track protocol-typed params before ty_c is moved if let Some(proto_name) = self.extract_protocol_type_name(&p.ty) { self.protocol_vars.insert(p.name.clone(), proto_name); } // Plan 72 P3-B: a protocol-typed parameter is a `NovaBox_*` fat // pointer — `var_types` must reflect that so method calls on it // dispatch via `.vtable` instead of emitting E7201. if let Some((box_ty, _, _)) = self.protocol_box_c_type_for(&p.ty) { ty_c = box_ty; } // Plan 72 P1-C: track Result[T,E]-typed params if let Some(result_params) = self.extract_result_type_params(&p.ty) { self.result_type_params.insert(p.name.clone(), result_params); } else { self.result_type_params.remove(&p.name); } self.var_types.insert(p.name.clone(), ty_c); // Register function-typed params so body() calls emit proper function pointer calls // [D52-амендмент, ОКНО-5]: `resolve_fn_typeref` ALSO recognizes a // newtype-over-fn (`Handler`)/alias-of-fn param here — this is the // primary site `next(req)` call-through (222.4 §4а) hits for a // plain top-level `fn`. if let Some(_ft) = self.resolve_fn_typeref(&p.ty) { let TypeRef::Func { params: fp, return_type, .. } = &_ft else { unreachable!() }; // Erase unknown Nova pointer types (Nova_T*, Nova_U*, etc.) to nova_int. // These appear in erased contexts like array extension methods (fn []T @map[U]) // where T and U are type params, not real Nova record/sum types. let erase_unk = |c: String| -> String { self.debt_erase_unknown_nova_ptr(c) }; // Plan 70 Cat B (intentional erasure): erase_unk нормализует // unknown→nova_int для consistent pointer-stomping в emit_generic_* // (erased generics). type_ref_to_c fail здесь = type-param ещё не // mono'd, erase_unk применяет к "nova_int" → tracks эту erasure. // Strict-mode false-positive — это namespace squat для erased dispatch. let param_c_tys: Vec<String> = fp.iter() .map(|t| erase_unk(self.type_ref_to_c(t).unwrap_or_else(|_| "nova_int".into()))) .collect(); let ret_c = match return_type { Some(rt) => erase_unk(self.type_ref_to_c(rt).unwrap_or_else(|_| "nova_int".into())), None => "nova_unit".into(), }; self.fn_param_sigs.insert(p.name.clone(), (param_c_tys, ret_c)); // [fix M-nested-fn-newtype-bind-then-call-broken, реестр // 221.1 №78, форма 2]: this param's OWN call sig (just // above) says nothing about what CALLING it returns being // itself callable — needed when the param's fn-newtype // RETURN is itself a (possibly nested) fn-newtype (`m Mid` // where `Mid = fn(Hnd) -> Hnd`). No save/restore here, // matching the un-scoped `fn_param_sigs.insert` two lines // above (this whole per-param loop is not save/restored // either — see `nested_fn_return_sig` doc for the full // failure mode this closes: bogus `nova_fn_h2` link error). if let Some(sig) = self.nested_fn_return_sig(&_ft) { self.fn_returns_fn_sig.insert(p.name.clone(), sig); } } // Register element type for array params of non-primitive types if let TypeRef::Array(inner, _) = &p.ty { // Plan 55 Ф.1: `[]fn(P...) -> R` param → record element-closure // signature so emit_for can register loop var in fn_param_sigs. if let TypeRef::Func { params: fp, return_type, .. } = inner.as_ref() { // Plan 70 PhaseA2: strict — emit_fn array-of-fn param element sig let inner_ptys: Vec<String> = fp.iter() .map(|t| self.type_ref_to_c(t).map_err(|e| self.err_no_int_fallback( &format!("fn `{}` array-of-fn param `{}` element param", f.name, p.name), &e, ))) .collect::<Result<Vec<_>, _>>()?; let inner_ret = match return_type.as_ref() { Some(rt) => self.type_ref_to_c(rt).map_err(|e| self.err_no_int_fallback( &format!("fn `{}` array-of-fn param `{}` element return", f.name, p.name), &e, ))?, None => "nova_unit".to_string(), }; self.array_param_fn_sigs.insert(p.name.clone(), (inner_ptys, inner_ret)); } else if let Ok(elem_ty) = self.type_ref_to_c(inner) { if elem_ty != "nova_int" && elem_ty != "nova_bool" && elem_ty != "nova_f64" && elem_ty != "nova_f32" && elem_ty != "nova_str" && elem_ty != "nova_char" && elem_ty != "nova_byte" { // Arrays store tuples and structs as heap pointers let stored_ty = if elem_ty.starts_with("_NovaTuple") && !elem_ty.ends_with('*') { format!("{}*", elem_ty) } else { elem_ty }; self.array_element_types.insert(p.name.clone(), stored_ty); } } } } } // Buffer the function body so lambdas can be prepended before it let saved_out = std::mem::take(&mut self.out); let saved_indent = self.indent; self.indent = 0; // Plan 172.14 (sret/_out §2): __sret-вариант — доп. параметр `S* _out`. let params = if self.sret_variant_emit && fn_sret_eligible { if params == "void" { format!("{} _out", ret) } else { format!("{}, {} _out", params, ret) } } else { params }; self.line(&format!("{}{} {}({}) {{", self.top_level_storage(), ret, mangled, params)); self.indent = 1; // Plan 44.7: preemption safepoint. First statement of every Nova // function — a TLS-flag check that cooperatively yields when the // M:N sysmon flagged this worker for an overrun. No-op (≈1 cycle, // predicted-not-taken) in single-thread mode where the flag is // never raised. Together with the loop-backedge check this gives // observable Go-style preemption: a CPU-bound fiber can't starve // its peers even with no explicit runtime.yield(). // // Plan 143.2 [M-opt-leaf-preempt-entry-elision]: ELIDE this prologue // check on provably-leaf functions. A fiber can run unbounded without // yielding only via a loop (its OWN back-edge check is preserved // separately, emit_loop_body_inline_ex) or via recursion (a call-graph // cycle). The whole-program pre-pass (`preempt_keep`, computed in // emit_module) marks a fn KEEP iff it is on a cycle, makes an // indirect/closure/fn-ptr call, makes an FFI/extern call, or is // address-taken. CONSERVATIVE: when the pre-pass did not run // (`populated()==false`, e.g. direct CEmitter construction in unit // tests) we KEEP unconditionally — never elide under doubt. self.emit_prologue_preempt_check(f); // Plan 173 Ф.5 (#8, D188 R2 exactly-once): prologue guard in the // generated `Nova_<T>_consume_cleanup` — the single chokepoint EVERY // invocation path funnels through (the consume-scope's own dispatch // AND a manual call smuggled past the compile-time D188-r2 checker // through a function boundary / FFI). Second invocation on the same // instance = D188 R2 violation → loud panic, not silent double-run. // Gated on `consume_ccount_structs` (struct actually carries the // hidden field) so the guard can never reference a missing member. if f.name == "cleanup" { if let Some(recv) = &f.receiver { if recv.consume && matches!(recv.kind, ReceiverKind::Instance) && self.consume_ccount_structs.contains(&Self::receiver_type_c_ident(&recv.type_name)) { self.line("if (nova_self && nova_self->_consume_ccount >= 1) { nv_panic(nova_str_from_cstr(\"D188-on-exit-double-invocation\")); }"); self.line("if (nova_self) { nova_self->_consume_ccount += 1; }"); } } } let saved_expected = self.expected_record_type.clone(); self.expected_record_type = Self::debt_struct_name_from_c_type(&ret); // Plan 33.1 Ф.4 (D24): emit contracts. // Plan 140 Ф.1 (D24 amend): «enforce-with-elision» — недоказанные // контракты эмитятся БЕЗУСЛОВНО (debug И release), Z3-proven // элидируются на codegen через `continue` (zero-cost). Прежняя // модель «в release стираются» (NDEBUG/assert) — retracted. // Plan 194 A4 (ретракт `#unchecked`): per-fn/module opt-out убран — // `contracts_elided_for(kind)` константно `false` (requires/ensures // ВСЕГДА enforced, кроме Z3-proven-elided сайтов через `continue`). // №172: `#unverified` гасит только SMT-верификацию, НЕ runtime-emit. let verifiable = !f.contracts.is_empty(); let has_contracts = verifiable && !self.contracts_elided_for(ContractKind::Requires); // Plan 33.3 Ф.9.4 (D24): `decreases <expr>` для fn → recursion-depth // guard. Каждый entry в fn инкрементит thread-local counter; если // превышает порог (10000) — runtime panic. Это catches infinite // recursion в debug. Полный well-founded check (m_new < m_old) — // ждёт SMT (Z3 backend). let _depth_var = if f.decreases.is_some() && !self.contracts_elided_for(ContractKind::Requires) { // Sanitize fn name для C-identifier. let san: String = f.name.chars() .map(|c| if c.is_ascii_alphanumeric() { c } else { '_' }) .collect(); let var = format!("_nova_decreases_depth_{}", san); // Declare thread-local counter BEFORE fn (на file-scope). // Делаем через separate preamble line — emit'им сюда не можем, // используем static local внутри fn (init=0, sticky между calls). self.line(&format!("static int {} = 0;", var)); // Plan 140 Ф.1 (D24 amend): эмитим безусловно (enforce-in-release). // Plan 55 Ф.7: lower limit to 10000 чтобы trigger ДО stack // overflow (frame ~1KB debug × 1M limit > stack 1MB — никогда // не triggered, был latent bug). 10K — safe (stack ~10MB позволяет). // Plan 140.1 Ф.2 (D24 amend): location-first format; no user msg. let (file_lit, line) = self.loc_for_span(f.span.start); self.line(&format!("if ({}++ > 10000) nova_contract_violation(NOVA_CONTRACT_PRE, \"{}\", \"decreases recursion depth exceeded 10000\", \"{}\", {}, NULL);", var, f.name, file_lit, line)); Some(var) } else { None }; // emit requires checks // Plan 140 Ф.1 (D24 amend): эмитим безусловно — НЕ под // `#ifdef NOVA_CONTRACTS_RUNTIME`. Недоказанные requires проверяются // и в release (enforce-with-elision); Z3-proven элидируются ниже через // `continue` (zero-cost). Plan 194 A4: per-fn/module `#unchecked` // opt-out retired. if has_contracts { for c in &f.contracts { if matches!(c.kind, ContractKind::Requires) { // Plan 194 A2.2 (D421 §3): `#debug requires` erased outside `checked`. if c.debug_only && self.mode_erases_debug() { continue; } // Plan 33.3 Ф.9.9: skip emit для proven контрактов // (true zero-cost). proven_contracts — set от // VerificationPipeline. Key: (fn_name, span.start). if self.proven_contracts.contains(&(f.name.clone(), c.span.start)) { continue; } // D.1.3: квантор не может быть проверен в runtime — пропускаем. if matches!(c.expr.kind, ExprKind::Forall { .. } | ExprKind::Exists { .. }) { continue; } let expr_c = self.emit_expr(&c.expr)?; let expr_src = Self::expr_to_display(&c.expr); // Plan 140.1 Ф.2 (D24 amend): location-first format // `<file>:<line>: requires failed: [<msg> (]<expr>[)]`. let (file_lit, line) = self.loc_for_span(c.span.start); self.emit_contract_check( &expr_c, "NOVA_CONTRACT_PRE", &f.name, &expr_src, &file_lit, line, &c.message, &c.message_expr, )?; } } } // Plan 113 (D172): set in_realtime / in_blocking for fn bodies so enforcement // checks fire correctly inside #realtime fn / #blocking fn bodies. let prev_in_realtime = self.in_realtime; let prev_in_blocking = self.in_blocking; if !matches!(f.realtime_attr, crate::ast::RealtimeAttr::None) { self.in_realtime = true; } if f.blocking_attr { self.in_blocking = true; } // Plan 127 Ф.3: track current fn-id for escape-result lookup при // emit_let / emit_record_lit. Format must match // `escape_analyze::fn_id` (free fn → name; method → `<recv>::<name>`). // Cleared at function-body emit end (parallel to in_realtime). let prev_fn_id = self.current_fn_id.replace(if let Some(recv) = &f.receiver { format!("{}::{}", recv.type_name, f.name) } else { f.name.clone() }); let prev_promoted_locals = std::mem::take(&mut self.promoted_value_record_locals); let prev_promoted_prim_locals = std::mem::take(&mut self.promoted_primitive_locals); // emit body — collect into _nova_result if ensures present // Plan 140.3: ensures gated INDEPENDENTLY from requires (has_contracts). let has_ensures = verifiable && !self.contracts_elided_for(ContractKind::Ensures) && f.contracts.iter().any(|c| matches!(c.kind, ContractKind::Ensures)); match &f.body { FnBody::Expr(e) => { self.emit_source_annotation_for_expr(e); // Plan 172.1 [M-172.1-some-target-coerce]: an arrow-body `=> expr` return // coerces the expr TO the return type for the NovaOpt_<X>/typed-int surface, // so `=> Some(<int-literal>)` in `-> Option[uint]` builds NovaOpt_nova_uint, // not the literal-default NovaOpt_nova_int (named-priority int-collapse → // CC-FAIL, surfaced by d86_coalesce_width). Common path keeps `emit_expr`. // [M-d55-str-literal-coercion-name-gated] fix: `[]u8` return // also routes through target-typed emission (D55 amend). // [M-callnorm-free-fn-name-collision] followup (closure-megacu // regression, 2026-07-19): `_NovaTuple_` target — see the sibling // gate above (`emit_block_stmts` trailing) for the full rationale; // same tuple-embeds-fixarr nested-literal `[P67] nova_int collapse` // ICE, same fix (docs/plans/wip/closure-megacu-fix-notes.md). let val = if ret.starts_with("NovaOpt_") || ret.starts_with("_NovaFixArr_") || ret.starts_with("_NovaTuple_") || Self::is_typed_integer(&ret) || Self::is_bytes_slice_c_ty(&ret) { self.emit_expr_with_target_type(e, &ret)? } else { self.emit_expr(e)? }; // Plan 72 P3-B return: box the trailing value for a protocol return type. let val = self.wrap_protocol_return(val, e); let val = self.wrap_any_return(val, e); // Plan 265 Ф.1 discovery (mirrors `[M-184-mut-chain-return-position]`, // emit_block_stmts_trailing above): an arrow-body (`=> expr`) fn/method // whose bare tail IS a value-record fluent `-> @` chain (e.g. // `fn f() -> Command => Command.new(x).arg(y)`) was missed when that fix // landed — only the block-body (`{ ... }`) trailing-expression path was // covered. Same predicate, same fix: the emitted call is `ref Self` // (`NovaValue_X*`); a by-value return consumer must deref it. let val = if self.is_fluent_value_ptr_for_target(e, &ret) { format!("(*({}))", val) } else { val }; if ret == "nova_unit" { self.line(&format!("{};", val)); if has_ensures { self.emit_ensures_checks(f)?; } self.line("return NOVA_UNIT;"); } else if has_ensures { self.line(&format!("{} _nova_result = {};", ret, val)); // Register `result` as visible var inside ensures. self.var_types.insert("result".into(), ret.clone()); self.emit_ensures_checks(f)?; self.var_types.remove("result"); self.line("return _nova_result;"); } else { self.line(&format!("return {};", val)); } } FnBody::Block(block) => { if has_ensures { // Plan 33.1 Ф.4 (D24 production-grade): block-body + // ensures. Перехватываем все return'ы через goto post-label, // collect'им результат в `_nova_result`, потом ensures-checks // + final return. let post_label = format!("_nova_contract_post_{}", f.name); // Объявляем _nova_result заранее. Для unit-return игнорируем // value, но всё равно нужен label-target. if ret != "nova_unit" { self.line(&format!("{} _nova_result;", ret)); } let saved_label = self.contracts_post_label.take(); self.contracts_post_label = Some(post_label.clone()); self.var_types.insert("result".into(), ret.clone()); self.emit_block_stmts(block, &ret)?; // Post-label: ensures-checks + return. // Эмитим label на 0-индентации (C label синтаксис). let saved_indent = self.indent; self.indent = 0; self.line(&format!("{}:;", post_label)); self.indent = saved_indent; self.emit_ensures_checks(f)?; self.var_types.remove("result"); self.contracts_post_label = saved_label; if ret == "nova_unit" { self.line("return NOVA_UNIT;"); } else { self.line("return _nova_result;"); } } else { self.emit_block_stmts(block, &ret)?; } } // D82: external — этот path не должен вызываться для external fn, // т.к. emit_fn skip'ает их раньше. Safety-fallback. FnBody::External => {} } // Plan 113 (D172): restore in_realtime / in_blocking after fn body. self.in_realtime = prev_in_realtime; self.in_blocking = prev_in_blocking; // Plan 127 Ф.3: restore prev fn-id + promoted-locals after fn body. self.current_fn_id = prev_fn_id; self.promoted_value_record_locals = prev_promoted_locals; self.promoted_primitive_locals = prev_promoted_prim_locals; self.expected_record_type = saved_expected; // Plan 72 P0: restore protocol_vars after fn body (clear param-registered entries). self.protocol_vars = saved_protocol_vars_fn; // Plan 72 P1-C: restore result_type_params after fn body. self.result_type_params = saved_result_type_params_fn; // Plan 72 P3-B: restore protocol_var_vtable after fn body. self.protocol_var_vtable = saved_protocol_var_vtable_fn; // Plan 72 P3-B return: restore protocol-return state. self.current_fn_returns_protocol = saved_fn_returns_protocol; // Plan 174.3: restore any-return state. self.current_fn_returns_any = saved_fn_returns_any; // Plan 55 Ф.4: restore prior current_fn_return_ty (mono-pass leak fix). self.current_fn_return_ty = saved_ret_ty; // Plan 63 Fix F+ [M-result-erased-no-mono]: restore pending state. // Discards любые leaked pending_* values из этого fn body (e.g. fn, // которая делает `Ok((..))` или `Some(..)` без surrounding let). // Без этого pending leak'ает в caller's let-binding и overwrites // правильный тип из fn_result_ok_inner_types registry. self.pending_result_ok_inner_type = saved_pending_result; self.pending_option_inner_type = saved_pending_option; // [M-176-conformance-cu-map-closure]: restore caller's mut-binding set // (this fn's own `mut` locals must not leak to sibling fns). self.var_mutable = saved_var_mutable_fn; // Plan 172.5 (D326 R5): restore caller's ref-param set. self.ref_params = saved_ref_params_fn; // Plan 248 (wave 3): restore caller's mut-param-name set. self.mut_param_names = saved_mut_param_names_fn; // Undef any heap-promoted mut-captures so macros don't leak to sibling fns. self.flush_boxed_vars(); self.indent = 0; self.line("}"); self.line(""); let fn_body = std::mem::replace(&mut self.out, saved_out); self.indent = saved_indent; // Flush any lambdas discovered during this function's emit if !self.lambda_forward_decls.is_empty() { self.out.push_str(&std::mem::take(&mut self.lambda_forward_decls)); } if !self.lambda_impls.is_empty() { self.out.push_str(&std::mem::take(&mut self.lambda_impls)); } self.out.push_str(&fn_body); // Plan 170 (D307): restore the previous emission-file context. self.current_emit_file_id = saved_emit_file_id; // Plan 172.14 (sret/_out §2): вторая эмиссия — `__sret`-вариант того же // AST-тела (classic выше остаётся байт-идентичной; аллокация листа // элидируется только в __sret-цепочке). Рекурсия однократна (флаг). if fn_sret_eligible && !self.sret_variant_emit { self.sret_variant_emit = true; self.sret_fn_out = Some("_out".to_string()); let r = self.emit_fn(f); self.sret_fn_out = None; self.sret_variant_emit = false; r?; } Ok(()) } fn emit_nova_main(&mut self, f: &FnDecl) -> Result<(), String> { // [race-198 class-closure]: см. override_maps_scope_enter doc. let ovr_saved = self.override_maps_scope_enter(); let r = self.emit_nova_main_scoped_inner(f); self.override_maps_scope_exit(ovr_saved, r.is_ok()); r } fn emit_nova_main_scoped_inner(&mut self, f: &FnDecl) -> Result<(), String> { // nova main() → stored separately, called from C main() // Buffer body so spawn-ctx typedefs (lambda_forward_decls) flush // BEFORE main's body — иначе typedef в out появится после своего // первого usage внутри тела main. let saved_out = std::mem::take(&mut self.out); let saved_indent = self.indent; self.indent = 0; self.line(&format!("{}nova_unit nova_fn_main_impl(void) {{", self.top_level_storage())); self.indent = 1; match &f.body { FnBody::Expr(e) => { self.emit_source_annotation_for_expr(e); let val = self.emit_expr(e)?; self.line(&format!("{};", val)); self.line("return NOVA_UNIT;"); } FnBody::Block(block) => { self.emit_block_stmts(block, "nova_unit")?; } // D82: main() не может быть external. Safety-fallback. FnBody::External => {} } self.flush_boxed_vars(); self.indent = 0; self.line("}"); self.line(""); let body = std::mem::replace(&mut self.out, saved_out); self.indent = saved_indent; // Flush forward decls for spawn-ctx typedefs / lambda forward decls // accumulated during main's body emit — they must precede the body. if !self.lambda_forward_decls.is_empty() { self.out.push_str(&std::mem::take(&mut self.lambda_forward_decls)); } if !self.lambda_impls.is_empty() { self.out.push_str(&std::mem::take(&mut self.lambda_impls)); } self.out.push_str(&body); Ok(()) } fn emit_main_wrapper(&mut self, module: &Module) { let has_main = module.items.iter().any(|i| { if let Item::Fn(f) = i { f.name == "main" } else { false } }); let tests: Vec<&TestDecl> = module.items.iter().filter_map(|i| { if let Item::Test(t) = i { Some(t) } else { None } }).collect(); // Plan 57.B.3 / 57.B.5: expand parameterized + grouped sweeps в plain // entries. Каждый bench с params даёт N entries с suffix `/p=<value>`; // каждый bench с groups даёт M entries с suffix `/<group>/<case>`. let mut benches_expanded: Vec<(String, &BenchDecl)> = Vec::new(); for it in &module.items { if let Item::Bench(b) = it { if !b.groups.is_empty() { for grp in &b.groups { for case in &grp.cases { benches_expanded.push(( format!("{}/{}/{}", b.name, grp.name, case.name), b)); } } } else if let Some(p) = &b.params { for v in &p.values { benches_expanded.push((format!("{}/p={}", b.name, v), b)); } } else { benches_expanded.push((b.name.clone(), b)); } } } let benches: Vec<&BenchDecl> = module.items.iter().filter_map(|i| { if let Item::Bench(b) = i { Some(b) } else { None } }).collect(); let _ = benches; // (kept для legacy ref'ов) // Plan 57: bench-mode main — приоритет над test-runner main. // Игнорируем test-items, эмитим bench runner который последовательно // вызывает все nova_bench_main_<idx>(). if self.bench_mode && !benches_expanded.is_empty() && !has_main { self.line(&format!("{}nova_unit nova_fn_main_impl(void) {{", self.top_level_storage())); self.indent += 1; self.line(&format!("int _nova_benches_total = {};", benches_expanded.len())); // Bench filter via NOVA_BENCH_FILTER env: comma-separated substrings. self.line("const char* _bench_filter = getenv(\"NOVA_BENCH_FILTER\");"); self.line("fprintf(stderr, \"Running %d benches...\\n\", _nova_benches_total);"); for (idx, (effective_name, _bd)) in benches_expanded.iter().enumerate() { let safe = Self::mangle_test_name_indexed(effective_name, idx); let escaped = Self::escape_c_str(effective_name); self.line("{"); self.indent += 1; self.line(&format!("const char* _bench_name = \"{}\";", escaped)); self.line("int _run = 1;"); self.line("if (_bench_filter && *_bench_filter) {"); self.indent += 1; self.line("_run = 0;"); self.line("const char* _f = _bench_filter;"); self.line("while (*_f) {"); self.indent += 1; self.line("const char* _comma = strchr(_f, ',');"); self.line("size_t _flen = _comma ? (size_t)(_comma - _f) : strlen(_f);"); self.line("if (_flen > 0) {"); self.indent += 1; self.line("size_t _bn_len = strlen(_bench_name);"); self.line("if (_bn_len >= _flen) {"); self.indent += 1; self.line("for (size_t _i = 0; _i + _flen <= _bn_len; _i++) {"); self.indent += 1; self.line("if (memcmp(_bench_name + _i, _f, _flen) == 0) { _run = 1; break; }"); self.indent -= 1; self.line("}"); self.indent -= 1; self.line("}"); self.indent -= 1; self.line("}"); self.line("if (_run) break;"); self.line("if (!_comma) break;"); self.line("_f = _comma + 1;"); self.indent -= 1; self.line("}"); self.indent -= 1; self.line("}"); self.line("if (_run) {"); self.indent += 1; self.line(&format!("nova_bench_main_{}();", safe)); self.indent -= 1; self.line("} else {"); self.indent += 1; self.line(&format!("fprintf(stderr, \" SKIP: {} (filtered)\\n\");", escaped)); self.indent -= 1; self.line("}"); self.indent -= 1; self.line("}"); } self.line("return NOVA_UNIT;"); self.indent -= 1; self.line("}"); self.line(""); } else if !tests.is_empty() && !has_main { // [8a-fix] Plan 198 defect #8a: each test used to get a // `{ NovaTestFrame _tf; NovaFailFrame _tf_fail; ... }` scope // INLINED straight into one giant nova_fn_main_impl. `_tf`'s // address escapes into the global `_nova_test_frame`, so clang // cannot prove sibling test scopes are non-overlapping and // keeps every scope's stack slot live for the whole function — // a merged CU with N test-blocks grew main_impl's C stack frame // O(N) (>1MB at N=2589 → stack overflow 0xC00000FD at process // startup, before a single test runs). Fix: split test bodies // into fixed-size (TEST_CHUNK_SIZE) chunk functions; each // function's frame is bounded by the chunk size regardless of // total corpus size, and nova_fn_main_impl becomes a // constant-frame loop of chunk calls. (Bonus: fewer live // stack slots per function scope also trims merged-CU // startup/compile cost — relevant to Plan 200.1 §1.) const TEST_CHUNK_SIZE: usize = 64; let test_chunks: Vec<&[&TestDecl]> = tests.chunks(TEST_CHUNK_SIZE).collect(); for (chunk_idx, chunk) in test_chunks.iter().enumerate() { self.line(&format!("{}int nova_test_chunk_{}(void) {{", self.top_level_storage(), chunk_idx)); self.indent += 1; self.line("int _nova_tests_failed = 0;"); // [race-198 / 196.6]: snapshot the IMPLICIT MAIN SCOPE (D92 — // established once in the emitted main() wrapper before any // chunk runs; at every chunk entry _nova_active_scope is that // scope, pristine or restored below). The per-test // nova_runtime_reset() NULLs _nova_active_scope — correct for // discarding a DANGLING scope a longjmp-unwound test left // behind, but a later test's main-flow Time.sleep would then // hit the D92 FATAL (sleep outside any scope). Restore the // known-good main scope after every reset instead. self.line("NovaFiberQueue* _chunk_main_scope = _nova_active_scope;"); self.line("int _chunk_main_slot = _nova_active_slot;"); for (local_idx, t) in chunk.iter().enumerate() { let idx = chunk_idx * TEST_CHUNK_SIZE + local_idx; let safe = Self::mangle_test_name_indexed(&t.name, idx); let escaped = Self::escape_c_str(&t.name); self.line("{"); self.indent += 1; self.line("NovaTestFrame _tf;"); self.line("_tf.fail_msg = NULL;"); self.line("_nova_test_frame = &_tf;"); /* Push a fail-frame too: assertion failures inside a fiber are * routed to the nearest NovaFailFrame (so longjmp stays on the * fiber's own stack); supervised_run re-throws on main flow via * nova_throw. Without a top-level fail-frame here, that re-throw * would abort. We catch it via _tf_fail and report as test * failure. _tf still catches plain main-flow asserts. */ self.line("NovaFailFrame _tf_fail;"); self.line("_tf_fail.error_msg = (nova_str){.ptr=NULL, .len=0};"); // Plan 173 Ф.6 (D348): kind sentinel — panics-ветка дискриминирует // PANIC-класс (D13) от throw/cancel по error_kind. self.line("_tf_fail.error_kind = NOVA_THROW_USER;"); self.line("nova_fail_push(&_tf_fail);"); self.line("int _tf_jmp = setjmp(_tf.jmp);"); self.line("int _tf_fail_jmp = (_tf_jmp == 0) ? setjmp(_tf_fail.jmp) : 0;"); if let Some(pat) = &t.panics { // Plan 173 Ф.6 (D348): panics-клаузула — ИНВЕРСИЯ PASS/FAIL. // PASS ⇔ тело запаниковало (PANIC-класс) сообщением ⊇ паттерн. let pat_escaped = Self::escape_c_str(pat); self.line("if (_tf_jmp == 0 && _tf_fail_jmp == 0) {"); self.indent += 1; self.line("fflush(stdout);"); self.line(&format!("nova_test_{}();", safe)); // Тело завершилось нормально — ожидали панику → FAIL. self.line(&format!( "printf(\" FAIL: {} — expected panic containing \\\"{}\\\" but test completed normally\\n\"); fflush(stdout);", escaped, pat_escaped)); self.line("_nova_tests_failed++;"); self.indent -= 1; self.line("} else {"); self.indent += 1; // Сообщение + PANIC-дискриминатор: // - fail-frame route (_tf_fail_jmp): kind==PANIC — panic()/ // assert-in-fiber/overflow; nova_str (ptr,len). // - test-frame route (_tf_jmp): nv_panic-без-fail-frame пишет // "panic: …", assert-on-main пишет "…assert failed…" (оба // PANIC-класс D13); nv_exit пишет "exit(N): …" — НЕ паника. self.line("const char* _p_msg; size_t _p_len; int _p_is_panic;"); self.line("if (_tf_fail_jmp != 0) {"); self.indent += 1; self.line("_p_msg = _tf_fail.error_msg.ptr; _p_len = (size_t)_tf_fail.error_msg.len;"); self.line("_p_is_panic = (_tf_fail.error_kind == NOVA_THROW_PANIC);"); self.indent -= 1; self.line("} else {"); self.indent += 1; self.line("_p_msg = _tf.fail_msg ? _tf.fail_msg : \"\"; _p_len = strlen(_p_msg);"); self.line("_p_is_panic = (_p_len < 5 || memcmp(_p_msg, \"exit(\", 5) != 0);"); self.indent -= 1; self.line("}"); self.line(&format!( "if (_p_is_panic && nova_test_msg_contains(_p_msg, _p_len, \"{}\")) {{", pat_escaped)); self.indent += 1; self.line(&format!("printf(\" PASS: {}\\n\"); fflush(stdout);", escaped)); self.indent -= 1; self.line("} else if (_p_is_panic) {"); self.indent += 1; self.line(&format!( "printf(\" FAIL: {} — panic message did not contain \\\"{}\\\": %.*s\\n\", (int)_p_len, _p_msg); fflush(stdout);", escaped, pat_escaped)); self.line("_nova_tests_failed++;"); self.indent -= 1; self.line("} else {"); self.indent += 1; self.line(&format!( "printf(\" FAIL: {} — failed without panic (throw/cancel/exit is not a panic): %.*s\\n\", (int)_p_len, _p_msg); fflush(stdout);", escaped)); self.line("_nova_tests_failed++;"); self.indent -= 1; self.line("}"); self.indent -= 1; self.line("}"); self.line("nova_fail_pop();"); self.line("_nova_test_frame = NULL;"); // Plan 173 Ф.5 п.6: сброс висящих fail/interrupt-frames и // handler-слотов после ПОЙМАННОЙ паники (longjmp мимо // эпилогов) — N panics-тестов в одном процессе безопасны. self.line("nova_runtime_reset();"); // [race-198 / 196.6]: re-establish the implicit main scope // (see _chunk_main_scope snapshot at chunk entry). self.line("_nova_active_scope = _chunk_main_scope;"); self.line("_nova_active_slot = _chunk_main_slot;"); } else { self.line("if (_tf_jmp == 0 && _tf_fail_jmp == 0) {"); self.indent += 1; self.line(&format!("fflush(stdout);")); self.line(&format!("nova_test_{}();", safe)); self.line(&format!("printf(\" PASS: {}\\n\"); fflush(stdout);", escaped)); self.indent -= 1; self.line("} else {"); self.indent += 1; self.line("const char* _tf_msg = _tf.fail_msg ? _tf.fail_msg : (_tf_fail.error_msg.ptr ? _tf_fail.error_msg.ptr : \"assertion failed\");"); self.line(&format!("printf(\" FAIL: {} — %s\\n\", _tf_msg);", escaped)); self.line("_nova_tests_failed++;"); self.indent -= 1; self.line("}"); self.line("nova_fail_pop();"); self.line("_nova_test_frame = NULL;"); // [race-state-dump 2026-07-13] Plan 173 Ф.5 п.6 originally // scoped `nova_runtime_reset()` to ONLY the panics-clause // branch above ("N panics-тестов в одном процессе // безопасны") — but the same longjmp-past-epilogues hazard // (stale _nova_fail_top/_nova_interrupt_top/handler-vtable // slots/_nova_active_scope/_nova_active_finalizer_stack) // is reachable from an ORDINARY (non-panics) test whose // body catches a Fail via `with Fail[E] = |e| interrupt // (...)`, or whose `assert()` fails deep inside nested // calls (D89 plain-fail route) — both unwind via longjmp // past intermediate epilogues just like a panics-clause // catch does. In a merged folder-module CU (spec_tests/ // conformance: ~1000 files / ~2500 test-blocks in ONE // process) an ordinary test leaving any of that TLS state // dirty leaks it into the NEXT, unrelated test-block — // observed as a deterministic cross-test-block assertion // corruption (D158/D188 "pocket"/"cleanup dispatches // exactly once" cluster) and, when the leaked pointer is a // dangling stack address of the completed test's own C // frame, a composition-dependent access violation in a // LATER test. Reset unconditionally after every test-block, // not only after panics-clause ones. self.line("nova_runtime_reset();"); // [race-198 / 196.6]: re-establish the implicit main scope // (see _chunk_main_scope snapshot at chunk entry) — the // reset NULLs _nova_active_scope, and a later test's // main-flow Time.sleep would hit the D92 FATAL otherwise. self.line("_nova_active_scope = _chunk_main_scope;"); self.line("_nova_active_slot = _chunk_main_slot;"); } self.indent -= 1; self.line("}"); } self.line("return _nova_tests_failed;"); self.indent -= 1; self.line("}"); self.line(""); } self.line(&format!("{}nova_unit nova_fn_main_impl(void) {{", self.top_level_storage())); self.indent += 1; self.line(&format!("int _nova_tests_total = {};", tests.len())); self.line("int _nova_tests_failed = 0;"); self.line("printf(\"Running %d tests...\\n\", _nova_tests_total);"); self.line("fflush(stdout);"); for chunk_idx in 0..test_chunks.len() { self.line(&format!("_nova_tests_failed += nova_test_chunk_{}();", chunk_idx)); } self.line("printf(\"%d/%d passed\\n\", _nova_tests_total - _nova_tests_failed, _nova_tests_total);"); self.line("if (_nova_tests_failed > 0) { exit(1); }"); self.line("return NOVA_UNIT;"); self.indent -= 1; self.line("}"); self.line(""); } // Plan 83.10.4 Ф.3 [M-83.10.1-per-fiber-handler-tls-race]: // File-scope helper function emitted before main() so it can be // assigned to _nova_register_effects_fn. C does not allow nested // function definitions, so it must be at file scope. self.line(&format!("{}void _nova_register_all_effects_(void) {{", self.top_level_storage())); self.indent += 1; self.line("nova_register_effect_storage((void**)&_nova_handler_Fail);"); // Plan 175 Ф.2-v3: "Time" registration MOVED into the generic // `emit_user_effect_registrations` loop below — Time is no longer a // hardcoded builtin vtable (см. RUNTIME_DEFINED_TYPES/BUILTIN_VTABLE_ // NAMES comments), so its `_nova_handler_Time` is registered exactly // like any user effect's handler slot. // User-defined effects — additional calls emitted here: self.emit_user_effect_registrations(); self.indent -= 1; self.line("}"); self.line(""); // Plan 173 Ф.5 п.2 (D192-ретракт): __CLEANUP_TIMEOUT_IMPL__ marker // удалён вместе с CleanupTimeoutError typed-throw механизмом. // Plan 174 (D349): typed TimeoutError throw impl splice — emitted // ПОСЛЕ user-type struct definitions (Nova_TimeoutError + tid macro), // ДО int main(). self.line("/*__SCOPE_TIMEOUT_IMPL__*/"); // Plan 173.2 (supervision-as-effect): Supervisor decision-bridge impl // splice — after NovaVtable_Supervisor + Nova_Decision definitions // (type-decl stage), before main(). Replaced in finalize when the CU // knows the Supervisor effect + Decision sum (prelude present). self.line("/*__SUPERVISOR_DECIDE_IMPL__*/"); // Plan 22 Ф.5 / [M-bare-fiber-accept-bootstrap-park-invalid-slot] // (221.1 №108, D92 Правило 6 ретракция): main-body now runs AS A // FIBER (a real scheduler slot) instead of a direct C call on the // raw OS thread with `_nova_active_slot == -1`. This is THE fix for // "no second door" — park/wake (D93), and therefore blocking // `TcpListener.accept()` / `Time.sleep()` called DIRECTLY in // `main()`, need `mco_running() != NULL` + a valid // `nova_sched_park` slot; slot -1 has neither. Wrapping main-body // in the SAME spawn+drive machinery every `supervised { spawn {…} }` // already uses (see `emit_spawn` above — this mirrors it 1:1, minus // user captures: main has none) gives it that slot for free, reusing // 100%-battle-tested runtime primitives rather than inventing a new // scheduling path (mn-coding-conventions: no bespoke concurrency // code where an existing primitive already does the job). // // `NovaSpawnCtxBase` (fibers.h) is used directly as the ctx type — // no extra fields needed since main-body captures nothing (it's the // top-level fn, not a closure). The entry function below is a // hand-transcribed copy of emit_spawn's generated entry-fn body // (preamble slot-alloc / fail-frame catch / kinded error report / // epilogue slot-free + pending_remote decrement) — same contract, // same ordering discipline (mn-coding-conventions §1-§11), just // calling `nova_fn_main_impl()` instead of AST-emitted statements. self.line(&format!( "{}void _nova_main_fiber_entry(mco_coro* _co) {{", self.top_level_storage() )); self.indent += 1; self.line("NovaSpawnCtxBase* _c = (NovaSpawnCtxBase*)mco_get_user_data(_co);"); // [P108-followup, integrator gate red 2026-07-25, round 2 — // repro_cross_effect_throw wrong-value (not a hang)]: mark this // coroutine as THE root main-fiber for the duration of its run. // `nova_interrupt`/`nova_interrupt_ptr` (effects.c) compare // `mco_running()` against this to treat main-fiber-hosted code as // main-flow-equivalent for the D61 cross-effect handler-arm // routing fast-path (see effects.h `_nova_main_fiber_co` doc // comment for the full rationale — that fast-path used to gate on // `!mco_running()`, a proxy that broke once main itself became a // fiber). Cleared before returning (both exit paths) so a LATER // fiber that happens to reuse this same arena slot's memory // address (e.g. a detach spawned during D92 drain, after // main-body itself has finished) never falsely matches. self.line("_nova_main_fiber_co = (void*)_co;"); // [P108-followup, integrator gate red 2026-07-25]: main-body is // ALWAYS bootstrap-spawned (`nova_fiber_spawn_into`, never // `nova_runtime_spawn_into`) — see the call site below for the full // rationale. `_c->_nova_parent_scope` is therefore ALWAYS NULL here; // `nova_supervised_step` (which resumes bootstrap fibers directly on // ITS OWN calling thread — the true process main thread) owns slot // lifecycle for this fiber exactly like any other bootstrap-spawned // fiber, so there is nothing to self-allocate on first resume. Kept // as an explicit no-op assignment (not deleted) so this stays // byte-structurally comparable to `emit_spawn`'s identical preamble // shape, and so a FUTURE change of the call-site's spawn choice // (should one ever be needed) does not silently skip re-adding this. self.line("_c->_nova_worker_slot = -1;"); // Fail-frame so a throw/panic inside main-body longjmps back HERE // (on this fiber's own stack) rather than escaping across the // coroutine boundary. D92 Правило 3: main-body errors propagate as // usual — reported to the scope (nova_supervised_run re-throws them // on the calling C thread after drain), matching the pre-fix direct- // call behaviour byte-for-byte (still no top-level fail-frame around // the WHOLE program → still aborts with the same diagnostic on an // uncaught error). self.line("NovaFailFrame _ff;"); self.line("nova_fail_push(&_ff);"); self.line("if (setjmp(_ff.jmp) == 0) {"); self.indent += 1; self.line("nova_fn_main_impl();"); self.line("nova_fail_pop();"); self.indent -= 1; self.line("} else {"); self.indent += 1; self.line("nova_fail_pop();"); self.line("if (_ff.error_msg.ptr && _ff.error_msg.len == 18 && memcmp(_ff.error_msg.ptr, \"__nova_interrupt__\", 18) == 0) {"); self.indent += 1; self.line("/* interrupt: scope state already set, fiber dies cleanly (mirrors emit_spawn). */"); self.indent -= 1; self.line("} else {"); self.indent += 1; // [P108-followup]: always the bootstrap/local report path — see the // call site below for why main-body is NEVER remote/worker-spawned // (`_c->_nova_parent_scope` is always NULL here, so the // `nova_fiber_report_child_kinded` remote-report branch `emit_spawn` // needs never applies to main). self.line("nova_fiber_report_error_kinded(_ff.error_msg.ptr, _ff.error_kind, _ff.error_reason_ptr);"); self.indent -= 1; self.line("}"); self.indent -= 1; self.line("}"); // No epilogue needed: bootstrap-spawned fibers (this one, always — // see call site) never increment `pending_remote`/allocate a worker // slot in the first place (`nova_fiber_spawn_into`, not // `nova_runtime_spawn_into`) — `nova_supervised_step` owns this // fiber's slot lifecycle entirely, exactly as for any other // bootstrap-spawned fiber (mirrors `emit_spawn`'s identical // no-op-under-bootstrap epilogue shape). self.line("_nova_main_fiber_co = NULL;"); self.indent -= 1; self.line("}"); self.line(""); self.line("int main(int argc, char** argv) {"); self.indent += 1; self.line("nova_gc_init();"); // `[M-lazy-const-init-race]`: eager module-const init — MUST run // after `nova_gc_init()` (the init bodies call `nova_gc_add_root`) // and BEFORE `nova_runtime_auto_arm()` below (which spawns the M:N // worker threads). Every lazy const is populated, single-threaded, // before any worker can observe it — replaces the retired per-const // check-then-act lazy getter (data race under concurrent first-touch). self.line("nova_consts_init();"); // Plan 176 Ф.3 (D324): capture argv for the std/os `Os` effect // (os.args). Stored into os_env.h file-scope globals read by // os_arg_count/os_arg_at. Harmless when os is unused. self.line("nova_os_set_args(argc, argv);"); // Plan 83.4.5.8 Ф.0 (2026-05-24): flip activation. Atomic state // machine из Plan 83.4.5.7 Ф.1 + uncollectable SpawnCtx allocation // из Plan 83.4.5.8 → default-on M:N runtime активен по D138. // Все 5 предшествующих fix'ов сошлись: // - Plan 83.4.1: park-with-predicate (D93 ASYNC close_cb). // - Plan 83.4.2 Ф.1+Ф.2: supervised_step worker-owned skip + // per-fiber handler-snapshot save/restore. // - Plan 83.4.3 B5: cancel_requested atomic. // - Plan 83.4.5.1: cancel-wake-all + dispatch_ready re-queue. // - Plan 83.4.5.2: AsyncDetach production-grade + orphan-spawn. // - Plan 83.4.5.4: spawn-time handler-snapshot TLS capture. // - Plan 83.4.5.5: NOVA_NO_AUTOARM=1 escape hatch (cooperative). // - Plan 83.4.5.7 Ф.1: atomic fiber state machine + CAS guards // mco_resume sites + idempotent wake CAS на parked flag + // nova_runtime_shutdown ordering pre evloop_close. // - Plan 83.4.5.8: nova_alloc_uncollectable для SpawnCtx + // init_snapshot под armed M:N (defeats GC race на Windows // fiber arena ctx visibility). // // Runtime активируется default-on per D138. self.line("nova_runtime_auto_arm();"); // Plan 22 Ф.2: глобальный event loop. Под NOVA_USE_LIBUV даёт // настоящий uv_default_loop, иначе — stub no-op. Idempotent. self.line("nova_evloop_init();"); // Plan 83.10.4 Ф.3 [M-83.10.1-per-fiber-handler-tls-race]: // Per-fiber handler scoping: register all built-in effect-storage // addresses. Set the function pointer so workers call it at startup, // then call it for the main thread. // (The file-scope helper function is emitted just before main() in // emit_main_function via emit_effects_registrar_fn.) self.line("_nova_register_effects_fn = _nova_register_all_effects_;"); self.line("_nova_register_all_effects_();"); // Plan 175 Ф.2-v3: the standalone `_nova_time_default_ctor` runtime // hook (Ф.2-v2 special-case) is GONE — Time now goes through the // fully generic `#default_handler` mechanism baked directly into // each `Nova_Time_<op>()` dispatcher body by `emit_effect_type` // (same lazy-install-once pattern any other `#default_handler` // effect gets — see the `default_handler_fns.get(name)` check // inside `emit_effect_type`). No per-effect wiring needed here // anymore; `#default_handler(Time)` now lives in // std/prelude/effects.nv (always in scope — ambient fallback // without import), so every CU registers it uniformly. // Plan 173 Ф.5 п.2 (D192-ретракт): __CLEANUP_TIMEOUT_INIT__ удалён. // Plan 174 (D349): assign typed TimeoutError throw fn pointer (if // TimeoutError referenced). Spliced via __SCOPE_TIMEOUT_INIT__. self.line("/*__SCOPE_TIMEOUT_INIT__*/"); // Plan 173.2: assign the Supervisor decision bridge (if the CU knows // the Supervisor effect). Spliced via __SUPERVISOR_DECIDE_INIT__. self.line("/*__SUPERVISOR_DECIDE_INIT__*/"); // Plan 22 Ф.5 (D92): implicit main-scope. Top-level main теперь // имеет supervised-like scope для detach'ей, pending timer'ов и // background fiber'ов. Они доработают до quiescence перед exit. // _nova_active_slot = -1 означает "main-flow, не fiber". self.line("NovaFiberQueue _nova_main_scope; nova_scope_init(&_nova_main_scope);"); self.line("_nova_active_scope = &_nova_main_scope;"); self.line("_nova_active_slot = -1;"); // Plan 22 Ф.10 + F2: SIGINT handler — Ctrl+C → cancel main-scope → // graceful shutdown. libuv mandatory (Plan 22 F2), без #ifdef. self.line("nova_evloop_install_sigint(&_nova_main_scope);"); // [M-bare-fiber-accept-bootstrap-park-invalid-slot] (221.1 №108, // D92 Правило 6 ретракция): spawn main-body as a real fiber INTO // the implicit main-scope, then drive the scheduler with // `nova_supervised_run` until it completes. This is what gives // main-body a genuine (scope, slot) pair — `_nova_active_slot >= 0` // for the entire duration of user code — so `nova_sched_park` (D93 // park/wake: blocking accept/recv/Time.sleep) works directly in // `main()` without any `supervised{spawn{…}}` wrapper. // // [P108-followup, integrator gate red 2026-07-25 — mega-CU 577/5, // all 5 TIMEOUT]: the FIRST cut of this fix mirrored `emit_spawn`'s // dual bootstrap/armed-M:N branch verbatim (`nova_runtime_is_ // initialized() ? nova_runtime_spawn_into(...) : nova_fiber_spawn_ // into(...)`) — WRONG for main specifically. `nova_runtime_auto_ // arm()` (a few lines above `main_wrapper`, unconditional at // program start, D138 default-on) sets `_armed = true` BEFORE this // site ever runs, so `nova_runtime_is_initialized()` is ALWAYS true // here — every program's main-body was being pushed onto a WORKER // THREAD's deque via `nova_runtime_spawn_into`, never the true // process main OS thread. That silently flips EVERY top-level // `supervised{}`/`detach{}`/cross-effect-throw/test-runner-chunk in // the ENTIRE program into "nested supervised on a worker thread" // from the runtime's perspective (`_nova_on_worker_thread()` is // thread-identity-based) — a fundamentally different, narrower // code path (cooperative `nova_runtime_worker_pump_scope` instead // of the plain main-thread `uv_run` wait-loop, watchdog disabled, // different orphan-scope/signal_main thread-affinity assumptions) // that most tests never hit but several did (top-level detach, // cross-effect throw, multierror-scope, the folder-CU test-runner // driving hundreds of chunks) — all 5 timeouts, zero crashes, // exactly the "wrong door reached silently" signature. // // Fix: main-body is ALWAYS bootstrap-spawned (`nova_fiber_spawn_ // into`) — pinned to THIS calling OS thread (the real process main // thread), driven by `nova_supervised_step`'s direct `mco_resume` // on that same thread, regardless of the process-wide armed/ // unarmed state. This is NOT a loss of concurrency: children the // user's code spawns/detaches from within main-body still go // through the ordinary armed/bootstrap decision at THEIR OWN // `spawn{}`/`detach{}` call sites (`emit_spawn`/`emit_detach`, // unchanged) — only main-body's OWN hosting thread is pinned, so // every existing "am I on the main thread or a worker" assumption // in the runtime keeps seeing exactly what it saw before #108. // `nova_supervised_run` re-throws the scope's first_error on normal // completion (D92 Правило 3: main-body errors propagate as usual) // — since this outer C frame still has no NovaFailFrame, an // uncaught error still aborts with the same diagnostic as the old // direct call. self.line("{"); self.indent += 1; self.line("NovaSpawnCtxBase* _nova_main_ctx = (NovaSpawnCtxBase*)nova_alloc(sizeof(NovaSpawnCtxBase));"); self.line("_nova_main_ctx->_nova_worker_slot = -1;"); self.line("_nova_main_ctx->_nova_parent_slot = -1;"); self.line("_nova_main_ctx->_nova_parent_scope = NULL;"); self.line("_nova_main_ctx->_nova_init_snapshot = NULL;"); self.line("nova_fiber_spawn_into(&_nova_main_scope, _nova_main_fiber_entry, _nova_main_ctx);"); self.line("nova_supervised_run(&_nova_main_scope);"); self.indent -= 1; self.line("}"); // D92: drain implicit main-scope до quiescence перед exit. // Detach'ы / pending fiber'ы пробуждённые callback'ами после // main-body доработают. Не используем nova_supervised_run для ЭТОГО // вызова потому что он re-throws fiber-errors на main-flow (которого // уже нет), вызывая abort. Используем drain-no-throw variant — // fiber-throw'ы в detach'ах logged but не abort'ят процесс (D50 // fire-and-forget). [108]: nova_supervised_run above already fully // drained/cleaned up _nova_main_scope's OWN queue (the one main-body // fiber) — this call is now over an empty, already-`nova_sched_ // drop_state`'d scope; kept unchanged (idempotent — nova_sched_ // drop_state is a plain NULL-store) both for API/doc parity with // Правило 2's wording and as the belt-and-braces safety net for any // future direct population of _nova_main_scope. self.line("nova_supervised_drain_main_scope(&_nova_main_scope);"); self.line("_nova_active_scope = NULL;"); self.line("_nova_active_slot = -1;"); // [p418, №418 корень A] explicit synchronous orphan-drain ДО // runtime.shutdown/evloop.close — SAME class of hazard as the // shutdown-ordering comment right below, just for `detach {}` // fire-and-forget fibers instead of supervised children. Prior to // this fix `nova_runtime_drain_orphans()` was ONLY reachable via // `atexit(nova_runtime_drain_orphans)` (registered lazily on the // first `detach`, runtime.c `_orphan_scope_ensure_init`) — atexit // handlers run AFTER main()'s own body (including its OWN // `nova_evloop_close()` two lines below) has already completed. // An orphan still in flight (e.g. a `detach`-ed TcpStream.connect // that hasn't finished when main-body returns) then gets drained // by the atexit handler against an ALREADY-CLOSED event loop: // `nova_supervised_drain_main_scope`'s `uv_run(nova_current_loop(), // ...)` reads `nova_current_loop()`'s cached per-thread TLS pointer // (set once in `nova_evloop_init`, never invalidated by `nova_ // evloop_close`) — a dangling `uv_loop_t*` — bypassing the // `nova_evloop()` accessor's `_evloop_state == 2` guard entirely. // SIGSEGV inside `uv_run`/`uv__run_pending`. Draining HERE (workers // + event loop still alive) lets any in-flight orphan actually // finish; the pending `atexit(nova_runtime_drain_orphans)` call // then runs a no-op (`_nova_orphan_scope.count == 0` and // `pending_remote/pending_sweeps == 0` already — the `alive==0 && // remote==0` branch in `nova_supervised_drain_main_scope` breaks // BEFORE ever calling `uv_run`, so the second, atexit-driven call // never touches the loop). Idempotent — a program with no `detach` // never initialized `_nova_orphan_scope` and this is a cheap no-op // (`nova_runtime_drain_orphans` returns immediately when `!_nova_ // orphan_scope_inited`). self.line("nova_runtime_drain_orphans();"); // Plan 83.4.5.7 Ф.4 (2026-05-23): explicit runtime.shutdown ДО // evloop.close. Под armed M:N worker'ы могут быть ещё активны после // drain (e.g. pending uv_async_send в полёте). evloop.close → close // _main_wake handle → следующий worker's signal_main → uv_async_send // на CLOSING handle → libuv assertion abort. // Shutdown сигналит stop + join'ит workers + cleanup'ит — после него // workers не вызывают signal_main. Идемпотентен; под bootstrap // (_armed == false) — no-op. Под armed: atexit-registered shutdown // тоже сработает (idempotent), но мы хотим SYNCHRONOUS shutdown // перед evloop_close. self.line("nova_runtime_shutdown();"); // Plan 22 Ф.2: graceful shutdown event loop'а перед GC shutdown. // Закрывает active handles, drain pending callbacks. Под stub'ом — // no-op. self.line("nova_evloop_close();"); self.line("nova_gc_shutdown();"); self.line("return 0;"); self.indent -= 1; self.line("}"); } /// Register handler-storage TLS addresses for all user-defined effects /// so per-fiber snapshot mechanism (effects.h) can swap them. fn emit_user_effect_registrations(&mut self) { // effect_schemas содержит и built-in (Fail, Time) и user-defined. // Built-in уже регистрируются явно в emit_main_wrapper. Для user-defined // эмитим nova_register_effect_storage для каждого `_nova_handler_X`. let mut names: Vec<String> = self.effect_schemas.keys().cloned().collect(); names.sort(); // deterministic order for name in names { // Skip built-ins (зарегистрированы явно ИЛИ direct-C без handler-slot'а). // Plan 175 Ф.1: TimerMetrics — direct-C introspection, нет // `_nova_handler_TimerMetrics`-слота → регистрировать нечего. // Plan 175 Ф.2-v3: "Time" REMOVED from skip-list — теперь generic // effect (emit_effect_type generates `_nova_handler_Time` same as // any user effect), значит ОБЯЗАН per-fiber TLS registration как // все прочие (иначе `with Time = ...` не изолируется между // fiber'ами на одном OS-thread — [M-83.10.1-per-fiber-handler- // tls-race] класс бага). "Fail" зарегистрирован явно отдельно // (см. `_nova_handler_Fail` в effects.h/emit_main_wrapper). // "Mem" REMOVED (D76 amend, [M-mem-effect-demote-to-namespace], // 2026-08-01): no longer an effect — never appears in // `effect_schemas` at all now, so this skip-arm is unreachable // for it (kept removed rather than dead, for clarity). if name == "Fail" || name == "TimerMetrics" { continue; } self.line(&format!( "nova_register_effect_storage((void**)&_nova_handler_{});", name)); } } // ---- block / statements ---- // ---- Plan 20 Ф.4: defer/errdefer codegen helpers ---- /// Scan a block for any `defer` stmt (non-recursive — defers in nested /// blocks have their own scope). Used to decide whether to set up /// defer-state for this block at all (fast-path otherwise). Plan 173 Ф.1 /// (#4): only plain `defer` exists now (errdefer/okdefer retracted, D189), /// so there is no path-selective flag — all defers fire on every exit. fn block_has_defers(block: &Block) -> bool { block.stmts.iter().any(|s| matches!(s, Stmt::Defer { .. })) } /// Plan 217 (D-новый, гибрид C): if `decl` is a bare `consume X = e;` /// (single `Ident` pattern — guard 3: no drop-glue, only named scalar /// bindings qualify, never tuple/record destructure) whose init /// expression's type has a registered effect-pure `@cleanup` /// (`auto_cleanup_types`, guard 1), return `Some(init_c_type)` (the raw /// C type as `infer_expr_c_type` reports it — same shape `Stmt:: /// ConsumeScope` codegen already strips via `debt_strip_nova_trim_start` /// + `NovaValue_` peel). `None` for anything that must keep strict /// linearity (non-consume, non-Ident pattern, or no pure `@cleanup`). fn auto_cleanup_qualifies(&self, decl: &LetDecl) -> Option<String> { if !decl.consume { return None; } if !matches!(&decl.pattern, Pattern::Ident { .. }) { return None; } let init_c_type = self.infer_expr_c_type(&decl.value); let type_name = self.debt_strip_nova_trim_start(&init_c_type); let type_name = type_name .strip_prefix("NovaValue_") .map(|s| s.to_string()) .unwrap_or(type_name); if self.auto_cleanup_types.contains(&type_name) { Some(init_c_type) } else { None } } /// Plan 217: does this block contain at least one bare consume-let that /// qualifies for auto-cleanup (`auto_cleanup_qualifies`)? Non-recursive, /// mirrors `block_has_defers` (nested blocks get their own scope). fn block_has_auto_cleanup_lets(&self, block: &Block) -> bool { block.stmts.iter().any(|s| matches!(s, Stmt::Let(decl) if self.auto_cleanup_qualifies(decl).is_some())) } /// Push a new defer scope onto the stack and emit its prologue: /// declaration of activation flags (zero-init), and the NovaFailFrame /// setjmp wrapper for errdefer-bearing blocks. Returns block_id. fn enter_defer_scope(&mut self, block: &Block, is_loop_body: bool) -> usize { if !Self::block_has_defers(block) && !self.block_has_auto_cleanup_lets(block) { return 0; } self.defer_block_counter += 1; let block_id = self.defer_block_counter; let mut entries: Vec<DeferEntry> = Vec::new(); let mut idx = 0usize; for s in &block.stmts { // Plan 173 Ф.1 (#4): only plain `defer` remains (D189). // Plan 217 (гибрид C): bare auto-cleanup-eligible `consume X = e;` // gets its OWN entry too (`consume_policy: Some`) — same per- // block LIFO stack, same four run-sites (leave_defer_scope / // emit_early_exit_cleanup already branch on `consume_policy` // generically, see их doc-comments). `active_var`/`prevdl`/ // `ccount` are declared here (prologue, unconditionally 0/NULL) // but only ARMED when `emit_stmt` actually reaches this exact // `Stmt::Let` (partial-init safety — matches ConsumeScope's own // "arm only after init captured" discipline). if let Stmt::Let(decl) = s { if let Some(init_c_type) = self.auto_cleanup_qualifies(decl) { if let Pattern::Ident { name, .. } = &decl.pattern { // Plan 217 BUGFIX (2nd — found via probe run after the // c_binding-timing fix: "use of undeclared identifier // 'r'"): the FAIL/INTERRUPT run-site code emitted // right below (still inside THIS prologue) references // the binding's C name — but the REAL `Stmt::Let` // declaration only happens later, when `emit_stmt` // sequentially reaches this exact statement. Hoist a // pre-declaration here (same discipline as the // `errdefer_refs` mechanism just below, which covers // OTHER defer bodies referencing a not-yet-declared // var — but that pass doesn't know about THIS var // since my "body" is a placeholder UnitLit, not a // real AST reference). `hoisted_let_vars` makes the // real `Stmt::Let` emit assignment-only (no // redeclaration, `is_hoisted` branch). if !self.hoisted_let_vars.contains(name.as_str()) { // NovaValue_/mono-`____` value-records — структуры, // которых is_struct_type не знает (флагман-регрессия // `NovaValue_TcpStream st = 0`): им тоже `{0}`. let init_expr = if init_c_type.ends_with('*') { "NULL".to_string() } else if Self::is_struct_type(&init_c_type) || init_c_type.starts_with("NovaValue_") || init_c_type.contains("____") { "{0}".to_string() } else { "0".to_string() }; let name_c = Self::mangle_field_name(name); self.line(&format!( "{} {} = {}; /* Plan 217: hoisted for auto-cleanup FAIL/INTERRUPT run-site */", init_c_type, name_c, init_expr)); self.hoisted_let_vars.insert(name.clone()); self.var_types.insert(name.clone(), init_c_type.clone()); } } // План 253.4 Ф.1: NOVA-имя биндинга берётся РОВНО ОДИН // раз (прежде `pattern_binding` звался дважды — здесь и // в `emit_auto_cleanup_arm`; для `Pattern::Wildcard` он // выдаёт СВЕЖИЙ `fresh_tmp` на каждый вызов, то есть два // разных имени на одну переменную — ещё одна расхождение- // точка, снятая заодно). let nova_binding = self.pattern_binding(&decl.pattern).unwrap_or_default(); // План 253.4 Ф.1 (решение владельца 2026-08-08, уточнение // №2): имя флага строится из C-ИДЕНТИФИКАТОРА переменной // (`mangle_field_name` — то же отображение, которым // объявляется сама переменная выше), а НЕ из номера блока // и порядкового номера. Прежнее `_defer_<bid>_<idx>_active` // из имени переменной не выводилось — именно это и // вынуждало вести реестр (№480). Теневание разрешает сам // C: вложенный блок объявит свой флаг рядом со своей // одноимённой переменной и затенит внешний ровно так же, // как затеняет переменную. let active_var = format!("_defer_{}_active", Self::mangle_field_name(&nova_binding)); let prevdl_var = format!("_defer_{}_prevdl", Self::mangle_field_name(&nova_binding)); let ccount_var = format!("_defer_{}_ccount", Self::mangle_field_name(&nova_binding)); let type_name = { let t = self.debt_strip_nova_trim_start(&init_c_type); t.strip_prefix("NovaValue_").map(|s| s.to_string()).unwrap_or(t) }; // Plan 217 BUGFIX (found via probe run — CC-FAIL "expected // expression", `Nova_P217Res_consume_cleanup(, x)`): the // FAIL/INTERRUPT run-site C code below is emitted RIGHT // HERE, in this prologue — i.e. BEFORE the `Stmt::Let` is // ever sequentially reached by `emit_stmt`. Deferring // `c_binding` to `emit_auto_cleanup_arm` (which patches // `self.defer_scopes`'s copy much later) is USELESS for // this immediately-emitted code — it read the empty // placeholder. The binding's C NAME (and whether it needs // the value-record `&`-wrap) is fully determined by the // AST + `init_c_type` ALREADY, so compute it now instead; // `emit_auto_cleanup_arm` no longer needs to patch it // (kept as a defensive no-op re-assignment there). let c_binding = if init_c_type.starts_with("NovaValue_") && !init_c_type.ends_with('*') { format!("(&{})", nova_binding) } else { nova_binding.clone() }; entries.push(DeferEntry { active_var: active_var.clone(), body: Expr::new(ExprKind::UnitLit, Span::default()), outcome_binding: None, consume_policy: Some(ConsumePolicy { type_name, c_binding, prev_deadline_var: prevdl_var.clone(), // Plan 217: ResourceTrace observability reused // as-is (same NULL-guarded `on_resource_enter`/ // `_exit` pair as ConsumeScope — cheap, and gives // the feature the same structural observability // D185 already grants explicit `consume{}`). has_resource_trace: self.effect_schemas.contains_key("ResourceTrace"), count_var: ccount_var.clone(), // Plan 217 keystone (§6 оговорка): cancel-shield // ЕСТЬ (cleanup should not be cancel-interrupted // mid-flight — same reasoning as ConsumeScope), // watchdog-порог НЕ подключён (secondary D188 R3 // feature, вне keystone-скоупа 217; "0" = // disarmed watchdog, harmless literal per // `nv_cleanup_watchdog_arm`). threshold_var: "0".to_string(), // План 253.4 Ф.1: связь «переменная → её флаг → // её defer» существует ПО ПОСТРОЕНИЮ — одна // запись несёт всё трое. Реестра нет. nova_binding: Some(nova_binding.clone()), re_consume: false, }), }); self.line(&format!("int {} = 0;", active_var)); self.line(&format!("int64_t {} = 0;", prevdl_var)); self.line(&format!("int {} = 0;", ccount_var)); self.auto_cleanup_arm_sites.insert(decl.span, (block_id, idx, init_c_type)); idx += 1; continue; } } let (body, outcome_binding) = match s { Stmt::Defer { body, outcome_binding, .. } => (body, outcome_binding.clone()), _ => continue, }; let var = format!("_defer_{}_{}_active", block_id, idx); entries.push(DeferEntry { active_var: var.clone(), body: body.clone(), outcome_binding, consume_policy: None, }); self.line(&format!("int {} = 0;", var)); idx += 1; } // Plan 100.8 (D166) C-codegen fix: hoist Let bindings referenced in // errdefer/defer bodies so they're declared BEFORE the setjmp handler. // In C, a variable's scope starts at its declaration; if an errdefer // handler references `tx` but `consume tx = begin()` is emitted AFTER // the setjmp, clang/gcc reject it as "use of undeclared identifier". // Fix: pre-declare as `type* name = NULL;` (pointer) or `type name = 0;` // (scalar). The real `emit_stmt` for `Stmt::Let` then emits only the // assignment, and removes from `hoisted_let_vars`. { let mut errdefer_refs: HashSet<String> = HashSet::new(); for entry in &entries { // Collect from ALL defer kinds (errdefer, plain, with-result) // since any of them could reference a variable before declaration. Self::collect_free_idents(&entry.body, &mut errdefer_refs); } if !errdefer_refs.is_empty() { for s in &block.stmts { if let Stmt::Let(decl) = s { if let crate::ast::Pattern::Ident { name, .. } = &decl.pattern { if errdefer_refs.contains(name.as_str()) && !self.hoisted_let_vars.contains(name.as_str()) { // Infer C type for the pre-declaration. // [M-172.1-d174-sync-consume-registry] phase-safety (§0c): // этот hoist — PRE-PASS на входе в блок, ДО эмиссии // let-binding'ов; полный re-derive `infer_expr_c_type(value)` // здесь бьётся в P67-LEGACY пробы на ещё-не-зарегистрированных // локалах (например `consume g = mu.lock()` при `defer // g.unlock()`). Потребляем только МАТЕРИАЛИЗОВАННОЕ: канал // чекера; неизвестно → opaque `void*`-predecl (это C-механизм // forward-декларации, не «тип» — реальный тип придёт при // эмиссии Let-присваивания; указатели C конвертирует неявно). let c_ty = if let Some(ty) = &decl.ty { self.type_ref_to_c(ty).unwrap_or_else(|_| "void*".to_string()) } else { let ch = if decl.value.id.is_set() { self.resolved_types .get(&decl.value.id) .and_then(|rt| self.resolved_type_to_c(rt).ok()) .filter(|t| !t.is_empty()) } else { None }; ch.unwrap_or_else(|| "void*".to_string()) }; // Null/zero initializer based on type. let init = if c_ty.ends_with('*') { "NULL" } else { "0" }; // [M-c-keyword-ident-collision]: text-only mangle; // `hoisted_let_vars`/downstream lookups stay keyed // by the raw Nova name. self.line(&format!("{} {} = {}; /* hoisted for errdefer */", c_ty, Self::mangle_field_name(name), init)); self.hoisted_let_vars.insert(name.clone()); // [M-runtime-sync-guard-consume-p67] fix: the C-level // forward-decl above is not enough — the FAIL-path // defer body for THIS SAME entry is emitted a few // lines below (`emit_defer_body_with_outcome`, still // inside `enter_defer_scope`, i.e. BEFORE the block's // sequential `Stmt::Let` loop ever reaches the real // `consume <name> = …` and registers it). Without a // matching `var_types` entry, any expr in the defer // body that references `<name>` as a bare Ident // (receiver of a method call, e.g. `guard.unlock()`) // falls through `infer_expr_c_type`'s Ident arm all // the way to the `[P67-LEGACY]` ICE — witnessed via // `Mutex.with_lock[R]`'s own body (`consume guard = // self.lock(); defer guard.unlock(); body()`) during // its generic mono-instantiation. The real `Stmt::Let` // unconditionally overwrites this entry with the // precise inferred type once it actually runs (see // the plain `self.var_types.insert(binding.clone(), // ty_c.clone())` in the `Stmt::Let` arm below) — this // is only a placeholder for the narrow FAIL-path // pre-emission window. self.var_types.insert(name.clone(), c_ty.clone()); } } } } } } let failframe_var = format!("_defer_{}_ff", block_id); let failframe_popped_var = format!("_defer_{}_ff_popped", block_id); // Plan 20 Ф.8 follow-up (3): fail-frame нужен ВСЕГДА когда есть // defer. Spec D90 п.8: defer fires on throw. Без local fail-frame'а // throw скипает scope с longjmp'ом, defer cleanup пропускается. // Plan 173 Ф.1 (#4): все defer'ы плейн — бегут на fail-path (LIFO). self.line(&format!("int {} = 0;", failframe_popped_var)); self.line(&format!("NovaFailFrame {};", failframe_var)); self.line(&format!("nova_fail_push(&{});", failframe_var)); self.line(&format!("if (setjmp({}.jmp) != 0) {{", failframe_var)); self.indent += 1; // Error path: invoke defer + errdefer; SKIP okdefer (D160: success-only). // Plan 100.4.4 (D161): per-defer NovaFailFrame wrap; LIFO continues // despite individual failures. Primary error = scope's outer fail-frame // (`failframe_var`); defer-fails compose к suppressed chain. let comp_chain_t = format!("_defer_{}_throw_chain", block_id); self.line(&format!("NovaErrorChain* {} = {}.error_suppressed;", comp_chain_t, failframe_var)); for (i, entry) in entries.iter().enumerate().rev() { // Plan 217: consume-flavored entry (bare auto-cleanup consume- // let) — dispatch the REAL `@cleanup(outcome)` via // `emit_consume_entry_cleanup`, mirroring `enter_consume_defer_ // scope`'s own FAIL run-site exactly (that copy is what the // explicit `consume X = e { body }` form uses; this generic // per-block loop previously only special-cased `consume_policy` // in `leave_defer_scope`/`emit_early_exit_cleanup` — a bare // auto-cleanup binding whose OWN block throws mid-way re-enters // HERE first, and fell through to the plain-defer path (a no-op // placeholder body), silently skipping cleanup on this run-site. // Found via probe3's throw-path fixture — genuine gap, not a // hypothetical. if let Some(policy) = &entry.consume_policy { self.line(&format!("if ({}) {{", entry.active_var)); self.indent += 1; self.line(&format!("{} = 0;", entry.active_var)); let df = format!("_defer_{}_{}_tcdf", block_id, i); self.emit_consume_entry_cleanup( policy, DeferOutcome::FromFrame(&failframe_var), &df, ConsumeTail::FailChain { failframe: failframe_var.clone(), chain: comp_chain_t.clone() }); self.indent -= 1; self.line("}"); continue; } // Plan 173 Ф.1 (#4): все defer'ы плейн — бегут на error-path. let df = format!("_defer_{}_{}_tdf", block_id, i); self.line(&format!("if ({}) {{", entry.active_var)); self.indent += 1; self.line(&format!("{} = 0;", entry.active_var)); self.line(&format!("NovaFailFrame {};", df)); self.line(&format!("nova_fail_push(&{});", df)); // Plan 173 Ф.4 #6 (model B): unwind-path cleanup — a failing body // must land HERE (compose), не в scope-handler (dispatch bypass). self.line(&format!("{}.is_cleanup = 1;", df)); self.line(&format!("{}.error_suppressed = NULL;", df)); self.line(&format!("if (setjmp({}.jmp) == 0) {{", df)); self.indent += 1; // Plan 173 Ф.2.B2: throw/panic/cancel-path — outcome из scope fail-frame. let _ = self.emit_defer_body_with_outcome(&entry.outcome_binding, &entry.body, DeferOutcome::FromFrame(&failframe_var)); self.line("nova_fail_pop();"); self.indent -= 1; self.line("} else {"); self.indent += 1; self.line("nova_fail_pop();"); // Plan 173 Ф.2.B3-merge (D314 §4a): panic-dominance compose. self.emit_fail_cleanup_compose(&df, &failframe_var, &comp_chain_t); self.indent -= 1; self.line("}"); self.indent -= 1; self.line("}"); } self.line("nova_fail_pop();"); self.line(&format!("{} = 1;", failframe_popped_var)); // Plan 100.4.4: re-throw к outer fail-frame preserving primary + // composed suppressed chain (typed payload preserved). // Plan 173 Ф.2.C: TRANSPARENT terminal — единый nova_scope_exit (любой // не-Success kind проброшен через nova_rethrow_with_suppressed; ≡ прежнему // безусловному rethrow, т.к. FAIL run-site достижим только через throw). self.line(&format!("{}.error_suppressed = {};", failframe_var, comp_chain_t)); self.line(&format!("nova_scope_exit(&{}, NOVA_SCOPE_EXIT_TRANSPARENT);", failframe_var)); self.indent -= 1; self.line("}"); // Plan 20 Ф.8 (2): interrupt-path cleanup для `defer`. // По D90 п.8 `defer` запускается на ВСЕХ exit'ах, включая // `interrupt v` (когда outer handler делает interrupt → longjmp // на NovaInterruptFrame, минуя fail-frame). // Эмитим local interrupt-frame setjmp wrapper, который перехватывает // interrupt longjmp, запускает `defer` cleanup (НЕ errdefer/okdefer — это // handled exit), pop'ает interrupt-frame и re-interrupt'ит с тем // же value, чтобы outer interrupt-frame получил value. let intframe_var = format!("_defer_{}_if", block_id); let intframe_popped_var = format!("_defer_{}_if_popped", block_id); self.line(&format!("int {} = 0;", intframe_popped_var)); self.line(&format!("NovaInterruptFrame {};", intframe_var)); // Plan 61 followup #1: defer scopes использ kind=DEFER_SCOPE — это // tells nova_interrupt route handler-arm interrupts через cleanup // chain (vs cross-effect direct-jump к owner). self.line(&format!("nova_interrupt_push_defer(&{});", intframe_var)); self.line(&format!("if (setjmp({}.jmp) != 0) {{", intframe_var)); self.indent += 1; // Interrupt path: invoke defer (Plan 173 Ф.1 #4 — все defer'ы плейн, // бегут на interrupt так же как на normal/error exit). // // Plan 173 Ф.4 #6 (D158 model B): a `with Fail = |e| interrupt …` catch // recovers the primary at the throw-site, then unwinds THIS scope via // `interrupt` (kind=DEFER_SCOPE keeps `_nova_last_error.live` set). A // cleanup that ITSELF fails while unwinding must NOT be lost and must NOT // hijack the interrupt — it composes into the suppressed "pocket" // (`_nova_last_error.frame.error_suppressed`, read post-catch by // `suppressed()`). Each cleanup body runs in its own NovaFailFrame so a // throwing cleanup returns here (compose + continue re-issue) instead of // longjmp'ing past the remaining cleanups. A cleanup PANIC (D13 // abort-class) is NOT swallowed — it re-raises via nv_panic. for (i, entry) in entries.iter().enumerate().rev() { // Plan 217: same gap as the FAIL run-site above — consume- // flavored entry dispatches the real `@cleanup(Interrupt)` here // too (mirrors `enter_consume_defer_scope`'s own INTERRUPT // run-site; `ConsumeTail::Swallow` — a cleanup-failure during // an interrupt-unwind is dropped, control must still reach the // interrupt target). if let Some(policy) = &entry.consume_policy { self.line(&format!("if ({}) {{", entry.active_var)); self.indent += 1; self.line(&format!("{} = 0;", entry.active_var)); let df = format!("_defer_{}_{}_icdf", block_id, i); self.emit_consume_entry_cleanup(policy, DeferOutcome::Interrupt, &df, ConsumeTail::Swallow); self.indent -= 1; self.line("}"); continue; } self.line(&format!("if ({}) {{", entry.active_var)); self.indent += 1; let idf = format!("_defer_{}_{}_idf", block_id, i); let saved = format!("_defer_{}_{}_idf_supp", block_id, i); // Preserve the accumulating pocket across the cleanup's OWN throw: // a throwing cleanup calls nova_throw*/nova_last_error_set, which // resets `_nova_last_error.frame.error_suppressed = NULL`. Snapshot // the head first, then relink the composed node onto it — so each // firing cleanup PREPENDS rather than clobbering the chain. self.line(&format!( "NovaErrorChain* {} = _nova_last_error.frame.error_suppressed;", saved)); self.line(&format!("NovaFailFrame {};", idf)); self.line(&format!("nova_fail_push(&{});", idf)); // Plan 173 Ф.4 #6 (model B): unwind-path cleanup — dispatch bypass // (см. FAIL-path `_tdf` mark; иначе scope-handler misfires/hijacks). self.line(&format!("{}.is_cleanup = 1;", idf)); self.line(&format!("{}.error_suppressed = NULL;", idf)); self.line(&format!("if (setjmp({}.jmp) == 0) {{", idf)); self.indent += 1; // Plan 173 Ф.2.B2: interrupt-path → Failure (core.nv:130). let _ = self.emit_defer_body_with_outcome(&entry.outcome_binding, &entry.body, DeferOutcome::Interrupt); self.line("nova_fail_pop();"); // Clean cleanup: restore the pocket (an inner throw+catch inside the // body may have reset it) — the accumulated chain must survive. self.line(&format!("_nova_last_error.frame.error_suppressed = {};", saved)); self.indent -= 1; self.line("} else {"); self.indent += 1; self.line("nova_fail_pop();"); // Cleanup failed while unwinding an interrupt-recovered error. self.line(&format!("if ({}.error_kind == NOVA_THROW_PANIC) {{", idf)); self.indent += 1; // D13: a cleanup panic dominates — re-raise (fiber death), не глотаем. self.line(&format!("nv_panic({}.error_msg);", idf)); self.indent -= 1; self.line("} else {"); self.indent += 1; // Compose (prepend) into the suppressed pocket; head = most-recent // (suppressed() walks back-to-front → chronological firing order). self.line("NovaErrorChain* _inode = (NovaErrorChain*)nova_alloc(sizeof(NovaErrorChain));"); self.line(&format!("_inode->msg = {}.error_msg;", idf)); self.line(&format!("_inode->kind = {}.error_kind;", idf)); self.line(&format!("_inode->user_payload = {}.error_user_payload;", idf)); self.line(&format!("_inode->user_type_id = {}.error_user_type_id;", idf)); self.line(&format!("_inode->next = {};", saved)); self.line("_nova_last_error.frame.error_suppressed = _inode;"); self.indent -= 1; self.line("}"); self.indent -= 1; self.line("}"); self.indent -= 1; self.line("}"); } self.line("nova_interrupt_pop();"); self.line(&format!("{} = 1;", intframe_popped_var)); // Re-interrupt с тем же value через nova_interrupt — find outer // interrupt frame and longjmp туда с captured value. // Plan 39 Issue A: defer-scope не знает category outer with-блока, // поэтому re-issue ОБА slot'а: outer frame прочитает нужный по // своей category. Сохраняем оба значения, выбираем по тому что // непустое: если value_ptr != NULL — pointer-route, иначе int. self.line(&format!( "if ({}.value_ptr) {{ nova_interrupt_ptr({}.value_ptr); }} else {{ nova_interrupt({}.value); }}", intframe_var, intframe_var, intframe_var)); self.indent -= 1; self.line("}"); self.defer_scopes.push(DeferScope { block_id, entries, next_idx: 0, // Plan 173 Ф.1 (#4): fail-frame ставится всегда при наличии defer // (не только при path-selective, коих больше нет); поле diagnostic-only. needs_failframe: false, failframe_var, failframe_popped_var, intframe_var, intframe_popped_var, is_loop_body, }); block_id } /// Emit cleanup for the current top defer scope (normal-exit path): /// invokes each entry's body in LIFO. Skips `errdefer` entries since /// is_error=0 on normal exit. Pops fail-frame if present, and pops /// the scope from the stack. fn leave_defer_scope(&mut self, block_id: usize) { if block_id == 0 { return; } let scope = self.defer_scopes.pop().expect("defer_scopes balanced"); debug_assert_eq!(scope.block_id, block_id); // Plan 217: drop this block's auto-cleanup bindings from the // disarm-lookup registry — the C variables they'd disarm no longer // exist past this point (either already fired below, or the block // never reached them). Single choke-point (every block already // calls `leave_defer_scope` exactly once) — no per-call-site // bookkeeping needed for the (name, block_id, idx) triples pushed // by `emit_auto_cleanup_arm`. // Plan 100.4.4 (D161): Per-defer NovaFailFrame wrap — каждый defer body // в своём setjmp envelope. LIFO continues despite individual failures; // failures accumulate в local compose-state, re-throw at end если any. // // Normal exit: run all defers (Plan 173 Ф.1 #4 — plain-only, D189). // // Composition rules (D161): // - 1st fail → primary; 2nd+ fails → appended to suppressed chain (LIFO order). // - After all defers attempted, scope's own fail-frame popped, then если // accumulated fail → longjmp to outer (_nova_fail_top) с composed payload. let comp_msg = format!("_defer_{}_comp_msg", scope.block_id); let comp_kind = format!("_defer_{}_comp_kind", scope.block_id); let comp_payload = format!("_defer_{}_comp_payload", scope.block_id); let comp_tid = format!("_defer_{}_comp_tid", scope.block_id); let comp_chain = format!("_defer_{}_comp_chain", scope.block_id); let comp_has = format!("_defer_{}_comp_has", scope.block_id); self.line(&format!("nova_str {} = (nova_str){{0}};", comp_msg)); self.line(&format!("NovaThrowKind {} = NOVA_THROW_USER;", comp_kind)); self.line(&format!("void* {} = NULL;", comp_payload)); self.line(&format!("NovaTypeId {} = 0;", comp_tid)); self.line(&format!("NovaErrorChain* {} = NULL;", comp_chain)); self.line(&format!("int {} = 0;", comp_has)); for (i, entry) in scope.entries.iter().enumerate().rev() { // Plan 173 Ф.2.B3-merge (D314 §3): consume-flavored entry → normal // scope exit runs `@cleanup(Success)` + policy (shield-leave, RT-exit), // composing a failing cleanup into the leave-compose slot (§4a). if let Some(policy) = &entry.consume_policy { self.line(&format!("if ({}) {{", entry.active_var)); self.indent += 1; self.line(&format!("{} = 0;", entry.active_var)); let df = format!("_defer_{}_{}_cdf", scope.block_id, i); self.emit_consume_entry_cleanup(policy, DeferOutcome::Success, &df, ConsumeTail::LeaveComp { comp_has: comp_has.clone(), comp_msg: comp_msg.clone(), comp_kind: comp_kind.clone(), comp_payload: comp_payload.clone(), comp_tid: comp_tid.clone(), comp_chain: comp_chain.clone(), }); self.indent -= 1; self.line("}"); continue; } // Plan 173 Ф.1 (#4): все defer'ы плейн — бегут на success-path. let df = format!("_defer_{}_{}_df", scope.block_id, i); self.line(&format!("if ({}) {{", entry.active_var)); self.indent += 1; self.line(&format!("{} = 0;", entry.active_var)); self.line(&format!("NovaFailFrame {};", df)); self.line(&format!("nova_fail_push(&{});", df)); self.line(&format!("{}.error_suppressed = NULL;", df)); self.line(&format!("if (setjmp({}.jmp) == 0) {{", df)); self.indent += 1; // Plan 173 Ф.2.B2: normal-exit path → Success. let _ = self.emit_defer_body_with_outcome(&entry.outcome_binding, &entry.body, DeferOutcome::Success); self.line("nova_fail_pop();"); self.indent -= 1; self.line("} else {"); self.indent += 1; self.line("nova_fail_pop();"); // Plan 173 Ф.2.B3-merge (D314 §4a): panic-dominance. A cleanup that // PANICs dominates any prior cleanup-throw-primary (D13: panic = // abort-class bug > recoverable throw) — override the compose slot to // PANIC. Otherwise: first fail → primary; later fails → suppressed // LIFO chain (D158). self.line(&format!("if ({}.error_kind == NOVA_THROW_PANIC) {{", df)); self.indent += 1; self.line(&format!("{} = 1;", comp_has)); self.line(&format!("{} = {}.error_msg;", comp_msg, df)); self.line(&format!("{} = NOVA_THROW_PANIC;", comp_kind)); self.line(&format!("{} = {}.error_user_payload;", comp_payload, df)); self.line(&format!("{} = {}.error_user_type_id;", comp_tid, df)); self.indent -= 1; self.line(&format!("}} else if (!{}) {{", comp_has)); self.indent += 1; self.line(&format!("{} = 1;", comp_has)); self.line(&format!("{} = {}.error_msg;", comp_msg, df)); self.line(&format!("{} = {}.error_kind;", comp_kind, df)); self.line(&format!("{} = {}.error_user_payload;", comp_payload, df)); self.line(&format!("{} = {}.error_user_type_id;", comp_tid, df)); self.indent -= 1; self.line("} else {"); self.indent += 1; self.line("NovaErrorChain* _node = (NovaErrorChain*)nova_alloc(sizeof(NovaErrorChain));"); self.line(&format!("_node->msg = {}.error_msg;", df)); self.line(&format!("_node->kind = {}.error_kind;", df)); self.line(&format!("_node->user_payload = {}.error_user_payload;", df)); self.line(&format!("_node->user_type_id = {}.error_user_type_id;", df)); self.line(&format!("_node->next = {};", comp_chain)); self.line(&format!("{} = _node;", comp_chain)); self.indent -= 1; self.line("}"); self.indent -= 1; self.line("}"); self.indent -= 1; self.line("}"); } // Fail-frame теперь всегда push'нут (Plan 20 Ф.8 follow-up). // Skip повторный pop, если early-exit cleanup уже сделал pop. self.line(&format!("if (!{}) {{ nova_fail_pop(); }}", scope.failframe_popped_var)); // Plan 20 Ф.8 (2): pop interrupt-frame (всегда push'нут когда has_defer). self.line(&format!("if (!{}) {{ nova_interrupt_pop(); }}", scope.intframe_popped_var)); // Plan 100.4.4: if defer-cleanup accumulated failures — re-throw к outer // с composed chain. Если нет outer fail-frame — abort с diagnostic dump. self.line(&format!("if ({}) {{", comp_has)); self.indent += 1; self.line("if (_nova_fail_top) {"); self.indent += 1; self.line(&format!("_nova_fail_top->error_msg = {};", comp_msg)); self.line(&format!("_nova_fail_top->error_kind = {};", comp_kind)); self.line(&format!("_nova_fail_top->error_user_payload = {};", comp_payload)); self.line(&format!("_nova_fail_top->error_user_type_id = {};", comp_tid)); self.line(&format!("_nova_fail_top->error_suppressed = {};", comp_chain)); self.line("longjmp(_nova_fail_top->jmp, 1);"); self.indent -= 1; self.line("} else {"); self.indent += 1; self.line("fflush(stdout);"); self.line(&format!("fprintf(stderr, \"nova: defer cleanup-fail with no outer handler: %.*s\\n\", (int){}.len, {}.ptr);", comp_msg, comp_msg)); self.line("abort();"); self.indent -= 1; self.line("}"); self.indent -= 1; self.line("}"); } /// Emit defer-cleanup for an early exit (return/break/continue) walking /// scopes from innermost outward. `stop_at_loop` means walk only inner /// scopes up to (but not including) the first loop-body scope — used by /// break/continue. `stop_at_loop=false` means walk ALL scopes — used by /// return. /// Emit defer-cleanup for an early exit: /// - return: walk ALL scopes (fn-level exit; ALL leave_defer_scope's /// remaining cleanup will NOT run, so pop fail-frames manually). /// - break/continue: walk ONLY the innermost loop-body scope (the C /// `break`/`continue` exits one loop level — outer scopes remain /// active and clean themselves up later via their own leave_defer_scope). /// /// In both cases we DEACTIVATE the defer flag (`= 0`) so that the eventual /// leave_defer_scope or fail-frame longjmp handler doesn't re-invoke. fn emit_early_exit_cleanup(&mut self, stop_at_loop: bool) { // Plan cleanup без clone(): сначала вытаскиваем scopes из // self.defer_scopes (mem::take заменяет на пустой Vec, освобождая // borrow), iterate over них, emit, потом возвращаем обратно. // Это позволяет вызывать &mut-методы (self.line, emit_defer_body_void) // внутри loop'а без borrow conflict. let scopes = std::mem::take(&mut self.defer_scopes); 'outer: for scope in scopes.iter().rev() { // Early exit via return — run all defers (Plan 173 Ф.1 #4: plain-only). for (i, entry) in scope.entries.iter().enumerate().rev() { // Plan 173 Ф.2.B3-merge (D314 §3): consume-flavored entry → run // `@cleanup(Success)` + policy on the early exit (return/break/ // continue). The `Return` handler stashes the value BEFORE this, // so the cleanup cannot invalidate it. Cleanup-failure during the // exit is dropped (Swallow) — the control-flow must reach its target. if let Some(policy) = &entry.consume_policy { self.line(&format!("if ({}) {{", entry.active_var)); self.indent += 1; self.line(&format!("{} = 0;", entry.active_var)); let df = format!("_defer_{}_{}_ecdf", scope.block_id, i); self.emit_consume_entry_cleanup(policy, DeferOutcome::Success, &df, ConsumeTail::Swallow); self.indent -= 1; self.line("}"); continue; } self.line(&format!("if ({}) {{", entry.active_var)); self.indent += 1; // Plan 173 Ф.2.B2: early-exit (return/break/continue) → Success. let _ = self.emit_defer_body_with_outcome(&entry.outcome_binding, &entry.body, DeferOutcome::Success); self.line(&format!("{} = 0;", entry.active_var)); self.indent -= 1; self.line("}"); } // For return: pop fail-frames as we go (control will never reach // leave_defer_scope of these scopes again). For break/continue: // ONLY the innermost loop scope gets the pop (we walk just one). // Outer scopes remain active, will pop normally at their own // leave_defer_scope. // Fail-frame теперь всегда push'нут (Ф.8 follow-up). self.line("nova_fail_pop();"); self.line(&format!("{} = 1;", scope.failframe_popped_var)); // Plan 20 Ф.8 (2): pop interrupt-frame для early-exit тоже. self.line("nova_interrupt_pop();"); self.line(&format!("{} = 1;", scope.intframe_popped_var)); if stop_at_loop && scope.is_loop_body { break 'outer; } } // Восстанавливаем scopes — early-exit cleanup НЕ pop'ает scopes из // стека (это разные операции; pop scope происходит только в // leave_defer_scope). self.defer_scopes = scopes; } /// Emit a loop-body block (for/while/loop): integrates defer-scope around /// the body so defer/errdefer на каждой итерации correctly runs (LIFO, /// throw-path through NovaFailFrame). is_loop_body=true: break/continue /// в нашей собственной body — local, не пересекают loop boundary. fn emit_loop_body_inline(&mut self, body: &Block) -> Result<(), String> { self.emit_loop_body_inline_ex(body, false) } /// [M-opt-preempt-strided-loop] Part A: `skip_preempt` omits the /// per-iteration safepoint for provably-short loops (constant/small /// bound). Such loops cannot monopolise a worker (bounded by /// construction), and the `nova_preempt_check()` call is a barrier that /// blocks clang vectorization/unroll of tight bodies. Variable/unbounded /// loops MUST pass `false` — else tight-loop-starvation returns (a /// CPU-bound fiber on a large/unbounded loop monopolises its worker, /// exactly what the per-iteration safepoint prevents). fn emit_loop_body_inline_ex(&mut self, body: &Block, skip_preempt: bool) -> Result<(), String> { let block_id = self.enter_defer_scope(body, true); // [M-217-break-continue-loop-boundary-bleed]: record whether THIS // loop actually registered a real defer-scope (`block_id != 0`) so // `Stmt::Break`/`Stmt::Continue` can tell a genuine loop-boundary // marker from an absent one — see `loop_body_has_scope` field doc. self.loop_body_has_scope.push(block_id != 0); // Plan 44.7: preemption safepoint at the loop backedge. Emitted as // the first statement of the body so it runs at the start of every // iteration — this also covers the `continue` edge (continue jumps // to the condition, re-enters the body, hits this check). Without // it a tight arithmetic loop with no function calls (`while i < N // { i = i + 1 }`) would never reach a prologue safepoint and could // monopolise its worker. No-op in single-thread mode. if !skip_preempt { self.line("nova_preempt_check();"); } for stmt in &body.stmts { self.emit_stmt(stmt)?; } if let Some(trailing) = &body.trailing { let v = self.emit_expr(trailing)?; self.line(&format!("(void)({});", v)); } self.leave_defer_scope(block_id); self.loop_body_has_scope.pop(); Ok(()) } /// [M-opt-preempt-strided-loop] Part A: compile-time integer value of a /// literal range bound (`0`, `16`). `None` if not a plain integer literal /// — callers then conservatively KEEP the preempt-check (correctness over /// optimization). Only plain literals fold; variables/arithmetic/negatives /// are treated as non-const (safe under-approximation). fn loop_bound_int_literal(e: &Expr) -> Option<i64> { match &e.kind { ExprKind::IntLit(n) => Some(*n), _ => None, } } /// [M-opt-preempt-strided-loop] Part B: recognize a pure element-wise copy /// loop `for i in lo..hi { dst[i] = src[i] }` over `Vec[T]` and lower it to /// a single overlap-safe bulk copy (memmove) — eliminating the per-element /// loop entirely (no preempt-check, no per-element bounds branch; clang /// emits a vectorized memmove). Returns `Some(tmp)` when lowered, `None` /// when the body is not a recognized safe copy (caller emits the loop). /// /// Conservative — lowers ONLY when ALL hold (else fall back to the loop): /// * body is exactly one `dst[i] = src[i]` plain assign, no trailing; /// * both indices are exactly the loop variable; /// * `dst`/`src` are plain identifiers (pure, single-eval, no side fx); /// * both are the SAME `Vec[T]` type (flat `T*` storage → slot-copy == /// value-copy), excluding raw `*mut Vec` buffers (`...**`); /// * element is a flat POD: primitive/value-record (`nova_*`) or pointer /// (`..._p`) — inline-struct elements are skipped (slot-copy may differ). /// Overlap-safe `memmove` (RawMem.copy semantics) handles self-copy / views; /// the bounds guard preserves the per-element OOB-panic. fn try_emit_range_copy_memmove( &mut self, binding: &str, start: &Expr, end: &Expr, inclusive: bool, body: &Block, ) -> Result<Option<String>, String> { if body.stmts.len() != 1 || body.trailing.is_some() { return Ok(None); } let (target, value) = match &body.stmts[0] { Stmt::Assign { target, op: AssignOp::Assign, value, .. } => (target, value), _ => return Ok(None), }; let (dst_obj, dst_idx) = match &target.kind { ExprKind::Index { obj, index } => (obj, index), _ => return Ok(None), }; let (src_obj, src_idx) = match &value.kind { ExprKind::Index { obj, index } => (obj, index), _ => return Ok(None), }; // Both indices must be exactly the loop variable. let idx_ok = |k: &ExprKind| matches!(k, ExprKind::Ident(n) if n == binding); if !idx_ok(&dst_idx.kind) || !idx_ok(&src_idx.kind) { return Ok(None); } // `dst`/`src` must be plain identifiers — pure, evaluated once, no aliasing // surprises from complex receiver exprs. if !matches!(&dst_obj.kind, ExprKind::Ident(_)) || !matches!(&src_obj.kind, ExprKind::Ident(_)) { return Ok(None); } // Same `Vec[T]` value type on both sides (flat `T*` slot storage). // Exclude raw `*mut Vec` (`Nova_Vec____...**`) — that is a pointer, not a Vec. let dst_ty = self.infer_expr_c_type(dst_obj); let src_ty = self.infer_expr_c_type(src_obj); if !dst_ty.starts_with("Nova_Vec____") || dst_ty.trim_end().ends_with("**") || dst_ty != src_ty { return Ok(None); } // Element must be a flat POD slot (slot-copy == value-copy): primitive / // value-record (`nova_*`) or pointer element (`..._p`). Inline composite // elements are skipped — conservative. let elem_suffix = dst_ty .trim_end_matches('*') .trim() .strip_prefix("Nova_Vec____") .unwrap_or(""); let elem_flat_pod = elem_suffix.starts_with("nova_") || elem_suffix.ends_with("_p"); if !elem_flat_pod { return Ok(None); } // --- Recognized: emit overlap-correct bulk copy in place of the loop. --- let dst_c = self.emit_expr(dst_obj)?; let src_c = self.emit_expr(src_obj)?; let lo_c = self.emit_expr(start)?; let hi_c = self.emit_expr(end)?; let vd = self.fresh_tmp(); let vs = self.fresh_tmp(); let vlo = self.fresh_tmp(); let vlast = self.fresh_tmp(); let vn = self.fresh_tmp(); let vds = self.fresh_tmp(); let vss = self.fresh_tmp(); let vnb = self.fresh_tmp(); let vk = self.fresh_tmp(); let tmp = self.fresh_tmp(); self.line(&format!("nova_unit {};", tmp)); self.line("{"); self.indent += 1; self.line(&format!("{} {} = {};", dst_ty, vd, dst_c)); self.line(&format!("{} {} = {};", src_ty, vs, src_c)); self.line(&format!("nova_int {} = ({});", vlo, lo_c)); // `last` = highest index the per-element loop would touch. Formed WITHOUT // `hi + 1` so an inclusive end == I64_MAX cannot signed-overflow (UB); // the `+1` lives only in the count, computed after the bounds check has // proven `last < len <= I64_MAX`. let last_expr = if inclusive { format!("({})", hi_c) } else { format!("({}) - 1", hi_c) }; self.line(&format!("nova_int {} = {};", vlast, last_expr)); self.line(&format!("if ({} >= {}) {{", vlast, vlo)); self.indent += 1; // Preserve the per-element OOB-panic (highest accessed index = `last`). self.line(&format!("if ({} < 0) nv_panic_index_oob({}, ({})->len);", vlo, vlo, vd)); self.line(&format!( "if ({} >= ({})->len) nv_panic_index_oob({}, ({})->len);", vlast, vs, vlast, vs )); self.line(&format!( "if ({} >= ({})->len) nv_panic_index_oob({}, ({})->len);", vlast, vd, vlast, vd )); self.line(&format!("nova_int {} = {} - {} + 1;", vn, vlast, vlo)); self.line(&format!("void* {} = (void*)(({})->data + {});", vds, vd, vlo)); self.line(&format!("void* {} = (void*)(({})->data + {});", vss, vs, vlo)); self.line(&format!("size_t {} = (size_t){} * sizeof(*({})->data);", vnb, vn, vd)); // The ascending per-element loop `dst[i]=src[i]` equals memmove EXCEPT // under destructive forward overlap — dst strictly inside [src, src+n), // reachable via writable offset-overlapping Vec views (`a=v[1..]; // b=v[0..]; a[i]=b[i]`), where the loop PROPAGATES. Fast-path memmove // (vectorized, overlap-safe) when that cannot happen; otherwise fall // back to the propagating ascending element copy to match the loop. self.line(&format!( "if ((uintptr_t){d} <= (uintptr_t){s} || (uintptr_t){d} >= (uintptr_t){s} + {nb}) {{", d = vds, s = vss, nb = vnb )); self.indent += 1; self.line(&format!("memmove({}, {}, {});", vds, vss, vnb)); self.indent -= 1; self.line("} else {"); self.indent += 1; self.line(&format!( "for (nova_int {k} = 0; {k} < {n}; {k}++) ({d})->data[{lo} + {k}] = ({s})->data[{lo} + {k}];", k = vk, n = vn, d = vd, s = vs, lo = vlo )); self.indent -= 1; self.line("}"); self.indent -= 1; self.line("}"); self.indent -= 1; self.line("}"); self.line(&format!("{} = NOVA_UNIT;", tmp)); Ok(Some(tmp)) } /// Emit a defer/errdefer body as void-effect statements (no result value, /// no return). Used to splice defer body code into the cleanup-cascade. /// The body itself was already verified in Ф.3 to be infallible (no Fail, /// no suspend, no top-level return/break/continue/throw), so we can emit /// raw stmts + trailing-as-void. fn emit_defer_body_void(&mut self, body: &Expr) -> Result<(), String> { match &body.kind { // Common case: parser wraps `defer { ... }` body in ExprKind::Block. ExprKind::Block(b) => { for stmt in &b.stmts { self.emit_stmt(stmt)?; } if let Some(trailing) = &b.trailing { let v = self.emit_expr(trailing)?; self.line(&format!("(void)({});", v)); } } // Fallback: treat body as a single expression. _ => { let v = self.emit_expr(body)?; self.line(&format!("(void)({});", v)); } } Ok(()) } /// Plan 173 Ф.2.B2 (D314): эмитит тело `defer`'а, материализуя `ScopeOutcome` /// для `defer(o ScopeOutcome) { … }`. Для плейн-defer (`binding == None`) — /// просто `emit_defer_body_void` (path byte-identical). Иначе: объявляет /// per-path `Nova_ScopeOutcome* <tmp>`, `#define`'ит его как имя биндинга и /// регистрирует в `var_types` (тело видит `o: ScopeOutcome`), затем `#undef`. /// Plan 173 Ф.2.B3 (D314): материализует `Nova_ScopeOutcome* <c_local>` для /// заданного exit-path (Success / Interrupt / FromFrame). Единый примитив, /// переиспользуемый телом `defer(o)` И consume-cleanup'ом (десугар D314 §3). /// Объявляет + инициализирует `c_local`; вызывающий уже сгенерил имя. fn materialize_scope_outcome(&mut self, c_local: &str, outcome: DeferOutcome) { match outcome { DeferOutcome::Success => { self.line(&format!("Nova_ScopeOutcome* {} = nova_make_ScopeOutcome_Success();", c_local)); } DeferOutcome::Interrupt => { // Plan 173 Ф.2.B2 + Ф.4 #5: an `interrupt` that unwinds a // defer/consume is a scope **Failure** (core.nv:130). Surface the // ACTUAL error identity from the thread-local stable error slot // `_nova_last_error` (captured at throw-time by nova_throw*/nv_panic // — it OUTLIVES the throwing function's stack fail-frame, which the // interrupt unwind may have already destroyed; reading a raw // `_nova_fail_top` here segfaults cross-function). Only a pure // value-`interrupt` with no in-flight throw (`_nova_last_error` // not live) falls back to the plain `str "interrupt"` marker. let str_info = self.debt_register_any_typeinfo("nova_str"); self.line(&format!("Nova_ScopeOutcome* {};", c_local)); self.line("if (_nova_last_error.live) {"); self.indent += 1; self.assign_scope_outcome_from_frame(c_local, "_nova_last_error.frame"); self.indent -= 1; self.line("} else {"); self.indent += 1; let itmp = self.fresh_tmp(); self.line(&format!("nova_str {} = nova_str_from_cstr(\"interrupt\");", itmp)); self.line(&format!( "{} = nova_make_ScopeOutcome_Failure(nova_any_box(&{}, &{}, sizeof(nova_str)));", c_local, str_info, itmp)); self.indent -= 1; self.line("}"); } DeferOutcome::FromFrame(frame) => { self.line(&format!("Nova_ScopeOutcome* {};", c_local)); self.assign_scope_outcome_from_frame(c_local, frame); } } } /// Plan 173 Ф.2.B3 (D314) + Ф.4 #5 (D188/D190): присваивает уже-объявленному /// `var Nova_ScopeOutcome*` исход из fail-frame по `<frame>.error_kind`. /// `Failure` теперь несёт ТИПИЗИРОВАННЫЙ payload как `any` (было bootstrap-`str`, /// [M-110-multierror-any] закрыт) — тело `@cleanup`/`defer(o)` восстанавливает /// причину через `if err is T` (D54/174.3): /// * PANIC → `Panic(msg)` (Panic остаётся `str`); /// * CANCEL → `Failure(any = CancelError{reason=msg})` /// (типизированный — `err is CancelError`; D90 §7 amend, префикс `"cancel: "` /// убран); `Nova_CancelError` в `RUNTIME_DEFINED_TYPES` (§0, C layout /// hand-written в `nova_rt/array.h`, [M-consume-block-cancelerror-bare-cu]) /// — ВСЕГДА доступен независимо от prelude-подмножества (`.nv`-декларация /// в `std/prelude/errors.nv` нужна ТОЛЬКО для `err is CancelError` /// type-check, не для этого C-layout'а); /// * USER_TYPED (payload != NULL) → `Failure(any)` усыновляет throw-site box /// (`error_user_payload`) + его runtime tid → `err is <ThrownType>`; /// * USER (голый `str`) → `Failure(any = str)`. /// Единый источник для defer(o)-FromFrame И consume-cleanup (десугар D314). fn assign_scope_outcome_from_frame(&mut self, var: &str, frame: &str) { // Register the per-type NovaTypeInfo statics referenced below (idempotent); // finalize splices them at __TYPEID_DEFINES__. `CancelError` TID here is the // SAME `NOVA_TID_USER_CancelError` a user `err is CancelError` resolves to. let cancel_info = self.debt_register_any_typeinfo("Nova_CancelError"); let str_info = self.debt_register_any_typeinfo("nova_str"); self.line(&format!("if ({}.error_kind == NOVA_THROW_PANIC) {{", frame)); self.indent += 1; self.line(&format!("{} = nova_make_ScopeOutcome_Panic({}.error_msg);", var, frame)); self.indent -= 1; self.line(&format!("}} else if ({}.error_kind == NOVA_THROW_CANCEL) {{", frame)); self.indent += 1; // `CancelError` is a record → pointer representation in `any`: heap-alloc // the struct and box its `Nova_CancelError*` (narrowing = `*(Nova_CancelError**)data`). let ce = self.fresh_tmp(); self.line(&format!( "Nova_CancelError* {} = (Nova_CancelError*)nova_alloc(sizeof(Nova_CancelError));", ce)); self.line(&format!("{}->reason = {}.error_msg;", ce, frame)); self.line(&format!( "{} = nova_make_ScopeOutcome_Failure(nova_any_box(&{}, &{}, sizeof(Nova_CancelError*)));", var, cancel_info, ce)); self.indent -= 1; self.line(&format!("}} else if ({}.error_user_payload != NULL) {{", frame)); self.indent += 1; // USER_TYPED: adopt the heap-boxed throw-site payload directly (survives // unwind) + its runtime type_id → `err is <ThrownType>` narrowing works. self.line(&format!( "{} = nova_make_ScopeOutcome_Failure(nova_any_from_boxed({}.error_user_payload, {}.error_user_type_id));", var, frame, frame)); self.indent -= 1; self.line("} else {"); self.indent += 1; let ms = self.fresh_tmp(); self.line(&format!("nova_str {} = {}.error_msg;", ms, frame)); self.line(&format!( "{} = nova_make_ScopeOutcome_Failure(nova_any_box(&{}, &{}, sizeof(nova_str)));", var, str_info, ms)); self.indent -= 1; self.line("}"); } fn emit_defer_body_with_outcome(&mut self, binding: &Option<String>, body: &Expr, outcome: DeferOutcome) -> Result<(), String> { let Some(name) = binding else { return self.emit_defer_body_void(body); }; let c_local = self.fresh_tmp(); self.materialize_scope_outcome(&c_local, outcome); self.line(&format!("#define {} {}", name, c_local)); let prev = self.var_types.insert(name.clone(), "Nova_ScopeOutcome*".to_string()); let r = self.emit_defer_body_void(body); self.line(&format!("#undef {}", name)); match prev { Some(p) => { self.var_types.insert(name.clone(), p); } None => { self.var_types.remove(name); } } r } /// Plan 173 Ф.2.B3-merge (D314 §4a): FAIL-path cleanup-error composition with /// **panic-dominance**. When a cleanup body (defer / consume-cleanup) itself /// fails while the scope is already unwinding a body-throw, compose per the /// unified table (D13: `panic` = abort-class bug, strictly more urgent than a /// recoverable `throw`): /// * cleanup **PANIC** + body **throw** → cleanup panic DOMINATES: promote /// the scope fail-frame to `PANIC` so the generic `nova_rethrow_with_suppressed` /// propagates it (body-throw suppressed, matching monolith `nv_panic`). /// * cleanup PANIC + body **panic** (failframe already PANIC) → body-panic /// stays primary; cleanup appended to the suppressed chain. /// * cleanup **throw** (USER/CANCEL) → append to suppressed chain (D158). /// `df` = per-cleanup NovaFailFrame (already popped); `failframe_var` = scope /// fail-frame (primary); `comp_chain_t` = running suppressed-chain local. fn emit_fail_cleanup_compose(&mut self, df: &str, failframe_var: &str, comp_chain_t: &str) { self.line(&format!( "if ({df}.error_kind == NOVA_THROW_PANIC && {ff}.error_kind != NOVA_THROW_PANIC) {{", df = df, ff = failframe_var)); self.indent += 1; // §4a: cleanup-panic dominates body-throw — promote primary to PANIC. self.line(&format!("{}.error_msg = {}.error_msg;", failframe_var, df)); self.line(&format!("{}.error_kind = NOVA_THROW_PANIC;", failframe_var)); self.indent -= 1; self.line("} else {"); self.indent += 1; // Append к suppressed chain (primary stays = original throw). self.line("NovaErrorChain* _node = (NovaErrorChain*)nova_alloc(sizeof(NovaErrorChain));"); self.line(&format!("_node->msg = {}.error_msg;", df)); self.line(&format!("_node->kind = {}.error_kind;", df)); self.line(&format!("_node->user_payload = {}.error_user_payload;", df)); self.line(&format!("_node->user_type_id = {}.error_user_type_id;", df)); self.line(&format!("_node->next = {};", comp_chain_t)); self.line(&format!("{} = _node;", comp_chain_t)); self.indent -= 1; self.line("}"); } /// Plan 173 Ф.2.B3-merge (D314 §3): emit ONE consume-flavored cleanup entry — /// the desugar of `X.@cleanup(o)` as a defer-entry. Called from all four /// defer run-sites (FAIL / LEAVE / EARLY / INTERRUPT), each supplying the /// exit-path `outcome` + a `tail` describing how a failing cleanup composes. /// /// Structure (mirrors the ex-monolith on_exit-frame, R4b): materialize the /// `ScopeOutcome`, invoke `Nova_<T>_consume_cleanup(binding, o)` inside its OWN /// `NovaFailFrame` (so a throwing/panicking cleanup returns here rather than /// jumping straight to the caller — `nv_consume_leave_shield` ALWAYS runs), /// fire `ResourceTrace.on_resource_exit` only on a clean cleanup, leave the /// cancel-shield unconditionally, and compose per §4a via `tail`. fn emit_consume_entry_cleanup(&mut self, policy: &ConsumePolicy, outcome: DeferOutcome, df: &str, tail: ConsumeTail) { // §0: cleanup symbol derived from the type name (pinned R2), never hardcoded. let cleanup_sym = format!("Nova_{}_consume_cleanup", policy.type_name); // Plan 173 Ф.4 #6 (model B): FAIL/INTERRUPT run-sites = cleanup during // unwind → mark the frame so throw dispatch bypasses handlers (failure // composes into the pocket). LEAVE/EARLY (Success) = normal exit → the // cleanup failure IS the primary; handlers fire as usual. let unwind_cleanup = matches!(outcome, DeferOutcome::FromFrame(_) | DeferOutcome::Interrupt); let o_local = self.fresh_tmp(); self.materialize_scope_outcome(&o_local, outcome); // [M-cancel-loop-accept-swallowed-residual] (221.1 №15, D188 R3 // amendment, ОКНО-3 2026-07-23, владелец-решение (б)): the cancel- // shield is armed HERE — immediately before the cleanup dispatch — // narrowed from the previous "over body+cleanup" scope (D159's // letter: only cleanup itself is shielded; the body's own suspend // points stay cancellable). `policy.prev_deadline_var` was already // declared (bare, `=0`) by the prologue; this assigns it. The // matching `nv_consume_leave_shield` runs right after the cleanup // call below (both success and throwing paths, unchanged). self.line(&format!( "{} = nv_consume_enter_shield({});", policy.prev_deadline_var, policy.threshold_var)); // Plan 173 Ф.5 п.2 (D192-ретракт): watchdog armed around the CLEANUP // call only («fiber застрял в cleanup» — body не под порогом). t0 for // the duration measured into the ResourceTrace exit-event. Both set // BEFORE setjmp (values stable across the longjmp per C semantics). let wd_prev = self.fresh_tmp(); let wd_t0 = self.fresh_tmp(); self.line(&format!("int64_t {} = nv_cleanup_watchdog_arm({});", wd_prev, policy.threshold_var)); self.line(&format!("int64_t {} = (int64_t)uv_hrtime();", wd_t0)); self.line(&format!("NovaFailFrame {};", df)); self.line(&format!("nova_fail_push(&{});", df)); if unwind_cleanup { self.line(&format!("{}.is_cleanup = 1;", df)); } self.line(&format!("{}.error_suppressed = NULL;", df)); self.line(&format!("if (setjmp({}.jmp) == 0) {{", df)); self.indent += 1; // Plan 173 Ф.5 (#8, D188 R2 exactly-once): runtime-checked, not just // structural. Panics loudly on a second dispatch instead of relying // solely on the `active`-flag skip (which the compile-time // `D188-r2-manual-on-exit` checker can miss via aliasing/FFI). self.line(&format!("if ({} >= 1) {{ nv_panic(nova_str_from_cstr(\"D188-on-exit-double-invocation\")); }}", policy.count_var)); self.line(&format!("{} += 1;", policy.count_var)); // Реестр 221.1 №583: `cleanup_sym` (`Nova_<T>_consume_cleanup`) is // only ever EMITTED for a type with a declared `consume @cleanup` // method — user OR extern (`consume_cleanup_declared_types`; // NOT the narrower `consume_cleanup_types`, which deliberately // excludes `extern "nova"` cleanups like MutexGuard — an earlier // version of this gate used that narrower set and empirically // deadlocked an unrelated, consume-free fixture: MutexGuard's real, // hand-written `Nova_MutexGuard_consume_cleanup` got silently // skipped, leaving a lock held). A `spawn consume w = e { .. }`/ // `consume w = e { .. }` scope (`Stmt::ConsumeScope`, D415 §4 // move-capture) accepts ANY type the checker allows in that // position, including a plain `value`-kind type with NO cleanup // lifecycle at all and no declaration anywhere (observed: `type // Handle value { .. }`, captured via `spawn consume w = h.share() // { .. }` — nothing to release, the `consume` here is // move-into-fiber semantics, not a linear-resource contract). // Calling a symbol that was never emitted for such a type is // undefined at link time — under single-TU the whole dispatch was // `static`, unreferenced-and-dead, silently swept before the // linker ever saw it (same class as №577: a call site assumes a // definition the emission side never produces for this shape); // multi-TU's external linkage exposes the gap. Skip the call ONLY // when NO declaration exists anywhere — there is nothing to clean // up — while leaving the surrounding exactly-once/fail-frame/ // shield/watchdog protocol untouched, so a genuine consume type's // behavior (extern or not) is byte-identical. if self.consume_cleanup_declared_types.contains(&policy.type_name) { self.line(&format!("{}({}, {});", cleanup_sym, policy.c_binding, o_local)); } else { self.line(&format!( "/* реестр 221.1 №583: {} has no declared @cleanup — nothing to run */", policy.type_name)); } self.line("nova_fail_pop();"); self.line(&format!("nv_cleanup_watchdog_disarm({});", wd_prev)); // Clean cleanup → observability sees the final outcome (skip on throw, // R4b), then leave the shield. Plan 173 Ф.5 п.2 (D185 amend): the // exit-event carries the measured cleanup duration + overrun flag // (structural observability of a threshold overrun — the watchdog // stderr-warn is the fiber-suspend-point side of the same signal). if policy.has_resource_trace { let dur_ms = self.fresh_tmp(); self.line(&format!( "int64_t {} = ((int64_t)uv_hrtime() - {}) / 1000000LL;", dur_ms, wd_t0)); self.line(&format!( "if (_nova_handler_ResourceTrace) {{ Nova_ResourceTrace_on_resource_exit(nova_str_from_cstr(\"{}\"), {}, (nova_int){}, (nova_bool)({} > 0 && {} > (int64_t){})); }}", policy.type_name, o_local, dur_ms, policy.threshold_var, dur_ms, policy.threshold_var)); } self.line(&format!("nv_consume_leave_shield({});", policy.prev_deadline_var)); self.indent -= 1; self.line("} else {"); self.indent += 1; self.line("nova_fail_pop();"); // R4b: watchdog-disarm + leave-shield UNCONDITIONAL even when the // cleanup threw/panicked. self.line(&format!("nv_cleanup_watchdog_disarm({});", wd_prev)); self.line(&format!("nv_consume_leave_shield({});", policy.prev_deadline_var)); match tail { ConsumeTail::FailChain { failframe, chain } => { self.emit_fail_cleanup_compose(df, &failframe, &chain); } ConsumeTail::LeaveComp { comp_has, comp_msg, comp_kind, comp_payload, comp_tid, comp_chain } => { // §4a: cleanup-PANIC dominates; else fill compose slot (single // consume-entry ⇒ first-fail == primary; chain arm is defensive). self.line(&format!("if ({}.error_kind == NOVA_THROW_PANIC) {{", df)); self.indent += 1; self.line(&format!("{} = 1;", comp_has)); self.line(&format!("{} = {}.error_msg;", comp_msg, df)); self.line(&format!("{} = NOVA_THROW_PANIC;", comp_kind)); self.line(&format!("{} = {}.error_user_payload;", comp_payload, df)); self.line(&format!("{} = {}.error_user_type_id;", comp_tid, df)); self.indent -= 1; self.line(&format!("}} else if (!{}) {{", comp_has)); self.indent += 1; self.line(&format!("{} = 1;", comp_has)); self.line(&format!("{} = {}.error_msg;", comp_msg, df)); self.line(&format!("{} = {}.error_kind;", comp_kind, df)); self.line(&format!("{} = {}.error_user_payload;", comp_payload, df)); self.line(&format!("{} = {}.error_user_type_id;", comp_tid, df)); self.indent -= 1; self.line("} else {"); self.indent += 1; self.line("NovaErrorChain* _node = (NovaErrorChain*)nova_alloc(sizeof(NovaErrorChain));"); self.line(&format!("_node->msg = {}.error_msg;", df)); self.line(&format!("_node->kind = {}.error_kind;", df)); self.line(&format!("_node->user_payload = {}.error_user_payload;", df)); self.line(&format!("_node->user_type_id = {}.error_user_type_id;", df)); self.line(&format!("_node->next = {};", comp_chain)); self.line(&format!("{} = _node;", comp_chain)); self.indent -= 1; self.line("}"); } ConsumeTail::Swallow => { // interrupt / early-exit: cleanup-failure during unwind is dropped. } } self.indent -= 1; self.line("}"); } /// Plan 173 Ф.2.B3-merge (D314 §3): register a `consume X = e { … }` scope as a /// defer-scope carrying exactly ONE consume-flavored entry. Mirrors /// `enter_defer_scope`'s frame set-up (fail-frame + interrupt-frame with their /// setjmp run-sites) but the per-entry cleanup is the consume `@cleanup` /// dispatch (via `emit_consume_entry_cleanup`) rather than a user defer body. /// The caller emits the wrapper prologue (capture + shield-enter + RT-enter) /// BEFORE this, sets the entry active AFTER it, emits the body, then calls /// `leave_defer_scope` (LEAVE run-site handles the consume-entry via its branch). /// План 253.4 Ф.1: возвращает `(block_id, имя флага)` — прежде вызывающий /// пересобирал `_defer_<bid>_0_active` строкой у себя (две реконструкции /// одного имени, классическая точка расхождения). fn enter_consume_defer_scope(&mut self, policy: ConsumePolicy, is_loop_body: bool) -> (usize, String) { self.defer_block_counter += 1; let block_id = self.defer_block_counter; // План 253.4 Ф.1 (уточнение №2): имя флага — из C-ИДЕНТИФИКАТОРА // переменной, которую он сторожит (`_consume_<binding>_<id>`), а не // из номера блока. `c_binding` у value-record'ов обёрнут в `(&…)` // для передачи в cleanup — берём голый идентификатор. let c_ident = policy.c_binding .trim_start_matches("(&") .trim_end_matches(')') .to_string(); let active = format!("_defer_{}_active", c_ident); // Exactly-once + partial-init: 0 until the caller captures the resource. self.line(&format!("int {} = 0;", active)); // Plan 173 Ф.5 (#8, D188 R2): runtime exactly-once counter — genuine // per-invocation check (not merely the structural `active` flag), // see `emit_consume_entry_cleanup`. self.line(&format!("int {} = 0;", policy.count_var)); let failframe_var = format!("_defer_{}_ff", block_id); let failframe_popped_var = format!("_defer_{}_ff_popped", block_id); self.line(&format!("int {} = 0;", failframe_popped_var)); self.line(&format!("NovaFailFrame {};", failframe_var)); self.line(&format!("nova_fail_push(&{});", failframe_var)); self.line(&format!("if (setjmp({}.jmp) != 0) {{", failframe_var)); self.indent += 1; // FAIL run-site: body threw/panicked/cancelled. Run the consume-cleanup // with the outcome derived from the fail-frame, compose per §4a; the // generic pop + rethrow below carries the primary (body-throw, or a // cleanup-panic promoted to dominate). Do NOT rethrow before the pop. let comp_chain_t = format!("_defer_{}_throw_chain", block_id); self.line(&format!("NovaErrorChain* {} = {}.error_suppressed;", comp_chain_t, failframe_var)); self.line(&format!("if ({}) {{", active)); self.indent += 1; self.line(&format!("{} = 0;", active)); let df_fail = format!("_defer_{}_0_tcdf", block_id); self.emit_consume_entry_cleanup( &policy, DeferOutcome::FromFrame(&failframe_var), &df_fail, ConsumeTail::FailChain { failframe: failframe_var.clone(), chain: comp_chain_t.clone() }); self.indent -= 1; self.line("}"); self.line("nova_fail_pop();"); self.line(&format!("{} = 1;", failframe_popped_var)); self.line(&format!("{}.error_suppressed = {};", failframe_var, comp_chain_t)); // Plan 173 Ф.2.C: TRANSPARENT terminal (consume FAIL run-site) — единый // nova_scope_exit; body-throw (или cleanup-panic, промотнутый §4a в PANIC) // проброшен нагору. ≡ прежнему безусловному nova_rethrow_with_suppressed. self.line(&format!("nova_scope_exit(&{}, NOVA_SCOPE_EXIT_TRANSPARENT);", failframe_var)); self.indent -= 1; self.line("}"); // INTERRUPT run-site: `interrupt v` unwinds through here (D314 §2 aligns // impl to spec — the ex-monolith skipped cleanup on interrupt). Run the // consume-cleanup with a Failure("interrupt") outcome, then re-interrupt. let intframe_var = format!("_defer_{}_if", block_id); let intframe_popped_var = format!("_defer_{}_if_popped", block_id); self.line(&format!("int {} = 0;", intframe_popped_var)); self.line(&format!("NovaInterruptFrame {};", intframe_var)); self.line(&format!("nova_interrupt_push_defer(&{});", intframe_var)); self.line(&format!("if (setjmp({}.jmp) != 0) {{", intframe_var)); self.indent += 1; self.line(&format!("if ({}) {{", active)); self.indent += 1; self.line(&format!("{} = 0;", active)); let df_int = format!("_defer_{}_0_icdf", block_id); self.emit_consume_entry_cleanup(&policy, DeferOutcome::Interrupt, &df_int, ConsumeTail::Swallow); self.indent -= 1; self.line("}"); self.line("nova_interrupt_pop();"); self.line(&format!("{} = 1;", intframe_popped_var)); self.line(&format!( "if ({}.value_ptr) {{ nova_interrupt_ptr({}.value_ptr); }} else {{ nova_interrupt({}.value); }}", intframe_var, intframe_var, intframe_var)); self.indent -= 1; self.line("}"); self.defer_scopes.push(DeferScope { block_id, entries: vec![DeferEntry { active_var: active.clone(), body: Expr::new(ExprKind::UnitLit, Span::default()), outcome_binding: None, consume_policy: Some(policy), }], next_idx: 0, needs_failframe: false, failframe_var, failframe_popped_var, intframe_var, intframe_popped_var, is_loop_body, }); (block_id, active) } fn emit_block_stmts(&mut self, block: &Block, ret_ty: &str) -> Result<(), String> { // №240: every call here is a genuine new top-level C function body // start (fn/test/method/lambda) — see `detach_box_hoist` doc. self.detach_box_hoist = Some((self.out.len(), self.indent)); let block_id = self.enter_defer_scope(block, false); for stmt in &block.stmts { self.emit_stmt(stmt)?; } // Plan 33.1 Ф.4: при активных ensures (contracts_post_label установлен) // trailing expression идёт в `_nova_result` + goto, чтобы ensures-checks // отработали ПОСЛЕ body (как и для explicit `return X`). let post_label = self.contracts_post_label.clone(); if let Some(trailing) = &block.trailing { self.emit_source_annotation_for_expr(trailing); // Plan 217: a fn-body's trailing expr can itself be a bare // consuming receiver-call (`fn f() { consume g = ...; g.close() }` // — no explicit `return`/semicolon-statement, `g.close()` IS the // trailing). Same disarm as `Stmt::Expr` — safe regardless of // whether the trailing's VALUE is later used (disarming the flag // doesn't affect the call's own return value). self.disarm_auto_cleanup_receiver_call(trailing); // 172.4 Ф.3 блокер-1: trailing `@` fluent-метода — return-позиция ptr. let fluent_self_ret = self.var_types.get("nova_self") .map(|sv| Self::is_value_struct_ptr(sv) && sv == ret_ty) .unwrap_or(false); let prev_recv_ret = self.in_recv_ptr_return_position.replace(fluent_self_ret); // Plan 172.14 (sret/_out §2): в теле __sret-варианта tail-Call // цепочки прокидывает `_out` — взводим armed на ExprId trailing; // потребитель sret_maybe_rewrite_call перепишет `callee(args)` → // `callee__sret(args, _out)`. Безусловный сброс после эмиссии. if let Some(out_name) = self.sret_fn_out.clone() { // Plan 172.15 Ф.1: взводим на выражении, которое значение // СТРОИТ, — сквозь прозрачные блок-обёртки (`unsafe { … }`). // До 172.15 обёртка прятала хвост и проброс `_out` не // происходил: `str.bytes()` (`=> unsafe { []u8.new(…) }`) // получал `__sret`-вариант, который всё равно аллоцировал. let producer = crate::escape_analyze::sret_tail_expr(trailing); if producer.id.is_set() && matches!(&producer.kind, ExprKind::Call { .. }) { // Тип буфера `_out` = тип возврата текущей функции — // сверка на точке переписи. self.sret_out_dest_struct = self .current_fn_return_ty .as_deref() .and_then(Self::heap_record_struct_of_c) .map(|s| format!("Nova_{}", s)); self.sret_out_dest = Some((out_name, producer.id)); } } // Plan 172.1 [M-172.1-some-target-coerce]: NovaOpt_<X>/typed-int return coerces // the trailing to the return type — `Some(<int-literal>) -> Option[uint]` builds // `NovaOpt_nova_uint`, not the literal-default `NovaOpt_nova_int` (int-collapse). // [M-d55-str-literal-coercion-name-gated] fix: a `[]u8` return // type also needs target-typed routing (D55 amend, mirrors the // sibling gate above in the non-contract `FnBody::Block` arm). // [M-callnorm-free-fn-name-collision] followup (closure-megacu // regression, 2026-07-19): `_NovaTuple_` target — a tuple RETURN // type embedding a `_NovaFixArr_` element (`(int, [N]T)`) needs the // same target-typed routing, or the nested array-literal inside the // tuple literal loses its element-type hint and panics `[P67] // nova_int collapse` (legacy array-literal path). This is THE site // that fired for `tft_h() -> (int, [2](int,int)) { (9, [(1,2),(3,4)]) }` // (implicit trailing return of a `{}`-block body) — an explicit // `return (9, [...])` did NOT hit this bug: `Stmt::Return`'s own // emission (~line 27960, `ret_ty != "nova_int" && ret_ty != "nova_unit"`) // is unconditional for any non-erased return type, no narrow // whitelist — only the trailing/arrow-body sites were under-gated. // `emit_expr_with_target_type`'s `TupleLit` arm already decodes // `_NovaTuple_` elem types and recurses correctly; it just wasn't // reached from here. See docs/plans/wip/closure-megacu-fix-notes.md. let val = if ret_ty.starts_with("NovaOpt_") || ret_ty.starts_with("_NovaFixArr_") || ret_ty.starts_with("_NovaTuple_") || Self::is_typed_integer(ret_ty) || Self::is_bytes_slice_c_ty(ret_ty) { self.emit_expr_with_target_type(trailing, ret_ty)? } else { self.emit_expr(trailing)? }; if self.sret_fn_out.is_some() { self.sret_out_dest = None; self.sret_out_dest_struct = None; } self.in_recv_ptr_return_position.set(prev_recv_ret); // Plan 184 (Р5/Р7) [M-184-mut-chain-return-position]: a value-record // fluent `-> @` chain in trailing (implicit-return) position yields // `ref Self` (`NovaValue_X*` — emit-fact from `fn_ret_*`). The by-value // return consumer must deref it (auto-conversion `ref T -> T`), exactly // like the let-binding decay (~21061) and call-arg consumer // (`deref_fluent_value_args`) — shared `is_fluent_value_ptr_for_target` // predicate keeps all consumers in lockstep. Without this, // `emit_tuple_return_stash` below assigns the pointer to a value slot // (`NovaValue_Rec _nv_tmp = Nova_Rec_method_d(...)`) — a C type error for // `fn build() -> Rec { Rec.new().a(1)…d(4) }` at any chain depth ≥ 1. // No-op when ret_ty is a pointer (the `-> @` = `ref Self` fluent-self // case handled by `fluent_self_ret` above), so the two never conflict. let val = if self.is_fluent_value_ptr_for_target(trailing, ret_ty) { format!("(*({}))", val) } else { val }; // Plan 72 P3-B return: box the trailing value for a protocol return type. let val = self.wrap_protocol_return(val, trailing); let val = self.wrap_any_return(val, trailing); if let Some(label) = post_label { // Contracts mode: trailing → _nova_result; goto post. if ret_ty == "nova_unit" { self.line(&format!("{};", val)); } else { self.line(&format!("_nova_result = {};", val)); } self.leave_defer_scope(block_id); self.line(&format!("goto {};", label)); } else if ret_ty == "nova_unit" { self.line(&format!("{};", val)); self.leave_defer_scope(block_id); self.line("return NOVA_UNIT;"); } else { // Stash result in a tmp so defer cleanup runs *before* the return. let tmp = self.fresh_tmp(); // Plan 59: mono'd tuple type mismatch — field-wise copy. let trailing_ty = self.infer_expr_c_type(trailing); self.emit_tuple_return_stash(ret_ty, &tmp, &val, &trailing_ty); self.leave_defer_scope(block_id); self.line(&format!("return {};", tmp)); } } else if ret_ty == "nova_unit" { self.leave_defer_scope(block_id); if let Some(label) = post_label { self.line(&format!("goto {};", label)); } else { self.line("return NOVA_UNIT;"); } } else { self.leave_defer_scope(block_id); } Ok(()) } // ───────────────────────────────────────────────────────────────── // Plan 201 (D188-амендмент v3, 2026-07-13): «однократный вынос через // выражение». Checker (types/mod.rs ConsumeCtx) уже validated — // codegen НЕ переверяет consume-позицию, только (а) находит, какие из // АКТИВНЫХ re-consume guard'ов (`self.reconsume_scopes`) referenced // где-то внутри `return EXPR` голым идентификатором, (б) эмитит // disarm-присвоение (`_defer_<bid>_0_active = 0;`) ИМЕННО в момент // передачи владения — после того, как ВСЕ аргументы объемлющего // consuming-вызова вычислены (в исходном AST-порядке слева направо — // Nova's порядок вычисления), но до самого вызова. Каждый арг // hoist'ится в свой temp (`self.line`) — это ЕСТЕСТВЕННО сериализует // порядок вычисления, включая любые side-effecting под-вызовы (если // они кидают — throw срабатывает до disarm-строки, cleanup ещё // взведён). Итоговый Call пересобирается с temp-идент-аргументами и // эмитится ШТАТНЫМ `emit_expr` (mangling/overload dispatch не // дублируем — id/span вызова сохраняются, checker-resolved callee // остаётся привязан). // ───────────────────────────────────────────────────────────────── /// Plan 201 (D188-амендмент v3, non-consume-position fix, 2026-07-13): /// consume-параметр индексы ВЫЗОВА `e` (`e.id` — resolved-callee key) — /// зеркалит checker's `call_consume_idxs` (types/mod.rs) для disarm- /// детекции: НЕ каждое вхождение guarded-имени АРГУМЕНТОМ вызова — /// санкционированный вынос, ТОЛЬКО consume-параметр позиция (receiver /// НИКОГДА не консьюм-позиция здесь — consume-receiver-метод на /// guarded-имени уже поймана checker'ом как `E_CONSUME_BLOCK_MOVE_OUT` /// через `mark_consumed`/`guard_violations`, этот EXPR никогда не /// доходит до codegen). Overload-aware: если checker резолвнул /// конкретный callee для ЭТОГО call-site (`resolved_callees[e.id]`) — /// использует ЕГО `param_modes` (точно, как основной dispatch-путь, /// U.4.3 c2.2); иначе — последняя зарегистрированная сигнатура (нет /// overload'а по этому ключу → нет неоднозначности). fn call_consume_arg_idxs( &self, e: &Expr, func_kind: &ExprKind, ) -> std::collections::HashSet<usize> { let mut out = std::collections::HashSet::new(); // №548: `Ok(x)`/`Err(x)`/`Some(x)` are built-in Result/Option variant // constructors handled as codegen intrinsics (see the many `name == // "Ok"` special cases elsewhere in this file) — they never go // through ordinary function-call resolution, so they are never // registered in `self.method_overloads`, and the lookup below always // missed them (`out` stayed empty). That silently told // `collect_reconsume_occurrences_rec` that `Ok(stream)`'s argument is // NOT a consume position, so `return Ok(stream)` out of a // `consume stream { … }` block never disarmed `stream`'s scope-exit // cleanup — the generated C still called `..._consume_cleanup` on // `stream` right after copying it (by value) into the `Ok` payload, // closing the linear resource the caller was just handed. Their sole // positional argument is ALWAYS moved (there is no non-consuming // constructor form), so index 0 is unconditionally a consume // position — mirrors the intrinsic handling everywhere else in this // file instead of relying on `method_overloads`, which structurally // cannot see intrinsics. Root-caused via `socks5_connect`'s `return // Ok(stream)` (nova-socks `src/socks5.nv:417`) closing the SOCKS5 // tunnel out from under `examples/flagship/http_proxy_chain` // immediately after the handshake — registry №548. if let ExprKind::Ident(fname) = func_kind { if fname == "Ok" || fname == "Err" || fname == "Some" { if let ExprKind::Call { args, .. } = &e.kind { if args.len() == 1 { out.insert(0); return out; } } } } let key: Option<(String, String)> = match func_kind { ExprKind::Member { obj, name: method } => { if let ExprKind::Ident(recv) = &obj.kind { self.var_types.get(recv).map(|ty| { let bare = self.debt_strip_nova_trim_start(ty); let bare = bare.strip_prefix("NovaValue_") .map(|s| s.to_string()).unwrap_or(bare); (bare, method.clone()) }) } else { None } } ExprKind::Ident(fname) => Some((String::new(), fname.clone())), ExprKind::Path(parts) if parts.len() == 2 => { Some((parts[0].clone(), parts[1].clone())) } ExprKind::Path(parts) => parts.last().map(|last| (String::new(), last.clone())), _ => None, }; let Some(key) = key else { return out; }; let Some(sigs) = self.method_overloads.get(&key) else { return out; }; let chosen: Option<&MethodSig> = self.resolved_callees.get(&e.id) .and_then(|chosen_span| sigs.iter().find(|s| s.fn_span == Some(*chosen_span))) .or_else(|| sigs.last()); if let Some(sig) = chosen { for (i, m) in sig.param_modes.iter().enumerate() { if *m == 2 { out.insert(i); } } } out } /// Собрать подмножество `active`, встречающееся РОВНО ОДИН раз внутри /// `e`, В consume-параметр-позиции (любая глубина — Call-аргументы на /// любом уровне вложенности, прозрачные обёртки `Try`/`Bang`/`RefArg`/ /// `As`/`Is`/`Unary`/`Binary`/`Index`/`TurboFish`/`Member`-receiver). /// Plan 201 non-consume-position fix (2026-07-13): чекер теперь /// санкционирует НЕ только единственное consume-позиционное вхождение /// (disarm), но и ЛЮБОЕ количество вхождений ЦЕЛИКОМ вне consume- /// позиции (receiver / view- / mut-аргумент — обычное использование, /// БЕЗ disarm'а, см. `types::mod::classify_guard_scan`). Codegen /// больше не может слепо доверять «любое найденное вхождение — вынос» /// (это ломало `s.share()`-паттерн — receiver, не consume-арг) — /// считаем occurrences ПОЗИЦИОННО (`call_consume_arg_idxs`) и /// возвращаем имя, ТОЛЬКО если оно встретилось единожды И это вхождение /// — consume-параметр. /// `[M-178-consume-field-ctor-from-var]` × D188 v3: consume-поля record- /// литерала (зеркало checker'ского `ConsumeRegistry::consume_fields_for_lit`). /// Типизированный литерал — прямой lookup по последнему сегменту пути; /// анонимный (`type_name: None`) — unique структурный field-set match. fn consume_fields_for_lit( &self, type_name: &Option<Vec<String>>, fields: &[crate::ast::RecordLitField], ) -> Option<&HashSet<String>> { if let Some(p) = type_name { return p.last().and_then(|tn| self.record_consume_fields.get(tn)); } let lit_names: Vec<&str> = fields.iter() .filter(|f| !f.is_spread) .map(|f| f.name.as_str()) .collect(); if lit_names.is_empty() { return None; } let mut found: Option<&String> = None; for (tn, fnames) in &self.record_field_names { if lit_names.iter().all(|n| fnames.contains(*n)) { if found.is_some() { return None; // ambiguous — консервативно ничего } found = Some(tn); } } found.and_then(|tn| self.record_consume_fields.get(tn)) } fn collect_reconsume_disarm_names( &self, e: &Expr, active: &std::collections::HashSet<String>, ) -> std::collections::HashSet<String> { let mut occs: std::collections::HashMap<String, Vec<bool>> = std::collections::HashMap::new(); self.collect_reconsume_occurrences_rec(e, active, &mut occs); occs.into_iter() .filter(|(_, v)| v.len() == 1 && v[0]) .map(|(k, _)| k) .collect() } fn collect_reconsume_occurrences_rec( &self, e: &Expr, active: &std::collections::HashSet<String>, occs: &mut std::collections::HashMap<String, Vec<bool>>, ) { match &e.kind { ExprKind::Ident(name) => { if active.contains(name) { occs.entry(name.clone()).or_default().push(false); } } ExprKind::Call { func, args, .. } => { let func_u = func.unwrap_turbofish(); let consume_idxs = self.call_consume_arg_idxs(e, &func_u.kind); if let ExprKind::Member { obj, .. } = &func_u.kind { self.collect_reconsume_occurrences_rec(obj, active, occs); } for (i, a) in args.iter().enumerate() { let ax = a.expr(); if let ExprKind::Ident(name) = &ax.kind { if active.contains(name) { occs.entry(name.clone()).or_default() .push(consume_idxs.contains(&i)); continue; } } self.collect_reconsume_occurrences_rec(ax, active, occs); } } ExprKind::Member { obj, .. } => { self.collect_reconsume_occurrences_rec(obj, active, occs); } ExprKind::Index { obj, index } => { self.collect_reconsume_occurrences_rec(obj, active, occs); self.collect_reconsume_occurrences_rec(index, active, occs); } ExprKind::TurboFish { base, .. } => { self.collect_reconsume_occurrences_rec(base, active, occs); } ExprKind::Try(i) | ExprKind::Bang(i) | ExprKind::RefArg(i) | ExprKind::Unary { operand: i, .. } => { self.collect_reconsume_occurrences_rec(i, active, occs); } ExprKind::As(i, _) | ExprKind::Is(i, _) => { self.collect_reconsume_occurrences_rec(i, active, occs); } ExprKind::Binary { left, right, .. } => { self.collect_reconsume_occurrences_rec(left, active, occs); self.collect_reconsume_occurrences_rec(right, active, occs); } ExprKind::Coalesce(a, b) => { self.collect_reconsume_occurrences_rec(a, active, occs); self.collect_reconsume_occurrences_rec(b, active, occs); } // `[M-178-consume-field-ctor-from-var]` × D188 v3 (2026-07-13): // consume-поле record-литерала = consume-позиция (зеркало // checker'ского `scan_guard_rec`). Инициализация consume-поля // guarded-биндингом в tail/return EXPR — санкционированный // вынос: дизарм при конструировании литерала. ExprKind::RecordLit { type_name, fields, inferred_map_v, .. } => { let consume_field_names: Option<&HashSet<String>> = if inferred_map_v.is_some() { None } else { self.consume_fields_for_lit(type_name, fields) }; for f in fields { let is_consume_field = !f.is_spread && consume_field_names .map_or(false, |s| s.contains(f.name.as_str())); match &f.value { Some(v) => { if let ExprKind::Ident(name) = &v.kind { if active.contains(name) { occs.entry(name.clone()).or_default() .push(is_consume_field); continue; } } self.collect_reconsume_occurrences_rec(v, active, occs); } // D52 punning `{ name }` — implied ident `name`. None if !f.is_spread => { if active.contains(f.name.as_str()) { occs.entry(f.name.clone()).or_default() .push(is_consume_field); } } None => {} } } } _ => {} } } /// Emit EXPR (`return EXPR` value), дизармя `names` (подмножество /// `self.reconsume_scopes`) в момент передачи владения (см. блок- /// комментарий выше). `e` — Call на любом уровне: рекурсивно hoist'ит /// args, дизармит те `names`, что встретились ПРЯМЫМИ аргументами /// ЭТОГО вызова, сразу после того как все его args вычислены, затем /// делегирует итоговый (args-hoisted) Call в штатный `emit_expr`. /// Non-Call обёртки (`Try`/`As`/…) — прозрачная рекурсия. Прочие формы /// (Match/If как ПРЯМОЕ значение return) — консервативный fallback: /// disarm перед вычислением всего EXPR (за пределами узкого амендмента /// v3 — вложенность Call-в-Call, засвидетельствованная в nova-tls). /// Plan 217: resolve `name` to the C `_active` flag it should disarm, if /// any — checks `reconsume_scopes` first (re-consume `consume X { … }` /// blocks always use entry index 0, D188-амендмент Plan 201), then /// `auto_cleanup_active` (bare auto-cleanup consume-lets, arbitrary /// index within their block). Most-recently-armed wins (`.rev()`), /// mirroring shadowing semantics already used by `reconsume_scopes` /// alone before 217. /// План 253.4 Ф.1 (№480, решение владельца 2026-08-08): дизарм-резолв /// БЕЗ РЕЕСТРОВ. Прежде здесь искались два независимых стека /// (`reconsume_scopes`, `auto_cleanup_active`), наполнявшихся в двух /// РАЗНЫХ местах, каждое под свою форму объявления — и форма /// `spawn consume a, b { … }` не попадала ни в один (№456: двойное /// потребление, use-after-free, зависание в `GC_gcollect()`). /// /// Теперь спрашивается ровно та структура, которая ВЛАДЕЕТ флагом и его /// `defer`'ом — стек defer-скоупов. Запись (`DeferEntry` + /// `ConsumePolicy`) несёт имя переменной, имя флага и политику очистки /// ОДНИМ объектом, созданным одной операцией: завести флаг и «забыть /// зарегистрировать» его теперь невыразимо — регистрации нет. /// Теневание — обход стека с конца (внутренний скоуп раньше внешнего), /// в точности как у самих C-областей видимости. fn disarm_var_for(&self, name: &str) -> Option<String> { for scope in self.defer_scopes.iter().rev() { for entry in scope.entries.iter().rev() { let Some(policy) = &entry.consume_policy else { continue }; if policy.nova_binding.as_deref() == Some(name) { return Some(entry.active_var.clone()); } } } None } /// План 253.4 Ф.1: множество имён активных Plan-201 re-consume-guard'ов /// (`consume X { … }`) — вторая роль снятого реестра `reconsume_scopes`. /// Читается из того же стека defer-скоупов. fn reconsume_active_names(&self) -> std::collections::HashSet<String> { let mut out = std::collections::HashSet::new(); for scope in &self.defer_scopes { for entry in &scope.entries { if let Some(policy) = &entry.consume_policy { if policy.re_consume { if let Some(n) = &policy.nova_binding { out.insert(n.clone()); } } } } } out } /// План 253.4 Ф.1: владение биндингом ушло из этого кадра насовсем /// (`spawn`/`detach consume` передаёт его файберу) — запись остаётся /// ради своего run-site'а, но по имени больше не находится. Замена /// прежнему `auto_cleanup_active.retain(…)`. fn drop_disarm_binding(&mut self, name: &str) { for scope in self.defer_scopes.iter_mut().rev() { for entry in scope.entries.iter_mut().rev() { if let Some(policy) = &mut entry.consume_policy { if policy.nova_binding.as_deref() == Some(name) { policy.nova_binding = None; return; } } } } } /// Plan 217: if `e` is a Call, disarm auto-cleanup/re-consume bindings /// that are genuinely consumed by IT — either as the receiver of a /// registered consume-method (`X.method(...)`, gated on /// `consume_receiver_methods` — mirrors checker's `is_consume_method`), /// or as a direct argument at a `consume`-mode parameter position /// (`helper(X, other)`, gated on `free_fn_consume_param_positions` / /// `method_consume_param_positions` — mirrors checker's `consume_args`/ /// `consume_idxs`; found necessary via the `guard_cross_scope_transfer. /// nv` regression — MutexGuard `g` passed to a `consume`-param helper /// left `_active` armed after a legitimate transfer, double-`unlock`ing /// at the caller's scope-exit). Emits the disarm assignment right /// before `e` is evaluated. Called from every "statement-like" /// expression-emission site where `e`'s value is discarded (`Stmt:: /// Expr`, block trailing) AND from the central `emit_expr` choke-point /// (covers nested/return/tail positions too) — a pure flag-write has no /// bearing on the computed value, so firing it unconditionally, /// wherever this exact Call node is emitted, is sound (idempotent on /// repeat). Gating on the registered-method/position sets (not "any /// method call"/"any direct arg") matters both ways: a `ro`/`mut` /// helper method or a `ro`/`mut`-param argument position must NOT /// disarm — the checker doesn't mark `X` consumed for those either, so /// disarming here would be a silent resource leak (the OPPOSITE /// failure mode from the double-cleanup this fixes). fn disarm_auto_cleanup_receiver_call(&mut self, e: &Expr) { let ExprKind::Call { func, args, .. } = &e.kind else { return }; // (a) receiver form `X.method(...)`. if let ExprKind::Member { obj, name: method_name } = &func.kind { if let ExprKind::Ident(recv_name) = &obj.kind { let recv_ty = self.var_types.get(recv_name).cloned().unwrap_or_default(); let recv_ty_name = { let t = self.debt_strip_nova_trim_start(&recv_ty); t.strip_prefix("NovaValue_").map(|s| s.to_string()).unwrap_or(t) }; let is_consuming_call = self.consume_receiver_methods .get(&recv_ty_name) .map_or(false, |ms| ms.contains(method_name)); if is_consuming_call { if let Some(var) = self.disarm_var_for(recv_name) { self.line(&format!( "{} = 0; /* Plan 217: consuming-вызов (receiver) — cleanup дизармлен */", var)); } } } } // (b) direct call-argument at a `consume`-mode parameter position — // free-fn or method (args here EXCLUDE the receiver, matching // `method_consume_param_positions`'s indexing). let consume_positions: Option<HashSet<usize>> = match &func.kind { ExprKind::Ident(name) => self.free_fn_consume_param_positions.get(name).cloned(), ExprKind::Path(path) => path.last() .and_then(|name| self.free_fn_consume_param_positions.get(name)) .cloned(), ExprKind::Member { obj, name: method_name } => { let recv_ty = self.infer_expr_c_type(obj); let recv_ty_name = { let t = self.debt_strip_nova_trim_start(&recv_ty); t.strip_prefix("NovaValue_").map(|s| s.to_string()).unwrap_or(t) }; self.method_consume_param_positions .get(&(recv_ty_name, method_name.clone())) .cloned() } _ => None, }; if let Some(positions) = consume_positions { for (i, a) in args.iter().enumerate() { if !positions.contains(&i) { continue; } if let ExprKind::Ident(arg_name) = &a.expr().kind { if let Some(var) = self.disarm_var_for(arg_name) { self.line(&format!( "{} = 0; /* Plan 217: consume-param call-arg — cleanup дизармлен */", var)); } } } } } /// №465 (A8.29): given a C type string (`self.var_types` value) for a /// bare-Ident binding, return its source `#zero_on_move` type name IFF /// zeroing that binding's storage right now is proven safe by /// `zero_on_move_types`'s registration invariant (see that field's /// doc). The prefix match doubles as the promotion guard: a /// `AllocKind::Value` local that escape-analysis promoted to heap has /// C type `Nova_<X>*` (not `NovaValue_<X>`), which only matches the /// `Nova_` arm below — but `Nova_` only resolves when the type was /// registered `false` (named-tuple/newtype), so a promoted Value /// binding (registered `true`) fails BOTH arms and correctly returns /// `None`. fn zero_on_move_safe_source_name<'a>(&self, c_ty: &'a str) -> Option<&'a str> { if let Some(name) = c_ty.strip_prefix("NovaValue_") { if self.zero_on_move_types.get(name) == Some(&true) { return Some(name); } } else if let Some(name) = c_ty.strip_prefix("NovaTuple_") { if self.zero_on_move_types.get(name) == Some(&false) { return Some(name); } } else if let Some(name) = c_ty.strip_prefix("Nova_") { if self.zero_on_move_types.get(name) == Some(&false) { return Some(name); } } None } /// №465 (A8.29): emit `T __tmp = <val>; Nova_<X>_zero_storage(&(<ident>));` /// and return `__tmp` — the copy-out-then-zero-source pattern shared by /// both safe auto-inject sites (bare consume-return, consume-param-arg). /// MUST be called with `val` already the fully-computed RHS text for /// `ident_name` (so the temp captures the value BEFORE the source is /// zeroed) — callers hoist through this rather than zeroing in place, /// because in both call sites the zero call's target storage (`ident_ /// name`) is still needed downstream in the SAME C statement/expression /// (`return <val>;` reads `val` after; a call argument list still /// textually contains the original expression unless replaced). fn zero_on_move_hoist_and_zero(&mut self, ident_name: &str, zom_ty_name: &str, val: &str) -> String { let c_ty = self.var_types.get(ident_name).cloned().unwrap_or_default(); let tmp = self.fresh_tmp(); self.line(&format!("{} {} = {};", c_ty, tmp, val)); self.var_types.insert(tmp.clone(), c_ty); self.line(&format!( "Nova_{}_zero_storage(&({})); /* №465: zero-on-move — исходное хранилище занулено */", zom_ty_name, ident_name)); tmp } /// №465 (A8.29): consume-param-arg auto-inject. Mirrors the position- /// detection in `disarm_auto_cleanup_receiver_call` branch (b) — same /// `free_fn_consume_param_positions` / `method_consume_param_positions` /// channels — but REWRITES the call's arg list (copy-out-then-zero- /// source, see `zero_on_move_hoist_and_zero`) instead of just emitting a /// disarm flag-write, since the zero call must land strictly AFTER an /// independent copy of the argument's value exists (zeroing in place /// BEFORE the call would hand the callee already-zeroed data — the /// call's own by-value copy happens at the call, not before it). /// Returns `None` when no arg needed rewriting (the overwhelmingly /// common case — `zero_on_move_types` is empty for any module that /// doesn't declare a `#zero_on_move` type at all). fn zero_on_move_rewrite_call(&mut self, e: &Expr) -> Option<Expr> { if self.zero_on_move_types.is_empty() { return None; } let ExprKind::Call { func, args, trailing } = &e.kind else { return None }; let consume_positions: Option<HashSet<usize>> = match &func.kind { ExprKind::Ident(name) => self.free_fn_consume_param_positions.get(name).cloned(), ExprKind::Path(path) => path.last() .and_then(|name| self.free_fn_consume_param_positions.get(name)) .cloned(), ExprKind::Member { obj, name: method_name } => { let recv_ty = self.infer_expr_c_type(obj); let recv_ty_name = { let t = self.debt_strip_nova_trim_start(&recv_ty); t.strip_prefix("NovaValue_").map(|s| s.to_string()).unwrap_or(t) }; self.method_consume_param_positions .get(&(recv_ty_name, method_name.clone())) .cloned() } _ => None, }; let positions = consume_positions?; let mut new_args: Vec<CallArg> = Vec::with_capacity(args.len()); let mut rewrote = false; for (i, a) in args.iter().enumerate() { if positions.contains(&i) { if let ExprKind::Ident(arg_name) = &a.expr().kind { let c_ty = self.var_types.get(arg_name).cloned().unwrap_or_default(); if let Some(zom_name) = self.zero_on_move_safe_source_name(&c_ty) { let zom_name = zom_name.to_string(); let tmp = self.zero_on_move_hoist_and_zero(arg_name, &zom_name, arg_name); let tmp_expr = Expr::new(ExprKind::Ident(tmp), a.expr().span); new_args.push(match a { CallArg::Item(_) => CallArg::Item(tmp_expr), CallArg::Spread(_) => CallArg::Spread(tmp_expr), CallArg::Named { name, .. } => CallArg::Named { name: name.clone(), value: tmp_expr }, }); rewrote = true; continue; } } } new_args.push(a.clone()); } if !rewrote { return None; } Some(Expr { kind: ExprKind::Call { func: func.clone(), args: new_args, trailing: trailing.clone(), }, span: e.span, id: e.id, debug_only: e.debug_only, }) } fn emit_expr_with_reconsume_disarm( &mut self, e: &Expr, names: &std::collections::HashSet<String>, ) -> Result<String, String> { if names.is_empty() { return self.emit_expr(e); } match &e.kind { ExprKind::Call { func, args, trailing } => { let mut hoisted_args: Vec<CallArg> = Vec::with_capacity(args.len()); let mut disarmed_here: Vec<String> = Vec::new(); for a in args { let ax = a.expr(); let c = if let ExprKind::Ident(name) = &ax.kind { if names.contains(name) { disarmed_here.push(name.clone()); } self.emit_expr(ax)? } else { self.emit_expr_with_reconsume_disarm(ax, names)? }; let ty = self.infer_expr_c_type(ax); let tmp = self.fresh_tmp(); self.line(&format!("{} {} = {};", ty, tmp, c)); self.var_types.insert(tmp.clone(), ty); let tmp_expr = Expr::new(ExprKind::Ident(tmp), ax.span); hoisted_args.push(match a { CallArg::Item(_) => CallArg::Item(tmp_expr), CallArg::Spread(_) => CallArg::Spread(tmp_expr), CallArg::Named { name, .. } => CallArg::Named { name: name.clone(), value: tmp_expr }, }); } // Все args этого вызова вычислены (в исходном AST-порядке) — // момент передачи владения: дизармим guard'ы, бывшие ПРЯМЫМИ // аргументами именно этого вызова. for name in &disarmed_here { if let Some(var) = self.disarm_var_for(name) { self.line(&format!( "{} = 0; /* Plan 201 v3 / 217: consuming-вызов — cleanup дизармлен */", var)); } } let rebuilt = Expr { kind: ExprKind::Call { func: func.clone(), args: hoisted_args, trailing: trailing.clone(), }, span: e.span, id: e.id, debug_only: e.debug_only, }; self.emit_expr(&rebuilt) } _ => { // Fallback (не Call/Try на прямом пути) — не разбираем глубже; // дизармим всё, что EXPR ссылается, перед его вычислением. // Заведомо избыточно-консервативно (порядок относительно // внутренних под-throws не гарантирован для этих редких // форм), но узкий амендмент v3 не таргетирует их — // засвидетельствованные пути: Call-в-Call и (M-178 × // D188 v3, 2026-07-13) record-литерал с consume-полем, // инициализированным guarded-биндингом (`MyRec{res: s, …}` // как tail/return) — конструирование литерала не может // упасть ПОСЛЕ вычисления полей, окно «дизарм → до // передачи владения» схлопывается в сами field-exprs. let names_here = self.collect_reconsume_disarm_names(e, names); for name in &names_here { if let Some(var) = self.disarm_var_for(name) { self.line(&format!( "{} = 0; /* Plan 201 v3 / 217: consuming-вызов (fallback) — cleanup дизармлен */", var)); } } self.emit_expr(e) } } } /// Plan 217 (D-новый, гибрид C): arm the auto-cleanup entry for `decl` /// (a bare consume-let just declared by `emit_stmt_inner`) — resolves /// the `(block_id, idx, init_c_type)` stashed by `enter_defer_scope`'s /// prologue scan, patches in the REAL `c_binding` (incl. value-record /// `&`-wrap, mirrors `Stmt::ConsumeScope` codegen at its own binding /// point), enters the cancel-shield (`nv_consume_enter_shield(0)` — /// threshold 0 is a harmless literal, no watchdog-warn armed, см. /// `ConsumePolicy.threshold_var` doc), and flips `_active = 1`. Pushes /// `(name, block_id, idx)` onto `auto_cleanup_active` so the disarm /// sites (return / call-arg / bare-statement consuming call) can find /// it. Partial-init safety: armed only HERE (after `emit_stmt_inner` /// already emitted the declaration), never in the prologue — if `init` /// itself threw, we'd never reach this line, so no phantom cleanup. fn emit_auto_cleanup_arm(&mut self, decl: &LetDecl, span: Span) -> Result<(), String> { let (block_id, idx, init_c_type) = match self.auto_cleanup_arm_sites.remove(&span) { Some(v) => v, None => return Ok(()), }; // План 253.4 Ф.1: биндинг и имя флага БОЛЬШЕ НЕ ВЫЧИСЛЯЮТСЯ здесь // заново — оба уже лежат в записи, заведённой прологом вместе с // самой переменной. Прежде это место звало `pattern_binding` второй // раз и пересобирало `_defer_<bid>_<idx>_active` по номерам: две // независимые реконструкции одного и того же, каждая — точка // расхождения. let (binding, active_var) = { let scope = self.defer_scopes.iter().rev().find(|s| s.block_id == block_id); let entry = scope.and_then(|s| s.entries.get(idx)); match entry { Some(e) => ( e.consume_policy.as_ref().and_then(|p| p.nova_binding.clone()).unwrap_or_default(), e.active_var.clone(), ), None => return Ok(()), } }; let c_binding_arg = if init_c_type.starts_with("NovaValue_") && !init_c_type.ends_with('*') { format!("(&{})", binding) } else { binding.clone() }; let type_name = { let t = self.debt_strip_nova_trim_start(&init_c_type); t.strip_prefix("NovaValue_").map(|s| s.to_string()).unwrap_or(t) }; let _ = &decl.pattern; // [M-cancel-loop-accept-swallowed-residual] (221.1 №15, D188 R3 // amendment, ОКНО-3 2026-07-23, владелец-решение (б)): the shield is // NO LONGER armed here at resource-capture time — that held the // cancel-mask up over the ENTIRE scope BODY (every suspend/yield // point between capture and cleanup), contradicting D159's "cleanup // completes-then-cancel-propagates" (only CLEANUP itself should be // shielded). `nv_consume_enter_shield` is now called immediately // before the actual cleanup dispatch in `emit_consume_entry_cleanup` // (the ONE place per policy where `Nova_<T>_consume_cleanup` runs), // which also does the matching `nv_consume_leave_shield`. This body // no longer touches the shield at all — cancellation reaches the // body's own suspend points normally now. // Plan 217: mirror ConsumeScope's D185 R1 enter-event (NULL-guarded, // observability only) — cheap, keeps auto-cleanup structurally // observable the same way an explicit `consume{}` block already is. if self.effect_schemas.contains_key("ResourceTrace") { self.line(&format!( "if (_nova_handler_ResourceTrace) {{ Nova_ResourceTrace_on_resource_enter(nova_str_from_cstr(\"{}\")); }}", type_name)); } self.line(&format!("{} = 1; /* Plan 217: auto-cleanup armed */", active_var)); if let Some(scope) = self.defer_scopes.iter_mut().rev().find(|s| s.block_id == block_id) { if let Some(entry) = scope.entries.get_mut(idx) { if let Some(policy) = &mut entry.consume_policy { policy.c_binding = c_binding_arg; } } } // План 253.4 Ф.1: РЕГИСТРАЦИИ БОЛЬШЕ НЕТ. Прежде здесь стояло // `self.auto_cleanup_active.push((binding, block_id, idx))` — третий, // отдельный от заведения флага и от постановки defer'а шаг, который // форма `spawn consume a, b { … }` и пропустила (№456/№480). Ok(()) } /// Plan 217 (D-новый, гибрид C): thin wrapper around `emit_stmt_inner`. /// After a bare `Stmt::Let` finishes emitting its normal C declaration, /// checks whether this exact statement (keyed by its span) was flagged /// by `enter_defer_scope`'s prologue scan as an auto-cleanup arm-site — /// if so, arms the shield + `_active` flag right here (partial-init /// safety: the resource is now genuinely captured). Kept as a separate /// wrapper (rather than inlining into the giant `Stmt::Let` match arm /// below, which has many early `return Ok(())`s) to avoid touching that /// arm's internals at all. /// /// №409 fix (span-collision guard): `field_cache.rs`'s synthesized hoist /// lets (`build_at_field_let`/`make_hoist_let`/chain-prefix-sharing — /// ALL of them, confirmed by grep, hardcode `consume: false`) deliberately /// reuse the ENCLOSING statement's span for diagnostics (`region. /// first_span` = `stmt_span(s)`, `field_cache.rs:3040`). When the first /// repeated-field access sits inside a bare `consume X = …;` (Plan 217 /// auto-cleanup arm site), the hoist statement — prepended BEFORE the /// original in `b.stmts` — carries the EXACT SAME `Span` as that /// `consume`-let. `auto_cleanup_arm_sites` is keyed by `Span` alone, so /// this match used to fire on the (unfiltered) hoist statement FIRST: /// `emit_auto_cleanup_arm` ran on the hoist's own `LetDecl`, extracting /// its hoisted-pointer NAME (e.g. `_at_ch`, not `guard`) as the tracked /// consume-binding — the auto-cleanup dispatch and the `guard.unlock()` /// disarm-lookup then both operated on the WRONG name: the scope-exit /// cleanup called `Nova_MutexGuard_consume_cleanup(_at_ch, …)` (a /// `Chan*` where `MutexGuard*` is expected — real wrong-pointer /// miscompile) AND `guard.unlock()`'s disarm never matched (`guard` was /// never registered), so cleanup fired a SECOND time on exit — the two /// findings PROGRESS-p244.md reported as separate "layers" are this ONE /// span-collision bug. `decl.consume` disambiguates: the hoist let /// statements are never `consume`-bound (see the grep above), the REAL Plan 217 arm /// site always is (`auto_cleanup_qualifies` requires it) — requiring it /// here too makes this match immune to a same-span synthetic neighbor. fn emit_stmt(&mut self, stmt: &Stmt) -> Result<(), String> { let decl_span = match stmt { Stmt::Let(decl) if decl.consume && self.auto_cleanup_arm_sites.contains_key(&decl.span) => Some(decl.span), _ => None, }; self.emit_stmt_inner(stmt)?; if let Some(span) = decl_span { if let Stmt::Let(decl) = stmt { self.emit_auto_cleanup_arm(decl, span)?; } } Ok(()) } fn emit_stmt_inner(&mut self, stmt: &Stmt) -> Result<(), String> { // Source annotation hook: if --annotate-source enabled, emit the // originating Nova source as a /* SRC: ... */ comment. self.emit_source_annotation_for_stmt(stmt); match stmt { // Plan 114.4 Ф.2: scope-local `const N = expr` — constexpr, // emit как block-scope C const declaration. Тип / значение // через emit_const_expr_typed (как для module-level const). Stmt::Const(decl) => { let ty_c = if let Some(ty) = &decl.ty { self.type_ref_to_c(ty)? } else { self.infer_expr_c_type(&decl.value) }; // [M-d55-const-bytes-lit-not-constexpr] fix (2026-07-21): a // scope-local `const c []u8 = "hi"` is NOT walked by the D429 // `#coerce` str→[]u8 AST-rewrite pass (`MapLitAnnotator:: // walk_stmt`, types/mod.rs — `Stmt::Const(_) => {}`, a // deliberate no-op since Plan 114.4 Ф.2) — so `decl.value` stays // a bare `StrLit`. `[]u8` is a heap Vec POINTER (len/cap/data), // not a C-constexpr — `emit_const_expr`'s `StrLit` arm always // builds a `nova_str`-shaped `{.ptr=...,.len=...}` struct // literal (correct for a `str`-typed const) regardless of the // DECLARED target type, so this would silently emit an // ILL-TYPED C initializer (`const Nova_Vec____nova_byte* c = // {.ptr="hi", .len=2};` — CC-FAIL, or worse if the shapes ever // happened to typecheck) instead of a clear Nova-level // diagnostic. Module-level `const` (`Item::Const`) has a // genuine fix (IS walked by the D429 rewrite + falls to the // pre-existing `emit_lazy_const` runtime-init path, see its // doc) — scope-local const has no block-scope equivalent of // `nova_consts_init()` to lazy-init into (would need new // per-block init-ordering machinery, out of scope for this // fix). Turn the silent mis-compile into an explicit, // actionable error instead: `let`/`ro`/`mut` already coerce // str→[]u8 correctly at this exact scope (D429 R6) — steer // the author there. if Self::is_bytes_slice_c_ty(&ty_c) && matches!(decl.value.kind, ExprKind::StrLit(_)) { return Err(format!( "[E_CONST_BYTES_NOT_CONSTEXPR] scope-local `const {name} []u8 = \"...\"` \ is not supported — a `[]u8` value is a heap-allocated Vec, not a \ compile-time C constant, and this position has no runtime-init \ machinery (unlike a module-level `const`, which lazy-inits). Use \ `let`/`ro`/`mut {name} []u8 = \"...\"` instead (same zero-copy str→[]u8 \ view coercion, D429).", name = decl.name )); } let val = self.emit_const_expr_typed(&decl.value, Some(&ty_c)) .map_err(|e| format!("scope-local const `{}` codegen failed: {}", decl.name, e))?; self.line(&format!("const {} {} = {};", ty_c, decl.name, val)); self.var_types.insert(decl.name.clone(), ty_c); return Ok(()); } Stmt::Let(decl) => { // Plan 33.3 Ф.9.1 (D24): ghost erasure. // `ghost let x = ...` НЕ emit'ится в C-output (паритет с // Verus/Dafny). Ghost — spec-only, no runtime effect. // Type-check ensures ghost vars не reads из non-ghost code // (TODO: enforce когда добавится ghost-flow checker). if decl.is_ghost { return Ok(()); } // Special case: tuple destructure `let (a, b, c) = expr` if let Pattern::Tuple(pats, _) = &decl.pattern { return self.emit_tuple_destructure(pats, &decl.value, decl.ty.as_ref()); } // Plan 53: record destructure `let { tx, rx } = expr` / // `let Pair { tx, rx } = expr`. Делегирует биндинг // полей в существующий `pattern_bind_typed` (он умеет // plain-record case через record_schemas). Refutable // patterns (sum-variant в type_path) ловятся // type-checker'ом — codegen ассамит irrefutable. if let Pattern::Record { .. } = &decl.pattern { return self.emit_record_destructure(decl); } // Plan 184 (Р1, Ф.1-остаток): ref-локал — `ro y ref T = <place>` / // `mut y ref T = <place>` — непереселяемый УКАЗАТЕЛЬ-АЛИАС на // хранилище цели (аналог C++ `T& y = place`). Emit `T* y = // &(<place>);` и регистрируем `y` в `ref_params` → все чтения и // записи авто-деref (`(*y)`), тем же путём, что `mut ref`-параметр // (заход-4). `var_types[y]` = ПОИНТИ-тип `T` (не `T*`), как у // ref-параметров: инференс видит `T`, а деref делает codegen. // Кучевой `T` (Р6 `ref H ≡ H`) — inner_c уже `Nova_H*`, значит // `Nova_H** y = &(handle)`; деref `(*y)` даёт handle. Корректно. if let Some(TypeRef::Ref(inner, _)) = &decl.ty { if let Pattern::Ident { .. } = &decl.pattern { let binding = self.pattern_binding(&decl.pattern)?; let inner_c = self.type_ref_to_c(inner)?; let place = self.emit_expr(&decl.value)?; self.line(&format!("{}* {} = &({});", inner_c, binding, place)); self.var_types.insert(binding.clone(), inner_c); self.ref_params.insert(binding); return Ok(()); } } // Plan 186 (D412): blob materialization at the binding point. // ro-binding -> zero-copy view (default emit_expr path below); // mut-binding / consume-binding -> COPY into the GC heap here, // so the value is a normal writable/growable Vec[u8] buffer // (writes to the .rodata static are never emitted). if let ExprKind::HexBlobLit(bytes) = &decl.value.kind { if (decl.mutable || decl.consume) && matches!(&decl.pattern, Pattern::Ident { .. }) { let bytes = bytes.clone(); let binding = self.pattern_binding(&decl.pattern)?; let vec_c = self.ensure_vec_u8_instance(); let (src, n) = if bytes.is_empty() { ("(const uint8_t*)0".to_string(), 0usize) } else { let n = bytes.len(); (self.intern_blob_literal(&bytes), n) }; self.line(&format!( "{vec_c}* {binding} = ({vec_c}*)nova_blob_copy({src}, {n});" )); self.var_types.insert(binding, format!("{vec_c}*")); return Ok(()); } } // [221.1 №37 guard, M-method-value-static-ret-type-ice] D35 // §Method values form 3 ("static": `Type.method`, fn(...)->R) // bound as a VALUE (`ro mk = Account.new` — NOT itself a // `Call`) is NOT YET properly supported end-to-end: only // form 2 (unbound-instance `Type.@method`, the // `name.starts_with('@')` case a few lines below in // `infer_expr_c_type`'s own Member arm) is wired to a real // C closure/fn-pointer type. A bare dotted qualified // reference like `Account.new` parses as `ExprKind::Path( // ["Account", "new"])` (the SAME shape a module-qualified // path uses), NOT `ExprKind::Member` — confirmed empirically // (repro's own `decl.value.kind` dump), so both AST shapes // are checked below. Left unguarded, this RHS reaches // `infer_expr_c_type` (no case for either shape here), // which silently degrades to an EMPTY C type string — the // emitted declaration comes out as bare `mk = Account_new;` // (no type at all), a CC-FAIL one step removed from the // real cause, or an ICE in other call shapes downstream // (see the marker's own history, docs/plans/backlog- // followups.md). Guard, NOT a feature: an honest, // actionable diagnostic in place of that downstream // degradation — checked HERE (not in the type checker) // because codegen's own `method_overloads` map (populated // per-CU, keyed `(type_name, method_name)`, `MethodSig:: // is_instance` distinguishing `@method` from `.method`) is // exactly the registry that already tells // `emit_method_value_typed` (form 2) apart from a plain // static call — no new registry needed. let static_method_value_parts: Option<(&str, &str)> = match &decl.value.kind { ExprKind::Member { obj, name } if !name.starts_with('@') => { match &obj.kind { ExprKind::Ident(n) if n.chars().next() .map(|c| c.is_ascii_uppercase()).unwrap_or(false) => Some((n.as_str(), name.as_str())), ExprKind::Path(parts) if parts.len() == 1 => Some((parts[0].as_str(), name.as_str())), _ => None, } } ExprKind::Path(parts) if parts.len() == 2 && parts[0].chars().next().map(|c| c.is_ascii_uppercase()).unwrap_or(false) && !parts[1].starts_with('@') => { Some((parts[0].as_str(), parts[1].as_str())) } _ => None, }; if let Some((type_name, method_name)) = static_method_value_parts { let is_static_method = self.method_overloads .get(&(type_name.to_string(), method_name.to_string())) .map(|overloads| overloads.iter().any(|s| !s.is_instance)) .unwrap_or(false); if is_static_method { return Err(format!( "[E_METHOD_VALUE_STATIC_UNSUPPORTED] static-form \ method values are not yet supported; use a \ closure `|| {t}.{m}(...)` instead of binding \ `{t}.{m}` itself as a value (D35 §Method values \ form 3 — codegen cannot yet annotate a bare \ static-method reference's fn-pointer type).", t = type_name, m = method_name, )); } } // Infer type BEFORE emitting so record literals get the right type let binding = self.pattern_binding(&decl.pattern)?; // Plan 173.1 Ф.1 [M-codegen-let-locals-overlay-supervised]: when the // RHS is a body-bearing construct whose trailing may reference the // body's OWN top-level `let`s (e.g. `ro v = supervised { ro inner = // …; inner + 1 }`), pre-register those locals into `var_types` before // probing — mirrors `emit_block_expr`'s identical overlay for plain // `{ … }` blocks (~line 35148). Without this the probe recurses into // an unregistered Ident and hits the `[P67-LEGACY]` panic (found // testing Ф.1's newly-legal `supervised { … v }` nesting — Supervised // wasn't previously a value-position RHS, so this gap never surfaced). let mut _overlay_saved: Vec<(String, Option<String>)> = Vec::new(); if let Some(inner_body) = Self::body_bearing_block(&decl.value) { for s2 in &inner_body.stmts { if let Stmt::Let(d2) = s2 { if let Pattern::Ident { name, .. } = &d2.pattern { let ty2 = d2.ty.as_ref().and_then(|t| self.type_ref_to_c(t).ok()) .unwrap_or_else(|| self.infer_expr_c_type(&d2.value)); _overlay_saved.push((name.clone(), self.var_types.insert(name.clone(), ty2))); } } } } let ty_c_probe = if let Some(ty) = &decl.ty { self.type_ref_to_c(ty) } else { Ok(self.infer_expr_c_type(&decl.value)) }; for (name, old) in _overlay_saved.drain(..).rev() { match old { Some(t) => { self.var_types.insert(name, t); } None => { self.var_types.remove(&name); } } } let mut ty_c = if decl.ty.is_some() { ty_c_probe? } else { let inferred = ty_c_probe?; if inferred == "__none_ambiguous__" { // None без контекста — дефолт nova_int (legacy). // Ошибка только если тип аннотирован и несовместим // (например let x Option[str] = None без hint). // TODO(62.B): bidirectional inference из usage. "NovaOpt_nova_int".into() } else if Self::is_value_struct_ptr(&inferred) && self.is_fluent_value_ptr_for_target(&decl.value, inferred.trim_end_matches('*')) { inferred.trim_end_matches('*').to_string() /* [реестр №36] unannotated let from fluent `-> @` setter: decay ptr->value */ } else { inferred } }; // Plan 174.3 (D53): implicit upcast for `ro x any = <concrete>` — // box the value into a `NovaAny` (void*). Explicit `<v> as any` // already yields a void* (boxed by the As arm), so this only fires // for a genuinely non-`any` RHS. Bypasses the array/value-record // promotion paths below (irrelevant to an `any` binding). if matches!(&decl.ty, Some(TypeRef::Named { path, generics, .. }) if generics.is_empty() && path.len() == 1 && path[0] == "any") { let val_c = self.infer_expr_c_type(&decl.value); if val_c != "void*" && !val_c.is_empty() { let v = self.emit_expr(&decl.value)?; let boxed = self.emit_any_box(&val_c, &v); self.line(&format!("void* {} = {};", binding, boxed)); self.var_types.insert(binding.clone(), "void*".to_string()); return Ok(()); } } // Plan 91.8a.2 followup 2026-05-29: heterogeneous []Protocol. // Detect annotation `[]Protocol` (TypeRef::Array(inner) // where inner is protocol type). Override storage к // NovaArray_void_p* (each slot — heap-alloc'd NovaBox_<P>* // pointer). Set current_array_protocol_box so emit_array_lit // boxes each element + heap-allocs + pushes pointer. let array_protocol_save = self.current_array_protocol_box.clone(); if let Some(TypeRef::Array(inner, _)) = &decl.ty { if let Some(proto_name) = self.extract_protocol_type_name(inner.as_ref()) { let type_args: Vec<String> = if let TypeRef::Named { generics, .. } = inner.as_ref() { generics.iter().filter_map(|g| self.type_ref_to_c(g).ok()).collect() } else { vec![] }; // Pre-emit typedef so box_value_for_protocol can find it. let _ = self.emit_protocol_box_typedef(&proto_name, &type_args); self.current_array_protocol_box = Some((proto_name, type_args)); ty_c = "NovaArray_void_p*".to_string(); } } // target-type-aware emit: для typed-integer ty_c литералы внутри // Binary получают native-typed cast вместо ((nova_int)NLL). // // Plan 51 Ф.1: typed `let x T = { ... }` — anonymous record // literal без префикса берёт тип из аннотации (D55, mirror // `const`-handling). Гейт узкий: значение — **напрямую** // typeless record-литерал. Иначе expected_record_type не // трогаем, чтобы тип `x` не «протёк» во вложенные литералы // внутри других выражений (`let x T = foo({ ... })`). let direct_typeless_record = matches!( &decl.value.kind, ExprKind::RecordLit { type_name: None, .. }); let saved_expected = self.expected_record_type.clone(); if decl.ty.is_some() && direct_typeless_record { self.expected_record_type = Self::debt_struct_name_from_c_type(&ty_c); } // Plan 127 Ф.3: if escape analysis flagged this binding as // value-record-promoted, signal emit_record_lit to switch to // heap allocation path. Resolution algorithm: // 1. Need a fn-id (current_fn_id) — sanity: emit_fn sets it // before body emit; methods like main's body sets too. // 2. Need escape_result populated by emit_module(). // 3. Compute value-record type-name from the binding's // annotation OR RHS record-lit type_name. Stamp transient // signal `pending_value_record_heap_promote` BEFORE // emit_expr_with_target_type. // 4. After emit, transient signal cleared either by // emit_record_lit (consume path) или explicit reset. if let (Some(fn_id), Some(esc)) = (self.current_fn_id.as_ref(), self.escape_result.as_ref()) { if esc.is_promoted(fn_id, &binding) { let type_name_opt = Self::value_record_type_name_for_let(decl, &ty_c); if let Some(type_name) = type_name_opt { // Only switch if this is actually a known value-record // declared в this module (i.e. NovaValue_<X>-style C ty). // №390 fix: OR one of the RUNTIME_DEFINED_TYPES sync // primitives (AtomicInt & co.) — these are hand-C- // backed and consequently don't always populate // `value_record_names` via the module.items forward- // decl loop (see RUNTIME_VALUE_RECORD_CTOR_TYPES's own // doc comment for why); escape_analyze already applied // the same fallback when it flagged `binding` promoted. if self.value_record_names.contains(&type_name) || RUNTIME_VALUE_RECORD_CTOR_TYPES.contains(&type_name.as_str()) { self.pending_value_record_heap_promote = Some(type_name.clone()); self.promoted_value_record_locals.insert(binding.clone()); // Override binding C-type к pointer (NovaValue_X*). ty_c = format!("NovaValue_{}*", type_name); } } } } // Plan 118 Ф.1: primitive-scalar escape promotion. // Detect BEFORE emit_expr: if promoted and NOT a value-record, // override ty_c to pointer, emit alloc+assign after val is known. let primitive_heap_promoted = if let (Some(fn_id), Some(esc)) = (self.current_fn_id.as_ref(), self.escape_result.as_ref()) { esc.is_promoted(fn_id, &binding) && !self.promoted_value_record_locals.contains(&binding) && !ty_c.ends_with('*') // not already a pointer type && !ty_c.starts_with("NovaValue_") && !ty_c.starts_with("Nova") // primitive C types don't start with Nova } else { false }; // D73/D84: when the RHS is a .into() call, set expected_into_target // so the resolver can pick the user's T.from(v) over stdlib types // (e.g. StringBuilder.from(str)) that also accept the source type. let saved_into_target = self.expected_into_target.clone(); let is_into_call = matches!(&decl.value.kind, ExprKind::Call { func, args, .. } if args.is_empty() && matches!(&func.kind, ExprKind::Member { name, .. } if name == "into")); if is_into_call && decl.ty.is_some() { let target_name = Self::debt_nova_type_name_from_c(&ty_c); if !target_name.is_empty() { self.expected_into_target = Some(target_name); } } // Plan 172.14 (sret/_out §3): placement-предикат стек-слота // дескриптора вида. ro-биндинг + не эскейпит (ident_escapes, // V1 OVER) + RHS — прямой Range-срез (форма; Vec-принадлежность // проверит сама range-slice ветка по obj_ty) → эмитим zero-init // слот ДО RHS (до любой GC-точки, дизайн §3) и взводим armed // с ExprId RHS (защита от утечки во вложенный вызов). // // Plan 172.15 Ф.1 — ЕДИНСТВЕННЫЙ ОСТАТОК, заданный именем типа, // и остаётся он НАМЕРЕННО. Это ДРУГОЙ механизм, чем периметр // `__sret` (`sret_fn_eligible`): там решается, умеет ли ФУНКЦИЯ // писать результат в чужой буфер (снято до нуля), здесь — // вправе ли ВЫЗЫВАЮЩИЙ дать под этот буфер свой кадр. Второе // есть спуск куча→стек, а он по правилу двух фаз (плана 172.15, // раздел «Разбор чужих реализаций», П1) требует сперва ответа // фазы I — «обязан ли адрес этого типа быть стабильным». Ответ // по типу сегодня давать нечем: атрибута `#no_move` нет, и Ф.1 // установила, что носителей у него в Nova пока не нашлось (см. // док `sret_phase1_type_admits_out`). Пока фаза I не отвечает, // расширять фазу II на все кучевые записи значило бы поставить // выгоду впереди корректности: промах `ident_escapes` (V1 OVER) // дал бы висячий указатель на освобождённый кадр. Снятие этого // остатка — предмет Ф.1-бис/Ф.1-тер (один порог ≥24Б и спуск // куча→стек), которые идут ПОСЛЕ тега. let sret_stack_armed = !decl.mutable && !decl.consume && matches!(&decl.pattern, Pattern::Ident { .. }) && ty_c.starts_with("Nova_Vec____") && ty_c.ends_with('*') && !ty_c.ends_with("**") && decl.value.id.is_set() && (matches!(&decl.value.kind, ExprKind::Index { index, .. } if matches!(index.kind, ExprKind::Range { .. })) // RHS Call: потребитель (sret_maybe_rewrite_call) сам // проверит cname ∈ sret_fns; непотреблённый armed — // безусловный сброс ниже, слот останется мёртвым. || matches!(&decl.value.kind, ExprKind::Call { .. })) && self.current_fn_id.as_ref().zip(self.escape_result.as_ref()) .map_or(false, |(fid, esc)| !esc.ident_escapes(fid, &binding)); if sret_stack_armed { let slot = self.fresh_tmp(); let s_ty = ty_c.trim_end_matches('*').trim(); self.line(&format!("{} {} = {{0}};", s_ty, slot)); self.sret_out_dest_struct = Some(s_ty.to_string()); self.sret_out_dest = Some((format!("(&{})", slot), decl.value.id)); } let val = self.emit_expr_with_target_type(&decl.value, &ty_c)?; // Безусловный сброс: непотреблённый armed не должен пережить RHS. self.sret_out_dest = None; self.sret_out_dest_struct = None; self.expected_into_target = saved_into_target; // 172.4 Ф.3 блокер-2 (binding-decay): fluent value-record цепочка // возвращает ptr (NovaValue_X*), биндинг к value-локалу требует // deref. Robust EMIT-сигнал (урок реверта 2026-06-28): C-возврат // метода из реестра fn_ret_* (эмит-факт), НЕ context-fragile infer. // 172.4 Ф.3 A6 / Plan 184 Р5-Р7: NT/VR fluent `-> @` возвращает ptr // (`NovaValue_X*`/`NovaTuple_X*` = ref Self); биндинг к value-локалу — // deref (auto-conversion ref T -> T). Общий предикат — см. // `is_fluent_value_ptr_for_target` (let/arg/return в lockstep). let val = if self.is_fluent_value_ptr_for_target(&decl.value, &ty_c) { format!("(*({}))", val) } else { val }; // №390 fix: if this binding was flagged value-record-promoted // (line ~31314 above) but the RHS was NOT a RecordLit — // e.g. `mut counter = AtomicInt.new(1)`, a constructor CALL // recognized by escape_analyze's `infer_value_record_from_expr` // `X.new(...)` arm — `emit_record_lit`'s heap-promote // consumption (which only fires for an actual RecordLit expr) // never ran, so the signal survives to here unconsumed: `val` // is the PLAIN by-value call result, but `ty_c` was already // overridden to the pointer type above. `.take()` (not a // plain reset) lets us detect this leak and wrap the value // ourselves — same alloc+copy shape as `primitive_heap_promoted` // below. Root cause: docs/plans/221.1-bug-sweep.md №390 — // without this, either a compile-time type mismatch (caught by // docs/plans/repro/p390_scratch_escape_rc.nv) or, if types // happened to coincide, a binding that LOOKS promoted // (pointer-typed) but whose pointee is still stack-allocated — // `&binding`-derived pointers stored elsewhere (e.g. a // returned record's field) go dangling the instant the // constructing fn returns. let value_record_call_leaked = self.pending_value_record_heap_promote.take().is_some(); self.expected_record_type = saved_expected; // Plan 91.8a.2 followup: restore protocol-box hint after array // literal emission consumed it. self.current_array_protocol_box = array_protocol_save; // For pointer types: the emitted tmp expression already carries the type. // Just declare the binding with the right type. self.var_types.insert(binding.clone(), ty_c.clone()); // [M-value-record-param-default-after-indirection] (ICE-пачка // п.3): `ref_params` (Plan 184) is populated ONCE per enclosing // fn from its OWN by-pointer params (value-record/big-struct, // Plan 172.14) and is only saved/restored at FN boundaries — // never at block scope. `callnorm.rs`'s default/named-arg // desugar (Plan 46 D102) synthesizes a fresh `let <param_name> = // <temp>` binding NAMED AFTER THE CALLEE'S OWN PARAMETER — when // that name happens to COLLIDE with an enclosing by-pointer // param of the SAME NAME (`fn callee(f Foo, stop Option[int] = // None)` called as `callee(f)` inside `fn caller(f Foo) { ... }` // — both params are literally `f`), this THIS declaration is a // plain-value local (`ty_c`/`val` here never carry pointer-ness // — `ref_params` reads are hidden behind an explicit `(*name)` // deref at emission, never surfaced into `ty_c`), but the STALE // `ref_params["f"]` entry from the OUTER fn scope survives // untouched, so the call-argument emission for THIS shadowed // `f` (a few lines later, in the SAME synthesized block) still // treats it as needing a `(*f)` deref — "indirection requires // pointer operand ('NovaValue_Foo' invalid)": the shadow's // ACTUAL C storage is a plain value, not a pointer. A fresh // `let`-declared local is NEVER itself a by-pointer parameter, // regardless of what name it reuses — remove the shadowed name // from `ref_params` here (`emit_block_expr` already snapshots/ // restores `var_types` around a `{ }` scope for the identical // shadowing hazard; mirror that restore for `ref_params` there // too, so code AFTER a callnorm block that did NOT hit this // exact collision keeps seeing the outer ref-param correctly). self.ref_params.remove(&binding); // Plan 72 P0 (E7201): track protocol-typed bindings so method calls // on erased protocol vars emit E7201 instead of silent NULL. // Also propagates through plain assignment: `let xx = x` where x is protocol-typed. if let Some(ty_ann) = &decl.ty { if let Some(proto_name) = self.extract_protocol_type_name(ty_ann) { self.protocol_vars.insert(binding.clone(), proto_name); } else { self.protocol_vars.remove(&binding); } } else if let ExprKind::Ident(rhs_var) = &decl.value.kind { if let Some(proto_name) = self.protocol_vars.get(rhs_var.as_str()).cloned() { self.protocol_vars.insert(binding.clone(), proto_name); } else { self.protocol_vars.remove(&binding); } } else { self.protocol_vars.remove(&binding); } // Plan 72 P1-C: track Result[T,E]-typed bindings for correct .unwrap()/.ok() types. if let Some(ty_ann) = &decl.ty { if let Some(result_params) = self.extract_result_type_params(ty_ann) { self.result_type_params.insert(binding.clone(), result_params); } else { self.result_type_params.remove(&binding); } } else if let ExprKind::Ident(rhs_var) = &decl.value.kind { if let Some(params) = self.result_type_params.get(rhs_var.as_str()).cloned() { self.result_type_params.insert(binding.clone(), params); } else { self.result_type_params.remove(&binding); } } else if let ExprKind::Call { func, .. } = &decl.value.kind { // Channel-first (196.3 wave-2, D30/D85): `let r = f(...)` // — prefer the checker's resolved Result[T,E] for this // call (`resolved_types[decl.value.id]`; 196.4 Stage-1a // materializes method-level-generic returns here) over // the legacy name-keyed re-derivation. // // Plan 72 P2-A (legacy fallback): pull (ok_c, err_c) from // the callee's registered DECLARED return type so // r.unwrap()/.unwrap_or()/.ok()/.err() get T/E — still // reached for producers Stage-1a/1b don't cover yet. if let Some(params) = self.channel_result_type_params_c(&decl.value) .or_else(|| self.call_result_type_params_key(func) .and_then(|k| self.fn_result_type_params.get(&k).cloned())) { self.result_type_params.insert(binding.clone(), params); } else { self.result_type_params.remove(&binding); } } else { self.result_type_params.remove(&binding); } // Plan 72 P3-B (fat pointer): if this var is protocol-typed and RHS // has a concrete pointer type, generate vtable + NovaBox fat pointer. // The box initialization replaces the plain `void* x = val` declaration // (see line below). We stash (vtable_instance, box_c_type) here. let mut protocol_box: Option<(String, String)> = None; // (vtable_instance, box_c_type) if let Some(proto_name) = self.protocol_vars.get(&binding).cloned() { let type_args: Vec<String> = if let Some(TypeRef::Named { generics, .. }) = &decl.ty { generics.iter().filter_map(|g| self.type_ref_to_c(g).ok()).collect() } else { vec![] }; let concrete_c = self.infer_expr_c_type(&decl.value); // Plan 91.8a.2 followup 2026-05-29: support non-generic // protocols (Printable, Equatable, Comparable) in coercion. // Plan 97.1 enabled vtable/box emission for empty type_args; // this site previously required `!type_args.is_empty()` and // silently fell through to bare assignment `NovaBox_P x = m;` // — invalid C. if concrete_c.ends_with('*') { if let Some((vtable_instance, box_c_type)) = self.emit_protocol_vtable_companion( &proto_name, &type_args, &concrete_c ) { // Override var_types so method dispatch sees the box type. self.var_types.insert(binding.clone(), box_c_type.clone()); // Keep protocol_var_vtable for backward compat (method dispatch checks it). self.protocol_var_vtable.insert(binding.clone(), vtable_instance.clone()); protocol_box = Some((vtable_instance, box_c_type)); } else { self.protocol_var_vtable.remove(&binding); } } else { self.protocol_var_vtable.remove(&binding); } } else { self.protocol_var_vtable.remove(&binding); } // Plan 49 Ф.6 P0 fix: track CancelToken[T] T для per-T `reason()` // un-box. Rebind ОЧИЩАЕТ предыдущую запись чтобы не утечь между // function/test bodies (cancel_token_t_map не scope'd). self.cancel_token_t_map.remove(&binding); if let Some(crate::ast::TypeRef::Named { path, generics, .. }) = &decl.ty { if path.len() == 1 && path[0] == "CancelToken" { if let Some(t_ref) = generics.first() { if let Ok(t_c) = self.type_ref_to_c(t_ref) { self.cancel_token_t_map.insert(binding.clone(), t_c); } } } } // Track mutability so spawn-capture can decide copy-by-value vs by-ptr. if decl.mutable { self.var_mutable.insert(binding.clone()); } else { self.var_mutable.remove(&binding); } // Propagate tuple element types so pair.0 can be correctly typed. // Mirror the result_ok_inner_types pattern: always update (insert or remove) // to prevent stale entries from a prior binding with the same name in a // sibling fn / test body (global maps are not scope-keyed). if let Some(elem_tys) = self.tuple_element_types.get(&val).cloned() { // RHS is a rebind of another known tuple var — propagate its elements. self.tuple_element_types.insert(binding.clone(), elem_tys); } else if ty_c.starts_with("_NovaTuple_") { // RHS is a call/expr whose inferred type is a mono tuple — derive // element types from the mangled name so .0/.1 etc. cast correctly. if let Some(elems) = Self::parse_mono_tuple_elements(&ty_c) { self.tuple_element_types.insert(binding.clone(), elems); } else { self.tuple_element_types.remove(&binding); } } else { // RHS is not a tuple — evict any stale entry for this binding name. self.tuple_element_types.remove(&binding); } // Propagate array element type so xs[i].field can be correctly typed if let Some(arr_elem_ty) = self.array_element_types.get(&val).cloned() { self.array_element_types.insert(binding.clone(), arr_elem_ty); } else if let Some(arr_elem_ty) = self.channel_array_elem_c(&decl.value) { // Plan 123 chain-cache fix: RHS — field/chain access // (`@map._buckets` → `_at_map__buckets_chain`). `val` — // C-строка field-access, не ключ в array_element_types, // поэтому lookup выше промахивается. Вычисляем element type // из AST поля напрямую (через template+subst для generic // полей вроде `_buckets []Slot[K,V]`), иначе индексация // кэш-temp `_at_..._chain[i]` падает в nova_int fallback // → `slot->tag` на nova_int (CC-FAIL: set/hashmap iter). self.array_element_types.insert(binding.clone(), arr_elem_ty); } // Plan 63 Fix F+ [M-result-erased-no-mono]: production-grade // propagation. Если RHS — call к fn'у с зарегистрированным // Result Ok boxed type (fn_result_ok_inner_types), register // на binding. Pending mechanism Fix F не покрывает этот case // потому что boxing happens внутри callee scope. Always // overwrite — helper authoritative из fn signature (vs // possibly-stale value from prior let binding с тем же name // в sibling fn body — global maps не scope'ятся). if let Some(inner) = self.try_get_result_ok_inner_type_for_expr(&decl.value) { self.result_ok_inner_types.insert(binding.clone(), inner); } else { // RHS не Result/non-mono — снять стале запись (если // binding имя совпало с прежним let из другого fn). self.result_ok_inner_types.remove(&binding); } // Plan 55 Ф.1: track element closure-sig for local `[]fn(...) -> T` vars // so `for f in xs { f() }` and `xs.push(|| ...)` work in non-param contexts. if let Some(crate::ast::TypeRef::Array(inner, _)) = &decl.ty { if let crate::ast::TypeRef::Func { params: fp, return_type, .. } = inner.as_ref() { // Plan 70 PhaseA1.4: strict — array-of-fn element sig lowering. let ptys: Vec<String> = fp.iter() .map(|t| self.type_ref_to_c(t).map_err(|e| self.err_no_int_fallback( &format!("let `{}` array-of-fn element param type", binding), &e, ))) .collect::<Result<Vec<_>, _>>()?; let rty = match return_type.as_ref() { Some(rt) => self.type_ref_to_c(rt).map_err(|e| self.err_no_int_fallback( &format!("let `{}` array-of-fn element return type", binding), &e, ))?, None => "nova_unit".to_string(), }; self.array_param_fn_sigs.insert(binding.clone(), (ptys, rty)); // Hint emit_array_lit when value is `[]` so storage uses void_p. if matches!(decl.value.kind, ExprKind::ArrayLit(ref e) if e.is_empty()) { // Already handled via emit_expr_with_target_type below if hint set. } } } // Special case: `let xs = s.bytes()` / `s.as_bytes()` / `s.chars()` — set element // type explicitly, even though val is not a known variable. if let ExprKind::Call { func, .. } = &decl.value.kind { // D38: turbofish прозрачен — смотрим под него. let func = func.unwrap_turbofish(); if let ExprKind::Member { obj, name } = &func.kind { if self.infer_expr_c_type(obj) == "nova_str" { match name.as_str() { "bytes" => { // D410 (ex-D178/D176 as_bytes): same element type self.array_element_types .insert(binding.clone(), "nova_byte".into()); } // D410: `chars()` now returns `CharsIter` (lazy), not an // array — the retired `to_chars()` (owned `[]char`) arm no // longer applies; materialization is `chars().collect()`. _ => {} } } } } // Consume pending Option inner type (set when boxing a struct for nova_make_Option_Some) if let Some(inner_ty) = self.pending_option_inner_type.take() { self.option_inner_types.insert(binding.clone(), inner_ty); } // Plan 63 Fix F: consume pending Result Ok inner type (boxed tuple/struct). if let Some(inner_ty) = self.pending_result_ok_inner_type.take() { self.result_ok_inner_types.insert(binding.clone(), inner_ty); } // Plan 72 P3-B (fat pointer): protocol-typed var → NovaBox init. // Plan 100.8 fix: if binding was hoisted (pre-declared before setjmp), // emit assignment-only (no type) to avoid C redeclaration error. // [M-consume-rebind-nested-block-shadow] (Plan 172.13): a // `consume x = expr` rebind whose prior binding lives in an // ENCLOSING scope (alpha_rename-flagged by span) reuses that // SAME live C variable the same way — plain reassignment, no // fresh block-scoped declaration (which would go out of scope // at the end of this block, leaving the outer `x` stale). let is_hoisted = self.hoisted_let_vars.remove(&binding) || self.consume_reuse_spans.contains(&decl.span); // [M-c-keyword-ident-collision] (Plan 172.13): mangle ONLY the // C-text form of the binding name here — `binding` itself stays // RAW for every map key above/below (var_types, var_mutable, // hoisted_let_vars, promoted_primitive_locals, ...), which is // what all later `ExprKind::Ident` reads look up by. let binding_c = Self::mangle_field_name(&binding); if let Some((vtable_instance, box_c_type)) = &protocol_box { // №546-дополнение (интегратор, 2026-08-10): это ЧЕТВЁРТЫЙ // драйвер боксинга, и он строит литерал сам, минуя // `box_value_for_protocol`. После того как коэрсия // появилась в `emit_expr_with_target_type`, `val` сюда // приходит УЖЕ боксированным — и обёртка давала // `NovaBox_P g = { .data = (void*)((NovaBox_P){…}), … }`, // то есть бокс внутри бокса (CC-FAIL: fat pointer нельзя // положить в `void*`). Готовый бокс присваиваем как есть. if Self::value_is_protocol_box(&val) { if is_hoisted { self.line(&format!("{} = {};", binding_c, val)); } else { self.line(&format!("{} {} = {};", box_c_type, binding_c, val)); } } else if is_hoisted { self.line(&format!( "{} = {{ .data = (void*)({}), .vtable = &{} }};", binding_c, val, vtable_instance )); } else { self.line(&format!( "{} {} = {{ .data = (void*)({}), .vtable = &{} }};", box_c_type, binding_c, val, vtable_instance )); } } else if value_record_call_leaked { // №390 fix: value-record-promoted binding whose RHS was a // constructor CALL (`X.new(...)`), not a RecordLit — see // the comment at the `.take()` site above. `ty_c` is // already the pointer type (`NovaValue_X*`, set at the // promotion-detection site); allocate storage for the // POINTEE and copy the plain by-value call result in. let base_ty = ty_c.trim_end_matches('*'); self.line(&format!( "{} {} = ({})nova_alloc(sizeof({}));", ty_c, binding_c, ty_c, base_ty )); self.line(&format!("*{} = {};", binding_c, val)); } else if primitive_heap_promoted { // Plan 118 Ф.1: heap-promote primitive local. // Emit: `C_ty* name = (C_ty*)nova_alloc(sizeof(C_ty));` // `*name = val;` // Plan 116 Ф.3 fix: var_types keeps the BASE type (not `T*`). // Value-position reads now emit `(*name)` (see the // promoted_primitive_locals arm in emit_expr::Ident), so type // inference must agree with that deref'd text — storing `T*` // here made every downstream infer_expr_c_type see a pointer // for what reads as a value (wrong casts in interpolation / // comparisons). `&name` still infers `T*` via the AddrOf arm. self.promoted_primitive_locals.insert(binding.clone()); self.var_types.insert(binding.clone(), ty_c.clone()); self.line(&format!( "{}* {} = ({}*)nova_alloc(sizeof({}));", ty_c, binding_c, ty_c, ty_c )); self.line(&format!("*{} = {};", binding_c, val)); } else if is_hoisted { self.line(&format!("{} = {};", binding_c, val)); } else { self.line(&format!("{} {} = {};", ty_c, binding_c, val)); } // Plan 11 Ф.4: RHS — method value `obj.@method` или `Type.@method`. // Регистрируем binding в fn_param_sigs так чтобы `f(args)` работало. // Plan 11 Ф.5: `expr as fn(P...) -> R` — type annotation для disambig // overloaded method values. Берём signature из аннотации, а не из registry. let (mv_expr, type_anno_sig): (&Expr, Option<(Vec<String>, String)>) = if let ExprKind::As(inner, ty) = &decl.value.kind { if let TypeRef::Func { params: fp, return_type, .. } = ty { // Plan 70 PhaseA1.4: strict — `as fn(...) -> R` annotation sig. let ptys: Vec<String> = fp.iter() .map(|t| self.type_ref_to_c(t).map_err(|e| self.err_no_int_fallback( &format!("let `{}` `as fn` param annotation", binding), &e, ))) .collect::<Result<Vec<_>, _>>()?; let rty = match return_type.as_ref() { Some(rt) => self.type_ref_to_c(rt).map_err(|e| self.err_no_int_fallback( &format!("let `{}` `as fn` return annotation", binding), &e, ))?, None => "nova_unit".to_string(), }; (inner.as_ref(), Some((ptys, rty))) } else { (&decl.value, None) } } else { (&decl.value, None) }; if let ExprKind::Member { obj, name } = &mv_expr.kind { if let Some(method_name) = name.strip_prefix('@') { // Plan 11 Ф.5: type annotation override — берём sig из // `as fn(...) -> R` если есть. Иначе — first overload. if let Some(anno_sig) = type_anno_sig.clone() { self.fn_param_sigs.insert(binding.clone(), anno_sig); } else { // Resolve receiver type. let (type_name, is_unbound) = match &obj.kind { ExprKind::Ident(n) => { let is_type = n.chars().next().map(|c| c.is_ascii_uppercase()).unwrap_or(false) || matches!(n.as_str(), // Plan 172.1-K5: `uint`/`size` были ПРОПУЩЕНЫ → // `uint.@method` мисхэндлился как переменная. "int" | "i8" | "i16" | "i32" | "i64" | "u8" | "u16" | "u32" | "u64" | "uint" | "size" | "f32" | "f64" | "bool" | "char" | "str"); if is_type { (n.clone(), true) } else { let obj_ty = self.var_types.get(n).cloned().unwrap_or_default(); let t = Self::debt_nova_type_name_from_c(&obj_ty); (t, false) } } ExprKind::Path(parts) if parts.len() == 1 => (parts[0].clone(), true), _ => { let obj_ty = self.infer_expr_c_type(obj); let t = Self::debt_nova_type_name_from_c(&obj_ty); (t, false) } }; let key = (type_name.clone(), method_name.to_string()); if let Some(overloads) = self.method_overloads.get(&key).cloned() { if let Some(sig) = overloads.first() { let recv_c_ty = match type_name.as_str() { "int" | "i64" => "nova_int".to_string(), "f64" => "nova_f64".to_string(), "f32" => "nova_f32".to_string(), "str" => "nova_str".to_string(), "char" => "nova_int".to_string(), "u8" => "nova_byte".to_string(), "bool" => "nova_bool".to_string(), _ => format!("Nova_{}*", type_name), }; let param_tys: Vec<String> = if is_unbound { std::iter::once(recv_c_ty).chain(sig.param_c_types.iter().cloned()).collect() } else { sig.param_c_types.clone() }; self.fn_param_sigs.insert(binding.clone(), (param_tys, sig.return_c_type.clone())); } } } } } // [Plan 228 Ф.2(a), реестр 221.1 №94-v2] Unified channel-fed // HOF-binding registration: read the CHECKER's own materialized // return type for this let's RHS (`resolved_types[decl.value.id]`, // 196.4 channel — Ф.2's companion checker-producer donation // (`resolve_fn_newtype_typeref` in types/mod.rs) additionally // populates it for "calling a scope-bound fn-newtype-typed // local", the exact form the six legacy arms below exist for). // If that result is a fn-newtype/alias-of-fn NAME (peels via // `fn_newtype_sigs`, D52-амендмент's own registry — same // call-through `resolve_fn_typeref` performs) OR already a bare // `Func` shape, ONE registration gives `binding` both (a) its // OWN callable signature (`fn_param_sigs`) and (b) — if THAT // signature's return is itself another fn-newtype — // `binding`'s nested callable signature (`fn_returns_fn_sig`, // `nested_fn_return_sig`) — regardless of which AST shape // (`Ident`/`Call`/`Member` RHS) produced the value. This is // exactly what the six hand-written arms below (Ident/ // Call-L1/Call-L2/Member-L1/Member-L2) each re-derive // independently by re-scanning the RHS's OWN AST shape — the // checker already told us the call's RESULT type, which alone // determines both signatures. Gated to fire ONLY on a genuine // channel hit; a miss (residual/erased, or // any scenario the channel doesn't cover) leaves the SIBLING // fallbacks below (Lambda/ClosureLight/ClosureFull/method-value, // untouched by this plan — different RHS shapes) to run as // before. The six legacy arms this registration REPLACES // (Ident/Call-L1/Call-L2/Member-L1/Member-L2 + // `fn_returns_fn_sig_l2`) were removed outright after δ0-корпус // NO-HIT-верификация — see docs/plans/228-fnnt-channel-materialization.md. // [M-closure-param-fn-newtype-field-access-int-miscompile] // (реестр 221.1 №104): precise per-DECLARATION flag — `binding` // is a GLOBAL, ever-growing, name-keyed map across the WHOLE // compile unit (never cleared between declarations), so a // RE-CHECK of `self.fn_param_sigs.contains_key(binding)` after // this block is NOT proof that THIS channel hit is what put it // there — an EARLIER, wholly unrelated declaration reusing the // same local variable NAME elsewhere in the corpus (`f`/`g`/`h` // are extremely common) can ALREADY have an entry, giving a // false "already handled" positive that would wrongly skip the // `ClosureLight` arm's OWN registrations below (in particular // `unanno_light_clos`, the D402 call-site width-preserving // re-derivation — NOT gated on `fn_param_sigs` at all — was the // real regression this exact re-check caused; confirmed via // `d402_closure_return_width.nv` isolated-file RUN-FAIL). let mut hof_channel_handled = false; if decl.value.id.is_set() { if let Some(rt) = self.resolved_types.get(&decl.value.id).cloned() { // Two channel shapes reach here: (1) a fn-newtype/alias // NAME (`Named{X}`) — peel via `fn_newtype_sigs` // directly (`resolve_fn_typeref`'s own registry), the // common case for a CALL whose return is a fn-newtype // (№78/№90 Call/Member forms) — OR, since №104, the // checker's OWN let-annotation fact for a `ClosureLight` // RHS (types/mod.rs `f1_check_assign_let`); (2) a bare // `ResolvedType::Func` — the checker's universal // per-Ident writer (types/mod.rs `f1_expr_inner` // ~7836) channels this directly for a RHS that is // simply a NAME reference to a free fn (`ro m Mid = // identity_mw`, no call at all — the №78 Ident form), // round-tripped back to `TypeRef` by // `resolved_type_to_typeref_named` (Named/Func/Unit // only — the shapes a HOF-binding signature can ever // carry) so both cases share the SAME lowering tail. let peeled: Option<TypeRef> = match &rt { crate::types::ResolvedType::Named { name, args, .. } if args.is_empty() => { self.fn_newtype_sigs.get(name).cloned() } crate::types::ResolvedType::Func { .. } => { self.resolved_type_to_typeref_named(&rt, decl.value.span) } _ => None, }; if let Some(TypeRef::Func { params, return_type, .. }) = &peeled { let ptys_r: Result<Vec<String>, String> = params.iter().map(|t| self.type_ref_to_c(t)).collect(); let rty_r: Result<String, String> = match return_type.as_deref() { Some(t) => self.type_ref_to_c(t), None => Ok("nova_unit".to_string()), }; if let (Ok(ptys), Ok(rty)) = (ptys_r, rty_r) { self.icr_trace("N228_hof_binding_channel_hit"); self.fn_param_sigs.insert(binding.clone(), (ptys, rty)); hof_channel_handled = true; } } if let Some(func_ty) = &peeled { if let Some(sig2) = self.nested_fn_return_sig(func_ty) { self.fn_returns_fn_sig.insert(binding.clone(), sig2); } } } } // Plan 14 Ф.3: если RHS — Ident, ссылающийся на user fn // (`let f = inc`), регистрируем binding в fn_param_sigs // через user_fn_sigs. Тогда `f(x)` пойдёт через // NOVA_CLOS_CALL_* macro (так же как для lambda). if let ExprKind::Ident(rhs_name) = &decl.value.kind { if !self.var_types.contains_key(rhs_name) { if let Some(sig) = self.user_fn_sigs.get(rhs_name).cloned() { self.fn_param_sigs.insert(binding.clone(), sig); } // [Plan 228 Ф.2(a) снос, реестр 221.1 №94-v2, было fix // M-nested-fn-newtype-bind-then-call-broken №78]: the // `fn_returns_fn_sig`-forward-propagation legacy arm // (`ro m Mid = identity_mw; ro h2 = m(h); h2(5)`) is now // NO-HIT — the unified channel-fed registration above // (above) covers this exact RHS shape (a bare // reference to a free fn, or to a previously-propagated // local — `types/mod.rs` `infer_expr_type`'s Ident arm now // types the bare-fn-value case directly, so the checker's // universal per-Ident writer channels it). Verified // NO-HIT on δ0-корпус (14 фикстур, isolated compiles, // `NOVA_TRACE_ICR`): trace ID `N78_ident_rhs_fnretsig_ // propagate` never fires. Removed per NO-HIT-снос // protocol — see docs/plans/228-fnnt-channel-materialization.md. } } // If RHS is a lambda, register the binding in fn_param_sigs so inc(5) works if let ExprKind::Lambda { params, return_type, .. } = &decl.value.kind { // Plan 70 PhaseA1.4: strict — Lambda annotated params. Untyped param // (no `p.ty`) defaults to nova_int — Cat D legitimate (lambda params // unannotated inferred from context; default int is the documented // bootstrap fallback, not silent miscompilation). let param_c_tys: Vec<String> = params.iter().map(|p| { match &p.ty { Some(ty) => self.type_ref_to_c(ty).map_err(|e| self.err_no_int_fallback( &format!("let `{}` lambda param `{}` annotation", binding, p.name), &e, )), None => Ok("nova_int".into()), // Cat D — bootstrap default for unannotated } }).collect::<Result<Vec<_>, _>>()?; // Infer return type: priority — let-annotation > lambda annotation > default. let ret_c = if let Some(TypeRef::Func { return_type: rt, .. }) = decl.ty.as_ref() { // let-annotation `fn(...) -> R` — strict для R если есть. match rt.as_ref() { Some(t) => self.type_ref_to_c(t).map_err(|e| self.err_no_int_fallback( &format!("let `{}` annotation return type", binding), &e, ))?, None => "nova_int".to_string(), // Cat D — fn без -> default int } } else if let Some(rt) = return_type { // Plan 08 Ф.4 prerequisite: lambda с явной `-> T`-аннотацией. self.type_ref_to_c(rt).map_err(|e| self.err_no_int_fallback( &format!("let `{}` lambda return annotation", binding), &e, ))? } else { "nova_int".into() // Cat D — bootstrap default }; self.fn_param_sigs.insert(binding.clone(), (param_c_tys, ret_c)); } // Plan 19, C5: closure-light в let-биндинге. Параметры // untyped, типы выводятся из контекста. Bootstrap — без // глубокого inference: если есть `let f fn(...) -> R = |x|...` // аннотация, берём типы оттуда; иначе все params/ret // дефолтятся в nova_int. Это позволяет `let zero = || 0; // zero()` корректно резолвиться через NOVA_CLOS_CALL_*. // // [M-closure-param-fn-newtype-field-access-int-miscompile] // (реестр 221.1 №104): this arm's OWN `decl.ty.as_ref()` // structural match only recognizes a BARE `fn(...) -> ...` // annotation — a NAMED fn-newtype annotation (`type Handler // fn(ServerRequest) -> str`) fell straight to the // `nova_int`-default branch below, miscompiling every // str/record-typed closure param access. `binding`'s channel // block above (N228_hof_binding_channel_hit) now ALSO covers // this exact case for a `ClosureLight` RHS (types/mod.rs // `f1_check_assign_let` writes the let-annotation's // `ResolvedType` into `resolved_types` for a closure-literal // RHS — a slot no other producer ever wrote for a closure) — // skip this arm's WEAKER fallback derivation entirely when the // channel already resolved `binding`'s signature THIS // declaration (`hof_channel_handled` — a precise per-statement // flag, NOT a `fn_param_sigs.contains_key` re-check: that map // is name-keyed and global across the whole compile unit, so // it can already contain an unrelated EARLIER declaration's // entry for the SAME common local name — see the flag's own // doc above for the regression that caused). if hof_channel_handled { // handled by the channel block above. } else if let ExprKind::ClosureLight { params, body } = &decl.value.kind { let arity = params.len(); let (param_c_tys, ret_c) = if let Some(TypeRef::Func { params: anno_params, return_type: anno_ret, .. }) = decl.ty.as_ref() { // Plan 70 PhaseA1.4: strict — ClosureLight typed via let-annotation. let ptys: Vec<String> = anno_params.iter() .map(|t| self.type_ref_to_c(t).map_err(|e| self.err_no_int_fallback( &format!("let `{}` ClosureLight annotation param", binding), &e, ))) .collect::<Result<Vec<_>, _>>()?; let rty = match anno_ret.as_ref() { Some(t) => self.type_ref_to_c(t).map_err(|e| self.err_no_int_fallback( &format!("let `{}` ClosureLight annotation return", binding), &e, ))?, None => "nova_int".to_string(), // Cat D — fn без -> default int }; (ptys, rty) } else { // Без annotation: дефолт nova_int для arity и // ret. Bidirectional inference из first-use — // C6 фаза Plan 19; здесь — bootstrap fallback. // Plan 172.1 D402: also store param names + body for call-site // re-derivation when actual arg types are more specific than nova_int. let param_names: Vec<String> = params.iter() .map(|p| p.name.clone()) .collect(); let body_expr: Expr = match body { crate::ast::ClosureBody::Expr(e) => (**e).clone(), crate::ast::ClosureBody::Block(b) => Expr::new( ExprKind::Block(b.clone()), b.span, ), }; self.unanno_light_clos.insert(binding.clone(), (param_names, body_expr)); let ptys: Vec<String> = (0..arity).map(|_| "nova_int".to_string()).collect(); (ptys, "nova_int".to_string()) }; self.fn_param_sigs.insert(binding.clone(), (param_c_tys, ret_c)); } // Plan 19, C5: closure-full в let-биндинге. Типы // параметров и return явные — берём из FnSigBody. if let ExprKind::ClosureFull(sb) = &decl.value.kind { // Plan 70 PhaseA1.4: strict — ClosureFull typed params + return. let param_c_tys: Vec<String> = sb.params.iter() .map(|p| self.type_ref_to_c(&p.ty).map_err(|e| self.err_no_int_fallback( &format!("let `{}` ClosureFull param `{}`", binding, p.name), &e, ))) .collect::<Result<Vec<_>, _>>()?; let ret_c = match sb.return_type.as_ref() { Some(t) => self.type_ref_to_c(t).map_err(|e| self.err_no_int_fallback( &format!("let `{}` ClosureFull return type", binding), &e, ))?, None => "nova_unit".to_string(), }; self.fn_param_sigs.insert(binding.clone(), (param_c_tys, ret_c)); } // If RHS is a call to a function that returns fn(...), propagate closure sig to binding if let ExprKind::Call { func, args, .. } = &decl.value.kind { // D38: turbofish прозрачен — смотрим под него. let func = func.unwrap_turbofish(); if let ExprKind::Ident(fname) = &func.kind { // [Plan 228 Ф.2(a) снос, реестр 221.1 №94-v2, было fix // M-nested-fn-newtype-bind-then-call-broken №78 форма 3]: // both the L1 (`fn_returns_fn_sig`) and L2 // (`fn_returns_fn_sig_l2`) forward-propagation legacy // arms (`ro c Mid = compose(..); ro h2 = c(h)`) are now // NO-HIT — the unified channel-fed registration above // (above) covers this call-RHS shape via the // checker's 196.4 return-type channel. Verified NO-HIT // on δ0-корпус (14 фикстур, isolated compiles, // `NOVA_TRACE_ICR`). Removed per NO-HIT-снос protocol — // see docs/plans/228-fnnt-channel-materialization.md. // If RHS is a call to a generic fn returning a tuple, infer element types from args if let Some(&arity) = self.generic_fn_tuple_arity.get(fname.as_str()) { let elem_tys: Vec<String> = args.iter().take(arity) .map(|a| self.infer_expr_c_type(a.expr())) .collect(); if elem_tys.len() == arity { self.tuple_element_types.insert(binding.clone(), elem_tys); } } } // [Plan 228 Ф.2(a) снос, реестр 221.1 №94-v2, было fix // M-nested-fn-newtype-bind-then-call-broken №90]: the // METHOD-call RHS mirror of the `Ident` arm above (`ro // wrapped = mw.apply(h)`, `func.kind` is `Member{obj, // name}` — a method whose OWN return is a fn-newtype) is // now NO-HIT — the unified channel-fed registration above // (above) covers method-call RHS the same way // it covers free-fn-call RHS (both go through the SAME // 196.4 return-type channel keyed by `decl.value.id`, not // by the RHS's AST shape). Verified NO-HIT on δ0-корпус // (14 фикстур, isolated compiles, `NOVA_TRACE_ICR`). // Removed per NO-HIT-снос protocol — see // docs/plans/228-fnnt-channel-materialization.md. } // [M-vec-of-fn-newtype-codegen] (реестр 221.1 №47): RHS is `v[i]` // indexing a `Vec[QH]` whose element type is a fn-newtype/closure // (`QH` = `type QH fn(int)->int`) — the C-level element type is // `void*` (the erased closure representation, D239/A7), so nothing // above registers `binding` as callable, and a later `got(21)` falls // through to the "must be a free function" default (mangles a // nonexistent `nova_fn_got` symbol — undefined-symbol link error). // `generic_type_instance_info` only carries the ALREADY-erased C-name // ("void*") for the element — indistinguishable from any OTHER // closure-valued element type — so recovering "this element is // specifically QH" needs the checker's STRUCTURAL channel // (`resolved_types[obj.id]`, D315), which still carries the real // `Named{Vec, [Named{QH}]}` (checker types concrete user code fully, // unlike a generic template body). Mirrors `resolve_fn_typeref`'s own // newtype-over-fn call-through resolution (D52-амендмент). if let ExprKind::Index { obj, .. } = &decl.value.kind { if let Some(crate::types::ResolvedType::Named { name: base_name, args: elem_args, .. }) = self.channel_arg_rt(obj) { if base_name == "Vec" { // Two element shapes both erase to `void*` and both need this // registration: a fn-newtype name (`QH`, call-through via // `fn_newtype_sigs`, D52-амендмент) and a BARE closure type // (`Vec[fn(int)->int]`, the RT already IS the `Func` shape — // no name/registry indirection needed). let sig: Option<(Vec<String>, String)> = match elem_args.first() { Some(crate::types::ResolvedType::Named { name: elem_name, .. }) => self.fn_newtype_sigs.get(elem_name.as_str()).cloned().map(|f| { if let TypeRef::Func { params: fp, return_type, .. } = f { let ptys: Vec<String> = fp.iter() .map(|t| self.type_ref_to_c(t).unwrap_or_else(|_| "nova_int".to_string())) .collect(); let rty = return_type.as_ref() .and_then(|t| self.type_ref_to_c(t).ok()) .unwrap_or_else(|| "nova_int".to_string()); (ptys, rty) } else { (Vec::new(), "nova_int".to_string()) } }), Some(crate::types::ResolvedType::Func { params, ret, .. }) => { let ptys: Vec<String> = params.iter() .map(|t| self.resolved_type_to_c(t).unwrap_or_else(|_| "nova_int".to_string())) .collect(); let rty = self.resolved_type_to_c(ret).unwrap_or_else(|_| "nova_int".to_string()); Some((ptys, rty)) } _ => None, }; if let Some(sig) = sig { self.fn_param_sigs.insert(binding.clone(), sig); } } } } } Stmt::Expr(e) => { // Plan 217 (гибрид C, §8а п.6а drop-флаг): bare-statement // consuming operation on an active auto-cleanup binding — // disarm BEFORE evaluating. A bare statement has no // downstream value to preserve across reordering (unlike // return/tail position), so disarming first is safe and // simpler than the arg-hoist dance those use. // (a) receiver form `X.method(...)` (e.g. `g.unlock()`) — // `X` sits inside `func` (Member.obj), NOT in `args`, // so the arg-scan below alone would miss it. // (b) any other consuming shape (`foo(X)`, nested calls, // `X` as a direct call-arg) — delegate to the // existing arg-scan/disarm machinery. self.disarm_auto_cleanup_receiver_call(e); // Plan 217 (same KNOWN GAP as the non-bare return case above): // the generic arg-scan disarm below is kept `reconsume_scopes` // -only (unchanged pre-217 behaviour) — auto-cleanup bindings // passed as a plain call-arg (`foo(g)`, not `g.method()`) are // NOT disarmed here without callee param-mode resolution. The // receiver-call case just above (`g.method()`) is the one // shape proven safe (gated on `consume_receiver_methods`). let active = self.reconsume_active_names(); let val = if active.is_empty() { self.emit_expr(e)? } else { self.emit_expr_with_reconsume_disarm(e, &active)? }; // Plan 55 Ф.3: nova_unit — struct{}; `_tmp;` invalid C для struct. // Cast в (void) даёт valid expression-statement и работает для всех типов. let ty = self.infer_expr_c_type(e); if ty == "nova_unit" || Self::is_struct_type(&ty) { self.line(&format!("(void)({});", val)); } else { self.line(&format!("{};", val)); } } Stmt::Assign { target, op, value, span } => { // Special case: array element assignment where elements are stored // as pointer-stomped nova_int (e.g. @buckets[idx] = Occupied{...}). // emit_expr(target) returns a cast lvalue which is illegal in C. // Instead emit: raw_arr->data[idx] = (nova_int)(intptr_t)val. if *op == AssignOp::Assign { if let ExprKind::Index { obj: arr_obj, index } = &target.kind { let arr_obj_ty = self.infer_expr_c_type(arr_obj); // Plan 145.1 — struct-VALUE element write (nova_str / value-record): // cl.exe (MSVC) отвергает `*(struct*)void_ptr = v` (C2440) и // `(struct)(const v)` struct-cast. Пишем через memcpy в слот-адрес: // nova_idx_chk/nochk возвращает void* (адрес элемента) — без // struct-assign-через-void-deref и без struct-cast. `val` // материализуется в temp (init принимает const, single-eval). // ГЕЙТ: только NovaArrHdr-layout коллекции (NovaArray_*/Nova_Vec____*), // НЕ raw `*mut T` буферы (`@data[i]=v`: nova_str* и т.п.) — иначе // nova_idx_chk кастит raw-указатель в NovaArrHdr* и читает мусорный len. let we_elem_ty = self.infer_expr_c_type(target); let we_hdr_collection = arr_obj_ty.starts_with("NovaArray_") || (arr_obj_ty.starts_with("Nova_Vec____") && !arr_obj_ty.trim_end().ends_with("**")); if we_hdr_collection && Self::is_struct_c_type(&we_elem_ty) && !we_elem_ty.ends_with('*') { let arr_c = self.emit_expr(arr_obj)?; let idx_c = self.emit_expr(index)?; let val_c = self.emit_expr(value)?; let helper = if self.index_site_elided(target.span.start) { "nova_idx_nochk" } else { "nova_idx_chk" }; self.line(&format!( "{{ {ty} _nv_set = ({val}); memcpy({helper}((void*)({arr}), ({idx}), sizeof({ty})), &_nv_set, sizeof({ty})); }}", ty = we_elem_ty, val = val_c, helper = helper, arr = arr_c, idx = idx_c )); return Ok(()); } // [M-fixed-array-value-semantics] (2026-07-10, D27-амендмент): // `arr[i] = v` on `[N]T` — INLINE struct `{ T data[N]; }` write. // Mirrors the Vec[T] write branch below, but `N` is a COMPILE-TIME // literal (no runtime len/cap header) and the data member is // direct (`.data`/`->data`), not a `->data` POINTER field. `arr_obj_ty` // is either the bare mono struct (a local/field VALUE) or a pointer to // it (a `ref`/`mut ref` D326 receiver/param — in-place mutation, // no copy). MUST precede the Vec/NovaArray checks below — a distinct // struct family, checked first so it never falls through to them. { let (bare, is_ptr) = match arr_obj_ty.strip_suffix('*') { Some(s) => (s, true), None => (arr_obj_ty.as_str(), false), }; if let Some((n, elem_ty)) = Self::parse_mono_fixed_array_name(bare) { let arr_c = self.emit_expr(arr_obj)?; let idx_c = self.emit_expr(index)?; let val_c = self.emit_expr(value)?; let data_expr = if is_ptr { format!("(({})->data)", arr_c) } else { format!("(({}).data)", arr_c) }; let wchk = if self.index_site_elided(target.span.start) { String::new() } else { format!( "if (_wi < 0 || _wi >= ((nova_int){n})) nv_panic_index_oob(_wi, ((nova_int){n})); ", n = n, ) }; self.line(&format!( "{{ nova_int _wi = ({idx}); {chk}({data})[_wi] = ({ty})({val}); }}", idx = idx_c, val = val_c, ty = elem_ty, chk = wchk, data = data_expr )); return Ok(()); } } // Plan 138 Ф.2 (D240): `v[i] = val` on Vec[T] — inline bounds-checked write. // Vec[T] layout: { T* data; nova_int len; nova_int cap }. // Emit: { _v->data[_i] = val; } with bounds check + panic. // Plan 138.1 Ф.3: guard against `Nova_Vec____<T>**` — // a `*mut Vec[T]` field (the buffer of a `Vec[Vec[T]]`) // is a RAW pointer, not a Vec value; it must fall through // to the raw-pointer-index write below. if arr_obj_ty.starts_with("Nova_Vec____") && !arr_obj_ty.trim_end().ends_with("**") { let arr_c = self.emit_expr(arr_obj)?; let idx_c = self.emit_expr(index)?; let val_c = self.emit_expr(value)?; // Plan 138.2 C3: recover the element C type robustly // from the instance registry (mirrors the read path at // `ExprKind::Index`). For a record element like // `Vec[Point]` the mangle is `Nova_Vec____Nova_Point_p` // (the `*` of `Point*` element encoded as `_p`). The // naive `trim_end_matches('*')` leaves `Nova_Point_p`, // a NON-EXISTENT typedef, producing an illegal cast // `(Nova_Point_p)(...)`. Use the registry (gives the // real `Nova_Point*`); fall back to de-sanitizing the // mangled suffix (`Nova_X_p` -> `Nova_X*`). let mangled = arr_obj_ty.trim_end_matches('*').trim().to_string(); let elem_ty = self.generic_type_instance_info.borrow() // A1‴: registry arg is `ResolvedType` — lower to C-name. .get(&mangled).and_then(|(_, a)| a.first().map(|rt| self.arg_c(rt))) .unwrap_or_else(|| Self::desanitize_c_from_ident( arr_obj_ty .strip_prefix("Nova_Vec____") .unwrap_or_else(|| panic!("[P67] nova_int collapse")) .trim() )); // Plan 140.2 B.4 / [M-140.2-elision-writeback]: элидировать // bounds-check записи `v[i]=val` на доказанных in-range // write-сайтах (frame-safe len-invariant цикл). Иначе always-on. let wchk = if self.index_site_elided(target.span.start) { String::new() } else { format!( "if (_wi < 0 || _wi >= ({arr})->len) nv_panic_index_oob(_wi, ({arr})->len); ", arr = arr_c, ) }; self.line(&format!( "{{ nova_int _wi = ({idx}); {chk}(({arr})->data)[_wi] = ({ty})({val}); }}", arr = arr_c, idx = idx_c, val = val_c, ty = elem_ty, chk = wchk )); return Ok(()); } // Plan 138.1 Ф.3 (D239): raw `*mut T` pointer-index write // — `@data[i] = v` inside Vec method bodies, where // `@data` is the typed `*mut T` buffer (`Nova_Point**`, // `nova_int*`, ...), NOT a NovaArray/Vec. Index the // pointer directly: `(ptr)[i] = v`. Must precede the // NovaArray boxed-element (`->data[i]`) path below, which // assumes a `->data` field that a raw pointer lacks. // Identified by: ends with `*`, and is not a NovaArray / // Vec / str / known struct collection type. if Self::is_raw_pointer_storage_c(&arr_obj_ty) { // Plan 174.5 Ф.3 (retraction, §3/§9, D216 amend): // `p[i] = v` index WRITE on a raw pointer — this // branch is the ASSIGNMENT-side counterpart of the // `ExprKind::Index` read-arm retraction (this // Stmt::Assign special-case bypasses that arm // entirely via its own direct emission, so it // needs its own retraction check). `Nova_X*` // single-pointer (record/sum VALUE, not excluded // by `is_raw_pointer_storage_c`) is out of scope // here too — no `.write_at()` method exists for a // bare record value receiver. if !(arr_obj_ty.starts_with("Nova_") && !arr_obj_ty.ends_with("**")) { self.strict_errors.borrow_mut().push( "error: [E_POINTER_OP_USE_METHOD] operator `p[i] = v` on \ raw pointer retired (Plan 174.5 §3/§9, D216 amend) — use \ `p.write_at(i, v)`" .to_string()); } let arr_c = self.emit_expr(arr_obj)?; let idx_c = self.emit_expr(index)?; let val = self.emit_expr(value)?; self.line(&format!("({})[{}] = ({});", arr_c, idx_c, val)); return Ok(()); } let elem_ty = self.infer_expr_c_type(target); if elem_ty.ends_with('*') && elem_ty != "nova_int*" { let arr_c = self.emit_expr(arr_obj)?; let idx_c = self.emit_expr(index)?; let val = self.emit_expr(value)?; self.line(&format!("{}->data[{}] = (nova_int)(intptr_t)({});", arr_c, idx_c, val)); return Ok(()); } } } // Plan 153.5 (D263 / Q-vec-operator-plus): operator-overloaded // compound assignment. `a += b` on a type whose `+` lowers to a // method (`@plus` / `@concat`) — `str`, `Vec[T]`, or any `Nova_*` // record with a registered `@plus` — must route through that // method, NOT a raw C `a += b` (illegal on a struct/pointer // operand → CC-FAIL). Desugar to `a = a + b` by synthesising a // `Binary` Add node and re-emitting through the full binop // dispatch (which already picks str-concat / Vec-plus / sum-plus). // `a += b` on a `Vec[T]` therefore yields a NEW Vec (concat // semantics) — to grow in place use `a.append(b)` (D263). // План 234 Ф.2а (D46 §C): `&=`/`|=`/`^=` — тот же route, // desugar `a = a <op> b`. `<<=`/`>>=` НЕ включены — `@shl`/ // `@shr` без flat-record dispatch (пред-сущ. гэп, см. отчёт). // №356: `*=`/`/=` были исключены из ветки целиком (для ЛЮБОГО // ABI) — тот же raw-C-CC-FAIL класс, что и `+=`/`-=` до №284, // просто по op, а не по типу. Включены сюда же. if matches!(op, AssignOp::Add | AssignOp::Sub | AssignOp::Mul | AssignOp::Div | AssignOp::BitAnd | AssignOp::BitOr | AssignOp::BitXor) { let tgt_ty = self.infer_expr_c_type(target); let is_overloaded_add_ty = tgt_ty == "nova_str" || (tgt_ty.starts_with("Nova_Vec____") && !tgt_ty.trim_end().ends_with("**")) || (tgt_ty.starts_with("Nova_") && tgt_ty.ends_with('*') && !tgt_ty.ends_with("**") && tgt_ty != "nova_int" && !tgt_ty.starts_with("Nova_Vec____")) // №284: a VALUE-record (`NovaValue_X`, by-value, no // pointer) with an operator method fell through to raw // C `+=` on a struct operand (CC-FAIL) — same route as // the heap-record arm above. || (tgt_ty.starts_with("NovaValue_") && !tgt_ty.ends_with('*')) // №356: named-tuple (`NovaTuple_X`, D215/Plan 120) // matched NEITHER `Nova_` (5th char `T` != `_`) NOR // `NovaValue_` — fell through, raw-C CC-FAIL. || (tgt_ty.starts_with("NovaTuple_") && !tgt_ty.ends_with('*')); if is_overloaded_add_ty { let bin_op = match op { AssignOp::Add => BinOp::Add, AssignOp::Sub => BinOp::Sub, AssignOp::Mul => BinOp::Mul, AssignOp::Div => BinOp::Div, AssignOp::BitAnd => BinOp::BitAnd, AssignOp::BitOr => BinOp::BitOr, AssignOp::BitXor => BinOp::BitXor, _ => unreachable!(), }; let synth = Expr::new( ExprKind::Binary { op: bin_op, left: Box::new(target.clone()), right: Box::new(value.clone()), }, *span, ); // `emit_expr` on the synthesised Binary dispatches the // operator to its method (e.g. `Nova_str_method_concat`, // `Nova_Vec____<elem>_method_plus`). A non-overloaded // operand-type pair surfaces here as a normal binop error // rather than silently emitting bad C. let rhs = self.emit_expr(&synth)?; let tgt = self.emit_expr(target)?; self.line(&format!("{} = {};", tgt, rhs)); return Ok(()); } } let tgt = self.emit_expr(target)?; // [M-153.2-flat-map-inner-option]: use target type to correctly // resolve type-directed literals on the RHS (e.g. `inner = None` // when `inner: Option[BoxIter[U]]` — plain emit_expr emits // `(NovaOpt_nova_int){None}` regardless of the LHS type). // // [M-exp-promotion-blockers: csv nested [][]str runtime bug] // extended to `Nova_Vec____<elem>*` targets: a bare `x = []` // RESET reassignment (`current_record = []` inside a parse // loop — encoding/csv.nv's `Csv.parse`) hit the exact same // gap `emit_expr_with_target_type` ALREADY closes for Option // (line above) but this call site never routed a Vec-typed // LHS through it. Plain `emit_expr([])` has no assignment- // target context, so the empty array literal silently // defaulted to `Vec[nova_int]` (`try_emit_typed_vec_literal`'s // own "empty literal, no hint → nova_int" fallback) — a // POINTER-TYPE-MISMATCHED value (`Nova_Vec____nova_int*`) // then got assigned into a `Nova_Vec____nova_str*`-typed // variable (C allows the incompatible-pointer assignment with // only a warning), so every subsequent `.push()`/read on that // variable used the WRONG element stride/layout — corrupting // `records`/nested nova[][]str state (never a `None`-style // hard type mismatch, so it silently produced wrong LENGTHS/ // VALUES rather than a compile error, matching the marker's // "differs from an expected type error" symptom). // `emit_expr_with_target_type` already has a dedicated // ArrayLit-vs-`Nova_Vec____` branch (sets `current_array_elem_ // hint` from the target's element type) — this just routes // Vec-typed assignment targets through it too. let lhs_ty = self.infer_expr_c_type(target); let val = if *op == AssignOp::Assign && ((lhs_ty.starts_with("NovaOpt_") && lhs_ty != "NovaOpt_nova_int") || (lhs_ty.starts_with("Nova_Vec____") && !lhs_ty.trim_end().ends_with("**"))) { self.emit_expr_with_target_type(value, &lhs_ty)? } else { self.emit_expr(value)? }; // Plan 33.8 Ф.6.1: знаковая `int` compound-арифметика // (`+=`/`-=`/`*=`) — checked, паника при переполнении (как и // обычные `+`/`-`/`*`, Ф.1.2). Через lvalue-указатель, чтобы // target вычислялся ровно один раз (важно для `arr[f()] += y`). // Plan 206 Ф.1b (D423): sized-типы ТОЖЕ checked (было — wrap); // см. `sized_checked_helper`/`nova_<T>_checked_*` (effects.h). // Plan 206.1 (D423.1): `/=` ТОЖЕ guarded теперь (было — сырой // C `/=`, тот же div-by-zero/MIN-overflow крэш-вектор как // голый `/`; НЕТ `%=` в языке — `AssignOp` не имеет варианта // Mod, только `Div`). let checked_helper = match op { AssignOp::Add => Some("nova_int_checked_add".to_string()), AssignOp::Sub => Some("nova_int_checked_sub".to_string()), AssignOp::Mul => Some("nova_int_checked_mul".to_string()), AssignOp::Div => Some("nova_int_checked_div".to_string()), _ => None, }; if let Some(helper) = checked_helper { let tgt_ty = self.infer_expr_c_type(target); let val_ty = self.infer_expr_c_type(value); if tgt_ty == "nova_int" && val_ty == "nova_int" { let p = self.fresh_tmp_named("ca"); self.line(&format!("nova_int* {} = &({});", p, tgt)); self.line(&format!("*{} = {}(*{}, {});", p, helper, p, val)); return Ok(()); } if tgt_ty == val_ty { if let Some(sized_helper) = Self::sized_checked_helper(&tgt_ty, match op { AssignOp::Add => BinOp::Add, AssignOp::Sub => BinOp::Sub, AssignOp::Mul => BinOp::Mul, AssignOp::Div => BinOp::Div, _ => unreachable!(), }) { let p = self.fresh_tmp_named("ca"); self.line(&format!("{}* {} = &({});", tgt_ty, p, tgt)); self.line(&format!("*{} = {}(*{}, {});", p, sized_helper, p, val)); return Ok(()); } } } // План 234 Ф.2а: raw C compound-assign — byte-identical к // `x = x <op> y` под C-promotion на ЛЮБОЙ ширине (в отличие // от `~`, таблица не нужна, D46 §C). let op_str = match op { AssignOp::Assign => "=", AssignOp::Add => "+=", AssignOp::Sub => "-=", AssignOp::Mul => "*=", AssignOp::Div => "/=", AssignOp::BitAnd => "&=", AssignOp::BitOr => "|=", AssignOp::BitXor => "^=", AssignOp::Shl => "<<=", AssignOp::Shr => ">>=", }; self.line(&format!("{} {} {};", tgt, op_str, val)); } Stmt::Return { value, .. } => { // Plan 33.1 Ф.4 (D24): если функция имеет ensures-контракты, // `return X` подменяется на `{ _nova_result = X; goto <label>; }` // чтобы ensures-checks работали для **всех** return-точек, // включая early-return в block-bodies. let post_label = self.contracts_post_label.clone(); // Plan 20 Ф.4: emit defer cleanup for ALL outer scopes before // returning (return is functional-level exit — walks ALL). // If no defers active, this is a no-op. if let Some(v) = value { let ret_ty = self.current_fn_return_ty.clone().unwrap_or_else(|| "nova_int".to_string()); // 172.4 Ф.3 блокер-1: fluent `-> @` value-record — return-позиция ptr. let fluent_self_ret = self.var_types.get("nova_self") .map(|sv| Self::is_value_struct_ptr(sv) && *sv == ret_ty) .unwrap_or(false); let prev_recv_ret = self.in_recv_ptr_return_position.replace(fluent_self_ret); // Plan 153.2: emit the return value with the function's return // type as the target so type-directed literals resolve to the // declared type instead of the erased default. The motivating // case is a bare `return None` inside a monomorphized closure // whose return type is `NovaOpt_<mono>` (e.g. a tuple option in // `take`/`skip`/`filter` over an `enumerate()` iterator): plain // `emit_expr` emitted `(NovaOpt_nova_int){None}`, a C type // mismatch against the closure's `NovaOpt__NovaTuple_…` result. // `emit_expr_with_target_type` only specializes None/typed-int/ // array-literal targets and otherwise delegates to `emit_expr`, // so non-literal returns are unchanged. // // Plan 201 (D188-амендмент v3): non-bare `return EXPR` that // references an ACTIVE re-consume guard (checker already // validated single-occurrence consume-position) — compute // `val` through `emit_expr_with_reconsume_disarm` so the // disarm-assignment lands exactly between "consuming call's // args evaluated" and "call invoked" (see helper doc above). // Plan 217 (KNOWN GAP, честно задокументировано — см. // 217-impl-notes.md): non-bare `return f(g)` where `g` is // an auto-cleanup binding passed as a call-arg is NOT // disarmed here — unlike `reconsume_scopes` (safe by // construction: the checker's block_guards reject any // non-sanctioned occurrence before codegen ever sees it), // a bare auto-cleanup binding has NO such restriction — // the checker only counts an arg as a transfer when the // CALLEE's parameter is actually `consume`-mode // (`consume_idxs`, types/mod.rs `consume_args`), and // codegen does not (yet) re-derive that per-callee mode // set. Blindly disarming on "any direct Ident arg" (as // `reconsume_scopes` does) would risk a SILENT LEAK when // the callee's param is actually `ro`/`mut` (checker // leaves `g` Live, but codegen would wrongly zero its // flag). Left strict for now: `return X` bare (below) and // the `Stmt::Expr` receiver-call case (gated on // `consume_receiver_methods`) are the two disarm paths // proven safe without this extra machinery. let reconsume_disarm_names: std::collections::HashSet<String> = if matches!(&v.kind, ExprKind::Ident(_)) { std::collections::HashSet::new() } else { let active = self.reconsume_active_names(); if active.is_empty() { std::collections::HashSet::new() } else { self.collect_reconsume_disarm_names(v, &active) } }; let val = if !reconsume_disarm_names.is_empty() { self.emit_expr_with_reconsume_disarm(v, &reconsume_disarm_names)? } else if ret_ty != "nova_int" && ret_ty != "nova_unit" { self.emit_expr_with_target_type(v, &ret_ty)? } else { self.emit_expr(v)? }; // Plan 184 (Р5/Р7) [M-184-mut-chain-return-position]: explicit // `return <value-record fluent chain>` — same by-value consumer // deref as the trailing-expr path (emit_block_stmts_trailing) and // the let/arg consumers. `is_fluent_value_ptr_for_target` is a // no-op when ret_ty is a pointer (the `-> @` = `ref Self` fluent- // self case handled by `in_recv_ptr_return_position` above), so // the two never conflict. let val = if self.is_fluent_value_ptr_for_target(v, &ret_ty) { format!("(*({}))", val) } else { val }; self.in_recv_ptr_return_position.set(prev_recv_ret); // Plan 72 P3-B return: box explicit return value for a protocol return type. let val = self.wrap_protocol_return(val, v); let val = self.wrap_any_return(val, v); // Plan 201 (D188-амендмент): `return X` ГОЛЫМ идентификатором // из-под re-consume блока `consume X { … }` — санкционированный // вынос владения: дизармим cleanup ИМЕННО этого блока (innermost // с совпадающим binding'ом) ПЕРЕД прогоном early-exit cleanup'ов. // Объемлющие consume-блоки НЕ трогаем — их ресурсы не выносятся. // Plan 217: same sanctioned-transfer disarm for a bare // `return X` where `X` is a bare auto-cleanup binding // (not a re-consume-block guard) — `disarm_var_for` // checks both registries. if let ExprKind::Ident(ret_name) = &v.kind { if let Some(var) = self.disarm_var_for(ret_name) { self.line(&format!( "{} = 0; /* Plan 201 / 217: return-вынос — cleanup дизармлен */", var)); } } // №465 (A8.29): bare `return X` consume-return site — // `val` is currently just `X`'s C text (a plain Ident // read, the wraps above are no-ops for a bare // zero_on_move-eligible ident). Hoist through a temp // (captures the value BEFORE zeroing) so the eventual // `return <val>;` below returns the intact copy while // `X`'s own storage is left zeroed — the copy-out // happens here, in THIS statement's temp-decl line, not // as part of the `return` statement itself (which // cannot sequence "copy, then zero, then return" on its // own). let val = if let ExprKind::Ident(ret_name) = &v.kind { let c_ty = self.var_types.get(ret_name).cloned().unwrap_or_default(); match self.zero_on_move_safe_source_name(&c_ty).map(|s| s.to_string()) { Some(zom_name) => self.zero_on_move_hoist_and_zero(ret_name, &zom_name, &val), None => val, } } else { val }; if let Some(label) = post_label { // Contracts mode: stash в _nova_result, defer cleanup, // потом goto. Если defers пустой — просто assign + goto. self.line(&format!("_nova_result = {};", val)); if !self.defer_scopes.is_empty() { self.emit_early_exit_cleanup(/*stop_at_loop=*/false); } self.line(&format!("goto {};", label)); } else if self.defer_scopes.is_empty() { self.line(&format!("return {};", val)); } else { // Stash result in a tmp so defer bodies can't see // it / mutate it. let tmp = self.fresh_tmp(); // Plan 59: mono'd tuple type mismatch — field-wise copy. let val_ty = self.infer_expr_c_type(v); self.emit_tuple_return_stash(&ret_ty, &tmp, &val, &val_ty); self.emit_early_exit_cleanup(/*stop_at_loop=*/false); self.line(&format!("return {};", tmp)); } } else { if let Some(label) = post_label { // Contracts mode unit-return. if !self.defer_scopes.is_empty() { self.emit_early_exit_cleanup(/*stop_at_loop=*/false); } self.line(&format!("goto {};", label)); } else { if !self.defer_scopes.is_empty() { self.emit_early_exit_cleanup(/*stop_at_loop=*/false); } self.line("return NOVA_UNIT;"); } } } Stmt::Break(_) => { // [M-217-break-continue-loop-boundary-bleed]: only walk when // the NEAREST enclosing loop actually registered a defer- // scope of its own (`loop_body_has_scope`'s top). A trivial // loop (no `defer`/auto-cleanup consume-let at ITS OWN top // level) registers none — `self.defer_scopes` at this point // holds only OUTER, still-open scopes that this `break` // must NOT touch (they haven't exited; see field doc). // `unwrap_or(true)` is a defensive fallback for the (should // never happen outside a loop) empty-stack case — preserves // the pre-fix behavior rather than silently under-cleaning. if !self.defer_scopes.is_empty() && self.loop_body_has_scope.last().copied().unwrap_or(true) { self.emit_early_exit_cleanup(/*stop_at_loop=*/true); } self.line("break;"); } Stmt::Continue(_) => { // [M-217-break-continue-loop-boundary-bleed]: see Stmt::Break above. if !self.defer_scopes.is_empty() && self.loop_body_has_scope.last().copied().unwrap_or(true) { self.emit_early_exit_cleanup(/*stop_at_loop=*/true); } self.line("continue;"); } Stmt::Throw { value, span } => { // `throw expr` — Fail.fail(expr). D25/D62/D65. // // Plan 61 Ф.2: typed dispatch. // - nova_str payload → legacy `Nova_Fail_fail(msg)` (string-slot). // - typed payload (record/sum) → `nova_throw_typed(msg_repr, // payload, NOVA_TID_<E>)` — fall through по precedence: // per-E typed slot (Ф.3) → erased Fail[any] slot → legacy // string slot → unwind с typed payload preserved в // fail-frame. let val_ty = self.infer_expr_c_type(value); let val = self.emit_expr(value)?; // Plan 173 Ф.5 п.7 (Zig-парность, минимум): стемп throw-site — // uncaught-abort ветки печатают `at file:line`. Error-path only. { let (file_lit, line) = self.loc_for_span(span.start); self.line(&format!("nova_throw_site_set(\"{}\", {});", file_lit, line)); } if val_ty == "nova_str" { self.line(&format!("Nova_Fail_fail({});", val)); } else { // Typed payload path. Plan 61 followup #4: prefer per-E // fast-path entry если concrete E type registered. Для // primitives (int/bool/etc) per-E не делается — erased // path через nova_throw_typed. let is_primitive = Self::primitive_type_id(&val_ty).is_some(); let payload_expr = if val_ty.ends_with('*') { format!("({})", val) } else { let tmp = self.fresh_tmp(); self.line(&format!( "{ty}* {tmp} = ({ty}*)nova_alloc(sizeof({ty}));", ty = val_ty, tmp = tmp )); self.line(&format!("*{} = {};", tmp, val)); format!("({})", tmp) }; if is_primitive { // Erased path — primitive use existing NOVA_TID_<type>. let tid_macro = self.debt_typeid_macro_for(&val_ty); self.line(&format!( "nova_throw_typed(nova_str_from_cstr(\"<{nm}>\"), (void*){p}, {tid});", nm = val_ty.replace('*', ""), p = payload_expr, tid = tid_macro )); } else { self.register_fail_e_type(&val_ty); let mangled_e = Self::debt_per_e_mangle(&val_ty); self.line(&format!( "_nova_throw_typed_{m}({p});", m = mangled_e, p = payload_expr )); } } } // D90 Plan 20 Ф.4: defer/errdefer codegen. Активация флага в // позиции defer/errdefer; cleanup инвоцируется в leave_defer_scope // (для normal-exit) и в setjmp-fail-handler (для throw-path). // enter_defer_scope уже декларировал `int _defer_BID_N_active = 0`. // Здесь — просто переключаем флаг и инкрементим next_idx. Stmt::Defer { .. } => { let scope = self.defer_scopes.last_mut() .expect("defer outside defer scope (enter_defer_scope missed?)"); let idx = scope.next_idx; let var = scope.entries[idx].active_var.clone(); scope.next_idx += 1; self.line(&format!("{} = 1;", var)); } // Plan 173 Ф.2.B3-merge (D314 §3): `consume X = e { body }` = sugar over // an outcome-defer. The consume-specific policy (capture + must-consume // exactly-once + partial-init + cancel-shield over body+cleanup + 3-level // timeout + ResourceTrace enter/exit) is re-homed onto a CONSUME-flavored // defer-entry — it is NOT lost by the desugar. `@cleanup` dispatch = the // pinned `Nova_<T>_consume_cleanup` symbol (R2). Structure: // { <capture>; <#define>; <timeout>; nv_consume_enter_shield → prev; // on_resource_enter; // enter_consume_defer_scope (fail-frame + interrupt-frame run-sites); // _defer_<id>_0_active = 1; (resource captured → cleanup armed) // <body, in its own nested defer-scope> (LIFO: body-defers < cleanup) // leave_defer_scope (normal-exit run-site → @cleanup(Success)); // #undef } // The four run-sites (FAIL/LEAVE/EARLY/INTERRUPT) each drive the cleanup // via `emit_consume_entry_cleanup` with the exit-path outcome, composing a // failing cleanup per §4a (cleanup-PANIC dominates). Routing through the // defer-kernel also aligns impl to spec: `@cleanup` now runs on `interrupt` // (D314 §2) and on early `return`/`break`/`continue` — the ex-monolith // skipped both (raw `return`/no interrupt-frame). Stmt::ConsumeScope { binding, type_annot: _, init, body, re_consume, result, .. } => { let init_c_type = self.infer_expr_c_type(init); let init_c_code = self.emit_expr(init)?; // Plan 217 BUGFIX (folder-CU regression, `d188_reconsume_ // block.nv` — D201Boom sentinel panics + D188-on-exit- // double-invocation): a re-consume block `consume X { // body }` takes over `X`'s cleanup ENTIRELY for its // duration (exactly-once, tail/return escape дизармит ITS // OWN scope) — but if `X` was ALSO a bare auto-cleanup- // eligible `consume X = e;` binding (registered in // `auto_cleanup_active` by `enter_defer_scope`), that OUTER // entry had NO idea this block exists and stayed armed. // At the outer scope's exit it fired `@cleanup` A SECOND // TIME (once via the block's own exactly-once dispatch, // once via the outer auto-cleanup) — exactly the D188-r2 // ccount guard's "on-exit-double-invocation" panic, or (for // a sentinel type whose cleanup itself panics) the wrong // panic firing on a path that's supposed to be disarmed. // Fix: entering ANY re-consume block on `X` is ITSELF a // disarm point for the OUTER auto-cleanup flag — ownership // management is handed to the block for its duration, // unconditionally (mirrors the checker's own `owned` // check + eventual `mark_consumed_bypass_guard`). if *re_consume { if let ExprKind::Ident(outer_name) = &init.kind { if let Some(var) = self.disarm_var_for(outer_name) { self.line(&format!( "{} = 0; /* Plan 217: re-consume блок берёт cleanup на себя — outer auto-cleanup дизармлен */", var)); } } } let scope_id = self.defer_block_counter; self.defer_block_counter += 1; let c_binding = format!("_consume_{}_{}", binding, scope_id); // Strip Nova_ prefix + pointer star for symbol/label resolution. let type_name = self.debt_strip_nova_trim_start(&init_c_type); // Plan 201: value-record (`consume value`, C-тип NovaValue_<T>) // — cleanup-символ (`Nova_<T>_consume_cleanup`), exit_timeout- // lookup и trace-label используют NOVA-имя типа `<T>` // (receiver_type_c_ident), не C-обёртку NovaValue_. let type_name = type_name .strip_prefix("NovaValue_") .map(|s| s.to_string()) .unwrap_or(type_name); // Plan 201 (D188-амендмент): блок = ВЫРАЖЕНИЕ. result-приёмник // (`ro s = consume X { …; X }`) декларируется ПЕРЕД блоком // (переживает его); tail-значение присваивается на выходе; // tail-вынос самого X дизармит cleanup. var_types регистрируем // persistently (не снимаем на #undef-е блока). // Тип result'а: tail-вынос → тип X; прочий tail — best-effort // инференс trailing-выражения (fallback nova_int — канон-путь // = tail-вынос, там тип точный). if let Some(r) = result { let tail_is_escape = matches!( &body.trailing, Some(t) if matches!(&t.kind, ExprKind::Ident(n) if n == binding)); let res_ty = if tail_is_escape { init_c_type.clone() } else if let Some(t) = &body.trailing { let tt = self.infer_expr_c_type(t); if tt.is_empty() { "nova_int".to_string() } else { tt } } else { "nova_int".to_string() }; self.line(&format!("{} {};", res_ty, r.name)); self.var_types.insert(r.name.clone(), res_ty); } self.line(&format!("/* Plan 173 Ф.2.B3-merge: consume {} = ... {{ ... }} */", binding)); self.line("{"); self.indent += 1; self.line(&format!("{} {} = {};", init_c_type, c_binding, init_c_code)); self.line(&format!("#define {} {}", binding, c_binding)); // Register binding's C type for downstream member/method dispatch // (var_types drives `r.field` → `r->field` on pointer types). self.var_types.insert(binding.clone(), init_c_type.clone()); // Plan 110.2.3 (D192): resolve exit_timeout via 3-level fallback — // L1 per-type WithExitTimeout (exit_timeout_ms method), // L2 Application handler (default_exit_timeout_ms), // L3 hardcoded (nv_resolve_exit_timeout_ms). #realtime → 0 (D198). let timeout_var = format!("_consume_timeout_{}", scope_id); let level1_key = (type_name.clone(), "exit_timeout_ms".to_string()); let has_level1 = self.method_overloads.contains_key(&level1_key); if self.in_realtime { self.line(&format!( "int {} = 0; /* Plan 110.2.4 (D198): #realtime bypass — no timeout */", timeout_var)); } else if has_level1 { self.line(&format!( "int {} = (int)Nova_{}_method_exit_timeout_ms({}); /* Plan 110.9.2 V1.1 Level 1 */", timeout_var, type_name, c_binding)); } else if self.effect_schemas.contains_key("Application") { self.line(&format!("int {};", timeout_var)); self.line("if (_nova_handler_Application) {"); self.indent += 1; self.line(&format!("{} = (int)Nova_Application_default_exit_timeout_ms();", timeout_var)); self.indent -= 1; self.line("} else {"); self.indent += 1; self.line(&format!("{} = nv_resolve_exit_timeout_ms();", timeout_var)); self.indent -= 1; self.line("}"); } else { self.line(&format!("int {} = nv_resolve_exit_timeout_ms();", timeout_var)); } // [M-cancel-loop-accept-swallowed-residual] (221.1 №15, D188 // R3 amendment, ОКНО-3 2026-07-23, владелец-решение (б)): // cancel-shield NARROWED to the cleanup phase only (D159's // letter — "cleanup completes-then-cancel-propagates" — the // BODY between capture and cleanup is NOT part of cleanup and // must stay cancellable at its own suspend/yield points). // `nv_consume_enter_shield` is no longer called here (that // held the mask up for the WHOLE body); it now fires // immediately before the cleanup dispatch itself, inside // `emit_consume_entry_cleanup` (the ONE place per policy // where `Nova_<T>_consume_cleanup` actually runs, for // whichever of the four run-sites triggers it) — which // already does the matching `nv_consume_leave_shield` right // after. Just pre-declare the C local here (bare, `=0`) so // all four run-sites can still reference the SAME name for // `nv_consume_leave_shield` after cleanup returns. let prev_deadline_var = self.fresh_tmp(); self.line(&format!("int64_t {} = 0;", prev_deadline_var)); // Plan 110.4.4.a (D185, R1) + Plan 173 Ф.5 п.2 (D185 amend): // ResourceTrace.on_resource_enter — observability only // (NULL-guarded). Label = type name; timeout ДРОПНУТ из enter // (§3a/п.8) — порог наблюдаем структурно через exit-событие // (duration_ms/overrun). let has_resource_trace = self.effect_schemas.contains_key("ResourceTrace"); if has_resource_trace { self.line(&format!( "if (_nova_handler_ResourceTrace) {{ Nova_ResourceTrace_on_resource_enter(nova_str_from_cstr(\"{}\")); }}", type_name)); } // Register the consume-scope as a defer-scope with ONE consume-entry. // Plan 173 Ф.5 (#8, D188 R2): runtime exactly-once counter, declared // alongside the `_active` flag by `enter_consume_defer_scope`. let count_var = format!("_consume_ccount_{}", scope_id); // Plan 201: value-record binding (`NovaValue_<T>` по значению) — // cleanup-метод принимает `NovaValue_<T>* nova_self` → передаём // адрес локала. Heap-record — уже указатель, как есть. let c_binding_arg = if init_c_type.starts_with("NovaValue_") && !init_c_type.ends_with('*') { format!("(&{})", c_binding) } else { c_binding.clone() }; let policy = ConsumePolicy { type_name, c_binding: c_binding_arg, prev_deadline_var, has_resource_trace, count_var, threshold_var: timeout_var.clone(), // План 253.4 Ф.1: ОБЕ прежние «регистрации» (ветка // `re_consume=true` в `reconsume_scopes` и ветка // `re_consume=false` в `auto_cleanup_active`, добавленная // фиксом №456) сведены в ДВА ПОЛЯ КОНСТРУКТОРА. Забыть // зарегистрировать форму больше нельзя: без этих полей // `ConsumePolicy` не собирается. nova_binding: Some(binding.clone()), re_consume: *re_consume, }; let (consume_block_id, consume_active_var) = self.enter_consume_defer_scope(policy, false); // Partial-init + exactly-once: arm the cleanup only now that the // resource is captured (init above cannot have thrown past here). self.line(&format!("{} = 1;", consume_active_var)); // 221.1 №456/№480 (`[M-consume-scope-cleanup-not-disarmable]`, // D415 §4 амендмент 2026-08-08) — ИСТОРИЯ МЕСТА: у форм с // `re_consume=false` (`spawn`/`detach consume c { … }`, их // мульти-var зеркало `parse_spawn_detach_consume_multivar` и // D188 binding-форма `consume c = e { … }`) тело ВЛАДЕЕТ // биндингом по-настоящему и вправе потребить его ЯВНО // (D415 §4: «move-out изнутри тела разрешён … move-out-запрет // [INV-PROPERTY] не вводился»). Флаг `_active` взводился, но НИ ОДНА // дизарм-точка не могла его найти — форма не попадала ни в // `reconsume_scopes`, ни в `auto_cleanup_active`. Итог: // `spawn consume r, w { … r.close(); w.close() }` потреблял // каждую половину ДВАЖДЫ (явный `close` + авто-`@cleanup` на // выходе тела) — нарушение exactly-once (D131/D133) и, на // разделённом TCP-потоке, use-after-free по `split_refcount` // с зависанием в `GC_gcollect()`. // // План 253.4 Ф.1: ветвления «куда зарегистрировать» здесь // БОЛЬШЕ НЕТ — регистрации нет вовсе. Обе формы описаны // одними и теми же полями `ConsumePolicy` выше, и обе // находятся одним и тем же обходом `defer_scopes`. Решения о // том, ЧТО считать потреблением, по-прежнему принимает канал // чекера (`consume_receiver_methods`, // `*_consume_param_positions`, `resolved_callees`). let _ = consume_block_id; // Body in its OWN nested defer-scope so body-defers run BEFORE the // consume-cleanup (LIFO). enter/leave are no-ops when body has none. // // Re-give form inside a spawn (`spawn consume x { … }`): `x` is a // spawn-capture for the INIT read above (`_c->x`), but body // references must hit the local `_consume_x_N` via the `#define` — // suspend the capture-rewrite for the body, restore after. let suspended_capture = self.current_spawn_captures.as_mut() .map(|s| s.remove(binding)).unwrap_or(false); let suspended_by_value = self.current_spawn_capture_by_value.as_mut() .map(|s| s.remove(binding)).unwrap_or(false); let body_defer_id = self.enter_defer_scope(body, false); for s in &body.stmts { self.emit_stmt(s)?; } if let Some(t) = &body.trailing { // Plan 201: tail-вынос `X` (голый binding, при result- // приёмнике) — САНКЦИОНИРОВАННЫЙ вынос владения: cleanup // дизармится, значение уезжает в result. Прочий tail — // обычное значение блока-выражения. let tail_escape = *re_consume && result.is_some() && matches!(&t.kind, ExprKind::Ident(n) if n == binding); // Plan 201 (D188-амендмент v3): non-bare tail EXPR — те же // disarm-at-consuming-call правила, что и у `return EXPR` // (см. block-comment у `emit_expr_with_reconsume_disarm` // выше `emit_stmt`). Гейт `result.is_some()` зеркалит // checker'а (ConsumeCtx `walk_guarded_escape_expr_multi` // вызывается ТОЛЬКО при наличии result-приёмника). let reconsume_active = self.reconsume_active_names(); let v = if tail_escape || result.is_none() || reconsume_active.is_empty() { self.emit_expr(t)? } else { let disarm_names = self.collect_reconsume_disarm_names(t, &reconsume_active); if disarm_names.is_empty() { self.emit_expr(t)? } else { self.emit_expr_with_reconsume_disarm(t, &disarm_names)? } }; if tail_escape { // План 253.4 Ф.1: имя флага берётся у скоупа, а не // пересобирается строкой по номеру блока. self.line(&format!( "{} = 0; /* Plan 201: tail-вынос — cleanup дизармлен */", consume_active_var)); } match result { Some(r) => self.line(&format!("{} = {};", r.name, v)), None => self.line(&format!("(void)({});", v)), } } // План 253.4 Ф.1: снимать запись отдельным `pop` больше не // нужно — она уходит вместе со своим скоупом в // `leave_defer_scope(consume_block_id)` ниже. self.leave_defer_scope(body_defer_id); if suspended_capture { if let Some(s) = self.current_spawn_captures.as_mut() { s.insert(binding.clone()); } } if suspended_by_value { if let Some(s) = self.current_spawn_capture_by_value.as_mut() { s.insert(binding.clone()); } } // Normal-exit run-site: @cleanup(Success) + policy; a failing cleanup // re-throws to the caller (leave-compose longjmp). self.leave_defer_scope(consume_block_id); self.line(&format!("#undef {}", binding)); self.var_types.remove(binding); self.indent -= 1; self.line("}"); } // Plan 33.2 Ф.8 (D24): `assert_static <expr>` — intermediate // proof obligation. Сейчас (без full SMT body-encoding) // эмитим как runtime check. Plan 140 Ф.1 (D24 amend): проверка // остаётся и в release (enforce-with-elision) — НЕ под // `#ifdef NOVA_CONTRACTS_RUNTIME`. Stmt::AssertStatic { expr, span } => { // Plan 33.3 Ф.9.1: skip runtime check если expr читает // ghost-var (ghost эрейзится в codegen; SMT-verify в Z3 // будет работать). assert_static с ghost — pure spec-level. // Plan 194 A4: per-fn/module `#unchecked` opt-out retired — // `contracts_elided_here()` константно `false`. if Self::expr_uses_ghost(expr, &self.ghost_vars) || self.contracts_elided_here() { // No-op в codegen — assert_static с ghost / под opt-out. } else { let v = self.emit_expr(expr)?; let src = Self::expr_to_display(expr); // Plan 140.1 Ф.2 (D24 amend): location-first format; no msg. let (file_lit, line) = self.loc_for_span(span.start); self.line(&format!( "if (!({})) nova_contract_violation(NOVA_CONTRACT_PRE, \"<assert_static>\", \"{}\", \"{}\", {}, NULL);", v, Self::escape_c_str(&src), file_lit, line )); } } // Plan 33.3 (D24): `assume <expr>` — runtime check // (программист подтверждает что expr истинен; если нет — // это bug в коде, не bug в верификации). Plan 140 Ф.1 // (D24 amend): проверка остаётся и в release — НЕ под // `#ifdef NOVA_CONTRACTS_RUNTIME`. Stmt::Assume { expr, span } => { // Plan 33.3 Ф.9.1: skip если expr читает ghost-var. // Plan 194 A4: per-fn/module `#unchecked` opt-out retired — // `contracts_elided_here()` константно `false`. if Self::expr_uses_ghost(expr, &self.ghost_vars) || self.contracts_elided_here() { // No-op в codegen — assume с ghost / под opt-out. } else { let v = self.emit_expr(expr)?; let src = Self::expr_to_display(expr); // Plan 140.1 Ф.2 (D24 amend): location-first format; no msg. let (file_lit, line) = self.loc_for_span(span.start); self.line(&format!( "if (!({})) nova_contract_violation(NOVA_CONTRACT_PRE, \"<assume>\", \"{}\", \"{}\", {}, NULL);", v, Self::escape_c_str(&src), file_lit, line )); } } // Plan 33.5 Ф.4.1: `apply lemma_name(args)` — ghost statement, // полностью стирается в codegen. SMT-семантика обрабатывается // в verify/pipeline.rs (assert lemma.ensures[args/params]). Stmt::Apply { .. } => { // Ghost erasure — никакого C-кода не эмитируем. } // Plan 33.5 Ф.4.2: `calc { ... }` — ghost statement, полностью // стирается в codegen. SMT-семантика в verify/pipeline.rs. Stmt::Calc { .. } => { // Ghost erasure. } // Plan 33.9 Ф.2: `reveal name` — ghost statement, полностью // стирается в codegen. V1 — нет SMT-эффекта, V2 будет emit // axiom `forall args. name(args) == body` в SMT scope. Stmt::Reveal { .. } => { // Ghost erasure. } // Plan 136 Ф.3 / M-136-cycle-decomp V2: tuple destructuring assignment codegen. // PART A: detect if rhs is a pure permutation of lhs lvalues. // If yes → cycle-decomposition (1 tmp per cycle, optimal). // If no → conservative-tmp fallback (V1, unchanged). Stmt::TupleAssign { lhs, rhs, .. } => { self.line("{"); self.indent += 1; let n = lhs.len(); // PART A: build perm_opt[i] = Some(j) if rhs[i] structurally equals lhs[j]. let mut perm_opt: Vec<Option<usize>> = Vec::with_capacity(n); for i in 0..n { let mut found: Option<usize> = None; for j in 0..n { if Self::exprs_lvalue_eq_ta(&rhs[i], &lhs[j]) { found = Some(j); break; } } perm_opt.push(found); } // Check all rhs entries map to some lhs entry (all Some). let is_pure_perm = perm_opt.iter().all(|x| x.is_some()) && { // Check bijection: no duplicate j values. let mut seen = vec![false; n]; let mut ok = true; for opt in &perm_opt { let j = opt.unwrap(); if seen[j] { ok = false; break; } seen[j] = true; } ok }; if is_pure_perm { // PART B: cycle-decomposition — 1 tmp per non-trivial cycle. // perm[i] = j means: lhs[i] gets the value currently in lhs[j] // (since rhs[i] == lhs[j]). // Build the permutation array. let perm: Vec<usize> = perm_opt.iter().map(|x| x.unwrap()).collect(); let mut visited = vec![false; n]; for start in 0..n { if visited[start] { continue; } // Collect cycle starting at `start`. let mut cycle: Vec<usize> = vec![start]; let mut cur = perm[start]; while cur != start { cycle.push(cur); cur = perm[cur]; } // Mark all in cycle as visited. for &idx in &cycle { visited[idx] = true; } // Fixed point: no-op. if cycle.len() == 1 { continue; } // Non-trivial cycle: save lhs[cycle[0]] into tmp, // then rotate: lhs[cycle[i]] = lhs[cycle[i+1]] for i in 0..len-1, // finally lhs[cycle[last]] = tmp. let c0 = cycle[0]; let tmp_name = self.fresh_tmp_named("cd"); let lhs_c0_clone = lhs[c0].clone(); let c_ty = self.infer_expr_c_type(&lhs_c0_clone); let val0 = self.emit_expr(&lhs_c0_clone)?; self.line(&format!("{} {} = {};", c_ty, tmp_name, val0)); for i in 0..cycle.len() - 1 { let dst = lhs[cycle[i]].clone(); let src = lhs[cycle[i + 1]].clone(); let dst_s = self.emit_expr(&dst)?; let src_s = self.emit_expr(&src)?; self.line(&format!("{} = {};", dst_s, src_s)); } let last_dst = lhs[*cycle.last().unwrap()].clone(); let last_dst_s = self.emit_expr(&last_dst)?; self.line(&format!("{} = {};", last_dst_s, tmp_name)); } } else { // PART C: V1 conservative fallback (verbatim). // Step 1: collect root ident names from all lhs targets. let lhs_names: std::collections::HashSet<String> = lhs.iter() .flat_map(|e| Self::lvalue_root_names_ta(e)) .collect(); // Step 2: for each rhs[i] that reads a lhs name, pre-compute // into a typed tmp variable. let mut tmps: Vec<Option<String>> = Vec::with_capacity(n); for i in 0..n { let r = rhs[i].clone(); if Self::expr_reads_lhs_ta(&r, &lhs_names) { let tmp_name = self.fresh_tmp_named("ta"); let c_ty = self.infer_expr_c_type(&r); let val = self.emit_expr(&r)?; self.line(&format!("{} {} = {};", c_ty, tmp_name, val)); tmps.push(Some(tmp_name)); } else { tmps.push(None); } } // Step 3: emit assignments lhs[i] = tmp_or_rhs[i]. for i in 0..n { let val = match &tmps[i] { Some(t) => t.clone(), None => { let r = rhs[i].clone(); self.emit_expr(&r)? } }; let l = lhs[i].clone(); let target = self.emit_expr(&l)?; self.line(&format!("{} = {};", target, val)); } } self.indent -= 1; self.line("}"); } } Ok(()) } // ---- expressions ---- /// Emit `expr` zная c-тип цели. Если target — typed-integer /// (uint8/16/32/64, int8/16/32), литералы (IntLit/CharLit) и операнды /// integer-арифметических Binary/Unary получают «нативный» суффикс/cast: /// `((uint32_t)NU)` вместо стандартного `((nova_int)NLL)`. /// /// Для не-typed-integer target или non-арифметического выражения — /// fallback в обычный `emit_expr`. Это **обёртка**, не замена. /// /// Используется в let-binding с известным `ty_c`, в `emit_block_into` /// (trailing — известен ty блока), и `emit_if_expr` для `else if` /// ветки (известен if_ty). fn emit_expr_with_target_type(&mut self, expr: &Expr, target_ty_c: &str) -> Result<String, String> { // Plan 125 followup [M-125-codegen-never-cast]: divergent expressions // (throw / panic / exit / interrupt / user fn -> never / method->never // / recursive composition) emit comma-expr `(side_effect, dummy)` where // legacy dummy = `(nova_int)0LL`. When target_ty_c ≠ nova_int, this // dummy implicit-cast fails в C (e.g. `nova_str result = (call, 0LL);`). // // Solution: post-process emit_expr result, substitute trailing // `(nova_int)0LL` dummy with target-typed zero. Skip if target is // already nova_int (legacy path unchanged) или если expr не diverge'ит. // // [M-coalesce-panic-unit-result-cc-fail] (реестр 221.1 №118): `nova_unit` // used to be excluded from this substitution ALONGSIDE `nova_int` — but // unlike `nova_int`, `nova_unit` is a STRUCT typedef (`NOVA_UNIT` is a // compound-literal macro, not `0`), so the untouched legacy dummy // `(nova_int)0LL` left in place for a `nova_unit` target is NOT a no-op // widen (as it genuinely is for the `nova_int` case this skip is // actually for) — it is a real C type mismatch. Concretely: // `Result[(), E] ?? panic("...")` builds the ternary // `(tag==Ok ? payload.Ok._0 /* nova_unit */ : (panic(...), (nova_int)0LL))` // — CC-FAIL "incompatible operand types ('nova_unit' and 'nova_int')". // `typed_zero_value_125`/`emit_divergent_with_target_125` ALREADY handle // `nova_unit` correctly (return the `NOVA_UNIT` macro) — this exclusion // just never routed a `nova_unit` target into that existing, correct // machinery. Dropping the exclusion is additive: for a NON-divergent // expr (the overwhelming majority of `nova_unit`-target call sites — // ordinary unit-valued statements/returns) `expr_diverges_125` is // `false` and this whole branch stays a no-op, byte-identical to before. // [M-402-match-all-diverge-int-target] (реестр 221.1 №402): the bare // `target_ty_c != "nova_int"` skip above is correct for a SIMPLE divergent // expr (`throw e`, `panic(...)`) — its own comma-expr dummy IS ALREADY // `(nova_int)0LL`, so an `nova_int` target needs no substitution. It is // WRONG for `ExprKind::Match` whose arms ALL diverge (`match err_opt { // Some(e) => throw e, None => throw …!! }` — `std/src/concurrency/ // retry.nv`'s `result ?? match last_error {…}`, found CI nightly via // `retry_test.nv`): `emit_match`'s OWN result-type derivation (first/ // second legacy pass) explicitly SKIPS every diverging arm while hunting // for a value type — with ZERO non-diverging arms it falls through to its // bail-safe default `nova_unit`, irrespective of the caller's target. Left // unrouted here, `emit_expr` returns that `nova_unit`-typed match temp // DIRECTLY as the value — CC-FAIL "incompatible operand types ('nova_int' // and 'nova_unit')" at the `??` ternary. Only the T=int/E=int monomorphization // hit this: T=str/E=str already routed correctly (target ≠ "nova_int" there). // Same fix SHAPE as №118 just above (`nova_unit`-target exclusion-drop): // narrow the `nova_int` skip so it still covers the bare-divergent-dummy // case (unaffected: non-Match, or a Match with at least one non-diverging // arm) but NOT a fully-divergent Match, which now routes into the SAME // existing `emit_divergent_with_target_125` machinery the №118 fix already // proved correct for `nova_unit` (comma-discards the mistyped match temp, // substitutes a target-typed zero as the visible value) — no new // special-casing, just closing the one shape this dispatcher didn't cover. let diverges = self.expr_diverges_125(expr); let bare_int_dummy_already_correct = target_ty_c == "nova_int" && !matches!(&expr.kind, ExprKind::Match { .. }); if diverges && !bare_int_dummy_already_correct { return self.emit_divergent_with_target_125(expr, target_ty_c); } // Реестр 221.1 №546 (K1): target-typed coercion INTO a protocol's // `NovaBox_*` fat pointer. `wrap_protocol_return` (return position) // and the call-argument pre-box hook (`emit_call`, [M-protocol-param- // free-fn-only]) already box a concrete value into its existential — // but BOTH derive `(proto, type_args)` from a DECLARED `TypeRef` (fn // return / param signature), which a branch-merge target (if/else // `emit_block_into`, match `emit_match_arm_body` — both route their // per-branch value through THIS fn with an INFERRED C type string as // `target_ty_c`) does not have. Without this hook, a protocol-typed // if/else or match — TWO implementers on different branches — left // the concrete implementer pointer un-boxed; `emit_assign_typed` // then had to force it into the `NovaBox_*` slot with a bare C cast // `(NovaBox_X)(ptr)`, which C rejects for a struct target ("used // type 'NovaBox_X' where arithmetic or pointer type is required"). // Single choke point: EVERY target-typed sink that calls this fn // gets the box for free, no per-site duplication. Non-generic // protocols only (`protocol_method_registry` is keyed by bare proto // name) — a generic protocol's mangled `NovaBox_<Proto>_<args>` // suffix isn't reversible from the bare string; that gap already // pre-dates this fix (same limitation on the call-arg/array-literal // box hooks) and is not widened by it. if let Some(proto_name) = target_ty_c.strip_prefix("NovaBox_") { if self.protocol_method_registry.contains_key(proto_name) { let concrete_c = self.infer_expr_c_type(expr); if !concrete_c.starts_with("NovaBox_") { let v = self.emit_expr(expr)?; return Ok(self.box_value_for_protocol(v, &concrete_c, proto_name, &[])); } } } // [M-d55-str-literal-coercion-name-gated] fix (2026-07-17): D55 amend // (spec/decisions/02-types.md §Str-литерал→[]u8) — a bare str-LITERAL // (never a variable/`InterpolatedStr` — D176 still requires an explicit // `.bytes()` for those) at a position whose RESOLVED expected C type is // `[]u8` coerces to a `.bytes()`-view, zero-copy (str is already UTF-8 // bytes). This function IS the checker-independent "resolved expected // C type" choke point every OTHER target-typed literal-coercion rule in // this function keys on (let/const `ty_c`, return `ret_ty`, assign // `lhs_ty`, array-literal-element `elem_c`, tuple/record-field, if/match // tail) — replaces the retired `synthesize_write_str_lit_bytes_coercion` // name-gate (method literally spelled `write`, receiver required a // REGISTERED `write` method) for these positions; the call-arg position // is covered by the sibling `synthesize_bytes_lit_call_args` pre-pass in // `emit_call` (this function is never reached for a bare, un-target-typed // call argument). Rewrites AST and re-enters `emit_expr` so the EXISTING, // already-correct `.bytes()` codegen path builds the view — zero new // C-formatting surface. if let ExprKind::StrLit(_) = &expr.kind { if Self::is_bytes_slice_c_ty(target_ty_c) { let bytes_call = Self::wrap_str_lit_as_bytes_call(expr); return self.emit_expr(&bytes_call); } } // Plan 48: `None` initializer should match target NovaOpt_X type when target is known. // Otherwise None falls back to NovaOpt_nova_int (per current_fn_return_ty), which // breaks `let mut result NovaOpt_nova_str = None` in mono'd generic bodies. if target_ty_c.starts_with("NovaOpt_") { if let ExprKind::Ident(name) = &expr.kind { if name == "None" { // Plan 118 Ф.5: NPO-aware None constructor. let sani = target_ty_c.strip_prefix("NovaOpt_").unwrap_or(target_ty_c); return Ok(self.option_none_expr(sani)); } } // Plan 172.1 [M-172.1-some-target-coerce] (2026-06-30): `Some(arg)` in a // `NovaOpt_<X>` target — emit the payload WITH target X so a context-typed // numeric literal coerces to X (D55) instead of defaulting to `nova_int`. // Without this, `Some(<int-literal>) -> Option[uint]` builds `NovaOpt_nova_int` // ≠ the declared `NovaOpt_nova_uint` (named-priority int-collapse → CC-FAIL, // surfaced by d86_coalesce_width). Parallel to the `None` arm above; reached // ONLY in genuinely target-typed positions (return / annotated let / `??` // fallback target), NOT bare sub-exprs (those keep the deliberate arg-type- // priority of the `Some` emit, :22982). Gated on a TYPED-INTEGER inner (the // int-collapse surface) so pointer/struct payloads keep the existing // sanitize/register flow untouched. if let ExprKind::Call { func, args, .. } = &expr.kind { if let ExprKind::Ident(cn) = &func.kind { if cn == "Some" && args.len() == 1 { let inner = target_ty_c .strip_prefix("NovaOpt_") .unwrap_or(target_ty_c); if Self::is_typed_integer(inner) { let arg_v = self .emit_expr_with_target_type(args[0].expr(), inner)?; self.register_novaopt_decl(inner, inner); return Ok(self.option_some_expr(inner, &arg_v)); } } } } } // Bare unit-variant `Empty` whose name collides across sum-types // (Node.Empty vs Slot.Empty in the same folder-module): the default // `find_variant_compat` is first-wins and may pick the WRONG sum, // emitting `nova_make_Slot_Empty()` for a `Nova_Node*` target. When the // target type is a concrete `Nova_<Sum>*` and that sum actually has this // unit variant, build the constructor for THAT sum. This is the // disambiguation the explicit `ro e Node = Empty` annotation requests. if let ExprKind::Ident(name) = &expr.kind { let base = target_ty_c.trim_end_matches('*').trim(); if let Some(sum) = Self::debt_strip_nova_prefix_opt(base) { if let Some(entry) = self.sum_schema_registry.lookup_sum_schema(sum) { if let Some(v) = entry.variants.iter().find(|v| v.variant_name == *name) { if v.field_c_types.is_empty() { // [M-generic-method-self-recursive-return] (Plan 186, // recursive-mono): a plain (non-generic) sum's variant // ctor is named WITHOUT the `Nova_` prefix on the bare // lookup key (`nova_make_LinkedList_Empty` — matches // `emit_sum_type`'s `nova_make_{name}_{var}`), but a // MONO'D GENERIC instance's ctor keeps the FULL mangled // `Nova_<mono>` name (`nova_make_Nova_LinkedList____ // nova_int_Empty` — matches `emit_generic_type_instance`'s // `nova_make_{mangled}_{var}`, `mangled` always carrying // the `Nova_` prefix). `sum` (the bare lookup key, used // below) is only correct for the FIRST case; a mono // instance (detected by the `____` mangling delimiter, // same signal used throughout — `debt_is_mono_nova_name` // et al.) must use the registry's stored `c_name` // instead — this is exactly the bare `Empty` inside a // generic method's OWN body (`LinkedList[T].new() => // Empty`) that used to link against a symbol nothing // emitted. let ctor_base: &str = if sum.contains("____") { &entry.c_name } else { sum }; return Ok(format!("nova_make_{}_{}()", ctor_base, name)); } } } } } // When target is NovaArray_X* or (Plan 138.1 Ф.2) the flipped // Vec[X] (`Nova_Vec____<X>*`) and expr is an array literal, set hint so // emit_array_lit / try_emit_typed_vec_literal uses X as the element // type instead of defaulting to nova_int. Handles empty `[]` in typed // contexts: `let xs []str = []` or `{ items: [] }`. // [M-fixed-array-value-semantics] (2026-07-10, D27-амендмент): target is the // `[N]T` INLINE mono struct (`_NovaFixArr_<N>_<L>_<T>`) — build a compound // literal `(StructName){ .data = { e0, e1, ..., eN-1 } }` directly, NOT the // Vec/NovaArray push-loop path below (that's for `[]T`, a DIFFERENT type // since this change). Each element coerces to the element C type (recurses // through `emit_expr_with_target_type` so a nested `[N][M]T` row literal // targets the INNER `_NovaFixArr_<M>_..._<T>` struct too — row-major, D27). if let ExprKind::ArrayLit(elems) = &expr.kind { if let Some((n, elem_c)) = Self::parse_mono_fixed_array_name(target_ty_c) { let direct_items: Vec<&Expr> = elems.iter().filter_map(|e| match e { ArrayElem::Item(x) => Some(x), ArrayElem::Spread(_) => None, }).collect(); if direct_items.len() != elems.len() { return Err(format!( "[M-fixed-array-value-semantics] `[N]T` array literal против `{}` \ не поддерживает `...spread` элементы (N фиксирован компайл-тайм)", target_ty_c)); } if direct_items.len() != n { return Err(format!( "[M-fixed-array-value-semantics] array literal с {} элементами \ не соответствует размеру `[{}]T` таргета `{}`", direct_items.len(), n, target_ty_c)); } let mut vals: Vec<String> = Vec::with_capacity(n); for item in &direct_items { vals.push(self.emit_expr_with_target_type(item, &elem_c)?); } return Ok(format!("({}){{ .data = {{ {} }} }}", target_ty_c, vals.join(", "))); } // Recover the element C type of a flipped `[]X` ≡ `Vec[X]` target. let vec_elem = self.vec_or_array_elem_c(target_ty_c) .filter(|_| target_ty_c.starts_with("Nova_Vec____")); if let Some(hint_elem) = vec_elem { let prev = self.current_array_elem_hint.replace(hint_elem); let result = self.emit_expr(expr); self.current_array_elem_hint = prev; return result; } if let Some(inner) = Self::debt_strip_novaarray_prefix_opt(target_ty_c) { let hint_elem = inner.trim_end_matches('*').to_string(); let prev = self.current_array_elem_hint.replace(hint_elem); let result = self.emit_expr(expr); self.current_array_elem_hint = prev; return result; } } // Plan 172.1 [literal-coercion channel]: a TUPLE literal against a mono-tuple // target coerces each element to the target's element type (decoded from the // mangled `_NovaTuple_<arity>_<L>_<T>…` name) so `?? (0,0)` against `(uint,uint)` // builds `_NovaTuple_2_…uint…uint` to MATCH the Ok/Some side, not the collapsed // `(int,int)` (CC-FAIL incompatible-operand clash). Mirrors the mono-tuple // construction in `emit_expr`'s TupleLit arm (:21550) but drives element types // from the TARGET (decoded), recursing each element through this coercion. if let ExprKind::TupleLit(elems) = &expr.kind { if let Some(elem_tys) = Self::parse_mono_tuple_elements(target_ty_c) { if elem_tys.len() == elems.len() { let mangled = self.register_mono_tuple(&elem_tys); let tmp = self.fresh_tmp(); self.line(&format!("{} {};", mangled, tmp)); for (i, (e, ety)) in elems.iter().zip(elem_tys.iter()).enumerate() { let v = self.emit_expr_with_target_type(e, ety)?; self.line(&format!("{}.f{} = {};", tmp, i, v)); } self.var_types.insert(tmp.clone(), mangled.clone()); self.tuple_element_types.insert(tmp.clone(), elem_tys); return Ok(tmp); } } } // Только typed-integer target пропагируем — для nova_int/struct/etc. // обычный emit_expr уже корректен. if !Self::is_typed_integer(target_ty_c) { return self.emit_expr(expr); } match &expr.kind { ExprKind::IntLit(n) => Ok(Self::emit_typed_int_literal(*n, target_ty_c)), ExprKind::CharLit(cp) => Ok(Self::emit_typed_int_literal(*cp as i64, target_ty_c)), ExprKind::Unary { op: UnOp::Neg, operand } => { if let ExprKind::IntLit(n) = &operand.kind { return Ok(Self::emit_typed_int_literal(-*n, target_ty_c)); } // Иначе рекурсивно с тем же target. let inner = self.emit_expr_with_target_type(operand, target_ty_c)?; // Plan 206.1 (D423.1): sized-SIGNED target — same neg-guard as // the main `emit_expr` Unary arm (`x == T.MIN` traps). `target_ty_c` // is guaranteed a sized type here (the `is_typed_integer` gate // above excludes `nova_int`); unsigned targets fall through to // the raw `-` unchanged (`sized_checked_neg_helper` → `None`). if let Some(helper) = Self::sized_checked_neg_helper(target_ty_c) { if !self.overflow_site_elided(expr.span.start) { return Ok(format!("{}({})", helper, inner)); } } Ok(format!("(-{})", inner)) } ExprKind::Binary { op, left, right } => { // Пропагируем target только для integer-арифметики/побитовых/сдвигов. // Сравнения (Eq/Neq/Lt/...) и logic (And/Or) — bool result; их operand'ы // могут быть разных типов и не должны получать typed-cast. let is_integer_arith = matches!(op, BinOp::Add | BinOp::Sub | BinOp::Mul | BinOp::Div | BinOp::Mod | BinOp::BitAnd | BinOp::BitOr | BinOp::BitXor | BinOp::Shl | BinOp::Shr ); if !is_integer_arith { return self.emit_expr(expr); } // Если operand'ы — non-integer типы (например str + str), fallback. // Проверяем по infer_expr_c_type: если any side — не nova_int/typed-int/void*, // обычный emit_expr корректнее обработает special-cases. let lty = self.infer_expr_c_type(left); let rty = self.infer_expr_c_type(right); let lhs_ok = lty == "nova_int" || Self::is_typed_integer(<y) || lty == "void*"; let rhs_ok = rty == "nova_int" || Self::is_typed_integer(&rty) || rty == "void*"; if !lhs_ok || !rhs_ok { return self.emit_expr(expr); } let l = self.emit_expr_with_target_type(left, target_ty_c)?; let r = self.emit_expr_with_target_type(right, target_ty_c)?; // Plan 33.8 Ф.1.2 / Plan 206 Ф.1b (D423): checked-форма. // `target_ty_c` — гарантированно sized `Ints`-тип (guard в // начале функции исключает `nova_int` — тот идёт через // отдельную ветку в главном `emit_expr`-Binary-арме ниже, // ~L29388); `l`/`r` уже приведены к `target_ty_c` рекурсией // выше, так что дispatch по НЕМУ (не по исходным `lty`/`rty` // до приведения) корректен для обоих операндов. if let Some(helper) = Self::sized_checked_helper(target_ty_c, *op) { // Plan 140.4: элидировать checked-форму на доказанном сайте. if !self.overflow_site_elided(expr.span.start) { return Ok(format!("{}({}, {})", helper, l, r)); } } let op_str = match op { BinOp::Add => "+", BinOp::Sub => "-", BinOp::Mul => "*", BinOp::Div => "/", BinOp::Mod => "%", BinOp::BitAnd => "&", BinOp::BitOr => "|", BinOp::BitXor => "^", BinOp::Shl => "<<", BinOp::Shr => ">>", _ => unreachable!(), }; Ok(format!("({} {} {})", l, op_str, r)) } _ => self.emit_expr(expr), } } /// Plan 48 Ф.4 ([M-spawn-closure-capture-mono]): if `name` is captured by /// the current spawn-body, return the C expression to read it from the /// spawn ctx (`_c->name` or `(*_c->name)`). Returns `None` otherwise so /// the caller can use the bare identifier. Centralizes the rewrite so /// both Ident reads and indirect uses (closure-call callee, address-of /// for spawn nesting) stay consistent. fn spawn_capture_access(&self, name: &str) -> Option<String> { let caps = self.current_spawn_captures.as_ref()?; if !caps.contains(name) { return None; } let by_value = self.current_spawn_capture_by_value.as_ref() .map(|s| s.contains(name)).unwrap_or(false); Some(if by_value { format!("_c->{}", name) } else { format!("(*_c->{})", name) }) } /// Plan 217 (D-новый, гибрид C): thin wrapper around `emit_expr_inner`. /// Central choke-point for the auto-cleanup receiver-call disarm /// (`disarm_auto_cleanup_receiver_call`) — rather than hunting down /// every one of the ~20 `block.trailing`/`b.trailing` emission call /// sites scattered across if/match/loop/supervised/with/spawn bodies /// (each a DIFFERENT choke point for "the last expr of a block used as /// a statement"), the check lives HERE: every `X.method()` call is /// necessarily emitted via `emit_expr` at some point, wherever it /// syntactically sits (bare statement, block trailing, nested in a /// bigger expression). Disarming is a pure flag-write with no bearing on /// the computed value, so firing it unconditionally before delegating — /// regardless of nesting position — is sound (idempotent even if some /// OTHER choke-point's own explicit call to /// `disarm_auto_cleanup_receiver_call` already fired it for the same /// node). fn emit_expr(&mut self, expr: &Expr) -> Result<String, String> { self.disarm_auto_cleanup_receiver_call(expr); // №465 (A8.29): same choke-point rationale as the disarm above — // every consume-param-arg Call is emitted via `emit_expr` somewhere, // regardless of nesting. Unlike the disarm (a pure flag-write, order // w.r.t. the call doesn't matter), the zero-storage call MUST run // after an independent copy of the arg's value exists, so this // REWRITES the Call (arg → hoisted temp) rather than just emitting a // preceding line — see `zero_on_move_rewrite_call` doc. match self.zero_on_move_rewrite_call(expr) { Some(rewritten) => self.emit_expr_inner(&rewritten), None => self.emit_expr_inner(expr), } } fn emit_expr_inner(&mut self, expr: &Expr) -> Result<String, String> { match &expr.kind { // Plan 172.5 (D326 R4/R5): call-site `ref <place>` — pass the // address of the addressable place. A `mut ref`/`ro ref` parameter // is lowered to a C pointer (`T*`), so the argument is `&place`. // (Call emission normally consumes RefArg args directly; this arm // is the value-position fallback and keeps the ABI consistent.) ExprKind::RefArg(inner) => { let place = self.emit_expr(inner)?; // Lazy-const Ident может лоуериться в не-адресуемую форму — // принудительный hoist (зеркало prepare_method_recv guard'а). let is_lazy_const_ident = matches!(&inner.kind, ExprKind::Ident(name) if self.lazy_consts.contains(name)); // Адресуемое место (включая ref-параметры: `&((*p))` ≡ `p`) — // прямое взятие адреса (легаси-поведение, Р10/D326). if !is_lazy_const_ident && Self::is_lvalue_receiver(inner) { return Ok(format!("(&({}))", place)); } // Plan 172.14 Ф.1: rvalue-аргумент к by-ref параметру — // materialize-temp (зеркало rvalue-ветки prepare_method_recv). let ty = self.infer_expr_c_type(inner); if Self::is_value_struct_ptr(&ty) { // fluent `-> @` результат — уже указатель на value-slot; // передаём как есть (прецедент prepare_method_recv Р7). return Ok(place); } if Self::is_byref_candidate_c(&ty) { // Ф.3 (дёшево): rvalue уже материализован в НАШ свежий // temp (`_nv_tmp_*` — зарезервированное пространство // fresh_tmp) — второй temp избыточен, берём адрес прямо. // НЕ применяется к mut-копиям (Block-обёртка возвращает // ИМЯ пользовательской переменной, не `_nv_tmp_*`). if place.starts_with("_nv_tmp") && Self::looks_like_ident_str(&place) { return Ok(format!("(&{})", place)); } let tmp = self.fresh_tmp(); self.line(&format!("{} {} = {};", ty, tmp, place)); return Ok(format!("(&{})", tmp)); } Ok(format!("(&({}))", place)) } // Plan 172.1 [literal-coercion channel] (§0/§1): if the checker materialized a // sized-integer coercion for THIS literal (D55), emit it WITH that type so a // context-typed literal does not collapse to `nova_int` (named-priority: // uint≠int, u8≠i16). `channel_int_c_type` returns `None` for un-annotated / // `nova_int`-seeded literals → byte-identical legacy `((nova_int)NLL)`. ExprKind::IntLit(n) => Ok(self .channel_int_c_type(expr.id) .map(|ty_c| Self::emit_typed_int_literal(*n, &ty_c)) .unwrap_or_else(|| format!("((nova_int){}LL)", n))), // Plan 70.3/152.8: char literal cast к distinct `nova_char` typedef (uint32_t). ExprKind::CharLit(cp) => Ok(format!("((nova_char){}U)", cp)), ExprKind::FloatLit(f) => { // f.to_string() для 1e20 даёт "100000000000000000000" (без точки/exp) // — это integer-литерал в C, переполняет u64. Принудительно // используем scientific notation, добавляем суффикс если нужен dot. let s = if f.is_finite() && (f.abs() >= 1e16 || (f.abs() != 0.0 && f.abs() < 1e-4)) { format!("{:e}", f) // scientific для очень больших/малых } else { let raw = f.to_string(); if raw.contains('.') || raw.contains('e') || raw.contains('E') { raw } else { format!("{}.0", raw) // целые f64-литералы — добавим .0 } }; Ok(format!("((nova_f64){})", s)) } ExprKind::BoolLit(b) => Ok(if *b { "true".into() } else { "false".into() }), ExprKind::UnitLit => Ok("NOVA_UNIT".into()), // Plan 134: `null ptr` literal → C `((void*)0)` (*() = void*). ExprKind::NullPtrLit => Ok("((void*)0)".into()), // Plan 186 (D412): hex-blob / embed literal -> `[]u8` zero-copy VIEW // over the interned static rodata blob (data -> static, len == cap // == N; no memcpy on this path). The mut/consume-binding COPY path // is handled in emit_stmt's Let arm (materialize-by-copy at the // binding point). ExprKind::HexBlobLit(bytes) => { let bytes = bytes.clone(); let vec_c = self.ensure_vec_u8_instance(); if bytes.is_empty() { return Ok(format!("(({}*)nova_blob_view((const uint8_t*)0, 0))", vec_c)); } let n = bytes.len(); let sym = self.intern_blob_literal(&bytes); Ok(format!("(({}*)nova_blob_view({}, {}))", vec_c, sym, n)) } ExprKind::StrLit(s) => { // Plan 139 Ф.6: literal interning. Identical string literals // share one `static const uint8_t[]` rodata buffer + one // `static const nova_str` value (see intern_str_literal). The // expression evaluates to that shared value. Semantically // invisible: str eq/hash are byte-content (Ф.3), so the shared // pointer identity cannot be observed; the buffer is `*ro` // immutable so sharing is sound (R14). Falls back to an inline // compound literal only for the empty string (no buffer needed). Ok(self.intern_str_literal(s)) } ExprKind::InterpolatedStr { parts } => { self.emit_interpolated_str(parts) } ExprKind::Ident(name) => { // Plan 172.1 U.1.3b: explicit `self` keyword parses to `Ident("self")` // (NOT `SelfAccess` — only `@` does, parser/mod.rs:7554/7563), but it IS // the receiver. The checker resolves it as self (Gap A, `31ed22b7`); codegen // must lower it to the C receiver param `nova_self`, mirroring the SelfAccess // arm — else an inlined Nova-body method using `self.m()` (e.g. sync // `Once.start` → `self.start_won()`, exposed by U.1.3b sync-inline) // emits `Nova_..._method_m(self)` with an undeclared C identifier `self`. // `self` is reserved (never a user variable), so this is unambiguous. The // value-record-by-pointer deref mirrors the SelfAccess arm exactly. if name == "self" { // Owner fix 2026-08-09 (closes №468): `nova_self` is // ALSO a pointer for a `mut @` PRIMITIVE receiver now // (`is_primitive_mut_recv_ptr`) — deref it the same way // as the value-struct pointer case. let self_by_ptr_value_record = self.var_types.get("nova_self") .map(|c| Self::is_value_struct_ptr(c) || Self::is_primitive_mut_recv_ptr(c)) .unwrap_or(false) || self.current_receiver_type.as_deref() .map(|t| t != "str" && self.value_record_names.contains(t)) .unwrap_or(false); return Ok(if self_by_ptr_value_record { // 172.4 Ф.3 A1: зеркало SelfAccess-арма — в return-позиции // `-> @` fluent-метода `return self` эмитится ptr. if self.in_recv_ptr_return_position.get() { "nova_self".into() } else { "(*nova_self)".into() } } else { "nova_self".into() }); } // Plan 172.5 (D326 R5): a `ro ref`/`mut ref` parameter is a C // pointer — dereference on every value-position read/write so // the body sees a `T`, and assignment lands in the caller's // storage. Checked early: ref-params are user locals, never // variant/ctor names. // // [M-nv-spawn-ctx-capture-mut-param-ptr-mismatch] fix (222.7): // `ref_params` is populated once per ENCLOSING function and // never cleared/rescoped when emission descends into a // spawned fiber's own body — so a name that was a by-pointer // param/ref-local IN THE OUTER FUNCTION stayed in this set // even after `current_spawn_captures` activated the SAME name // as a captured ctx field for the fiber (spawn's `refs` scan // — `collect_idents_expr`/`collect_idents_stmt` — walks INTO // a nested `consume X = e { .. }`'s own init expression, e.g. // `spawn consume ws = s.share() { .. }` capturing the outer // `mut TcpStream` param `s`). Since this arm returns early, // it pre-empted the `current_spawn_captures` arm below — // which is the one that knows `s` is no longer a real C // variable inside the fiber body at all, only reachable via // `_c->s`/`(*_c->s)` — emitting a bare `(*s)` against an // undeclared C identifier (CC-FAIL: "use of undeclared // identifier 's'"). An active spawn-capture for this exact // name always wins: skip the ref-param branch and fall // through to the capture-access arm below. let shadowed_by_spawn_capture = self.current_spawn_captures.as_ref() .map(|c| c.contains(name)).unwrap_or(false); if self.ref_params.contains(name) && !shadowed_by_spawn_capture { return Ok(format!("(*{})", name)); } // Plan 61 Ф.3: typed Fail[E] handler-arm parameter — name // resolves через fail-frame typed payload, не как обычная // переменная. См. emit_handler_lit Plan 61 Ф.3 populator. // // ВАЖНО: handler-arm с `|e: E|` annotation (e ≠ nova_str) // получает E-typed view через cast'нутый deref payload. // Field-access потом работает естественно: `e.detail` → // `(*(E*)payload).detail`. if let Some(e_c_type) = self.fail_e_map.get(name).cloned() { if e_c_type.ends_with('*') { // Pointer-typed E (sum/record): payload уже holds // pointer-to-instance. Cast directly. return Ok(format!( "(({})_nova_fail_top->error_user_payload)",