/
nv-lang
/
nova
Обзор
Документация
Войти
/
nv-lang
/
nova
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
main
compiler-codegen/src/types/mod.rs
53 845 строк
3 MB
Evgeniy Golovin
gate: two refusals, one real finding and one probe of my own making
11 авг 2026, 03:26
11 авг 2026, 03:26
d7b0d7c
Код
Авторство
О чём код?
//! Type checker и effect inference. //! //! Минимальная реализация: проверяем имена типов, выводим типы локальных //! переменных, выводим эффекты для private функций (D28). Generic-параметры //! проверяются как abstract names — мономорфизация делается при //! интерпретации (treewalk не требует всего). use crate::ast::*; use crate::diag::{Diagnostic, FileId, MAIN_FILE_ID, Span}; use crate::parser::{impl_spec_base_name, impl_spec_args_text}; use std::collections::{HashMap, HashSet}; /// Plan 238 Ф.1 (D446 "Ф.8-НОВАЯ"): total per-function/method M:N-safety /// tag over the resolved call graph — measurement/dump only, no /// enforcement (Ф.2). See `fiber_safety.rs`'s module doc for the full /// design and `docs/plans/238-fiber-memory-model.md`. mod fiber_safety; /// Plan 221.1 п.11 №428 (D62/№113 "форма vs свойство"): transitive `Fail`- /// reachability over the resolved call graph — see `fail_reach.rs`'s own /// module doc for the full design/rationale. mod fail_reach; /// Plan 196 (gs-bounds migration, spike `docs/plans/wip/196-gs-spike.md`): /// `gs` ("generics in scope") used to be `HashSet<String>` — ONLY the names of the /// generic-parameters visible in the current fn/type-decl body, no protocol bounds /// (`GenericParam.bounds`, `ast/mod.rs`). That lost the one piece of information a /// resolver needs to dispatch a call on a BARE generic-param receiver by its bound /// (`Option[T Debug]@debug`'s body calling `v.debug(f)` where `v: T`, `T: Debug`) — /// the pre-existing narrow precedent for carrying bounds through, `current_fn_generics` /// (`RefCell<Vec<GenericParam>>` below), proved the pattern works; this generalizes it /// to every `gs` site. Keyed by name (the lookup every reader already did via /// `.contains`/`.contains_key`); value is the full declaration, built ONCE per /// fn/type-decl (not on any per-expr hot path) and passed down by reference through /// the existing recursive `walk_*`/`f1_*` traversal — no ExprId involved (bounds are /// per-DECLARATION, not per-call-site; see spike §3). pub(crate) type GenericScope = HashMap<String, GenericParam>; /// Plan 196 gs-bounds: shared membership check for BOTH the legacy name-only carrier /// (`HashSet<String>` — still used for method-level generic sets like `method_names`/ /// `self_only`/`recv_generic_names`, which are a DIFFERENT concept from the /// declaration-level `GenericScope` above and are not part of this migration) and the /// new bounds-carrying `GenericScope`, so a single `typeref_mentions_any`/ /// `mark_type_params` body serves both without duplicating the recursive walk. pub(crate) trait GenericNameSet { fn has_generic_name(&self, name: &str) -> bool; } impl GenericNameSet for HashSet<String> { #[inline] fn has_generic_name(&self, name: &str) -> bool { self.contains(name) } } impl GenericNameSet for GenericScope { #[inline] fn has_generic_name(&self, name: &str) -> bool { self.contains_key(name) } } // Plan 172.13 Ф.1: constraint-based inference core scaffold (unification + // occurs-check + type-set membership). NOT wired into `f1_expr_inner` // globally yet — Ф.2 migrates ad-hoc producer packages onto it one at a // time, gated by byte-parity per package. See // `docs/plans/172.13-constraint-inference.md`. pub mod constraint_solver; // Plan 172.1 U.5.4: the lossy bootstrap `Ty` enum (+ `ty_of_ref`) was DELETED — its int // collapse + single `Ptr`/`TypedPtr` modelling is fully subsumed by the lossless // `ResolvedType` (defined below), the single type representation the checker uses. /// Plan 172.1 U.5.1 ([M-172.1-U5-structured-type]): single LOSSLESS structured /// type carrier — replaces the lossy `Ty`/`TyCat` int-collapse (both map every int /// width to ONE `Int` variant). Int-family carries EXACT `(width, signed)`; the L3 /// pointee modifier is carried via `TypedPtr` (mirrors `Ty::TypedPtr`). L2 view /// (`ro`/`mut`) is transparent here — matching the `is_int_narrowing`/`int_width_rank` /// unwrap (D227/D54); full L2 carriage is deferred to U.5.2 if the `assignable` /// rewire needs it (`[M-172.1-U5-l2-view-deferred]`). /// /// §2 exception: the primitive width/sign table (`int`≡`i64`=(64,signed), /// `uint`≡`u64`=(64,unsigned), …) is the sanctioned hardcode (also pinned by the /// `int`≡`i64` D-block). /// /// U.5.2 amend: `Scalar` also carries `wide_default` — `true` ONLY for `int`/`uint`, /// `false` for the explicit sized names (`i8`..`i64` / `u8`..`u64`). `int`≡`i64` and /// `uint`≡`u64` stay equal on `(width, signed)` (arithmetic / `would_narrow_into` /// read only those), but DIFFER on `wide_default`: the wide defaults skip the literal /// range-check (D227 Rule 1), the sized names are range-checked. This is the one bit /// the lossy `Ty`/`TyCat` could not express and the reason `sized_int_name` needs the /// structured carrier (`int`/`uint` → `None`; `i64`/`u64` → checked). #[derive(Debug, Clone, PartialEq)] pub enum ResolvedType { /// Int-family with EXACT bit-width + signedness (lossless replacement for /// `Ty::Int` / `TyCat::Int`). `wide_default`: `true` for `int`/`uint` only /// (D227 Rule 1 — no literal range-check), `false` for sized `i8..u64`. Scalar { width: u8, signed: bool, wide_default: bool }, /// Float-family with EXACT bit-width (`f32`=32, `f64`=64) — lossless like `Scalar` /// (U.5.3): `cat_compatible` is permissive on width (assignability), but the /// generic-arg type-identity check needs `f32`≠`f64`. Float { width: u8 }, Str, Bool, Unit, Never, /// Plan 115 D214 opaque `ptr`. Ptr, Any, /// 172.1.2 mono-канал (Шаг 1, 2026-07-02): residual generic-ПАРАМЕТР шаблона — /// ЯВНЫЙ носитель (не name-эвристика, §3-чисто). Аннотация generic-тела с /// TypeParam — ПРАВДА о шаблоне, валидная для всех mono-инстансов; подстановка — /// лоуэринг-забота: resolved_type_to_c берёт current_type_subst/overrides, /// промах = Err (channel-miss → legacy-навигация), НИКОГДА не Nova_T*/nova_int. /// Продюсеры помечают через mark_type_params (td.generics ∪ fn.generics ∪ gs). TypeParam(String), /// L3 typed pointer: pointee modifier + inner (mirrors `Ty::TypedPtr`). TypedPtr(crate::ast::PointerModifier, Box<ResolvedType>), /// L2 content-view `readonly T` (D246 axis L2). U.5.5(a): carried LOSSLESSLY (D315 — /// the canonical type holds ALL mutability axes) where the pre-U.5.5 `from_type_ref` /// unwrapped it. It is TRANSPARENT to (a) the int/never queries — `peel_view` strips it /// so narrowing/range-check/bottom-type see the inner type, byte-identical to the old /// unwrap — and (b) the C lowering (`readonly T` ABI ≡ `T`, exactly as /// `type_ref_to_c`). Produced ONLY by `from_type_ref`; `resolved_cat_of` stays /// transparent (never yields `Readonly`), so `cat_compatible_rt`/`distinct_mono` never /// observe it. The L2-`mut`/`unsafe` (binding-level, non-pointer) wrappers remain /// transparent — they are not a `readonly` view. Readonly(Box<ResolvedType>), /// Named record/sum/newtype/alias/`char` WITH its generic type-arguments (U.5.3: /// `args` preserves `Stack[int]`≠`Stack[u32]` for the type-identity check; empty for /// non-generic names). `cat_compatible` compares the NAME only (args ignored — /// permissive assignability), the generic-arg check compares args exactly. /// /// U.5.5(a) — `module`: the qualifier path segments BEFORE the name (the syntactic /// `TypeRef::Named.path` minus its last segment); `[]` for a bare name. Makes the /// carrier LOSSLESS for the C mangle (D315): `type_ref_to_c` mangles a user type from /// `path.join("_")` → `Nova_<module…>_<name>*`, so the pre-U.5.5 `path.last()` dropped /// the module and mis-mangled `Nova_mymod_Foo*` as `Nova_Foo*`. Populated ONLY by /// `from_type_ref` (the direct C-lowering producer); `resolved_cat_of` and the literal /// producer leave it `[]` — they key category/identity on `name` alone, so the field is /// invisible to `cat_compatible_rt`/`distinct_mono` (both read `name`, not the struct). /// Full path for the future `resolved_type_to_c` lowering = `module ++ [name]`. Named { name: String, module: Vec<String>, args: Vec<ResolvedType> }, /// Plan 172.1: NO LONGER the `[]T` slice carrier — `[]T`/`Vec[T]` canonicalize to the /// NOMINAL `Named{Vec}` (D239/D315 §0, ONE C-lowering window). `R::Array` now carries /// ONLY (a) `[N]T` fixed-arrays from `from_type_ref` (the `N` is still dropped — /// pre-existing, [M-172.1-fixedarray-N]) and (b) the INTERNAL category key from /// `resolved_cat_of` (`Vec`/`[]`/`[N]` → `R::Array(elem)`, never lowered to C — used /// only by `cat_compatible_rt`/`distinct_mono`). Folding the category side onto /// `Named{Vec}` (then deleting this variant) is the orthogonal follow-up. Array(Box<ResolvedType>), /// [M-fixed-array-value-semantics] (2026-07-10, D27-амендмент): `[N]T` fixed-size /// array — DISTINCT from `Array` (which after this change carries ONLY the internal /// `resolved_cat_of` compat-category key, never a real `[N]T` C-lowering target). /// Lossless `N` — closes [M-172.1-fixedarray-N] for the C-LOWERING path (the /// category-compat key `resolved_cat_of_depth` deliberately still collapses `[N]T` /// into `R::Array(elem)` — orthogonal, untouched: assignability between `[]T`/`[N]T` /// is a separate concern from how `[N]T` is REPRESENTED in C). Produced ONLY by /// `from_type_ref`; lowered by `resolved_type_to_c` to an inline mono struct /// `{ T data[N]; }` (stack/field value, no heap pointer) via `register_mono_fixed_array`. FixedArray(usize, Box<ResolvedType>), Tuple(Vec<ResolvedType>), /// U.5.5(c) (D315 lossless): `effects` carries the FULL resolved effect TYPE, not a /// bare name — so `Fail[E]` keeps its `E` (`Named{name:"Fail", args:[E]}`), `Db[T]` /// keeps `T`, etc. Was `Vec<String>` (`from_type_ref` took `path.last()`, dropping /// generics) → lossy, a D315 violation («несёт ПОЛНУЮ семантическую личность») AND a /// blocker for typed-errors (Plan 173 Ф.4 `Fail[E]`-dispatch by `type_id`) + `any`/`is` /// (Plan 175). Like every other `ResolvedType` field it is keyed for category/identity /// on `name`/structure, so the effect args stay INVISIBLE to `cat_compatible_rt`/ /// `distinct_mono` (they never recurse into `effects`); only `from_type_ref` writes it /// and NO consumer reads it yet → byte-identical by construction (parity with U.5.5(a)). Func { params: Vec<ResolvedType>, ret: Box<ResolvedType>, effects: Vec<ResolvedType> }, /// Plan 172.12 A1′ — TRANSITIONAL DEBT carrier (the single `String`→`ResolvedType` /// lift target for `current_type_subst`). A mono type-arg C-name that is born as a /// C-string at the subst layer (the subst producers read the string-native mono /// registries — `generic_type_instance_info` / mangled-name split — which carry NO /// `ResolvedType` until A1″/A1‴). Wraps the C-name VERBATIM: NO parse, NO fabricated /// structure (§0-honest — this is the OPPOSITE of a reverse `c_type→RT` inversion; it /// claims no structure, only transports the string unchanged inside the widened /// carrier). Produced ONLY by `Emitter::lift_c_name`, consumed ONLY by /// `resolved_type_to_c` (prints the string verbatim) and `Emitter::subst_val_c` — /// NEVER reaches `cat_compatible_rt` / `distinct_mono` / `from_type_ref` (subst values /// are read as C only). Removed wholesale when A1″ makes the subst producers store a /// real `ResolvedType`. Raw(String), } impl ResolvedType { /// `(width, signed, wide_default)` `Scalar` for a bare int-family primitive NAME /// (`i8`..`i64` / `u8`..`u64` / `int` / `uint`), else `None`. THE single primitive /// width/sign table (`[M-172.1-U5-structured-type]` §2 exception) — shared by /// `from_type_ref` (empty-generics guarded) and `resolved_cat_of` (unguarded, /// mirroring `cat_of`'s name match) so the width/sign/wide-default mapping lives in /// ONE place (no drift, §2/§0). fn scalar_from_int_name(name: &str) -> Option<ResolvedType> { use ResolvedType as R; Some(match name { "i8" => R::Scalar { width: 8, signed: true, wide_default: false }, "i16" => R::Scalar { width: 16, signed: true, wide_default: false }, "i32" => R::Scalar { width: 32, signed: true, wide_default: false }, "i64" => R::Scalar { width: 64, signed: true, wide_default: false }, "int" => R::Scalar { width: 64, signed: true, wide_default: true }, "u8" => R::Scalar { width: 8, signed: false, wide_default: false }, "u16" => R::Scalar { width: 16, signed: false, wide_default: false }, "u32" => R::Scalar { width: 32, signed: false, wide_default: false }, "u64" => R::Scalar { width: 64, signed: false, wide_default: false }, "uint" => R::Scalar { width: 64, signed: false, wide_default: true }, _ => return None, }) } /// Total conversion from a `TypeRef`, mirroring `ty_of_ref`'s collapse rules /// (`Pointer`/`Mut(Pointer)`/`Unsafe(Pointer)` → `TypedPtr`; `Readonly` / /// `Mut(non-ptr)` / `Unsafe(non-ptr)` transparent) but LOSSLESS for int /// width/sign. Int-family resolves only with EMPTY type-args (mirrors /// `int_width_rank`'s generics-guard, so `int[X]` — an arity error caught /// elsewhere — is `Named`, never a bogus scalar). pub fn from_type_ref(tr: &TypeRef) -> ResolvedType { use ResolvedType as R; match tr { TypeRef::Named { path, generics, .. } => { let name = path.last().map(|s| s.as_str()); // Int-family only with EMPTY type-args (generics-guard mirrors // `int_width_rank`/`ty_of_ref`, so `int[X]` — arity error caught // elsewhere — is `Named`, never a bogus scalar). Category resolution // (`resolved_cat_of`) drops this guard to mirror `cat_of`. if generics.is_empty() { if let Some(s) = name.and_then(ResolvedType::scalar_from_int_name) { return s; } } match name { Some("f32") => R::Float { width: 32 }, Some("f64") => R::Float { width: 64 }, Some("str") => R::Str, Some("bool") => R::Bool, Some("never") => R::Never, // (legacy bare `ptr` name arm removed, U.5.4 — `ptr` is no longer a // type, Plan 134; it now falls through to `Named` like any unknown // name. `ResolvedType::Ptr` stays for typed pointers `*T`/`*()`.) Some(n) => R::Named { name: n.to_string(), // U.5.5(a): carry the qualifier prefix (path minus the name // segment) so the C mangle keeps the full identity, not just // `path.last()`. `Some(n)` ⇒ `path` non-empty ⇒ no underflow. module: path[..path.len() - 1].to_vec(), args: generics.iter().map(R::from_type_ref).collect(), }, None => R::Any, } } // D239 `[]T ≡ Vec[T]` — canonicalize the slice sugar to the NOMINAL `Vec[T]` // carrier (D315 §0: ONE canonical `ResolvedType`; `[]T` and `Vec[T]` are the // SAME type → MUST be structurally equal). Bare `Vec` ⇒ empty `module` (matches // the bare-`Vec[T]` Named arm above). RECURSIVE by construction: `[][]T` → // `Vec[Vec[T]]`, any nesting (the inner `from_type_ref` re-canonicalizes). The // C-lowering single-source is `resolved_array_to_c` (closure-array / stub-erasure // / Vec-mono worklist), reached for BOTH `[]T` and `Vec[T]` via the `"Vec"` arm // in `resolved_named_to_c` → byte-identical for `[]T`, D239-correct for `Vec[T]`. // (`resolved_cat_of` still yields `R::Array` for the category-compatibility key — // an INTERNAL structural key never lowered to C, so the D315 "two C windows" // concern is closed here; folding the category side onto `Named{Vec}` is the // orthogonal follow-up.) TypeRef::Array(inner, _) => R::Named { name: "Vec".to_string(), module: Vec::new(), args: vec![R::from_type_ref(inner)], }, // `[N]T` fixed-size array is a DISTINCT built-in (stack-allocated, carries `N`) — // NOT growable `Vec`. [M-fixed-array-value-semantics] (2026-07-10, D27-амендмент): // closes [M-172.1-fixedarray-N] for the C-lowering path — `N` carried losslessly // in its own `R::FixedArray` variant (was `R::Array`, dropping `N`). TypeRef::FixedArray(n, inner, _) => R::FixedArray(*n, Box::new(R::from_type_ref(inner))), TypeRef::Tuple(elems, _) => R::Tuple(elems.iter().map(R::from_type_ref).collect()), TypeRef::Func { params, return_type, effects, .. } => R::Func { params: params.iter().map(R::from_type_ref).collect(), ret: Box::new(return_type.as_ref().map(|t| R::from_type_ref(t)).unwrap_or(R::Unit)), // U.5.5(c) (D315 lossless): carry the FULL effect type (name + module + // args via the U.5.5(a)-lossless `Named`), not just `path.last()` — so // `Fail[E]` keeps `E`. Effects are always `Named`; non-Named are dropped // (as before). Write-only today → byte-identical (no consumer reads it). effects: effects .iter() .filter_map(|e| match e { TypeRef::Named { .. } => Some(R::from_type_ref(e)), _ => None, }) .collect(), }, TypeRef::Protocol { .. } => R::Any, TypeRef::Unit(_) => R::Unit, // U.5.5(a): carry the L2 `readonly` view (was unwrapped). Transparent to the // int/never queries via `peel_view` and to the C lowering, but lossless in the // carrier (D315). `mut`/`unsafe` on a non-pointer stay transparent below — they // are binding-level, not a `readonly` content-view. TypeRef::Readonly(inner, _) => R::Readonly(Box::new(R::from_type_ref(inner))), // U.5.5(a): `*mut T` / `*unsafe T` pointee-modifier fidelity. The canonical // mutable pointer is `Pointer(Mut(T))` / `Pointer(Unsafe(T))` (Nova V2 syntax, // Plan 131/118.5); the pre-U.5.5 arm collapsed BOTH spellings to // `TypedPtr(Ro, …)`, losing the modifier (so the C mangle would emit `const T*` // instead of `T*`). Mirror `type_ref_to_c`'s `(is_mutable_ptr, base_inner)` // split — strip a `Mut`/`Unsafe` wrapper for the modifier — so `Pointer(Mut(T))` // ≡ `Mut(Pointer(T))` → `TypedPtr(Mut, T)` (the `Mut(Pointer)`/`Unsafe(Pointer)` // spellings below already did this; this closes the divergent `Pointer(Mut)`). TypeRef::Pointer(inner, _) => { let (modifier, base) = match inner.as_ref() { TypeRef::Mut(ti, _) => (crate::ast::PointerModifier::Mut, ti.as_ref()), TypeRef::Uninit(ti, _) => (crate::ast::PointerModifier::Uninit, ti.as_ref()), _ => (crate::ast::PointerModifier::Ro, inner.as_ref()), }; R::TypedPtr(modifier, Box::new(R::from_type_ref(base))) } TypeRef::Mut(inner, _) => match inner.as_ref() { TypeRef::Pointer(p, _) => { R::TypedPtr(crate::ast::PointerModifier::Mut, Box::new(R::from_type_ref(p))) } _ => R::from_type_ref(inner), }, TypeRef::Uninit(inner, _) => match inner.as_ref() { TypeRef::Pointer(p, _) => { R::TypedPtr(crate::ast::PointerModifier::Uninit, Box::new(R::from_type_ref(p))) } _ => R::from_type_ref(inner), }, // Plan 184 (D326-ревизия): `ref T` — ограниченный ссылочный тип. // Для системы типов ПРОЗРАЧЕН (Р5: чтение = разыменование → `T`): // резолвится в тип цели. Р6 (`ref H ≡ H`) выпадает автоматически — // и value, и heap `T` дают тот же `ResolvedType`, что и без `ref`. // Указатель-алиас (value `T`) — деталь C-lowering (ref-локалы, // `type_ref_to_c`), не наблюдаемая на уровне резолва. TypeRef::Ref(inner, _) => R::from_type_ref(inner), } } /// U.5.5(a): strip the L2 `readonly` view wrapper(s) to reach the underlying type. /// The view is carried losslessly (D315 — all mutability axes live in the carrier) but /// is TRANSPARENT to the int/never queries (narrowing, range-check, bottom-type) and to /// the C lowering (`readonly T` ABI ≡ `T`) — byte-identical to the pre-U.5.5 unwrap and /// to `type_ref_to_c`. Idempotent on non-`Readonly`; loops to peel nested `readonly`. pub fn peel_view(&self) -> &ResolvedType { let mut t = self; while let ResolvedType::Readonly(inner) = t { t = inner; } t } /// `(width, signed)` if int-family, else `None`. Single source for the width/sign /// that `int_width_rank` recovers separately today (folded by U.5.2). Peels the L2 /// `readonly` view first (U.5.5(a)) so `readonly Tn` narrows exactly like `Tn`. pub fn int_width_sign(&self) -> Option<(u8, bool)> { match self.peel_view() { ResolvedType::Scalar { width, signed, .. } => Some((*width, *signed)), _ => None, } } /// Signedness if int-family. pub fn is_signed(&self) -> Option<bool> { self.int_width_sign().map(|(_, s)| s) } /// Canonical primitive NAME of an int-family scalar — `i8`..`i64`/`int`, /// `u8`..`u64`/`uint` — reconstructed from `(width, signed, wide_default)`, the /// inverse of `scalar_from_int_name` (THE single §2-sanctioned primitive table). /// `None` for non-int. Used for the diagnostic name in BOTH the sized range-check /// and the D227 unsigned-floor (where wide `uint` has no `sized_int_name`). pub fn int_name(&self) -> Option<&'static str> { // U.5.5(a): peel the L2 `readonly` view — `readonly Tn` names like `Tn`. let ResolvedType::Scalar { width, signed, wide_default } = self.peel_view() else { return None; }; Some(match (*width, *signed, *wide_default) { (64, true, true) => "int", (64, false, true) => "uint", (8, true, _) => "i8", (16, true, _) => "i16", (32, true, _) => "i32", (64, true, _) => "i64", (8, false, _) => "u8", (16, false, _) => "u16", (32, false, _) => "u32", (64, false, _) => "u64", _ => return None, }) } /// Sized-int name (`i8`..`i64` / `u8`..`u64`) for the literal range-check — /// `int_name` gated to the SIZED types: `None` for the wide defaults (`int`/`uint`, /// D227 Rule 1: a bare literal in `int`/`uint` context is not range-checked for the /// UPPER bound) and for non-int. (The unsigned FLOOR — negative into any unsigned, /// incl `uint` — is enforced separately, D227 amend.) Byte-identical to the deleted /// standalone `sized_int_name(&TypeRef)` when called on `from_type_ref(tr)`. pub fn sized_int_name(&self) -> Option<String> { // U.5.5(a): peel the L2 `readonly` view first — `readonly int` stays a wide default // (no range-check), `readonly u8` stays sized (range-checked), exactly like the // bare `int`/`u8`. `int_name` peels too, so the `_` arm is also view-transparent. match self.peel_view() { ResolvedType::Scalar { wide_default: true, .. } => None, _ => self.int_name().map(str::to_string), } } /// Would coercing a non-literal value of `self` into `target` LOSE range — i.e. /// a narrowing / value-range-unsafe int conversion requiring explicit `as`? /// Mirrors `is_int_narrowing` EXACTLY (single source for U.5.2). Non-int on /// either side ⇒ `false` (permissive, D54). pub fn would_narrow_into(&self, target: &ResolvedType) -> bool { let (Some((fw, fs)), Some((ew, es))) = (self.int_width_sign(), target.int_width_sign()) else { return false; }; if fw == ew && fs == es { return false; // identity widening (int≡i64, uint≡u64, Tn→Tn) } let safe_widening = match (fs, es) { (true, true) => ew > fw, // signed → wider signed (false, false) => ew > fw, // unsigned → wider unsigned (false, true) => ew > fw, // unsigned → strictly wider signed (range fits) (true, false) => false, // signed → unsigned: never implicit }; !safe_widening } /// Plan 172.1 U.4.4 / U.1.3b: the shared PRIMITIVE gate. A resolved type is /// primitive-lowerable iff its C-type is ALWAYS a registered builtin /// (`nova_int`/`nova_bool`/`uint32_t`/`nova_str`/`nova_char`/…) — its C name is /// CONTEXT-INDEPENDENT, with no undeclared-identifier, mono-substitution, tuple /// registration, or generic-inference hazard. Such a type can be materialized into /// the checker channel (`resolved_types` / `resolved_callees` return) and lowered by /// codegen authoritatively. Non-primitive (records / generics / `Vec` / pointers / /// enums / tuples / `Result`) need codegen mono/typedef context the channel cannot /// reproduce → they stay on the legacy path (U.4.3(d)/U.4.5 territory). View axes /// (L2 `readonly`) are peeled — `readonly T` lowers identically to `T`. SINGLE source /// consumed by BOTH the checker (`primitive_gate`) and codegen (Gap B `fn_ret_by_span` /// extern-method indexing) — §0/§3 (one gate, no duplication). pub fn is_primitive_lowerable(&self) -> bool { match self.peel_view() { ResolvedType::Scalar { .. } | ResolvedType::Float { .. } | ResolvedType::Bool | ResolvedType::Str | ResolvedType::Unit => true, ResolvedType::Named { name, args, .. } => args.is_empty() && name.as_str() == "char", _ => false, } } } #[cfg(test)] mod resolved_type_tests { use super::*; use crate::diag::Span; fn r(name: &str) -> ResolvedType { ResolvedType::from_type_ref(&prim_ref(name, Span::dummy())) } #[test] fn primitives_lossless_width_sign() { // `int`≡`i64` and `uint`≡`u64` on (width, signed) — the arithmetic / // narrowing axis — but DIFFER on `wide_default` (the D227 range-check axis). assert_eq!(r("int"), ResolvedType::Scalar { width: 64, signed: true, wide_default: true }); assert_eq!(r("i64"), ResolvedType::Scalar { width: 64, signed: true, wide_default: false }); assert_eq!(r("uint"), ResolvedType::Scalar { width: 64, signed: false, wide_default: true }); assert_eq!(r("u64"), ResolvedType::Scalar { width: 64, signed: false, wide_default: false }); // representation identical (int ≡ i64): same width/sign ⇒ no narrowing between them. assert_eq!(r("int").int_width_sign(), r("i64").int_width_sign()); assert_eq!(r("uint").int_width_sign(), r("u64").int_width_sign()); assert_eq!(r("i8"), ResolvedType::Scalar { width: 8, signed: true, wide_default: false }); assert_eq!(r("u32"), ResolvedType::Scalar { width: 32, signed: false, wide_default: false }); assert_eq!(r("f64"), ResolvedType::Float { width: 64 }); assert_eq!(r("f32"), ResolvedType::Float { width: 32 }); assert_eq!(r("str"), ResolvedType::Str); assert_eq!(r("bool"), ResolvedType::Bool); assert_eq!(r("never"), ResolvedType::Never); // `char` has no `Ty` primitive → `Named` (mirrors ty_of_ref). assert_eq!(r("char"), ResolvedType::Named { name: "char".to_string(), module: vec![], args: vec![] }); assert_eq!(r("Foo"), ResolvedType::Named { name: "Foo".to_string(), module: vec![], args: vec![] }); } #[test] fn is_primitive_lowerable_gates_primitives_not_aggregates() { // Plan 172.1 U.1.3b Gap B: the shared primitive gate. A return type passes iff // its C name is CONTEXT-INDEPENDENT (no mono/tuple/typedef context needed) — only // then is codegen's bare `fn_ret_by_span` span→C-name index for an EXTERN method // sound. Aggregates (tuple/`Result`/`Vec`/record/pointer) FAIL the gate and stay on // the legacy path (the U.4.3 tuple-mono hazard). let sp = Span::dummy(); let named = |n: &str, gen: Vec<TypeRef>| TypeRef::Named { path: vec![n.to_string()], generics: gen, span: sp, }; let rt = |tr: &TypeRef| ResolvedType::from_type_ref(tr); let prim = |n: &str| ResolvedType::from_type_ref(&prim_ref(n, sp)); // PRIMITIVES (context-independent C name) → gate TRUE. for n in ["int", "i64", "i8", "u32", "uint", "u64", "f64", "f32", "str", "bool"] { assert!(prim(n).is_primitive_lowerable(), "primitive `{n}` must gate true"); } // `char` is the one `Named` that gates true (no `Ty` variant — `start_won`'s // sibling class: a primitive whose C name `nova_char` is fixed). assert!(prim("char").is_primitive_lowerable()); // `unit` → true. assert!(rt(&TypeRef::Unit(sp)).is_primitive_lowerable()); // L2 `readonly` view is peeled → `readonly bool` gates like `bool`. assert!(rt(&TypeRef::Readonly(Box::new(prim_ref("bool", sp)), sp)).is_primitive_lowerable()); // AGGREGATES / context-dependent C name → gate FALSE. // tuple `(int, int)` — the net `split→(R,W)` class (`_NovaTuple_*` mono hazard). assert!(!rt(&TypeRef::Tuple(vec![prim_ref("int", sp), prim_ref("int", sp)], sp)) .is_primitive_lowerable()); // `Result[int, str]` / `Option[int]` / `Vec[int]` — `Named` WITH args (erasure/mono). assert!(!rt(&named("Result", vec![prim_ref("int", sp), prim_ref("str", sp)])) .is_primitive_lowerable()); assert!(!rt(&named("Option", vec![prim_ref("int", sp)])).is_primitive_lowerable()); assert!(!rt(&named("Vec", vec![prim_ref("int", sp)])).is_primitive_lowerable()); // user record type `Foo` — `Named`, no args, not `char`. assert!(!rt(&named("Foo", vec![])).is_primitive_lowerable()); // `never` (bottom) is NOT primitive-lowerable (excluded from the gate). assert!(!prim("never").is_primitive_lowerable()); // array `[]int` and typed pointer `*int` → false. assert!(!rt(&TypeRef::Array(Box::new(prim_ref("int", sp)), sp)).is_primitive_lowerable()); assert!(!rt(&TypeRef::Pointer(Box::new(prim_ref("int", sp)), sp)).is_primitive_lowerable()); } #[test] fn l2_readonly_view_carried_but_query_transparent() { let int = || prim_ref("int", Span::dummy()); let ro = |t| TypeRef::Readonly(Box::new(t), Span::dummy()); let scal_int = || ResolvedType::Scalar { width: 64, signed: true, wide_default: true }; // U.5.5(a): the L2 `readonly` view is now CARRIED losslessly (pre-U.5.5 it was // unwrapped to the inner type) — the carrier distinguishes `readonly T` from `T`. assert_eq!( ResolvedType::from_type_ref(&ro(int())), ResolvedType::Readonly(Box::new(scal_int())) ); // …but TRANSPARENT to the int/never queries: `peel_view` strips it, so // narrowing/range/bottom see the inner type exactly as the old unwrap did. assert_eq!(ResolvedType::from_type_ref(&ro(int())).peel_view(), &scal_int()); assert_eq!(ResolvedType::from_type_ref(&ro(int())).int_width_sign(), Some((64, true))); // nested `readonly readonly T` peels fully. assert_eq!(ResolvedType::from_type_ref(&ro(ro(int()))).peel_view(), &scal_int()); // `readonly never` is still bottom after peeling (never-propagation unchanged). assert!(matches!( ResolvedType::from_type_ref(&ro(prim_ref("never", Span::dummy()))).peel_view(), ResolvedType::Never )); // `mut`/`unsafe` on a NON-pointer stay transparent (binding-level, not a readonly // content-view) — unchanged from pre-U.5.5. assert_eq!( ResolvedType::from_type_ref(&TypeRef::Mut(Box::new(int()), Span::dummy())), scal_int() ); // Query transparency through the view: sized-vs-wide-default + narrowing verdict // are identical to the bare inner type (the byte-identical gate for narrowing/D227). let ro_u8 = ResolvedType::from_type_ref(&ro(prim_ref("u8", Span::dummy()))); assert_eq!(ro_u8.sized_int_name(), Some("u8".to_string())); // sized → range-checked assert_eq!(ResolvedType::from_type_ref(&ro(int())).sized_int_name(), None); // wide default let ro_u32 = ResolvedType::from_type_ref(&ro(prim_ref("u32", Span::dummy()))); let i32t = ResolvedType::from_type_ref(&prim_ref("i32", Span::dummy())); assert!(ro_u32.would_narrow_into(&i32t)); // `readonly u32` → i32 narrows like `u32` } #[test] fn l3_typed_pointer_modifier_preserved() { use crate::ast::PointerModifier; let int = || prim_ref("int", Span::dummy()); let ptr = |inner| TypeRef::Pointer(Box::new(inner), Span::dummy()); let scal = || ResolvedType::Scalar { width: 64, signed: true, wide_default: true }; assert_eq!( ResolvedType::from_type_ref(&ptr(int())), ResolvedType::TypedPtr(PointerModifier::Ro, Box::new(scal())) ); assert_eq!( ResolvedType::from_type_ref(&TypeRef::Mut(Box::new(ptr(int())), Span::dummy())), ResolvedType::TypedPtr(PointerModifier::Mut, Box::new(scal())) ); assert_eq!( ResolvedType::from_type_ref(&TypeRef::Uninit(Box::new(ptr(int())), Span::dummy())), ResolvedType::TypedPtr(PointerModifier::Uninit, Box::new(scal())) ); // U.5.5(a) fidelity: the CANONICAL `*mut T` / `*unsafe T` spelling // `Pointer(Mut(T))` / `Pointer(Unsafe(T))` (Plan 131) — pre-U.5.5 these collapsed // to `TypedPtr(Ro)` (modifier lost → `const T*`). They now carry the modifier AND // agree with the `Mut(Pointer)` / `Unsafe(Pointer)` spelling above (no divergence). let mutw = |inner| TypeRef::Mut(Box::new(inner), Span::dummy()); let unsw = |inner| TypeRef::Uninit(Box::new(inner), Span::dummy()); assert_eq!( ResolvedType::from_type_ref(&ptr(mutw(int()))), ResolvedType::TypedPtr(PointerModifier::Mut, Box::new(scal())) ); assert_eq!( ResolvedType::from_type_ref(&ptr(unsw(int()))), ResolvedType::TypedPtr(PointerModifier::Uninit, Box::new(scal())) ); assert_eq!( ResolvedType::from_type_ref(&ptr(mutw(int()))), ResolvedType::from_type_ref(&mutw(ptr(int()))) ); } #[test] fn array_element_lossless() { // D239 `[]T ≡ Vec[T]` (D315 §0): the slice sugar canonicalizes to the NOMINAL // `Vec[T]` carrier (`Named{Vec}` with bare/empty module), element lossless. let arr = TypeRef::Array(Box::new(prim_ref("u32", Span::dummy())), Span::dummy()); assert_eq!( ResolvedType::from_type_ref(&arr), ResolvedType::Named { name: "Vec".to_string(), module: Vec::new(), args: vec![ResolvedType::Scalar { width: 32, signed: false, wide_default: false }], } ); } #[test] fn nested_array_canonicalizes_recursively() { // `[][]u8` → `Vec[Vec[u8]]` (recursive canonicalization, any nesting). let inner = TypeRef::Array(Box::new(prim_ref("u8", Span::dummy())), Span::dummy()); let outer = TypeRef::Array(Box::new(inner), Span::dummy()); let u8_scalar = ResolvedType::Scalar { width: 8, signed: false, wide_default: false }; let vec = |a: ResolvedType| ResolvedType::Named { name: "Vec".to_string(), module: Vec::new(), args: vec![a], }; assert_eq!(ResolvedType::from_type_ref(&outer), vec(vec(u8_scalar))); } #[test] fn int_with_type_args_is_named_not_scalar() { // `int[X]` (arity error elsewhere) must NOT become a bogus scalar — // mirrors int_width_rank's generics-guard. let int_x = TypeRef::Named { path: vec!["int".to_string()], generics: vec![prim_ref("X", Span::dummy())], span: Span::dummy(), }; // `int[X]` → `Named { "int", [X] }` (NOT a bogus scalar — generics-guard). assert!(matches!( ResolvedType::from_type_ref(&int_x), ResolvedType::Named { name, .. } if name == "int" )); } #[test] fn named_carries_module_identity_lossless() { // U.5.5(a): the carrier keeps the FULL syntactic identity — the qualifier prefix // (`module`) plus the `name` — so the C mangle reconstructs `path.join("_")` // (`Nova_<module…>_<name>*`) instead of dropping the module (the pre-U.5.5 // `path.last()` mis-mangled `Nova_mymod_Foo*` as `Nova_Foo*`). let named = |segs: &[&str], gen: Vec<TypeRef>| TypeRef::Named { path: segs.iter().map(|s| s.to_string()).collect(), generics: gen, span: Span::dummy(), }; // bare name → empty module. assert_eq!( ResolvedType::from_type_ref(&named(&["Foo"], vec![])), ResolvedType::Named { name: "Foo".into(), module: vec![], args: vec![] } ); // single-segment qualifier preserved. assert_eq!( ResolvedType::from_type_ref(&named(&["mymod", "Foo"], vec![])), ResolvedType::Named { name: "Foo".into(), module: vec!["mymod".into()], args: vec![] } ); // multi-segment qualifier preserved in order (full path = module ++ [name]). assert_eq!( ResolvedType::from_type_ref(&named(&["std", "collections", "Map"], vec![])), ResolvedType::Named { name: "Map".into(), module: vec!["std".into(), "collections".into()], args: vec![], } ); // generics recurse and carry their own identity; outer module preserved. assert_eq!( ResolvedType::from_type_ref(&named(&["mymod", "Box"], vec![prim_ref("int", Span::dummy())])), ResolvedType::Named { name: "Box".into(), module: vec!["mymod".into()], args: vec![ResolvedType::Scalar { width: 64, signed: true, wide_default: true }], } ); // CALIBRATION (byte-identical gate): `module` is invisible to category/identity // comparison — `cat_compatible_rt`/`distinct_mono` read `name` only, so a // module-qualified and a bare same-name type stay category-compatible and are NOT // a distinct-mono mismatch (the enrichment cannot shift any existing decision). let qual = ResolvedType::from_type_ref(&named(&["mymod", "Foo"], vec![])); let bare = ResolvedType::from_type_ref(&named(&["Foo"], vec![])); assert!(cat_compatible_rt(&qual, &bare)); assert!(!distinct_mono(&qual, &bare)); } #[test] fn func_effects_carry_type_args_lossless() { // U.5.5(c) (D315): the carrier keeps the FULL effect TYPE — `Fail[E]` keeps its // `E` — not a bare name (was `Vec<String>` taking `path.last()`, dropping the // generics). This is the D315 «несёт ПОЛНУЮ семантическую личность» requirement and // the un-blocker for typed-errors (Plan 173 Ф.4 `Fail[E]`-dispatch by `type_id`) + // `any`/`is` (Plan 175). The effect args stay INVISIBLE to category/identity // comparison (write-only today), so the enrichment is byte-identical by construction. let named = |segs: &[&str], gen: Vec<TypeRef>| TypeRef::Named { path: segs.iter().map(|s| s.to_string()).collect(), generics: gen, span: Span::dummy(), }; // `fn() Fail[MyErr]` — the `Fail` effect carries its `MyErr` type-arg losslessly. let func = TypeRef::Func { params: vec![], return_type: None, effects: vec![named(&["Fail"], vec![named(&["MyErr"], vec![])])], extern_abi: None, span: Span::dummy(), }; match ResolvedType::from_type_ref(&func) { ResolvedType::Func { effects, .. } => { assert_eq!(effects.len(), 1); assert_eq!( effects[0], ResolvedType::Named { name: "Fail".into(), module: vec![], args: vec![ResolvedType::Named { name: "MyErr".into(), module: vec![], args: vec![], }], }, "Fail[MyErr] effect must carry its `MyErr` type-arg (D315 lossless)" ); } other => panic!("expected Func, got {:?}", other), } // A module-qualified effect with a nested generic stays fully lossless: // `Db[std.Conn]` → `Named{name:Db, args:[Named{name:Conn, module:[std]}]}`. let func2 = TypeRef::Func { params: vec![], return_type: None, effects: vec![named(&["Db"], vec![named(&["std", "Conn"], vec![])])], extern_abi: None, span: Span::dummy(), }; if let ResolvedType::Func { effects, .. } = ResolvedType::from_type_ref(&func2) { assert_eq!( effects[0], ResolvedType::Named { name: "Db".into(), module: vec![], args: vec![ResolvedType::Named { name: "Conn".into(), module: vec!["std".into()], args: vec![], }], } ); } else { panic!("expected Func"); } } #[test] fn would_narrow_into_int_semantics() { // narrowing reads only (width, signed) — `wide_default` is irrelevant here. let s = |w, sg| ResolvedType::Scalar { width: w, signed: sg, wide_default: false }; // narrowing / value-range-unsafe: assert!(s(32, false).would_narrow_into(&s(32, true))); // u32 → i32 assert!(s(64, false).would_narrow_into(&s(64, true))); // u64 → int(i64) assert!(s(32, true).would_narrow_into(&s(32, false))); // i32 → u32 (signed→unsigned) assert!(s(64, true).would_narrow_into(&s(8, true))); // i64 → i8 // safe widening / identity: assert!(!s(8, false).would_narrow_into(&s(64, true))); // u8 → int (range fits) assert!(!s(64, true).would_narrow_into(&s(64, true))); // int ≡ i64 identity assert!(!s(8, true).would_narrow_into(&s(16, true))); // i8 → i16 assert!(!s(8, false).would_narrow_into(&s(16, false))); // u8 → u16 // over real TypeRefs (the narrowing surface `assignable` exercises) — these are // the exact pairs the deleted `is_int_narrowing` decided; pinned to the SAME // verdicts (the U.5.2 fold is byte-identical, also gated by the corpus run). let tr = |n| prim_ref(n, Span::dummy()); for (f, e, expect) in [ ("u32", "i32", true), // narrowing (same width, sign flip) ("u8", "int", false), // safe widening (range fits) ("i32", "u32", true), // signed → unsigned ("int", "i64", false), // int ≡ i64 identity ("u64", "int", true), // u64 → i64 narrowing ("u8", "u16", false), // safe widening ("i64", "i8", true), // narrowing ] { assert_eq!( ResolvedType::from_type_ref(&tr(f)) .would_narrow_into(&ResolvedType::from_type_ref(&tr(e))), expect, "would_narrow_into({f} → {e})" ); } } #[test] fn sized_int_name_distinguishes_wide_default() { let tr = |n| prim_ref(n, Span::dummy()); // The whole reason for `wide_default`: `int`/`uint` skip the range-check // (D227 Rule 1), the sized names are checked — a distinction `Scalar{w,s}` // alone could NOT express (`uint`≡`u64`≡(64,false)). Replaces the deleted // standalone `sized_int_name(&TypeRef)` — same verdicts, pinned directly. assert_eq!(r("int").sized_int_name(), None); assert_eq!(r("uint").sized_int_name(), None); assert_eq!(r("i64").sized_int_name(), Some("i64".to_string())); assert_eq!(r("u64").sized_int_name(), Some("u64".to_string())); assert_eq!(r("u8").sized_int_name(), Some("u8".to_string())); assert_eq!(r("i8").sized_int_name(), Some("i8".to_string())); assert_eq!(r("i16").sized_int_name(), Some("i16".to_string())); assert_eq!(r("i32").sized_int_name(), Some("i32".to_string())); assert_eq!(r("u16").sized_int_name(), Some("u16".to_string())); assert_eq!(r("u32").sized_int_name(), Some("u32".to_string())); assert_eq!(r("str").sized_int_name(), None); assert_eq!(r("bool").sized_int_name(), None); assert_eq!(r("Foo").sized_int_name(), None); // L2 view (ro/mut/unsafe) transparent — `from_type_ref` unwraps, so the method // sees the inner sized int (same as the deleted fn's unwrap). let ro_u8 = TypeRef::Readonly(Box::new(tr("u8")), Span::dummy()); assert_eq!( ResolvedType::from_type_ref(&ro_u8).sized_int_name(), Some("u8".to_string()) ); } #[test] fn int_name_full_table_incl_wide_defaults() { // int_name covers ALL int spellings (incl wide int/uint) — the name source for // the D227 unsigned-floor where sized_int_name(uint) is None. assert_eq!(r("int").int_name(), Some("int")); assert_eq!(r("uint").int_name(), Some("uint")); assert_eq!(r("i64").int_name(), Some("i64")); assert_eq!(r("u64").int_name(), Some("u64")); assert_eq!(r("i8").int_name(), Some("i8")); assert_eq!(r("u32").int_name(), Some("u32")); assert_eq!(r("str").int_name(), None); assert_eq!(r("Foo").int_name(), None); // round-trips through scalar_from_int_name (the single primitive table): for n in ["i8", "i16", "i32", "i64", "int", "u8", "u16", "u32", "u64", "uint"] { assert_eq!(r(n).int_name(), Some(n)); } } #[test] fn cat_compatible_rt_mirrors_legacy_arms() { use ResolvedType as R; let sc = |w, s| R::Scalar { width: w, signed: s, wide_default: false }; let nm = |n: &str| R::Named { name: n.to_string(), module: vec![], args: vec![] }; let f = |w| R::Float { width: w }; // `Any` permissive (= old `Other`): assert!(cat_compatible_rt(&R::Any, &R::Str)); assert!(cat_compatible_rt(&R::Bool, &R::Any)); // Scalar/Float permissive on width+sign (= old (Int,Int)/(Int,Float)) — narrowing // is decided SEPARATELY by `would_narrow_into`, not here: assert!(cat_compatible_rt(&sc(8, false), &sc(64, true))); assert!(cat_compatible_rt(&sc(64, false), &sc(8, true))); assert!(cat_compatible_rt(&sc(32, true), &f(64))); assert!(cat_compatible_rt(&f(32), &sc(16, false))); assert!(cat_compatible_rt(&f(32), &f(64))); // float width permissive here // same-kind primitives: assert!(cat_compatible_rt(&R::Bool, &R::Bool)); assert!(cat_compatible_rt(&R::Str, &R::Str)); assert!(cat_compatible_rt(&R::Unit, &R::Unit)); assert!(cat_compatible_rt(&R::Ptr, &R::Ptr)); // Named by name (incl. `char` riding as `Named { "char", .. }` = old `(Char,Char)`); // generic args ignored here (permissive) — exactness is the type-identity check: assert!(cat_compatible_rt(&nm("char"), &nm("char"))); assert!(cat_compatible_rt(&nm("Foo"), &nm("Foo"))); assert!(cat_compatible_rt( &R::Named { name: "Box".into(), module: vec![], args: vec![sc(64, true)] }, &R::Named { name: "Box".into(), module: vec![], args: vec![sc(32, false)] } )); // same NAME, different args → still compatible at this layer assert!(!cat_compatible_rt(&nm("Foo"), &nm("Bar"))); assert!(!cat_compatible_rt(&nm("char"), &R::Str)); // Array recurses on element: assert!(cat_compatible_rt( &R::Array(Box::new(sc(8, false))), &R::Array(Box::new(sc(64, true))) )); assert!(!cat_compatible_rt( &R::Array(Box::new(R::Bool)), &R::Array(Box::new(R::Str)) )); // mismatched kinds → false: assert!(!cat_compatible_rt(&R::Bool, &R::Str)); assert!(!cat_compatible_rt(&sc(8, true), &R::Bool)); } } /// Результат проверки модуля — карта имён top-level → тип. /// /// **D84 overloading:** `fns` хранит **Vec** для каждого имени, потому /// что одно имя может иметь несколько перегрузок (методы с одним именем /// на одном receiver-type, free-functions с разными signatures, разные /// `From[X]`). Резолв на call-site по argument-types — ответственность /// codegen / bound-checker. #[derive(Debug, Default)] pub struct ModuleEnv { pub types: HashMap<String, TypeDecl>, pub fns: HashMap<String, Vec<FnDecl>>, pub consts: HashMap<String, ConstDecl>, /// Plan 33.1 Ф.3: список доказанных (fn_name, contract span) контрактов. /// Codegen в release-сборке стирает соответствующие runtime-checks /// (zero-cost guarantee). В debug — checks всегда emit'ятся. pub proven_contracts: Vec<(String, Span)>, /// Plan 140.2 Part B (D257 / B.4): spans Index-сайтов, доказанных из LOOP/CODE /// (без contract-requires). Codegen элидит ВСЕГДА. pub proven_index_sites: Vec<Span>, /// Plan 140.2 followup §2: Index-сайты, доказанные ТОЛЬКО с fn-`requires`. /// Codegen элидит ТОЛЬКО при включённых контрактах (не `--contracts=off`). pub proven_index_sites_contract: Vec<Span>, /// Plan 140.4 ([M-opt-elide-proven-overflow-checks]): spans `int` `+`/`-`/`*`, /// доказанных в диапазоне i64 из LOOP/CODE. Codegen элидит `nova_int_checked_*` /// ВСЕГДА (safe даже под `--contracts=off`). pub proven_overflow_sites: Vec<Span>, /// Plan 140.4: `int`-арифм. сайты, доказанные ТОЛЬКО с fn-`requires`. Codegen /// элидит ТОЛЬКО при включённых контрактах (не `--contracts=off`). pub proven_overflow_sites_contract: Vec<Span>, /// Plan 172.1 U.4.1: per-Expr resolved-type annotations (ExprId → ResolvedType) /// from the semantic pass — codegen reads them instead of re-deriving /// (`infer_expr_c_type`, §0/§1). Part 2 seeds LITERALS via `number_exprs`; the /// checker annotates the rest in U.4.2+. pub resolved_types: HashMap<crate::ast::ExprId, ResolvedType>, /// №279 [M-nested-err-pattern-shared-variant-wrong-enum-tag]: per-pattern /// resolved-SUM-NAME channel (bare `Pattern::Variant`'s own `span` → the /// Nova sum type's simple name resolved structurally against the /// scrutinee). See `TypeCheckCtx::pattern_variant_types_buf` for the full /// rationale. Lifted from that buffer after the check pass (mirrors /// `resolved_types`). pub pattern_variant_types: HashMap<Span, String>, /// Plan 172.1 U.3.4: per-call resolved-CALLEE channel (call-site `ExprId` → chosen /// callee `FnDecl` declaration `Span`). The checker resolves each call's overload /// ONCE (it already does, for arg-checking) and records WHICH `FnDecl` it picked; /// codegen will READ this and lower (mangle) that callee instead of re-resolving the /// overload itself (§0 — the ~900-line Call-arm re-derivation, U.4.3). The callee /// identity is the declaration `Span` — a STABLE cross-layer key BOTH sides already /// hold (`&FnDecl`); codegen keeps doing its own (state-dependent / mono) mangling for /// the chosen `FnDecl`, so this does NOT require unifying the mangling schemes (U.2.4 /// is a SEPARATE concern, not a gate here). Populated for `.nv`-declared callees that /// the checker resolves UNAMBIGUOUSLY; intrinsics / unresolved stay codegen-side (gap, /// not wrong). U.3.4 = ADDITIVE substrate (this map is written but not yet read — /// byte-identical, mirrors U.4.1); U.4.3 wires the codegen consumption + equivalence /// assert. pub resolved_callees: HashMap<crate::ast::ExprId, Span>, /// Plan 196.5 Stage-A: per-call SUBST-VALUE channel (call-site `ExprId` → ordered /// `(generic-param name → concrete ResolvedType)`, declaration order). Lifted from /// `TypeCheckCtx.node_substs` after the check pass (mirrors `resolved_callees`). ADDITIVE /// substrate — codegen does not read it yet (Stage-B); mono-manging /// (`compute_mono_name`/`register_mono_method_instance`) will read it directly instead of /// re-deriving the subst from C-types (§0 — the "three hand-duplicated inference /// engines"). See `docs/plans/196.5-node-substs-channel.md`. pub node_substs: HashMap<crate::ast::ExprId, Vec<(String, ResolvedType)>>, /// Plan 104.10 Ф.2 (D379): OPT-IN per-expression type map for the IDE — expression /// source `Span` → its inferred `TypeRef` (the checker's real expression type, e.g. /// `int`, `str`, `Named{Rec}`, `Range`, `(int, str)`). Populated ONLY by /// [`check_module_with_expr_types`] (flag `TypeCheckCtx::record_expr_types`); the normal /// [`check_module`] leaves it EMPTY — zero cost for `nova check`/`build`/`test`. The IDE /// keys by cursor→span (hover, type-driven completion, signature help, inlay hints). /// /// SEMANTICS: absence of a span in the map means "unknown" — the IDE degrades /// gracefully. Synthetic (default / zero-width) spans and un-inferrable types (bare /// generic `T`, unit/never as a value) are intentionally NOT recorded, so the map never /// carries garbage. This is REAL inference lifted from the checker (`infer_expr_type` + /// the semantic `resolved_types_buf` channel), never a textual heuristic. pub expr_types: HashMap<Span, crate::ast::TypeRef>, } /// Минимальная проверка модуля. Регистрирует имена и базовую структуру — /// для bootstrap'а этого достаточно: интерпретатор ловит ошибки типов в /// runtime через match-mismatch и method-not-found. pub fn check_module(module: &Module) -> Result<ModuleEnv, Vec<Diagnostic>> { let (env, errors) = check_module_impl(module, None, false); if errors.is_empty() { Ok(env) } else { Err(errors) } } /// Plan 104.10 Ф.2 (D379): like [`check_module`], but ALSO records the per-expression type /// map (`ModuleEnv.expr_types`: expr `Span` → inferred `TypeRef`) for the IDE. This is the /// ONLY entry-point that fills `expr_types`; the plain [`check_module`] leaves it empty so the /// hot `nova check`/`build`/`test` path pays ZERO overhead. Same diagnostics / same checker /// decisions as [`check_module`] — the recording is a pure side-channel (`infer_expr_type` + /// the semantic `resolved_types_buf`), it never influences type-checking. Returns the env on /// success (with `expr_types` populated) or the diagnostics on failure. pub fn check_module_with_expr_types(module: &Module) -> Result<ModuleEnv, Vec<Diagnostic>> { let (env, errors) = check_module_impl(module, None, true); if errors.is_empty() { Ok(env) } else { Err(errors) } } /// Plan 104.10 Ф.5: **lenient** IDE variant of [`check_module_with_expr_types`]. /// /// Returns the [`ModuleEnv`] (with `expr_types` populated) **regardless of /// type-check diagnostics**, discarding the errors. This is exactly what /// interactive IDE features (type-driven method completion, hover on member /// access) need: the buffer under the cursor is almost always *momentarily /// invalid* mid-edit (a dangling `.`, a half-typed call), yet the receiver /// expression before the edit point already has a well-defined inferred type. /// /// The recording is a pure side-channel — the checker makes the *same* /// decisions as [`check_module`]; only the accept/reject envelope differs. Never /// used by the compile pipeline (which must reject invalid programs); only by /// nova-lsp, which prefers a best-effort partial `env` over nothing. pub fn check_module_with_expr_types_ide(module: &Module) -> ModuleEnv { check_module_impl(module, None, true).0 } /// Internal implementation shared by [`check_module`] and /// [`check_module_with_sig_table`]. `sig_table` is `None` in the /// default (no-I/O) path and `Some(table)` when the caller has /// already collected cross-module signatures. /// /// Plan 162.1 Step 3: the `sig_table` is threaded into: /// - `TypeCheckCtx::build_with_sig_table` → `is_known_type` / /// `is_known_fn` used in `verify_impl_protocols` to suppress false /// `E_UNKNOWN_PROTOCOL` when the protocol is from a transitively /// imported module not yet inline-merged. /// - `check_protocol_embeds` → suppresses `E_PROTOCOL_EMBED_UNKNOWN`. /// - `check_generic_bound_declarations` → suppresses `E_BOUND_UNKNOWN`. /// Coarse per-phase wall-clock instrumentation for `check_module_impl` /// (Plan 221.1 №437 — "the authoritative gate stopped finishing, find WHERE /// the time goes instead of guessing"). Disabled unless `NOVA_PERF` is set /// in the environment, so the release binary pays one `env::var` per /// compile unit and nothing else. struct PerfPhase { on: bool, t: std::time::Instant, } impl PerfPhase { fn new() -> Self { PerfPhase { on: std::env::var("NOVA_PERF").is_ok(), t: std::time::Instant::now() } } /// Print (and reset) the time spent since the previous mark. fn mark(&mut self, label: &str) { if self.on { eprintln!("[perf] {:>12.1}ms {}", self.t.elapsed().as_secs_f64() * 1000.0, label); } self.t = std::time::Instant::now(); } } fn check_module_impl( module: &Module, sig_table: Option<&crate::imports::ModuleSigTable>, record_expr_types: bool, ) -> (ModuleEnv, Vec<Diagnostic>) { let mut env = ModuleEnv::default(); let mut errors = Vec::new(); let mut names: HashSet<String> = HashSet::new(); let mut perf = PerfPhase::new(); // D82: `external fn` whitelisted только в `std/runtime/*.nv`. User-код // не должен использовать external — это keyword для документирования // stdlib runtime-функций, реализованных в nova_rt/*.h. Будущий // `extern("C")` для FFI к сторонним libs — отдельный keyword. // // Plan 42 Sub-plan 42.6: detect runtime module по обоих declaration // форматов (rev-1 legacy + rev-3 parent.X). Logic — в manifest helper. // // Plan 62.A: also whitelist `std.prelude.*` submodules. Prelude // sub-modules (`std/prelude/core.nv`, etc.) declare types/methods // implemented by codegen helpers in `nova_rt/*.h` — same pattern // as `std.runtime.*` (declaration-only, no Nova body). // // Plan 62.A (2026-05-18): check only items DECLARED HERE (in entry // peers' `items_here`), not items merged from imports. Otherwise // `external fn`-карта prelude'а проседает на каждом user-модуле: // user `module foo` импортирует `std.prelude` → prelude.core external // fns merge'нутся в `module.items` → check fires на foo. Items // источника prelude.core валидируются при компиляции САМОГО // prelude.core (отдельный `check_module` invocation на std). let is_runtime_module = crate::manifest::is_stdlib_runtime_module(&module.name) || crate::manifest::is_prelude_self_module(&module.name); if !is_runtime_module { // Collect entry peers' items_here (items declared в этом модуле // самим, не pulled через imports). Fallback на module.items если // peer_files пуст (legacy single-file). let entry_items: Vec<&Item> = if module.peer_files.is_empty() { module.items.iter().collect() } else { module.peer_files .iter() .filter(|pf| pf.is_entry_module) .flat_map(|pf| pf.items_here.iter()) .collect() }; for item in entry_items { if let Item::Fn(fd) = item { if fd.is_external { // Plan 91.10 (D163 retracted): `needs <Cap>` clause удалён. // // Plan 115 D214 amend D82 (2026-05-31): D82 restriction // "external fn only allowed in std.runtime.*" SNYATA. // Foundational FFI требует user-level `external fn` для // bindings к третьесторонним C libraries (libsqlite, libpng, // libcurl, etc) без участия compiler-team. User несёт // ответственность за: // - правильную C shim implementation (Layer 4), // - safe memory ownership (consume close() pattern), // - link-time provision shim object files (`nova build // --c-shim path/to/shim.c`). // // Verification: D214 §«Layered FFI pattern». Future // `[M-115-ffi-build-pipeline]` formalizes shim linking // CLI. // Plan 118 (D216 §20): `external fn ... Fail -> ...` — // E_EXTERNAL_FN_FAIL_EFFECT. C ABI не propagates Nova // exception machinery; Fail effect crossing FFI boundary // = undefined behavior (no DWARF unwinder hookup). User // должен catch внутри callback / wrapper, return sentinel. // V1 enforcement: rejected on declaration. Future // `extern "C-unwind"` (Rust 2024 model) — V2 research // [M-118-extern-c-unwind]. for eff in &fd.effects { if let TypeRef::Named { path, .. } = eff { if path.last().map_or(false, |n| n == "Fail") { errors.push(Diagnostic::new( format!( "[E_EXTERNAL_FN_FAIL_EFFECT] `external fn {}` declares `Fail` effect — \ not allowed на Nova→C FFI boundary (D216 §20). C runtime не propagates \ Nova exception machinery; throwing across C ABI = undefined behavior. \ Workaround: catch внутри wrapper, return sentinel value (e.g. negative \ error code) — let Nova-side caller convert sentinel к Fail. Future \ `extern \"C-unwind\"` (Rust 2024 model) — `[M-118-extern-c-unwind]`.", fd.name, ), eff.span(), )); } } } } } // D126 (Plan 62.D.bis) + D163 (Plan 100.5) both fully retracted: // - `external type X` — RETRACTED (E_EXTERNAL_TYPE_RETRACTED). Migration: `type X(ptr)`. // - `external type X consume` — RETRACTED (D163 removed). Migration: `type X value { priv handle int }`. // - `external fn` — RETRACTED (E_EXTERNAL_FN_RETRACTED) in parser. Use `extern "nova" fn`. if let Item::Type(td) = item { if matches!(td.kind, TypeDeclKind::Opaque) { let hint = if td.consume { format!( "[E_EXTERNAL_TYPE_RETRACTED] `external type {name} consume` (D163) retracted. \ Use: `type {name} consume value {{ priv handle int }}` (D241 canonical order: consume value priv). \ Add cleanup method: `fn {name} consume @close() -> () {{ ... }}`.", name = td.name ) } else { format!( "[E_EXTERNAL_TYPE_RETRACTED] `external type {name}` (D126) retracted. \ Use tuple-newtype: `type {name}(ptr)` (Plan 115 D214). \ Migration guide: docs/migration/d126-to-tuple-newtype.md.", name = td.name ) }; errors.push(Diagnostic::new(hint, td.span)); } } } } // Plan 163 Ф.1 (D282): E_REEXPORT_GLOB — запрет whole-module re-export. // `export import m` без `.{}` селектора = неконтролируемый barrel-реэкспорт; // гигиена поверхности имён требует явного именованного списка. // Разрешены: `export import m.{a, b}` (named). Forbidden: `export import m`. // Нулевая миграция: все 39 существующих `export import` уже именованные. // // Plan 163 Ф.2 (D282): E_IMPORT_GLOB — запрет whole-module import без alias. // `import m` без `.{}` селектора и без `as alias` = неконтролируемый glob; // загрязняет пространство имён неизвестным набором bare-имён. // Разрешены: `import m.{a, b}` (named), `import m as m` (qualified namespace). // Forbidden: `import m` (без selectors и без alias). // Exemption: prelude auto-imports (`std.prelude.*`) — служебный путь компилятора. // // Источник импортов: если peer_files заполнен (после resolve_imports_inline), // используем pf.imports каждого peer'а — они охватывают и entry и siblings. // Fallback на module.imports если peer_files пуст (legacy single-file без resolve). { let peer_imports_iter: Box<dyn Iterator<Item = &Import>> = if module.peer_files.is_empty() { Box::new(module.imports.iter()) } else { Box::new(module.peer_files.iter().flat_map(|pf| pf.imports.iter())) }; for imp in peer_imports_iter { if imp.is_export && imp.items.is_none() && imp.alias.is_none() { errors.push(Diagnostic::new( format!( "[E_REEXPORT_GLOB] `export import {}` re-exports the entire module \ without a name selector — this is a barrel re-export (Plan 163 / D282). \ Hint: use `export import {}.{{name1, name2}}` to re-export specific names.", imp.path.join("."), imp.path.join("."), ), imp.span, )); } // D289 amend: `import m` without `.{}` or `as` is legal — last segment // becomes the qualified namespace name (import vec_iter → vec_iter.Foo). // imported_modules already inserts path.last() unconditionally (Plan 81 Ф.2). // E_IMPORT_GLOB removed; only E_REEXPORT_GLOB (export form) remains. // // E_REDUNDANT_IMPORT_ALIAS: `import a.b.YYY as YYY` is forbidden — // alias equals last segment, which is the default; just write `import a.b.YYY`. if let Some(alias) = &imp.alias { if imp.items.is_none() { if let Some(last) = imp.path.last() { if alias == last { errors.push(Diagnostic::new( format!( "[E_REDUNDANT_IMPORT_ALIAS] `import {} as {}` — alias \ matches the last path segment, which is the default. \ Write `import {}` instead.", imp.path.join("."), alias, imp.path.join("."), ), imp.span, )); } } } } } } // Plan 62.D bis-1 (2026-05-18, D29 W_PRELUDE_SHADOW basic): // Determine which items in `module.items` came from imports (vs the // user's own entry file) — these are conflict candidates for D29 lint // and for the "codegen-completeness invisible merge" detection. // // The merge logic in `imports.rs` is two-phase: // - `merged_items` (→ `module.items`): ALL items from imported peer // modules are pulled in for codegen completeness (e.g. typedef'ы // should be available even if not selectively imported). This // causes apparent name conflicts when the user re-declares a name // that's in a merged-but-not-visible item. // - `imported_item_names` (per-peer): names actually VISIBLE to the // user via explicit imports + selective re-exports. This is the // proper "what does user see" set. // // D29 rule (W_PRELUDE_SHADOW basic): user declarations that conflict // with names brought in via prelude auto-import → warning (not error). // User declarations that conflict with codegen-only merged items (not // user-visible) → silently accept user's declaration. // // We collect entry-visible names via prelude into `prelude_visible_names`; // items in `module.items` NOT in this set AND NOT in user's own // `items_here` are codegen-only merges — silently allowed to be // shadowed by user code. // // Detection of "prelude-brought-in": // Pass 1: names declared directly in `std/prelude/*` or // `std/prelude.nv` peer files (items_here of those peers). // Pass 2: names re-exported through prelude facade via selective // `export import X.{A, B}` lists in prelude peer imports. The // re-exported alias (if any) is the visible name. // // Without this fix, enabling `export import // std.collections.range.{Range, RangeIter}` in `std/prelude.nv` broke // `nova_tests/syntax/for_in_range_iter.nv` (which locally declares // `type Range` and `type StepRangeIter`). // // Plan 62.F.bis Ф.2 (2026-05-18): visibility computation вынесена в // `lints::collect_prelude_visibility`. types::check_module использует // её для silent classify duplicate'ов (user-decl wins); structured // W_PRELUDE_SHADOW warning эмитится через `lints::lint_prelude_shadow` // — отдельно, в pipeline после check_module. Раньше eprintln здесь // дублировал диагностику; теперь silent — warnings приходят как // structured LintWarning через `cmd_check` warnings field. let prelude_vis = crate::lints::collect_prelude_visibility(module); // Classify a duplicate top-level name: // - `Some(true)` → name is visible via prelude → user-decl wins, // structured warning emitted by `lints::lint_prelude_shadow` // - `Some(false)` → name is merged-from-imports (codegen-only, not // user-visible) → silent (user wins) // - `None` → genuine duplicate (e.g. user code declared same name twice) // → error let classify_dup = |name: &str| -> Option<bool> { if prelude_vis.visible.contains(name) { Some(true) } else if prelude_vis.merged_from_imports.contains(name) { Some(false) } else { None } }; // Plan 154 [M-method-override-silent-noop]: типы, объявленные ЛОКАЛЬНО в // user-коде (entry peers). Если пользователь сам (пере)объявил `type Range`, // то `fn Range @step_by` — метод на ЕГО типе (legit shadow всего типа, // Plan 62 user-wins), а не silent-override prelude/built-in метода. Ошибку // E_METHOD_REDEFINITION шлём ТОЛЬКО для методов на типах, которые юзер НЕ // объявлял локально (str/Vec/… из prelude — footgun, см. ниже). let user_declared_types: std::collections::HashSet<String> = module .peer_files .iter() .filter(|pf| pf.is_entry_module) .flat_map(|pf| pf.items_here.iter()) .filter_map(|it| if let Item::Type(td) = it { Some(td.name.clone()) } else { None }) .collect(); // Plan 170 (D307): file-private dedup support. Two `priv(file)` symbols // with the SAME name in DIFFERENT peer files are NOT a conflict — they // occupy disjoint file-scopes. Collect, per top-level name, the set of // file_ids in which a `priv(file)` symbol of that name is declared. A // name-collision is тогда suppressed iff EVERY declaration carrying that // name (existing + new) is file-private AND they live in distinct files. // Module-private / export collisions remain errors as before (D281/D29). let mut file_priv_decl_files: std::collections::HashMap<String, std::collections::HashSet<FileId>> = std::collections::HashMap::new(); // Count of NON-file-private (module-default / export) decls per name — // if any exists, the name lives in the shared module namespace and a // collision is a genuine duplicate even if other copies are file-private. let mut non_file_priv_names: std::collections::HashSet<String> = std::collections::HashSet::new(); { let mut record = |name: &str, fp: bool, fid: FileId| { if fp { file_priv_decl_files.entry(name.to_string()).or_default().insert(fid); } else { non_file_priv_names.insert(name.to_string()); } }; for item in &module.items { match item { Item::Type(td) => record(&td.name, td.file_private, td.span.file_id), Item::Const(cd) => record(&cd.name, cd.file_private, cd.span.file_id), Item::Fn(fd) if fd.receiver.is_none() => { record(&fd.name, fd.file_private, fd.span.file_id); } _ => {} } } } // A name's collision is suppressible (file-private dedup) iff there is NO // non-file-private decl of that name AND it is file-private in ≥2 files. let is_file_priv_dedup = |name: &str| -> bool { !non_file_priv_names.contains(name) && file_priv_decl_files.get(name).map_or(false, |s| s.len() >= 2) }; for item in &module.items { match item { Item::Type(td) => { if !names.insert(td.name.clone()) { // Plan 170 (D307): file-private dedup — two `priv(file)` // types with the same name in different peer files are NOT // a conflict (disjoint file-scopes). Silently keep the // first registered (env.types is name-keyed; both share an // identical user-visible signature within their own file). if is_file_priv_dedup(&td.name) { continue; } // Plan 62.D bis-1: classify the duplicate per D29. match classify_dup(&td.name) { Some(true) => { // Visible via prelude → user-declaration wins // silently here; structured W_PRELUDE_SHADOW // warning emitted by `lints::lint_prelude_shadow` // (Plan 62.F.bis Ф.2 — see warnings field в // cmd_check для surface). User-decl still wins; // qualify as `std.prelude.<sub>.<name>` для // прямого доступа к prelude version. env.types.insert(td.name.clone(), td.clone()); continue; } Some(false) => { // Codegen-only merge (not user-visible). User's // declaration silently wins. env.types.insert(td.name.clone(), td.clone()); continue; } None => { errors.push(Diagnostic::new( format!("duplicate top-level name `{}`", td.name), td.span, )); } } } env.types.insert(td.name.clone(), td.clone()); } Item::Fn(fd) => { let key = match &fd.receiver { Some(r) => format!("{}.{}", r.type_name, fd.name), None => fd.name.clone(), }; // D84: overload по любой из четырёх осей (receiver-type, // arg-types, result-type, arity). Под одним именем может // быть несколько overloads, различающихся sig'ами; codegen // и bound-checker резолвят call-site по argument-types. // // Запрещено только **точное дублирование signature** // (одинаковые arity + одинаковые arg-types) — это была бы // ambiguity без возможности резолва. Проверка ниже. names.insert(key.clone()); // names — для конфликтов с типами/const'ами let entry = env.fns.entry(key.clone()).or_default(); // D84: overload-disambiguation по любой из четырёх осей. // Точное дублирование запрещено — это требует одновременного // совпадения **arity + arg-types + return-type** (плюс // receiver-type, который уже включён в `key`). Если хоть одна // ось различается — overload валиден. // Plan 135: receiver-mutability is a valid overload axis. // `fn T @m()` and `fn T mut @m()` are distinct overloads. let new_recv_mut = fd.receiver.as_ref().map(|r| r.mutable).unwrap_or(false); // Plan 184 (Р13/Р14): parameter MODE {ro,mut,consume} is a valid // overload axis too (unified with the receiver axis: `@` is the // zeroth parameter). `f(x T)` / `f(mut x T)` / `f(consume x T)` // are DISTINCT overloads — dispatch by argument-binding mutability // / last-use (D84 amendment). Two params are mode-equal iff same // `is_mut` AND same `consume`. let dup_existing = entry.iter().find(|existing| { // Plan 135: if receiver-mutability differs, NOT a duplicate. let existing_recv_mut = existing.receiver.as_ref().map(|r| r.mutable).unwrap_or(false); if existing_recv_mut != new_recv_mut { return false; } // Arity + arg-types + param-modes одинаковы? let args_equal = existing.params.len() == fd.params.len() && existing.params.iter().zip(fd.params.iter()) .all(|(p, np)| typeref_equal(&p.ty, &np.ty) && p.is_mut == np.is_mut && p.consume == np.consume); if !args_equal { return false; } // Return-type одинаков? (None / None или Some/Some equal). match (&existing.return_type, &fd.return_type) { (None, None) => true, (Some(a), Some(b)) => typeref_equal(a, b), _ => false, } }); if dup_existing.is_some() { // Plan 62.D bis-1: D29 — duplicate fn signature shadowing // a prelude-imported definition → warning (not error). // E.g. `fn Range @step_by(int) -> StepRangeIter` declared // in both user file and the merged-via-prelude // std/collections/range.nv. User wins. let dup_pos = entry.iter().position(|existing| { let existing_recv_mut = existing.receiver.as_ref().map(|r| r.mutable).unwrap_or(false); if existing_recv_mut != new_recv_mut { return false; } let args_equal = existing.params.len() == fd.params.len() && existing.params.iter().zip(fd.params.iter()) .all(|(p, np)| typeref_equal(&p.ty, &np.ty) && p.is_mut == np.is_mut && p.consume == np.consume); if !args_equal { return false; } match (&existing.return_type, &fd.return_type) { (None, None) => true, (Some(a), Some(b)) => typeref_equal(a, b), _ => false, } }); // Plan 154 [M-method-override-silent-noop]: переопределение // **метода** (receiver present) с той же сигнатурой, что у // метода из std/prelude/импортированного модуля — это SILENT // NO-OP: codegen `method_overloads` резолвит call-site // first-match, а prelude/std prepend'ится первым → выигрывает // существующее определение, тело пользователя НИКОГДА не // вызывается (проверено: `fn str @to_lower` → "ABC".to_lower() // печатает "abc", std-версия). Раньше — молча принималось // (Some(_) → user-wins в env.fns, но codegen игнорирует) → нет // диагностики. Теперь — ошибка: extension-метод должен иметь // другое имя ИЛИ отличаться сигнатурой (overload, D84); для // смены поведения типа — newtype + own-method (02-types §override // через own-methods). Type/const/free-fn shadowing — без изменений // (Plan 62 prelude-shadow: user-wins + W_PRELUDE_SHADOW). // // ИСКЛЮЧЕНИЕ (Plan 62): если receiver-тип объявлен ЛОКАЛЬНО юзером // (`user_declared_types`) — это методы на ЕГО (пере)объявленном // типе (shadow всего типа), не override чужого метода → НЕ ошибка // (напр. nova_tests/syntax/for_in_range_iter.nv: локальные // `type Range`+`fn Range @step_by`). // Two IDENTICAL `extern` declarations (same signature, same // C symbol) across peer files of one folder-module are NOT a // conflict — they are repeated forward-declarations of a // single external symbol. Codegen emits one prototype; the // duplicate is harmless. Silent-merge (keep one) instead of // erroring. Fires e.g. when two plan139 t4_* peers both // declare `extern "nova" fn p139_make_str() -> str`. let both_external = fd.is_external && dup_existing.map(|e| e.is_external).unwrap_or(false); if both_external { continue; } // Plan 170 (D307): file-private dedup — two `priv(file)` // free functions with the same name in different peer files // are NOT a conflict. Applies only to free fns (`key` == // bare name); methods carry a receiver-qualified key and are // out of scope for file-private. Codegen gives them distinct // C symbols (file-discriminator), so no link collision; the // first-registered overload is kept for type-checking. if fd.receiver.is_none() && is_file_priv_dedup(&key) { continue; } let is_method = fd.receiver.is_some(); let recv_user_local = fd.receiver.as_ref() .map(|r| user_declared_types.contains(&r.type_name)) .unwrap_or(false); match classify_dup(&key) { Some(_) if is_method && !recv_user_local => { errors.push(Diagnostic::new( format!( "[E_METHOD_REDEFINITION] метод `{}` уже определён \ (std/prelude или импортированный модуль); \ переопределение с той же сигнатурой молча игнорируется \ кодогеном (выигрывает существующее определение, тело \ не вызывается). Используй extension-метод с ДРУГИМ именем \ или overload с отличающейся сигнатурой (D84); для смены \ поведения типа — newtype + own-method.", key ), fd.span, )); continue; } Some(true) => { // Plan 62.F.bis Ф.2: silent user-wins; structured // W_PRELUDE_SHADOW warning эмитится через // `lints::lint_prelude_shadow`. (free-fn/type/const) if let Some(pos) = dup_pos { entry[pos] = fd.clone(); } continue; } Some(false) => { // Codegen-only merge — silent shadow. (free-fn/type/const) if let Some(pos) = dup_pos { entry[pos] = fd.clone(); } continue; } None => { errors.push(Diagnostic::new( format!( "duplicate definition `{}` with same signature \ (overload requires distinct param types, arity, или return type — \ см. D84); previous definition has identical params and return type", key ), fd.span, )); } } } else { entry.push(fd.clone()); } } Item::Const(cd) => { if !names.insert(cd.name.clone()) { // Plan 170 (D307): file-private dedup — two `priv(file)` // consts with the same name in different peer files are NOT // a conflict (disjoint file-scopes). Keep first registered. if is_file_priv_dedup(&cd.name) { continue; } // Plan 62.D bis-1: same prelude-shadow rule as for types. match classify_dup(&cd.name) { Some(true) => { // Plan 62.F.bis Ф.2: silent user-wins; structured // W_PRELUDE_SHADOW warning эмитится через // `lints::lint_prelude_shadow`. env.consts.insert(cd.name.clone(), cd.clone()); continue; } Some(false) => { env.consts.insert(cd.name.clone(), cd.clone()); continue; } None => { errors.push(Diagnostic::new( format!("duplicate top-level name `{}`", cd.name), cd.span, )); } } } env.consts.insert(cd.name.clone(), cd.clone()); } Item::Let(ld) => { // Plan 152.4 (D199 ro-runtime side): a module-level `ro NAME = EXPR` // is a lazy-static global. The strict const/ro partition // (`check_ro_module_partition`) forces a constexpr-eligible RHS to // `const`; a genuinely runtime RHS (call/effect/alloc) stays `ro` // and reaches here. Register it like a const so USES resolve with // the right type (incl. method receivers, e.g. `tbl.get(k)`). The // constexpr-check pass iterates only `Item::Const`, so a runtime // RHS registered here is not rejected. Single named binding only // (Ident / single-segment unit Variant for UPPER_CASE), non-ghost. if !ld.is_ghost { let name = match &ld.pattern { crate::ast::Pattern::Ident { name, .. } => Some(name.clone()), crate::ast::Pattern::Variant { path, kind: crate::ast::VariantPatternKind::Unit, .. } if path.len() == 1 => Some(path[0].clone()), _ => None, }; if let Some(name) = name { names.insert(name.clone()); env.consts.insert( name.clone(), crate::ast::ConstDecl { doc: None, doc_attrs: Vec::new(), is_export: false, name, ty: ld.ty.clone(), value: ld.value.clone(), span: ld.span, file_private: false, is_lazy_ro: true, }, ); } } } Item::Test(_) | Item::Bench(_) | Item::Lemma(_) => { // test/bench — регистрируются отдельно (имя — string-literal, не // идентификатор), конфликта по имени быть не может. // Ф.4.1: lemma — ghost, только для proof; не регистрируется в env. } } } // (typeref_equal — helper для D84 duplicate-signature detection, // определён в конце файла.) // Plan 15 (D72): generic bounds enforcement. // // Собираем protocol_specs (методы каждого protocol-типа) и // method_table (методы каждого concrete-типа). Затем ходим по // всем call-сайтам в bodies, для generic-вызовов с bounds // проверяем satisfaction concrete-аргументов. // Plan 101.4 (D145 Ред. 5): protocol-composition validation — // embed target существует и есть protocol; нет cycle; нет duplicate // signature collision при flatten'е. Запускается ДО BoundCtx::build // чтобы errors на cycle не превращались в infinite recursion внутри // flatten_dfs (хотя у flatten_dfs есть `seen`-guard — safety belt). check_protocol_embeds(module, sig_table, &mut errors); // Plan 101.3 (D145 Ред. 5): generic-bound declaration validation — // каждое имя bound'а в `[T A + B]` должно быть объявленным protocol'ом // (или well-known stdlib alias типа Hashable/Eq/Ord/Display). Раньше // bound-resolve был permissive (silent skip unknown) — Plan 101 делает // strict. Pre-Plan 101 tests, ссылающиеся на неизвестные bound'ы, // должны их объявить или удалить. check_generic_bound_declarations(module, sig_table, &mut errors); // Plan 172.1 U.2.3.2: ONE base signature registry, built once and shared by // BoundCtx / CapabilityCtx (and TypeCheckCtx, U.2.3.3) — replaces the three // duplicated `fn_decls`/`method_table` build loops (§0 single source). BASE // ONLY: synthesized auto-derive methods are a TypeCheckCtx-private overlay (F2), // so this shared registry is byte-identical to what Bound/Cap built themselves. let mut sig = crate::sig_registry::SigRegistry::build_base(module); let bound_ctx = BoundCtx::build(module, &sig); bound_ctx.check_module(module, &mut errors); // Plan 16 (D63 forbid + D64 realtime): capability enforcement. // // Walk fn bodies + tests, отслеживая forbidden-effects стек + // realtime-флаг. На каждом Call-сайте — проверка intersect'а // callee.effects с forbidden-set; в realtime — Net/Fs/Db/Time // suspend-effects запрещены; в `realtime nogc` — alloc-fn'ы // запрещены. Установка handler'а для forbidden-эффекта внутри // forbid-блока — error. let cap_ctx = CapabilityCtx::build(module, &sig); cap_ctx.check_module(module, &mut errors); // Plan 197 (--strict-effects, experimental, D62): E_EFFECT_ERASED_IN_FN_TYPE. // No-op (byte-identical) unless the CLI flag is set — see // `strict_effects::strict_effects_enabled()`. `E_UNDECLARED_TRANSITIVE_EFFECT` // (the flag's other diagnostic) is driven from inside `CapabilityCtx:: // check_callee_effects` just above, reusing its handler-scope tracking. crate::strict_effects::check_effect_erasure(module, &sig, &mut errors); // D90 Plan 20 Ф.3: defer/errdefer body constraints. // // Body запрещает: // - exit-control (return/throw/break/continue) — нельзя hijack // exit семантику scope'а. // - Fail-эффект (?/!!/throw) — double-throw невозможно сделать // корректно. throw обнаруживается через AST-walk; ?/!! — в codegen // они desugar'ятся в throw, поэтому достаточно catch throw. // - suspend-операции (Net.*, Fs.*, Db.*, Time.sleep, parallel for, // spawn, supervised, select) — defer должен быть быстрым cleanup. // // Walks по всем bodies всех функций. Spec — D90. perf.mark("pre-passes (decl scan .. before defer bodies)"); check_defer_bodies(module, &mut errors); perf.mark("check_defer_bodies (D90/D158)"); // D61 §1430-1434 / D90 Ф.8 (1): handler-method для эффект-операции // с return type `never` ОБЯЗАН закончиться exit-control'ом // (`interrupt v` или `throw err` / `panic` / `exit`). Рначе нет // значения типа never для возврата — handler не может законно // завершиться normally. // // Применяется к: Fail.fail (built-in, return never), любым // user-defined effect-operations с return type never. // // Walks все handler-литералы в module, проверяет для каждого // method'а, является ли соответствующая operation never-возврат- // ной, и если да — body должен diverge (static analysis). check_handler_never_ops(module, &mut errors); perf.mark("check_handler_never_ops"); // Plan 77 (D132): `-> @` fluent-return — тело метода обязано // вернуть `@`. Делает гарантию проверяемой для consume-checker. check_fluent_return(module, &mut errors); perf.mark("check_fluent_return"); // Plan 128 Ф.3 → RETIRED 2026-08-09 (owner decision, closes №468): // `check_primitive_mut_method`/E_PRIMITIVE_MUT_METHOD used to reject // `fn <primitive> mut @method(...)` because codegen passed the receiver // by value and silently dropped the mutation — R5 (`02-types.md:16101`) // says `mut @` is ALWAYS by-pointer regardless of size, so the ban was // an implementation limitation promoted to a language rule instead of // being fixed. `emit_c.rs::receiver_c_type`/`prepare_method_recv` now // pass primitive `mut @` receivers by pointer like every other type — // the form is legal and observably mutates the caller's binding. // Plan 73 (D131): consume-qualifier flow-sensitive check. Use-after- // consume и maybe-consumed (consume на части веток) → compile error. check_consume(module, &mut errors); perf.mark("check_consume (D131)"); // №325 Ш.0 barrier (hardcoded 8-name std collection list) REMOVED by // Ш.2 (D156-амендмент 2026-08-04, plan 246 step B): container-inherits- // linearity is now enforced generally inside `check_consume` itself — // ANY generic container (std or user-defined) instantiated with a // must-consume element type becomes itself a must-consume binding via // the ordinary D180 `consume`-keyword requirement (see // `ConsumeCtx::infer_let_type_ref` / `turbofish_ctor_type_ref` in this // file), discharged by the same three existing mechanisms as any other // consume-obligation: `for consume` iteration, pass-through, return. // Plan 127 Ф.4 (D228 amend): E_VALUE_RECORD_ESCAPE_AFTER_CONSUME — // hard error для `&v` после consume value-record local. Detects // syntactic pattern: a `consume v = ...` LetDecl (or method-call // consume of binding) followed by `&v` usage в той же fn-scope. More // specific message than generic D131 use-after-consume — helps user // понять value-record semantics. check_value_record_escape_after_consume(module, &mut errors); perf.mark("check_value_record_escape_after_consume"); // Plan 184 (Р8): `&@` (адрес value-приёмника = `ref Self`) эскейпящий // наружу (возврат / захват / поле) → E_REF_ESCAPE (авто-промоут D216 §4 // не спасает — ref на чужой слот). Downward-заём `&@`/`&v` в вызов — // легален (не флагаем). check_ref_addr_escape(module, &mut errors); perf.mark("check_ref_addr_escape"); // Plan 248 (wave 2, D447): `#no_copy` — «второе имя запрещено» для // Affine-типов. Отдельный (не flow-sensitive) проход — Affine не несёт // consume-обязанности, поэтому не переиспользует `check_consume`'s // Live/Consumed машину. См. доккомментарий `check_no_copy_second_name`. check_no_copy_second_name(module, &mut errors); perf.mark("check_no_copy_second_name"); // Plan 91.10 (D163 retracted, 2026-05-30): check_external_fn_needs_caps // удалён. Capability tracking via отдельный syntax — redundant с effect // system. См. docs/plans/91.10-d163-retract-capability-syntax.md. // Plan 33.3 Ф.9 (D24): validate axiom-bodies в effect-блоках. // Каждый axiom должен ссылаться только на binders + pure_view-ops // **того же эффекта** + литералы + boolean/arith operators. Любой // другой identifier (включая non-pure_view ops) → error. Это // фундамент SMT encoding (UF mapping в Ф.9.4). check_effect_axioms(module, &mut errors); perf.mark("check_effect_axioms"); // Plan 33.3 Ф.9.6: handler verification gate. // Если эффект имеет pure_view-ops, любая `with E = handler` для // этого эффекта обязана быть помечена `#verify_handler` или // `#trusted_handler`. Без атрибута — compile error. check_handler_verification_gate(module, &mut errors); perf.mark("check_handler_verification_gate"); // Name-resolution фаза: статический поиск undefined идентификаторов // в expr-position. Запускается ПОСЛЕ BoundCtx/CapabilityCtx, чтобы // более фундаментальные ошибки (signatures/effects) приходили первыми. // // Без этой фазы код вроде `let r = 1 | undefined_var` проходил // typecheck и падал только на cc-этапе с малочитаемой ошибкой // "необъявленный идентификатор". См. NameResCtx ниже. let name_res = NameResCtx::build(module); perf.mark("NameResCtx::build"); name_res.check_module(module, &mut errors); perf.mark("NameResCtx::check_module"); // Plan 33.1 Ф.2 (D24): contract checking + purity inference. // Минимальный pass: проверка базовых правил для контрактов: // - `result` запрещён в `requires`; // - `old(...)` запрещён в `requires`; // - composition (вызов другой fn в контракте) запрещён в 33.1 // (будет разрешён для #pure в 33.2). let contract_ctx = ContractCtx::build(module); perf.mark("ContractCtx::build"); contract_ctx.check_module(module, &mut errors); perf.mark("ContractCtx::check_module"); // Plan 33.3 Ф.9.7 (D24): ghost-var usage check. // Non-ghost код не может читать ghost-var (Verus/Dafny semantics). // До этого: catch'илось на C-level через «undeclared identifier»; // теперь — proper compile-error с понятным сообщением. check_ghost_usage(module, &mut errors); perf.mark("check_ghost_usage"); // Plan 52 Ф.2 (D108): map-литерал `[k: v]` type-checking. // // Focused expected-type проход: обходит fn-bodies/tests/consts, // протаскивая ожидаемый тип в let-аннотацию / return / argument- // позицию. На каждом `MapLit` — вывод `HashMap[K, V]` из ключей/ // значений (или из ожидаемого типа), enforce `K: Hashable`, // унификация ключей и значений. Пустой `[]` в позиции, ожидающей // `HashMap` — валиден; неоднозначный `[]` без типа — error. // Не заменяет существующие walk'и — отдельный проход (как // NameResCtx / ContractCtx), минимум регрессий. let mut map_lit_ctx = MapLitCtx::build(module); perf.mark("MapLitCtx::build"); map_lit_ctx.check_module(module, &mut errors); perf.mark("MapLitCtx::check_module"); // Plan 79: type-checker hardening — «no silent fallback» на уровне // типов. Отдельный проход (паттерн NameResCtx / MapLitCtx): доводит // type-checker до типовой полноты. Ф.2 — арность type-аргументов. // Plan 126.2 Ф.1: arena for synthesized auto-derive FnDecls. Created here // (in the caller) so it outlives `TypeCheckCtx`, allowing synthesized // methods to be registered in `method_table` as `&'a FnDecl`. let synth_arena = FnDeclArena::new(); // Plan 172.1.1 (U.1): merge registry-only builtin method sigs (StringBuilder/WriteBuffer/ // ReadBuffer) into `sig` for the TypeCheckCtx pass — paired with the TYPE merge in // TypeCheckCtx::build (so the checker knows them as types AND has their method sigs → resolves // their call callees → Call-channel, §0.7). AFTER Bound/Cap/contract/map_lit (byte-identical // base) so ONLY the type-check pass sees the extra sigs. ADDITIVE (no load_builtins removal, §10). for ext_mod in crate::codegen::external_registry::builtin_sig_modules() { sig.merge_module_fns(ext_mod); } // Plan 162.1 Step 3: when a sig_table is available, use // build_with_sig_table so that is_known_type / is_known_fn // can consult cross-module signatures during type-checking. perf.mark("rest of mid-passes (hardening/sig-merge)"); let mut type_check_ctx = match sig_table { Some(st) => TypeCheckCtx::build_with_sig_table(module, &synth_arena, st.clone(), &sig), None => TypeCheckCtx::build(module, &synth_arena, &sig), }; perf.mark("TypeCheckCtx::build"); // Plan 104.10 Ф.2: opt-in per-expression type recording (IDE). Default path leaves this // false → the f1_expr walk never records → zero overhead for nova check/build/test. type_check_ctx.record_expr_types = record_expr_types; type_check_ctx.check_module(module, &mut errors); perf.mark("TypeCheckCtx::check_module (main inference pass)"); // **Plan 118.5 V3 Ф.2 / D216 V3 §V3.1 (2026-06-04):** ro+mut conflict // check (storage-class-aware). Walks all param types, return types, // and field types для each Fn/TypeDecl item; emits // E_MUTABILITY_CONFLICT_VALUE_TYPE для value-T cases. // // Reuses TypeCheckCtx's `types` registry для storage-class detection. for item in &module.items { match item { Item::Fn(fd) => { for p in &fd.params { check_v3_ro_mut_conflict( &p.ty, &type_check_ctx.types, true, &mut errors); } if let Some(rt) = &fd.return_type { check_v3_ro_mut_conflict( rt, &type_check_ctx.types, true, &mut errors); } } Item::Type(td) => { if let crate::ast::TypeDeclKind::Record(fields) = &td.kind { for f in fields { check_v3_ro_mut_conflict( &f.ty, &type_check_ctx.types, true, &mut errors); } } if let crate::ast::TypeDeclKind::NamedTuple(fields) = &td.kind { for f in fields { check_v3_ro_mut_conflict( &f.ty, &type_check_ctx.types, true, &mut errors); } } } _ => {} } } // Plan 114.4.2 (D199) Ф.1 const fn body check pass. // 1) Collect const fn names. 2) Validate each const fn body против // V1 whitelist (literals/arithmetic/casts/refs to params/locals/const // fn calls). 3) Build call-graph and detect cycles (mutual recursion). { use std::collections::{HashMap as Map, HashSet as Set}; // Plan 114.4.3 Ф.3 (V2 mixed-args): два set'а — // * const_fn_names: ВСЕ fn с const-surface (any const param OR const // return) — для call-site arg validation. Includes mixed fns. // * fully_const_fn_names: ВСЕ params const AND return const — // evaluator-inlined + dropped из codegen. V2 body whitelist // applied только к этим (mixed fns — runtime body, normal rules). let mut const_fn_names: Set<String> = Set::new(); let mut fully_const_fn_names: Set<String> = Set::new(); let mut fully_const_fns: Vec<&FnDecl> = Vec::new(); for item in &module.items { if let Item::Fn(fd) = item { let any_const = fd.return_is_const || fd.params.iter().any(|p| p.is_const); let all_const_params = !fd.params.is_empty() && fd.params.iter().all(|p| p.is_const); let is_fully_const = (all_const_params || fd.params.is_empty()) && fd.return_is_const; if any_const { const_fn_names.insert(fd.name.clone()); } if is_fully_const { fully_const_fn_names.insert(fd.name.clone()); fully_const_fns.push(fd); } } } let mut call_graph: Map<String, Set<String>> = Map::new(); for fd in &fully_const_fns { let mut targets: Set<String> = Set::new(); // Body checker against fully-const fn set (для transitivity: // fully-const fn calling mixed fn = forbidden, runtime escapes). if let Err(d) = check_const_fn_decl(fd, &fully_const_fn_names, &mut targets) { errors.push(d); } call_graph.insert(fd.name.clone(), targets); } // Cycle detection: DFS с three-color marker (WHITE/GRAY/BLACK). // GRAY → ребро в текущий путь → cycle. #[derive(Clone, Copy, PartialEq)] enum C { White, Gray, Black } let mut color: Map<String, C> = Map::new(); for k in const_fn_names.iter() { color.insert(k.clone(), C::White); } fn visit( node: &str, graph: &Map<String, Set<String>>, color: &mut Map<String, C>, ) -> Option<Vec<String>> { color.insert(node.to_string(), C::Gray); if let Some(neigh) = graph.get(node) { for n in neigh { match color.get(n).copied().unwrap_or(C::White) { C::Gray => return Some(vec![node.to_string(), n.clone()]), C::White => { if let Some(mut path) = visit(n, graph, color) { path.insert(0, node.to_string()); return Some(path); } } C::Black => {} } } } color.insert(node.to_string(), C::Black); None } // Plan 114.4.3 Ф.2 (V2): recursion (direct + mutual) allowed. // Cycle detection retained but downgraded — no error fired. // Evaluator enforces depth-limit + memoization runtime safety. let mut reported: Set<String> = Set::new(); for fd in &fully_const_fns { if matches!(color.get(&fd.name), Some(C::White)) { if let Some(cycle) = visit(&fd.name, &call_graph, &mut color) { let key = { let mut v = cycle.clone(); v.sort(); v.join("→") }; // V2: cycle reported only once per cycle (dedup), // no error emitted — evaluator depth-limit enforces. let _ = reported.insert(key); let _ = cycle; if false { errors.push(Diagnostic::new( format!( "[E_CONST_FN_RECURSION] cycle detection: {}", fd.name ), fd.span, )); } } } } } // Plan 33.1 Ф.3 (D24): SMT verification. // TrivialBackend по умолчанию (Z3 — отдельная feature в будущем). // Доказанные контракты записываются в env для zero-cost release. // `#must_verify` errors / counterexample warnings — попадают в errors. // ПЕРФ (реестр 221.1 №437, решение владельца 2026-08-08 «выключи проверку по // умолчанию, пусть всё в рантайме проверяется»): `verify_module` гонялся на // КАЖДОМ модуле безусловно, включая модули БЕЗ единого контракта. Пофазный // замер (`NOVA_PERF=1`, 5 conformance-файлов): SMT + хвост — 10 013 мс, 47,1 % // всего времени чекера, при том что в этих файлах контрактов нет вовсе. // // ПОЧЕМУ ПРОПУСК БЕЗОПАСЕН ПО ПОСТРОЕНИЮ: результат верификации используется // ТОЛЬКО чтобы УБРАТЬ рантайм-проверки (`proven_contracts`, // `proven_index_sites`, `proven_overflow_sites` → zero-cost release). Нет // доказательств → ничего не элидируется → ВСЕ проверки остаются в рантайме. // Пропуск не может сделать программу менее безопасной, только менее быстрой // на исполнении — ровно тот размен, который выбрал владелец. // // УПРАВЛЕНИЕ — ЯВНЫМ ФЛАГОМ, а не догадкой по содержимому модуля (решение // владельца 2026-08-08). Первая редакция интегратора включала SMT // автоматически, если в модуле есть `requires`/`ensures`/`invariant` — от // этого отказались: поведение зависело бы от того, что написано ВНУТРИ файла, // и человек, дописавший один контракт, получал бы кратное замедление сборки // без единого намёка почему. Флаг предсказуем: включает тот, кто просит. // // ПО УМОЛЧАНИЮ ВЫКЛЮЧЕНО. Включение: `nova build|check --verify` // (пробрасывается как `NOVA_VERIFY=1`). let verify_enabled = std::env::var("NOVA_VERIFY").ok().as_deref() == Some("1"); // ЗАМЕР: закрыть интервал ДО верификации ВСЕГДА, а не только когда она // включена. Прежняя редакция ставила эту метку ВНУТРЬ if-блока — при // пропуске верификации метка не срабатывала, и следующая (ниже) охватывала // ВЕСЬ хвост, приписывая себе чужую работу. Интегратор прочитал это как // «SMT = 47 % времени чекера» и доложил владельцу; честная проверка флагом // дала совсем другой порядок. Метка, охватывающая больше, чем обещает её // имя, — источник ложных выводов, а не измерение. perf.mark("post-check passes (before verify)"); if errors.is_empty() && verify_enabled { // Verify только если предыдущие фазы прошли (иначе encode на // невалидном AST может крашнуть). let report = crate::verify::verify_module(module); env.proven_contracts = report.proven; env.proven_index_sites = report.proven_index_sites; env.proven_index_sites_contract = report.proven_index_sites_contract; env.proven_overflow_sites = report.proven_overflow_sites; env.proven_overflow_sites_contract = report.proven_overflow_sites_contract; for e in report.errors { errors.push(e); } // warnings пока silent — добавим warning infrastructure // в Plan 36 production hardening. // Note: counterexample-warnings (без #must_verify) бэк-port'ятся // в errors временно, чтобы в 33.1 negative-тесты могли их детектить. // Это будет уточнено когда добавится warning severity (Plan 36). let _ = report.warnings; // intentionally silent } // ЗАМЕР: отдельная метка РОВНО на верификацию — интервал от метки выше и // до этой строки содержит ТОЛЬКО `verify_module`, ничего больше. Когда // верификация выключена (по умолчанию), интервал близок к нулю, и это // честно видно в выводе `NOVA_PERF=1`, а не растворяется в соседней фазе. perf.mark(if verify_enabled { "verify_module (SMT) — ТОЛЬКО верификация, --verify включён" } else { "verify_module (SMT) — ПРОПУЩЕНА (--verify выключен, по умолчанию)" }); // Plan 118 D216 §8 (Ф.3.5): enforce E_UNSAFE_REQUIRED — `&value` / // `*expr` pointer ops require unsafe context (block.is_unsafe = true // OR enclosing #unsafe fn). Walks fn bodies + test bodies, maintains // depth counter, emits diagnostic при depth == 0. // ПЕРЕИМЕНОВАНО 2026-08-08: было «verify_module (SMT) + tail of post-check // passes» — имя приписывало верификации ЧУЖОЕ время (хвост пост-проходов), // и это стоило владельцу ложного доклада «SMT = 47 %». Верификация теперь // меряется своей меткой выше; здесь — только то, что здесь и происходит. perf.mark("tail of post-check passes (после verify, до unsafe-context)"); check_unsafe_context_in_module(module, &type_check_ctx.resolved_callees.borrow(), &mut errors); perf.mark("check_unsafe_context_in_module"); // Plan 221.1 п.11 №428 (D62/№113 "форма vs свойство"): `[E_BANG_ // REQUIRES_FAIL]` at the `export fn` boundary, computed over the // TRANSITIVE closure of the resolved call graph (any depth of private- // helper chaining, methods, generics, closures, handler-literal op // bodies) — not just literal `!!` tokens written directly in an export // fn's own body (the OLD per-fn walker that used to live in `check_fn` // above; moved HERE because it needs the FINAL `resolved_callees`, same // requirement `fiber_safety`/`check_unsafe_context_in_module` already // have). See `fail_reach.rs`'s own module doc for the full design. fail_reach::run(module, &type_check_ctx.resolved_callees.borrow(), &mut errors); perf.mark("fail_reach::run (№428)"); // Plan 238 Ф.1 (D446 "Ф.8-НОВАЯ"): total per-fn/method M:N-safety tag, // built over the NOW-FINAL `resolved_callees` (must run after the main // check pass populates it, same requirement `check_unsafe_context_in_ // module` above already has). Prints nothing unless // `NOVA_DEBUG_FIBER_SAFETY=1`. let fiber_safety_tags = fiber_safety::run(module, &type_check_ctx.resolved_callees.borrow()); perf.mark("fiber_safety::run (238 Ф.1)"); // Plan 238 Ф.2 (D446 "Ф.8-НОВАЯ" П.2, seeding-point enforcement): every // call reached inside a `spawn`/`detach`/`parallel for` body must // resolve to a `Safe`-tagged fn/method — an indirect call (D446 §4) or // a call whose tag is `Unsafe`/`Undecided` is a hard error // (`E_FIBER_UNSAFE_CALL`/`E_FIBER_INDIRECT_CALL`). Checker-channel only // (§0/196) — reuses the SAME `resolved_callees`/`fiber_safety_tags` this // module already computed, emits into the SAME `errors` every other // checker pass in this fn writes to. fiber_safety::check_seed_points(module, &type_check_ctx.resolved_callees.borrow(), &fiber_safety_tags, &mut errors); perf.mark("fiber_safety::check_seed_points (238 Ф.2)"); // Plan 238 Ф.3 (D446 §4/§5 амендмент, owner decision 2026-08-06): // AUTOMATIC per-parameter safety-requirement inference + exact // enforcement at the PASSING site (`E_FIBER_UNSAFE_ARG`) — see // `fiber_safety.rs`'s own "Plan 238 Ф.3" module-doc section for the // full design. Same checker-channel-only discipline as Ф.1/Ф.2 above; // additive, reuses the SAME `resolved_callees`/`fiber_safety_tags`. let fiber_required_params = fiber_safety::compute_required_params(module, &type_check_ctx.resolved_callees.borrow()); perf.mark("fiber_safety::compute_required_params (238 Ф.3)"); fiber_safety::check_param_passing( module, &type_check_ctx.resolved_callees.borrow(), &type_check_ctx.resolved_types_buf.borrow(), &fiber_safety_tags, &fiber_required_params, &mut errors, ); perf.mark("fiber_safety::check_param_passing (238 Ф.3)"); // Plan 174.6 M1 (D282 rule 2 / D353): validate that `extern "C" fn` // signatures (params + return) — and every `*extern "C" fn` fn-pointer // signature — use only C-ABI-compatible types. Non-C-ABI types (GC // records, `Vec`, `Result`, `Option[non-ptr]`, Nova-ABI `*fn`, …) → // `E_FFI_NON_C_ABI_TYPE`. Pure checker-channel diagnostic (§1/§6): the // C-ABI grammar is read off the TYPE structure, never off type names. check_ffi_c_abi_signatures(module, &mut errors); // Plan 175 Ф.2-v2: `#default_handler(X)` validation (duplicate/arity/ // return-type/unknown-effect/cross-default cycle). See // `check_default_handlers` below. check_default_handlers(module, &mut errors); // Plan 175.2 Ф.2-v4 (П4, D-амендмент): handler-literal op declarations // must be fully typed (`-> Type` mandatory, must match effect schema). check_handler_op_declarations(module, &mut errors); // Plan 172.1 U.3.4: lift the per-call resolved-callee channel (call-site `ExprId` → // chosen callee `FnDecl` span) out of the checker into the `ModuleEnv` so the pipeline // hands it to codegen. ADDITIVE substrate — codegen does not read it yet (U.4.3), so // byte-identical. Populated even when there are errors (harmless; only read on success). env.resolved_callees = type_check_ctx.resolved_callees.take(); // Plan 196.5 Stage-A: lift the per-call subst-value channel out of the checker (mirrors // resolved_callees above). ADDITIVE — not read by codegen yet. env.node_substs = type_check_ctx.node_substs.take(); // Plan 172.1 U.4.4(b): lift the checker-side resolved-type channel; the pipeline merges // it OVER the number_exprs seed (main.rs / test_runner). env.resolved_types = type_check_ctx.resolved_types_buf.take(); // №279: lift the pattern-variant resolved-sum-name channel (mirrors resolved_types). env.pattern_variant_types = type_check_ctx.pattern_variant_types_buf.take(); // Plan 104.10 Ф.2 (D379): lift the opt-in IDE per-expression type map. Empty unless // `record_expr_types` was set (i.e. via check_module_with_expr_types) — zero-overhead // guarantee for the normal compile path. env.expr_types = type_check_ctx.expr_types_buf.take(); perf.mark("tail passes (after fiber-safety .. end of check_module_impl)"); // Return env + errors unconditionally; Result-returning public wrappers // convert (Ok/Err), while the lenient IDE entry point keeps the env. (env, errors) } // Plan 162.1 Step 3: public entry-point that feeds a pre-built // `ModuleSigTable` into the type-checker. Callers that have already // run `crate::imports::collect_all_signatures` can pass the resulting // table here so that cross-module symbol lookups in the type-checker // use the sig-table instead of only consulting `module.items`. // // Concretely, the sig_table suppresses false-positive // `E_UNKNOWN_PROTOCOL` / `E_PROTOCOL_EMBED_UNKNOWN` / `E_BOUND_UNKNOWN` // errors when the referenced type is declared in a transitively // imported module that has been captured in the signature pre-pass // but whose items may not yet be inline-merged into `module.items` // (the lazy-resolution scenario described in Plan 162.1 Step 3). // // The existing `check_module` is kept as the zero-arg entry-point // (backward-compatible); it uses an empty sig_table so that all // existing checks fire normally. pub fn check_module_with_sig_table( module: &Module, sig_table: crate::imports::ModuleSigTable, ) -> Result<ModuleEnv, Vec<Diagnostic>> { let (env, errors) = check_module_impl(module, Some(&sig_table), false); if errors.is_empty() { Ok(env) } else { Err(errors) } } // ─── internal shared core ──────────────────────────────────────────────────── // ============================================================================ // Plan 79: type-checker hardening — «no silent fallback» на уровне типов. // // Type-checker bootstrap'а проверяет имена/структуру/эффекты/контракты, но НЕ // базовую совместимость типов. Plan 79 доводит его до типовой полноты: каждое // использование типа проверяется, несовместимость → compile-error (серия // E73xx) вместо silent miscompilation или поздней CC-FAIL. // // Ф.2 — арность type-аргументов (`Result[int]` → E7310). [реализовано] // Ф.1 — assignability arg↔param и annotation↔RHS. [pending] // Ф.3 — существование поля / варианта. [pending] // Ф.4 — type-vs-value. [pending] // // Отдельный проход `TypeCheckCtx` (паттерн NameResCtx / ContractCtx / // MapLitCtx) — растёт по фазам, минимум регрессий к существующим walk'ам. // ============================================================================ /// Объявленная арность generic-типа. struct ArityInfo { /// Число объявленных generic-параметров. count: usize, /// Span объявления `type` — для note «declared here». `None` у /// built-in типов (Option/Result/примитивы), чьё объявление не /// находится в текущем модуле. decl_span: Option<Span>, } /// Plan 114.4 Ф.1: constexpr-eligibility check для `const X = expr`. /// /// Проверяет рекурсивно что RHS — literal-eligible: literals + арифметика /// над constexpr operands + record/tuple/array literals из constexpr-полей + /// references на другие top-level `const`. /// /// Возвращает `Err(Diagnostic)` если non-constexpr: /// - `E_CONST_NOT_CONSTEXPR` — generic non-constexpr expr. /// - `E_CONST_REFERS_NON_CONSTEXPR` — Ident на non-const binding. /// - `E_CONST_EFFECT_IN_INIT` — runtime call / effect / allocation. /// /// `known_consts` — set имен top-level `const` (для Ident-резолва). fn check_const_constexpr( expr: &crate::ast::Expr, known_consts: &HashSet<String>, ) -> Result<(), Diagnostic> { let empty: HashSet<String> = HashSet::new(); check_const_constexpr_ex(expr, known_consts, &empty, &empty) } /// Plan 114.4.2 (D199): extended constexpr validator с awareness of /// const fn names. Used by check_module's scope-local const validation /// + module-level const validation when const fn registry уже built. /// `const_fn_names` — set of all const fn names в module (если empty — /// backward-compatible с original check_const_constexpr behavior). /// `named_tuple_names` — set of named tuple type names; their constructors /// are constexpr (D215 amend: pure value construction, no side effects). fn check_const_constexpr_ex( expr: &crate::ast::Expr, known_consts: &HashSet<String>, const_fn_names: &HashSet<String>, named_tuple_names: &HashSet<String>, ) -> Result<(), Diagnostic> { use crate::ast::ExprKind as E; match &expr.kind { // Literals — всегда constexpr. E::IntLit(_) | E::FloatLit(_) | E::StrLit(_) | E::BoolLit(_) | E::CharLit(_) | E::UnitLit => Ok(()), // Unary над constexpr operand. E::Unary { operand, .. } => check_const_constexpr_ex(operand, known_consts, const_fn_names, named_tuple_names), // Binary над constexpr operands. E::Binary { left, right, .. } => { check_const_constexpr_ex(left, known_consts, const_fn_names, named_tuple_names)?; check_const_constexpr_ex(right, known_consts, const_fn_names, named_tuple_names) } // Plan 114.4.2 D199: `as`-cast — constexpr if inner is constexpr. E::As(inner, _) => check_const_constexpr_ex(inner, known_consts, const_fn_names, named_tuple_names), // Tuple-литерал — каждый элемент constexpr. E::TupleLit(elems) => { for e in elems { check_const_constexpr_ex(e, known_consts, const_fn_names, named_tuple_names)?; } Ok(()) } // Array-литерал (без spread) — каждый элемент constexpr. E::ArrayLit(elems) => { for el in elems { match el { crate::ast::ArrayElem::Item(e) => check_const_constexpr_ex(e, known_consts, const_fn_names, named_tuple_names)?, crate::ast::ArrayElem::Spread(_) => { return Err(Diagnostic::new( "[E_CONST_NOT_CONSTEXPR] spread `...` not allowed \ в const initialiser — runtime operation. Inline \ literals или use `ro X = …` for runtime value \ (Plan 114.4 Ф.1 D199).".to_string(), expr.span, )); } } } Ok(()) } // Record-литерал — каждое поле constexpr. E::RecordLit { fields, .. } => { for f in fields { if f.is_spread { return Err(Diagnostic::new( "[E_CONST_NOT_CONSTEXPR] spread `...` not allowed в \ const-record initialiser (Plan 114.4 Ф.1).".to_string(), expr.span, )); } match &f.value { Some(v) => check_const_constexpr_ex(v, known_consts, const_fn_names, named_tuple_names)?, None => { // Shorthand `{ name }` — refers binding called `name`. if !known_consts.contains(&f.name) { return Err(Diagnostic::new( format!( "[E_CONST_REFERS_NON_CONSTEXPR] field shorthand `{}` \ в const-record refers binding which is not a \ top-level const. Use explicit `{}: <literal>` либо \ declare referenced `const {}` (Plan 114.4 Ф.1).", f.name, f.name, f.name ), expr.span, )); } } } } Ok(()) } // Ident — должен ссылаться на другой known top-level `const` // ИЛИ const fn (Plan 114.4.3 Ф.5 V2: first-class alias). E::Ident(name) => { if known_consts.contains(name) || const_fn_names.contains(name) { Ok(()) } else { Err(Diagnostic::new( format!( "[E_CONST_REFERS_NON_CONSTEXPR] `const` initialiser \ refers `{}` which is not a top-level `const` or `const fn`. \ Only literals + arithmetic on literals + record/tuple/array \ literals из constexpr fields + references to other \ `const` / `const fn` are allowed. For runtime / lazy-init \ use `ro {} = …` (Plan 114.4 Ф.1 / D199).", name, name ), expr.span, )) } } // Path (e.g. `Module.NAME` cross-module const, или `LOCAL.field` // member-access на local-const). V1 conservative: запрещаем все // Path формы в const-RHS (cross-module — followup // [M-114.4-cross-module-const-ref]; field-access на local-const — // runtime-only, эквивалент `ro X = LOCAL.field`). E::Path(_) => Err(Diagnostic::new( "[E_CONST_NOT_CONSTEXPR] path expression (Module.NAME / Type.field) \ not allowed в `const` initialiser в V1. Cross-module const refs — \ followup [M-114.4-cross-module-const-ref]. Field access на local \ const → use `ro X = …` (runtime ok) (Plan 114.4 Ф.1).".to_string(), expr.span, )), // Plan 114.4.2 D199 / Plan 114.4.3 Ф.4 V2: Call к const fn — constexpr, // если callee = Ident (или TurboFish<Ident, ...> для generic const fn) // и зарегистрирован как const fn, и каждый arg constexpr. // D215 amend: named tuple constructors `T(...)` are also constexpr // (pure value construction, no side effects — same as record literals). E::Call { func, args, trailing: None } => { // Unwrap TurboFish to get underlying Ident name (generic const fn). let callee_name_opt: Option<&String> = match &func.kind { E::Ident(n) => Some(n), E::TurboFish { base, .. } => match &base.kind { E::Ident(n) => Some(n), _ => None, }, _ => None, }; if let Some(name) = callee_name_opt { // Plan 114.4.4 Ф.5 V4: size_of/align_of intrinsics accepted // на module-level const RHS (`const SIZE = size_of[int]()`). if name == "size_of" || name == "align_of" { return Ok(()); } // D215 amend: named tuple constructor — each arg must be constexpr. if named_tuple_names.contains(name) { for a in args { let arg_expr = match a { crate::ast::CallArg::Item(e) => e, crate::ast::CallArg::Named { value, .. } => value, crate::ast::CallArg::Spread(e) => e, }; check_const_constexpr_ex(arg_expr, known_consts, const_fn_names, named_tuple_names)?; } return Ok(()); } if const_fn_names.contains(name) { for a in args { match a { crate::ast::CallArg::Item(e) => { // Arg recursion должен пройти как constexpr; // если нет — переэмитим с E_CONST_FN_NON_CONST_ARG // (per-D199 более информативный код для caller'а). if let Err(_inner) = check_const_constexpr_ex( e, known_consts, const_fn_names, named_tuple_names, ) { return Err(Diagnostic::new( format!( "[E_CONST_FN_NON_CONST_ARG] call to \ const fn `{}` has non-constexpr \ argument — all args must be literals, \ arithmetic on literals, references to \ top-level const, or other const fn \ calls with constexpr args (D199).", name ), e.span, )); } } _ => { return Err(Diagnostic::new( "[E_CONST_FN_NON_CONST_ARG] only positional \ constexpr args allowed when calling const fn \ в const initialiser (D199).".to_string(), expr.span, )); } } } return Ok(()); } } Err(Diagnostic::new( "[E_CONST_NOT_CONSTEXPR] non-const-fn call в `const` initialiser \ — only literals, arithmetic, `as`-casts, record/tuple/array \ literals, named-tuple constructors, references to top-level consts, \ и calls to other `const fn` are allowed. Use `ro X = …` для \ runtime / lazy-init, либо declare `fn ... const param ... -> const T` \ (D199).".to_string(), expr.span, )) } // Member access / Index / InterpolatedStr / MapLit / call с trailing — // runtime. E::Call { .. } | E::Member { .. } | E::Index { .. } | E::InterpolatedStr { .. } | E::MapLit { .. } => Err(Diagnostic::new( "[E_CONST_NOT_CONSTEXPR] non-constexpr expression в `const` \ initialiser (member/index/interpolation/map/call-with-trailing). \ Use `ro X = …` для runtime / lazy-init value (D199).".to_string(), expr.span, )), // Любые другие конструкции (if, match, blocks, closures, etc.) — runtime. _ => Err(Diagnostic::new( "[E_CONST_NOT_CONSTEXPR] non-constexpr expression в `const` \ initialiser (control flow / closures / blocks not allowed). \ Use `ro X = …` для runtime / lazy-init value (Plan 114.4 Ф.1).".to_string(), expr.span, )), } } /// Plan 148 Ф.3 ([M-114.4-strict-partition]): forward direction of the /// strict module-level `const`/`ro` partition (spec D199 / 03-syntax /// «Strict module-level partition»). /// /// `E_CONST_NOT_CONSTEXPR` (reverse direction) already rejects a module-level /// `const X = …` whose RHS is *not* constexpr-eligible. This helper is the /// forward direction: a module-level `ro X = …` whose RHS *is* fully /// constexpr-eligible must instead be declared `const` — the keyword is not /// a user choice, it follows the RHS. Such a binding is flagged with /// `E_RO_FOR_CONSTEXPR_PREFER_CONST`. /// /// Scope is deliberately narrow — this matches the spec, which restricts the /// strict partition to **module-level** bindings only (scope-local `ro x = 5` /// and `const x = 5` are both valid, with different guarantees): /// - Only module-level `Item::Let` (which, post-D184, can only originate /// from the `ro` keyword — `let`/`mut`/`consume` are rejected at module /// level by the parser). /// - Only a single named binding (`Pattern::Ident`, or — for the usual /// UPPER_CASE constant name — a single-segment unit `Pattern::Variant`, /// see the NB in the body); `const` has no destructuring form, so /// `ro (a, b) = …` is never convertible and is left alone. /// - `ghost` bindings are spec-only and never emitted — left alone. /// - The RHS must be *fully* constexpr-eligible per the very same /// `check_const_constexpr_ex` predicate that drives `E_CONST_NOT_CONSTEXPR`, /// so the two directions can never disagree about what «constexpr» means. /// /// Returns `Some(diagnostic)` when the binding should be `const`, else `None`. fn check_ro_module_partition( decl: &crate::ast::LetDecl, known_consts: &HashSet<String>, const_fn_names: &HashSet<String>, named_tuple_names: &HashSet<String>, ) -> Option<Diagnostic> { // `ghost ro X = …` — spec-only binding, not subject to the partition. if decl.is_ghost { return None; } // `const` only supports a single named binding; non-binding patterns // (tuple / record / wildcard / multi-segment or sub-pattern variant) have // no `const` equivalent → leave alone. // // NB: a module-level constant is conventionally UPPER_CASE / PascalCase, so // `ro MAX = …` parses its binder as `Pattern::Variant { path: ["MAX"], // kind: Unit }` (the parser cannot tell a fresh binder from a unit-variant // match at parse time). A *single-segment unit* variant pattern in binding // position is always a fresh name (there is nothing to match against in a // `ro X = …` declaration), so it is the UPPER_CASE binding form. A // multi-segment path (`Mod.Variant`) or a variant with sub-patterns is a // genuine destructuring pattern, which has no `const` equivalent. let name = match &decl.pattern { crate::ast::Pattern::Ident { name, .. } => name.as_str(), crate::ast::Pattern::Variant { path, kind: crate::ast::VariantPatternKind::Unit, .. } if path.len() == 1 => path[0].as_str(), _ => return None, }; // Fire only when the RHS is *fully* constexpr-eligible (same predicate // as `E_CONST_NOT_CONSTEXPR`). A runtime call / effect / allocation / // reference to another `ro` makes the binding genuinely runtime → `ro` // is correct and we stay silent. if check_const_constexpr_ex(&decl.value, known_consts, const_fn_names, named_tuple_names).is_err() { return None; } Some(Diagnostic::new( format!( "[E_RO_FOR_CONSTEXPR_PREFER_CONST] module-level `ro {name} = …` has a \ constexpr-eligible initialiser (literal / arithmetic on literals / \ record/tuple/array literal of constexpr fields / reference to another \ `const` / `const fn` call). The module-level `const`/`ro` partition is \ strict: a constexpr-eligible RHS must be declared `const {name} = …`, \ not `ro {name} = …`. Use `const` here, or keep `ro` only for a runtime \ value (Plan 114.4 / D199, spec 03-syntax «Strict module-level partition»). \ Scope-local `ro`/`const` are both fine — this rule applies at module level." ), decl.span, )) } /// Plan 114.4.2 (D199) Ф.1 body checker: validate const fn body against /// V1 whitelist (literals + arithmetic + as-cast + ident refs to const /// params/locals + local const + final expr + calls to other const fn). /// /// Returns Err on first violation with appropriate error code. /// `param_consts` — set of const param names visible в body. /// `const_fn_names` — set of all const fn names (для call validation). /// `local_consts` — mutable set extended при встрече Stmt::Const. /// `current_fn` — for self-recursion detection. /// `call_targets` — mutable set — populated с именами callee const fn /// (для post-pass cycle detection). fn check_const_fn_expr( expr: &crate::ast::Expr, param_consts: &std::collections::HashSet<String>, const_fn_names: &std::collections::HashSet<String>, local_consts: &std::collections::HashSet<String>, current_fn: &str, call_targets: &mut std::collections::HashSet<String>, ) -> Result<(), Diagnostic> { use crate::ast::ExprKind as E; match &expr.kind { E::IntLit(_) | E::FloatLit(_) | E::StrLit(_) | E::BoolLit(_) | E::CharLit(_) | E::UnitLit => Ok(()), E::Unary { operand, .. } => check_const_fn_expr( operand, param_consts, const_fn_names, local_consts, current_fn, call_targets, ), E::Binary { left, right, .. } => { check_const_fn_expr(left, param_consts, const_fn_names, local_consts, current_fn, call_targets)?; check_const_fn_expr(right, param_consts, const_fn_names, local_consts, current_fn, call_targets) } E::As(inner, _) => check_const_fn_expr( inner, param_consts, const_fn_names, local_consts, current_fn, call_targets, ), // Plan 114.4.4 Ф.3 V3: Range expr — recurse on start/end. E::Range { start, end, .. } => { if let Some(s) = start { check_const_fn_expr(s, param_consts, const_fn_names, local_consts, current_fn, call_targets)?; } if let Some(e) = end { check_const_fn_expr(e, param_consts, const_fn_names, local_consts, current_fn, call_targets)?; } Ok(()) } E::Ident(name) => { if param_consts.contains(name) || local_consts.contains(name) { Ok(()) } else if const_fn_names.contains(name) { Err(Diagnostic::new( format!( "[E_CONST_FN_FIRST_CLASS] const fn `{}` used as first-class \ value в body — not supported в V1 (D199). Followup \ `[M-114.4.2-first-class]`. Direct call `{}(arg)` instead.", name, name ), expr.span, )) } else { Err(Diagnostic::new( format!( "[E_CONST_FN_REF_NON_CONST] const fn body refers `{}` which \ is not a const param, local const, or const fn (D199). \ Only const params/locals/literals allowed в const fn V1 body.", name ), expr.span, )) } } E::Call { func, args, trailing } => { if trailing.is_some() { return Err(Diagnostic::new( "[E_CONST_FN_CONTROL_FLOW] trailing-block calls (DSL syntax) \ not allowed в const fn body (D199): require runtime closure / \ control-flow. Use const-eligible call syntax." .to_string(), expr.span, )); } // Callee должен быть Ident (или TurboFish<Ident> для generic // const fn / size_of/align_of intrinsics). let callee_name = match &func.kind { E::Ident(n) => n.clone(), E::TurboFish { base, .. } => match &base.kind { E::Ident(n) => n.clone(), _ => { return Err(Diagnostic::new( "[E_CONST_FN_EFFECT_IN_BODY] non-ident turbofish base \ (D199).".to_string(), expr.span, )); } } _ => { return Err(Diagnostic::new( "[E_CONST_FN_EFFECT_IN_BODY] indirect / method / path calls \ not allowed в const fn body (D199). Use direct const fn \ call by name." .to_string(), expr.span, )); } }; // Plan 114.4.4 Ф.4 V4: variant constructor heuristic — // Call(Ident(Name), args) где Name начинается с uppercase // letter и НЕ const fn — treat as variant constructor // (e.g. `Some(5)`, `Ok(v)`, `Err(e)`, `Cons(h, t)`). let is_variant_constructor = !const_fn_names.contains(&callee_name) && callee_name.chars().next().map_or(false, |c| c.is_uppercase()); // Plan 114.4.4 Ф.5 V4: t-reflection intrinsics — sizeof / align_of. // Recognized как built-in const fn без registration. let is_t_reflection = callee_name == "size_of" || callee_name == "align_of"; if !const_fn_names.contains(&callee_name) && !is_variant_constructor && !is_t_reflection { return Err(Diagnostic::new( format!( "[E_CONST_FN_EFFECT_IN_BODY] call `{}(...)` from const fn \ body — `{}` is not a `const fn` или variant constructor \ (D199). Only calls к другим const fn или Variant constructors \ (TitleCase names) allowed. Use runtime fn if needed.", callee_name, callee_name ), expr.span, )); } // Plan 114.4.3 Ф.2 (V2): direct self-recursion allowed. // Evaluator enforces depth limit + memoization. let _self_call = callee_name == current_fn; if !is_variant_constructor { call_targets.insert(callee_name); } // Args также constexpr-eligible — recurse. for a in args { match a { crate::ast::CallArg::Item(e) => check_const_fn_expr( e, param_consts, const_fn_names, local_consts, current_fn, call_targets, )?, crate::ast::CallArg::Spread(_) => { return Err(Diagnostic::new( "[E_CONST_FN_EFFECT_IN_BODY] spread args `...` not allowed \ в const fn body (D199): runtime collection operation." .to_string(), expr.span, )); } _ => { return Err(Diagnostic::new( "[E_CONST_FN_EFFECT_IN_BODY] named / non-positional args \ not supported в const fn body (D199 V1)." .to_string(), expr.span, )); } } } Ok(()) } // Block expression — recurse через statements. E::Block(block) => { check_const_fn_block( block, param_consts, const_fn_names, local_consts, current_fn, call_targets, ) } // Plan 114.4.3 Ф.1 (D199 V2): `if`/`else` allowed — recurse // on cond + each branch. Все sub-expressions должны быть constexpr. E::If { cond, then, else_ } => { check_const_fn_expr( cond, param_consts, const_fn_names, local_consts, current_fn, call_targets, )?; check_const_fn_block( then, param_consts, const_fn_names, local_consts, current_fn, call_targets, )?; if let Some(eb) = else_ { match eb { crate::ast::ElseBranch::Block(b) => check_const_fn_block( b, param_consts, const_fn_names, local_consts, current_fn, call_targets, )?, crate::ast::ElseBranch::If(ie) => check_const_fn_expr( ie, param_consts, const_fn_names, local_consts, current_fn, call_targets, )?, } } // Plan 114.4.4 Ф.3 V3: if без else allowed как side-effect // statement (для loops с conditional continue/break). Evaluator // skips body если cond=false, returns Unit. Ok(()) } // Plan 114.4.3 Ф.1 (D199 V2): `match` allowed — recurse on scrutinee + // каждый arm body. Pattern V2.0 subset: literal + wildcard + ident bind. E::Match { scrutinee, arms } => { check_const_fn_expr( scrutinee, param_consts, const_fn_names, local_consts, current_fn, call_targets, )?; for arm in arms { check_const_fn_pattern(&arm.pattern, expr.span)?; // Plan 114.4.4 Ф.4 V4: bindings из pattern добавляются в // arm-local scope для body/guard validation. let mut arm_locals = local_consts.clone(); collect_const_fn_pattern_bindings(&arm.pattern, &mut arm_locals); if let Some(g) = &arm.guard { check_const_fn_expr( g, param_consts, const_fn_names, &arm_locals, current_fn, call_targets, )?; } match &arm.body { crate::ast::MatchArmBody::Expr(e) => check_const_fn_expr( e, param_consts, const_fn_names, &arm_locals, current_fn, call_targets, )?, crate::ast::MatchArmBody::Block(b) => check_const_fn_block( b, param_consts, const_fn_names, &arm_locals, current_fn, call_targets, )?, } } Ok(()) } // IfLet — V2.1 deferred (pattern-bind complexity). E::IfLet { .. } => Err(Diagnostic::new( "[E_CONST_FN_CONTROL_FLOW] `if let` pattern-bind в const fn body \ не allowed в V2.0 (D199). Followup `[M-114.4.3-pattern-record-sum]`. \ Use `if cond { ... } else { ... }` с literal comparison." .to_string(), expr.span, )), // Plan 114.4.4 Ф.3 (D199 V3): for/while/loop allowed — // evaluator enforces termination через MAX_LOOP_ITERATIONS. E::For { pattern, iter, body, .. } => { check_const_fn_expr(iter, param_consts, const_fn_names, local_consts, current_fn, call_targets)?; // Pattern var добавляется в locals для body validation. let mut body_locals = local_consts.clone(); if let crate::ast::Pattern::Ident { name, .. } = pattern { body_locals.insert(name.clone()); } check_const_fn_block(body, param_consts, const_fn_names, &body_locals, current_fn, call_targets) } E::While { cond, body, .. } => { check_const_fn_expr(cond, param_consts, const_fn_names, local_consts, current_fn, call_targets)?; check_const_fn_block(body, param_consts, const_fn_names, local_consts, current_fn, call_targets) } E::Loop { body, .. } => check_const_fn_block(body, param_consts, const_fn_names, local_consts, current_fn, call_targets), // ParallelFor + WhileLet остаются rejected. E::ParallelFor { .. } | E::WhileLet { .. } => Err(Diagnostic::new( "[E_CONST_FN_CONTROL_FLOW] `parallel for` / `while let` не \ разрешены в const fn body (D199). Use plain `for`/`while`." .to_string(), expr.span, )), // Try/Bang — effect propagation. E::Try(_) | E::Bang(_) => Err(Diagnostic::new( "[E_CONST_FN_EFFECT_IN_BODY] Try / Bang (?/!!) propagate effects — \ const fn body должен быть pure (D199)." .to_string(), expr.span, )), // Allocations / collection literals. E::ArrayLit(_) | E::MapLit { .. } | E::InterpolatedStr { .. } => Err(Diagnostic::new( "[E_CONST_FN_ALLOCATION] allocations (arrays/maps/string interp) \ not allowed в const fn body V1 (D199)." .to_string(), expr.span, )), // Plan 114.4.4 Ф.4 V4: tuple literals — recursive sub-validation. // ConstValue::Tuple supports structured comptime data. E::TupleLit(elems) => { for e in elems { check_const_fn_expr( e, param_consts, const_fn_names, local_consts, current_fn, call_targets, )?; } Ok(()) } // Plan 114.4.4 Ф.4 V4: record literals — recursive sub-validation // на каждое field value. ConstValue::Record supports structured // comptime data. E::RecordLit { fields, .. } => { for f in fields { if f.is_spread { return Err(Diagnostic::new( "[E_CONST_FN_ALLOCATION] spread `...` в record literal не \ разрешён в const fn body (D199).".to_string(), expr.span, )); } if let Some(v) = &f.value { check_const_fn_expr( v, param_consts, const_fn_names, local_consts, current_fn, call_targets, )?; } } Ok(()) } // Member/Index/Path — runtime access. E::Member { .. } | E::Index { .. } | E::Path(_) | E::TurboFish { .. } | E::SelfAccess => Err(Diagnostic::new( "[E_CONST_FN_EFFECT_IN_BODY] member/index/path access — runtime \ operations, not allowed в const fn body V1 (D199)." .to_string(), expr.span, )), // Coalesce/Is — runtime checks. E::Coalesce(_, _) | E::Is(_, _) => Err(Diagnostic::new( "[E_CONST_FN_EFFECT_IN_BODY] coalesce (??) / type-check (is) — \ runtime semantics, not allowed в const fn body V1 (D199)." .to_string(), expr.span, )), // Прочее (closures / lambda / spawn / supervised / handler etc.) — reject. _ => Err(Diagnostic::new( "[E_CONST_FN_EFFECT_IN_BODY] expression form not allowed в const fn \ body V1 (closures/spawn/handler/etc) (D199)." .to_string(), expr.span, )), } } /// Plan 114.4.3 Ф.1 (D199 V2): pattern V2.0 subset для match arm validation. /// Allowed: literal (Int/Bool/Char/Str/Unit), wildcard (_), single Ident /// bind (which binds a fresh local — caller responsibility to register). /// Rejected V2.0: record/sum/tuple destructuring patterns. fn check_const_fn_pattern( pat: &crate::ast::Pattern, span: Span, ) -> Result<(), Diagnostic> { use crate::ast::{Pattern, VariantPatternKind}; match pat { Pattern::Wildcard(_) => Ok(()), Pattern::Ident { is_mut: false, .. } => Ok(()), Pattern::Ident { is_mut: true, .. } => Err(Diagnostic::new( "[E_CONST_FN_PATTERN_NOT_SUPPORTED] `mut` pattern в const fn match \ arm not allowed (D199 V4). Remove `mut`." .to_string(), span, )), Pattern::Literal(_, _) => Ok(()), Pattern::Or { alternatives, .. } => { for alt in alternatives { check_const_fn_pattern(alt, span)?; } Ok(()) } // Plan 114.4.4 Ф.4 V4: tuple destructuring pattern. Pattern::Tuple(pats, _) => { for p in pats { check_const_fn_pattern(p, span)?; } Ok(()) } // Plan 114.4.4 Ф.4 V4: variant destructuring pattern. Pattern::Variant { kind, .. } => { match kind { VariantPatternKind::Unit => Ok(()), VariantPatternKind::Tuple { patterns, .. } => { for p in patterns { check_const_fn_pattern(p, span)?; } Ok(()) } } } // Plan 114.4.4 Ф.4 V4: record destructuring pattern. Pattern::Record { fields, .. } => { for f in fields { if let Some(p) = &f.pattern { check_const_fn_pattern(p, span)?; } } Ok(()) } Pattern::Binding { inner, .. } => check_const_fn_pattern(inner, span), _ => Err(Diagnostic::new( "[E_CONST_FN_PATTERN_NOT_SUPPORTED] pattern form not supported в \ const fn match arm (D199). Allowed: literal patterns, wildcard `_`, \ ident bind, `|` alternation, tuple `(a, b)`, variant `Name(args)`, \ record `{ field: pat }`." .to_string(), span, )), } } /// Plan 114.4.4 Ф.4 V4: collect bindings from match arm pattern. /// Used при validating arm body — bindings из pattern visible. fn collect_const_fn_pattern_bindings( pat: &crate::ast::Pattern, locals: &mut std::collections::HashSet<String>, ) { use crate::ast::{Pattern, VariantPatternKind}; match pat { Pattern::Wildcard(_) | Pattern::Literal(_, _) => {} Pattern::Ident { name, .. } => { locals.insert(name.clone()); } Pattern::Tuple(pats, _) => { for p in pats { collect_const_fn_pattern_bindings(p, locals); } } Pattern::Variant { kind, .. } => match kind { VariantPatternKind::Unit => {} VariantPatternKind::Tuple { patterns, .. } => { for p in patterns { collect_const_fn_pattern_bindings(p, locals); } } } Pattern::Record { fields, .. } => { for f in fields { match &f.pattern { Some(p) => collect_const_fn_pattern_bindings(p, locals), None => { locals.insert(f.name.clone()); } } } } Pattern::Or { alternatives, .. } => { if let Some(first) = alternatives.first() { collect_const_fn_pattern_bindings(first, locals); } } Pattern::Binding { name, inner, .. } => { locals.insert(name.clone()); collect_const_fn_pattern_bindings(inner, locals); } _ => {} } } /// Plan 114.4.4 Ф.3 V3: collect bindings from `let` pattern in const fn body. /// V3.0 supports only single Ident patterns. Record/tuple destructuring — /// V3.1 followup `[M-114.4.4-let-destructure]`. fn collect_pattern_bindings_const_fn( pat: &crate::ast::Pattern, locals: &mut std::collections::HashSet<String>, span: Span, ) -> Result<(), Diagnostic> { use crate::ast::Pattern; match pat { Pattern::Ident { name, .. } => { locals.insert(name.clone()); Ok(()) } Pattern::Wildcard(_) => Ok(()), _ => Err(Diagnostic::new( "[E_CONST_FN_PATTERN_NOT_SUPPORTED] only single ident / wildcard \ pattern allowed в `let` binding в const fn body V3.0 (D199). \ Record/tuple destructure — followup `[M-114.4.4-let-destructure]`." .to_string(), span, )), } } fn check_const_fn_block( block: &crate::ast::Block, param_consts: &std::collections::HashSet<String>, const_fn_names: &std::collections::HashSet<String>, local_consts: &std::collections::HashSet<String>, current_fn: &str, call_targets: &mut std::collections::HashSet<String>, ) -> Result<(), Diagnostic> { use crate::ast::Stmt; let mut locals = local_consts.clone(); // Block has stmts (non-final) + optional trailing expr (final value). // Если trailing нет — последний stmt из stmts становится final. let n = block.stmts.len(); let has_trailing = block.trailing.is_some(); for (idx, st) in block.stmts.iter().enumerate() { let is_last = !has_trailing && idx == n - 1; match st { Stmt::Const(cd) => { // Validate RHS as const-fn expression (allowed const fn calls). check_const_fn_expr( &cd.value, param_consts, const_fn_names, &locals, current_fn, call_targets, )?; locals.insert(cd.name.clone()); } Stmt::Expr(e) => { // Plan 114.4.4 Ф.3 V3: intermediate Stmt::Expr accepted — // body может содержать loops / if-without-else / etc. как // statements. Value extraction handled by caller (trailing // expr или final Stmt::Expr становится return value). let _ = is_last; check_const_fn_expr( e, param_consts, const_fn_names, &locals, current_fn, call_targets, )?; } // Plan 114.4.4 Ф.3 V3: mut let bindings allowed для loops. // Stmt::Let добавляет name в locals. mut OK; ro/plain let // тоже OK — body checker treats them uniformly. Stmt::Let(ld) => { check_const_fn_expr( &ld.value, param_consts, const_fn_names, &locals, current_fn, call_targets, )?; // Collect bindings from pattern. Только Ident patterns // supported в V3.0 const fn body. Record/tuple destructure // — V3.1 followup. collect_pattern_bindings_const_fn(&ld.pattern, &mut locals, ld.span)?; } // Plan 114.4.4 Ф.3 V3: assignment к mut local allowed. Stmt::Assign { target, value, .. } => { check_const_fn_expr( target, param_consts, const_fn_names, &locals, current_fn, call_targets, )?; check_const_fn_expr( value, param_consts, const_fn_names, &locals, current_fn, call_targets, )?; } Stmt::Return { value, span } => { if !is_last { return Err(Diagnostic::new( "[E_CONST_FN_CONTROL_FLOW] `return` must be terminal в \ const fn body V1 (D199)." .to_string(), *span, )); } if let Some(v) = value { check_const_fn_expr( v, param_consts, const_fn_names, &locals, current_fn, call_targets, )?; } } Stmt::Throw { span, .. } => { return Err(Diagnostic::new( "[E_CONST_FN_EFFECT_IN_BODY] `throw` not allowed в const fn \ body (D199): effect propagation." .to_string(), *span, )); } Stmt::Defer { span, .. } | Stmt::ConsumeScope { span, .. } => { return Err(Diagnostic::new( "[E_CONST_FN_EFFECT_IN_BODY] defer / consume-scope не разрешены \ в const fn body (D199): cleanup-семантика — runtime." .to_string(), *span, )); } // Plan 114.4.4 Ф.3 V3: break/continue allowed внутри loops. // Checker не отслеживает context — evaluator catches stray // break/continue outside loop scope at runtime через // ControlFlow propagation. Stmt::Break(_) | Stmt::Continue(_) => {} // Ghost statements (assume/assert_static/apply/calc/reveal) — reject // в const fn V1 как unsupported. Stmt::AssertStatic { span, .. } | Stmt::Assume { span, .. } | Stmt::Apply { span, .. } | Stmt::Calc { span, .. } | Stmt::Reveal { span, .. } => { return Err(Diagnostic::new( "[E_CONST_FN_EFFECT_IN_BODY] ghost statements (assume/assert_static/\ apply/calc/reveal) not allowed в const fn body V1 (D199)." .to_string(), *span, )); } // Plan 136: tuple destructuring assignment — not supported in const fn V1. Stmt::TupleAssign { span, .. } => { return Err(Diagnostic::new( "[E_CONST_FN_EFFECT_IN_BODY] tuple destructuring assignment not \ supported in const fn body V1 (Plan 136 followup)." .to_string(), *span, )); } } } // Trailing expression — финальное значение блока. if let Some(trail) = &block.trailing { check_const_fn_expr( trail, param_consts, const_fn_names, &locals, current_fn, call_targets, )?; } Ok(()) } /// Top-level entrypoint: validate const fn `fd` body. /// Returns Err on first violation; updates `call_targets` для cycle detection. fn check_const_fn_decl( fd: &FnDecl, const_fn_names: &std::collections::HashSet<String>, call_targets: &mut std::collections::HashSet<String>, ) -> Result<(), Diagnostic> { let mut param_consts = std::collections::HashSet::new(); for p in &fd.params { param_consts.insert(p.name.clone()); } let local_consts = std::collections::HashSet::new(); // Plan 114.4.4.4 V4.3: closure-returning const fn — body is single // closure literal at top level. Allow it и validate closure body // с extended scope (const params + closure params). if let crate::ast::FnBody::Expr(e) = &fd.body { if let Some(extra) = closure_param_names(e) { let mut extended = param_consts.clone(); for n in &extra { extended.insert(n.clone()); } return validate_const_fn_closure_body( e, &extended, const_fn_names, &local_consts, &fd.name, call_targets, ); } } // Plan 114.4.4 V4.4 Ф.2 [M-114.4.4-closure-captures-outer]: Block body // where stmts = all Stmt::Const + trailing = closure literal. Each // const RHS validated с regular const fn rules (scope = host const // params + prior outer consts); closure body validated с extended // scope (host params + outer consts + closure params). if let crate::ast::FnBody::Block(b) = &fd.body { let stmts_all_const = !b.stmts.is_empty() && b.stmts.iter().all(|s| matches!(s, crate::ast::Stmt::Const(_))); let trailing_closure = b.trailing.as_ref() .and_then(|t| closure_param_names(t).map(|p| (t.as_ref(), p))); if stmts_all_const { if let Some((closure_expr, closure_params)) = trailing_closure { let mut outer_consts: std::collections::HashSet<String> = local_consts.clone(); for s in &b.stmts { if let crate::ast::Stmt::Const(cd) = s { // Each outer const RHS uses host const params + prior // outer consts. Validator treats them all uniformly через // param_consts + accumulated local_consts. check_const_fn_expr( &cd.value, ¶m_consts, const_fn_names, &outer_consts, &fd.name, call_targets, )?; outer_consts.insert(cd.name.clone()); } } let mut extended = param_consts.clone(); for n in &outer_consts { extended.insert(n.clone()); } for n in &closure_params { extended.insert(n.clone()); } return validate_const_fn_closure_body( closure_expr, &extended, const_fn_names, &local_consts, &fd.name, call_targets, ); } } } match &fd.body { crate::ast::FnBody::Expr(e) => check_const_fn_expr( e, ¶m_consts, const_fn_names, &local_consts, &fd.name, call_targets, ), crate::ast::FnBody::Block(b) => check_const_fn_block( b, ¶m_consts, const_fn_names, &local_consts, &fd.name, call_targets, ), crate::ast::FnBody::External => Err(Diagnostic::new( "[E_CONST_FN_EXTERNAL] external fn не может быть const fn (D199)." .to_string(), fd.span, )), } } /// Plan 114.4.4.4 V4.3: if `e` is closure literal at top level — return /// its parameter names. Otherwise None. fn closure_param_names(e: &crate::ast::Expr) -> Option<Vec<String>> { use crate::ast::ExprKind as E; match &e.kind { E::Lambda { params, .. } => { Some(params.iter().map(|p| p.name.clone()).collect()) } E::ClosureLight { params, .. } => { Some(params.iter().map(|p| p.name.clone()).collect()) } E::ClosureFull(sb) => { Some(sb.params.iter().map(|p| p.name.clone()).collect()) } _ => None, } } /// Plan 114.4.4.4 V4.3: validate closure body of closure-returning const /// fn. Closure body может использовать host fn's const params + closure's /// own params как identifier sources. Other constructs validated по /// regular V1 const fn body rules. fn validate_const_fn_closure_body( closure_expr: &crate::ast::Expr, extended_params: &std::collections::HashSet<String>, const_fn_names: &std::collections::HashSet<String>, local_consts: &std::collections::HashSet<String>, current_fn: &str, call_targets: &mut std::collections::HashSet<String>, ) -> Result<(), Diagnostic> { use crate::ast::{ExprKind as E, FnBody, ClosureBody}; match &closure_expr.kind { E::Lambda { body, .. } => check_const_fn_expr( body, extended_params, const_fn_names, local_consts, current_fn, call_targets, ), E::ClosureLight { body, .. } => match body { ClosureBody::Expr(e) => check_const_fn_expr( e, extended_params, const_fn_names, local_consts, current_fn, call_targets, ), ClosureBody::Block(b) => check_const_fn_block( b, extended_params, const_fn_names, local_consts, current_fn, call_targets, ), }, E::ClosureFull(sb) => match &sb.body { FnBody::Expr(e) => check_const_fn_expr( e, extended_params, const_fn_names, local_consts, current_fn, call_targets, ), FnBody::Block(b) => check_const_fn_block( b, extended_params, const_fn_names, local_consts, current_fn, call_targets, ), FnBody::External => Err(Diagnostic::new( "[E_CONST_FN_CLOSURE_EXTERNAL] closure-returning const fn body \ cannot be external (D199 V4.3).".to_string(), closure_expr.span, )), }, _ => Err(Diagnostic::new( "[E_CONST_FN_CLOSURE_BODY] expected closure literal в \ closure-returning const fn body (D199 V4.3).".to_string(), closure_expr.span, )), } } /// **Plan 118.5 V3 §V3.1 (D216 V3 amend, 2026-06-04):** detect value-type /// classification of a TypeRef для storage-class-aware ro+mut conflict check. /// /// Value types: primitives (int/uint as aliases для isize/usize, all sized /// ints + floats, bool/char/byte/str/ptr), value records (Plan 124.8 D228 /// `type X value { ... }`), named tuples (Plan 120 D215), anonymous tuples, /// unit. /// /// Reference types: heap records, arrays, pointers, func, protocol. fn is_value_type_for_v3( ty: &TypeRef, types: &HashMap<String, &TypeDecl>, ) -> bool { use TypeRef::*; match ty { Named { path, .. } if path.len() == 1 => { let name = path[0].as_str(); // Primitives per V3.1 + Plan 133 (int=intptr_t, uint=uintptr_t; usize/isize removed). // Plan 134: `ptr` removed; *() = TypeRef::Pointer(Unit) is handled below. if matches!(name, "int" | "uint" | "i8" | "i16" | "i32" | "i64" | "u8" | "u16" | "u32" | "u64" | "f32" | "f64" | "bool" | "char" | "byte" | "str" ) { return true; } // User type: value record OR named tuple. if let Some(td) = types.get(name) { let is_value_record = matches!( td.kind, crate::ast::TypeDeclKind::Record(_) ) && td.allocation == crate::ast::AllocKind::Value; let is_named_tuple = matches!( td.kind, crate::ast::TypeDeclKind::NamedTuple(_) ); return is_value_record || is_named_tuple; } false } Tuple(..) => true, // anonymous tuples = value Unit(..) => true, // [M-fixed-array-value-semantics] (2026-07-10, D27-амендмент): `[N]T` — // inline value-класс (стек / поле-по-месту), НЕ heap-tracked — реклассификация // владельца «должно быть на стеке». Элементы могут быть кучевыми (T=Vec/heap // record), но КОНТЕЙНЕР [N]T всегда inline, аналогично Tuple(..) выше (который // тоже не проверяет heap-ность элементов). См. ref_target_confirmed_heap ниже — // синхронная реклассификация. FixedArray(..) => true, Array(..) => false, // []T heap (Vec canon, D239) Pointer(..) => false, Func { .. } => false, Protocol { .. } => false, Readonly(inner, _) | Mut(inner, _) | Uninit(inner, _) => { is_value_type_for_v3(inner, types) } // Plan 184: `ref T` — ссылочный алиас; классифицируем по цели (Р5). Ref(inner, _) => is_value_type_for_v3(inner, types), // Module-qualified Named (path.len() > 1) — out-of-module type; // assume reference (conservative — won't break value semantic для // local types; cross-module value records require explicit detection // в future V3.1.1 followup). Named { .. } => false, } } /// D246-амендмент ([M-ro-launder-via-mut-binding], Ф.1, FINAL — owner /// decision 2026-07-23, "подтверждена доками+пробой"): true for the /// IRREDUCIBLE scalar primitives — no fields, no indirection, no possible /// aliasing EVER (copying the bit-pattern IS the value; there is no /// owned-graph to leak through). Base case of `is_fully_stack_value` /// (§72 D246-амендмент, Ф.2, 2026-07-24 — the RECURSIVE successor that /// replaced this fn's original TypeRef-wrapper sibling `is_bare_scalar_ /// primitive` at all three ro-launder call-sites; that wrapper is now dead /// code and was removed — see `is_fully_stack_value` doc for the full /// current boundary, incl. the `str`-by-immutability exception and probe /// G/E dispositions). fn is_bare_scalar_primitive_name(name: &str) -> bool { matches!( name, "int" | "uint" | "i8" | "i16" | "i32" | "i64" | "u8" | "u16" | "u32" | "u64" | "f32" | "f64" | "bool" | "char" | "byte" ) } /// D246-амендмент §72 ([M-ro-launder-fullstack-value-exemption], Ф.2 — owner /// decision 2026-07-24: "ДА — «полностью-стековый value-тип, проба G /// остаётся neg»"): RECURSIVE successor to the original scalar-only /// exemption (`is_bare_scalar_primitive_name` is now this predicate's base /// case) for the ro-launder exemption. True iff the type's ENTIRE /// owned-graph is stack-resident, i.e. a shallow bit-copy IS the /// independent value — no subpath can alias the original: /// /// - bare scalar primitives (base case, unchanged from Ф.1 — /// `is_bare_scalar_primitive_name`: no fields at all, nothing to recurse /// into). /// - `str` (base case, but by a DIFFERENT reasoning — **IMMUTABILITY, not /// stackness** (D26). A `str` value is itself a shallow `{ptr *u8, len}` /// heap handle — NOT stack in the literal sense the rest of this /// predicate uses — so admitting it here is a deliberate, documented /// special case: no method/index-write exists for `str` CONTENT through /// any binding, so an aliased copy has no live write-hole to exploit, /// regardless of depth (top-level local OR nested record field). The /// original Ф.1 `is_bare_scalar_primitive` wave excluded bare `str` /// locals too, but that was "out of scope for Ф.1" bookkeeping, not a /// soundness finding — see that fn's doc comment. /// - `Unit` / anonymous `Tuple(..)` / `[N]T` (`FixedArray`) — recurse into /// EVERY element. The container itself is inline/stack-embedded, but an /// ELEMENT can still secretly be a heap handle (`[3]Vec[int]` embeds 3 /// ALIASED heap buffers, copied by reference when the array is shallow- /// copied) — this is probe G's failure shape one level down, so (unlike /// `is_value_type_for_v3`, which classifies a container WITHOUT /// recursing into its elements — a different, narrower classification /// used by the ro/mut V3 conflict-check) this predicate MUST recurse. /// - a user `value`-record (`AllocKind::Value`) or `NamedTuple` whose /// fields are ALL `is_fully_stack_value` (recursive). A SINGLE heap /// field (`Vec`/`HashMap`/`Set`/a heap-record/`Array`) fails the WHOLE /// type — **probe G** (`ServerResponse{HeaderMap,[]u8}`) is the /// canonical rejection and MUST stay `E_READONLY_COERCE`: a shallow /// bit-copy of the record still shares the heap field's buffer with the /// original, so write-through aliasing survives the "stack" wrapper. /// /// Explicitly false (same boundary as ever, just reached recursively now): /// heap records (`AllocKind::Heap`/default — includes `AllocKind:: /// ValueHeapPromoted`, which never appears on a `TypeDecl.allocation` /// anyway, see that enum's invariant doc), `Array`/`[]T` (Vec, D239), /// `Pointer`, `Func`, `Protocol`, `Ref` (an aliasing VIEW — copying it does /// NOT yield an independent value, so it can never be "fully stack" /// regardless of what it targets), any generic-instantiated `Named` /// (non-empty `generics` — e.g. `Option[int]`, a user generic value-record /// — substitution wiring this predicate doesn't have), any module-external /// `Named` (path len > 1), and any type shape this checker cannot resolve /// structurally (`Option[T]`/enum sums generally — neither `Record` nor /// `NamedTuple` here, fall through to `false`). These gaps are KNOWN /// conservative false NEGATIVES (reject some sound cases, e.g. `Option /// [int]` could soundly be exempt) — never a false POSITIVE (never admits /// an unsound coercion). Left as a documented follow-up, not fixed in this /// wave (targeted delta, §72). fn is_fully_stack_value(ty: &TypeRef, types: &HashMap<String, &TypeDecl>) -> bool { // Cycle-guard wrapper: only Named→TypeDecl name-indirection can form a // recursion cycle (`value A { b B }` + `value B { a A }`, or a self- // referential `value A { a A }`). Structural nesting (Tuple/FixedArray/ // Unit) is a finite TypeRef tree and cannot loop on its own. Without the // guard a (mutually-)recursive value type overflows the stack — this bit // the mega-CU compile (a single conformance CU carries such a type). A // cycle means the value type is transitively self-containing, i.e. // INFINITE size — never a real fully-stack value — so treating it as // `false` (conservative, never a false positive) is also the correct // answer, not merely a termination hack. let mut on_path: HashSet<String> = HashSet::new(); is_fully_stack_value_guarded(ty, types, &mut on_path) } fn is_fully_stack_value_guarded( ty: &TypeRef, types: &HashMap<String, &TypeDecl>, on_path: &mut HashSet<String>, ) -> bool { use TypeRef::*; match ty.strip_modifiers() { Named { path, generics, .. } if path.len() == 1 && generics.is_empty() => { let name = path[0].as_str(); if is_bare_scalar_primitive_name(name) || name == "str" { return true; } match types.get(name) { Some(td) => match &td.kind { crate::ast::TypeDeclKind::Record(fields) if td.allocation == crate::ast::AllocKind::Value => { // Cycle back to a type already on the current recursion // path ⟹ transitively self-containing ⟹ not fully-stack. if !on_path.insert(name.to_string()) { return false; } let ok = fields.iter().all(|f| is_fully_stack_value_guarded(&f.ty, types, on_path)); on_path.remove(name); ok } crate::ast::TypeDeclKind::NamedTuple(elems) => { if !on_path.insert(name.to_string()) { return false; } let ok = elems.iter().all(|e| is_fully_stack_value_guarded(&e.ty, types, on_path)); on_path.remove(name); ok } _ => false, }, None => false, } } Unit(..) => true, Tuple(elems, ..) => elems.iter().all(|e| is_fully_stack_value_guarded(e, types, on_path)), FixedArray(_, inner, ..) => is_fully_stack_value_guarded(inner, types, on_path), _ => false, } } /// String-name sibling of `is_fully_stack_value`, for call-sites that only /// carry a resolved type-NAME (`ConsumeCtx.var_types`) plus a precomputed /// `stack_value_type_names` set (`ConsumeRegistry`, built once per module — /// see that field's doc for the exact recursive computation and its scope /// caveat). Mirrors `is_bare_scalar_primitive_name`'s role for the scalar- /// only predicate. fn is_fully_stack_value_name(name: &str, stack_value_type_names: &HashSet<String>) -> bool { is_bare_scalar_primitive_name(name) || name == "str" || stack_value_type_names.contains(name) } /// **Plan 118.5 V3 §V3.1 (2026-06-04):** check binding type for ro+mut /// conflict. Emits `E_MUTABILITY_CONFLICT_VALUE_TYPE` для: /// - Readonly(Mut(T)) или Mut(Readonly(T)) AST shape WHEN /// T (innermost after strip) is value-type /// - Pure-type-level Readonly(Mut(T)) inside Pointer/Array constructor — /// value-T → error /// /// For reference-type T at outermost binding position, the combination /// is allowed per §V3.1 («binding ro + content mut» semantics). fn check_v3_ro_mut_conflict( ty: &TypeRef, types: &HashMap<String, &TypeDecl>, at_binding_top: bool, errors: &mut Vec<Diagnostic>, ) { use TypeRef::*; match ty { Readonly(inner, span) => { if let Mut(_, _) = inner.as_ref() { let inner_strip = inner.strip_modifiers(); let is_value = is_value_type_for_v3(inner_strip, types); if is_value || !at_binding_top { let ctx = if is_value { "value-type T" } else { "nested-type context" }; errors.push(Diagnostic::new( format!( "[E_MUTABILITY_CONFLICT_VALUE_TYPE] `ro` and `mut` \ modifiers conflict on {} (Plan 118.5 V3 / D216 V3 \ §V3.1). `ro` (cannot mutate / cannot rebind value) \ contradicts `mut` (mutable). For value-type T, \ storage IS value — combination not meaningful. \ For reference-type T at binding top-level, the \ combination expresses «ro binding к mut content» \ but in nested constructor position there's no \ binding context к disambiguate.", ctx, ), *span, )); } } check_v3_ro_mut_conflict(inner, types, false, errors); } Mut(inner, span) => { if let Readonly(_, _) = inner.as_ref() { let inner_strip = inner.strip_modifiers(); let is_value = is_value_type_for_v3(inner_strip, types); if is_value || !at_binding_top { let ctx = if is_value { "value-type T" } else { "nested-type context" }; errors.push(Diagnostic::new( format!( "[E_MUTABILITY_CONFLICT_VALUE_TYPE] `mut` and `ro` \ modifiers conflict on {} (Plan 118.5 V3 / D216 V3 \ §V3.1). `mut` (mutable / can rebind) contradicts \ `ro` (immutable). For value-type T, storage IS \ value — combination not meaningful. For reference-\ type T at binding top-level, the combination \ expresses «mut binding к ro content» but in nested \ constructor position there's no binding context.", ctx, ), *span, )); } } check_v3_ro_mut_conflict(inner, types, false, errors); } Uninit(inner, _) => check_v3_ro_mut_conflict(inner, types, at_binding_top, errors), Pointer(inner, _) => check_v3_ro_mut_conflict(inner, types, false, errors), // Plan 184: `ref T` — прозрачно рекурсируем в цель. Ref(inner, _) => check_v3_ro_mut_conflict(inner, types, false, errors), Array(inner, _) | FixedArray(_, inner, _) => { check_v3_ro_mut_conflict(inner, types, false, errors); } Tuple(elems, _) => { for e in elems { check_v3_ro_mut_conflict(e, types, false, errors); } } Func { params, return_type, .. } => { for p in params { check_v3_ro_mut_conflict(p, types, false, errors); } if let Some(rt) = return_type { check_v3_ro_mut_conflict(rt, types, false, errors); } } Named { generics, .. } => { for g in generics { check_v3_ro_mut_conflict(g, types, false, errors); } } Protocol { .. } | Unit(_) => {} } } /// Plan 126.2 Ф.1: owned arena для synthesized auto-derive `FnDecl`s. /// /// Проблема: `TypeCheckCtx.method_table` хранит `&'a FnDecl` — заимствования /// из AST модуля. Synthesized auto-derive методы (Plan 126 V1 /// `auto_derive::synthesize_method`) — **owned** `FnDecl`, их негде заимствовать /// в исходном AST. Чтобы зарегистрировать их в `method_table` тем же путём, /// что и user-методы, нужен arena, который живёт не меньше `'a`. /// /// Реализация без внешних зависимостей (bootstrap-ethos — пустой lockfile): /// каждый `FnDecl` лежит в собственном `Box`, а боксы складываются в `Vec`. /// Содержимое `Box` стабильно по адресу: рост `Vec` перемещает только /// box-указатели, heap-аллокация самого `FnDecl` не двигается. Поэтому /// `&'arena FnDecl`, полученный из `&**boxed`, остаётся валидным даже после /// последующих `alloc`. Это стандартный safe-by-invariant arena-паттерн /// (тот же, что внутри `typed-arena`). #[derive(Default)] pub(crate) struct FnDeclArena { items: std::cell::RefCell<Vec<Box<FnDecl>>>, } impl FnDeclArena { fn new() -> Self { FnDeclArena { items: std::cell::RefCell::new(Vec::new()) } } /// Аллоцирует `fd` в arena, возвращает стабильную ссылку с lifetime arena. /// /// SAFETY: `fd` помещается в `Box`, чей heap-storage неподвижен на всё /// время жизни arena. Мы храним `Box` в `Vec` (под `RefCell`) — arena /// никогда не освобождает и не перемещает содержимое `Box` до своего /// собственного drop'а. Возвращаемая ссылка живёт `'arena` (привязана к /// `&'arena self`), что не дольше времени жизни arena. Aliasing: мы /// раздаём только shared (`&`) ссылки, мутаций содержимого нет. fn alloc<'arena>(&'arena self, fd: FnDecl) -> &'arena FnDecl { let boxed = Box::new(fd); let ptr: *const FnDecl = &*boxed; self.items.borrow_mut().push(boxed); unsafe { &*ptr } } } /// Plan 79: проход типовой полноты type-checker'а. struct TypeCheckCtx<'a> { /// Ф.2: имя типа → объявленная арность. arity: HashMap<String, ArityInfo>, /// Plan 172.1 U.2.3.3: shared base signature registry (free fns + base methods, /// §0 single source) — replaces the local `fn_decls`/`method_table` build loop. /// Read via `self.sig.fn_decls` / `self.sig.method_table`, or the synth-aware /// helpers `method_overloads` / `t_provides_method` / `find_method_decl`. sig: &'a crate::sig_registry::SigRegistry<'a>, /// Plan 172.1 U.2.3.3 (F2, [M-172.1-U2.3-synth-overlay]): synthesized auto-derive /// methods (Plan 126) — a TypeCheckCtx-PRIVATE overlay on top of `sig.method_table`. /// NOT in the shared registry: Bound/Cap must not see synth (their resolution is /// base-only). Keyed `type → method → overloads`; for a given (type, method) /// overloads come from EITHER base OR synth, never both (synth is registered only /// when base lacks the method — see `register_synthesized_methods`). synth_methods: HashMap<String, HashMap<String, Vec<&'a FnDecl>>>, /// [M-blanket-method-resolve] (merge: main 169.2 `ca85c001` ↔ U.2.3 shared registry). /// Имена blanket-методов — `fn[T] T @m`, где receiver = собственный type-параметр /// функции (recv.type_name ∈ f.generics). Метод применим к ЛЮБОМУ типу, но в реестре /// лежит под ключом параметра ("T"), не под конкретным типом → `f3_check_member` для /// `str.m()`/`UserType.m()` (тип ∈ self.types) его не находил → ложный E7320. Примитивы /// (int) случайно проходили (их нет в self.types → ранний return). Заполняется здесь, /// в `TypeCheckCtx::build` (детекция по `module.items`), независимо от `sig`-реестра. blanket_method_names: HashSet<String>, /// Ф.1: объявления типов — для разворачивания alias/newtype при /// категоризации (assignability сравнивает категории, не имена). types: HashMap<String, &'a TypeDecl>, /// [M-p67-path-call-const-receiver-method-ice]: top-level `const NAME TYPE /// = value` name → its declared `TYPE`, for every const in the merged CU /// (`module.items`, built once in `build`). Consts WITHOUT an explicit type /// annotation (`const X = 120`, inferred from `value`) are absent here — /// out of scope for this fix. Consumed by `infer_method_call_channel_type`'s /// Path-shape arm: a SCREAMING_SNAKE_CASE const receiver (`BUDGET_MS. /// to_millis()`) is folded by parser/mod.rs's PascalCase path-collector /// into `ExprKind::Path(["BUDGET_MS", "to_millis"])` (a spelling-only /// heuristic, blind to "is this actually a bound identifier") instead of /// the `Member{obj: Ident, name}` shape a lowercase-first variable /// receiver of the SAME method gets — this table lets the checker /// recognize the receiver anyway and route it through the identical /// instance-method resolution a Member-form call already uses. Types and /// consts are disjoint Nova DECL namespaces, so a name present here is /// NEVER also a genuine type/module Path receiver (`Monotonic.now()`) — /// unambiguous. const_types: HashMap<String, TypeRef>, /// [M-assoc-const-chained-method-call-p67] (окно №73, реестр 221.1 №73): /// `(owner_type, const_name) → declared TypeRef` for every out-of-body /// (D200 AMEND) assoc-const in the merged CU (`self.types`'s `TypeDecl. /// assoc_consts`, built once in `TypeCheckCtx::build`). Mirrors /// `const_types` above one level deeper: a chained method call directly /// on a bare assoc-const RECEIVER — `StatusCode.NOT_FOUND.into_response()`, /// no intermediate binding — is folded by the SAME PascalCase path- /// collector (parser/mod.rs `starts_uppercase` loop, ~8797) into a flat /// 3-segment `ExprKind::Path(["StatusCode", "NOT_FOUND", "into_response"])` /// instead of the `Member{obj: Member{obj: Path[2], name}, name}` shape a /// bound-then-called receiver would get. Consumed by /// `infer_method_call_channel_type`'s 3-segment Path arm: recognizes /// `parts[0..2]` as a known assoc-const receiver and routes `parts[2]` /// through the IDENTICAL instance-method resolution the 2-segment /// top-level-const arm already uses. Assoc-consts WITHOUT an explicit type /// annotation are absent here (out-of-body syntax always carries one — /// D200 AMEND grammar — so this is expected to be total in practice, same /// posture as `const_types`). assoc_const_types: HashMap<(String, String), TypeRef>, /// Plan 214 (D429): `#coerce` pair registry (I type name → applicable /// pairs), consumed by `assignable`'s accept-path fallback (tried AFTER /// the single-wrapper fallback — R11 makes the two mutually exclusive by /// construction, but the ordering mirrors the design note anyway). Owned /// (not `&'a`) — built by `collect_coerce_pairs`, independent parallel /// scan of `MapLitCtx`'s own copy (same data, R9 "one window"; see that /// function's doc for why two independent builds stay in sync). coerce_pairs: HashMap<String, Vec<CoercePairEntry>>, /// Plan 214.1 (D429 amend, R14 RETRACTED): GENERIC `#coerce` patterns — /// receiver base type NAME (`Json`) → applicable patterns, consumed by /// `assignable`'s accept-path fallback AFTER the concrete `coerce_pairs` /// lookup above misses (see `generic_coerce_lookup`). Same /// `collect_coerce_pairs` collector, same parallel-scan-twice shape as /// `coerce_pairs` (R9 "one window"). generic_coerce_patterns: HashMap<String, Vec<GenericCoercePattern>>, /// Plan 214.1: the enclosing `#coerce` fn's OWN declaration span while its /// body is being checked — `None` outside any `#coerce` fn body. Feeds /// R13' (anti-self-recursion): a GENERIC pattern's own declaration is /// excluded from `generic_coerce_lookup` candidates while checking that /// SAME declaration's body (`Json[T] @data() -> T => @` would otherwise /// rewrite `@` into `@.data()` — infinite recursion). RAII-published by /// `CoerceSelfGuard`, mirrors `current_fn_return_ty`'s pattern. current_coerce_decl_span: std::cell::RefCell<Option<Span>>, /// [M-compress-checksum-structvariant-ctor-xmodule] (Plan 173 P1): ВСЕ /// имена sum-вариантов (payload и unit), собранные по `module.items` /// НАПРЯМУЮ (Vec-обход — теряет ноль записей), в отличие от `types` /// (HashMap, keyed по имени суммы — при co-presence НЕСКОЛЬКИХ /// одноимённых sum-типов из разных модулей, например `ErrorKind` в /// http/io/compress, `types.insert` перезаписывает — выживает только /// ПОСЛЕДНИЙ; варианты остальных исчезают из `types.values()`). /// E_UNKNOWN_TYPE-гейт (RecordLit variant-ctor) обязан видеть variant-имя /// вне зависимости от того, чья одноимённая сумма победила слот в `types`. sum_variant_names: HashSet<String>, /// [M-198-f4c-1-privfile-type-not-discriminated] (originally `priv(file) /// type`-only) + [M-fmt-write-protocol-collision-cycle-adjacent] (2026-07-21, /// broadened to EVERY type decl): lossless per-file overlay — same /// collision class as `sum_variant_names` above (`types` last-write-wins /// on bare name), но для TYPE SHAPE / protocol-identity resolution. D307 /// already file-discriminates fn/method symbols (`sig.fn_decls: /// Vec<&FnDecl>` + caller-file filter в `f1_check_call`, fix 2d5f64e91) — /// types had no analogous multi-candidate registry, so two DIFFERENT /// modules declaring their OWN same-named `export type` (e.g. `std.io. /// Write` — `flush()`-bearing — vs `std.prelude.protocols.Write` — bare /// `@write` — a REAL production collision, not merely `priv(file)`) /// collapsed onto ONE slot in `types`; whichever decl the transitive- /// import merge order happened to insert LAST won CU-wide, for every /// reference regardless of which module's `Write` it actually meant — an /// import cycle in an unrelated part of the graph can flip that order /// with zero change at either `Write` declaration (see `docs/plans/wip/ /// write-collision-notes.md`, `protocol_mismatch_found`'s fix). Keyed by /// declaring file_id → name → decl; empty for every non-colliding name /// (the common case) — byte-parity preserved for callers that don't /// consult it. Read via `types_get_for_file`. file_local_types: HashMap<crate::diag::FileId, HashMap<String, &'a TypeDecl>>, /// Plan 81 Ф.2: префиксы импортированных модулей (alias + последний /// сегмент пути import'а) — для резолва module-qualified вызовов /// `alias.func(...)`. imported_modules: HashSet<String>, /// Plan 162 Ф.5: last-segments/aliases of modules imported DIRECTLY by /// entry-module peer files (is_entry_module = true). Excludes transitive /// imports from imported-module peers. Used for extension method policy: /// an extension method is accessible iff its declaring module's last-segment /// or alias is in this set. entry_imported_modules: HashSet<String>, /// Plan 114.4.2 (D199): const fn names в текущем модуле — для /// scope-local Stmt::Const RHS validation (calls к const fn разрешены). const_fn_names: HashSet<String>, /// Plan 114.4.2 (D199): flag — we are inside const fn body. Scope-local /// const validation skipped (body checker `check_const_fn_decl` covers /// param/local awareness more precisely). in_const_fn: std::cell::Cell<bool>, /// Plan 124 (D220): current receiver type — set перед walking method body /// в check_fn; cleared on exit. `None` если мы НЕ внутри type-method /// (free fn, top-level expr). f3_check_member использует для priv field /// access check: если field.priv_field И current_recv_type != obj's type /// → emit E_PRIV_FIELD_READ. current_recv_type: std::cell::RefCell<Option<String>>, /// Field-launder channel ([M-router-handler-mut-capture-escape-soundness] /// §2, срочный пакет звучности, owner decision 2026-08-01): is the /// CURRENT method's receiver `mut` (`fn T mut @m`) or `ro`/default /// (`fn T @m`)? Set alongside `current_recv_type` in `f1_check_fn` (same /// `PrivRecvGuard` RAII, restored on exit) — a SEPARATE cell because L1 /// receiver-mutability is orthogonal to `current_recv_type`'s L2/name /// tracking (D246: receiver `is_mut` is a `Receiver`-struct bool, never /// baked into `scope["@"]`'s `TypeRef`). Consumed by /// `check_readonly_source_coerce`'s NEW Member-arm (`@field` read) to /// decide whether the self-field read is sourced from a ro receiver. current_recv_is_mut: std::cell::Cell<bool>, /// Plan 174.2 Ф.B (cross-carrier `?` diagnostics): declared return type of /// the enclosing fn, set in `f1_check_fn`, cleared on exit. Lets the /// `ExprKind::Try` arm know the return CARRIER (`Result` vs `Option`) and /// error type so a carrier-mismatched `?` gets a specific fix-it hint /// (`.ok_or(..)` / `.ok()` / `.map_err(..)`) instead of a generic error. current_fn_return_ty: std::cell::RefCell<Option<TypeRef>>, /// `[M-202-ident-x-module-alias-collision]` follow-up fix (generic-match- /// scope-gap, 2026-07-21): the enclosing fn's OWN generic params (name + /// bounds), set in `f1_check_fn`, cleared on exit — mirrors /// `current_fn_return_ty`'s RAII pattern exactly. `TypeCheckCtx` has no /// general generic-bound machinery (that lives in the separate, heavier /// `BoundCtx` checker pass) — this is a narrow, ADDITIVE carrier for /// EXACTLY one consumer: `ExprKind::Match`'s scrutinee-type resolution /// (`f1_expr_inner`), used ONLY to widen `match_arm_bindings`'s pattern- /// bound `scope` extension when the scrutinee is a call to a method on a /// GENERIC-PARAM receiver bound to a known protocol (e.g. `@next()` on /// receiver `I` bound `I Next[T]` inside `fn[I Next[T], T] I mut @min()`). /// Without this, `infer_expr_type`'s deliberately-DECOUPLED general /// instance-method-call return inference (see its own doc, ~15605 — /// "GENERAL instance method-call return inference is DECOUPLED from /// `infer_expr_type`") returns `None` for ANY `match obj.method() {...}` /// scrutinee, generic or not — but a NON-generic concrete receiver still /// gets its arm-bindings scope-extension via codegen's OWN later channel /// (`resolved_callees`/`infer_method_call_channel_type`), so only the /// generic-bound-receiver case is a genuine gap (no other channel ever /// resolves `Option[T]` from an ABSTRACT `T` at check-time). Fixing the /// general decoupling is out of scope (perturbs other `infer_expr_type` /// consumers per that fn's own doc) — this is the minimal, local carrier /// needed for the ONE narrow fallback in the `Match` arm, nowhere else. current_fn_generics: std::cell::RefCell<Vec<GenericParam>>, /// Plan 124.6 (D225): current fn's `#test_access(TypeA, TypeB, ...)` list. /// Если non-empty, current fn body получает priv-field access ко всем /// перечисленным types (escape hatch для unit tests + sibling helper /// fns). Cleared on fn exit. Combines с current_recv_type для allow-list. current_fn_test_access: std::cell::RefCell<Vec<String>>, /// Plan 124.8 (D175 amend): set of `ro`-bound identifier names в текущем /// scope. Когда target assignment пути starts с Ident name ∈ этого /// set'а — binding dominates даже над `mut field` markers. /// Закрывает D175 §«binding dominates» — Rust-style rule. /// Tracks через f1_stmt Stmt::Let; cleared on scope exit (block end). ro_binding_names: std::cell::RefCell<std::collections::HashSet<String>>, /// №309/№317 (221.1, окно p-ovl-channel): mirror of `ro_binding_names` /// for `consume`-bound identifiers — set of local names whose CURRENT /// binding was introduced via `consume x = ...` (`LetDecl.consume`) or a /// `consume`-marked fn param (`Param.consume`). Independent axis from /// `ro_binding_names` (a `consume` binding is ALSO non-`mut`, so it lands /// in both sets — the two questions "can I write through this name" and /// "does this name carry a linear/consume obligation" are orthogonal). /// Consumed by `expr_mode_axis_consume_eligible` — the channel-first fix /// for `[M-309-narrow-by-param-mode-binding-form]`: a NAMED consume-bound /// argument now qualifies a `consume`-mode overload parameter, not only a /// syntactic rvalue temporary (mirrors codegen's rejected `var_consume` /// idea from the reverted 9fde1af15, but lives in the CHECKER channel /// per §0, not in `emit_c.rs`). Same snapshot/restore discipline as /// `ro_binding_names` (fn-entry params, `f1_block` block scope). consume_binding_names: std::cell::RefCell<std::collections::HashSet<String>>, /// Plan 172.5 (D326 R10): names of the current function's `mut ref` /// parameters. A `ref` borrow lives only for the synchronous call — it must /// NOT escape (no store, no closure/spawn capture, no return; return/store /// are already impossible since `ref` is not a type). This set drives the /// closure/spawn capture ban (`E_REF_ESCAPE_CAPTURE`). Populated at fn-body /// entry, restored at exit. mut_ref_param_names: std::cell::RefCell<std::collections::HashSet<String>>, /// Plan 138.2 Ф.0c (D29 method-level shadow): names of generic types /// imported into the merged module that the user has REDECLARED in /// entry-peer-files with a DIFFERENT arity (e.g. user `type Vec { x, y }` /// = arity 0 shadowing imported `type Vec[T]` = arity 1). The user /// declaration wins **entirely** (D29 full-shadow): the imported generic /// type's merged methods (`Vec[T] @push`, …) carry `Vec[T]` type-refs that /// would arity-check (E7310) against the user's non-generic `Vec`. We skip /// arity-checking those merged methods so they do not leak onto the /// user-shadowed receiver. Built in `build`. Empty in the common (no-shadow) /// case → zero overhead. Maps the shadowed name → the user's declared /// arity (so a merged method whose receiver carries a DIFFERENT carrier /// arity is recognised as the import's method and skipped, while a method /// the user wrote on their own redeclared type — matching carrier arity — /// is kept). user_shadowed_generic_types: HashMap<String, usize>, /// Plan 160 (D281) Ф.2: type name → declaring module name segments. /// Built from `module.peer_files.items_here` in `build`. Used by /// `module_priv_access_allowed` to compare with `current_module`. /// Types imported from other modules retain the module name from which /// they came (their type decl's module_name). Empty for types with /// unknown origin (imported without peer_files → conservative = deny). type_defining_modules: HashMap<String, Vec<String>>, /// Plan 160 (D281) Ф.2: module name of the code currently being /// checked — set to `module.name` at the start of `check_module`. /// Used together with `type_defining_modules` for module-boundary /// enforcement. Empty = no current module (conservative = deny). current_module: std::cell::RefCell<Vec<String>>, /// D281 follow-up (2026-07-08): file_id → module name владельца файла. /// Наполняется в `check_module` из `module.peer_files`. Нужна, чтобы /// module-boundary проверка судила доступ по модулю ФАЙЛА-СПАНА /// (где физически живёт код), а не по `current_module` CU: тело /// generic-метода из hashmap.nv, перечитываемое в CU потребителя, /// законно читает module-private поля своего модуля. file_modules: std::cell::RefCell<std::collections::HashMap<crate::diag::FileId, Vec<String>>>, /// Plan 162 Ф.3: global TypeMethodMap — type_name → method_name → /// list of module_names (Vec<String>) that declared this method. /// Built from `module.peer_files[*].items_here` in `build`. /// Inherent method: method declared in the SAME module as its type T /// (type_method_map[T][m] ∩ type_defining_modules[T] ≠ ∅). /// Extension method: method declared in a DIFFERENT module from T. /// Used by `is_inherent_method` to decide resolution priority. type_method_map: HashMap<String, HashMap<String, Vec<Vec<String>>>>, /// Plan 162 Ф.5: FileIds of entry-module peer files (is_entry_module=true). /// Extension method policy only fires for call sites in entry-module files. /// Prevents false positives from type-checking imported stdlib method bodies. entry_file_ids: HashSet<crate::diag::FileId>, /// Plan 124.6 (D224 §4): true when the checker is inside a `test "…" { }` /// block body. Controls rule-2 (implicit test grant — same-module access) /// and rule-3 (explicit `test_access` list check on the TestDecl itself). /// Set/cleared via `TestBlockGuard` RAII on `Item::Test` entry/exit. in_test_block: std::cell::Cell<bool>, /// Plan 124.6 (D224 §4): the `test_access` list from the *current* test /// block (populated from `TestDecl.test_access`). Non-empty only when /// `in_test_block` is true. Cleared on exit via `TestBlockGuard`. test_block_test_access: std::cell::RefCell<Vec<String>>, /// Plan 124.6 (D224 §4 rule-4): type name → pub_to friend list. /// Built in `build` from `TypeDecl.pub_to`. A `current_recv_type` that /// appears in `type_pub_to[tname]` gets priv-field access to `tname`. type_pub_to: HashMap<String, Vec<String>>, /// Plan 162.1 Step 2: cross-module signature table — maps declared module /// name to the set of type names and fn names it exports. Built lazily by /// `build_with_sig_table`; empty in the default `build` path (no I/O at /// type-check time). Used by `is_known_type` / `is_known_fn` to answer /// "does any imported module define symbol X?" without a full resolve. sig_table: crate::imports::ModuleSigTable, /// Plan 172.1 U.3.4: write-buffer for the per-call resolved-CALLEE channel /// (call-site `ExprId` → chosen callee `FnDecl` declaration `Span`). `f1_check_call` /// records WHICH `FnDecl` it resolved each call to; extracted into /// `ModuleEnv.resolved_callees` after the check pass. Interior-mutable because the /// check walk is `&self` (mirrors the other `RefCell` side-tables here). ADDITIVE — /// not yet consumed by codegen (U.4.3), so byte-identical. resolved_callees: std::cell::RefCell<HashMap<crate::ast::ExprId, Span>>, /// Plan 196.5 Stage-A: write-buffer for the per-call SUBST-VALUE channel (call-site /// `ExprId` → ordered `(generic-param name → concrete ResolvedType)`), mirrors /// `resolved_callees`'s plumbing exactly. `f1_check_call` (free-fn/static generic) and /// `resolve_return_channel` (instance-method carrier+method-level) already COMPUTE the /// full subst map for type-checking the call and used to discard it after applying it to /// the return type (196.4); this buffer captures the map itself instead of throwing it /// away. Order = generic-param DECLARATION order (carrier-generics of the receiver, then /// method-level) — mono-manging (`compute_mono_name`) needs positional order, not just /// names. ADDITIVE — not yet consumed by codegen (Stage-B), so byte-identical. /// Gated: only written when ALL of the callee's generic params resolved to a fully- /// concrete type (no residual `TypeParam`) — an erased-body caller leaves the channel /// unwritten for that call-site, exactly like `resolved_types_buf`'s materialize-only- /// when-fully-resolved contract. [M-196.5-node-substs] node_substs: std::cell::RefCell<HashMap<crate::ast::ExprId, Vec<(String, ResolvedType)>>>, /// Plan 172.1 U.4.4(b): per-expr resolved-type channel the checker fills during the /// scope-aware `f1_expr` walk (expr `ExprId` → its `ResolvedType`), for the structural / /// semantic arms the SYNTACTIC `number_exprs` producer cannot reach. FIRST arm: `Ident` /// (var-types from scope), gated to skip bare generic-params (their codegen lowering is /// erased `void*` / mono-substituted, which the generic-level annotation cannot reproduce). /// Lifted into `ModuleEnv.resolved_types` (merged OVER the `number_exprs` seed). Codegen /// reads it AUTHORITATIVELY (U.4.5 flip) — a BEHAVIOR-CHANGE: it FIXES legacy /// `infer_expr_c_type` `_=>nova_int` fallback bugs (§0/§1; e.g. a `bool` var legacy typed /// as `nova_int`). Verified by full regress (the divergence set is bounded — U.4.4-prep.b audit). resolved_types_buf: std::cell::RefCell<HashMap<crate::ast::ExprId, ResolvedType>>, /// №279 [M-nested-err-pattern-shared-variant-wrong-enum-tag]: per-pattern /// resolved-SUM-NAME channel (bare `Pattern::Variant`'s OWN `span` → the /// Nova sum type's simple name it was resolved against, e.g. /// `Err(OnlySign)`'s inner `OnlySign` span → `"ParseBigRatError"`). /// `resolve_pattern_variant_types` walks a match/if-let/while-let/for /// pattern against the STRUCTURAL scrutinee type (descending through /// Option[T]/Result[T,E]/general-sum tuple-variant payload positions, /// recursively) and writes here whenever a bare (single-segment) variant /// name unambiguously names a declared variant of the scrutinee's own /// sum type at that position. Consumed by codegen's `pattern_cond` /// BEFORE it falls back to `sum_schema_registry.find_variant_compat`'s /// first-registered-wins heuristic — the bug this closes: two DIFFERENT /// enums sharing a variant name (`ParseBigIntError::OnlySign` / /// `ParseBigRatError::OnlySign`) both match a bare `OnlySign` /// sub-pattern regardless of which enum the enclosing `Err(..)` /// scrutinee's `E` actually is. Defensive: only written on genuine, /// unambiguous structural resolution — every case the old heuristic /// already handled correctly stays untouched (the channel is consulted /// FIRST, not exclusively; `find_variant_compat` remains the fallback /// for spans this pass didn't reach/resolve). pattern_variant_types_buf: std::cell::RefCell<HashMap<Span, String>>, /// Plan 221.1 №286 residual gap (window p286, 2026-08-04): a BARE /// `Channel.new(cap)` (no turbofish, no `ChanWriter[T]`/`ChanReader[T]` /// annotation) left `T` permanently untracked (window p-chan, №143/№286 /// core fix) — `docs/guide/channels.md` explicitly documented this as /// accepted, undocumented-inference-never-held permissiveness. Measured /// this window (`docs/plans/repros/p286-bare-channel-erased/ /// bare_len_probe.nv`, RED without this fix): that permissiveness is /// exploitable, not just "no compile-time guarantee" — an untyped /// channel's erased `recv()` result still routes `.method()` calls /// through legacy name-only `method_receivers` codegen dispatch, so a /// co-present unrelated type sharing a method name (`.len()`) can /// SILENTLY win over the real element type. Fix: honor the channels.md /// promise for real — infer `T` from the first `.send`/`.try_send` call /// on the writer, textually later in the SAME block (lookahead only, no /// nested-block recursion — conservative). Keyed by the `Channel.new` /// call expr's own `ExprId` (computed once per enclosing `f1_block` /// pre-pass, consumed by the existing Tuple/Record destructure arms in /// `f1_stmt` as a fallback AFTER `channel_new_turbofish_elem`). Strictly /// additive: absent entry ⇒ unchanged pre-p286 behavior. channel_bare_send_elem_hint: std::cell::RefCell<HashMap<crate::ast::ExprId, TypeRef>>, /// [M-crossmodule-samename-typecheck-bleed] (221.1 Ф.2 №28, 2026-07-23): /// `ResolvedType::Named` carries no span/file identity (`{name, module, /// args}` only) — reconstructing a `TypeRef` from it for the checker's /// scope-binding channel (`resolved_to_typeref`) had NO choice but to /// re-stamp the CALL SITE's own `expr.span`, discarding the CALLEE's /// OWN return-type annotation span (where the type name was actually /// WRITTEN, in the callee's declaring file). Once two DIFFERENT modules /// of the same combined CU declare a same-named type (`Widget`), a /// bare `ro w = other_module_fn()` binding in a THIRD file that never /// itself mentions `Widget` resolved the field-access `w.field` against /// whichever same-named `Widget` won the GLOBAL name-only `self.types` /// slot (last declaration wins) instead of the callee's OWN module's /// `Widget` — false `[E7320] no field ... on type Widget`. This side- /// table records, for the free-fn/static-method single-overload call- /// return materialization site (`f1_check_call`, the ONLY current /// producer), the callee's OWN return-type annotation span — keyed by /// the CALL expr's `ExprId` — so the Call-arm reader in /// `infer_expr_type` can hand `resolved_to_typeref` the CALLEE's file /// identity instead of the caller's. Empty for any call this producer /// doesn't cover (generic/instance/multi-overload calls) — reader falls /// back to `expr.span`, today's existing (unchanged) behavior. call_return_decl_span: std::cell::RefCell<HashMap<crate::ast::ExprId, Span>>, /// 172.1.2 Binary-bounds (2026-07-03): generic-параметры ТЕКУЩЕЙ проверяемой fn /// с numeric type-set bound'ом (все члены сета — числовые примитивы). Ставится /// f1_check_fn на вход, очищается на выход. Для правила Binary /// (TypeParam(a),TypeParam(a)) → TypeParam(a): D46-риск снят bound'ом — /// на примитивах type-set operator-overload невозможен, арифметика сохраняет тип. numeric_bounded_params: std::cell::RefCell<HashSet<String>>, /// 172.1.2 Шаг 2 (same-name fix): f1 Call-арм ставит перед рекурсией в func, /// f1 Member-арм потребляет (replace(false)) — отличает `v.len()` (метод) /// от `@len` (field-read) при same-name field/method (Vec.len, str.len). in_call_func: std::cell::Cell<bool>, /// №367 (window p375-ptr2, D216/174.5 read-parity): mirrors `in_call_func`'s /// set-before/consume-on-entry protocol. `Stmt::Assign` sets this `true` /// immediately before its own `f1_expr(target, ..)` call; the /// `ExprKind::Unary`/`ExprKind::Index` arms consume it via /// `replace(false)` on entry — `true` ONLY for the exact top-level /// assignment-target node (`*p = v` / `p[i] = v`), `false` for every /// nested/recursive visit (including the SAME node reached any other /// way). Lets the READ-form pointer-op retirement check /// (`*p`/`p[i]` used as an rvalue) skip the exact span /// `check_target_readonly`'s WRITE-form arms already diagnose, without /// silently under-covering a nested read (`**p = v` — outer deref is the /// write target, inner is a genuine read). assign_target_top: std::cell::Cell<bool>, /// Plan 104.10 Ф.2 (D379): OPT-IN flag — when `true` the `f1_expr` walk records each /// expression's inferred type into `expr_types_buf` (lifted into `ModuleEnv.expr_types` /// for the IDE). `false` in the normal `check_module` path → ZERO overhead (the map /// stays empty, no per-node inference). Set ONLY by `check_module_with_expr_types`. Plain /// `bool` (not interior-mutable): fixed at build time, never mutated during the walk. record_expr_types: bool, /// Plan 104.10 Ф.2 (D379): write-buffer for the opt-in IDE per-expression type map /// (expr `Span` → inferred `TypeRef`). Filled by `record_expr_type_ide` during the /// scope-aware `f1_expr` walk ONLY when `record_expr_types` is set; extracted into /// `ModuleEnv.expr_types` after the check pass. Interior-mutable because the walk is /// `&self` (mirrors `resolved_types_buf` / `resolved_callees`). Empty (untouched) in the /// default compile path. expr_types_buf: std::cell::RefCell<HashMap<Span, TypeRef>>, } /// Plan 114.4.2 D199: RAII guard для in_const_fn flag. /// Restoring previous value on drop — works regardless of error path. struct ConstFnFlagGuard<'a, 'b> { ctx: &'b TypeCheckCtx<'a>, prev: bool, } impl<'a, 'b> Drop for ConstFnFlagGuard<'a, 'b> { fn drop(&mut self) { self.ctx.in_const_fn.set(self.prev); } } /// Plan 124 (D220): RAII guard для current_recv_type field в TypeCheckCtx. /// Restoring previous value on drop — works regardless of error path. struct PrivRecvGuard<'a, 'b> { ctx: &'b TypeCheckCtx<'a>, prev: Option<String>, /// Field-launder channel (§2, 2026-08-01): previous `current_recv_is_mut` /// value, restored alongside `current_recv_type`. Defaults to `false` /// (via `..Default` construction sites below being updated to pass it) /// so pre-existing call sites keep compiling — see the two construction /// sites in `f1_check_fn`. prev_mut: bool, } impl<'a, 'b> Drop for PrivRecvGuard<'a, 'b> { fn drop(&mut self) { *self.ctx.current_recv_type.borrow_mut() = self.prev.take(); self.ctx.current_recv_is_mut.set(self.prev_mut); } } /// Plan 174.2 Ф.B: RAII guard для current_fn_return_ty в TypeCheckCtx. struct FnReturnTyGuard<'a, 'b> { ctx: &'b TypeCheckCtx<'a>, prev: Option<TypeRef>, } impl<'a, 'b> Drop for FnReturnTyGuard<'a, 'b> { fn drop(&mut self) { *self.ctx.current_fn_return_ty.borrow_mut() = self.prev.take(); } } /// Plan 214.1 (D429 amend, R13'): RAII guard for `current_coerce_decl_span` /// (mirrors `FnReturnTyGuard` exactly). struct CoerceSelfGuard<'a, 'b> { ctx: &'b TypeCheckCtx<'a>, prev: Option<Span>, } impl<'a, 'b> Drop for CoerceSelfGuard<'a, 'b> { fn drop(&mut self) { *self.ctx.current_coerce_decl_span.borrow_mut() = self.prev.take(); } } /// generic-match-scope-gap fix: RAII guard для current_fn_generics в /// TypeCheckCtx (mirrors `FnReturnTyGuard` exactly). struct FnGenericsGuard<'a, 'b> { ctx: &'b TypeCheckCtx<'a>, prev: Vec<GenericParam>, } impl<'a, 'b> Drop for FnGenericsGuard<'a, 'b> { fn drop(&mut self) { *self.ctx.current_fn_generics.borrow_mut() = std::mem::take(&mut self.prev); } } /// 172.1.2 Binary-bounds: RAII-восстановление numeric_bounded_params. struct NumericBoundGuard<'a, 'b> { ctx: &'b TypeCheckCtx<'a>, prev: HashSet<String>, } impl<'a, 'b> Drop for NumericBoundGuard<'a, 'b> { fn drop(&mut self) { *self.ctx.numeric_bounded_params.borrow_mut() = std::mem::take(&mut self.prev); } } /// Plan 124.6 (D225): RAII guard для current_fn_test_access vec в TypeCheckCtx. struct PrivTestAccessGuard<'a, 'b> { ctx: &'b TypeCheckCtx<'a>, prev: Vec<String>, } impl<'a, 'b> Drop for PrivTestAccessGuard<'a, 'b> { fn drop(&mut self) { *self.ctx.current_fn_test_access.borrow_mut() = std::mem::take(&mut self.prev); } } /// Plan 160 (D281) Ф.2: RAII guard для current_module в TypeCheckCtx. /// Restores previous module name on drop. struct CurrentModuleGuard<'a, 'b> { ctx: &'b TypeCheckCtx<'a>, prev: Vec<String>, } impl<'a, 'b> CurrentModuleGuard<'a, 'b> { fn set(ctx: &'b TypeCheckCtx<'a>, module: Vec<String>) -> Self { let prev = std::mem::replace(&mut *ctx.current_module.borrow_mut(), module); CurrentModuleGuard { ctx, prev } } } impl<'a, 'b> Drop for CurrentModuleGuard<'a, 'b> { fn drop(&mut self) { *self.ctx.current_module.borrow_mut() = std::mem::take(&mut self.prev); } } /// Plan 124.6 (D224 §4): RAII guard для in_test_block + test_block_test_access /// в TypeCheckCtx. Set to (true, access_list) on entry; restored on drop. struct TestBlockGuard<'a, 'b> { ctx: &'b TypeCheckCtx<'a>, prev_in_test: bool, prev_access: Vec<String>, } impl<'a, 'b> TestBlockGuard<'a, 'b> { fn enter(ctx: &'b TypeCheckCtx<'a>, access: Vec<String>) -> Self { let prev_in_test = ctx.in_test_block.get(); ctx.in_test_block.set(true); let prev_access = std::mem::replace(&mut *ctx.test_block_test_access.borrow_mut(), access); TestBlockGuard { ctx, prev_in_test, prev_access } } } impl<'a, 'b> Drop for TestBlockGuard<'a, 'b> { fn drop(&mut self) { self.ctx.in_test_block.set(self.prev_in_test); *self.ctx.test_block_test_access.borrow_mut() = std::mem::take(&mut self.prev_access); } } /// `true` для имён, у которых arity **не** проверяется: referential-типы /// и эффекты с sugar/гибкой арностью. fn arity_exempt(name: &str) -> bool { matches!( name, // referential / top / bottom "Self" | "any" | "never" | "Never" // Fail[E] ≡ bare Fail (D65); Effect[E] ≡ Effect[E, never] (D88) // Plan 97 Ф.3 (D142): `Handler` → `Effect`. | "Fail" | "Effect" // built-in эффекты с параметрами — не объявлены как Item::Type, // в таблицу не попадут; перечислены явно для ясности | "Ask" | "Alloc" ) } /// Plan 126 (D230) Ф.4: bridge между TypeCheckCtx и auto_derive::DeriveQuery. /// /// Owns shared reference к TypeCheckCtx, exposes lookup_type / has-method /// queries по API contract auto_derive::DeriveQuery. Без этой обёртки /// auto_derive вынужден был бы знать про полный TypeCheckCtx (cycle через /// `types` модуль). pub(crate) struct AutoDeriveQueryBridge<'a> { ctx: &'a TypeCheckCtx<'a>, } /// Plan 126.2 Ф.1: build-time `DeriveQuery` над уже-построенными `types` + /// `method_table` (до конструирования `TypeCheckCtx`). Используется /// `register_synthesized_methods` для синтеза auto-derive методов на этапе /// `build`. pub(crate) struct BuildTimeDeriveQuery<'a, 'b> { types: &'b HashMap<String, &'a TypeDecl>, method_table: &'b HashMap<String, HashMap<String, Vec<&'a FnDecl>>>, } impl<'a, 'b> crate::protocols::auto_derive::DeriveQuery for BuildTimeDeriveQuery<'a, 'b> { fn lookup_type(&self, name: &str) -> Option<&TypeDecl> { self.types.get(name).copied() } fn type_provides_method(&self, t: &str, method_name: &str) -> bool { self.method_table.get(t).map_or(false, |m| { m.keys().any(|k| k.trim_start_matches('@') == method_name) }) } } impl<'a> crate::protocols::auto_derive::DeriveQuery for AutoDeriveQueryBridge<'a> { fn lookup_type(&self, name: &str) -> Option<&TypeDecl> { // TypeCheckCtx.types: HashMap<String, &'a TypeDecl>. Dereferences // outer reference twice (Option<&&'a TypeDecl> → Option<&TypeDecl>). self.ctx.types.get(name).copied() } fn type_provides_method(&self, t: &str, method_name: &str) -> bool { self.ctx.t_provides_method(t, method_name) } } impl<'a> TypeCheckCtx<'a> { fn build( module: &'a Module, synth_arena: &'a FnDeclArena, sig: &'a crate::sig_registry::SigRegistry<'a>, ) -> Self { let mut arity: HashMap<String, ArityInfo> = HashMap::new(); // U.2.3.3: fn_decls/method_table read from `sig` (shared base registry); // only `types` is still collected here (needed locally below). let mut types: HashMap<String, &'a TypeDecl> = HashMap::new(); // [M-198-f4c-1-privfile-type-not-discriminated]: lossless per-file // overlay populated alongside `types` below — see field doc. let mut file_local_types: HashMap<crate::diag::FileId, HashMap<String, &'a TypeDecl>> = HashMap::new(); // [M-blanket-method-resolve]: names of blanket methods (`fn[T] T @m`). let mut blanket_method_names: HashSet<String> = HashSet::new(); // [M-compress-checksum-structvariant-ctor-xmodule] (Plan 173 P1): ВСЕ // sum-variant-имена ЛОССЛЕСС (тот же Vec-обход `module.items`, что // строит `types` ниже) — параллельно `types`-HashMap, который при // co-presence нескольких одноимённых sum-типов из разных модулей // (например `ErrorKind` в http/io/compress) перезаписывает запись — // выживает только ПОСЛЕДНИЙ, варианты остальных пропадают из // `types.values()`. E_UNKNOWN_TYPE-гейт (RecordLit variant-ctor) // обязан видеть variant-имя вне зависимости от того, чья одноимённая // сумма победила слот в `types`. let mut sum_variant_names: HashSet<String> = HashSet::new(); for item in &module.items { match item { Item::Fn(f) => { // [M-blanket-method-resolve] (merge: main 169.2 ↔ U.2.3): detect a // blanket method `fn[T] T @m` (receiver IS one of the fn's own // type-params → applicable to ANY concrete type). U.2.3: `fn_decls`/ // `method_table` are built in `SigRegistry::build_base`, NOT here — so // ONLY the blanket-NAME detection lives here (consumed by // `f3_check_member` to accept the method on `self.types` types, not just // primitives, where the registry keys it under the param `"T"`). if let Some(recv) = &f.receiver { if f.generics.iter().any(|g| g.name == recv.type_name) { blanket_method_names.insert(f.name.clone()); } } } Item::Type(td) => { types.insert(td.name.clone(), td); // [M-198-f4c-1-privfile-type-not-discriminated] (was priv(file)- // only) + [M-fmt-write-protocol-collision-cycle-adjacent] // (2026-07-21, generalized): record EVERY type decl под своим // file_id — `types.insert` above just overwrote a same-name // cross-module collision (if any: e.g. TWO DIFFERENT protocols // both named `Write` — `std.io.Write` (`flush()`-bearing) vs // `std.prelude.protocols.Write` (bare `@write`) — whichever // lands LAST in the merged transitive-import order wins the // ONE slot in `types`, an order that a merely-ADJACENT // inter-module import cycle elsewhere in the graph can flip // with zero change at either Write declaration; empirically // confirmed via instrumented trace, `docs/plans/wip/ // write-collision-notes.md`). This per-file overlay keeps // EVERY same-named decl recoverable by its OWN declaring file // — was gated to `td.file_private` only (the narrower // priv(file) case D307 already needed); broadened to ALL types // so `types_get_for_file` (below) can also disambiguate a // PLAIN `export type` collision using the REFERENCING type // annotation's own file_id (the file declaring `sink Write` — // e.g. `protocols.nv` itself — always has ITS OWN `Write` in // this per-file map, independent of merge order). Cheap: one // extra HashMap entry per type per declaring file, no new // collision class (a file cannot declare the same type name // twice). file_local_types .entry(td.span.file_id) .or_default() .insert(td.name.clone(), td); if let TypeDeclKind::Sum(vs) = &td.kind { for v in vs { sum_variant_names.insert(v.name.clone()); } } } _ => {} } } // Plan 172.1.1 (U.1): merge registry-only builtin TYPE decls (StringBuilder/WriteBuffer/ // ReadBuffer — supplied to codegen via `load_builtins`, ABSENT from `module.items`) into // `types` so the checker KNOWS them as types → `infer_expr_type(StringBuilder.new())` // resolves → `sb: StringBuilder` → `check_instance_overload` records the callee (paired // with the method-sig merge in `check_module`). `or_insert` — a module-declared type WINS // (no override). `&'static TypeDecl` coerce into `&'a` (`'static: 'a`). DETECT-mode gate: // blast-radius measured via 0-new-FAIL before keeping (§7.2). for ext_mod in crate::codegen::external_registry::builtin_sig_modules() { for item in &ext_mod.items { if let Item::Type(td) = item { types.entry(td.name.clone()).or_insert(td); if let TypeDeclKind::Sum(vs) = &td.kind { for v in vs { sum_variant_names.insert(v.name.clone()); } } } } } // Все типы (пользовательские + merged-from-imports) — для подсчёта // арности; неверная арность на импортированном типе тоже ловится. // `decl_span: None` — у импортированных/prelude-типов объявление // не в текущем файле, note «declared here» был бы с чужим/битым // span'ом (см. Plan 81 Ф.8 file_id-утечки). for item in &module.items { if let Item::Type(td) = item { arity.insert( td.name.clone(), ArityInfo { count: td.generics.len(), decl_span: None }, ); } } // Типы, объявленные в самом компилируемом модуле (entry peers' // `items_here`) — для них note «declared here» указывает на // реальный исходник пользователя. let own_items: Vec<&Item> = if module.peer_files.is_empty() { module.items.iter().collect() } else { module .peer_files .iter() .filter(|pf| pf.is_entry_module) .flat_map(|pf| pf.items_here.iter()) .collect() }; for item in own_items { if let Item::Type(td) = item { arity.insert( td.name.clone(), ArityInfo { count: td.generics.len(), decl_span: Some(td.span) }, ); } } // Prelude-типы обычно приходят как Item::Type через auto-import; // fallback на известную арность для модулей без prelude. arity.entry("Option".to_string()) .or_insert(ArityInfo { count: 1, decl_span: None }); arity.entry("Result".to_string()) .or_insert(ArityInfo { count: 2, decl_span: None }); // Примитивы — арность 0 (`int[X]` / `bool[T]` — ошибка). for prim in [ "int", "i8", "i16", "i32", "i64", "u8", "u16", "u32", "u64", "uint", "f32", "f64", "str", "bool", "char", ] { arity.entry(prim.to_string()) .or_insert(ArityInfo { count: 0, decl_span: None }); } // Plan 81 Ф.2: префиксы импортированных модулей. let mut imported_modules: HashSet<String> = HashSet::new(); let mut collect = |imports: &[Import]| { for imp in imports { if let Some(a) = &imp.alias { imported_modules.insert(a.clone()); } if let Some(last) = imp.path.last() { imported_modules.insert(last.clone()); } } }; collect(&module.imports); for pf in &module.peer_files { collect(&pf.imports); } drop(collect); // Plan 162 Ф.5: entry_imported_modules — last-segments/aliases of // modules imported DIRECTLY by entry-module peer files only. // Excludes transitive imports from imported-module peers, so that // extension method policy can distinguish "explicitly imported" from // "transitively pulled in". let mut entry_imported_modules: HashSet<String> = HashSet::new(); // Plan 162 Ф.5: entry_file_ids — FileIds of entry-module peer files. // Extension method policy only fires for call sites in these files. let mut entry_file_ids: HashSet<crate::diag::FileId> = HashSet::new(); { let mut collect_entry = |imports: &[Import]| { for imp in imports { if let Some(a) = &imp.alias { entry_imported_modules.insert(a.clone()); } if let Some(last) = imp.path.last() { entry_imported_modules.insert(last.clone()); } } }; collect_entry(&module.imports); for pf in &module.peer_files { if pf.is_entry_module { collect_entry(&pf.imports); entry_file_ids.insert(pf.file_id); } } // [M-runtime-folder-run-ice-vec-ident] self-import gap follow-on // (Plan 172.13 batch 4): a module's OWN extension methods are // trivially available to itself — no `import` of one's own // module is written or expected. Without this, a file whose // declared module name lacks the "std" prefix (a pre-existing // stdlib naming inconsistency — e.g. `std/collections/vec_lazy.nv` // self-declares bare `module collections.vec_lazy`, not // `std.collections.vec_lazy`) fails the `is_stdlib_module` early-out // above (checks `m.first() == "std"`) AND fails the // `entry_imported_modules` import-check below (the file never // imports itself) — so its OWN test block calling its OWN // extension method (`v.lazy()` inside `vec_lazy.nv` itself) was // rejected as `[E_EXTENSION_METHOD_NEEDS_IMPORT]`. Registering the // entry's own last name segment closes this regardless of the // "std"-prefix inconsistency (the real, general fix: a module is // always "imported" into itself). if let Some(last) = module.name.last() { entry_imported_modules.insert(last.clone()); } } // Plan 114.4.2 D199 + Plan 114.4.3 Ф.5 V2: precompute const fn names // для scope-local const validation. Includes const fn declarations // AND const fn aliases (`const ALIAS = const_fn_name` form). let mut const_fn_names: HashSet<String> = module.items.iter() .filter_map(|it| match it { Item::Fn(fd) => { let is_const = fd.return_is_const || fd.params.iter().any(|p| p.is_const); if is_const { Some(fd.name.clone()) } else { None } } _ => None, }) .collect(); // Pass 2: include aliases (Ident RHS resolving to const fn name). // Iterative — alias-to-alias chains supported up to depth=10. for _ in 0..10 { let mut added = false; for item in &module.items { if let Item::Const(c) = item { if let crate::ast::ExprKind::Ident(target) = &c.value.kind { if const_fn_names.contains(target) && !const_fn_names.contains(&c.name) { const_fn_names.insert(c.name.clone()); added = true; } } } } if !added { break; } } // Plan 126.2 Ф.1: register synthesized auto-derive methods into // `method_table` alongside user-written methods, so codegen // method-dispatch (and the type-checker's own resolution) finds them. // // For each type T с `#impl(P1 + P2 + ...)`, for each built-in // auto-derivable protocol P, if T does NOT already provide the // protocol's method explicitly, synthesize the FnDecl и insert // `&'a FnDecl` (allocated in `synth_arena`) into // `method_table[T][@method]`. Synthesis failures (ineligible fields, // unsupported kind, cycles) are silently skipped here — the dedicated // `verify_impl_protocols` pass re-runs synthesis и emits the // E_AUTO_DERIVE_* / E_IMPL_MISSING_METHODS diagnostics. This pass is // registration-only. // U.2.3.3 (F2): synth goes into a TypeCheckCtx-private overlay (NOT the shared // base registry). DeriveQuery reads base (`sig.method_table`) for user-method // coverage — synth is registered only where the user did not provide the method. let mut synth_methods: HashMap<String, HashMap<String, Vec<&'a FnDecl>>> = HashMap::new(); Self::register_synthesized_methods( module, synth_arena, &types, &sig.method_table, &mut synth_methods, ); // Plan 138.2 Ф.0c (D29 method-level shadow): detect generic types that // the user redeclared in entry-peer-files with a DIFFERENT arity than a // merged-from-import declaration of the same name. Such a name is a // genuine generic-type shadow (user `type Vec { x, y }` over imported // `type Vec[T]`). The merged generic type's methods reference `Name[T]` // and would arity-check (E7310) against the user's non-generic decl — // we record the name here so `check_module` skips those merged methods // (user wins entirely per D29). Mitigation RC: we record ONLY names the // user declared in entry-peers AND that also exist merged with a // different arity — a legitimate `import …Vec` *without* a user-side // `type Vec` redeclaration leaves this set empty, so dispatch is // untouched. let mut user_shadowed_generic_types: HashMap<String, usize> = HashMap::new(); { // Per-name max arity seen across the WHOLE merged module // (`module.items`) — captures the imported generic decl. let mut merged_arity: HashMap<String, usize> = HashMap::new(); for item in &module.items { if let Item::Type(td) = item { let e = merged_arity.entry(td.name.clone()).or_insert(0); *e = (*e).max(td.generics.len()); } } // User-declared types in entry-peer-files only. let user_own: Vec<&Item> = if module.peer_files.is_empty() { module.items.iter().collect() } else { module .peer_files .iter() .filter(|pf| pf.is_entry_module) .flat_map(|pf| pf.items_here.iter()) .collect() }; for item in user_own { if let Item::Type(td) = item { if let Some(&m) = merged_arity.get(&td.name) { if m != td.generics.len() { // Same name, different arity, merged-vs-user → // genuine generic shadow. User wins entirely. user_shadowed_generic_types .insert(td.name.clone(), td.generics.len()); } } } } } // Plan 160 (D281) Ф.2: build type_defining_modules — maps type name // → the module_name of the peer_file that declared it. Used for // module-boundary enforcement of `priv` (module-private) types. // Only entry-peer-files contribute (imported modules bring in their // types with a different module_name = their own path, which is the // desired behaviour: cross-module access to those types is denied). let mut type_defining_modules: HashMap<String, Vec<String>> = HashMap::new(); if module.peer_files.is_empty() { // Single-file module: all items in module.items belong to module.name. for item in &module.items { if let Item::Type(td) = item { type_defining_modules .entry(td.name.clone()) .or_insert_with(|| module.name.clone()); } } } else { for pf in &module.peer_files { for item in &pf.items_here { if let Item::Type(td) = item { // Record the module_name of the peer that owns this type. type_defining_modules .entry(td.name.clone()) .or_insert_with(|| pf.module_name.clone()); } } } } // Plan 162 Ф.3: build type_method_map — maps // type_name → method_name → Vec<module_name> (all modules // that declare this method on this receiver type). // // Scans all peer_files (both entry-peers and imported-module // peers) to attribute each @method declaration to its source // module. When peer_files is empty (legacy/fallback case), // scans module.items and attributes everything to module.name. // // Together with type_defining_modules this enables: // is_inherent = type_method_map[T][m] ∩ {type_defining_modules[T]} ≠ ∅ let mut type_method_map: HashMap<String, HashMap<String, Vec<Vec<String>>>> = HashMap::new(); if module.peer_files.is_empty() { // Single-file / no peer attribution: attribute all @methods // to module.name (the entry module). This is conservative: // inherent == all methods when there's only one module. for item in &module.items { if let Item::Fn(f) = item { if let Some(recv) = &f.receiver { type_method_map .entry(recv.type_name.clone()) .or_default() .entry(f.name.clone()) .or_default() .push(module.name.clone()); } } } } else { for pf in &module.peer_files { for item in &pf.items_here { if let Item::Fn(f) = item { if let Some(recv) = &f.receiver { let module_list = type_method_map .entry(recv.type_name.clone()) .or_default() .entry(f.name.clone()) .or_default(); // Only push if this module isn't already in the list // (avoids duplicate entries from sibling peers of // the same module that share module_name). if !module_list.contains(&pf.module_name) { module_list.push(pf.module_name.clone()); } } } } } } // Plan 124.6 (D224 §4 rule-4): build type_pub_to — maps type name // → its `pub_to` friend list (from TypeDecl.pub_to). A type that // appears as a receiver (current_recv_type) and is in the list gets // priv-field access. Merges across all items (single- + multi-file). let mut type_pub_to: HashMap<String, Vec<String>> = HashMap::new(); for item in &module.items { if let Item::Type(td) = item { if !td.pub_to.is_empty() { type_pub_to .entry(td.name.clone()) .or_default() .extend(td.pub_to.iter().cloned()); } } } // Plan 214 (D429): `#coerce` pair registry for the accept-path // (`assignable`'s coerce fallback). Independent scan mirroring // `MapLitCtx::build`'s own copy (see `collect_coerce_pairs` doc) — // diagnostics discarded here (already surfaced via `MapLitCtx:: // check_module`, which runs earlier in `check_module_impl`). let coerce_lookup = |n: &str| types.get(n).map(|td| td.kind.clone()); let (coerce_pairs, generic_coerce_patterns, _coerce_errors_dup) = collect_coerce_pairs(module, &coerce_lookup); // [M-p67-path-call-const-receiver-method-ice]: const-name → declared // type, explicit-annotation consts only (see field doc on // `const_types` above). let mut const_types: HashMap<String, TypeRef> = HashMap::new(); for item in &module.items { if let Item::Const(cd) = item { if let Some(ty) = &cd.ty { const_types.entry(cd.name.clone()).or_insert_with(|| ty.clone()); } } } // [M-assoc-const-chained-method-call-p67] (окно №73): `(Type, CONST) → // declared TypeRef`, mirroring `const_types` above one level deeper — // see `assoc_const_types` field doc. Sourced from `self.types` // (already-merged CU, folder-module peers included) rather than // `module.items` directly, so a `type` declared in one peer file with // its out-of-body `const Type.NAME` attached from another peer (same // `attach_out_of_body_assoc_consts` merge `const_types` above doesn't // need — it's not per-type) is covered identically. let mut assoc_const_types: HashMap<(String, String), TypeRef> = HashMap::new(); for (type_name, td) in &types { for ac in &td.assoc_consts { if let Some(ty) = &ac.ty { assoc_const_types .entry((type_name.clone(), ac.name.clone())) .or_insert_with(|| ty.clone()); } } } TypeCheckCtx { arity, sig, synth_methods, blanket_method_names, types, const_types, assoc_const_types, coerce_pairs, generic_coerce_patterns, current_coerce_decl_span: std::cell::RefCell::new(None), sum_variant_names, file_local_types, imported_modules, entry_imported_modules, entry_file_ids, const_fn_names, in_const_fn: std::cell::Cell::new(false), current_recv_type: std::cell::RefCell::new(None), current_recv_is_mut: std::cell::Cell::new(false), current_fn_return_ty: std::cell::RefCell::new(None), current_fn_generics: std::cell::RefCell::new(Vec::new()), current_fn_test_access: std::cell::RefCell::new(Vec::new()), ro_binding_names: std::cell::RefCell::new(std::collections::HashSet::new()), consume_binding_names: std::cell::RefCell::new(std::collections::HashSet::new()), mut_ref_param_names: std::cell::RefCell::new(std::collections::HashSet::new()), user_shadowed_generic_types, type_defining_modules, current_module: std::cell::RefCell::new(Vec::new()), file_modules: std::cell::RefCell::new(std::collections::HashMap::new()), type_method_map, in_test_block: std::cell::Cell::new(false), test_block_test_access: std::cell::RefCell::new(Vec::new()), type_pub_to, // Plan 162.1 Step 2: default build uses empty sig_table; callers // that need cross-module symbol lookup call build_with_sig_table. sig_table: crate::imports::ModuleSigTable::new(), // Plan 172.1 U.3.4: empty callee channel; filled during the check walk. resolved_callees: std::cell::RefCell::new(HashMap::new()), // Plan 196.5 Stage-A: empty subst-value channel; filled during the check walk. node_substs: std::cell::RefCell::new(HashMap::new()), // Plan 172.1 U.4.4(b): empty checker-side resolved-type channel. resolved_types_buf: std::cell::RefCell::new(HashMap::new()), // №279: empty pattern-variant resolved-sum-name channel; filled // during the check walk. pattern_variant_types_buf: std::cell::RefCell::new(HashMap::new()), // Plan 221.1 №286 residual gap (window p286): empty first-send // T-inference hint channel; filled per-block during the check walk. channel_bare_send_elem_hint: std::cell::RefCell::new(HashMap::new()), // [M-crossmodule-samename-typecheck-bleed] (221.1 №28): empty // call-return decl-span side channel; filled during the check walk. call_return_decl_span: std::cell::RefCell::new(HashMap::new()), in_call_func: std::cell::Cell::new(false), assign_target_top: std::cell::Cell::new(false), numeric_bounded_params: std::cell::RefCell::new(HashSet::new()), // Plan 104.10 Ф.2: OFF by default (zero-overhead). check_module_with_expr_types // flips it AFTER build; the empty buffer stays empty on the normal compile path. record_expr_types: false, expr_types_buf: std::cell::RefCell::new(HashMap::new()), } } /// Plan 162.1 Step 2: variant of `build` that pre-populates the /// cross-module signature table. Callers that have already run /// `collect_all_signatures` pass the resulting `ModuleSigTable` here so /// that `is_known_type` / `is_known_fn` can answer cross-module questions /// during type-checking without additional I/O. fn build_with_sig_table( module: &'a Module, synth_arena: &'a FnDeclArena, sig_table: crate::imports::ModuleSigTable, sig: &'a crate::sig_registry::SigRegistry<'a>, ) -> Self { let mut ctx = Self::build(module, synth_arena, sig); ctx.sig_table = sig_table; ctx } /// Plan 162.1 Step 2: returns `true` if `name` is a known type in either: /// (a) the merged `module.items` (types already resolved into this module), /// or (b) any module in the cross-module `sig_table`. /// Used for disambiguation before full resolution (Step 3). fn is_known_type(&self, name: &str) -> bool { self.types.contains_key(name) || !self.sig_table.find_type_modules(name).is_empty() } /// [M-198-f4c-1-privfile-type-not-discriminated]: file-aware type lookup — /// mirrors 2d5f64e91's caller-file candidate filter for `sig.fn_decls` /// (D307 §1/§3), applied to TYPE shape resolution. `self.types` collapses /// same-name declarations onto one slot (last `module.items` write wins); /// when the name is a `priv(file) type` that collides across peer files of /// the same folder-module CU, this prefers the declaration belonging to /// `use_file_id` (the use-site's own file) via `file_local_types`, falling /// back to the legacy global slot otherwise. Non-colliding names never /// populate `file_local_types` (empty per-file map lookup, O(1) miss) — /// byte-identical to `self.types.get(name)` outside the collision case. fn types_get_for_file(&self, name: &str, use_file_id: crate::diag::FileId) -> Option<&'a TypeDecl> { self.file_local_types .get(&use_file_id) .and_then(|m| m.get(name)) .copied() .or_else(|| self.types.get(name).copied()) } /// Plan 162.1 Step 2: returns `true` if `name` is a known free function in /// either: /// (a) `fn_decls` (free functions already merged into this module), /// or (b) any module in the cross-module `sig_table`. /// Used for disambiguation before full resolution. fn is_known_fn(&self, name: &str) -> bool { self.sig.fn_decls.contains_key(name) || !self.sig_table.find_fn_modules(name).is_empty() } /// [Plan 228 Ф.2(a) producer, реестр 221.1 №94-v2] checker-side call-through /// resolver: is `ty` — directly, or through a newtype-over-fn / alias-of-fn /// name — structurally a `fn(...) -> ...` shape? Checker-side mirror of /// emit_c's `resolve_fn_typeref` (`fn_newtype_sigs`, D52-амендмент), but /// sourced from `self.types` (the checker's OWN registry — no separate /// pre-scanned table needed, it already has full `TypeDecl` access). /// Readonly/Mut/Uninit wrappers are transparent (mirror `resolve_fn_typeref`'s /// own peel, same reason: `ro next Handler` params/locals). fn resolve_fn_newtype_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_newtype_typeref(inner) } TypeRef::Named { path, generics, .. } if generics.is_empty() => { let name = path.last()?; match &self.types.get(name)?.kind { TypeDeclKind::Newtype(inner) | TypeDeclKind::Alias(inner) => { self.resolve_fn_newtype_typeref(inner) } _ => None, } } _ => None, } } /// U.2.3.3: overload set for `(type, method)`, base (`sig`) ∪ synth overlay. /// For any (type, method) the overloads live in EXACTLY ONE source (synth is /// registered only when base lacks the method), so synth-first ∥ base is /// collision-free; both Vecs preserve declaration order. fn method_overloads(&self, type_name: &str, method: &str) -> Option<&Vec<&'a FnDecl>> { self.synth_methods .get(type_name) .and_then(|m| m.get(method)) .or_else(|| self.sig.method_overloads(type_name, method)) } /// U.2.3.3: whether any method (any name) is declared on `type_name`, /// base (`sig`) ∪ synth overlay (legacy `method_table.contains_key`). fn type_has_any_method(&self, type_name: &str) -> bool { self.sig.has_type(type_name) || self.synth_methods.contains_key(type_name) } /// Plan 126.2 Ф.1: synthesize + register auto-derive methods into /// `method_table`. See call site in `build` for rationale. /// /// Idempotent w.r.t. user methods: a protocol method that T already /// provides explicitly (present in `method_table`) is never synthesized /// (user code wins). Synthesized `FnDecl`s carry `compiler_generated = true` /// (set by `auto_derive::make_synth_method`) so downstream passes can /// distinguish them. fn register_synthesized_methods( module: &'a Module, synth_arena: &'a FnDeclArena, types: &HashMap<String, &'a TypeDecl>, base_methods: &HashMap<String, HashMap<String, Vec<&'a FnDecl>>>, synth_methods: &mut HashMap<String, HashMap<String, Vec<&'a FnDecl>>>, ) { use crate::protocols::auto_derive::DeriveQuery as _; // Two-phase: phase 1 synthesizes against the immutable BASE methods // (`base_methods` = `sig.method_table`; DeriveQuery reads it for field // eligibility + user-method coverage — user wins); phase 2 inserts the // resulting `&'a FnDecl`s into the `synth_methods` overlay (U.2.3.3 F2). let mut pending: Vec<(String, &'a FnDecl)> = Vec::new(); { // Local DeriveQuery over the already-built `types` + BASE `method_table`. // Decouples synthesis from the not-yet-constructed `TypeCheckCtx`. let query = BuildTimeDeriveQuery { types, method_table: base_methods }; for item in &module.items { let Item::Type(td) = item else { continue; }; if td.impl_protocols.is_empty() { continue; } for proto_name in &td.impl_protocols { if !crate::protocols::auto_derive::is_builtin_protocol(proto_name) { continue; } let Some(method_name) = crate::protocols::auto_derive::builtin_protocol_method(proto_name) else { continue; }; // User wins: skip if T already provides method explicitly. if query.type_provides_method(&td.name, method_name) { continue; } let mut derive_ctx = crate::protocols::auto_derive::AutoDeriveCtx::new(&query); let synthesized = match crate::protocols::auto_derive::synthesize_method( &mut derive_ctx, td, proto_name, ) { Ok(fd) => fd, // Registration-only: diagnostics emitted by // verify_impl_protocols. Skip failures silently. Err(_) => continue, }; let fd_ref: &'a FnDecl = synth_arena.alloc(synthesized); pending.push((td.name.clone(), fd_ref)); } } } // Phase 2: insert into the synth overlay. Same key convention as user // instance methods: f.name (the protocol method bare name, e.g. "equal"/"hash"). for (type_name, fd_ref) in pending { let entry = synth_methods .entry(type_name) .or_default() .entry(fd_ref.name.clone()) .or_default(); // Guard против дубля: не вставляем второй synthesized FnDecl // с тем же именем. let already = entry.iter().any(|f| { f.compiler_generated && f.name == fd_ref.name }); if !already { entry.push(fd_ref); } } } /// Plan 138.2 Ф.0c (D29 method-level shadow): is `fd` a method that came /// from an imported generic type the user has shadowed with a different /// arity? Such methods carry the import's carrier arity (`Vec[T]` → /// receiver.generics.len() == 1) which differs from the user's redeclared /// arity (`type Vec { x, y }` → 0). A method the user wrote on their own /// redeclared type would carry the matching carrier arity and is kept. fn is_shadowed_import_method(&self, fd: &FnDecl) -> bool { if self.user_shadowed_generic_types.is_empty() { return false; } let Some(recv) = &fd.receiver else { return false; }; match self.user_shadowed_generic_types.get(&recv.type_name) { Some(&user_arity) => recv.generics.len() != user_arity, None => false, } } fn check_module(&self, module: &Module, errors: &mut Vec<Diagnostic>) { // Plan 160 (D281) Ф.2: set current_module for module-boundary checks. // Restored to empty Vec on scope exit via CurrentModuleGuard RAII. let _module_guard = CurrentModuleGuard::set(self, module.name.clone()); // D281 follow-up: запомнить ОБЪЯВЛЕННЫЙ модуль каждого файла (pf.module_name, // не module.name — в merged-CU файлы чужих модулей перечитываются под // именем entry-модуля). Для span-based module-boundary проверки. { let mut fm = self.file_modules.borrow_mut(); for pf in &module.peer_files { fm.insert(pf.file_id, pf.module_name.clone()); } } // Plan 91.9 (D186): verify `#impl(P1 + P2 + ...)` annotations. // Для каждого type T с impl_protocols, проверяем что: // 1. Каждый P в списке действительно protocol-тип (E_UNKNOWN_PROTOCOL). // 2. T provides каждый метод P либо напрямую (explicit `fn T @method`), // либо синтезируемо через P's default body (`default_body_calls_satisfy_for`). // Missing methods → E_IMPL_MISSING_METHODS со списком и hint'ом. for item in &module.items { if let Item::Type(td) = item { if td.impl_protocols.is_empty() { continue; } self.verify_impl_protocols(td, errors); } } // Plan 154.1 (D268): verify method-level `#impl(P1 + P2 + ...)` annotations // (`#impl(Debug)` directly before a `fn T @m`). Opt-in: a method without // `#impl` is unaffected (structural conformance unchanged). See // `verify_method_impl_protocols` for the 3 error codes. for item in &module.items { if let Item::Fn(fd) = item { if fd.impl_protocols.is_empty() { continue; } self.verify_method_impl_protocols(fd, errors); } } // Plan 161 Ф.2 (D355 §4/§5): blanket-protocol-receiver invariants. // Independent of the two `#impl` passes above (those are opt-in // annotations; these fire on STRUCTURAL shape alone, same as // blanket dispatch itself does at codegen time, D285). self.check_duplicate_protocol_impl(errors); self.check_blanket_conflict(module, errors); // [D73/D77 retraction 2026-07-06]: E_BLANKET_IDENTITY_OVERRIDE removed. // It rejected an explicit `fn TypeName.from(t TypeName) -> TypeName` // identity declaration because it would "override" the compiler- // synthesized blanket identity `fn[T] T.from(t T) -> T => t`. That // blanket no longer exists (From/Into protocols + auto-derive // retracted, spec/decisions/08-runtime.md#d73) — an identity `.from` // is now just an ordinary static method like any other; keeping this // check would reject legal code with no actual conflict to guard // against. See `[M-d73-d77-retraction-migration]`. // Plan 114.4 / D199 + Plan 148 Ф.3: constexpr-eligibility sets, shared // by the `const` enforcement (`E_CONST_NOT_CONSTEXPR`, reverse) and the // `ro` partition enforcement (`E_RO_FOR_CONSTEXPR_PREFER_CONST`, forward). // Both directions MUST use the same predicate so they can never // disagree about what «constexpr» means. let partition_known_consts: HashSet<String> = module .items .iter() .filter_map(|it| match it { Item::Const(c) => Some(c.name.clone()), _ => None, }) .collect(); // Plan 114.4.2 D199 + 114.4.3 Ф.5 V2: const fn names // (including aliases — `const ALIAS = const_fn`). let partition_const_fn_names: HashSet<String> = { let mut s: HashSet<String> = module .items .iter() .filter_map(|it| match it { Item::Fn(fd) => { let is_const = fd.return_is_const || fd.params.iter().any(|p| p.is_const); if is_const { Some(fd.name.clone()) } else { None } } _ => None, }) .collect(); for _ in 0..10 { let mut added = false; for it in &module.items { if let Item::Const(c) = it { if let crate::ast::ExprKind::Ident(target) = &c.value.kind { if s.contains(target) && !s.contains(&c.name) { s.insert(c.name.clone()); added = true; } } } } if !added { break; } } s }; // D215 amend: named tuple constructor names for constexpr recognition. let partition_named_tuple_names: HashSet<String> = module .items .iter() .filter_map(|it| match it { Item::Type(td) => { if matches!(td.kind, crate::ast::TypeDeclKind::NamedTuple(_)) { Some(td.name.clone()) } else { None } } _ => None, }) .collect(); for item in &module.items { match item { Item::Fn(fd) => { // Plan 138.2 Ф.0c (D29 method-level shadow): when the user // redeclared a generic type with a different arity (e.g. // `type Vec { x, y }` over imported `type Vec[T]`), the // import's merged methods carry `Vec[T]` type-refs that // arity-check (E7310) against the user's non-generic decl. // Skip those merged methods — user wins entirely (D29). if self.is_shadowed_import_method(fd) { continue; } self.check_fn(fd, errors) } Item::Type(td) => self.check_type_decl(td, errors), Item::Const(cd) => { let empty: GenericScope = HashMap::new(); if let Some(t) = &cd.ty { self.walk_typeref(t, &empty, errors); } self.walk_expr(&cd.value, &empty, errors); // Plan 114.4 Ф.1: strict constexpr-only enforcement. // `const X = expr` принимает только literal-eligible // RHS — арифметику над literals, record-literal из // constexpr-fields, references на другие const. // Runtime calls / effects / allocations / non-const // refs → E_CONST_NOT_CONSTEXPR. if let Err(d) = check_const_constexpr_ex( &cd.value, &partition_known_consts, &partition_const_fn_names, &partition_named_tuple_names, ) { errors.push(d); } } Item::Test(t) => { // Plan 124.6 (D224 §4): set test-block context so that // priv_access_allowed_base applies rules 2 + 3 (same-module // implicit grant + explicit test_access list). Guard // restores previous state on drop. let _tb_guard = TestBlockGuard::enter(self, t.test_access.clone()); let empty: GenericScope = HashMap::new(); self.walk_block(&t.body, &empty, errors); } // Plan 148 Ф.3 ([M-114.4-strict-partition]): module-level `ro` // bindings are subject to the strict partition forward direction. // A constexpr-eligible `ro X = …` must instead be `const X = …`. Item::Let(ld) => { if let Some(d) = check_ro_module_partition( ld, &partition_known_consts, &partition_const_fn_names, &partition_named_tuple_names, ) { errors.push(d); } } Item::Bench(_) | Item::Lemma(_) => {} } } // Plan 157 (D200 amend): associated `ro Type.NAME` — same strict // const/ro partition symmetry as bare module-level `ro` // (`check_ro_module_partition` above, [M-114.4-strict-partition]). // A constexpr-eligible RHS has a `const Type.NAME` equivalent and // must use it; `ro Type.NAME` is reserved for genuinely runtime // values (constructor calls / heap allocation / non-const refs). // Runs over `module.items` post-`attach_out_of_body_assoc_consts` // (imports.rs, pre-type-check), so every out-of-body `ro Type.NAME` // — same-file or split across folder-module peers — is already // attached to its `TypeDecl.assoc_consts` by this point. for item in &module.items { if let Item::Type(td) = item { for ac in &td.assoc_consts { if !ac.is_lazy_ro { continue; } if check_const_constexpr_ex( &ac.value, &partition_known_consts, &partition_const_fn_names, &partition_named_tuple_names, ).is_ok() { errors.push(Diagnostic::new( format!( "[E_RO_FOR_CONSTEXPR_PREFER_CONST] associated `ro {}.{} = …` \ has a constexpr-eligible initialiser (literal / arithmetic on \ literals / record/tuple/array literal of constexpr fields / \ reference to another `const` / `const fn` call). The `const`/\ `ro` partition is strict at the associated level too (Plan 157, \ D200 amend — same policy as bare module-level `ro`): a \ constexpr-eligible RHS must be declared `const {0}.{1} = …`, \ not `ro {0}.{1} = …`. Use `const` here, or keep `ro` only for \ a runtime value (constructor call / allocation).", td.name, ac.name ), ac.span, )); } } } } // Ф.1: assignability — отдельный scope-aware проход по телам // (var-типы локальных переменных нужны только здесь). for item in &module.items { match item { Item::Fn(fd) => { // Plan 138.2 Ф.0c: same method-level shadow-skip as the // arity pass above — merged methods of a user-shadowed // generic type are not the user's, skip body-checking them. if self.is_shadowed_import_method(fd) { continue; } self.f1_check_fn(fd, errors) } Item::Test(t) => { // Plan 124.6 (D224 §4): propagate test-block context into // the f1 assignability pass too (handles priv record-init // and write checks that fire from f1_check_assign_let). let _tb_guard = TestBlockGuard::enter(self, t.test_access.clone()); let gs: GenericScope = HashMap::new(); let mut scope: HashMap<String, TypeRef> = HashMap::new(); self.f1_block(&t.body, &gs, &mut scope, errors); } Item::Const(cd) => { let gs: GenericScope = HashMap::new(); let mut scope: HashMap<String, TypeRef> = HashMap::new(); if let Some(ann) = &cd.ty { // A `const` binding is immutable (ro content-view). self.f1_check_assign_let( &cd.value, ann, &cd.name, false, &gs, &scope, errors, ); } self.f1_expr(&cd.value, &gs, &mut scope, errors); } _ => {} } } // Plan 173.1 Ф.2 (D71): interim-guard `[E_PARFOR_RESULT_UNSUPPORTED]` // (Plan 173 Ф.1 #7, [M-parfor-record-result-miscompile]) УДАЛЁН вместе // со своим visitor-семейством (`check_parfor_result_*`/`parfor_elem_ // supported`): codegen собирает `parallel for → []T` для ЛЮБОГО T // через канал (emit_parallel_for, channel+drain lowering) — примитив- // whitelist больше не существует, guard стал бы ложным реджектом. // Plan 172.1 P67: Annotate peer_files.items_here ExprIds for codegen. // Architectural gap: codegen iterates peer_files.items_here (imported modules, // file-private items) with ExprIds distinct from merged module.items ExprIds // that f1_check_fn above annotated. Without this pass, infer_expr_c_type // channel 2 misses these ids → falls to legacy (panics). Errors suppressed // (same functions already checked via module.items — no duplicate diagnostics). { let mut peer_errors: Vec<crate::diag::Diagnostic> = Vec::new(); for pf in &module.peer_files { for item in &pf.items_here { match item { Item::Fn(fd) => { if !self.is_shadowed_import_method(fd) { self.f1_check_fn(fd, &mut peer_errors); } } Item::Test(t) => { let gs: GenericScope = HashMap::new(); let mut scope: HashMap<String, TypeRef> = HashMap::new(); self.f1_block(&t.body, &gs, &mut scope, &mut peer_errors); } Item::Const(cd) => { let gs: GenericScope = HashMap::new(); let mut scope: HashMap<String, TypeRef> = HashMap::new(); self.f1_expr(&cd.value, &gs, &mut scope, &mut peer_errors); } _ => {} } } } // peer_errors intentionally discarded (duplicate of module.items check). } // Plan 110.1.2 (D188 / D196): Cleanup[E] protocol satisfaction check. // Для каждого `Stmt::ConsumeScope { init, body, .. }` проверяем что // init expr resolves к типу с `cleanup` method. Если нет — emit // [D188-not-consumable] error. Полный D196 (Result/Option unwrap, // conditional, method chain — non-trivial type inference) — staged // delivery: дополнительные формы validated в Plan 110.1.3 / 110.1.4. for item in &module.items { match item { Item::Fn(fd) => { // D432-амендмент 2026-08-04 (№315 fix): fn's own // declared effects, needed to verify a block-form // `consume X = e { body }`'s auto-inserted cleanup call // doesn't carry an undeclared direct effect. let cfe = Some(fd.effects.as_slice()); match &fd.body { FnBody::Block(b) => self.check_consume_scopes_in_block(b, cfe, errors), FnBody::Expr(e) => self.check_consume_scopes_in_expr(e, cfe, errors), FnBody::External => {} } } // Test/bench bodies have no fn-sig to declare effects against — // `None` = ambient/unchecked, same treatment effect // declarations already get for test bodies elsewhere. Item::Test(t) => self.check_consume_scopes_in_block(&t.body, None, errors), Item::Bench(b) => { for s in &b.setup { self.check_consume_scopes_in_stmt(s, None, errors); } self.check_consume_scopes_in_block(&b.measure_body, None, errors); for s in &b.teardown { self.check_consume_scopes_in_stmt(s, None, errors); } } _ => {} } } } /// Plan 110.1.2 (D188): recursive walk через Block для ConsumeScope check. /// `current_fn_effects` — D432-амендмент 2026-08-04 (№315 fix): /// enclosing fn's declared effects (`None` for test/bench — ambient). fn check_consume_scopes_in_block(&self, b: &Block, current_fn_effects: Option<&[TypeRef]>, errors: &mut Vec<Diagnostic>) { for s in &b.stmts { self.check_consume_scopes_in_stmt(s, current_fn_effects, errors); } if let Some(t) = &b.trailing { self.check_consume_scopes_in_expr(t, current_fn_effects, errors); } } /// Plan 110.1.2 (D188): walk Stmt looking for ConsumeScope, recurse into children. fn check_consume_scopes_in_stmt(&self, s: &Stmt, current_fn_effects: Option<&[TypeRef]>, errors: &mut Vec<Diagnostic>) { match s { Stmt::ConsumeScope { binding, init, body, .. } => { self.validate_consume_scope_init(binding, init, current_fn_effects, errors); // Plan 110.1.5 (D188 R2 enforcement at compile time): // detect manual `binding.cleanup(...)` calls в body. // Runtime exactly-once guard prevents double dispatch; // здесь — compile-time gate чтобы избегать runtime panic. let mut tracked = std::collections::HashSet::new(); tracked.insert(binding.clone()); self.check_no_manual_on_exit_call_in_block(&tracked, body, errors); self.check_consume_scopes_in_expr(init, current_fn_effects, errors); self.check_consume_scopes_in_block(body, current_fn_effects, errors); } Stmt::Let(d) => self.check_consume_scopes_in_expr(&d.value, current_fn_effects, errors), // Plan 114.4 Ф.2: scope-local const — walk value for nested ConsumeScope. Stmt::Const(d) => self.check_consume_scopes_in_expr(&d.value, current_fn_effects, errors), Stmt::Expr(e) => self.check_consume_scopes_in_expr(e, current_fn_effects, errors), Stmt::Assign { target, value, .. } => { self.check_consume_scopes_in_expr(target, current_fn_effects, errors); self.check_consume_scopes_in_expr(value, current_fn_effects, errors); } Stmt::Return { value, .. } => { if let Some(v) = value { self.check_consume_scopes_in_expr(v, current_fn_effects, errors); } } Stmt::Throw { value, .. } => self.check_consume_scopes_in_expr(value, current_fn_effects, errors), Stmt::Defer { body, .. } => { self.check_consume_scopes_in_expr(body, current_fn_effects, errors); } Stmt::AssertStatic { expr, .. } | Stmt::Assume { expr, .. } => { self.check_consume_scopes_in_expr(expr, current_fn_effects, errors); } Stmt::Break(_) | Stmt::Continue(_) | Stmt::Reveal { .. } | Stmt::Apply { .. } | Stmt::Calc { .. } => {} // Plan 136: tuple destructuring assignment. Stmt::TupleAssign { lhs, rhs, .. } => { for e in lhs { self.check_consume_scopes_in_expr(e, current_fn_effects, errors); } for e in rhs { self.check_consume_scopes_in_expr(e, current_fn_effects, errors); } } } } /// Plan 110.1.2 (D188): walk Expr looking for nested ConsumeScope in /// bodies (lambdas, blocks, if-then-else, match arms, etc). fn check_consume_scopes_in_expr(&self, e: &Expr, current_fn_effects: Option<&[TypeRef]>, errors: &mut Vec<Diagnostic>) { use crate::ast::ExprKind; match &e.kind { ExprKind::Block(b) => self.check_consume_scopes_in_block(b, current_fn_effects, errors), ExprKind::If { cond, then, else_, .. } => { self.check_consume_scopes_in_expr(cond, current_fn_effects, errors); self.check_consume_scopes_in_block(then, current_fn_effects, errors); if let Some(eb) = else_ { match eb { crate::ast::ElseBranch::Block(b) => self.check_consume_scopes_in_block(b, current_fn_effects, errors), crate::ast::ElseBranch::If(ei) => self.check_consume_scopes_in_expr(ei, current_fn_effects, errors), } } } ExprKind::While { cond, body, .. } => { self.check_consume_scopes_in_expr(cond, current_fn_effects, errors); self.check_consume_scopes_in_block(body, current_fn_effects, errors); } ExprKind::For { iter, body, .. } => { self.check_consume_scopes_in_expr(iter, current_fn_effects, errors); self.check_consume_scopes_in_block(body, current_fn_effects, errors); } ExprKind::Loop { body, .. } => self.check_consume_scopes_in_block(body, current_fn_effects, errors), ExprKind::Call { func, args, .. } => { self.check_consume_scopes_in_expr(func, current_fn_effects, errors); for a in args { match a { crate::ast::CallArg::Item(e) | crate::ast::CallArg::Spread(e) => { self.check_consume_scopes_in_expr(e, current_fn_effects, errors); } _ => {} } } } ExprKind::Try(e) | ExprKind::Bang(e) | ExprKind::RefArg(e) => self.check_consume_scopes_in_expr(e, current_fn_effects, errors), ExprKind::Coalesce(a, b) => { self.check_consume_scopes_in_expr(a, current_fn_effects, errors); self.check_consume_scopes_in_expr(b, current_fn_effects, errors); } ExprKind::Binary { left, right, .. } => { self.check_consume_scopes_in_expr(left, current_fn_effects, errors); self.check_consume_scopes_in_expr(right, current_fn_effects, errors); } ExprKind::Unary { operand, .. } => self.check_consume_scopes_in_expr(operand, current_fn_effects, errors), ExprKind::Member { obj, .. } => self.check_consume_scopes_in_expr(obj, current_fn_effects, errors), // Lambda / closure bodies — separate scopes; walk their bodies too. ExprKind::Lambda { body, .. } => self.check_consume_scopes_in_expr(body, current_fn_effects, errors), _ => {} } } /// Plan 110.1.2 (D188 / D196): validate init expression's type implements /// Cleanup. Uses простой heuristic для type inference (Type.method() → /// return type; record literal → type; ?/!! → recurse). Полный inference /// (method chain, conditional, generic) — staged delivery 110.1.3+. /// `binding` + `current_fn_effects` — D432-амендмент 2026-08-04 (№315 /// fix): needed by the effect check at the end (block-form ALWAYS /// installs the cleanup call, regardless of whether `binding` is /// otherwise consumed in `body`). fn validate_consume_scope_init(&self, binding: &str, init: &Expr, current_fn_effects: Option<&[TypeRef]>, errors: &mut Vec<Diagnostic>) { // Plan 110.1.3 (D196 form 5 — wrapped без unwrap): detect raw // Option[T] / Result[T,_] returning expressions WITHOUT ?/!! // unwrap. Emit specific D196-wrapped-init-needs-unwrap hint. if let Some(wrapped) = self.detect_wrapped_init_typeref(init) { errors.push(Diagnostic::new( format!( "[D196-wrapped-init-needs-unwrap] `consume X = expr {{ body }}` \ init expr returns `{wrapped}[T, ...]` без unwrap. Required: \ either `consume X = expr!! {{ body }}` (Option unwrap), \ `consume X = expr? {{ body }}` (Result unwrap with Fail \ propagation), or distinguish None case explicitly через \ `if Some(X) = maybe_X() {{ consume X = X {{ ... }} }}`.", wrapped = wrapped ), init.span, )); return; } // Plan 110.1.3 (D196 form 3 — divergent conditional): if/match init // with branches returning incompatible Cleanup types. if let Some((t1, t2)) = self.detect_divergent_consumable(init) { errors.push(Diagnostic::new( format!( "[D196-divergent-consumable] `consume X = if cond {{ ... }} \ else {{ ... }} {{ body }}` branches return divergent \ Cleanup types: `{t1}` vs `{t2}`. Branches must return \ compatible type. Extract в polymorphic wrapper type \ или unify branches.", t1 = t1, t2 = t2 ), init.span, )); return; } let Some(type_name) = self.infer_consume_init_type(init) else { // Тип не выводится простыми heuristic'ами — staged delivery // через codegen-gate D188-codegen-not-yet-implemented. Полное // покрытие в Plan 110.1.4 / 110.1.3. return; }; // Special case: `never` (bottom-тип) — никогда не resolved init // type, skip без ошибки. if type_name == "never" || type_name == "Never" { return; } // Look up cleanup method on the type. let has_on_exit = self.method_overloads(&type_name, "cleanup").is_some(); if !has_on_exit { // Тип known? Если не known — это либо primitive (`int`/`str`) // либо unresolved (caught by name resolution). Skip primitive // случаи silently. let is_known_type = self.types.contains_key(&type_name) || self.type_has_any_method(&type_name); // Even for primitive types like `int`/`str` — нет cleanup → error. // Но diagnostic должен быть полезный (suggest implement). if is_known_type || self.type_has_any_method(&type_name) { let diag = Diagnostic::new( format!( "[D188-not-consumable] type `{name}` does not implement `Cleanup[E]` \ (method `cleanup` missing). \ To use `consume X = expr {{ body }}` scope-block, type must declare:\n \ `fn {name} consume @cleanup(outcome ScopeOutcome) Fail[E] -> () => {{ ... }}`\n\ where `E` is the cleanup-error type (or `never` for infallible — D194).\n\ Alternative: use raw `consume X = expr` (D180 linear binding) without block.", name = type_name ), init.span, ).with_note(format!( "Plan 110.6.1: see docs/idiom/consume-scope-cleanup.md \ Q-consumable-protocol for decision tree + implementation template. \ For infallible cleanup (Mutex/Sem/Lock) use `Cleanup[never]` — \ no Fail[E] effect (D194 hot-path eligible)." )); errors.push(diag); } } else { // cleanup existsует — validate signature (Plan 110.1.2 §D188-malformed-on-exit). // Минимальная проверка: первый param должен быть ScopeOutcome. // Глубокая validation (Fail[E] check, return type ()) — 110.1.3. self.validate_on_exit_signature(&type_name, init.span, errors); // D432-амендмент 2026-08-04 (№315 fix): block-form ALWAYS // installs the cleanup call (defer-desugar, D188/D314 §3) // regardless of whether `binding` is otherwise consumed in // `body` — its effects are DIRECT effects of the enclosing // function (D432 §1), checked here (BEFORE desugar/effect-check // run, so the desugar-invisible synthesized call is finally // caught). `current_fn_effects == None` (test/bench ambient // context) — deliberately skipped, see struct/field docs. if let Some(cfe) = current_fn_effects { if let Some(decls) = self.method_overloads(&type_name, "cleanup") { let mut row: Vec<TypeRef> = Vec::new(); let mut seen_names: std::collections::HashSet<String> = std::collections::HashSet::new(); for d in decls { for eff in &d.effects { if let TypeRef::Named { path, .. } = eff { if let Some(nm) = path.last() { if seen_names.insert(nm.clone()) { row.push(eff.clone()); } } } } } if !row.is_empty() { let missing = missing_cleanup_effect_names(&row, cfe); if !missing.is_empty() { errors.push(d432_cleanup_effect_diag(binding, &type_name, &missing, init.span)); } } } } } } /// Plan 110.1.2 / refine (D188-malformed-on-exit) + 110.9.5 V1.1 strict: /// cleanup signature check. /// Verifies: /// - Param[0] is `outcome ScopeOutcome`. /// - Exactly 1 param (D188 protocol contract). /// - Return type is `()` or absent (D188 protocol contract). /// - Effects are either empty (Cleanup[never]) or `Fail[E]` only /// (no other effects). /// - No generic parameters (cleanup methods on concrete types). fn validate_on_exit_signature(&self, type_name: &str, init_span: Span, errors: &mut Vec<Diagnostic>) { let Some(decls) = self.method_overloads(type_name, "cleanup") else { return; }; for decl in decls { // Param[0] must be `outcome ScopeOutcome`. let first_param_ok = decl.params.first() .map(|p| matches!(&p.ty, TypeRef::Named { path, .. } if path.last().map_or(false, |s| s == "ScopeOutcome"))) .unwrap_or(false); if !first_param_ok { errors.push(Diagnostic::new( format!( "[D188-malformed-on-exit] `fn {tn} @cleanup(...)` signature invalid: \ first parameter must be `outcome ScopeOutcome` (D188 protocol contract). \ Correct form: \ `fn {tn} consume @cleanup(outcome ScopeOutcome) Fail[E] -> () => {{ ... }}`. \ (Diagnostic emitted at `consume {{}}` use-site for context.)", tn = type_name ), init_span, )); continue; } // Exactly 1 param (D188 protocol contract). if decl.params.len() != 1 { errors.push(Diagnostic::new( format!( "[D188-malformed-on-exit] `fn {tn} @cleanup(...)` has {n} params; \ protocol requires exactly 1 (`outcome ScopeOutcome`). \ Remove extra parameters; resource state available via `@`.", tn = type_name, n = decl.params.len() ), init_span, )); continue; } // Plan 110.9.5 V1.1 [M-110.9.5-on-exit-strict-signature] + // Plan 110.9.5.a (2026-06-05): canonical strict check. // // **Pre-110.9.5.a state:** pragmatic accept-both allowed Tuple([]), // Named("unit"), and TypeRef::Unit equivalents. Documented as // "backward-compat без parser canonicalization". // // **110.9.5.a finding:** Recon (Workflow `wf_ccdccc85-007`) // confirmed parser ALREADY canonicalizes `()` к `TypeRef::Unit(Span)` // (parser/mod.rs line 5031). NO Tuple([]) или Named("unit") // construction sites в parser. The pragmatic accept-both was // defensive code for a non-existent variance. Removing accept-both // aligns with peer is_unit_or_none() at line 9137+ which already // checks ONLY TypeRef::Unit. // // Strict checks (unchanged from V1.1): // (a) Return type must be TypeRef::Unit (canonical form). // (b) Effects must be subset of `{ Fail[E] }`. // (c) No generic params allowed. // // Permitted forms: TypeRef::Unit и TypeRef::Readonly(Unit) — последний // transparent wrapper (readonly modifier shouldn't change semantic equality). fn is_unit_tr(t: &TypeRef) -> bool { match t { TypeRef::Unit(_) => true, TypeRef::Readonly(inner, _) => is_unit_tr(inner), _ => false, } } let ret_ok = match &decl.return_type { None => true, // implicit Unit → OK. Some(rt) => is_unit_tr(rt), }; if !ret_ok { errors.push(Diagnostic::new( format!( "[D188-malformed-on-exit] `fn {tn} @cleanup(...)` return type \ invalid (Plan 110.9.5 V1.1): protocol requires `()` (Unit) return. \ Got `{rt}`. Correct: `fn {tn} consume @cleanup(outcome ScopeOutcome) \ Fail[E] -> () => {{ ... }}` (or omit `-> ()`).", tn = type_name, rt = decl.return_type.as_ref() .map(|t| format!("{:?}", t)) .unwrap_or_else(|| "<missing>".to_string()), ), init_span, )); continue; } // 2. Effects: pre-D432-amendment (2026-08-04) this protocol // allowed ONLY `Fail[E]` (or no effects) — Plan 110.9.5 V1.1's // structural gate, `D188-malformed-on-exit`. The amendment (§1) // LIFTS that restriction: any effect is legal on `@cleanup` now // — it becomes a DIRECT effect of whichever function triggers // the auto-insertion (bare-form leftover-at-exit or block-form // `consume X = e { body }`), checked at EACH such site instead // of at the declaration (`check_obligations_at_exit` / // `validate_consume_scope_init`'s effect check below, D432 §1, // №315 fix). No structural gate here anymore. // 3. No generic params (would imply per-call mono за scope of cleanup). if !decl.generics.is_empty() { errors.push(Diagnostic::new( format!( "[D188-malformed-on-exit] `fn {tn} @cleanup(...)` declares generic \ params `[{gens}]` (Plan 110.9.5 V1.1): protocol forbids generics \ on cleanup methods — resource-state types already concrete by \ construction.", tn = type_name, gens = decl.generics.iter().map(|g| g.name.clone()).collect::<Vec<_>>().join(", ") ), init_span, )); continue; } } } /// Plan 110.1.5 (D188 R2) + Plan 173 Ф.5 (#8) hardening: detect manual /// `binding.cleanup(...)` calls в ConsumeScope body. Auto cleanup dispatch /// happens at scope-exit; manual call → double invocation → runtime panic /// (R2 exactly-once violation). Compile-time gate preferred to runtime panic. /// /// `names` — the set of identifiers КОТОРЫЕ могут указывать на та же /// consume-binding: seeded with the binding itself, GROWN by simple direct /// aliases (`let y = x` / `ro y = x` где x уже в `names`) found while /// walking the block (Plan 173 Ф.5: the original single-`&str` version /// only caught the literal identifier — a one-line `let y = tx; y.cleanup(...)` /// alias slipped past it into codegen, where the manual call and the /// scope's own generated dispatch both reach `Nova_<T>_consume_cleanup`, /// producing a REAL runtime double-invocation the syntactic check exists /// to prevent). Residual: passing the binding through an arbitrary /// function boundary (`identity(tx).cleanup(...)`) is still flagged /// because `expr_mentions_any` scans the WHOLE receiver subexpression, /// not just a bare `Ident`; genuinely opaque escapes (FFI/reflection) are /// the acknowledged residual (spec 03-syntax.md D188 R2) — caught instead /// by the runtime `_consume_count` guard (`emit_consume_entry_cleanup`). fn check_no_manual_on_exit_call_in_block(&self, names: &std::collections::HashSet<String>, b: &Block, errors: &mut Vec<Diagnostic>) { let mut names = names.clone(); for s in &b.stmts { self.check_no_manual_on_exit_call_in_stmt(&mut names, s, errors); } if let Some(t) = &b.trailing { self.check_no_manual_on_exit_call_in_expr(&names, t, errors); } } fn check_no_manual_on_exit_call_in_stmt(&self, names: &mut std::collections::HashSet<String>, s: &Stmt, errors: &mut Vec<Diagnostic>) { use crate::ast::ExprKind; match s { Stmt::Let(d) => { self.check_no_manual_on_exit_call_in_expr(names, &d.value, errors); // Grow the alias set: `let/ro y = x` where `x` is already // tracked — `y` becomes an equally-valid manual-call target. if let crate::ast::Pattern::Ident { name: alias_name, .. } = &d.pattern { if let ExprKind::Ident(rhs_name) = &d.value.kind { if names.contains(rhs_name) { names.insert(alias_name.clone()); } } } } Stmt::Expr(e) => self.check_no_manual_on_exit_call_in_expr(names, e, errors), Stmt::Assign { target, value, .. } => { self.check_no_manual_on_exit_call_in_expr(names, target, errors); self.check_no_manual_on_exit_call_in_expr(names, value, errors); } Stmt::Return { value, .. } => { if let Some(v) = value { self.check_no_manual_on_exit_call_in_expr(names, v, errors); } } Stmt::Throw { value, .. } => self.check_no_manual_on_exit_call_in_expr(names, value, errors), Stmt::Defer { body, .. } => { self.check_no_manual_on_exit_call_in_expr(names, body, errors); } Stmt::ConsumeScope { init, body, binding: inner_binding, .. } => { self.check_no_manual_on_exit_call_in_expr(names, init, errors); // Nested consume scope с inner binding NEW — outer `names` // check still applies inside (если inner body references // outer's binding manually — same violation). self.check_no_manual_on_exit_call_in_block(names, body, errors); // Дополнительно: recurse с inner binding (D197 re-entrance). let mut inner = std::collections::HashSet::new(); inner.insert(inner_binding.clone()); self.check_no_manual_on_exit_call_in_block(&inner, body, errors); } Stmt::AssertStatic { expr, .. } | Stmt::Assume { expr, .. } => { self.check_no_manual_on_exit_call_in_expr(names, expr, errors); } _ => {} } } fn check_no_manual_on_exit_call_in_expr(&self, names: &std::collections::HashSet<String>, e: &Expr, errors: &mut Vec<Diagnostic>) { use crate::ast::ExprKind; // Detect `<tracked-name-or-expr-mentioning-it>.cleanup(...)` call. // Plan 173 Ф.5 (#8): widened from a bare `Ident(binding)` match to // ANY receiver subexpression that mentions a tracked name — catches // the pass-through-function bypass (`identity(tx).cleanup(...)`) // in addition to the direct/aliased identifier form. if let ExprKind::Call { func, .. } = &e.kind { if let ExprKind::Member { obj, name, .. } = &func.kind { if name == "cleanup" { if let Some(hit) = Self::expr_mentions_any(names, obj) { errors.push(Diagnostic::new( format!( "[D188-r2-manual-on-exit] `{hit}.cleanup(...)` (or an expression \ referencing it) cannot be called manually from inside a \ `consume {hit} = ... {{ body }}` scope-block body. Auto cleanup \ dispatch на scope exit гарантирует exactly-once invariant \ (D188 R2). Manual call → double invocation → runtime panic. \ Remove the explicit call; scope-exit will dispatch cleanup \ с appropriate ScopeOutcome value.", hit = hit ), e.span, )); } } } } // Recurse into children regardless. self.check_no_manual_on_exit_recurse(names, e, errors); } /// Plan 173 Ф.5 (#8): does `e` (recursively) mention any name in `names` /// as a bare identifier? Returns the FIRST matching tracked name (for the /// diagnostic message) or `None`. Conservative over-approximation — scans /// through calls/members/binary/unary/casts so a tracked binding passed /// through an intervening function call (`identity(tx)`) is still caught. fn expr_mentions_any(names: &std::collections::HashSet<String>, e: &Expr) -> Option<String> { use crate::ast::ExprKind; match &e.kind { ExprKind::Ident(n) => names.get(n).cloned(), ExprKind::Call { func, args, .. } => { Self::expr_mentions_any(names, func).or_else(|| { args.iter().find_map(|a| match a { crate::ast::CallArg::Item(ae) | crate::ast::CallArg::Spread(ae) => { Self::expr_mentions_any(names, ae) } _ => None, }) }) } ExprKind::Member { obj, .. } => Self::expr_mentions_any(names, obj), ExprKind::Binary { left, right, .. } => { Self::expr_mentions_any(names, left).or_else(|| Self::expr_mentions_any(names, right)) } ExprKind::Unary { operand, .. } | ExprKind::Try(operand) | ExprKind::Bang(operand) | ExprKind::RefArg(operand) => Self::expr_mentions_any(names, operand), ExprKind::Coalesce(a, b) => { Self::expr_mentions_any(names, a).or_else(|| Self::expr_mentions_any(names, b)) } _ => None, } } fn check_no_manual_on_exit_recurse(&self, names: &std::collections::HashSet<String>, e: &Expr, errors: &mut Vec<Diagnostic>) { use crate::ast::ExprKind; match &e.kind { ExprKind::Block(b) => self.check_no_manual_on_exit_call_in_block(names, b, errors), ExprKind::Call { func, args, .. } => { self.check_no_manual_on_exit_call_in_expr(names, func, errors); for a in args { match a { crate::ast::CallArg::Item(ae) | crate::ast::CallArg::Spread(ae) => { self.check_no_manual_on_exit_call_in_expr(names, ae, errors); } _ => {} } } } ExprKind::If { cond, then, else_, .. } => { self.check_no_manual_on_exit_call_in_expr(names, cond, errors); self.check_no_manual_on_exit_call_in_block(names, then, errors); if let Some(eb) = else_ { match eb { crate::ast::ElseBranch::Block(b) => self.check_no_manual_on_exit_call_in_block(names, b, errors), crate::ast::ElseBranch::If(ei) => self.check_no_manual_on_exit_call_in_expr(names, ei, errors), } } } ExprKind::While { cond, body, .. } => { self.check_no_manual_on_exit_call_in_expr(names, cond, errors); self.check_no_manual_on_exit_call_in_block(names, body, errors); } ExprKind::For { iter, body, .. } => { self.check_no_manual_on_exit_call_in_expr(names, iter, errors); self.check_no_manual_on_exit_call_in_block(names, body, errors); } ExprKind::Try(inner) | ExprKind::Bang(inner) | ExprKind::RefArg(inner) => self.check_no_manual_on_exit_call_in_expr(names, inner, errors), ExprKind::Coalesce(a, b) => { self.check_no_manual_on_exit_call_in_expr(names, a, errors); self.check_no_manual_on_exit_call_in_expr(names, b, errors); } ExprKind::Binary { left, right, .. } => { self.check_no_manual_on_exit_call_in_expr(names, left, errors); self.check_no_manual_on_exit_call_in_expr(names, right, errors); } ExprKind::Unary { operand, .. } => self.check_no_manual_on_exit_call_in_expr(names, operand, errors), ExprKind::Member { obj, .. } => self.check_no_manual_on_exit_call_in_expr(names, obj, errors), _ => {} } } /// Plan 110.1.2 / 110.1.3 (D196): infer the resulting type name from /// `consume X = INIT { body }` init expression. Heuristics: /// - `Type.method(args)` → look up method's return type via method_table. /// - `Type { fields }` → type name directly. /// - `expr?` / `expr!!` → если inner returns `Result[T,E]` / `Option[T]`, /// unwrap до T (D196 form 2: Result/Option unwrap). /// - `expr as Type` → cast target. /// - Other forms → None (staged delivery — full inference 110.1.4). fn infer_consume_init_type(&self, e: &Expr) -> Option<String> { self.infer_consume_init_typeref(e) .as_ref() .and_then(Self::typeref_to_name) } /// Extract final type name from TypeRef::Named (last path segment). fn typeref_to_name(t: &TypeRef) -> Option<String> { if let TypeRef::Named { path, .. } = t { path.last().cloned() } else { None } } /// Plan 110.1.3 (D196 form 5): detect if init returns raw `Option[T]` /// or `Result[T,_]` без unwrap operator. Returns wrapper name если /// detected, None если init is direct (unwrapped) or non-wrapped type. fn detect_wrapped_init_typeref(&self, init: &Expr) -> Option<String> { use crate::ast::ExprKind; // `?` and `!!` are unwrap operators — they're EXPLICITLY safe. if matches!(init.kind, ExprKind::Try(_) | ExprKind::Bang(_) | ExprKind::RefArg(_)) { return None; } // For direct Call (e.g., `try_new()` without `?`), inspect return type. if let ExprKind::Call { func, .. } = &init.kind { if let ExprKind::Path(parts) = &func.kind { if parts.len() >= 2 { let type_name = &parts[parts.len() - 2]; let method_name = &parts[parts.len() - 1]; if let Some(decls) = self.method_overloads(type_name, method_name) { if let Some(decl) = decls.first() { if let Some(TypeRef::Named { path, .. }) = &decl.return_type { if let Some(outer) = path.last() { if outer == "Option" || outer == "Result" { return Some(outer.clone()); } } } } } } } } None } /// Plan 110.1.3 (D196 form 3): detect if/match init with branches /// returning incompatible Cleanup types. Returns (t1, t2) pair если /// detected. fn detect_divergent_consumable(&self, init: &Expr) -> Option<(String, String)> { use crate::ast::ExprKind; if let ExprKind::If { then, else_, .. } = &init.kind { // Plan 125.1 D196 amend: divergent branch (`never` через любой // путь — trailing throw/interrupt/panic, stmt-position // throw/return, divergent call) не участвует в conflict-check — // bottom type subtype of any. Используем `block_diverges` // (полный stmts+trailing walk) вместо trailing-only check, и // SKIP (continue к following branch / return None) вместо // `?`-propagation abort. let then_diverges = block_diverges(then); let else_b = else_.as_ref()?; let else_diverges = match else_b { crate::ast::ElseBranch::Block(b) => block_diverges(b), crate::ast::ElseBranch::If(ei) => expr_diverges(ei), }; // Если ЛЮБАЯ ветка diverges — пары для conflict-check нет. if then_diverges || else_diverges { return None; } // Both branches must end в expression returning Cleanup. let then_ty = self.infer_block_trailing_typeref(then)?; let else_ty = match else_b { crate::ast::ElseBranch::Block(b) => self.infer_block_trailing_typeref(b)?, crate::ast::ElseBranch::If(ei) => self.infer_consume_init_typeref(ei)?, }; let then_name = Self::typeref_to_name(&then_ty)?; let else_name = Self::typeref_to_name(&else_ty)?; // Plan 125.1 (Ф.3): defensive — Ф.3 уже фильтрует trailing- // divergent в "never" name, оставляем guard для symmetry. if then_name == "never" || else_name == "never" { return None; } if then_name != else_name { return Some((then_name, else_name)); } } None } fn infer_block_trailing_typeref(&self, b: &crate::ast::Block) -> Option<TypeRef> { if let Some(t) = &b.trailing { // Plan 125.1 (Ф.3): trailing-divergence detection. По спеке D25 // bottom-тип `never` propagates как тип блока, когда trailing // expression diverges (throw / interrupt / never-returning call). // Conservative: НЕ ходим по preceding stmts (только trailing). // Hookpoint feeds assignable() never-subtype branch из Ф.1. if Self::expr_diverges_at_top(t) { return Some(prim_ref("never", t.span)); } self.infer_consume_init_typeref(t) } else { None } } /// Plan 125.1 (Ф.3): top-level divergence check для trailing expression. /// Возвращает true, если expression имеет тип `never` (D25 bottom): /// `throw e`, `interrupt v?`, или call к never-returning builtin /// (panic/exit/abort/unreachable). Conservative — не walks вложенные /// statements, только распознаёт top-level shape. Mirrors detection /// logic из `infer_expr_type` Ф.2 (без scope-зависимости). fn expr_diverges_at_top(e: &Expr) -> bool { use crate::ast::ExprKind; match &e.kind { ExprKind::Throw(_) | ExprKind::Interrupt(_) => true, ExprKind::Call { func, .. } => { if let ExprKind::Ident(name) = &func.kind { matches!(name.as_str(), "panic" | "exit" | "abort" | "unreachable") } else { false } } _ => false, } } /// Plan 110.1.3 (D196): infer full TypeRef для init expression. Используется /// для Result/Option unwrap (D196 form 2) — нужен дополнительный slot /// для unwrap'нутого T type-ref'а. fn infer_consume_init_typeref(&self, e: &Expr) -> Option<TypeRef> { use crate::ast::ExprKind; match &e.kind { ExprKind::Call { func, .. } => { match &func.kind { ExprKind::Path(parts) if parts.len() >= 2 => { let type_name = &parts[parts.len() - 2]; let method_name = &parts[parts.len() - 1]; let decls = self.method_overloads(type_name, method_name)?; let decl = decls.first()?; let rt = decl.return_type.as_ref()?; // Self → receiver type substitution. if let TypeRef::Named { path, .. } = rt { if path.last().map_or(false, |s| s == "Self") { return Some(TypeRef::Named { path: vec![type_name.clone()], generics: Vec::new(), span: e.span, }); } } Some(rt.clone()) } _ => None, } } ExprKind::RecordLit { type_name: Some(name), .. } => { Some(TypeRef::Named { path: name.clone(), generics: Vec::new(), span: e.span, }) } ExprKind::Try(inner) => { // D196 form 2 (?): unwrap Result[T, E] → T. let inner_ty = self.infer_consume_init_typeref(inner)?; if let TypeRef::Named { path, generics, .. } = &inner_ty { if path.last().map_or(false, |s| s == "Result") && !generics.is_empty() { return Some(generics[0].clone()); } // Option[T] через ? тоже разворачивается (R/E aware). if path.last().map_or(false, |s| s == "Option") && !generics.is_empty() { return Some(generics[0].clone()); } } Some(inner_ty) } ExprKind::Bang(inner) => { // D196 form 2 (!!): unwrap Option[T] → T или Result[T,_] → T. let inner_ty = self.infer_consume_init_typeref(inner)?; if let TypeRef::Named { path, generics, .. } = &inner_ty { if path.last().map_or(false, |s| s == "Option" || s == "Result") && !generics.is_empty() { return Some(generics[0].clone()); } } Some(inner_ty) } ExprKind::As(_, ty) => Some(ty.clone()), ExprKind::Ident(_) | ExprKind::Path(_) => None, _ => None, } } // --- Ф.2: walk сигнатур --------------------------------------------- /// [M-channel-try-recv-ro-binding-p67-ice] (ICE-пачка п.6): `ty` (peeled /// of `readonly`/`mut` wrappers) is exactly the bare `Channel[T]` type — /// the constructor-only namespace, never a real instantiable value /// (see `check_fn`'s call-site doc for the full rationale). `label` names /// the position for the message (`p.name` for a param, `"<return>"` for /// a return type). fn channel_bare_type_diag(ty: &TypeRef, label: &str, span: Span) -> Option<Diagnostic> { let mut t = ty; loop { match t { TypeRef::Readonly(inner, _) | TypeRef::Mut(inner, _) => t = inner, _ => break, } } let TypeRef::Named { path, generics, .. } = t else { return None }; if path.len() != 1 || path[0] != "Channel" { return None; } let where_txt = if label == "<return>" { "return type".to_string() } else { format!("param `{}`", label) }; let generic_txt = if generics.is_empty() { "Channel" } else { "Channel[..]" }; Some(Diagnostic::new( format!( "[E_CHANNEL_TYPE_NOT_INSTANTIABLE] {} has type `{}` — `Channel[T]` \ is a CONSTRUCTOR NAMESPACE only (`Channel.new(cap)` / \ `Channel.with_capacity(cap)`), never a real value type; the \ runtime never materializes one (`Channel.new()` returns a \ `(ChanWriter[T], ChanReader[T])` capability pair — there is no \ `Channel` struct to receive method calls on). Use \ `ChanReader[T]` (for `.recv()`/`.try_recv()`/...) or \ `ChanWriter[T]` (for `.send()`/`.try_send()`/...) instead — \ whichever capability this position actually needs.", where_txt, generic_txt, ), span, )) } fn check_fn(&self, fd: &FnDecl, errors: &mut Vec<Diagnostic>) { // [M-channel-try-recv-ro-binding-p67-ice] (ICE-пачка п.6): `Channel[T]` // is declared `external type Channel[T]` purely as a NAMESPACE for the // `Channel.new(cap)`/`Channel.with_capacity(cap)` static constructors // (`is_channel_ctor`-style gates elsewhere in this file only ever // recognize "Channel" for `new`/`with_capacity`) — the runtime never // materializes a value of Nova-level type `Channel[T]` itself: // `Channel.new()` returns a `(ChanWriter[T], ChanReader[T])` pair // (D91 capability split, `nova_rt/channels.h`'s `Nova_ChannelPair` — // `Nova_ChanWriter`/`Nova_ChanReader` each own struct, holding a // `Nova_ChannelState*`; there is no `Nova_Channel` struct at all). A // param/local explicitly ANNOTATED `Channel[T]` (instead of the real // `ChanReader[T]`/`ChanWriter[T]` capability types) was silently // ACCEPTED by the checker (no producer ever validated this type // position) and reached codegen with C receiver type `Nova_Channel*` // — a name nothing lowers or dispatches, so ANY method call on it // (`.try_recv()`, `.recv()`, `.try_send()`, ...) panicked emit_c's // P67-LEGACY terminal ("method call return type unknown"). Reject at // the type-annotation site instead — clear compile-time diagnostic, // no ICE, no risk of miscompiling through a nonexistent C layout. for p in &fd.params { if let Some(diag) = Self::channel_bare_type_diag(&p.ty, &p.name, p.span) { errors.push(diag); } } if let Some(rt) = &fd.return_type { if let Some(diag) = Self::channel_bare_type_diag(rt, "<return>", fd.span) { errors.push(diag); } } // Plan 173 Ф.1 (#3 / Plan 174.2): `?` строго return-only. Свободный `?` // осмыслен лишь в fn, возвращающей Result/Option (проброс значением); // в Fail-эффект-fn → `[E_TRY_IN_FAIL_FN]` (там `!!`/`throw`). Consume-init // `?` (D196 form 2) и `?` в defer-body/closure — exempt (см. walker). { let ret_ok = fd.return_type.as_ref() .map(type_ref_is_result_or_option) .unwrap_or(false); match &fd.body { FnBody::Block(b) => check_try_return_only_block(b, ret_ok, errors), FnBody::Expr(e) => check_try_return_only_expr(e, ret_ok, errors), FnBody::External => {} } } // Plan 221.1 №113/№428 (D85/D62 ENFORCED): `!!` always throw-style, // requires enclosing `Fail[E']`-compatible effect (или `with Fail = // ...` local handler). Scope: EXPORTED fn only (D62) — private fn // остаются под D28 auto-inference (`infer_effects`). // // №428 fix (2026-08-07): the per-fn, syntax-only `check_bang_ // requires_fail_block`/`_expr` walk that used to live HERE was // blind to `!!`/`Fail` reached through a CALL into another // function (any depth of private-helper chaining, a directly // Fail-declared callee with no unwrap operator at the call site at // all, methods, generics — see `fail_reach.rs`'s module doc for // the probes). Resolving a call's target needs the FULL, // module-wide `resolved_callees` channel (§0/196), which is only // FINAL after this whole `check_module` walk returns — so the // check moved OUT of this per-fn early position into a separate // pass, `fail_reach::run`, called once from `check_module_impl` // right after `resolved_callees` is finalized (same timing as // `fiber_safety::run`). See that call site for the replacement. // Plan 114.4.2 (D199): set flag для scope-local const skip // когда мы внутри const fn body. Body checker // (check_const_fn_decl) точнее покрывает validation. let is_const_fn = fd.return_is_const || fd.params.iter().any(|p| p.is_const); let prev_in_const_fn = self.in_const_fn.get(); self.in_const_fn.set(is_const_fn); let _guard = ConstFnFlagGuard { ctx: self, prev: prev_in_const_fn }; // Plan 124 (D220): set current_recv_type для priv field access scope // tracking. Instance + Static methods обa get receiver's type_name; // priv field access разрешён в both contexts. let prev_recv = self.current_recv_type.borrow().clone(); let new_recv = fd.receiver.as_ref().map(|r| r.type_name.clone()); *self.current_recv_type.borrow_mut() = new_recv; let prev_recv_mut = self.current_recv_is_mut.get(); // №462/№370: `consume` ТОЖЕ может мутировать — владеющий получатель // имеет не меньше прав, чем `mut`. `mutable` и `consume` // взаимоисключающие (parser enforce'ит), поэтому у `consume` [INV-TODO: №462] // `mutable == false`, и одиночное чтение считало владельца за `ro`. // Закрыто здесь ПОСЛЕ того, как тот же промах в `ConsumeCtx` уже // покраснел на законном коде nova-tls: №370 был закрыт наполовину. self.current_recv_is_mut.set(fd.receiver.as_ref().map_or(false, |r| r.mutable || r.consume)); let _recv_guard = PrivRecvGuard { ctx: self, prev: prev_recv, prev_mut: prev_recv_mut }; // Plan 124.6 (D225): set current_fn_test_access — fn body gets priv // access к listed types (escape hatch для tests + helper fns). let prev_ta = std::mem::take(&mut *self.current_fn_test_access.borrow_mut()); *self.current_fn_test_access.borrow_mut() = fd.test_access_for.clone(); let _ta_guard = PrivTestAccessGuard { ctx: self, prev: prev_ta }; // Generic-scope функции: её собственные generic-параметры + // generic-параметры receiver-типа (`fn Box[T] @get() -> T`). let mut gs: GenericScope = HashMap::new(); for g in &fd.generics { gs.insert(g.name.clone(), g.clone()); } if let Some(r) = &fd.receiver { for tr in &r.generics { if let TypeRef::Named { path, span, .. } = tr { if path.len() == 1 { // Plan 196 gs-bounds: see `fn_generic_scope`'s twin doc — a // receiver carrier-bracket bound (`fn Option[T Debug] @debug`) // lives in `r.carrier_bounds`, NOT `r.generics` (bare names); // prefer it over a synthesized bound-less entry. gs.entry(path[0].clone()).or_insert_with(|| { r.carrier_bounds.iter() .find(|cb| cb.name == path[0]) .cloned() .unwrap_or_else(|| GenericParam::unbounded(path[0].clone(), *span)) }); } } } // Plan 153.5 (D263) / [M-153.5-flatten-nested-receiver]: a NESTED // CARRIER receiver (`Vec[Vec[T]]`) declares its typevars in the // carrier brackets; they land in `r.generics` (as `Named{T}`) and so // are already in scope via the loop above. A NESTED SLICE receiver // (`fn[T] [][]T`) declares `T` via the `fn[T]` prefix (already in // `fd.generics` → `gs`). The structured `r.receiver_ty` is therefore // NOT used to seed `gs`: doing so would mask the // `E_UNDECLARED_TYPEVAR_IN_RECEIVER` diagnostic for an UNdeclared // slice typevar (`fn []T @m` without a `fn[T]` prefix), which is the // whole point of that check. } // Plan 101.1 B1 (Ф.2 E_UNDECLARED_TYPEVAR_IN_RECEIVER): // Detect `fn []T @method` где T — single-uppercase letter без // `fn[T]` префикса (не в gs). Это silent miscompile в old codegen // (defaults T=nova_int). Loud error suggests `fn[T]` prefix fix. if let Some(r) = &fd.receiver { if r.type_name.starts_with("[]") { let elem = &r.type_name[2..]; let is_single_upper = elem.len() <= 2 && elem.chars().all(|c| c.is_ascii_uppercase()); if is_single_upper && !gs.contains_key(elem) { errors.push(Diagnostic::new( format!( "[E_UNDECLARED_TYPEVAR_IN_RECEIVER] `fn []{elem} @{m}` — \ typevar `{elem}` не объявлен. Добавьте `fn[{elem}]` префикс \ (Plan 101.1 / D145):\n \ fn[{elem}] []{elem} @{m}(...) -> ...", elem = elem, m = fd.name ), r.span, )); } } // Plan 101.1 B2 (Ф.2 E_BARE_TYPEVAR_NEEDS_PREFIX): // Detect `fn T @method` где T — bare single-uppercase letter // (not array, not other shape) без `fn[T]` prefix. Allowed only // если T in gs (declared via prefix) OR T — named type (но // type-check error elsewhere). Distinct from B1 (which targets `[]T`). let tn = r.type_name.as_str(); if tn.len() <= 2 && tn.chars().all(|c| c.is_ascii_uppercase()) { if !gs.contains_key(tn) && !self.types.contains_key(tn) { errors.push(Diagnostic::new( format!( "[E_BARE_TYPEVAR_NEEDS_PREFIX] `fn {tn} @{m}` — \ bare typevar `{tn}` receiver требует `fn[{tn}]` префикс \ (Plan 101.1 / D145):\n \ fn[{tn}] {tn} @{m}(...) -> ...\n \ OR declare `type {tn} {{ ... }}` если intended named type.", tn = tn, m = fd.name ), r.span, )); } } // Plan 101.1 C8 (Ф.2 E_UNUSED_PREFIX_TYPEVAR): // Каждый prefix-generic должен использоваться в receiver/params/return. // Если объявлен но не используется — error. { let mut referenced: HashSet<String> = HashSet::new(); // Collect from receiver type-name (bare T case). let tn_rec = r.type_name.as_str(); if tn_rec.len() <= 2 && tn_rec.chars().all(|c| c.is_ascii_uppercase()) { referenced.insert(tn_rec.to_string()); } // Collect from receiver type generics (`[]T`, Option[T], etc.). for tr in &r.generics { Self::collect_named_idents(tr, &mut referenced); } // Array element if receiver is []T. if let Some(elem) = r.type_name.strip_prefix("[]") { if elem.len() <= 2 && elem.chars().all(|c| c.is_ascii_uppercase()) { referenced.insert(elem.to_string()); } } // Plan 153.5 (D263) / [M-153.5-flatten-nested-receiver]: a NESTED // receiver (`[][]T`, `Vec[Vec[T]]`) references its typevars only // through the structured `receiver_ty` — collect them so a // legitimately-used nested typevar is not flagged unused. if let Some(rty) = &r.receiver_ty { Self::collect_named_idents(rty, &mut referenced); } // Collect from params. for p in &fd.params { Self::collect_named_idents(&p.ty, &mut referenced); } // Collect from return type. if let Some(rt) = &fd.return_type { Self::collect_named_idents(rt, &mut referenced); } // Collect from the method's OWN top-level effect-clause // (`Time Random Fail[E] -> T`, between `)` and `->`) — a // prefix-declared typevar used only there (effect-polymorphism, // D145) is a legitimate usage, not unused. See sibling fix in // `collect_named_idents`'s `TypeRef::Func` arm for the analogous // fn-typed-PARAM effect-clause case. for e in &fd.effects { Self::collect_named_idents(e, &mut referenced); } // №254 (221.1, Iter-delegate blankets): a prefix typevar used // ONLY inside a SIBLING prefix generic's protocol bound // (`fn[C Iter[I], I Next[T]] C @collect() -> Vec[T]` — `I` // never appears literally in receiver/params/return, only // inside `C`'s bound `Iter[I]`) is genuinely constrained, not // orphaned — `I` fixes WHICH iterator `@iter()` must return // for `C`'s bound to typecheck at a call site. Scan every // prefix generic's OWN bounds for nested typevar references // before the unused-check below (mirrors the effects-clause // carve-out above — same D145 "legitimate usage" spirit). for g in &fd.generics { for b in &g.bounds { Self::collect_named_idents(b, &mut referenced); } } // Check each fd.generics — must be referenced. for g in &fd.generics { if !referenced.contains(&g.name) { errors.push(Diagnostic::new( format!( "[E_UNUSED_PREFIX_TYPEVAR] generic `{name}` declared в \ `fn[…]` prefix но не используется в receiver, params, \ или return type (Plan 101.1 / D145). Удалите из prefix.", name = g.name ), r.span, )); } } } // Plan 101.1 B4 (Ф.2 E_PREFIX_SHADOWS_NAMED_TYPE): // Detect `fn[T] T @method` + `type T { ... }` в scope. fn-prefix // shadows named type — ambiguous. Loud error suggests rename. for g in &fd.generics { if self.types.contains_key(&g.name) { errors.push(Diagnostic::new( format!( "[E_PREFIX_SHADOWS_NAMED_TYPE] `fn[{tn}] ...` — \ generic `{tn}` shadows named type `{tn}` in scope \ (Plan 101.1 / D145). Rename one:\n \ - rename prefix generic: `fn[T2] {tn} @{m}(...)` (use named T)\n \ - rename named type: `type {tn}_New {{ ... }}` (free up T)", tn = g.name, m = fd.name ), r.span, )); } } // Plan 221.1 №88 (iv) [M-structured-receiver-generic-not-enforced]: // ЗАПРЕТ ЗАТЕНЕНИЯ (owner decision) — a receiver CARRIER-BRACKET // slot (no `fn[T]` prefix; declared by APPEARANCE per (ii)'s // doctrine, `fn Vec[Vec[T]] @flatten` style) whose name COINCIDES // with an already-declared real type (`self.types`) or a // primitive scalar name is ambiguous: is `Wid` meant as a fresh // typevar that happens to collide, or a deliberate concrete // specialization (`fn Vec[Vec[Wid]] @wsum` binding NOTHING, // silently registering under the SAME `method_table["Vec"]` key // as every other Vec-shaped receiver)? Specialization by concrete // type is NOT supported (may become a feature later) — reject // loudly instead of accepting it as an unenforced no-op that // dispatches on ANY `Vec[...]` receiver at the call site. // // Walks EVERY leaf slot of `r.receiver_ty` (bare single-segment // `Named` with empty generics — a "slot" position, not a // container name like `Vec`/`OneBox` itself), at any nesting // depth, so `fn Vec[Vec[Wid]] @wsum` is caught even though `Wid` // (not typevar-shaped: >2 chars) was never harvested into // `r.generics` by the parser's `ident_is_typevar` heuristic — // parser/mod.rs's `collect_free_typevars` deliberately SKIPS // non-typevar-shaped names (so a genuine nested type reference // like `Vec[Vec[int]]`'s "int" isn't mis-harvested as a param); // this checker-side walk has no such gate — EVERY leaf is a // candidate for the collision check, harvested-as-generic or not. // // Skip names already in `fd.generics` (the `fn[T]`-PREFIX set) — // those are B4's turf (`E_PREFIX_SHADOWS_NAMED_TYPE` just above); // this check is the carrier-bracket-only counterpart. // Named-carrier receivers ONLY (`Vec[...]`, `OneBox[...]`) — the // top-level SLICE-sugar receiver (`fn[T] [][]T @m`, `type_name` // synthesized as `"[]T"`/`"[][]T"`) is a SEPARATE, already-sound // mechanism (its own `E_UNDECLARED_TYPEVAR_IN_RECEIVER`/ // `E_BARE_TYPEVAR_NEEDS_PREFIX` gates just above already narrow // typevar-treatment to short-uppercase names ONLY) — a CONCRETE // slice element (`fn []u8 @to_str_unchecked`, std/runtime/string) // is an intentional, unambiguous, already-relied-upon concrete // receiver, not a shadow. if !r.type_name.starts_with("[]") { if let Some(rty) = &r.receiver_ty { let fd_generic_names: HashSet<&str> = fd.generics.iter().map(|g| g.name.as_str()).collect(); let mut leaves: Vec<(String, Span)> = Vec::new(); Self::collect_receiver_carrier_slot_leaves(rty, 0, &mut leaves); let mut reported: HashSet<String> = HashSet::new(); for (leaf_name, leaf_span) in leaves { if fd_generic_names.contains(leaf_name.as_str()) { continue; } if !reported.insert(leaf_name.clone()) { continue; } let is_shadow = self.types.contains_key(&leaf_name) || Self::is_primitive_scalar_type_name(&leaf_name); if is_shadow { errors.push(Diagnostic::new( format!( "[E_RECV_GENERIC_SHADOWS_TYPE] receiver generic param \ `{leaf}` затеняет объявленный тип `{leaf}` — переименуй \ параметр (Plan 221.1 №88, доктрина владельца: \ специализация receiver'а конкретным типом НЕ \ поддерживается). Если `{leaf}` — реальный тип, дай \ параметру другое имя (`fn {rn}[…{leaf}2…] @{m}(...)`); \ если это опечатка typevar'а — используй короткое \ uppercase-имя (`T`/`U`/`K`/`V`).", leaf = leaf_name, rn = r.type_name, m = fd.name, ), leaf_span, )); } } } } // Plan 101.1 B3 (Ф.2 E_DUPLICATE_GENERIC_DECL): // Detect `fn[K, V] HashMap[K, V] @method` — generics в `fn[…]` // дублируют carrier-brackets `Name[K, V]`. Удалите fn-prefix // OR удалите из carrier. // // Collect carrier-declared generics (single-upper names from // receiver.generics) and check if any fn-prefix-generic // (fd.generics from prefix) duplicates them. let carrier_decls: HashSet<String> = r.generics.iter() .filter_map(|tr| { if let TypeRef::Named { path, .. } = tr { if path.len() == 1 { let n = &path[0]; if n.len() <= 2 && n.chars().all(|c| c.is_ascii_uppercase()) { return Some(n.clone()); } } } None }) .collect(); for g in &fd.generics { if carrier_decls.contains(&g.name) { errors.push(Diagnostic::new( format!( "[E_DUPLICATE_GENERIC_DECL] generic `{tn}` уже введён через \ receiver `{rn}[{ts}]` — удалите из `fn[…]` префикса \ (Plan 101.1 / D145):\n \ fn {rn}[{ts}] @{m}(...) // без fn[{tn}]", tn = g.name, rn = r.type_name, ts = r.generics.iter().map(|t| format!("{:?}", t)).collect::<Vec<_>>().join(", "), m = fd.name ), r.span, )); } } } // Bounds и defaults generic-параметров. for g in &fd.generics { for b in &g.bounds { self.walk_typeref(b, &gs, errors); } if let Some(d) = &g.default { self.walk_typeref(d, &gs, errors); } } // Параметры, return, эффекты. for p in &fd.params { self.walk_typeref(&p.ty, &gs, errors); if let Some(dv) = &p.default { self.walk_expr(dv, &gs, errors); } } if let Some(rt) = &fd.return_type { // Plan 184 (Р1): возврат — легальная top-level ref-позиция // (`-> ref Self` де-сахар `-> @` для value-типов). self.walk_ref_return(rt, &gs, errors); } for e in &fd.effects { self.walk_typeref(e, &gs, errors); } for c in &fd.contracts { self.walk_expr(&c.expr, &gs, errors); } if let Some(d) = &fd.decreases { self.walk_expr(d, &gs, errors); } // Тело. match &fd.body { FnBody::Expr(e) => self.walk_expr(e, &gs, errors), FnBody::Block(b) => self.walk_block(b, &gs, errors), FnBody::External => {} } } fn check_type_decl(&self, td: &TypeDecl, errors: &mut Vec<Diagnostic>) { // Plan 167: single-letter type names forbidden (D30 amend). // Generic parameters are conventionally single-letter (T, S, K, V) — naming // conflict with type S causes E_PREFIX_SHADOWS_NAMED_TYPE. Ban type names // of length 1 to make the namespaces non-overlapping. if td.name.chars().count() == 1 { errors.push(Diagnostic::new( format!( "[E_TYPE_NAME_TOO_SHORT] type name `{name}` is a single character — \ must be at least 2 characters (Plan 167, D30 §naming). \ Rename to a descriptive PascalCase name, e.g. `{name}Val`, \ `{name}Node`, `{name}Item`. \ Single-letter names conflict with generic parameters by convention.", name = td.name ), td.span, )); } let mut gs: GenericScope = HashMap::new(); for g in &td.generics { gs.insert(g.name.clone(), g.clone()); } for g in &td.generics { for b in &g.bounds { self.walk_typeref(b, &gs, errors); } if let Some(d) = &g.default { self.walk_typeref(d, &gs, errors); } } match &td.kind { TypeDeclKind::Record(fields) => { for f in fields { self.walk_typeref(&f.ty, &gs, errors); } } TypeDeclKind::Sum(variants) => { for v in variants { match &v.kind { SumVariantKind::Unit => {} SumVariantKind::Tuple(tys) => { for t in tys { self.walk_typeref(t, &gs, errors); } } SumVariantKind::Record(fields) => { for f in fields { self.walk_typeref(&f.ty, &gs, errors); } } } } } TypeDeclKind::Effect(methods) => { for m in methods { let mut ms = gs.clone(); for g in &m.generics { ms.insert(g.name.clone(), g.clone()); } for p in &m.params { self.walk_typeref(&p.ty, &ms, errors); } if let Some(rt) = &m.return_type { self.walk_ref_return(rt, &ms, errors); } for e in &m.effects { self.walk_typeref(e, &ms, errors); } } } TypeDeclKind::Protocol { methods, embeds } => { for m in methods { let mut ms = gs.clone(); for g in &m.generics { ms.insert(g.name.clone(), g.clone()); } for p in &m.params { self.walk_typeref(&p.ty, &ms, errors); } if let Some(rt) = &m.return_type { self.walk_ref_return(rt, &ms, errors); } for e in &m.effects { self.walk_typeref(e, &ms, errors); } } // Plan 101.4: validate embedded protocol type references. for e in embeds { self.walk_typeref(e, &gs, errors); } } // Plan 120 (D215): walk field types in named tuple declarations. TypeDeclKind::NamedTuple(fields) => { for f in fields { self.walk_typeref(&f.ty, &gs, errors); } } TypeDeclKind::Newtype(tr) => { self.walk_typeref(tr, &gs, errors); // Plan 91.12 V2 followup [M-91.12-generic-newtype-non-ptr-inner] // (2026-06-02): generic newtype `type X[T](INNER)` где INNER // использует generic param T (e.g. `type Wrap[T](T)`) НЕ // поддерживается. Causes: codegen emit'ит `typedef Nova_T // Nova_Wrap;` где `Nova_T` — type-param placeholder, не // resolved C type → C compile error. // // Semantically tuple newtype = transparent typedef (Plan 115 // D214); same C ABI shared across mono'd instances. Per-T // storage variance (sizeof(T) differs) — это record-semantics, // не newtype. User should migrate к record form: // type Wrap[T] { value T } ← properly mono'd per T if !td.generics.is_empty() { let type_params: HashSet<String> = td.generics.iter() .map(|g| g.name.clone()) .collect(); if Self::typeref_uses_param(tr, &type_params) { let inner_span = match tr { TypeRef::Named { span, .. } => *span, TypeRef::Array(_, span) => *span, TypeRef::FixedArray(_, _, span) => *span, TypeRef::Tuple(_, span) => *span, TypeRef::Func { span, .. } => *span, TypeRef::Protocol { span, .. } => *span, TypeRef::Unit(span) => *span, TypeRef::Readonly(_, span) => *span, _ => td.span, }; errors.push(Diagnostic::new( format!( "[E_GENERIC_NEWTYPE_INNER_USES_PARAM] generic newtype \ `type {}[..](..)` cannot use type-parameter \ in inner position — newtype = transparent typedef \ (Plan 115 D214), shared C ABI across all T's. \ Per-T storage requires record-semantics: replace \ with `type {} {{ value T }}` (record form, properly \ mono'd per T).", td.name, td.name ), inner_span, )); } } } TypeDeclKind::Alias(tr) => self.walk_typeref(tr, &gs, errors), // Plan 172.3 (D310): walk type-set member type-refs (validates each // member is a known type; further member-concreteness/signedness checks // are done in the type-set declaration pass). TypeDeclKind::TypeSet(members) => { for m in members { self.walk_typeref(m, &gs, errors); } } TypeDeclKind::Opaque => {} } // Plan 124.8 [M-124.8-zero-on-move] (2026-06-03): validation — // `#zero_on_move` применим только к Record (heap + value), NamedTuple, // Newtype. Не применим к Effect/Protocol (нет storage), Sum (variant // storage non-trivial; V2 followup), Alias/Opaque (нет own layout). if td.zero_on_move { let kind_ok = matches!( &td.kind, TypeDeclKind::Record(_) | TypeDeclKind::NamedTuple(_) | TypeDeclKind::Newtype(_) ); if !kind_ok { let kind_str = match &td.kind { TypeDeclKind::Record(_) => "record", TypeDeclKind::Sum(_) => "sum", TypeDeclKind::Effect(_) => "effect", TypeDeclKind::Protocol { .. } => "protocol", TypeDeclKind::Alias(_) => "alias", TypeDeclKind::Opaque => "external opaque", TypeDeclKind::NamedTuple(_) => "named tuple", TypeDeclKind::Newtype(_) => "newtype", TypeDeclKind::TypeSet(_) => "type set", // Plan 172.3 (D310) }; errors.push(Diagnostic::new( format!( "[E_ZERO_ON_MOVE_INVALID_KIND] `#zero_on_move` cannot be \ applied to `{}` ({}). Allowed kinds: record \ (heap + value), named tuple, newtype. Plan 124.8 \ [M-124.8-zero-on-move].", td.name, kind_str ), td.span, )); } else { // №465 (D-amendment, [M-124.8-zero-on-move-auto-inject] V2): // auto-inject only fires at consume-tracked call sites (D432 // §4 — receiver-consuming-call/consume-param-arg/bare // consume-return, keyed off `consume_receiver_methods`/ // `*_consume_param_positions`, which are themselves keyed // off `consume`). A `#zero_on_move` type without `consume` // has NO tracked ownership-transfer point to hook into — it // would stay the exact silent no-op #465 reported, just // narrower. Require `consume` explicitly rather than let it // compile to an inert attribute. if !td.consume { errors.push(Diagnostic::new( format!( "[E_ZERO_ON_MOVE_REQUIRES_CONSUME] `#zero_on_move` on \ `{}` has no effect without `consume` — the \ auto-inject hooks into consume-tracked ownership- \ transfer sites only (D432 §4); a non-`consume` \ type has none. Add `consume` to `{}`'s \ declaration. Plan 124.8 [M-124.8-zero-on-move-auto-inject].", td.name, td.name ), td.span, )); } // №465 (D-amendment): heap-allocated Record rejected — // Nova's ownership-transfer for `AllocKind::Heap` records is // pointer-ALIASING (the moved-to binding and the moved-from // binding reference the IDENTICAL heap block; `consume` // never deep-copies a heap record). Zeroing the pointee at a // consume call site would zero the value for the NEW owner // too — the same memory, not a stale copy. Only genuine // copy-semantics storage (value record / named tuple / // newtype) can be safely auto-zeroed on move. See // spec/decisions/02-types.md D-amendment for #465 for the // full analysis (probed empirically via generated-C // inspection, scratch465/probe1-3.nv). if let TypeDeclKind::Record(_) = &td.kind { if td.allocation == AllocKind::Heap { errors.push(Diagnostic::new( format!( "[E_ZERO_ON_MOVE_ALIASED_STORAGE] `#zero_on_move` \ cannot be applied to `{}` — it is a heap-allocated \ record (`type {} {{ ... }}`, no `value` modifier). \ Ownership transfer for heap records is pointer- \ aliasing, not a byte copy: zeroing the storage \ after a move would corrupt the value for the new \ owner (same memory). Only `value`-allocated \ records, named tuples, and newtypes (byte-copy \ storage) support `#zero_on_move`. Plan 124.8 \ [M-124.8-zero-on-move-auto-inject].", td.name, td.name ), td.span, )); } } } } // Q-infinite-value-type (D280 §4): genuinely-infinite VALUE types — // a value record / named-tuple / newtype / alias whose layout transitively // contains ITSELF through INLINE edges only (no pointer/heap/slice/Option // indirection). Such a type has no finite object layout. The const_fn_eval // size walk degrades gracefully (depth-guard → None, surfaced only when // `size_of` forces it); this dedicated check reports it at type-check time. self.check_infinite_type(td, errors); // Plan 173.3 (D415 §1): `#share` — applicable to kinds that have a // concrete (if compiler-opaque) instance identity: Record, NamedTuple, // Newtype, Opaque, Sum. NOT applicable to Effect/Protocol/Alias/TypeSet // (no own storage / not a concrete instantiable type to vouch for). if td.attrs.contains(&crate::ast::TypeAttr::Share) { let kind_ok = matches!( &td.kind, TypeDeclKind::Record(_) | TypeDeclKind::NamedTuple(_) | TypeDeclKind::Newtype(_) | TypeDeclKind::Opaque | TypeDeclKind::Sum(_) ); if !kind_ok { let kind_str = match &td.kind { TypeDeclKind::Record(_) => "record", TypeDeclKind::Sum(_) => "sum", TypeDeclKind::Effect(_) => "effect", TypeDeclKind::Protocol { .. } => "protocol", TypeDeclKind::Alias(_) => "alias", TypeDeclKind::Opaque => "external opaque", TypeDeclKind::NamedTuple(_) => "named tuple", TypeDeclKind::Newtype(_) => "newtype", TypeDeclKind::TypeSet(_) => "type set", }; errors.push(Diagnostic::new( format!( "[E_SHARE_INVALID_KIND] `#share` cannot be applied to `{}` \ ({}) — no own instance storage to vouch for. Allowed kinds: \ record, named tuple, newtype, opaque, sum. Plan 173.3 (D415 §1).", td.name, kind_str ), td.span, )); } } } /// Q-infinite-value-type (D280 §4): report an `E_INFINITE_TYPE` diagnostic /// when this value type's layout transitively embeds itself through INLINE /// edges only — i.e. there is a value-containment cycle with no /// pointer/heap/slice/Option indirection to break it. /// /// The INLINE-vs-INDIRECTION classification MIRRORS /// `const_fn_eval::type_size_or_align_resolved_d` (post-D280): /// - INLINE (recurse, can carry a cycle): a field whose named type is a /// VALUE record / named-tuple / newtype / alias-to-value; tuples; /// fixed-arrays of inline types; transparent `ro`/`mut`/`unsafe` /// wrappers over an inline type. /// - INDIRECTION (STOP — finite, breaks the cycle): a HEAP type (Sum is /// ALWAYS heap; a Record with `td.allocation.is_heap()`), a pointer /// `*T`, a slice `[]T`/Array, `str`, any generics-carrying Named /// (`Option[..]`/`Vec[..]`/`Wrapper[..]` — they box / are not inlined /// by the size walk), primitives, `Unit`, `Func`, `Protocol`. /// /// Detect with a DFS over the inline-containment graph keyed by type name, /// with an on-path set (mirrors `type_is_consume_v`'s visited-set, except we /// only follow inline edges and report on a back-edge into a node already on /// the current path). Catches direct self-cycles (`type N value { next N }`) /// and mutual cycles (`type A value { b B }` + `type B value { a A }`). /// /// Only types DECLARED in entry-peer files (the user's own source) are /// reported — a cycle is reported once, on the decl currently being checked, /// when the DFS starting at `td` revisits `td` itself. Reporting on the /// entry node (rather than every node on the cycle) keeps the diagnostic /// stable and avoids duplicate errors for mutual cycles (each peer reports /// its own self-reaching path). fn check_infinite_type(&self, td: &TypeDecl, errors: &mut Vec<Diagnostic>) { // Only value records / named-tuples / newtypes / aliases can be the // *subject* of a value-containment cycle. A heap record / sum boxes to a // pointer and is never inlined into a field, so it can never close a // cycle (it is an INDIRECTION boundary). Skip everything else fast. let subject_inlineable = match &td.kind { TypeDeclKind::Record(_) => !td.allocation.is_heap(), TypeDeclKind::NamedTuple(_) | TypeDeclKind::Newtype(_) | TypeDeclKind::Alias(_) => true, _ => false, }; if !subject_inlineable { return; } // Generic params of the decl under check — a bare param `T` is not a // user type and cannot close a value cycle (mirrors the size walk, which // has no entry for an unresolved param → None). let mut gs: GenericScope = HashMap::new(); for g in &td.generics { gs.insert(g.name.clone(), g.clone()); } // DFS over the inline-containment graph starting at `td.name`. `on_path` // is the set of type names currently on the DFS stack; a back-edge into // a node on the path is a cycle. We only care whether the cycle passes // through `td.name` (the subject) — if so, `td` is infinite. let mut on_path: HashSet<String> = HashSet::new(); // The field whose type carries the back-edge to `td` — captured for the // diagnostic span + message. `(field_label, field_ty)`. let root = td.name.clone(); on_path.insert(root.clone()); for (label, field_ty, _path_segs) in Self::inline_fields_of(td) { if let Some(offender_span) = self.infinite_dfs(field_ty, &root, &mut on_path, &gs) { // Found a value-containment cycle reaching `td` through INLINE // edges only. Report once, on the offending field of `td`. let field_ty_str = Self::typeref_display(field_ty); errors.push(Diagnostic::new( format!( "[E_INFINITE_TYPE] value type `{name}` has infinite size: \ field `{label}` of type `{fty}` contains `{name}` by value \ (no pointer/heap indirection), so the layout recurses without \ bound. Break the cycle with indirection: box it behind a sum \ variant (sums are always heap), wrap the field in `Option[{fty}]`, \ use a pointer `*{fty}`, or a slice `[]{fty}`.", name = td.name, label = label, fty = field_ty_str, ), offender_span, )); // One diagnostic per offending decl is enough — the first inline // field that closes a cycle is the actionable site. (A type with // several independent cyclic fields still gets reported via the // first; fixing it re-runs the check.) break; } } } /// Q-infinite-value-type: the INLINE-edge field list of a value-carrying /// `TypeDecl` — the contained types that participate in the value layout. /// Returns `(field_label, &TypeRef, ())` for each inline edge. Heap records /// and sums are NOT subjects here (handled by the caller's gate), so this /// only enumerates value Record / NamedTuple / Newtype / Alias contents. fn inline_fields_of(td: &TypeDecl) -> Vec<(String, &TypeRef, ())> { match &td.kind { TypeDeclKind::Record(fields) => fields .iter() .map(|f| (f.name.clone(), &f.ty, ())) .collect(), TypeDeclKind::NamedTuple(fields) => fields .iter() .map(|f| (f.name.clone(), &f.ty, ())) .collect(), TypeDeclKind::Newtype(inner) => vec![("(0)".to_string(), inner, ())], TypeDeclKind::Alias(inner) => vec![("(alias)".to_string(), inner, ())], _ => Vec::new(), } } /// Q-infinite-value-type: DFS over the inline-containment graph from a single /// field `TypeRef`. Returns `Some(span)` of the offending TypeRef if a value /// cycle reaching `root` (through INLINE edges only) is found, else `None`. /// `on_path` holds the type names currently on the DFS stack. `gs` is the set /// of generic-param names of the root decl (bare params are STOP leaves). /// /// Classification mirrors `type_size_or_align_resolved_d` exactly: see the /// per-`TypeRef` arms below. STOP arms return `None` (finite, breaks cycle); /// INLINE arms recurse; reaching a Named that resolves to `root` (or any node /// already on `on_path`, which transitively reaches `root`) is the cycle. fn infinite_dfs( &self, t: &TypeRef, root: &str, on_path: &mut HashSet<String>, gs: &GenericScope, ) -> Option<Span> { match t { TypeRef::Named { path, generics, span } => { // Mirror the size walk's inline Named arm gate exactly: // ONLY `path.len() == 1 && generics.is_empty()` is inlined. // A module-qualified path (len > 1) OR any non-empty generics // (Option[..]/Vec[..]/Wrapper[..]) falls to the size walk's // `_ => None` → INDIRECTION/finite here. STOP. if path.len() != 1 || !generics.is_empty() { return None; } let name = &path[0]; // Bare generic-param of the root decl — not a user type. STOP. if gs.contains_key(name) { return None; } // Primitives are finite leaves (and are NOT in self.types). STOP. if Self::is_primitive_type_name(name) { return None; } // A back-edge into a node already on the DFS path → cycle. Every // node on `on_path` transitively reaches `root` (the path was // grown from `root`), so reaching any of them means `root` is on // an inline cycle. Report at this field's span. if on_path.contains(name) { return Some(*span); } // Resolve the named TypeDecl. Unresolvable → STOP (the size walk // returns None there; a false positive would violate the // zero-false-positive gate). let td = match self.types.get(name).copied() { Some(td) => td, None => return None, }; // BOXING short-circuit — the load-bearing branch. A Sum is ALWAYS // heap; a Record is indirection iff `td.allocation.is_heap()`. // Either way the reference is an 8-byte pointer leaf → finite, // BREAKS the cycle. STOP. (Mirrors size walk lines 1147-1152.) let boxed_to_pointer = matches!(&td.kind, TypeDeclKind::Sum(_)) || (matches!(&td.kind, TypeDeclKind::Record(_)) && td.allocation.is_heap()); if boxed_to_pointer { return None; } // INLINE: value Record / NamedTuple / Newtype / Alias. Recurse // into its contained types, with `name` pushed on the path. on_path.insert(name.clone()); let mut found = None; for (_label, field_ty, _seg) in Self::inline_fields_of(td) { if let Some(s) = self.infinite_dfs(field_ty, root, on_path, gs) { found = Some(s); break; } } on_path.remove(name); found } // Tuples: INLINE of each element (size walk recurses each elem). TypeRef::Tuple(elems, _) => { for e in elems { if let Some(s) = self.infinite_dfs(e, root, on_path, gs) { return Some(s); } } None } // Fixed array `[n]T`: n inline copies of T → INLINE-recurse the elem // regardless of n (even one inline copy closes a cycle). TypeRef::FixedArray(_, elem, _) => self.infinite_dfs(elem, root, on_path, gs), // Transparent wrappers — recurse inner, preserve its inline/indirection // nature (`ro T`/`mut T`/`unsafe T`). The OUTER Pointer of `*ro T` // etc. has already stopped before reaching these. TypeRef::Readonly(inner, _) | TypeRef::Mut(inner, _) | TypeRef::Uninit(inner, _) => self.infinite_dfs(inner, root, on_path, gs), // Pointer `*T` (any pointee) — always 8 bytes. INDIRECTION. STOP. TypeRef::Pointer(_, _) => None, // Plan 184: `ref T` — указатель-алиас (8 байт). INDIRECTION. STOP. // (ref к тому же root не создаёт inline-цикл; к тому же Р1 запрещает // ref в полях — сюда он в норме не доходит.) TypeRef::Ref(_, _) => None, // Slice `[]T` — 16-byte {ptr,len}. INDIRECTION. STOP. TypeRef::Array(_, _) => None, // Unit / Func / Protocol — finite leaves / existentials. STOP. TypeRef::Unit(_) | TypeRef::Func { .. } | TypeRef::Protocol { .. } => None, } } /// Q-infinite-value-type: primitive type names that are finite leaves and /// are NOT present in `self.types` (mirrors the size walk's primitive table). #[inline] fn is_primitive_type_name(name: &str) -> bool { matches!( name, "int" | "i64" | "u64" | "f64" | "i32" | "u32" | "f32" | "i16" | "u16" | "i8" | "u8" | "uint" | "bool" | "char" | "str" ) } /// Plan 174.3 (D54 §6): validate the operand of `x is T`. `is` is legal on /// `any` (v1 — runtime type_id downcast) and on sum-values (v2 — variant /// check). On a record or primitive operand it is a compile error: the type /// is statically known, so the check is meaningless. Conservative — the /// diagnostic fires ONLY when the operand type is confidently a concrete /// non-`any`, non-sum type (unknown / generic-param / newtype / protocol / /// tuple / func → permissive, so no false positive on legal code, §7). fn check_is_operand( &self, inner: &Expr, _ty: &TypeRef, gs: &GenericScope, scope: &HashMap<String, TypeRef>, errors: &mut Vec<Diagnostic>, ) { let Some(op_tr) = self.infer_expr_type(inner, scope) else { return }; // `any` (empty-protocol top-type) → OK (v1). if is_unknown_type(&op_tr) { return; } let TypeRef::Named { path, .. } = &op_tr else { return }; let Some(base) = path.last() else { return }; // Generic type-parameter → unknown at this site → permissive. if gs.contains_key(base) { return; } // Prelude sum-types (variant check, v2) → OK. if base == "Option" || base == "Result" || base == "any" { return; } let is_record = match self.types.get(base) { Some(td) => match &td.kind { // Sum-typed operand (incl. enums) → OK (v2 variant check). TypeDeclKind::Sum(_) => return, TypeDeclKind::Record(_) | TypeDeclKind::NamedTuple(_) => true, // newtype / protocol / alias / typeset / opaque / effect — // conservatively skip (avoid false positives). _ => false, }, None => false, }; if is_record || Self::is_primitive_type_name(base) { errors.push(Diagnostic::new( format!( "[E_IS_NON_ANY] `is` работает только на `any` (runtime type-check, \ D54 v1) и на sum-значениях (variant-check, D54 v2). Операнд типа \ `{}` — не sum и не `any`: его тип известен статически, проверка \ `is` бессмысленна. Уберите `is` или сравнивайте значение через \ `==` / `match`.", typeref_render(&op_tr) ), inner.span, )); } } /// Q-infinite-value-type: compact display of a `TypeRef` for the diagnostic /// message (`Option[Node]`, `*Node`, `[]Node`, `Node`). Best-effort — only /// the common shapes; falls back to the leading segment name. fn typeref_display(t: &TypeRef) -> String { match t { TypeRef::Named { path, generics, .. } => { let base = path.join("."); if generics.is_empty() { base } else { let args: Vec<String> = generics.iter().map(Self::typeref_display).collect(); format!("{}[{}]", base, args.join(", ")) } } TypeRef::Array(inner, _) => format!("[]{}", Self::typeref_display(inner)), TypeRef::FixedArray(n, inner, _) => format!("[{}]{}", n, Self::typeref_display(inner)), TypeRef::Pointer(inner, _) => format!("*{}", Self::typeref_display(inner)), TypeRef::Readonly(inner, _) => format!("ro {}", Self::typeref_display(inner)), TypeRef::Mut(inner, _) => format!("mut {}", Self::typeref_display(inner)), // §10a rename (Plan 174.5, 2026-07-11): `Uninit` is shared between // the renamed possibly-uninit data modifier (`uninit T`) and the // UNRENAMED legacy fn-pointer shape (`unsafe fn(...)`, D216 §10 — // call-requires-unsafe, not possibly-uninit data). Disambiguate // by payload: `Func` keeps the `unsafe` spelling, else `uninit`. TypeRef::Uninit(inner, _) => { let kw = if matches!(inner.as_ref(), TypeRef::Func { .. }) { "unsafe" } else { "uninit" }; format!("{} {}", kw, Self::typeref_display(inner)) } TypeRef::Ref(inner, _) => format!("ref {}", Self::typeref_display(inner)), TypeRef::Tuple(elems, _) => { let parts: Vec<String> = elems.iter().map(Self::typeref_display).collect(); format!("({})", parts.join(", ")) } TypeRef::Unit(_) => "()".to_string(), TypeRef::Func { .. } => "fn(..)".to_string(), TypeRef::Protocol { .. } => "<protocol>".to_string(), } } /// Plan 91.12 V2 followup #3 (2026-06-02): thin wrapper над /// `TypeRef::uses_any_type_param` (ast::mod.rs) — extracted common /// helper, removed duplication с codegen's parallel impl (emit_c.rs). #[inline] fn typeref_uses_param(tr: &TypeRef, params: &HashSet<String>) -> bool { tr.uses_any_type_param(params) } /// **Plan 184 (Р6):** является ли цель `ref`-типа ПОДТВЕРЖДЁННО кучевой /// (тогда `ref H ≡ H` нормализуется и легален в любой позиции). Кучевые: /// indirection-формы (`[]T`/`*T`/fn/protocol), кучевой record /// (`allocation != Value`), `Vec`; newtype/alias — по обёрнутому. Value /// (примитивы, value-record, named-tuple, tuple, unit), **`[N]T` (D27- /// амендмент, [M-fixed-array-value-semantics], 2026-07-10 — inline value, /// НЕ heap, аналогично Tuple/Unit ниже)**, generic-параметр и НЕИЗВЕСТНОЕ /// имя → `false` (не подтверждён heap → `ref` в запрещённой позиции /// реджектится: «ref нехраним, не имеет размера как тип»). fn ref_target_confirmed_heap(&self, inner: &TypeRef, gs: &GenericScope) -> bool { use TypeRef::*; match inner { Array(..) | Pointer(..) | Func { .. } | Protocol { .. } => true, Readonly(i, _) | Mut(i, _) | Uninit(i, _) | Ref(i, _) => { self.ref_target_confirmed_heap(i, gs) } // [M-fixed-array-value-semantics] (2026-07-10, D27-амендмент): `[N]T` — // inline value (стек/поле-по-месту), НЕ heap — рядом с Tuple/Unit, а не // с Array/Pointer выше. Не проверяет heap-ность T (как и Tuple(..) не // проверяет своих элементов) — контейнер сам inline независимо от T. Tuple(..) | Unit(..) | FixedArray(..) => false, Named { path, .. } => { let name = path.last().map(|s| s.as_str()).unwrap_or(""); if gs.contains_key(name) { return false; // generic-параметр — storage неизвестен } if name == "Vec" { return true; // slice/Vec — GC-handle } match self.types.get(name) { Some(td) => match &td.kind { crate::ast::TypeDeclKind::Record(_) => { td.allocation != crate::ast::AllocKind::Value } crate::ast::TypeDeclKind::Newtype(nt) => { self.ref_target_confirmed_heap(nt, gs) } crate::ast::TypeDeclKind::Alias(al) => { self.ref_target_confirmed_heap(al, gs) } // Sum / named-tuple / protocol / effect / typeset — // консервативно НЕ подтверждаем heap (value/ambiguous). _ => false, }, None => false, // неизвестное имя — не подтверждён heap } } } } /// **Plan 184 (Р1):** обход типа в ЛЕГАЛЬНОЙ top-level ref-позиции (возврат /// `-> ref Self`, локальный алиас `ro y ref T = …`). Ведущий `ref` здесь /// разрешён (любой цели); снимаем его и проверяем цель как обычный тип — /// так вложенный `ref` (внутри цели) всё равно ловится walk_typeref. fn walk_ref_return( &self, tr: &TypeRef, gs: &GenericScope, errors: &mut Vec<Diagnostic>, ) { match tr { TypeRef::Ref(inner, _) => self.walk_typeref(inner, gs, errors), _ => self.walk_typeref(tr, gs, errors), } } /// Ф.2: рекурсивная проверка арности одного TypeRef-дерева. fn walk_typeref( &self, tr: &TypeRef, gs: &GenericScope, errors: &mut Vec<Diagnostic>, ) { match tr { TypeRef::Named { path, generics, span } => { for g in generics { self.walk_typeref(g, gs, errors); } let Some(name) = path.last() else { return; }; // Plan 118 Ф.5 A22 (D216 §7): W_OPTION_DOUBLE_NESTED warning // для `Option[Option[*T]]` — V2 follow-on. Requires lint // framework integration (LintWarning через lints.rs), so что // diagnostic emits as warning not hard error. V1 detection // path documented здесь для future implementation: // if name == "Option" && generics.len() == 1 && // inner is Named["Option"] with pointer-typed payload // → emit W_OPTION_DOUBLE_NESTED via lint framework. // Currently не fires — nested Option[Option[*T]] codegen // falls back в tagged form automatically (inner c_ty = // NovaOpt_X struct, не pointer). // generic-параметр в scope — абстрактное имя, не тип. if gs.contains_key(name) { return; } // Plan 134: встроенный тип `ptr` (и его C-имя `nova_ptr`) удалён. // `*()` (pointer-to-unit → `void*`) — канонический opaque-pointer. // Ловим использование на этапе `nova check` (не откладываем до // codegen): даём понятную миграционную ошибку с подсказкой. // A-134.a / Plan 134 Ф.1.7. if name == "ptr" || name == "nova_ptr" { errors.push(Diagnostic::new( format!( "[E_TYPE_UNKNOWN] type `{name}` is removed — use `*()` \ (pointer-to-unit = `void*`) instead (Plan 134). \ For a short alias write `type ptr = *()` in your own code.", ), *span, )); return; } if arity_exempt(name) { return; } // Неизвестное имя — не наша забота (name-resolution). let Some(info) = self.arity.get(name) else { return; }; let actual = generics.len(); // `actual == 0` — type-аргументы опущены и выводятся из // контекста (`fn f() -> Result { Ok(1) }`, `let x Option`). // Это легальный idiom Nova — не arity-ошибка. Ошибка только // когда аргументы УКАЗАНЫ, но их число неверно. if actual > 0 && actual != info.count { errors.push(arity_diag(name, info, actual, *span)); } } TypeRef::Array(inner, _) | TypeRef::FixedArray(_, inner, _) => { self.walk_typeref(inner, gs, errors); } TypeRef::Tuple(items, _) => { for it in items { self.walk_typeref(it, gs, errors); } } TypeRef::Func { params, effects, return_type, .. } => { for p in params { self.walk_typeref(p, gs, errors); } for e in effects { self.walk_typeref(e, gs, errors); } if let Some(rt) = return_type { self.walk_typeref(rt, gs, errors); } } // Plan 97 Ф.2 (D142): анонимный protocol-тип — рекурсивно // walk через сигнатуры методов; arity-checking применяется к // ссылкам внутри param-/return-/effect-типов. TypeRef::Protocol { methods, .. } => { for m in methods { for p in &m.params { self.walk_typeref(&p.ty, gs, errors); } for e in &m.effects { self.walk_typeref(e, gs, errors); } if let Some(rt) = &m.return_type { self.walk_typeref(rt, gs, errors); } } } TypeRef::Unit(_) => {} // D176 (Plan 108): readonly T — transparent, walk inner. TypeRef::Readonly(inner, _) => self.walk_typeref(inner, gs, errors), // Plan 118 D216 + Plan 118.5: typed pointer `*T` family // (Pointer/Mut/Unsafe) — all transparent, walk inner for arity checks. TypeRef::Pointer(inner, _) | TypeRef::Mut(inner, _) | TypeRef::Uninit(inner, _) => self.walk_typeref(inner, gs, errors), // Plan 184 (Р1/Р6): `ref T` встреченный ЗДЕСЬ = ЗАПРЕЩЁННАЯ позиция // (поле / элемент коллекции / вариант суммы / Option / тип-аргумент // дженерика / параметр). Легальные top-level позиции (возврат, // локальный алиас, приёмник) снимают ведущий `ref` ДО вызова // walk_typeref (`walk_ref_return`/`let`-аннотация), поэтому сюда // ведущий-легальный ref не доходит. Р6: `ref H` (heap) ≡ H — // нормализуется, легально; `ref V` (value) / ref generic/unknown — // реджект E_REF_TYPE_POSITION. TypeRef::Ref(inner, span) => { if !self.ref_target_confirmed_heap(inner, gs) { errors.push(Diagnostic::new( format!( "[E_REF_TYPE_POSITION] `ref {}` недопустим в этой позиции \ (поле / элемент коллекции / вариант суммы / Option / \ тип-аргумент дженерика / параметр): ссылка на стек не \ должна утекать в долгоживущую кучу (Р1, Plan 184). `ref` \ легален только в возврате (`-> ref Self`), локальном \ алиасе (`ro y ref T = ...`) и типе приёмника (`@`). Для \ кучевого типа `H` действует `ref H ≡ H` (Р6) — пишите \ сам `H` без `ref`.", typeref_display(inner) ), *span, )); } self.walk_typeref(inner, gs, errors); } } } /// Plan 101.1 C8: collect all Named-type identifiers referenced /// anywhere в typeref recursively. Used для unused-prefix-generic /// detection (compare against fd.generics names). fn collect_named_idents(tr: &TypeRef, out: &mut HashSet<String>) { match tr { TypeRef::Named { path, generics, .. } => { if let Some(name) = path.last() { out.insert(name.clone()); } for g in generics { Self::collect_named_idents(g, out); } } TypeRef::Array(inner, _) => Self::collect_named_idents(inner, out), TypeRef::FixedArray(_, inner, _) => Self::collect_named_idents(inner, out), TypeRef::Tuple(items, _) => { for it in items { Self::collect_named_idents(it, out); } } TypeRef::Func { params, return_type, effects, .. } => { for p in params { Self::collect_named_idents(p, out); } if let Some(rt) = return_type { Self::collect_named_idents(rt, out); } // [M-exp-promotion-blockers: retry E_UNUSED_PREFIX_TYPEVAR] // a fn-typed param's OWN effect-clause (`body fn() Fail[E] -> T`) // is a legitimate usage site for a prefix-declared typevar // (effect-polymorphism over an arbitrary `Fail[E]`, e.g. a // generic retry/decorator wrapping any fallible callback) — // without this, `E` appearing ONLY inside a callback param's // effects was invisible to every caller of // `collect_named_idents` (was previously the case, D145). for e in effects { Self::collect_named_idents(e, out); } } TypeRef::Protocol { methods: _, .. } => {} TypeRef::Unit(_) => {} // D176 (Plan 108): readonly T — transparent. TypeRef::Readonly(inner, _) => Self::collect_named_idents(inner, out), // Plan 118 D216 + Plan 118.5: typed pointer `*T` family // (Pointer/Mut/Unsafe) — all transparent, recurse on inner. TypeRef::Pointer(inner, _) | TypeRef::Mut(inner, _) | TypeRef::Uninit(inner, _) | TypeRef::Ref(inner, _) => Self::collect_named_idents(inner, out), } } /// Plan 221.1 №88 (iv): collect every "slot leaf" of a receiver's /// structured type (`r.receiver_ty`) — a bare single-segment `Named` with /// EMPTY generics — found at NESTING DEPTH ≥ 2 (i.e. reached only by /// descending into at least one INNER container slot: `Vec[Vec[Wid]]`'s /// `Wid`, not `HashMap[str, V]`'s direct `str`). A container position /// (`Named` with NONEMPTY generics, e.g. the `Vec`/`OneBox` in /// `Vec[Vec[T]]` / `OneBox[Vec[T]]`) is never itself reported — only /// descended into. /// /// **Depth ≥ 2 only, deliberately — NOT "any depth"**: a DIRECT /// (depth-1) carrier slot already has an established, RELIED-UPON /// permissive meaning distinct from a nested one — verified regression /// while implementing this check: `std/src/encoding/serde/serde.nv` /// declares `fn HashMap[str, V Serialize] @serialize[...]` — a direct /// slot named `str` (a real primitive!) intentionally partial-specializes /// the KEY type while leaving `V` generic. The parser's OWN carrier-slot /// harvest already treats depth-1 and depth-2+ differently (flat bare-ident /// slots — `parse_generic_decl_params_inner`'s non-nested branch — harvest /// UNCONDITIONALLY regardless of name shape; a NESTED slot — `Ident[` /// branch — only harvests typevar-SHAPED names, Plan 153.5's /// `ident_is_typevar` gate). The map's own probe for this bug is /// consistently nested (`Vec[Vec[Wid]]`, depth 2) — this walk mirrors that /// exact scope. A depth-1 name colliding with a real type is a SEPARATE, /// pre-existing, out-of-scope permissiveness (noted, not touched here — /// see commit message / report). /// /// Mirrors `parser::collect_free_typevars`'s traversal shape, but WITHOUT /// its `ident_is_typevar` shape-gate: this walk exists specifically to /// catch names that gate WOULD reject-as-a-typevar (real type names, /// primitives) — see the E_RECV_GENERIC_SHADOWS_TYPE call site above. fn collect_receiver_carrier_slot_leaves(ty: &TypeRef, depth: usize, out: &mut Vec<(String, Span)>) { match ty { TypeRef::Named { path, generics, span } => { if path.len() == 1 && generics.is_empty() { if depth >= 2 { out.push((path[0].clone(), *span)); } } else { for g in generics { Self::collect_receiver_carrier_slot_leaves(g, depth + 1, out); } } } TypeRef::Array(inner, _) | TypeRef::FixedArray(_, inner, _) => { // D239 canonicalization (iii) already rewrites every `[]`-alias // found inside a carrier slot into `Named{Vec,...}` at parse // time, so `Array` should not normally survive into a carrier // `receiver_ty` — kept defensively, depth pass-through (`[]` // is transparent sugar, not itself a "container slot" level). Self::collect_receiver_carrier_slot_leaves(inner, depth, out); } TypeRef::Tuple(items, _) => { for it in items { Self::collect_receiver_carrier_slot_leaves(it, depth + 1, out); } } TypeRef::Readonly(inner, _) | TypeRef::Mut(inner, _) | TypeRef::Uninit(inner, _) | TypeRef::Pointer(inner, _) | TypeRef::Ref(inner, _) => Self::collect_receiver_carrier_slot_leaves(inner, depth, out), TypeRef::Func { .. } | TypeRef::Protocol { .. } | TypeRef::Unit(_) => {} } } /// Plan 221.1 №88 (iv): primitive scalar type names — same set used /// elsewhere for the "is this a bound-method unbound-receiver type name" /// heuristic (`f3_check_member_ctx`'s `is_type_name` check). A receiver /// carrier slot named `int`/`str`/… is ALSO a shadow (the doctrine bans /// primitives too, not just user types — `self.types` never carries /// primitives, so they need this separate check). fn is_primitive_scalar_type_name(name: &str) -> bool { matches!( name, "int" | "i8" | "i16" | "i32" | "i64" | "u8" | "u16" | "u32" | "u64" | "f32" | "f64" | "bool" | "char" | "str" ) } // --- Ф.2: walk тел (turbofish / as / is / let-аннотации) ------------ fn walk_block( &self, b: &Block, gs: &GenericScope, errors: &mut Vec<Diagnostic>, ) { for s in &b.stmts { self.walk_stmt(s, gs, errors); } if let Some(t) = &b.trailing { self.walk_expr(t, gs, errors); } } fn walk_stmt( &self, s: &Stmt, gs: &GenericScope, errors: &mut Vec<Diagnostic>, ) { match s { Stmt::Expr(e) => self.walk_expr(e, gs, errors), Stmt::Let(d) => { if let Some(t) = &d.ty { // Plan 184 (Р1): локальная аннотация — легальная top-level // ref-позиция (`ro y ref T = …` / `mut y ref T = …`). self.walk_ref_return(t, gs, errors); } self.walk_expr(&d.value, gs, errors); } // Plan 114.4 Ф.2: scope-local const — strict constexpr enforce. // Same eligibility rule as module-level const (check_const_constexpr). // known_consts здесь conservatively empty (referencing другие // scope-locals — followup [M-114.4-scope-const-chain]). Stmt::Const(d) => { if let Some(t) = &d.ty { self.walk_typeref(t, gs, errors); } self.walk_expr(&d.value, gs, errors); // Plan 114.4.2 D199: внутри const fn body — scope-local // const validated by check_const_fn_decl (param/local // awareness). Здесь skip избегаем false-positives на // const param refs (e.g. `const c = b as int` где // `b` — const param). if !self.in_const_fn.get() { let empty_consts: HashSet<String> = HashSet::new(); let empty_nt: HashSet<String> = HashSet::new(); if let Err(diag) = check_const_constexpr_ex( &d.value, &empty_consts, &self.const_fn_names, &empty_nt, ) { errors.push(diag); } } } Stmt::Assign { target, value, .. } => { self.walk_expr(target, gs, errors); self.walk_expr(value, gs, errors); } Stmt::Return { value, .. } => { if let Some(v) = value { self.walk_expr(v, gs, errors); } } Stmt::Throw { value, .. } => self.walk_expr(value, gs, errors), Stmt::Break(_) | Stmt::Continue(_) | Stmt::Reveal { .. } => {} Stmt::Defer { body, .. } => { self.walk_expr(body, gs, errors); } // Plan 110 D188: walk init + body. Stmt::ConsumeScope { type_annot, init, body, .. } => { if let Some(t) = type_annot { self.walk_typeref(t, gs, errors); } self.walk_expr(init, gs, errors); for stmt in &body.stmts { self.walk_stmt(stmt, gs, errors); } if let Some(t) = &body.trailing { self.walk_expr(t, gs, errors); } } Stmt::AssertStatic { expr, .. } | Stmt::Assume { expr, .. } => { self.walk_expr(expr, gs, errors); } Stmt::Apply { args, .. } => { for a in args { self.walk_expr(a, gs, errors); } } Stmt::Calc { steps, .. } => { for step in steps { self.walk_expr(&step.expr, gs, errors); } } // Plan 136: tuple destructuring assignment. Stmt::TupleAssign { lhs, rhs, .. } => { for e in lhs { self.walk_expr(e, gs, errors); } for e in rhs { self.walk_expr(e, gs, errors); } } } } fn walk_expr( &self, e: &Expr, gs: &GenericScope, errors: &mut Vec<Diagnostic>, ) { match &e.kind { ExprKind::TurboFish { base, type_args } => { for t in type_args { self.walk_typeref(t, gs, errors); } // Если turbofish целится в известный тип — проверить // арность самого turbofish'а (`HashMap[str].new()`). // Generic-функции (`parse[int]`) в `arity` не попадают — // их арность с D88-дефолтами проверяется отдельно (не Ф.2). let target: Option<&String> = match &base.kind { ExprKind::Ident(n) => Some(n), ExprKind::Path(parts) => parts.last(), _ => None, }; if let Some(name) = target { if !gs.contains_key(name) && !arity_exempt(name) { if let Some(info) = self.arity.get(name) { // turbofish всегда указывает аргументы явно — // пустой `[]` не парсится; проверяем как есть. if !type_args.is_empty() && type_args.len() != info.count { errors.push(arity_diag( name, info, type_args.len(), e.span, )); } } } } self.walk_expr(base, gs, errors); } ExprKind::As(inner, ty) | ExprKind::Is(inner, ty) => { self.walk_expr(inner, gs, errors); self.walk_typeref(ty, gs, errors); } ExprKind::Call { func, args, trailing } => { self.walk_expr(func, gs, errors); for a in args { self.walk_expr(a.expr(), gs, errors); } if let Some(t) = trailing { match t { Trailing::Block(b) => self.walk_block(b, gs, errors), Trailing::LegacyBlockWithParams(tb) => { self.walk_block(&tb.body, gs, errors) } Trailing::Fn(sb) => self.walk_fn_sig_body(sb, gs, errors), } } } ExprKind::Binary { left, right, .. } => { self.walk_expr(left, gs, errors); self.walk_expr(right, gs, errors); } ExprKind::Unary { operand, .. } => self.walk_expr(operand, gs, errors), ExprKind::Try(inner) | ExprKind::Bang(inner) | ExprKind::RefArg(inner) => { self.walk_expr(inner, gs, errors) } ExprKind::Coalesce(a, b) => { self.walk_expr(a, gs, errors); self.walk_expr(b, gs, errors); } ExprKind::Member { obj, .. } => self.walk_expr(obj, gs, errors), ExprKind::Index { obj, index } => { self.walk_expr(obj, gs, errors); self.walk_expr(index, gs, errors); } ExprKind::If { cond, then, else_ } => { self.walk_expr(cond, gs, errors); self.walk_block(then, gs, errors); if let Some(eb) = else_ { self.walk_else(eb, gs, errors); } } ExprKind::IfLet { scrutinee, then, else_, .. } => { self.walk_expr(scrutinee, gs, errors); self.walk_block(then, gs, errors); if let Some(eb) = else_ { self.walk_else(eb, gs, errors); } } ExprKind::Match { scrutinee, arms } => { self.walk_expr(scrutinee, gs, errors); for arm in arms { if let Some(g) = &arm.guard { self.walk_expr(g, gs, errors); } match &arm.body { MatchArmBody::Expr(e) => self.walk_expr(e, gs, errors), MatchArmBody::Block(b) => self.walk_block(b, gs, errors), } } } ExprKind::Block(b) => self.walk_block(b, gs, errors), ExprKind::ArrayLit(elems) => { for el in elems { match el { ArrayElem::Item(e) | ArrayElem::Spread(e) => { self.walk_expr(e, gs, errors) } } } } ExprKind::MapLit { elems, .. } => { for (k, v) in crate::ast::MapElem::cloned_pairs(elems).iter() { self.walk_expr(k, gs, errors); self.walk_expr(v, gs, errors); } } ExprKind::TupleLit(elems) => { for e in elems { self.walk_expr(e, gs, errors); } } ExprKind::RecordLit { type_name, fields, .. } => { // Plan 173 Ф.5 (§4а zero-tolerance, вскрыто D192-ретрактом): // record literal с НЕИЗВЕСТНЫМ именем типа раньше молча // проходил чекер («name-resolution не наша забота») и codegen // генерил мусор — тихий miscompile-класс. Ловим здесь: // имя не в types-реестре CU (user+prelude+builtin merged), // не generic-параметр в scope, не вариант известной суммы → // [E_UNKNOWN_TYPE]. if let Some(tn) = type_name { if let Some(last) = tn.last() { if last != "Self" && !gs.contains_key(last) && !self.types.contains_key(last) // [M-compress-checksum-structvariant-ctor-xmodule]: // `self.types` — HashMap keyed по имени суммы; при // co-presence НЕСКОЛЬКИХ одноимённых sum-типов из // разных модулей (например `ErrorKind` http/io/ // compress) `types.insert` перезаписывает — выживает // только ПОСЛЕДНИЙ, варианты остальных пропадают из // `types.values()`. `sum_variant_names` собран // ЛОССЛЕСС (прямой Vec-обход `module.items`) — не // подвержен этой коллизии. && !self.sum_variant_names.contains(last) { errors.push(Diagnostic::new( format!( "[E_UNKNOWN_TYPE] unknown type `{last}` in record \ literal `{last} {{ … }}` — тип не объявлен и не \ импортирован (или удалён: см. D192-ретракт для \ `CleanupTimeoutError`).", ), e.span, )); } } } if let Some(tn) = type_name { if let Some(last) = tn.last() { if let Some(td) = self.types.get(last) { for f in fields { if f.is_spread { continue; } if td.assoc_consts.iter().any(|ac| ac.name == f.name) { errors.push(Diagnostic::new( format!( "[E_CONST_FIELD_IN_LITERAL] field `{}` \ в record literal `{}{{ … }}` — это \ associated constant (zero-storage, \ namespace access `{}.{}`); НЕ указывается \ и НЕ инициализируется в record literal \ (Plan 114.4.1 D200).", f.name, last, last, f.name, ), f.span, )); } } // Plan 124 (D220/D221) + 124.6 (D225): priv field INIT // check — record literal outside type-method scope cannot // init priv fields (unless #test_access). if let TypeDeclKind::Record(rec_fields) = &td.kind { let base_allowed = self.priv_access_allowed_base(last.as_str()); if !base_allowed { let has_priv = rec_fields.iter().any(|fd| fd.priv_field); // Plan 160 (D281) Ф.2: module access context. let module_allowed = self.module_priv_access_allowed(last.as_str(), e.span); let has_type_priv = rec_fields.iter().any(|fd| fd.priv_field && !fd.priv_module_field); for f in fields { // Plan 124.2 (D221 §5): spread `...other` outside // type-method scope on a type WITH priv fields // — implicitly initializes priv via copy → emit // E_PRIV_FIELD_INIT_SPREAD. if f.is_spread { if has_priv { // Plan 160 Ф.2: spread allowed within same // module for module-priv-only types. let has_module_priv_only = has_priv && !has_type_priv; let spread_ok = has_module_priv_only && module_allowed; if !spread_ok { errors.push(Diagnostic::new( format!( "[E_PRIV_FIELD_INIT_SPREAD] cannot use \ spread `...` in record literal of `{}` \ outside type-method scope: type has \ private fields which would be \ implicitly initialized via copy \ (Plan 124 / D221 §5). Hint: use \ factory method `{}.new(...)` or list \ each public field explicitly.", last, last, ), f.span, )); } } continue; } if let Some(fdecl) = rec_fields.iter().find(|fd| fd.name == f.name) { if fdecl.priv_field && !self.priv_field_access_allowed(last.as_str(), &fdecl.visible_to) { // Plan 160 (D281) Ф.2: module-private init. if fdecl.priv_module_field { if !self.module_priv_access_allowed(last.as_str(), f.span) { errors.push(Diagnostic::new( format!( "[E_FIELD_MODULE_PRIVATE] cannot \ initialize module-private field \ `{}.{}` via record literal from \ outside its module. Type declared \ with bare `priv` (Plan 160 / \ D281). Hint: use factory method \ `{}.new(...)`.", last, f.name, last, ), f.span, )); } } else { errors.push(Diagnostic::new( format!( "[E_PRIV_FIELD_INIT] cannot \ initialize private field `{}.{}` \ via record literal outside type-\ method scope. Field marked `priv` \ (Plan 124 / D220). Hint: use \ factory method like `{}.new(...)`, \ or use `#test_access({})` on test fn.", last, f.name, last, last, ), f.span, )); } } } } } } } } } for f in fields { if let Some(v) = &f.value { self.walk_expr(v, gs, errors); } } } ExprKind::TaggedTemplate { tag, args, .. } => { self.walk_expr(tag, gs, errors); for a in args { self.walk_expr(a, gs, errors); } } ExprKind::InterpolatedStr { parts } => { for p in parts { if let InterpStrPart::Expr { expr: e, spec: _ } = p { self.walk_expr(e, gs, errors); } } } ExprKind::Lambda { body, .. } => self.walk_expr(body, gs, errors), ExprKind::ClosureLight { body, .. } => match body { ClosureBody::Expr(e) => self.walk_expr(e, gs, errors), ClosureBody::Block(b) => self.walk_block(b, gs, errors), }, ExprKind::ClosureFull(sb) => self.walk_fn_sig_body(sb, gs, errors), ExprKind::Spawn(body) => self.walk_expr(body, gs, errors), ExprKind::Detach(body) | ExprKind::Blocking(body) => self.walk_block(body, gs, errors), ExprKind::Supervised { body, cancel, deadline, on_timeout } => { if let Some(c) = cancel { self.walk_expr(c, gs, errors); } if let Some(_dl) = deadline { let _dl_e = &_dl.expr; self.walk_expr(_dl_e, gs, errors); } if let Some(oh) = on_timeout { self.walk_expr(oh, gs, errors); } self.walk_block(body, gs, errors); } ExprKind::Forbid { body, .. } => self.walk_block(body, gs, errors), ExprKind::Realtime { body, .. } => self.walk_block(body, gs, errors), ExprKind::ParallelFor { iter, body, .. } => { self.walk_expr(iter, gs, errors); self.walk_block(body, gs, errors); } ExprKind::For { iter, body, .. } => { self.walk_expr(iter, gs, errors); self.walk_block(body, gs, errors); } ExprKind::While { cond, body, .. } => { self.walk_expr(cond, gs, errors); self.walk_block(body, gs, errors); } ExprKind::WhileLet { scrutinee, body, .. } => { self.walk_expr(scrutinee, gs, errors); self.walk_block(body, gs, errors); } ExprKind::Loop { body, .. } => self.walk_block(body, gs, errors), ExprKind::Select { arms } => { for arm in arms { match &arm.op { SelectOp::Recv { chan, .. } => { self.walk_expr(chan, gs, errors) } SelectOp::Send { chan, value } => { self.walk_expr(chan, gs, errors); self.walk_expr(value, gs, errors); } SelectOp::Default => {} } if let Some(g) = &arm.guard { self.walk_expr(g, gs, errors); } self.walk_block(&arm.body, gs, errors); } } ExprKind::Range { start, end, .. } => { if let Some(s) = start { self.walk_expr(s, gs, errors); } if let Some(e) = end { self.walk_expr(e, gs, errors); } } ExprKind::Throw(inner) => self.walk_expr(inner, gs, errors), ExprKind::Interrupt(opt) => { if let Some(e) = opt { self.walk_expr(e, gs, errors); } } // [E_COALESCE_RETURN_FALLBACK]: `X ?? return R` walked defensively // here (generic-param usage scan runs regardless of the dedicated // `check_coalesce_return_fallback` rejection elsewhere). ExprKind::CoalesceReturnFallback(opt) => { if let Some(e) = opt { self.walk_expr(e, gs, errors); } } ExprKind::With { body, .. } => self.walk_block(body, gs, errors), ExprKind::Forall { range, body, .. } | ExprKind::Exists { range, body, .. } => { self.walk_expr(range, gs, errors); self.walk_expr(body, gs, errors); } // Plan 97 Ф.4 (D142): protocol-литерал — walk идентичен. ExprKind::HandlerLit { methods, .. } | ExprKind::ProtocolLit { methods, .. } => { for m in methods { match &m.body { HandlerMethodBody::Expr(e) => self.walk_expr(e, gs, errors), HandlerMethodBody::Block(b) => self.walk_block(b, gs, errors), } } } ExprKind::IntLit(_) | ExprKind::FloatLit(_) | ExprKind::BoolLit(_) | ExprKind::StrLit(_) | ExprKind::CharLit(_) | ExprKind::UnitLit | ExprKind::HexBlobLit(_) | ExprKind::NullPtrLit | ExprKind::Ident(_) | ExprKind::Path(_) | ExprKind::SelfAccess => {} } } fn walk_else( &self, eb: &ElseBranch, gs: &GenericScope, errors: &mut Vec<Diagnostic>, ) { match eb { ElseBranch::Block(b) => self.walk_block(b, gs, errors), ElseBranch::If(e) => self.walk_expr(e, gs, errors), } } fn walk_fn_sig_body( &self, sb: &FnSigBody, gs: &GenericScope, errors: &mut Vec<Diagnostic>, ) { for p in &sb.params { self.walk_typeref(&p.ty, gs, errors); } for e in &sb.effects { self.walk_typeref(e, gs, errors); } if let Some(rt) = &sb.return_type { self.walk_typeref(rt, gs, errors); } match &sb.body { FnBody::Expr(e) => self.walk_expr(e, gs, errors), FnBody::Block(b) => self.walk_block(b, gs, errors), FnBody::External => {} } } // ================================================================ // Ф.1 — assignability: arg↔param и annotation↔RHS. // // Scope-aware проход: трекает типы локальных переменных, на каждом // call-site и `let`-аннотации сверяет совместимость. Числовые // литералы полиморфны по контексту (D44: `let x u8 = 200` валиден), // поэтому проверка literal-aware. Несовместимость → E7301. // // Резолвятся только однозначные callee (free fn / static-метод, // ровно один overload): instance-методы требуют receiver-type // inference, ненадёжной в bootstrap — их резолвит codegen. // ================================================================ fn f1_check_fn(&self, fd: &FnDecl, errors: &mut Vec<Diagnostic>) { if std::env::var_os("NOVA_F1_TRACE").is_some() { eprintln!("[F1] {}.{} fid={:?}", fd.receiver.as_ref().map(|r| r.type_name.as_str()).unwrap_or("-"), fd.name, fd.span.file_id); } // Plan 124 (D220): set current_recv_type для priv field access scope // tracking. Instance + Static methods обa get receiver's type_name. let prev_recv = self.current_recv_type.borrow().clone(); let new_recv = fd.receiver.as_ref().map(|r| r.type_name.clone()); *self.current_recv_type.borrow_mut() = new_recv; let prev_recv_mut = self.current_recv_is_mut.get(); // №462/№370: `consume` ТОЖЕ может мутировать — владеющий получатель // имеет не меньше прав, чем `mut`. `mutable` и `consume` // взаимоисключающие (parser enforce'ит), поэтому у `consume` [INV-TODO: №462] // `mutable == false`, и одиночное чтение считало владельца за `ro`. // Закрыто здесь ПОСЛЕ того, как тот же промах в `ConsumeCtx` уже // покраснел на законном коде nova-tls: №370 был закрыт наполовину. self.current_recv_is_mut.set(fd.receiver.as_ref().map_or(false, |r| r.mutable || r.consume)); let _recv_guard = PrivRecvGuard { ctx: self, prev: prev_recv, prev_mut: prev_recv_mut }; // Plan 174.2 Ф.B: publish the enclosing fn's return type so the // `ExprKind::Try` arm can diagnose carrier-mismatched `?`. Restored on // exit via `FnReturnTyGuard` (RAII — covers early returns). let prev_ret_ty = self.current_fn_return_ty.borrow_mut().take(); *self.current_fn_return_ty.borrow_mut() = fd.return_type.clone(); let _ret_ty_guard = FnReturnTyGuard { ctx: self, prev: prev_ret_ty }; // Plan 214.1 (D429 amend, R13'): publish `fd`'s OWN span while its // body is being checked, so a GENERIC `#coerce` pattern's own // declaration is excluded from matching against itself (anti-self- // recursion) — a no-op unless `fd.coerce_attr` (cheap either way). let prev_coerce_span = self.current_coerce_decl_span.borrow_mut().take(); if fd.coerce_attr { *self.current_coerce_decl_span.borrow_mut() = Some(fd.span); } let _coerce_self_guard = CoerceSelfGuard { ctx: self, prev: prev_coerce_span }; // generic-match-scope-gap fix: publish the enclosing fn's OWN generic // params (name + bounds) — see `current_fn_generics` field doc. // Restored on exit via `FnGenericsGuard` (RAII — covers early returns). let prev_fn_generics = std::mem::take(&mut *self.current_fn_generics.borrow_mut()); *self.current_fn_generics.borrow_mut() = fd.generics.clone(); let _fn_generics_guard = FnGenericsGuard { ctx: self, prev: prev_fn_generics }; // Plan 124.6 (D225): set current_fn_test_access — fn body gets priv // access к listed types. let prev_ta = std::mem::take(&mut *self.current_fn_test_access.borrow_mut()); *self.current_fn_test_access.borrow_mut() = fd.test_access_for.clone(); let _ta_guard = PrivTestAccessGuard { ctx: self, prev: prev_ta }; let gs = fn_generic_scope(fd); // 172.1.2 Binary-bounds: параметры с numeric type-set bound'ом. let prev_nb = std::mem::take(&mut *self.numeric_bounded_params.borrow_mut()); let _nb_guard = NumericBoundGuard { ctx: self, prev: prev_nb }; { let mut nb = self.numeric_bounded_params.borrow_mut(); for g in &fd.generics { let numeric = g.bounds.iter().any(|b| { let TypeRef::Named { path, generics: bg, .. } = b else { return false }; if !bg.is_empty() || path.len() != 1 { return false; } self.types.get(&path[0]).map_or(false, |td| { if let TypeDeclKind::TypeSet(members) = &td.kind { !members.is_empty() && members.iter().all(|m| { matches!(m, TypeRef::Named { path: mp, generics: mg, .. } if mg.is_empty() && mp.len() == 1 && ResolvedType::scalar_from_int_name(&mp[0]).is_some()) }) } else { false } }) }); if numeric { nb.insert(g.name.clone()); } } } let mut scope: HashMap<String, TypeRef> = HashMap::new(); for p in &fd.params { scope.insert(p.name.clone(), p.ty.clone()); } // D175 (Plan 108): inject receiver type as "@" in scope so that // `check_target_readonly` can resolve @.field type for self-assignments. // // [M-slice-ext-receiver-for-in-elem-type] (2026-07-18): prefer the FULL // structured receiver type (`receiver_ty` — `Array(Named(T))` for a // slice-extension `fn []T @m`, mirrors the SAME "prefer receiver_ty, // else flat Named{type_name,generics}" idiom already used by // `resolve_return_channel`'s `recv_pattern_tr`, ~10343) over the flat // `Named{path:[type_name]}` fallback. The flat form, for a slice // receiver, spells `type_name` as the LITERAL flattened string // `"[]T"` (parser `first_ident = "[]" + elem_name`, parser/mod.rs // ~3006) — a `Named` whose path is a synthetic slice-sugar spelling, // not a real registered type. Method-resolution consumers of // scope["@"] already special-case-detect that `"[]"`-prefixed Named // (`path[0].starts_with("[]")` → normalize to "Vec", ~15537/~16350) // so they were unaffected either way — but `infer_iter_elem_type`'s // structural match (`TypeRef::Array(inner,_) => elem`, ~10068) only // recognizes the real `Array` shape, NOT the flattened Named spelling. // `for r in @` inside a slice-extension therefore fell through to // `None` (no elem type), and codegen's C emission guessed `nova_int` // for the loop variable (`r.field` → "member reference base type // 'nova_int'", match-tag resolution picked an unrelated sum type's // tags). `receiver_ty` already carries the correct structured shape // (`Array(Named(T))`, depth-aware for `[][]T`) — using it here closes // the gap at the channel source instead of teaching codegen a second // slice-sugar special case. if let Some(recv) = &fd.receiver { if matches!(recv.kind, ReceiverKind::Instance) { let self_ty = recv.receiver_ty.clone().unwrap_or_else(|| TypeRef::Named { path: vec![recv.type_name.clone()], generics: recv.generics.clone(), span: recv.span, }); scope.insert("@".to_string(), self_ty); } } // Plan 147 Ф.7 [M-147-param-index-freeze]: non-mut params are ro by // default (D176 §«Параметры функций»). Register them in ro_binding_names // so that check_target_readonly's is_through_ro_binding covers index // writes `arr[i] = x` on ro params, mirroring the Member-field check. // // Guard: only apply to functions defined in entry-module files (i.e. // the user's own code). Imported functions from std/prelude have already // been checked when their source was type-checked; re-enforcing here // would fire false positives on std library bodies that intentionally // write through array params. // // When the module has peer files (multi-file module), `entry_file_ids` // contains the FileIds of entry-module peers. When testing a single file // (no peer files), `entry_file_ids` is empty; in that case we use // file_id == 0 (the initial parse file) as the entry-module heuristic — // inline-merged imports are parsed as separate source strings with // file_id > 0, so this correctly skips them. // // Note: `ro b []int` (prefix synonym, valid per D176) and `b ro []int` // (type-position ro) produce the same AST (Readonly([]int), is_mut=false) // so no redundancy check is added here — the prefix synonym form is // explicitly documented as valid. The parser already catches `ro x ro T` // and `mut x mut T` for let-bindings (lines 5198-5215 in parser/mod.rs). let is_entry_fn = if self.entry_file_ids.is_empty() { fd.span.file_id == 0 } else { self.entry_file_ids.contains(&fd.span.file_id) }; let ro_snap_fn: std::collections::HashSet<String> = self.ro_binding_names.borrow().clone(); if is_entry_fn { for p in &fd.params { // Add non-mut, non-consume params to ro_binding_names so // is_through_ro_binding fires on index writes `arr[i] = x` (P7 // freeze, D246). `mut` params and `consume` params may mutate. // D246-амендмент ([M-ro-launder-via-mut-binding], Ф.1б, // 2026-07-23): EXCLUDE variadic params (`...args []T`) — each // call site's vararg-collected array is FRESHLY materialized // for that call, with no external alias to launder against // (unlike a normal named param, which aliases whatever the // caller passed in). False-positive found via `Vec[T].of` // (`=> args` — variadic passthrough as `Self`). if !p.is_mut && !p.consume && !p.is_variadic { self.ro_binding_names.borrow_mut().insert(p.name.clone()); } } } // №309/№317 (окно p-ovl-channel): `consume_binding_names` fn-entry // seed — NOT gated to `is_entry_fn` (unlike `ro_binding_names` above, // which guards against false-positive WRITE-through-ro diagnostics on // std bodies). This tracker feeds ONLY the mode-axis overload // tiebreak (a dispatch DECISION, never a diagnostic), so it is safe // — and necessary for correctness — to seed it for every fn, std // included: a `consume`-mode overload call inside a std body must // resolve exactly like one in user code. let consume_snap_fn: std::collections::HashSet<String> = self.consume_binding_names.borrow().clone(); for p in &fd.params { if p.consume { self.consume_binding_names.borrow_mut().insert(p.name.clone()); } } // Plan 184 (заход-5, п.7): `mut ref`-параметров больше нет (ref сняты с // сигнатур заходом-1), поэтому набор пуст. Оставляем snapshot/clear для // сохранения формы (mut_ref_param_names ещё читается capture-баном ниже // — теперь всегда пустой; переиспользование под ref-локалы — follow-up). let mut_ref_snap_fn: std::collections::HashSet<String> = self.mut_ref_param_names.borrow().clone(); { let mut s = self.mut_ref_param_names.borrow_mut(); s.clear(); } match &fd.body { FnBody::Expr(e) => { self.f1_expr(e, &gs, &mut scope, errors); self.f4_check_value(e, &scope, errors); // Plan 172.1 [literal-coercion channel] (§0/§1): the arrow-body `=> <expr>` // value coerces to the DECLARED return type — DEFINITE site. Fixes the // tuple-return gap (`=> (0x80, 5) -> (uint, uint)` built `NovaTuple..int..` // ≠ the declared `..uint..` → CC-FAIL) and subsumes the Some/Ok return // coercions (the tactical codegen target-passing). No `assignable` runs here // (return-type compat is checked elsewhere) — pure channel materialization. if let Some(ret) = &fd.return_type { // [M-closure-trailing-scalar-coercion-no-typecheck] fix: reject a // closure literal against a scalar return type BEFORE the literal // coercion (which only widens INT literals — it does not reject). self.check_closure_scalar_return(e, ret, errors); // D246-амендмент ([M-ro-launder-via-mut-binding], Ф.1б, // 2026-07-23): 3rd position of the norm — RETURN. self.check_ro_launder_return(e, ret, &scope, errors); self.materialize_literal_coercion(e, ret); // a block-arrow body `=> { …; return X }` can hold explicit returns. self.materialize_returns_in_expr(e, ret); self.check_closure_scalar_return_in_expr(e, ret, errors); self.check_ro_launder_return_in_expr(e, ret, &scope, errors); } } FnBody::Block(b) => { self.f1_block(b, &gs, &mut scope, errors); if let Some(ret) = &fd.return_type { // implicit tail return (block trailing) coerces to the return type … if let Some(trailing) = &b.trailing { // [M-closure-trailing-scalar-coercion-no-typecheck] fix (see the // dedicated block above `check_closure_scalar_return`'s definition). self.check_closure_scalar_return(trailing, ret, errors); // D246-амендмент ([M-ro-launder-via-mut-binding], Ф.1б). self.check_ro_launder_return(trailing, ret, &scope, errors); self.materialize_literal_coercion(trailing, ret); } // … and every explicit `return <expr>` anywhere in the body. self.materialize_returns_in_block(b, ret); self.check_closure_scalar_return_in_block(b, ret, errors); self.check_ro_launder_return_in_block(b, ret, &scope, errors); } else { // [реестр 221.1 №493, K1] no annotation at all — D45's other // half (see `check_missing_return_annotation`'s own doc): // a non-unit trailing here is a forgotten `-> T`, not a // legitimate `()`. self.check_missing_return_annotation(fd, b, &scope, errors); } } FnBody::External => {} } // Restore ro_binding_names to the state before this fn's params were // added — function scopes are independent; param names from one fn // must not bleed into the next fn's checks. *self.ro_binding_names.borrow_mut() = ro_snap_fn; // №309/№317: restore consume_binding_names symmetrically. *self.consume_binding_names.borrow_mut() = consume_snap_fn; // Plan 172.5 (D326 R10): restore the enclosing fn's mut-ref-param set. *self.mut_ref_param_names.borrow_mut() = mut_ref_snap_fn; } fn f1_block( &self, b: &Block, gs: &GenericScope, scope: &mut HashMap<String, TypeRef>, errors: &mut Vec<Diagnostic>, ) { // Snapshot let-имён этого блока — восстановить scope на выходе // (block-out shadowing, как BoundCtx::walk_block). let mut snapshot: Vec<(String, Option<TypeRef>)> = Vec::new(); for s in &b.stmts { if let Stmt::Let(d) = s { if let Some(name) = pattern_simple_name(&d.pattern) { snapshot.push((name.clone(), scope.get(&name).cloned())); } } } // Plan 124.8 [M-124.8-ro-binding-scope] fix (2026-06-03): // snapshot `ro_binding_names` at block entry, restore at exit. // Without restore, `ro x = ...` inside ANY block leaks `x` into the // global ctx state — survives even cross-module type-check passes // (e.g., stdlib sha1.nv `ro v = ...` polluted user fixtures that // later used `mut v = ...`). Each block now properly cleans up the // entries it added. Outer scopes are preserved (transitivity D175). let ro_snapshot: std::collections::HashSet<String> = self.ro_binding_names.borrow().clone(); // №309/№317: same block-scope snapshot/restore for consume_binding_names. let consume_snapshot: std::collections::HashSet<String> = self.consume_binding_names.borrow().clone(); // Plan 221.1 №286 residual gap (window p286): seed first-send T- // inference hints for bare `Channel.new` bindings in THIS block // BEFORE walking its statements — see `seed_channel_bare_send_hints`. self.seed_channel_bare_send_hints(b, scope); for s in &b.stmts { self.f1_stmt(s, gs, scope, errors); } if let Some(t) = &b.trailing { self.f1_expr(t, gs, scope, errors); self.f4_check_value(t, scope, errors); } for (n, prev) in snapshot { match prev { Some(t) => { scope.insert(n, t); } None => { scope.remove(&n); } } } *self.ro_binding_names.borrow_mut() = ro_snapshot; *self.consume_binding_names.borrow_mut() = consume_snapshot; } /// [реестр 221.1 №493] Reconstruct the `TypeRef` scope a block's OWN /// top-level `let`/`ro`/`mut` bindings contribute, layered onto the /// (already-restored) outer `scope` passed in. `f1_block` above /// snapshots exactly these entries at block-entry and restores them at /// block-exit (block-out shadowing, ~L8003-8012/8036-8041) — by the /// time `f1_check_fn`'s `FnBody::Block` arm can react to a missing /// `-> T` (right after `f1_block` returns), the block's own locals are /// gone from `scope` again. This is a pure, side-effect-free /// re-derivation (no diagnostics, no `ro_binding_names`/channel /// writes) — it exists ONLY so `infer_expr_type` can see the same /// names `f1_block` saw while it ran, when re-typing the trailing /// expression for `check_missing_return_annotation`. fn scope_with_block_lets( &self, b: &Block, scope: &HashMap<String, TypeRef>, ) -> HashMap<String, TypeRef> { let mut s = scope.clone(); for stmt in &b.stmts { if let Stmt::Let(d) = stmt { if let Some(name) = pattern_simple_name(&d.pattern) { match d.ty.clone().or_else(|| self.infer_expr_type(&d.value, &s)) { Some(t) => { s.insert(name, t); } None => { s.remove(&name); } } } } } s } /// [реестр 221.1 №493, K1] D45 (`03-syntax.md`): "block-body — `-> T` /// обязателен, если тип не unit". Before this fix that half of D45 was /// NOT enforced anywhere: a block-body function with no `-> T` at all /// compiled cleanly regardless of what its trailing expression /// produced — codegen's `return_type_c` (emit_c.rs) treats ANY /// annotation-less block-body as `nova_unit`, so the actual trailing /// value is silently discarded (never returned to the caller) with /// zero diagnostic at any stage — the caller either gets a confusing /// raw C compile error (if it tries to use the "returned" value) or, /// if it ignores the return value (legal for any call), nothing at /// all indicates the value was lost. Fixes docs/plans/221.1-bug- /// sweep.md №493. /// /// Deliberately conservative: fires ONLY when `infer_expr_type` (the /// same best-effort inferer the ro-launder/literal-coercion channels /// above already rely on) can CONFIDENTLY resolve the trailing /// expression's type AND that type is not `Unit`. `None` (type not /// inferable by this best-effort pass) or an already-`Unit` trailing /// stay silent on purpose — a false positive here (flagging a /// legitimate void procedure) is strictly worse than under-catching: /// it would force `-> ()` noise across the tree, which D45 explicitly /// rejects ("`-> ()` опускается всегда", D20). A block with NO /// trailing expression at all (`b.trailing.is_none()` — the body's /// last construct is a genuine statement: `let`/`return`/loop/ /// assignment) is unconditionally unit and never reaches this check. fn check_missing_return_annotation( &self, fd: &FnDecl, b: &Block, scope: &HashMap<String, TypeRef>, errors: &mut Vec<Diagnostic>, ) { let Some(trailing) = &b.trailing else { return }; // [реестр 221.1 №493] carrier scan finding (std/src/time/civil/ // tz_test.nv `push_u32`, std/src/unicode/collate.nv // `push_implicit`): a trailing call to a `mut`-receiver FLUENT // mutator (`b.push(x)` — `[]T`/`WriteBuffer`/`StringBuilder` // core mutators, `push`/`append`/`insert`/… — chain_norm.rs's // `FLUENT_BUILTIN_METHODS`, always reference-type buffers that // mutate in place) is NOT a forgotten annotation: the call's // effect already landed through the mutated (aliased) receiver // — the `@`-fluent return is a redundant handle to the SAME // object, so discarding it (no `-> T`) loses nothing, unlike a // genuinely fresh value a pure computation hands back only // through its return. Contrast with value-type `with_*` // builders (D-with-star: "with_* всегда новое значение") — those // do NOT mutate their receiver, so dropping their return WOULD // be the real K1 bug; this exemption is syntactically scoped to // the known-mutating builtin list only, not to fluent calls in // general. if trailing_is_fluent_mutator_call(trailing) { return; } let infer_scope = self.scope_with_block_lets(b, scope); let Some(mut ty) = self.infer_expr_type(trailing, &infer_scope) else { return }; if matches!(ty, TypeRef::Unit(_)) { return; } // [реестр 221.1 №493] "every branch ends in an explicit `return`" // (task's own required boundary case): `infer_expr_type`'s If/Match // divergence handling (~L19801 `then_div && else_div`) truthfully // reports the TRAILING POSITION's type as `never` here — no branch // ever falls through to hand the if/match a value. `never` is a // useless `-> T` suggestion (and arguably not even what D45 means // by "unit" vs "not unit" — the fn DOES return a value, just always // via an explicit `return`). Recover the real payload type from the // `return <expr>` statements themselves; if they don't all agree on // one concrete type, stay silent rather than suggest something // possibly wrong (same conservative bias as the rest of this check). if type_ref_is_never(&ty) { match self.resolve_never_trailing_return_type(trailing, &infer_scope) { Some(t) => ty = t, None => return, } } let ty_str = render_type_ref(&ty); errors.push(Diagnostic::new( format!( "[E_MISSING_RETURN_TYPE] function `{name}` has a block-body (`{{ … }}`) \ ending in a non-unit expression of type `{ty}`, but declares no `-> T` \ return type. D45 requires an explicit return type for a block-body \ function whenever the result is not unit — omitting `-> T` here silently \ changes the function's return type to `()`, discarding this value. Add \ `-> {ty}` to the function signature, or turn the trailing expression into \ a statement (drop the last value) if nothing should be returned.", name = fd.name, ty = ty_str, ), trailing.span, )); } /// [реестр 221.1 №493] All `return <expr>` values reachable from `e` /// WITHOUT crossing into a different execution context — same /// reachable-position set as `materialize_returns_in_expr`/`_in_block` /// above (If/IfLet/Match/While/WhileLet/Loop/For/Block; deliberately /// NOT `detach`/`spawn`/`parallel for`/closures, whose `return` /// belongs to a different fn). If every value that resolved agrees on /// one concrete type, that's the fn's real return type; otherwise /// (mixed, or none resolved) `None` — the caller stays silent. fn resolve_never_trailing_return_type( &self, e: &Expr, scope: &HashMap<String, TypeRef>, ) -> Option<TypeRef> { let mut out: Vec<TypeRef> = Vec::new(); self.collect_return_value_types_expr(e, scope, &mut out); let first = out.first()?; let first_r = ResolvedType::from_type_ref(first); if out.iter().all(|t| ResolvedType::from_type_ref(t) == first_r) { Some(first.clone()) } else { None } } fn collect_return_value_types_block( &self, b: &Block, scope: &HashMap<String, TypeRef>, out: &mut Vec<TypeRef>, ) { for s in &b.stmts { self.collect_return_value_types_stmt(s, scope, out); } if let Some(t) = &b.trailing { self.collect_return_value_types_expr(t, scope, out); } } fn collect_return_value_types_stmt( &self, s: &Stmt, scope: &HashMap<String, TypeRef>, out: &mut Vec<TypeRef>, ) { match s { Stmt::Return { value: Some(e), .. } => { if let Some(t) = self.infer_expr_type(e, scope) { out.push(t); } } Stmt::Expr(e) => self.collect_return_value_types_expr(e, scope, out), Stmt::ConsumeScope { body, .. } => self.collect_return_value_types_block(body, scope, out), _ => {} } } fn collect_return_value_types_expr( &self, e: &Expr, scope: &HashMap<String, TypeRef>, out: &mut Vec<TypeRef>, ) { match &e.kind { ExprKind::If { then, else_, .. } | ExprKind::IfLet { then, else_, .. } => { self.collect_return_value_types_block(then, scope, out); if let Some(eb) = else_ { match eb { ElseBranch::Block(b) => self.collect_return_value_types_block(b, scope, out), ElseBranch::If(x) => self.collect_return_value_types_expr(x, scope, out), } } } ExprKind::Match { arms, .. } => { for arm in arms { match &arm.body { MatchArmBody::Expr(x) => self.collect_return_value_types_expr(x, scope, out), MatchArmBody::Block(b) => self.collect_return_value_types_block(b, scope, out), } } } ExprKind::While { body, .. } | ExprKind::WhileLet { body, .. } | ExprKind::Loop { body, .. } | ExprKind::For { body, .. } => self.collect_return_value_types_block(body, scope, out), ExprKind::Block(b) => self.collect_return_value_types_block(b, scope, out), _ => {} } } fn f1_stmt( &self, s: &Stmt, gs: &GenericScope, scope: &mut HashMap<String, TypeRef>, errors: &mut Vec<Diagnostic>, ) { match s { Stmt::Expr(e) => self.f1_expr(e, gs, scope, errors), Stmt::Let(d) => { self.f1_expr(&d.value, gs, scope, errors); self.f4_check_value(&d.value, scope, errors); // Plan 124 (D220/D221): priv field PATTERN check — recursive, // covers nested destructure через sub-field types. // D411 (`[M-d411-record-binding-destructuring]`): same walk // ALSO enforces the record-binding `..`-partial rule here // (`enforce_binding_rest = true`) — only `ro`/`mut` bindings // (this is the `Stmt::Let` walk), not match-arms/for-loops. let scrut_ty = self.infer_expr_type(&d.value, scope); self.check_priv_pattern_recursive_inner( &d.pattern, scrut_ty.as_ref(), true, errors, ); // №145 gap 3: positional `(…)` destructure of a NAMED tuple // is forbidden by canon — curly `{…}` only. self.check_positional_destructure_on_named_tuple( &d.pattern, scrut_ty.as_ref(), errors, ); // §0 channel-first: codegen's `Pattern::Record` destructure // (emit_c.rs `emit_record_destructure`) needs the scrutinee's // BARE Nova type name to pick the right field-schema registry // — hand it the already-resolved `scrut_ty` here instead of // making codegen re-derive it by parsing the C-type string // (that string-parsing is what dropped `NovaTuple_` — №145). // Scoped to a DECLARED named-tuple type only (excludes a // compiler-intrinsic scrutinee with no `.nv` decl, e.g. // `Channel.new(...)`, whose type must not shadow the // hardcoded `record_schemas["ChannelPair"]` codegen has). if let (Pattern::Record { .. }, true, Some(TypeRef::Named { path, .. })) = (&d.pattern, d.value.id.is_set(), &scrut_ty) { let is_nt = path.last().and_then(|n| self.types.get(n.as_str())) .map_or(false, |td| matches!(td.kind, TypeDeclKind::NamedTuple(_))); if is_nt { self.resolved_types_buf.borrow_mut().entry(d.value.id) .or_insert_with(|| ResolvedType::from_type_ref(scrut_ty.as_ref().unwrap())); } } // Plan 124.8 (D175 amend): track ro-binding names. `ro x = expr` // делает binding immutable — даже `mut field` через `x.f = ...` // блокируется (Rust-style binding dominates). // Cleared on enclosing block exit via f1_block snapshot/restore // (fixed 2026-06-03 — [M-124.8-ro-binding-scope] closed). // // Shadow semantics: at each let, replace the prior entry (if // any) — `ro x; { mut x; x.field = ... }` works because the // inner `mut x` removes `x` from ro_binding_names. Outer // state restored by enclosing f1_block snapshot/restore. if let Some(name) = pattern_simple_name(&d.pattern) { let mut set = self.ro_binding_names.borrow_mut(); set.remove(&name); if !d.mutable { set.insert(name); } } // №309/№317 (окно p-ovl-channel): track `consume x = ...` // bindings the same way — shadow semantics identical to // `ro_binding_names` just above (replace prior entry at each // `let`, restored by the enclosing `f1_block`/fn-entry // snapshot). Independent of the `mutable` axis above (a // `consume` binding is also non-`mut`, so a name can be in // BOTH sets at once — orthogonal questions). if let Some(name) = pattern_simple_name(&d.pattern) { let mut set = self.consume_binding_names.borrow_mut(); set.remove(&name); if d.consume { set.insert(name); } } // Ф.1: annotation ↔ RHS. if let (Some(ann), Some(name)) = (&d.ty, pattern_simple_name(&d.pattern)) { // Plan 147 (D246): pass the binding's L1 mutability so the // E_READONLY_COERCE content-view check honours P7 (bare // `ro x` = freeze) — a ro source into `ro a T` is OK, into // `mut a T` is a coercion error. self.f1_check_assign_let( &d.value, ann, &name, d.mutable, gs, scope, errors, ); } else if pattern_simple_name(&d.pattern).is_some() { // D246-амендмент ([M-ro-launder-via-mut-binding], Ф.1, // 2026-07-23): UNANNOTATED `let` — `mut b = a` / `ro b = a` // — has no `ann` so `f1_check_assign_let` above never runs // (its call is gated on `d.ty.is_some()`). This was the // exact gap the hole exploited (probes A/D/F/G: `mut w = v` // with no type annotation). Content-view of a bare, no- // annotation target follows the binding directly (P7). let target_content_is_mut = Self::let_target_content_is_mut(None, d.mutable); self.check_readonly_source_coerce( &d.value, target_content_is_mut, scope, errors, ); } // Регистрируем переменную в scope: тип = аннотация, иначе // inferred из RHS. 172.1.2 (let-мост): третий источник — КАНАЛ // (f1_expr выше уже аннотировал RHS: generic-call return / Member // TypeParam-типы, которых TypeRef-инференс не видит); локальное // восстановление TypeRef через resolved_to_typeref_tp (TypeParam(n) // → Named{n} — при потреблении заново пометит mark_type_params). if let Some(name) = pattern_simple_name(&d.pattern) { // Plan 196.2 CAP-A (a) — chained-receiver scope propagation: // for a METHOD-CALL RHS (`let m = v.iter().map(f)`), the CHANNEL // (`infer_method_call_channel_type`, materialized into // `resolved_types_buf` by `f1_expr(&d.value)` above) threads the FULL // nested generic (`MapIter[VecIter[T],T,T]`) — while the inline // `infer_expr_type` TRUNCATES it to `MapIter[VecIter]` (drops T/U), so a // subsequent `m.next()` resolves against a degenerate receiver → an // unbound method-generic return erased to `nova_unit` (CC-FAIL). Prefer // the channel for a method-call RHS (annotation still wins first; the // channel is conservative — `None` when unsure → falls to `infer_expr_type`). // Gated to `Call{ Member }` so free-fn / non-call RHS is unchanged. let chain_ty: Option<TypeRef> = if d.ty.is_none() && d.value.id.is_set() && matches!(&d.value.kind, ExprKind::Call { func, .. } if matches!(&func.kind, ExprKind::Member { .. })) { let rt = self.resolved_types_buf.borrow().get(&d.value.id).cloned(); rt.and_then(|rt| Self::resolved_to_typeref_tp(&rt, d.value.span)) } else { None }; match d.ty.clone() .or(chain_ty) .or_else(|| self.infer_expr_type(&d.value, scope)) .or_else(|| { if !d.value.id.is_set() { return None; } let buf = self.resolved_types_buf.borrow(); let rt = buf.get(&d.value.id)?.clone(); drop(buf); Self::resolved_to_typeref_tp(&rt, d.value.span) }) { Some(t) => { scope.insert(name, t); } None => { scope.remove(&name); } } } else if let Pattern::Tuple(pats, _) = &d.pattern { // Plan 221.1 №286/№143 (окно p-chan): `ro (tx, rx) = // Channel[T].new(n)` is a two-name tuple-destructure whose // RHS is NEVER a real `TypeRef::Tuple` (`Channel.new` has // no registered `FnDecl` at all — it is a pure compiler // intrinsic, no `.nv` declaration anywhere, see PROGRESS- // pchan.md) — the generic tuple-typing branch below can // therefore never fire for it (falls to its own `else`, // which used to just `scope.remove` tx/rx — the ROOT of // №143: `tx`/`rx` NEVER got a typed scope entry in the // main checker pass at all). Bind them POSITIONALLY here // instead: `ChanWriter[T]`/`ChanReader[T]`, `T` from the // explicit turbofish when present (`channel_new_turbofish_ // elem`), else `generics: vec![]` (T left untracked — same // permissive pre-existing behavior for the bare // `Channel.new(cap)` form; every NEW check this window adds // — `channel_elem_type` — is gated on `generics.len()==1`, // so an empty-generics ChanWriter/ChanReader silently falls // back to legacy, byte-identical to before this window). if pats.len() == 2 && is_channel_new_call(&d.value) { // №286 residual-gap fallback (window p286): explicit // turbofish wins; else consult the first-send hint // seeded by `seed_channel_bare_send_hints` above. let elem_t = channel_new_turbofish_elem(&d.value).or_else(|| { d.value.id.is_set() .then(|| self.channel_bare_send_elem_hint.borrow().get(&d.value.id).cloned()) .flatten() }); for (i, pp) in pats.iter().enumerate() { if let Pattern::Ident { name: pn, .. } = pp { let base = if i == 0 { "ChanWriter" } else { "ChanReader" }; scope.insert(pn.clone(), TypeRef::Named { path: vec![base.to_string()], generics: elem_t.clone().into_iter().collect(), span: d.value.span, }); } } } else { // 172.1.2 (tuple-let регистрация, 2026-07-04): `let (a, b) = rhs` // — элементы регистрируются из Tuple-типа RHS (аннотация / // infer / канал); несопоставимое — remove (не наследовать тень). let rhs_tr = d.ty.clone() .or_else(|| self.infer_expr_type(&d.value, scope)) .or_else(|| { if !d.value.id.is_set() { return None; } let buf = self.resolved_types_buf.borrow(); let rt = buf.get(&d.value.id)?.clone(); drop(buf); Self::resolved_to_typeref_tp(&rt, d.value.span) }); if let Some(TypeRef::Tuple(tys, _)) = rhs_tr { if tys.len() == pats.len() { for (pp, ty) in pats.iter().zip(tys) { if let Pattern::Ident { name: pn, .. } = pp { scope.insert(pn.clone(), ty); } } } } else { for pp in pats { if let Pattern::Ident { name: pn, .. } = pp { scope.remove(pn); } } } } } else if let Pattern::Record { fields, .. } = &d.pattern { // Plan 221.1 №286/№143 (окно p-chan): the RECORD-destructure // spelling of the SAME `Channel.new` binding — `ro { tx, rx } // = Channel[T].new(n)` / renamed `{ tx: sender, rx: receiver }` // — is actually the MORE common form in this repo's existing // Channel fixtures (`channel_elem_type_word_safe.nv`, // `neg/channel_elem_str_payload_neg.nv`). The main checker // pass has NO general `Pattern::Record` destructure-typing // branch at all (a wider, pre-existing gap, not Channel- // specific) — mirror the Tuple-branch fix above, keyed by // FIELD NAME ("tx"/"rx") rather than position so a renamed // destructure (`{ tx: sender, rx: receiver }`) still binds // the RIGHT capability type to the RIGHT local name. if is_channel_new_call(&d.value) { // №286 residual-gap fallback (window p286): explicit // turbofish wins; else consult the first-send hint // seeded by `seed_channel_bare_send_hints` above. let elem_t = channel_new_turbofish_elem(&d.value).or_else(|| { d.value.id.is_set() .then(|| self.channel_bare_send_elem_hint.borrow().get(&d.value.id).cloned()) .flatten() }); for f in fields { let base = match f.name.as_str() { "tx" => Some("ChanWriter"), "rx" => Some("ChanReader"), _ => None, }; let Some(base) = base else { continue }; let bound_name = match &f.pattern { Some(Pattern::Ident { name: pn, .. }) => Some(pn.clone()), None => Some(f.name.clone()), _ => None, }; if let Some(bn) = bound_name { scope.insert(bn, TypeRef::Named { path: vec![base.to_string()], generics: elem_t.clone().into_iter().collect(), span: d.value.span, }); } } } } } // Plan 114.4 Ф.2: scope-local const — pass-through (no-op for now). Stmt::Const(_) => {} Stmt::Assign { target, value, .. } => { // №367 (window p375-ptr2): mark `target` as the top-level // assignment place BEFORE walking it — consumed by the // `ExprKind::Unary`/`ExprKind::Index` arms (see // `assign_target_top`'s field doc) so the READ-form pointer- // op retirement check does not also fire on a WRITE target // already covered by `check_target_readonly` below. self.assign_target_top.set(true); self.f1_expr(target, gs, scope, errors); self.assign_target_top.set(false); self.f1_expr(value, gs, scope, errors); // D175/D176 (Plan 108): check that we're not assigning to a // readonly field or through a readonly index. self.check_target_readonly(target, scope, errors); // **Plan 147 Ф.3 (D246, L1 binding):** reassigning the NAME // (`x = v` where target is a plain Ident) requires a `mut` // binding. A `ro`-bound local is fixed (L1) — even an explicit // `mut T` content-view does NOT make the name reassignable // (R2-split: `ro r mut Point` → `r.x` ✅ / `r = X` ❌). This // closes the latent reassignment hole in D36 (previously only // mut-method / index-write were enforced, plain rebind slipped // through). if let ExprKind::Ident(name) = &target.kind { if self.ro_binding_names.borrow().contains(name) { errors.push(Diagnostic::new( format!( "[E_LOCAL_NOT_MUT] local-binding `{}` не помечен `mut`, \ но переприсваивается (`{} = ...`). `ro`-binding фиксирует \ имя (L1 — Plan 147 / D246); для переприсваивания используй \ `mut {} = ...`. (Явный `mut T` content-view не делает имя \ переприсваиваемым — это R2-split: `ro r mut T` разрешает \ `r.field = v`, но не `r = X`.)", name, name, name), target.span, )); } } // Plan 157 (D200 amend): `Type.NAME = …` — reassigning a // namespace-qualified associated value (`const Type.NAME` OR // `ro Type.NAME`). Neither has a mutable binding to begin // with (there is no `mut Type.NAME` form — D200 §Modifier- // conflicts), so this is ALWAYS an error, same class/code as // the plain-`ro`-local reassignment check just above // (E_LOCAL_NOT_MUT) — an associated `ro`/`const` value is // even MORE fixed than a local: there is no `mut`-annotated // escape hatch for it at all. if let ExprKind::Path(parts) = &target.kind { if parts.len() == 2 { if let Some(td) = self.types.get(parts[0].as_str()) { if let Some(ac) = td.assoc_consts.iter().find(|ac| ac.name == parts[1]) { let kw = if ac.is_lazy_ro { "ro" } else { "const" }; errors.push(Diagnostic::new( format!( "[E_LOCAL_NOT_MUT] associated `{kw} {ty}.{name}` не \ может быть переприсвоено (`{ty}.{name} = ...`) — \ namespace-qualified associated values (D200 / Plan \ 157) read-only ВСЕГДА, `mut Type.NAME` формы не \ существует. Та же диагностика, что у обычного \ `ro`-local reassignment.", kw = kw, ty = parts[0], name = parts[1] ), target.span, )); } } } } // [M-scalar-nonliteral-narrowing-not-enforced] (D54): reassignment // `a = c` where a non-literal wider int narrows into `a`'s declared // int type needs an explicit `as`. Reassignment has no general // `assignable()` check, so do a focused narrowing-only compare here. // Literals coerce by context (skip); `as`-casts infer as their // target width (no narrowing seen). Only fires int→int narrowing. if let ExprKind::Ident(name) = &target.kind { let is_num_lit = matches!( value.kind, ExprKind::IntLit(_) | ExprKind::FloatLit(_) ) || matches!(&value.kind, ExprKind::Unary { op: UnOp::Neg, operand } if matches!(operand.kind, ExprKind::IntLit(_) | ExprKind::FloatLit(_))); if !is_num_lit { if let (Some(target_ty), Some(value_ty)) = (scope.get(name).cloned(), self.infer_expr_type(value, scope)) { // U.5.2: single-source narrowing via `would_narrow_into` // on DIRECT types (folds the legacy `is_int_narrowing`). if ResolvedType::from_type_ref(&value_ty) .would_narrow_into(&ResolvedType::from_type_ref(&target_ty)) { errors.push(Diagnostic::new( format!( "[E_IMPLICIT_NARROWING] cannot assign value of \ type `{}` to `{}` of narrower type `{}` — \ implicit int narrowing loses range; use an \ explicit `... as {}` cast (D54)", typeref_display(&value_ty), name, typeref_display(&target_ty), typeref_display(&target_ty), ), value.span, )); } } } } } Stmt::Return { value, .. } => { if let Some(v) = value { self.f1_expr(v, gs, scope, errors); self.f4_check_value(v, scope, errors); // №375 (window p375-ptr2): `return &ro_x` from a fn // declared `-> *mut T`. if let Some(ret_ty) = self.current_fn_return_ty.borrow().clone() { self.check_addrof_mut_from_ro_source(v, &ret_ty, errors); } } } Stmt::Throw { value, .. } => { self.f1_expr(value, gs, scope, errors); self.f4_check_value(value, scope, errors); } Stmt::Break(_) | Stmt::Continue(_) | Stmt::Reveal { .. } => {} Stmt::Defer { body, .. } => { self.f1_expr(body, gs, scope, errors); } // Plan 110 D188: walk init + body (full D188 R1-R6 check лежит // в Plan 110.1.2/110.1.3 — здесь scaffold walking). Stmt::ConsumeScope { binding, type_annot, init, body, .. } => { self.f1_expr(init, gs, scope, errors); self.f4_check_value(init, scope, errors); // №49 (221.1) [M-module-name-shadows-local]: `binding` was // NEVER registered in `scope` for the extent of `body` — a // `spawn consume ws = s.share() { ws.read_bytes(...) }` // (or plain `consume ws = … { … }`) whose binding name // collides with an IMPORTED MODULE name then had `ws.foo(…)` // inside `body` wrongly resolved as a module-qualified call // (`f1_check_call`'s `scope.contains_key(prefix)` guard saw // no entry → fell through to the module branch → false // `[E7401]`/wrong callee) instead of an instance-method call // on the binding — same shadow rule `f1_block` already // applies to a plain `let`. Snapshot/restore mirrors // `f1_block`'s own let-shadowing pattern so the outer scope // is byte-identical once `body` is done. let bind_ty = type_annot.clone() .or_else(|| self.infer_expr_type(init, scope)) .or_else(|| { if !init.id.is_set() { return None; } let buf = self.resolved_types_buf.borrow(); let rt = buf.get(&init.id)?.clone(); drop(buf); Self::resolved_to_typeref_tp(&rt, init.span) }) .unwrap_or_else(|| prim_ref("Any", init.span)); let prev_binding = scope.insert(binding.clone(), bind_ty); for s in &body.stmts { self.f1_stmt(s, gs, scope, errors); } if let Some(t) = &body.trailing { self.f1_expr(t, gs, scope, errors); } match prev_binding { Some(t) => { scope.insert(binding.clone(), t); } None => { scope.remove(binding); } } } Stmt::AssertStatic { expr, .. } | Stmt::Assume { expr, .. } => { self.f1_expr(expr, gs, scope, errors); } Stmt::Apply { args, .. } => { for a in args { self.f1_expr(a, gs, scope, errors); } } Stmt::Calc { steps, .. } => { for step in steps { self.f1_expr(&step.expr, gs, scope, errors); } } // Plan 136: tuple destructuring assignment. Stmt::TupleAssign { lhs, rhs, .. } => { for e in lhs { self.f1_expr(e, gs, scope, errors); } for e in rhs { self.f1_expr(e, gs, scope, errors); } } } } /// Plan 152.1 Ф.1: strip `ro`/`mut`/`unsafe` wrappers and return the base /// `Named` type's last path segment (e.g. `"str"`, `"Range"`, `"CharsIter"`), /// or `None` for non-Named types. fn typeref_named_base(t: &TypeRef) -> Option<&str> { match t { TypeRef::Named { path, .. } => path.last().map(|s| s.as_str()), TypeRef::Readonly(inner, _) | TypeRef::Mut(inner, _) | TypeRef::Uninit(inner, _) => Self::typeref_named_base(inner), _ => None, } } /// Plan 174.2 Ф.B — cross-carrier `?` diagnostics. /// /// `?` (return-only, D85) пробрасывает **тем же** носителем, что и /// return-тип enclosing fn: `Option`-`?` требует `Option`-fn, `Result`-`?` /// требует `Result`-fn. Если носитель операнда не совпадает с носителем /// return-типа — вместо generic type-mismatch на выведенном `return /// None`/`return Err` даём конкретную подсказку-конверсию (`.ok_or(..)` / /// `.ok()`). /// /// Свободный `?` в fn, чей return **вообще** не Result/Option, уже режется /// `[E_TRY_IN_FAIL_FN]` (см. `check_fn`) — сюда попадаем только когда /// return-носитель ЕСТЬ Result или Option, поэтому дубля нет. /// /// Консервативно: молчим, если тип операнда не выводится, если операнд/ /// return несут generic-параметры (`gs`), или если носители совпадают. fn check_try_carrier_match( &self, inner: &Expr, gs: &GenericScope, scope: &HashMap<String, TypeRef>, errors: &mut Vec<Diagnostic>, ) { // Return-носитель enclosing fn. let ret_ty = match self.current_fn_return_ty.borrow().clone() { Some(t) => t, None => return, }; if typeref_mentions_any(&ret_ty, gs) { return; } let ret_carrier = match Self::typeref_named_base(&ret_ty) { Some(b @ ("Result" | "Option")) => b, _ => return, // не Result/Option → зона [E_TRY_IN_FAIL_FN]. }; // Носитель операнда. let op_ty = match self.infer_expr_type(inner, scope) { Some(t) => t, None => return, // не выводится — молчим (safe false-negative). }; if typeref_mentions_any(&op_ty, gs) { return; } let op_carrier = match Self::typeref_named_base(&op_ty) { Some(b @ ("Result" | "Option")) => b, _ => return, // не Result/Option — иная ошибка (type-mismatch). }; if op_carrier == ret_carrier { return; } // носители совпали — OK. match (ret_carrier, op_carrier) { ("Result", "Option") => errors.push(Diagnostic::new( "[E_TRY_OPTION_IN_RESULT_FN] `?` на `Option` в функции, \ возвращающей `Result` — `?` пробросил бы `return None`, но `None` \ не `Result`. Сконвертируй в `Result` перед `?`: \ `opt.ok_or(<error>)?` (даёт `Err(<error>)` на `None`).".to_string(), inner.span, )), ("Option", "Result") => errors.push(Diagnostic::new( "[E_TRY_RESULT_IN_OPTION_FN] `?` на `Result` в функции, \ возвращающей `Option` — `?` пробросил бы `return Err(e)`, но `Err` \ не `Option`. Сконвертируй в `Option` перед `?`: `res.ok()?` \ (отбрасывает ошибку, даёт `None`).".to_string(), inner.span, )), _ => {} } } /// [E_COALESCE_RETURN_FALLBACK] (D86 AMEND 2026-07-23, ретракция формы /// `X ?? return R`): парсер принял форму в AST /// (`ExprKind::CoalesceReturnFallback` — parse-then-diagnose, rustc-style), /// эта функция ВСЕГДА отвергает её здесь, контекстной подсказкой по /// (`тип X`, return-тип enclosing fn) — общая decision-функция /// `coalesce_return_fallback_advice` переиспользуется линтом /// `W_MANUAL_COALESCE` (Ф.3, `lints.rs`). /// /// В отличие от `check_try_carrier_match` (где совпавший носитель — OK, /// молчим) — здесь диагностика срабатывает ВСЕГДА, форма отвергнута /// целиком, не только carrier-mismatch. fn check_coalesce_return_fallback( &self, a: &Expr, ret_value: &Option<Box<Expr>>, whole_span: Span, gs: &GenericScope, scope: &HashMap<String, TypeRef>, errors: &mut Vec<Diagnostic>, ) { let op_ty = self.infer_expr_type(a, scope); let ret_ty = self.current_fn_return_ty.borrow().clone(); let advice = coalesce_return_fallback_advice(op_ty.as_ref(), ret_ty.as_ref(), gs); // Суффикс `?? return ...` целиком — от конца операнда `X` до конца // всего выражения. Замена этого диапазона НЕ требует знания текста // `X` (он остаётся нетронутым слева от span'а) — machine-applicable // без source-map в чекере. let suffix_span = Span::with_file(a.span.end, whole_span.end, a.span.file_id); let ret_span = ret_value.as_ref().map(|v| v.span).unwrap_or(whole_span); let (note, suggestion) = coalesce_advice_render(&advice, suffix_span); let mut diag = Diagnostic::new( "[E_COALESCE_RETURN_FALLBACK] `return` не может быть fallback'ом \ оператора `??` (D86, ретракция формы 2026-07-23). `??` подставляет \ значение и продолжает вычисление; ранний возврат — это поток \ управления, для него отдельные операторы." .to_string(), ret_span, ); diag = diag.with_note(note); if let Some(s) = suggestion { diag = diag.with_suggestion(s); } errors.push(diag); } fn f1_expr( &self, e: &Expr, gs: &GenericScope, scope: &mut HashMap<String, TypeRef>, errors: &mut Vec<Diagnostic>, ) { // Run the full checker walk first (unchanged behavior). self.f1_expr_inner(e, gs, scope, errors); // Plan 104.10 Ф.2 (D379): opt-in IDE per-expression type recording. ZERO overhead // when off (single predictable branch, no per-node inference). POST-ORDER — the inner // walk has finished, so the semantic `resolved_types_buf` channel this node depends on // (Call return, Range, RecordLit, Tuple, …) is fully populated. Every recursion goes // through this wrapper (the inner arms call `self.f1_expr`), so nested `a.b.c` records // all three levels. Pure side-channel: it reads inference, never changes it. if self.record_expr_types { self.record_expr_type_ide(e, gs, scope); } } /// Plan 104.10 Ф.2 (D379): record `e`'s inferred type into the opt-in IDE map /// (`expr_types_buf` → `ModuleEnv.expr_types`). Called ONLY when `record_expr_types` is /// set. The type comes from the REAL checker inference — the syntactic `infer_expr_type` /// (rich `TypeRef` with qualified path + generics, ideal for hover) with a fallback to the /// semantic `resolved_types_buf` channel (covers `Range`, tuples, literals and record-lits /// the syntactic pass leaves to the checker). Never a textual heuristic. /// /// Skips (absence = "unknown", IDE degrades gracefully): /// - synthetic / zero-width spans (`start >= end`, includes `Span::default()`) — no source /// range to anchor a cursor to; /// - un-inferrable types: `resolved_to_typeref` returns `None` for a bare generic /// type-param (`T`) coming from the semantic channel, and the `gs`-gate below drops a /// generic-param type reached via scope (`fn g[T](x T)` → `x` is `T`), plus unit / any / /// raw-ptr / fn types → no garbage / no unsubstituted type-vars in the map. /// /// `gs` = the set of generic type-parameter names in scope for the enclosing fn/impl (the /// same set `f1_expr` threads for its channel gating) — a type MENTIONING any of them is /// not a concrete, IDE-displayable type, so it is skipped (D379 "absence = unknown"). /// /// [M-104.10-expr-types-coverage] COVERAGE REMAINDER (production, not a simplification — /// this reuses the checker's REAL inference, `infer_expr_type` + the semantic /// `resolved_types_buf` channel, NOT a textual heuristic). The plan-required set is fully /// covered (literals, Ident, Member obj+result, Index, Call return, Binary, Range, As, /// Tuple / Array / Record literals). NOT yet recorded (bounded by what those channels /// annotate today): (a) GENERIC instance method-chain returns — `infer_expr_type`'s Call /// arm is intentionally decoupled from general instance-method return inference (172.1 /// perturbation lesson), so `v.map(f).filter(g)` mid-chain types are absent unless the /// buf already holds them; (b) NON-primitive `TupleLit` (the buf annotates only /// all-primitive tuples; `infer_expr_type` has no `TupleLit` arm); (c) GENERIC-instance /// `RecordLit` / container-element types carrying an unbound type-param (gated out by /// design). These degrade gracefully (absence = unknown) and are the follow-up remainder. fn record_expr_type_ide( &self, e: &Expr, gs: &GenericScope, scope: &HashMap<String, TypeRef>, ) { // Synthetic / compiler-generated expressions carry a default (zero-width) span. if e.span.start >= e.span.end { return; } let ty = self.infer_expr_type(e, scope).or_else(|| { if e.id.is_set() { self.resolved_types_buf .borrow() .get(&e.id) .and_then(|rt| Self::resolved_to_typeref(rt, e.span)) } else { None } }); if let Some(tr) = ty { // Skip un-substituted generic-param types (`T`, `Vec[T]`, `[]T`, …) — not a // concrete IDE type. Mirrors the checker channel's own `gs`-gating. if typeref_mentions_any(&tr, gs) { return; } self.expr_types_buf.borrow_mut().insert(e.span, tr); } } fn f1_expr_inner( &self, e: &Expr, gs: &GenericScope, scope: &mut HashMap<String, TypeRef>, errors: &mut Vec<Diagnostic>, ) { // Plan 172.1 U.4.4(b): annotate this Ident's resolved type into the checker channel // (lifted into `ModuleEnv.resolved_types` → read AUTHORITATIVELY by codegen instead of // re-deriving in `infer_expr_c_type`, §0/§1). This is the scope-aware producer the // syntactic `number_exprs` cannot be. GATE: skip bare generic-params (`T`) — their // codegen lowering is erased `void*` in the generic stub / mono-substituted at // instantiation, which the generic-LEVEL annotation `Named{T}` cannot reproduce // (U.4.4-prep.b audit: the `Nova_T*` vs `void*` class). Concrete-typed Idents are the // BEHAVIOR-CHANGE set: where the checker resolves a real type the legacy // `infer_expr_c_type` mis-fell-back to `nova_int`, codegen now emits the correct type. if let ExprKind::Ident(_) = &e.kind { if e.id.is_set() { if let Some(tr) = self.infer_expr_type(e, scope) { let rt = ResolvedType::from_type_ref(&tr); // Plan 172.1.1: annotate ALL Idents (primitive + non-primitive). The consumer // (infer_expr_c_type) applies a var_types-gate: an Ident whose name IS in codegen // `var_types` (the reliable per-local C-type set at decl-emit) → legacy (var_types // — correct incl. mono generic-containers, NOT a re-derive); an Ident ABSENT from // var_types (Cat-B13: pre-decl / synthesized / generic-fn-body) → channel (§1-fix: // legacy `_=>nova_int` fallback). This gate avoids the earlier full-corpus break // (non-primitive mono lowering needs codegen typedef/subst context — now routed to // legacy via var_types, never the generic-level channel). self.resolved_types_buf.borrow_mut().insert(e.id, rt); } } } // Plan 172.1.1 (substrate probe — gap#2 hunt). if let ExprKind::SelfAccess = &e.kind { if e.id.is_set() { if let Some(tr) = self.infer_expr_type(e, scope) { let rt = ResolvedType::from_type_ref(&tr); self.resolved_types_buf.borrow_mut().insert(e.id, rt); } } } // Plan 172.1.1 (U.4.5 — Index probe): annotate `obj[i]` with the checker's element type. // The checker infers the element DIRECTLY (e.g. `@_order[i]` → `str` for a `Vec[str]`), // авторитетно — whereas legacy re-derives it from codegen `array_element_types` and falls // to `nova_int` when absent (§1 bug). Tests whether direct Index annotation sidesteps the // SelfAccess-cascade gap #2 (bare `@` → erased schema → nova_int element). if let ExprKind::Index { obj: ix_obj, index: ix_index } = &e.kind { if e.id.is_set() { if let Some(tr) = self.infer_expr_type(e, scope) { let rt = ResolvedType::from_type_ref(&tr); self.resolved_types_buf.borrow_mut().insert(e.id, rt); } else if ix_obj.id.is_set() && !matches!(ix_index.kind, ExprKind::Range { .. }) { // 172.1.2 (Index-мост): obj аннотирован каналом (Шаг 2b — `@data` // TypedPtr(T), `@_buckets` Named{Vec,[Slot..]}), но TypeRef-инференс // его не видит. СТРУКТУРНЫЕ формы only (урок POISON 6453 — никакого // user-@index здесь): Vec[E]/[]E → E; *mut E (raw-buffer) → E. // Plan 196 Ф.4b: правило контейнер→элемент — в решателе // (`Constraint::Project`, §0-единственный источник); byte-parity // сохранён (`project` повторяет ТУ ЖЕ структурную раскладку). let container = self.resolved_types_buf.borrow().get(&ix_obj.id).cloned(); let elem = container.and_then(|c| { Self::project_channel(&c, constraint_solver::ProjectKind::Element) }); if let Some(rt) = elem { self.resolved_types_buf.borrow_mut().insert(e.id, rt); } } } } // Plan 172.1.1 (U.4.5 — Member probe): annotate `obj.field` with the checker's field type. // Same authoritative-channel logic as Index. `infer_expr_type` returns Err/None for // method-receiver / module-path / enum-path Members → those fall through to legacy via the // consumer's `Ok(ir_c)` guard, so this only covers genuine field accesses. if let ExprKind::Member { .. } = &e.kind { if e.id.is_set() { if let Some(tr) = self.infer_expr_type(e, scope) { let rt = ResolvedType::from_type_ref(&tr); if std::env::var_os("NOVA_MEMBER_INT_TRACE").is_some() && matches!(&rt, ResolvedType::Scalar { wide_default: true, signed: true, .. }) { eprintln!("[MEMBER-INT preamble] id={:?} span={:?} tr={:?}", e.id, e.span, tr); } self.resolved_types_buf.borrow_mut().insert(e.id, rt); } } } // Plan 172.1 tally (2026-07-02) — тривиальные каналы: // Range: `a..b` ВСЕГДА имеет тип Range (std value-record) — фундаментальный факт §0. // Лоуэринг — через единый resolved_type_to_c (NovaValue_Range / Nova_Range*). if matches!(&e.kind, ExprKind::Range { .. }) && e.id.is_set() { self.resolved_types_buf.borrow_mut().insert( e.id, ResolvedType::Named { name: "Range".into(), module: vec![], args: vec![] }, ); } // RecordLit: тип литерала = сам объявленный тип. Гейт: только NON-generic // объявленный тип (generic-инстанс требует mono-подстановку — 172.1.2). if let ExprKind::RecordLit { type_name: Some(rl_name), .. } = &e.kind { if let Some(rl_last) = rl_name.last() { if e.id.is_set() && self.types.get(rl_last.as_str()).map_or(false, |td| td.generics.is_empty()) { self.resolved_types_buf.borrow_mut().insert( e.id, ResolvedType::Named { name: rl_last.clone(), module: vec![], args: vec![] }, ); } } } // Plan 196 stage1 (§0 materialize, mirror of the 6z HandlerLit arm): a handler // literal `effect E {…}` has a purely SYNTACTIC type — `NovaVtable_<E>*` — modelled // as `Effect[<E>]` (module empty, name == `effect_name.join("_")`), which the SINGLE // canonical lowering `resolved_type_to_c` ("Effect" arm, UNCONDITIONAL — no registry // dependency) turns into the SAME string the legacy arm built. Materialize into the // channel so Channel 2 covers it and the legacy 6z arm retires (§0). if let ExprKind::HandlerLit { effect_name, .. } = &e.kind { if e.id.is_set() { self.resolved_types_buf.borrow_mut().insert( e.id, ResolvedType::Named { name: "Effect".into(), module: vec![], args: vec![ResolvedType::Named { name: effect_name.join("_"), module: vec![], args: vec![], }], }, ); } } // Plan 196 stage1 (§0 materialize, mirror of the 6z ProtocolLit arm): a protocol // literal `protocol P {…}` has the syntactic fat-pointer type `NovaBox_<P>`. A // literal only COMPILES when its protocol is registered (vtable emission needs it), // so `resolved_type_to_c`'s protocol branch (`protocol_types.contains(name)` → // `NovaBox_<name>`) is guaranteed to fire and yield the SAME string the legacy arm // built (module empty, name == `proto_name.join("_")`; every literal is single- // segment). Materialize so Channel 2 covers it and the legacy 6z arm retires (§0). if let ExprKind::ProtocolLit { proto_name, .. } = &e.kind { if e.id.is_set() { self.resolved_types_buf.borrow_mut().insert( e.id, ResolvedType::Named { name: proto_name.join("_"), module: vec![], args: vec![], }, ); } } // Plan 172.1.1 (U.4.5 — control-flow probe): annotate Block/If/IfLet/Path with the // checker's value type (block tail / branch-join / variant). Each propagates an ALREADY- // checked inner type — no new re-derive hazard. `infer_expr_type` returns None for // unit/never/unresolved → those fall through to legacy via the consumer's `Ok` guard. // NOTE: Match is intentionally excluded here — `infer_expr_type` for Match uses a // "first-wins" arm scan that skips pattern-binding arms (e.g. `d407V(v) => v`) and // can pick a WRONG later arm (IntLit → nova_int), poisoning the channel. Match // annotation is handled exclusively by `infer_match_common_primitive` (below, in the // main dispatch), which correctly requires ALL non-divergent arms to agree. if matches!( &e.kind, ExprKind::Block(_) | ExprKind::If { .. } | ExprKind::IfLet { .. } | ExprKind::Path(_) ) { if e.id.is_set() { if let Some(tr) = self.infer_expr_type(e, scope) { let rt = ResolvedType::from_type_ref(&tr); if std::env::var("NOVA_DEBUG_IF_INFER").is_ok() && matches!(&e.kind, ExprKind::If { .. }) { eprintln!("PREAMBLE-IF id={:?} span={:?} rt={:?}", e.id, e.span, rt); } self.resolved_types_buf.borrow_mut().insert(e.id, rt); } } } // Plan 172.1 P67 ФАЗА 2 STEP 1 — numeric-type-constant Path (`int.MAX` / `u8.MIN` / // `f64.EPSILON` / `char.MAX`). The value `T.CONST` HAS type `T` — a fundamental fact // (§0), NOT a duplicated codegen table: channel `T`'s ResolvedType DIRECTLY (DECOUPLED — // no `infer_expr_type` extension, per the 172.1.2 perturbation lesson). Lowered via the // single `resolved_type_to_c` this is byte-identical to the legacy // `numeric_type_constant_mapping` c-type for EVERY row EXCEPT the two legacy collapses // `i64.MAX→nova_int` (channel→`int64_t`) and `char.MAX→nova_int` (channel→`nova_char`): // those two are the §0 / named-priority FIX (i64 ≠ int = intptr_t; char = codepoint, D327), // not a regression. The constant's C *value* (e.g. `((nova_int)INT64_MAX)`) stays emitted // by the legacy path (its side-effects always run); only the expression's *type* channels. if let ExprKind::Path(parts) = &e.kind { if e.id.is_set() && parts.len() == 2 { let is_numeric_const = matches!( parts[1].as_str(), "MAX" | "MIN" | "MIN_POSITIVE" | "EPSILON" | "NAN" | "INFINITY" | "NEG_INFINITY" | "PI" | "E" ); if is_numeric_const { let prim: Option<ResolvedType> = ResolvedType::scalar_from_int_name(&parts[0]).or_else(|| { match parts[0].as_str() { "f32" => Some(ResolvedType::Float { width: 32 }), "f64" => Some(ResolvedType::Float { width: 64 }), "char" => Some(ResolvedType::Named { name: "char".to_string(), module: Vec::new(), args: Vec::new(), }), _ => None, } }); if let Some(rt) = prim { self.resolved_types_buf.borrow_mut().insert(e.id, rt); } } } } // Plan 172.1 U.4.5 (RecordLit slice — NON-GENERIC records): materialize a typed // record literal's resolved type into the checker channel so codegen READS it via the // SINGLE `resolved_type_to_c` instead of the legacy `infer_expr_c_type` RecordLit // re-derive (§0/§1, emit_c.rs:36426). GATE to NON-generic record types // (`self.types[name]` a `Record` with no type-params): for those the bare `Named{name}` // annotation is the COMPLETE type — there are NO generic-mono args to lose (the // 66%-divergence source for generic records is exactly the missing `generics` in // `infer_arg_ty` :12113 → generic records STAY on legacy until the generic-mono // materialization slice). `resolved_type_to_c` reproduces the value-vs-heap choice from // codegen state (`value_record_names`) — the SAME source legacy uses (:36478) — so the // lowering is byte-identical. Sum types (kind=Sum) and sum-VARIANTS (not present in // `self.types` as records) and `Self`-lit (not in `self.types`) are excluded → legacy. if let ExprKind::RecordLit { type_name: Some(name), fields, .. } = &e.kind { if e.id.is_set() { if let Some(last) = name.last() { // Exclude RUNTIME_DEFINED_TYPES (str / Option / guards / …): those are // C-runtime-header-backed, NOT in codegen `record_schemas`, so the legacy // RecordLit arm falls to `void*` (:36484) — annotating their REAL type // (e.g. `str{…}` → `nova_str`) would DIVERGE (a §1 fix, but a separate // verified atom, not this slice). Shared single-source const (emit_c.rs, // introduced 83ad5c46) — §0, no duplicate list. if !crate::codegen::emit_c::RUNTIME_DEFINED_TYPES.contains(&last.as_str()) { // 172.1.2 (Self-литерал, 2026-07-04): `Self{...}` в методе — // тип = ресивер; гейт: non-generic recv (self.types). if last == "Self" && e.id.is_set() { if let Some(recv) = self.current_recv_type.borrow().clone() { // ТОЛЬКО non-generic recv: generic Self{...} в mono // требует инстанс-имени (bare сломал бы mono — // поймано гейтом 2026-07-04, расширение откачено). if self.types.get(&recv) .map_or(false, |td| td.generics.is_empty()) { self.resolved_types_buf.borrow_mut().insert( e.id, ResolvedType::Named { name: recv, module: vec![], args: vec![], }, ); } } } // 172.1.2 (record-вариант sum'а, 2026-07-03): `Cons{...}` — // имя ВАРИАНТА (не в self.types) → тип литерала = содержащий // sum; гейт: единственный non-generic sum с record-вариантом. if !self.types.contains_key(last.as_str()) && e.id.is_set() { let mut owner: Option<&String> = None; let mut ambiguous = false; for (tn, td2) in self.types.iter() { if let TypeDeclKind::Sum(vs) = &td2.kind { if td2.generics.is_empty() && vs.iter().any(|v| v.name == *last && matches!(v.kind, SumVariantKind::Record(_))) { if owner.is_some() { ambiguous = true; break; } owner = Some(tn); } } } if let (Some(tn), false) = (owner, ambiguous) { self.resolved_types_buf.borrow_mut().insert( e.id, ResolvedType::Named { name: tn.clone(), module: vec![], args: vec![], }, ); } } if let Some(td) = self.types.get(last) { if let TypeDeclKind::Record(field_decls) = &td.kind { // Generic args: для каждого generic-параметра тип-аргумент = // inferred-тип поля литерала, чей шаблонный тип — ровно этот // bare-параметр. 2026-07-02 (audit POISON 6630, аналог fd3207d6): // прежний `unwrap_or_else(int)` («byte-identical legacy nova_int») // УДАЛЁН — §1-нарушение: несопоставленный/невыводимый параметр // (`type Wrap[T]{items Vec[T]}`, опущенное поле, method-call // значение) штамповал ЛОЖНЫЙ `Wrap[int]` в канал как факт. // Теперь all-or-nothing: ЛЮБОЙ невыведенный параметр → БЕЗ // аннотации → legacy-навигация (честное отсутствие). // Non-generic record → empty generics → `Named{name}` как раньше. // Sum-types / sum-variants / `Self`-lit are not `Record` // in `self.types` → not annotated → legacy. let gen_args: Option<Vec<TypeRef>> = td .generics .iter() .map(|g| { field_decls.iter().find_map(|fd| { if let TypeRef::Named { path, generics: fg, .. } = &fd.ty { if fg.is_empty() && path.join("_") == g.name { return fields .iter() .find(|f| f.name == fd.name) .and_then(|f| f.value.as_ref()) .and_then(|v| { self.infer_expr_type(v, scope) }); } } None }) }) .collect(); if let Some(gen_args) = gen_args { let rt = ResolvedType::from_type_ref(&TypeRef::Named { path: name.clone(), generics: gen_args, span: e.span, }); self.resolved_types_buf.borrow_mut().insert(e.id, rt); } else if std::env::var_os("NOVA_RL_TRACE").is_some() { eprintln!("[RL-MISS] {} generics={} span={:?}", last, td.generics.len(), e.span); } } } } } } } // Plan 172.1 U.4.5 (TupleLit slice — ALL-PRIMITIVE elements) + Plan 196 Ф.2 // (TupleLit slice — NON-GENERIC value-named elements, docs/plans/196-audit.md // §2 row 10): materialize a tuple literal's resolved type into the checker // channel (§0/§1). GATE: every element infers to EITHER a PRIMITIVE resolved // type (shared `primitive_gate`) OR a CONCRETE non-generic declared value type // (record/sum/newtype/named-tuple, no type-args) — the SAME `concrete_value_named` // gate `infer_match_common_primitive` / `f3_check_member` already use (172.1.2, // mirrors the RecordLit-gate precedent at :7413 above). For an all-concrete tuple // the legacy `infer_expr_c_type(TupleLit)` (:36273) builds the SAME mono // `compute_mono_tuple_c_name`/`register_mono_tuple` that `resolved_type_to_c // (R::Tuple)` (:1982) builds — byte-identical: primitives lower context-free, and a // concrete non-generic Named element lowers via `resolved_named_to_c`'s // `type_aliases`/plain-pointer branch, exactly what `infer_expr_c_type` computes // for that same element expression (no type-param `is_empty`-vs-`Nova_`-schema // divergence — the §9 type-param gating point). A generic / un-inferrable element // → skip → legacy (sound; the genuine mono-inference residual stays there). if let ExprKind::TupleLit(elems) = &e.kind { if e.id.is_set() && !elems.is_empty() { let mut elem_rts: Vec<ResolvedType> = Vec::with_capacity(elems.len()); for el in elems { match self.infer_expr_type(el, scope) { Some(tr) => { let rt = ResolvedType::from_type_ref(&tr); let concrete_value_named = matches!(&rt, ResolvedType::Named { name, args, .. } if args.is_empty() && self.types.get(name).map_or(false, |td| matches!(&td.kind, TypeDeclKind::Record(_) | TypeDeclKind::Sum(_) | TypeDeclKind::Newtype(_) | TypeDeclKind::NamedTuple(_)))); if Self::primitive_gate(&rt) || concrete_value_named { elem_rts.push(rt); } else { break; } } None => break, } } if elem_rts.len() == elems.len() { self.resolved_types_buf .borrow_mut() .insert(e.id, ResolvedType::Tuple(elem_rts)); } } } // P67: control-flow loops always evaluate to unit — annotate directly. // Plan 173.1 Ф.2 (D71): `parallel for` with a trailing expression is the // EXCEPTION — it evaluates to `[]T` (≡ `Vec[T]`, D239) where T is the // trailing's type. Annotating it Unit here poisoned Channel 2 downstream: // `Stmt::Let`'s C-type probe adopted `nova_unit` while the emission // produced a `Nova_Vec____<T>*` → CC-FAIL «initializing 'nova_unit' with // 'Nova_Vec____nova_int *'» ([M-parfor-record-result-miscompile] surface). // Trailing-less (statement-mode) `parallel for` stays Unit. When the // element type can't be inferred here, DON'T annotate — honest channel // miss → codegen's `infer_expr_c_type` ParallelFor arm computes the // Vec[T]* itself (same Vec-mangle, one lowering window). if e.id.is_set() { let is_unit_loop = matches!( &e.kind, ExprKind::For { .. } | ExprKind::While { .. } | ExprKind::WhileLet { .. } | ExprKind::Loop { .. } ); if is_unit_loop { self.resolved_types_buf.borrow_mut().insert(e.id, ResolvedType::Unit); } if let ExprKind::ParallelFor { pattern, iter, body, elem_type } = &e.kind { match &body.trailing { None => { self.resolved_types_buf.borrow_mut().insert(e.id, ResolvedType::Unit); } Some(t) => { // The trailing may reference the LOOP VARIABLE, absent from // the outer `scope` at this preamble point — extend a local // scope copy with the pattern binding typed as the iterator's // element (same source `f1_for_body` uses). Non-Ident patterns // (destructure) or un-inferrable iterators → probe with the // outer scope as-is; a trailing that then fails to infer stays // un-annotated (honest miss → codegen fallback). let loop_elem_tr = elem_type.clone() .or_else(|| self.infer_iter_elem_type(iter, scope)); let mut body_scope = scope.clone(); if let (Pattern::Ident { name, .. }, Some(et)) = (pattern, &loop_elem_tr) { body_scope.insert(name.clone(), et.clone()); } // Body's own top-level lets can also feed the trailing. for s in &body.stmts { if let Stmt::Let(d) = s { if let Pattern::Ident { name, .. } = &d.pattern { let ty = d.ty.clone() .or_else(|| self.infer_expr_type(&d.value, &body_scope)); if let Some(ty) = ty { body_scope.insert(name.clone(), ty); } } } } if let Some(elem_tr) = self.infer_expr_type(t, &body_scope) { let elem_rt = ResolvedType::from_type_ref(&elem_tr); self.resolved_types_buf.borrow_mut().insert( e.id, ResolvedType::Named { name: "Vec".to_string(), module: vec![], args: vec![elem_rt], }, ); } } } } } // Plan 172.1 (P67 literal-annotation): literal types are structurally determined — // annotate into the checker channel to eliminate P67_STRICT fall-through for literals. // Byte-identical to legacy: IntLit→nova_int, FloatLit→nova_f64, BoolLit→nova_bool, // StrLit/InterpolatedStr→nova_str, CharLit→nova_char, UnitLit→nova_unit, NullPtrLit→void*. if e.id.is_set() { let lit_rt: Option<ResolvedType> = match &e.kind { ExprKind::IntLit(_) => { Some(ResolvedType::Scalar { width: 64, signed: true, wide_default: true }) } ExprKind::FloatLit(_) => Some(ResolvedType::Float { width: 64 }), ExprKind::BoolLit(_) => Some(ResolvedType::Bool), ExprKind::StrLit(_) | ExprKind::InterpolatedStr { .. } => Some(ResolvedType::Str), ExprKind::CharLit(_) => Some(ResolvedType::Named { name: "char".to_string(), module: Vec::new(), args: Vec::new(), }), ExprKind::UnitLit => Some(ResolvedType::Unit), ExprKind::NullPtrLit => Some(ResolvedType::Ptr), // D412 (Plan 186): hex-blob / embed → `[]u8` ≡ Vec[u8] (D239 nominal canon). ExprKind::HexBlobLit(_) => Some(ResolvedType::Named { name: "Vec".to_string(), module: Vec::new(), args: vec![ResolvedType::Scalar { width: 8, signed: false, wide_default: false, }], }), _ => None, }; if let Some(rt) = lit_rt { self.resolved_types_buf.borrow_mut().insert(e.id, rt); } } match &e.kind { ExprKind::Call { func, args, trailing } => { // 172.1.2 Шаг 2: func-позиция — Member здесь = метод-вызов, не field-read. self.in_call_func.set(true); self.f1_expr(func, gs, scope, errors); self.in_call_func.set(false); // Plan 174.5 §3/§4: write-cap check for the pointer WRITE- // method family (`.write`/`.write_at`/`.write_unaligned`/ // `.write_volatile`/`.copy_from`/`.copy_from_nonoverlapping`) // — mirrors `check_target_readonly`'s Deref/Index arms // (same `pointee_is_writable` helper), but at the CALL site // instead of an assignment target. `pointee_is_writable` // returns `None` for non-Pointer receiver types, so this is a // no-op for the many OTHER types that also happen to define a // `.write(...)` method (e.g. `io.Write`) — scoped exactly to // raw `*T`/`*mut T` receivers. Closes the gap noted in // §10a (`.write()` previously bypassed this checker-level // gate entirely, relying only on the codegen-side `is_const` // C-string heuristic, which misses cases like `ro-bound // local = vec.ptr()` where the C type carries no `const`). if let ExprKind::Member { obj, name } = &func.kind { if matches!( name.as_str(), "write" | "write_at" | "write_unaligned" | "write_volatile" | "copy_from" | "copy_from_nonoverlapping" ) { if let Some(ty) = self.infer_expr_type(obj, scope) { if let Some(writable) = pointee_is_writable(&ty) { if !writable { errors.push(Diagnostic::new( format!( "[E_POINTER_RO_ASSIGN] cannot call `.{}()` \ through a readonly pointer — `*T` is a \ readonly pointee (the L3 default is `ro`: \ `*T ≡ *ro T`, Plan 147 / D246 / 174.5 §4). \ A writable pointee requires the `*mut T` \ opt-in.", name ), e.span, )); } } } } } // №387 (221.1, окно pptr-ro-guard): `E_POINTER_RO_MUT_METHOD` // was DECLARED (02-types.md, Plan 118) but never enforced — // a mut-receiver method (`fn T mut @m(...)`) called through a // pointer whose POINTEE is readonly (`*T`, not opted into // `*mut T`) compiled clean and, on the forms that codegen // happens to emit correctly, silently mutated the pointee // (repro: `mut c; ro p *Counter = &c; p.bump()` — check PASS, // build+run mutates `c`; see 221.1 №387 registry / PROGRESS // report for the two textual forms probed). NOT the same gap // as №375 (which closed *mut T MATERIALIZATION from a ro // SOURCE binding, `check_addrof_mut_from_ro_source` above) — // this is the pointee's OWN writability (independent of // where the pointer came from: an explicit `*T` annotation // over a genuinely `mut` source is legal Nova and #375 does // not touch it, see `d375_ptr_mut_from_mut_source_pos.nv`) // gating METHOD CALLS, mirroring the `.write()`-family gate // immediately above but keyed off "is `name` a REGISTERED // mut-receiver method of the pointee's nominal type" instead // of the fixed pointer-builtin name list — closes the gap for // ANY user/std mut-method reached through a raw-pointer // receiver, not just the built-in memory-write family. // // Scope: direct `p.method()` where `p`'s OWN inferred type is // a pointer (`pointee_is_writable`/`pointee_named_type` both // return `Some` only for `TypeRef::Pointer`) — a bare record // receiver (`c.method()`) is untouched here (`pointee_is_ // writable` returns `None`, no double-fire with the pre- // existing `E_LOCAL_NOT_MUT`/`E_PARAM_NOT_MUT` ConsumeCtx // checks for non-pointer receivers). A ro/mut OVERLOAD PAIR // on the pointee type (Plan 135 — a genuine ro-callable // overload exists at the same name) is NOT rejected — mirrors // `consume_walk_expr`'s `has_ro_overload` escape hatch for // the very same reason (an ro-source can still legally call // the RO overload of an overloaded pair). `(*p).method()` // (explicit deref) and a pointer reached through a field/ // index chain are OUT OF SCOPE (родня, not this fix — same // "not this window" split as №377/№349/№353/№35x in the // registry). if let ExprKind::Member { obj, name } = &func.kind { if !matches!( name.as_str(), "write" | "write_at" | "write_unaligned" | "write_volatile" | "copy_from" | "copy_from_nonoverlapping" ) { if let Some(ty) = self.infer_expr_type(obj, scope) { if let Some(false) = pointee_is_writable(&ty) { if let Some(TypeRef::Named { path, .. }) = pointee_named_type(&ty) { if let Some(pointee_name) = path.last() { if let Some(overloads) = self.method_overloads(pointee_name, name) { // Arity-scoped, NOT name-only: a Plan 135 ro/mut // OVERLOAD PAIR at the SAME name but DIFFERENT // arity (`@peek() -> int` ro-getter vs `mut // @peek(v int) -> ()` mut-setter — the pervasive // std fluent-getter/setter idiom, D117 amend) must // not blanket-suppress the mut arm just because a // ro sibling exists at another arity — that would // let `p.peek(99)` (the mut-arity call) through a // readonly pointer slip past silently. Mirrors // `consume_walk_expr`'s `mut_methods_arity`/ // `ro_methods_arity` (same file) and the // `outer_arity`/`inner_arity` matching a few // hundred lines below (~L43116) — arity is the // established overload-pair discriminator // throughout this checker, not just here. let same_arity: Vec<&&FnDecl> = overloads.iter() .filter(|f| f.params.len() == args.len()) .collect(); let any_mut = same_arity.iter().any(|f| { f.receiver.as_ref().map_or(false, |r| r.mutable) }); let any_ro = same_arity.iter().any(|f| { f.receiver.as_ref().map_or(false, |r| !r.mutable) }); if any_mut && !any_ro { errors.push(Diagnostic::new( format!( "[E_POINTER_RO_MUT_METHOD] cannot call \ mut-receiver method `.{name}()` through a \ readonly pointer — `*{pointee}` is a readonly \ pointee (the L3 default is `ro`: `*T ≡ *ro T`, \ Plan 147 / D246 / 174.5 §4; declared as \ `E_POINTER_RO_MUT_METHOD` since Plan 118, now \ enforced — 221.1 №387). A writable pointee \ requires the `*mut T` opt-in on the pointer's \ type (annotation or inference from a `mut` \ source, D216/D246 variant Б, 221.1 №375).", name = name, pointee = pointee_name, ), e.span, )); } } } } } } } } // Plan 221.1 №286/№143 (окно p-chan, real Channel[T] mono): // `ChanWriter[T].send(v)`/`.try_send(v)` — when `T` is // STATICALLY known (`channel_elem_type` — turbofish-declared // `Channel[T].new` or a `ChanWriter[T]`-annotated param/local; // `None` for an untracked bare `Channel.new`, a no-op exactly // like every other check this window adds), the argument must // be assignable to `T`. Before this window `Channel[int].new` // silently accepted `send("string")` — only the codegen SLOT- // SIZE guard (`E_CHANNEL_UNSOUND_ELEM_TYPE`) caught same-word- // size mismatches (e.g. `Meters`/`Seconds`), never a real // per-`T` check; that measured hole is №143/№286. Same // diagnostic shape/style as the pointer-writability gate just // above and the general arg-assignability check (~13451). if let ExprKind::Member { obj, name } = &func.kind { if matches!(name.as_str(), "send" | "try_send") { if let Some(elem_t) = self.channel_elem_type(obj, scope) { if let Some(arg) = args.first() { match self.assignable(arg.expr(), &elem_t, gs, gs, scope) { Compat::Bad { found } => { errors.push(Diagnostic::new( format!( "[E_CHANNEL_ELEM_TYPE_MISMATCH] cannot send a \ value of type `{}` into a channel declared \ `Channel[{}]` — the element type `{}` is now \ tracked end-to-end from `Channel[T].new` \ through `ChanWriter[T]`/`ChanReader[T]` (Plan \ 221.1 №143/№286); use a value of type `{}` or \ declare the channel with the actual payload \ type.", found, typeref_display(&elem_t), typeref_display(&elem_t), typeref_display(&elem_t), ), arg.expr().span, )); } Compat::OutOfRange { msg } => { errors.push(Diagnostic::new( format!("[E_LIT_OUT_OF_RANGE] {msg}"), arg.expr().span, )); } Compat::Narrowing { from, to } => { errors.push(Diagnostic::new( format!( "[E_IMPLICIT_NARROWING] cannot send value of \ type `{}` into a channel of narrower \ declared element type `{}` — implicit int \ narrowing loses range; use an explicit \ `... as {}` cast (D54)", from, to, to, ), arg.expr().span, )); } Compat::CoerceConflict { msg } => { errors.push(Diagnostic::new(msg, arg.expr().span)); } Compat::Ok | Compat::Unknown => {} } } } } } // 172.1.2 C6(a): closure-параметры типизируются сигнатурой callee // ДО рекурсии в args — тела замыканий (`x*2`) f1-обходятся с // типизированными параметрами → дети аннотируются каналом. let seeds = self.closure_arg_param_seeds(e, scope); for (i, a) in args.iter().enumerate() { if let Some((_, binds)) = seeds.iter().find(|(ix, _)| *ix == i) { let mut saved: Vec<(String, Option<TypeRef>)> = Vec::new(); for (n, t) in binds { saved.push((n.clone(), scope.insert(n.clone(), t.clone()))); } self.f1_expr(a.expr(), gs, scope, errors); // 172.1.2 C6b (2026-07-03): сам closure-arg аннотируется // R::Func{params: посеянные, ret: infer тела в ext-scope}. // Источник согласован с emit_lambda (hof_param_fn_sigs — // та же сигнатура callee по позиции) → рассинхрона нет. // Потребитель: Channel 2 closure-гейт (clos_struct_name). let ae = a.expr(); if ae.id.is_set() { if let ExprKind::ClosureLight { body: cb, .. } = &ae.kind { let ret_tr: Option<TypeRef> = match cb { ClosureBody::Expr(be) => self.infer_expr_type(be, scope) .or_else(|| { if !be.id.is_set() { return None; } let buf = self.resolved_types_buf.borrow(); let rt = buf.get(&be.id)?.clone(); drop(buf); Self::resolved_to_typeref_tp(&rt, be.span) }), ClosureBody::Block(_) => None, }; if let Some(rtr) = ret_tr { let ret_rt = Self::mark_type_params( ResolvedType::from_type_ref(&rtr), gs); let ps: Vec<ResolvedType> = binds .iter() .map(|(_, t)| Self::mark_type_params( ResolvedType::from_type_ref(t), gs)) .collect(); self.resolved_types_buf.borrow_mut().insert( ae.id, ResolvedType::Func { params: ps, ret: Box::new(ret_rt), effects: vec![], }, ); } } } for (n, prev) in saved { match prev { Some(t) => { scope.insert(n, t); } None => { scope.remove(&n); } } } } else { self.f1_expr(a.expr(), gs, scope, errors); } self.f4_check_value(a.expr(), scope, errors); } if let Some(t) = trailing { match t { Trailing::Block(b) => self.f1_block(b, gs, scope, errors), Trailing::LegacyBlockWithParams(tb) => { self.f1_block(&tb.body, gs, scope, errors) } Trailing::Fn(sb) => { self.f1_fn_sig_body(sb, gs, scope, errors) } } } self.f1_check_call( func, args, trailing.is_some(), gs, scope, errors, e.id, ); self.f5_check_tuple_construct(func, args, e.span, scope, errors); // Plan 172.1.2 [M-172.1-U4-recv-infer]: materialize a method/free-call's // inferred RETURN type into the checker channel (§0/§1 — receiver-inference). // The consumer (`infer_expr_c_type`) prefers `resolved_callees`+`fn_ret_by_span` // when BOTH are present (non-generic resolved callees → byte-identical), and // falls to THIS `resolved_types` annotation otherwise — generic instance methods // (callee span absent from codegen's `fn_ret_by_span`) and un-channeled chains // `a.b().c()`. A present `fn_ret_by_span` short-circuits before this is read, so it // can only ADD coverage, never diverge a flipped one. GATE on `gs`: do NOT channel // a return mentioning an in-scope type-param — in an ERASED generic body // `resolved_type_to_c` would need `current_type_subst` (gap #2 subst-timing // divergence); a bare `-> T` (which the container guard does not catch, T being a // bare `Named`) is left to legacy here. The deeper container/carrier guards live in // `resolve_instance_method_return` so the INLINE inference is conservative too. if e.id.is_set() { if let Some(watch) = std::env::var_os("NOVA_CALL_TRACE") { if let ExprKind::Member { name: mn, .. } = &func.kind { if *mn == watch.to_string_lossy() { let r = self.infer_method_call_channel_type(e, scope, gs); if r.is_none() { let ExprKind::Member { obj: ao, .. } = &func.kind else { unreachable!() }; let ok = match &ao.kind { ExprKind::Ident(n) => format!("i:{}({})", n, scope.contains_key(n)), ExprKind::SelfAccess => "@".into(), ExprKind::Member { name, .. } => format!("m:.{}", name), _ => "other".into(), }; eprintln!("[AP-MISS] id={:?} fid={:?} obj={}", e.id, e.span.file_id, ok); } } } } if let Some(tr) = self.infer_method_call_channel_type(e, scope, gs) { // 172.1.2 Шаг 3.1: gs-gate заменён на mark_type_params — // return с residual-параметром аннотируется ЯВНЫМ TypeParam // (лоуэринг: receiver-instance map → subst → Err → legacy). let rt = Self::mark_type_params( ResolvedType::from_type_ref(&tr), gs); self.resolved_types_buf.borrow_mut().insert(e.id, rt); } else if let ExprKind::Member { obj: mo, name: method } = &func.kind { // Plan 221.1 №286/№143 (окно p-chan, real Channel[T] mono): // `rx.recv()`/`rx.try_recv()`/`tx.share()` — same producer // logic as `infer_expr_type`'s dedicated Call-arm above // (kept independent rather than merged: this fn feeds the // CODEGEN `resolved_types` CHANNEL — Channel 2 — the other // feeds inline checker inference; `Some(x)`-ctor producer // just below is the byte-pattern this mirrors). `None` from // `channel_elem_type` (untracked bare `Channel.new`) is a // silent no-op — falls through to every OTHER arm in this // chain unchanged, then to legacy `infer_call_ret_c` exactly // as before this window. if matches!(method.as_str(), "recv" | "try_recv") && args.is_empty() { if let Some(elem_t) = self.channel_elem_type(mo, scope) { let opt = TypeRef::Named { path: vec!["Option".to_string()], generics: vec![elem_t], span: e.span, }; if !typeref_mentions_any(&opt, gs) { let rt = ResolvedType::from_type_ref(&opt); self.resolved_types_buf.borrow_mut().insert(e.id, rt); } } } else if method == "share" && args.is_empty() { if let Some(obj_ty) = self.infer_expr_type(mo, scope) { if matches!(&obj_ty, TypeRef::Named { path, generics, .. } if generics.len() == 1 && path.last().map(String::as_str) == Some("ChanWriter")) && !typeref_mentions_any(&obj_ty, gs) { let rt = ResolvedType::from_type_ref(&obj_ty); self.resolved_types_buf.borrow_mut().insert(e.id, rt); } } } // 172.1.2 (static-ctor канал, 2026-07-03): TurboFish-статик // (`Vec[u8].of(...)`) исключён из method-resolve producer'а, // а infer_expr_type его резолвит (ctor-армы + // resolve_generic_static_return) — аннотируем Call отсюда. if matches!(&mo.kind, ExprKind::TurboFish { .. }) { if let Some(tr) = self.infer_expr_type(e, scope) { let rt = Self::mark_type_params( ResolvedType::from_type_ref(&tr), gs); self.resolved_types_buf.borrow_mut().insert(e.id, rt); } } else if let ExprKind::Path(parts) = &mo.kind { // [M-196-rtbuf-producers] Q1/static-return producer: // `[]T` slice-sugar static receiver — parses to // `Member{obj: Path(["__array", elem]), name}` // (`generic_static_receiver`'s doc, D216-era helper) — the // SAME identity as `Vec[elem].method(...)` (mirrors the // TurboFish arm just above), but a DIFFERENT AST shape the // checker's static-ctor arms in `infer_expr_type` ALSO // already resolve for the ctor names (`new`/ // `with_capacity`/`from`/`default`/`filled`/`of`) — just // never channeled into `resolved_types_buf`, so every such // call fell through Channel-1/2 at emit time straight to // legacy `infer_call_ret_c`'s OWN `[]T`→`Vec[T]` re- // synthesis (a SEPARATE Expr with `id: ExprId::UNSET`, // which is why Channel 1/2 — both `expr.id.is_set()`-gated // — could never have covered it even if THIS site had // channeled: the synthesized node has no id to look up). // Channeling HERE, keyed by the ORIGINAL call's `e.id` // (still set), lets Channel 2 answer before the // re-synthesis path ever runs. Same `infer_expr_type` // source as the TurboFish arm — no new lowering logic. if parts.len() == 2 && parts[0] == "__array" { if let Some(tr) = self.infer_expr_type(e, scope) { let rt = Self::mark_type_params( ResolvedType::from_type_ref(&tr), gs); self.resolved_types_buf.borrow_mut().insert(e.id, rt); } } } else if let ExprKind::Member { obj: mod_obj, name: tyname } = &mo.kind { // [196.5 Stage-D волна-4] B11ag producer (module-qualified // static extern call, external_registry feeds the checker): // a D289 last-segment-qualified static call // `mod.Type.static_method(args)` (`raw_mem.RawMem. // alloc_uncollectable(8)`, spec_tests/conformance/standalone/ // d289_import_last_segment) parses to nested // `Member{obj: Member{obj: Ident(mod), name: Type}, name: // method}`. `infer_method_call_channel_type` bailed above // because `mod.Type` is a MODULE-NAMESPACE-qualified TYPE // reference, NOT a value expression — `infer_expr_type` // returns None for it, so no receiver type was recoverable // and the call's declared return never reached Channel 2 // (co-miss → legacy `infer_call_ret_c` B11ag extern-registry // arm). Resolve the static method's DECLARED return directly // off the type name (`resolve_generic_static_return` with an // EMPTY turbofish — these builtins are non-generic, so // `type_args.len() == recv.generics.len() == 0` gates cleanly // and the concrete `-> *mut u8` / `-> ()` return substitutes // with no residual). Gated: `mod` is an in-scope Ident (an // import alias / module head, not itself a value) and `tyname` // is a known type — a genuine value-receiver method call never // matches this Member-of-Member-of-Ident shape. if matches!(&mod_obj.kind, ExprKind::Ident(_)) && self.types.contains_key(tyname) { if let Some((tr, _node_subst, _fn_span)) = self.resolve_generic_static_return(tyname, method, &[], e.span) { if !typeref_mentions_any(&tr, gs) { let rt = ResolvedType::from_type_ref(&tr); self.resolved_types_buf.borrow_mut().insert(e.id, rt); } } } } else if method == "serialize" { // 196.5 Stage-D волна-5 (`[P67-LEGACY]`-adjacent B11ai // producer, Member-form mirror of the Path-form // `deserialize` producer below): inside a generic // container-conformance body (`fn[T Serialize] []T // @serialize[S Serializer](mut s S) { for v in @ { // v.serialize(s) } }`, std/src/encoding/serde/serde.nv) // the receiver `v` is the method's OWN still-abstract // type param, spelled as a bare `Named{path:["T"]}` // (Nova has no separate `TypeParam` TypeRef variant). // `resolve_instance_method_return_arity`'s protocol- // receiver branch (~14728) only fires when the // receiver's type NAME literally IS the protocol // (`w Writer`) — a receiver that is a GENERIC PARAM // BOUND BY a protocol never matches // (`self.types.get("T")` finds nothing; "T" is not a // registered type) → co-miss, legacy `infer_call_ret_c` // B11ai fallback (gated there on a codegen-side C // typedef probe, since the checker is pre-mono and // cannot see emitted C state). The `Serialize` // contract's `@serialize` return does NOT mention // `Self` (unlike `Deserialize`'s `Result[Self, // DeError]`) — it is receiver-INVARIANT // (`Result[(), SerError]` for every implementor) — so // no substitution is needed. Gated: receiver's TYPE // (not name) resolves to a bare `Named{path:[n]}` with // `n` an IN-SCOPE GENERIC of the enclosing decl // (`gs`) — a concrete receiver (even one literally // named "T", which user code never does) is read from // `self.types`/`method_overloads` by // `infer_method_call_channel_type` already tried above // and would have returned `Some` there, never reaching // this arm. if let Some(recv_ty) = self.infer_expr_type(mo, scope).or_else(|| { if !mo.id.is_set() { return None; } let buf = self.resolved_types_buf.borrow(); let rt = buf.get(&mo.id)?.clone(); drop(buf); Self::resolved_to_typeref_tp(&rt, e.span) }) { let mut peeled = &recv_ty; loop { match peeled { TypeRef::Readonly(i, _) | TypeRef::Mut(i, _) => { peeled = i; } _ => break, } } if let TypeRef::Named { path, generics, .. } = peeled { if path.len() == 1 && generics.is_empty() && gs.contains_key(&path[0]) { if let Some(td) = self.types.get("Serialize") { if let TypeDeclKind::Protocol { methods, .. } = &td.kind { if let Some(m) = methods.iter().find(|m| { m.name == "serialize" || m.name.trim_start_matches('@') == "serialize" }) { let ret = match &m.return_type { Some(r) => r.clone(), None => TypeRef::Unit(m.span), }; let self_only: HashSet<String> = std::iter::once("Self".to_string()) .collect(); if !typeref_mentions_any(&ret, &self_only) { let rt = Self::mark_type_params( ResolvedType::from_type_ref(&ret), gs, ); self.resolved_types_buf .borrow_mut() .insert(e.id, rt); } } } } } } } } else if is_raw_pointer_intrinsic_method(method) { // [M-196-rtbuf-producers] Q6/elem producer: raw-pointer // (`*T`/`*mut T`/`*ro T`) intrinsic method family // (read/write/offset/dist/copy_*, `is_raw_pointer_ // intrinsic_method` table, D216 §21) — the "elem" flavor // of a declared-return producer, generalized to the // POINTEE of a pointer rather than a container's element. // Mirrors legacy `emit_c.rs` B11d_typed_pointer_methods // (obj_ty C-string strip: `T*` → pointee `T`), but derives // the pointee from the CHECKER's own `TypedPtr` // representation — Nova's `TypeRef::Pointer` structurally // IS the pointee type at CHECK time, no C-string parsing // needed. Gated: receiver resolves to a genuinely CLOSED // (`rt_is_closed`) `TypedPtr` — an erased/still-abstract // pointee (generic body, e.g. `fn[T] unsafe fn foo(p *T) // { p.read() }`) falls through to legacy unchanged, same // discipline as every other `gs`-gated producer in this // cascade. `*()` (void*) pointee excluded (mirrors legacy's // `obj_ty != "void*"` guard — no defined pointee to read). if let Some(obj_tr) = self.infer_expr_type(mo, scope).or_else(|| { if !mo.id.is_set() { return None; } let buf = self.resolved_types_buf.borrow(); let rt = buf.get(&mo.id)?.clone(); drop(buf); Self::resolved_to_typeref_tp(&rt, e.span) }) { let obj_rt = Self::mark_type_params( ResolvedType::from_type_ref(&obj_tr), gs); if let ResolvedType::TypedPtr(_, inner) = &obj_rt { if !matches!(inner.as_ref(), ResolvedType::Unit) && self.rt_is_closed(&obj_rt) { let result_rt: Option<ResolvedType> = match method.as_str() { "read" | "read_unaligned" | "read_volatile" if args.is_empty() => { Some((**inner).clone()) } "read_at" if args.len() == 1 => { Some((**inner).clone()) } "write" | "write_unaligned" | "write_volatile" if args.len() == 1 => { Some(ResolvedType::Unit) } "write_at" if args.len() == 2 => { Some(ResolvedType::Unit) } "copy_from" | "copy_from_nonoverlapping" | "copy_to" | "copy_to_nonoverlapping" if args.len() == 2 => { Some(ResolvedType::Unit) } "offset" if args.len() == 1 => { Some(obj_rt.clone()) } "dist" if args.len() == 1 => { Some(ResolvedType::Scalar { width: 64, signed: true, wide_default: true, }) } _ => None, }; if let Some(rt) = result_rt { if std::env::var_os("NOVA_RTBUF_PTR_TRACE").is_some() { eprintln!( "[RTBUF-PTR] producer=Q6-typed-ptr id={:?} method={} rt={:?}", e.id, method, rt, ); } self.resolved_types_buf.borrow_mut().insert(e.id, rt); } } } } } } else if let ExprKind::Path(parts) = &func.kind { // 196.5 Stage-D волна-3 п.2 (P67-LEGACY panic // `[P67-LEGACY] Path call return type unknown for // method=deserialize`, серде-корпус): `T.deserialize(d)` // inside a generic fn/method body bound `[T Deserialize]` // (std/src/encoding/serde/{serde,json}.nv) — the receiver // is the function's OWN still-abstract type param, spelled // as a `Path` (the parser turns ANY PascalCase identifier // followed by `.lowercase_ident` into `Path`, not just // declared type names — `T` is never in `self.types`/ // `method_overloads`, so no existing static-return path // (`resolve_generic_static_return`, the primitive arm // above in `infer_expr_type`) sees it). The `Deserialize` // protocol contract fixes `.deserialize[D Deserializer] // (mut d D) -> Result[Self, DeError]` for EVERY // implementor (mirrors the already-channelled `Serialize` // contract, `Result[(), SerError]` — but here `Self` is // NOT invariant, so it is carried through as an explicit // `TypeParam` rather than hardcoded). Gated on `parts[0]` // being an in-scope generic name (`gs`) so a REAL // concrete-type static call is unaffected (those resolve // via the paths named above). `resolved_type_to_c`'s // `TypeParam` arm already knows how to resolve this from // `current_type_subst` at the mono instance's own // emission time (same contract every other TypeParam- // channelled return relies on) — no new lowering needed. if parts.len() == 2 && parts[1] == "deserialize" && gs.contains_key(&parts[0]) { // Return type read from the PROTOCOL DECLARATION (not // hardcoded): `Result[Self, DeError]` with `Self` → `T`. // If the CU has no `Deserialize` protocol in scope, or // its `deserialize` return still mentions the method's // own generics after substitution — no annotation // (honest fall-through to legacy, as before). if let Some(td) = self.types.get("Deserialize") { if let TypeDeclKind::Protocol { methods, .. } = &td.kind { if let Some(m) = methods.iter().find(|m| m.name == "deserialize") { if let Some(ret) = &m.return_type { let mut self_subst: HashMap<String, TypeRef> = HashMap::new(); self_subst.insert( "Self".to_string(), TypeRef::Named { path: vec![parts[0].clone()], generics: Vec::new(), span: e.span, }, ); let substituted = crate::const_fn_trampoline::subst_type_ref_pub( ret, &self_subst); let own_gens: HashSet<String> = m .generics .iter() .map(|g| g.name.clone()) .collect(); if !typeref_mentions_any(&substituted, &own_gens) { let rt = Self::mark_type_params( ResolvedType::from_type_ref(&substituted), gs, ); self.resolved_types_buf .borrow_mut() .insert(e.id, rt); } } } } } } // [196.5 Stage-D волна-4] B12c producer (intrinsic-scheme // mirror): `ChanReader.close_after(d Duration) -> ChanReader` // is a COMPILER BUILTIN — no `.nv` declaration anywhere // (std/src/concurrency/timer.nv:84's `ChanReader_close_after_ // doc_marker` is a documentation-only stand-in; the real // codegen dispatch is name-keyed, `emit_c.rs` ~34500/~37766/ // ~50862). `ChanReader` itself has no `.nv` type-decl either // (`BUILTIN_RUNTIME_TYPES`, defined in `nova_rt/*.h`) — the // checker's static-return paths never see it, so this // Path-form call-site never reaches Channel 2. Mirror the // SAME fixed return the codegen fallback (`B12c_path_ // chanreader_close_after`) already hardcodes: a bare // `ResolvedType::Named { name: "ChanReader" }` — the // catch-all arm of `resolved_named_to_c` (no protocol/alias/ // generic-template/colliding-name match) lowers ANY // unregistered concrete Named type to `Nova_{name}*`, // producing the IDENTICAL `Nova_ChanReader*` byte-for-byte. if parts.len() == 2 && parts[0] == "ChanReader" && parts[1] == "close_after" { if std::env::var_os("NOVA_B12C_LOCATE").is_some() { eprintln!("[B12C-CHECKER] id={:?} id_set={} span={:?}", e.id, e.id.is_set(), e.span); } self.resolved_types_buf.borrow_mut().insert( e.id, ResolvedType::Named { name: "ChanReader".to_string(), module: vec![], args: vec![], }, ); } } else if let ExprKind::TurboFish { base: tf_base, type_args } = &func.kind { // 172.1.2 (Call:<expr>): интринсики size_of[T]()/align_of[T]() // — ВСЕГДА int (фундаментальный факт; чекер уже знает их // спец-кейсами :2000/:2271/:16365, codegen — sizeof/_Alignof). if let ExprKind::Ident(tf_n) = &tf_base.kind { if tf_n == "size_of" || tf_n == "align_of" { self.resolved_types_buf.borrow_mut().insert( e.id, ResolvedType::Scalar { width: 64, signed: true, wide_default: true, }, ); } else if !scope.contains_key(tf_n) { // [M-196.5-node-substs] Producer D (Stage-B4): FREE-FN // turbofish `f[U](args)` — the explicit `type_args` ARE // the subst, directly (no inference needed, unlike // Producer A at `f1_check_call` ~10664, which infers from // ARGS via `unify_type` and so MISSES a turbofish call // whose generics don't surface in any param — e.g. a // zero-arg `d326_mono[D326Node]()`). Single-overload-by- // arity resolve mirrors `BoundCtx::check_call_bounds`'s // free-fn turbofish arm (same AST shape/resolution // strategy) — kept independent here rather than threaded // through `BoundCtx` (a separate checker pass with no // `node_substs` handle). if let Some(overloads) = self.sig.fn_decls.get(tf_n.as_str()) { let arity_matches: Vec<&&FnDecl> = overloads.iter() .filter(|f| f.params.len() == args.len()) .collect(); if let [callee] = arity_matches.as_slice() { // Decl order = `callee.generics` — a FREE fn has // no carrier, so ALL its generics are the D372 // "method-level" tier; the turbofish binds them // 1:1 positionally (D16). Completeness gate: // turbofish arity matches DECLARED arity, and // every type-arg is itself concrete (gs-gated) — // an outer-generic caller re-spelling its OWN // unbound param (`d16_identity[T](x)` inside a // generic body) must not materialize a fake // binding. if !callee.generics.is_empty() && type_args.len() == callee.generics.len() && type_args.iter().all(|t| !typeref_mentions_any(t, gs)) { let ordered: Vec<(String, ResolvedType)> = callee .generics .iter() .zip(type_args.iter()) .map(|(g, t)| { (g.name.clone(), ResolvedType::from_type_ref(t)) }) .collect(); if std::env::var_os("NOVA_NODE_SUBSTS_TRACE").is_some() { eprintln!( "[NODE_SUBSTS] producer=D-freefn-turbofish \ call_id={:?} callee={} n={}", e.id, callee.name, ordered.len(), ); } self.node_substs.borrow_mut().insert(e.id, ordered); } } } } } } else if let ExprKind::Ident(fname) = &func.kind { // P67 ФАЗА 2 (variant ctor `Some(x)` — int-collapse-relevant, gap driver): // channel `Option[type-of-x]` so the ctor expr's type PRESERVES payload // width (`Some(uint_var)` → `Option[uint]`, not legacy `Option[nova_int]`). // Bounded to `Some` (Result needs BOTH Ok+Err → contextual). Payload from // the SIZED arg type via `infer_expr_type` (concrete scope); gs-gated; not // when `Some` is shadowed by a local. The Option WRAPPER type is concrete // (no context coercion of the payload — Nova has no implicit int-widening, // so `type-of-x` is the sound payload). §1 «материализуй резолв». if fname == "Some" && args.len() == 1 && !scope.contains_key(fname) { if let Some(payload_tr) = self.infer_expr_type(args[0].expr(), scope) { let opt = TypeRef::Named { path: vec!["Option".to_string()], generics: vec![payload_tr], span: e.span, }; if !typeref_mentions_any(&opt, gs) { let rt = ResolvedType::from_type_ref(&opt); self.resolved_types_buf.borrow_mut().insert(e.id, rt); } } } else if matches!( fname.as_str(), "println" | "print" | "assert" ) && !scope.contains_key(fname) { // [196.5 Stage-D волна-4] B10a producer (intrinsic-scheme // mirror): `println`/`print`/`assert` are // ALWAYS-Unit-returning compiler intrinsics — `extern "nova" // fn ... -> ()` in std/prelude/runtime.nv, but // `#no_prelude` modules (breaking the prelude→string→ // prelude import cycle, `[panic-assert-intrinsic]` above) // never see that declaration reachable, so no static- // return path materializes Channel 2 for these call-sites // there. codegen's `emit_call` already dispatches all three // NAME-KEYED regardless of declaration visibility // (`emit_c.rs` ~32213 println/print, // panic-assert-intrinsic doc for assert) — // mirror that SAME fact into the checker channel // unconditionally (Unit is invariant for these names // regardless of which declaration, if any, is visible; // in prelude-having modules this is a no-op re-write of // the same Unit the extern decl already gave). Legacy // `infer_call_ret_c` B10a_ident_println_assert arm remains // the fallback for any CU shape this producer misses. // Plan 194 A4: `debug_assert` retracted (role absorbed by // `#debug assert`, A2.2) — removed from this NAME-set. self.resolved_types_buf .borrow_mut() .insert(e.id, ResolvedType::Unit); } else if !scope.contains_key(fname) { // [M-196-rtbuf-producers] Q1/static-return producer: // `TypeName(args)` newtype/named-tuple CONSTRUCTOR call // (bare `Ident` callee resolving to a registered type, not // a fn) — mirrors legacy B10h_newtype_constructor / // B10l_named_tuple_constructor (both keyed off codegen's // OWN `type_aliases` C-string table). The checker's type // registry (`self.types`) already knows a bare name is one // of these two ctor-shaped kinds WITHOUT any C-string // lookup — the call's result is simply the nominal type // itself (`Named{name, args:[]}`); `resolved_type_to_c` // resolves the concrete C representation for either kind // by name the same way every other Channel-2-covered // constructor already does (record/sum/generic ctors). // Checked FIRST (before the free-fn lookup below): a type // name and a free-fn name never collide in Nova's // namespace, but checking type identity first is the // cheaper, more direct match for a ctor call. // [fix M-bare-variant-name-resolves-to-unrelated-type-in-cu, // реестр 221.1 №136]: a bare `fname` can ALSO legitimately be // a sum-variant constructor name (`Tagged(kind)` meaning // `SumRepr.Tagged`) — `self.types` only ever stores TOP-LEVEL // type declarations (a variant name is nested inside its // owning `Sum`'s variant list, never a separate `self.types` // entry), so an UNRELATED Newtype/NamedTuple sharing that bare // name (`type Tagged[T,U](int)` in a different, also-imported // file) silently won this producer unconditionally — the // channel recorded `Named{Tagged}` (the newtype), codegen's // OWN independent resolution then built the SUM's constructor // call text (see emit_call's `debt_find_variant_ctx` guard, // fixed the same window) — the TWO disagreed, one CC-FAIL // traded for another. Guard: skip the newtype/tuple producer // when `fname` is ALSO a variant name of some Sum visible in // this module's own `self.types` (imports included) — the // variant-constructor arms elsewhere in this match already // handle that shape correctly. Byte-identical when no sum in // scope declares a variant of this bare name. let shadows_sum_variant = self.types.values().any(|td| { matches!(&td.kind, TypeDeclKind::Sum(variants) if variants.iter().any(|v| v.name == *fname)) }); let is_newtype_or_tuple_ctor = !shadows_sum_variant && self.types.get(fname.as_str()) .map(|td| matches!(&td.kind, TypeDeclKind::Newtype(_) | TypeDeclKind::NamedTuple(_))) .unwrap_or(false); if is_newtype_or_tuple_ctor { self.resolved_types_buf.borrow_mut().insert( e.id, ResolvedType::Named { name: fname.clone(), module: vec![], args: vec![], }, ); } else if let Some(overloads) = self.sig.fn_decls.get(fname.as_str()) { // Q1/static-return producer: bare free-fn call // (`name(args)`) — channel the callee's OWN DECLARED // return type directly, mirroring legacy `user_fn_sigs` // (emit_c.rs B10f_user_fn_sigs doc: "registered ONLY // for non-generic free fns — the authoritative source // for a bare call's return type"). Gated: exactly ONE // arity-matching overload (same single-candidate // discipline as the free-fn-turbofish Producer D above) // and NO generics (a generic free fn's return may // depend on inferred/turbofish type-args — that is // Producer D's/B10j's job, not this plain declared- // return producer) — an ambiguous or generic callee is // honestly left to legacy. let arity_matches: Vec<&&FnDecl> = overloads.iter() .filter(|f| f.generics.is_empty() && f.params.len() == args.len()) .collect(); if let [callee] = arity_matches.as_slice() { let rt = match &callee.return_type { Some(ret_tr) if !typeref_mentions_any(ret_tr, gs) => { Some(ResolvedType::from_type_ref(ret_tr)) } Some(_) => None, // D45: no `-> T` annotation. Block/external body // → `nova_unit` is the spec-guaranteed reading // (03-syntax.md D45 "Реализация": block-body // без аннотации → nova_unit, inference in // block-body explicitly rejected by design). // Expr-body (`=> expr`), however, INFERS its // real return from the expression itself — this // producer doesn't do that inference, so it must // stay silent (None) rather than lie Unit and // shadow the true inferred type downstream. None => match &callee.body { FnBody::Expr(_) => None, _ => Some(ResolvedType::Unit), }, }; if let Some(rt) = rt { self.resolved_types_buf.borrow_mut().insert(e.id, rt); } } } } else if let Some(var_ty) = scope.get(fname).cloned() { // [Plan 228 Ф.2(a) producer, реестр 221.1 №94-v2] Calling a // SCOPE-BOUND LOCAL whose type is itself callable — a bare // `fn(...)->...` value OR a fn-newtype/alias name that peels // (through `self.types`, mirrors emit_c's `resolve_fn_typeref`) // to one (`ro m Mid = identity_mw; ro h2 = m(h)`, №78/№90 // family). Every arm above this one is explicitly gated // `!scope.contains_key(fname)` (constructor/free-fn producers, // which by construction never apply to a local) — a call whose // callee IS a scope binding fell through ALL of them silently: // `resolved_types_buf`/`resolved_types` (196.4 channel) was // NEVER written for this call shape at all, so Ф.2(a)'s unified // emit-side HOF-binding registration (reading // `resolved_types[decl.value.id]`) had nothing to read for // exactly the forms it targets. Donating the producer here // (not emit) per compiler-conventions §0: the checker already // knows `fname`'s scope type; peeling it through a newtype/ // alias name to find the declared `Func` shape is the SAME // call-through `resolve_fn_typeref` performs at emit-time — // no new inference, just not thrown away. if let Some(TypeRef::Func { return_type, .. }) = self.resolve_fn_newtype_typeref(&var_ty) { let ret = return_type.map(|b| *b) .unwrap_or_else(|| TypeRef::Unit(e.span)); if !typeref_mentions_any(&ret, gs) { let rt = ResolvedType::from_type_ref(&ret); self.resolved_types_buf.borrow_mut().insert(e.id, rt); } } } } } } ExprKind::TurboFish { base, .. } => { self.f1_expr(base, gs, scope, errors) } // Plan 172.5 (D326 R4): call-site `ref <place>` — transparent for // the type-annotation walk; the place's own type flows through. A // `RefArg` node ALWAYS marks a `mut ref` argument (BoundCtx enforces // marker⟺mode), so this is the single point where the borrowed place // is required to be mutable (E_REF_ARG_NOT_MUT), reusing the // assignment-target readonly machinery that lives on this checker. ExprKind::RefArg(inner) => { self.f1_expr(inner, gs, scope, errors); self.check_ref_marker_mutability(inner, scope, errors); } ExprKind::As(inner, cast_ty) => { self.f1_expr(inner, gs, scope, errors); // **№375 (D216 §4 AMEND / D246, Plan 118.6 restored, owner // decision 2026-08-06, spec commit 96100421e, window // p375-ptr2):** `(&place) as *mut T` / `(raw &place) as *mut // T` — an address-of expression cast to `*mut T` over the // SAME pointee `T` it already syntactically addresses — is a // RETIRED operator form — same family as the other // D216/174.5 operator retractions (`E_POINTER_OP_USE_METHOD`: // `p[i]=v`/`*p=v`/`p+i`/`p<q`). `&x` now infers `*mut T` // automatically from a `mut`-bound source (118.6 restored // above), so this cast is either (a) a redundant // re-assertion (source already `*mut T`) or (b) the ro→mut // escalation bypass the guide itself used to teach (`&ro_x` // infers the readonly `*T`; casting THAT to `*mut T` was the // one path №375 found still open) — both retired, full stop, // regardless of the source's own L1 binding. // // Deliberately scoped to a DIRECT `&`/`raw &` INNER syntax // only — not to any already-pointer-typed expression in // general. `Vec[T].new(ptr *T, len) -> ro Self`'s documented // VIEW-constructor internal (`unsafe { ptr as *mut T }`, // std/src/collections/vec/core.nv:119) reinterprets a // CALLER-SUPPLIED `*T` PARAMETER inside an `unsafe fn` whose // doc comment already places the safety obligation on the // call site — not an address-of-a-ro-binding bypass at all // (no local binding here to make `mut`, no // annotation/inference-only rewrite exists for a param). // Every `&x`-derived example this window is given (guide // lines 194/316, coordinator's own repro) is the direct // address-of shape; the general parameter-reinterpret idiom // stays the separate, pre-existing `unsafe fn` question this // window does not retract. if pointee_is_writable(cast_ty) == Some(true) && matches!(&inner.kind, ExprKind::Unary { op: UnOp::AddrOf | UnOp::RawAddrOf, .. }) { if let Some(src_ty) = self.infer_expr_type(inner, scope) { if pointee_is_writable(&src_ty).is_some() { let same_pointee = match ( pointee_named_type(&src_ty), pointee_named_type(cast_ty), ) { (Some(s), Some(t)) => typeref_equal(&s, &t), _ => false, }; if same_pointee { errors.push(Diagnostic::new( "[E_POINTER_OP_USE_METHOD] cast `... as *mut T` \ re-asserting a pointer's mutability over the SAME \ pointee type is retired (D216 §4 AMEND / D246, Plan \ 118.6 restored, owner decision 2026-08-06, №375) — a \ writable `*mut T` pointer is obtained ONLY by taking \ the address of a `mut`-bound source directly \ (`&mut_x` now infers `*mut T` on its own, no cast \ needed); a `ro`-bound source can never produce a \ writable pointer via ANY path (inference / \ annotation / param / field / return / cast). Fix-it: \ take the address from a `mut`-bound binding — \ `&mut_x` — instead of casting." .to_string(), e.span, )); } } } } // [M-option-int-cast-u64-cc-fail] (ICE-пачка п.7): `expr as // <numeric>` never validated that `expr`'s OWN type is // actually scalar-cast-compatible — a source expression whose // type is `Option[T]`/`Result[T,E]` (a NovaOpt/NovaRes C // STRUCT, never a bare scalar) passed straight through to // codegen, which just emits a blind C cast — clang then // rejects the struct operand ("operand of type 'NovaOpt_...' // where arithmetic or pointer type is required" / // "passing 'NovaOpt_...' to parameter of incompatible type"). // Repro class: `x.to_int()` (returns `Option[int]`, a CHECKED // conversion) cast directly `as u64` WITHOUT unwrapping first // — `(x.to_int() as u64)` — silently accepted by the checker, // CC-FAIL at codegen. Catch it here: a numeric-scalar cast // TARGET whose SOURCE resolves to `Option[..]`/`Result[..,..]` // is never valid — the correct spelling requires an explicit // unwrap (`match`/`??`/`!!`/`?`) before the numeric cast. if let Some(src_ty) = self.infer_expr_type(inner, scope) { if let TypeRef::Named { path: src_path, .. } = src_ty.strip_modifiers() { if src_path.len() == 1 && matches!(src_path[0].as_str(), "Option" | "Result") { if let TypeRef::Named { path: dst_path, generics: dst_gens, .. } = cast_ty.strip_modifiers() { if dst_path.len() == 1 && dst_gens.is_empty() && matches!(dst_path[0].as_str(), "int" | "uint" | "i8" | "i16" | "i32" | "i64" | "u8" | "u16" | "u32" | "u64" | "f32" | "f64" | "bool" | "char") { errors.push(Diagnostic::new( format!( "[E_CAST_UNWRAP_REQUIRED] cannot cast \ `{}` directly `as {}` — the source is \ `{}`, not a scalar; the checked-\ conversion result must be unwrapped \ FIRST (`match`/`??`/`!!`/`?`), THEN \ cast. Example: `match x.to_int() {{ \ Some(v) => v as {t}, None => ... }}` \ or `(x.to_int() ?? 0) as {t}`.", src_path[0], dst_path[0], src_path[0], t = dst_path[0], ), e.span, )); } } } } } // Plan 172.1 §0a (As): materialize the cast target type into the channel. // `infer_expr_type` already returns the `ty` for `As` — wire it into // `resolved_types_buf` so codegen reads the CAST TYPE, not a re-derive. // gs-gated: a `T as U` where U mentions a type-param stays on legacy // (U in generic body → resolved_type_to_c needs current_type_subst). if e.id.is_set() { if let Some(tr) = self.infer_expr_type(e, scope) { if !typeref_mentions_any(&tr, gs) { let rt = ResolvedType::from_type_ref(&tr); self.resolved_types_buf.borrow_mut().insert(e.id, rt); } } } } ExprKind::Is(inner, ty) => { self.f1_expr(inner, gs, scope, errors); // Plan 174.3 (D54 §6): `is` is defined only on `any` (v1 runtime // type_id downcast) and on sum-values (v2 variant check). On a // record / primitive operand it is a compile error — the type is // statically known, so the check is meaningless. Clean diagnostic // with code+span (not a downstream CC-FAIL). The `is` result stays // `bool` (seeded by number_exprs). Conservative: fire ONLY when the // operand type is confidently a concrete non-`any`, non-sum type // (§7 — zero false positives on legal code). self.check_is_operand(inner, ty, gs, scope, errors); } ExprKind::Binary { left, right, op } => { self.f1_expr(left, gs, scope, errors); self.f1_expr(right, gs, scope, errors); self.f4_check_value(left, scope, errors); self.f4_check_value(right, scope, errors); // Plan 172.1.1 (U.4.5 — Binary arm): materialize the binary expr's resolved type // into the channel so codegen READS it instead of re-deriving via legacy. // `infer_expr_type` has NO Binary arm (→ None), so compute the result type INLINE // here (keeps the change isolated to this annotation — does NOT alter // infer_expr_type's other consumers): // - comparison/logical/implication → `bool` (byte-identical to legacy `nova_bool`); // - arithmetic/bitwise/shift → the PRIMITIVE-PROMOTED operand type via the shared // `number_exprs::promote_arith_rt` (§0 — same rule as the seed + legacy): a // typed/narrow int (u8/i16/u32/uint…) or f64 beats wide `int` REGARDLESS of // operand position. Plan 172.1.1 RANK 1 fix: the former `infer(left).or_else(right)` // was positional luck — `2 * a` (a:u8) stamped `int` because the literal-LEFT // short-circuited, dropping the u8 width (uint≠int, u8≠int). Annotate ONLY when // BOTH operands infer; an un-inferrable operand → None → falls to legacy (which // has Member/Index arms). Non-numeric operands (operator-overload `@plus`, str, // generic `T`) keep the prior left-operand annotation (codegen resolves the // overload return / mono-substitutes `T` via `current_type_subst`). if e.id.is_set() { use crate::ast::BinOp; // 172.1.2 (None-операнд сравнения): `x == None` — тип None // ИЗВЕСТЕН из другого операнда (сравнение требует один тип): // Option[..] из infer/канала → аннотируем None-Ident. if matches!(op, BinOp::Eq | BinOp::Neq) { let none_side = |a: &Expr, b: &Expr| -> Option<()> { if !matches!(&a.kind, ExprKind::Ident(n) if n == "None") { return None; } if !a.id.is_set() || scope.contains_key("None") { return None; } // ТОЛЬКО канал (buf — прошёл гейты продюсеров; scope-infer // здесь ловил int-collapse) + гейт payload'а: конкретный // НЕ-wide-default (Option[int] от литерала — недостоверен). let other: Option<ResolvedType> = if b.id.is_set() { self.resolved_types_buf.borrow().get(&b.id).cloned() } else { None }; if let Some(rt) = other { // wide-default int payload допустим, если операнд — // НЕ литеральный Some-ctor (тот даёт контекстно-слепой // Option[int]); resolve-производные (m.get→Option[int]) // достоверны (declared V subst). let b_is_ctor = matches!(&b.kind, ExprKind::Call { func, .. } if matches!(&func.kind, ExprKind::Ident(f) if f == "Some")); let payload_ok = matches!(&rt, ResolvedType::Named { name, args, .. } if name == "Option" && args.len() == 1 && !(b_is_ctor && matches!(&args[0], ResolvedType::Scalar { wide_default: true, .. })) && (Self::primitive_gate(&args[0]) || matches!(&args[0], ResolvedType::Str) || matches!(&args[0], ResolvedType::Named { args: a2, .. } if a2.is_empty()))); if payload_ok { self.resolved_types_buf.borrow_mut().insert(a.id, rt); } } Some(()) }; let _ = none_side(left, right).or_else(|| none_side(right, left)); // [M-option-eq-some-literal-elem-adapt] (реестр 221.1 №364, // 2026-08-05): sibling of `none_side` above, for a LITERAL- // payload `Some(0)` compared against an operand whose type is // known+concrete (`limbs.last() == Some(0)`). Root cause // (confirmed, NOT the diagnosis's original hypothesis): the // literal-coercion channel's ctor arm (`materialize_literal_ // coercion`, `ctor_payload_expected`, ~16960 below) IS wired // at every other definite site (return/let/call-arg/ // turbofish) — Binary comparison was simply the one missing // call site. But ALSO, independently, the P67 Ф.2 `Some(x)` // producer (~9652 above, `ExprKind::Call` arm) unconditionally // stamps `resolved_types_buf[ctor_call.id]` from the // literal's OWN un-adapted seed type (`int`, wide-default) // the moment `f1_expr` walks the ctor call as a child of // this Binary node — BEFORE this code runs. So the ctor // call already carries a (wrong) buf entry; simply calling // the channel here still fixes it, because `materialize_ // literal_coercion`'s ctor arm OVERWRITES `resolved_types_ // buf[value.id]` (`insert`, not `entry().or_insert()`) once // `ctor_payload_expected` confirms the ctor/expected shape // matches — no separate P67-producer change needed. // Symmetric (the literal ctor may sit on either side of // `==`/`!=`). `other_ty` is read via `infer_expr_type` // (mirrors the `As` arm's own use of it, ~9895) rather than // straight off `resolved_types_buf` like `none_side` does — // `infer_expr_type`'s own `Call` arm (~19904) already reads // BACK `resolved_types_buf` first for any general method/fn // call, so this is a strict superset (covers Ident-from- // scope, Member, etc. too), not a weaker source. gs-gated // (`typeref_mentions_any`) exactly like every OTHER // `materialize_literal_coercion` call site — the shared // `ConcreteNamedNoArgs` gate this fn's ctor arm uses // structurally cannot tell a bare generic-param `Named("T")` // apart from a genuine concrete declared type, so callers // are the ones responsible for excluding in-scope generics. // // SCOPED TO `Some` ONLY (Option, ONE generic slot) — `Ok`/ // `Err` (Result, TWO generic slots) tried and REVERTED: this // channel write only ever supplies the slot the literal // itself carries (`E` for `Err(0)`, `T` for `Ok(0)`); the // OTHER (unsupplied) slot has no source here and stays on // whatever `expected`'s generics say — which IS structurally // correct (both slots concrete from `other_ty`) at the // CHECKER layer. But `emit_c`'s `Ok`/`Err` ctor emission // (`emit_c.rs` ~38169, gated on `self.current_fn_return_ty`) // does NOT consult `resolved_types_buf` for the ctor call's // own two-generic type outside return-position at all — for // the unsupplied slot it falls through to a hardcoded // default (`Err` → Ok-slot `nova_int`; `Ok` → Err-slot // `nova_str`) regardless of what the channel says. Verified // (fresh-dir A/B, baseline vs patched): `Result[str, u32] == // Err(0)` — baseline BUILDS (an unresolved ctor node routes // codegen down a different, untyped comparison path); // WITH `Ok`/`Err` wired into this arm, it CC-FAILs // (`passing 'nova_int' to incompatible type 'nova_str'`) — // making the channel entry "more correct" for the CHECKER // paradoxically REGRESSES this codegen consumer, because the // consumer only reads it well enough to pick the (broken) // hardcoded-default path instead of its prior safe fallback. // A real fix needs `emit_c`'s ctor emission extended to // consult the channel for BOTH slots — an `emit_c` change, // out of this window's channel-only mandate; left as a // separate, deeper finding (see window report). `Option` has // no such "other slot" — its ONE generic is always exactly // the literal's own (channel-corrected) type, so this class // of regression structurally cannot occur for `Some`. if matches!(op, BinOp::Eq | BinOp::Neq) { let ctor_side = |ctor_expr: &Expr, other: &Expr| -> Option<()> { let ExprKind::Call { func, args, .. } = &ctor_expr.kind else { return None; }; if args.len() != 1 { return None; } let ExprKind::Ident(ctor) = &func.kind else { return None }; // `Some` only — see doc-comment above (Ok/Err // reverted: regresses emit_c's Result ctor codegen). if ctor != "Some" || scope.contains_key(ctor.as_str()) { return None; } let other_ty = self.infer_expr_type(other, scope)?; if typeref_mentions_any(&other_ty, gs) { return None; } ctor_payload_expected(ctor, &other_ty)?; self.materialize_literal_coercion(ctor_expr, &other_ty); Some(()) }; let _ = ctor_side(left, right).or_else(|| ctor_side(right, left)); } } let res_rt: Option<ResolvedType> = match op { BinOp::Eq | BinOp::Neq | BinOp::Lt | BinOp::Le | BinOp::Gt | BinOp::Ge | BinOp::And | BinOp::Or | BinOp::Implies | BinOp::Iff => { Some(ResolvedType::Bool) } _ => { let is_num = |rt: &ResolvedType| { matches!( rt, ResolvedType::Scalar { .. } | ResolvedType::Float { .. } ) || matches!(rt, ResolvedType::Named { name, args, .. } if args.is_empty() && name.as_str() == "char") }; // Plan 196 Ф.4a (Binary-Join): the arithmetic PROMOTION RULE now // lives in the constraint solver (`Constraint::Join`, which delegates // to the §0-canonical `number_exprs::promote_arith_rt` — ONE source, // no re-derivation). This producer keeps only the CONTEXTUAL gates // (numeric / same-bounded-TypeParam / IntLit-coercion), exactly as // Ф.2 (literal-coercion) kept the AST traversal in the producer while // moving the type-set membership predicate into `Constraint::MemberOf`. // Byte-parity: `join` returns EXACTLY what the prior inline // `promote_arith_rt` / `TypeParam` results returned for every gated // input (verified — solver's Join is a thin wrapper over the same rule). let join = |l: &ResolvedType, r: &ResolvedType| -> Option<ResolvedType> { use crate::types::constraint_solver::{Constraint, Solver, Ty, VarGen}; let mut g = VarGen::new(); let out = g.fresh(); Solver::new() .solve(&[Constraint::Join { out: Ty::Var(out), left: Ty::from_resolved(l), right: Ty::from_resolved(r), }]) .ok() .and_then(|sol| sol.type_of(out)) }; // P67 ФАЗА 2 (Binary residual — gap driver #3, 12% of fall-through): // recover an operand RT from the CHANNEL when `infer_expr_type` can't // reach it (Member-field / Index-elem / Call operands — its arm-set has // no such arms). The children were annotated BEFORE this parent in // `f1_expr`, so `resolved_types_buf[operand.id]` is already populated. // RESTRICTED to NUMERIC operands → the result is a safe promoted // primitive (no generic/mono → no §0.95 subst-timing / GC-layout // perturbation); a non-numeric channel operand stays on legacy. This // ALSO fixes int-collapse for sized-field arith (`rec.u8field * 2` → // u8-promoted, not the legacy `_=>nova_int`). §1 «материализуй резолв». let operand_rt = |e: &Expr| -> Option<ResolvedType> { self.infer_expr_type(e, scope) .map(|t| ResolvedType::from_type_ref(&t)) .or_else(|| self.resolved_types_buf.borrow().get(&e.id).cloned()) }; let numeric = match (operand_rt(left), operand_rt(right)) { (Some(l), Some(r)) if is_num(&l) && is_num(&r) => join(&l, &r), // 172.1.2 Binary-bounds: ОДИНАКОВЫЙ numeric-bounded // TypeParam с обеих сторон → тот же TypeParam (Join видит // равные TypeParam-листы). D46-риск снят bound'ом (примитивы // type-set — operator-overload невозможен, арифметика // сохраняет тип операнда; D405 запрещает mixed-width). ( Some(ResolvedType::TypeParam(a)), Some(ResolvedType::TypeParam(b)), ) if a == b && self.numeric_bounded_params.borrow().contains(&a) => { join( &ResolvedType::TypeParam(a.clone()), &ResolvedType::TypeParam(b), ) } // TypeParam + литерал-операнд (литерал коэрсится к типу // параметра, D55) — моделируем как Join(T, T): литерал // берёт тип параметра, Join даёт тот же TypeParam. (Some(ResolvedType::TypeParam(a)), _) if matches!(&right.kind, ExprKind::IntLit(_)) && self.numeric_bounded_params.borrow().contains(&a) => { join( &ResolvedType::TypeParam(a.clone()), &ResolvedType::TypeParam(a), ) } (_, Some(ResolvedType::TypeParam(b))) if matches!(&left.kind, ExprKind::IntLit(_)) && self.numeric_bounded_params.borrow().contains(&b) => { join( &ResolvedType::TypeParam(b.clone()), &ResolvedType::TypeParam(b), ) } _ => None, }; // 2026-07-02 (audit POISON 6875): прежняя non-numeric ветка // `else { Some(l) }` («тип результата = тип ЛЕВОГО операнда») // УДАЛЕНА — §1-нарушение: возврат @-оператора в Nova СВОБОДНЫЙ // (D46, нормативный пример `fn Vector @times(other Vector) -> f64`), // штамп Named{Vector} для `v1*v2` — ложь в канале, требующая // парного недоверия потребителя (анти-паттерн fd3207d6). // Operator-overload операнды → БЕЗ аннотации → legacy-навигация // (резолв возврата overload'а — U.1-семейство, не guess). numeric.or_else(|| match ( self.infer_expr_type(left, scope), self.infer_expr_type(right, scope), ) { (Some(l_tr), Some(r_tr)) => { let l = ResolvedType::from_type_ref(&l_tr); let r = ResolvedType::from_type_ref(&r_tr); if is_num(&l) && is_num(&r) { join(&l, &r) } else { None } } _ => None, }) } }; // D263 fix (2026-07-02): NO int-fallback here. The former // `res_rt.or(Scalar{int})` violated §5 (запрет молчаливого fallback-типа // в НОВОМ пути) and was NOT byte-identical to legacy: for operator-overload // operands (`Vec + Vec` → `@plus`) legacy infers the LEFT operand's C-type, // while the poisoned channel stamped `nova_int` — downstream `ro c = a + b` // then bound `c: nova_int` and `c[0]` hit the P67 Index panic. Unresolved // operands now stay UN-annotated → legacy fall-through (tally, not lie). if let Some(rt) = res_rt { self.resolved_types_buf.borrow_mut().insert(e.id, rt); } // p-op-w1b spike (owner decision 2026-08-02, // docs/plans/wip/196-op-channel-progress.md рекомендация 1): // resolve the operator-overload CALLEE (WHICH FnDecl) for a // Heterogeneous binop (`- << >>`, D46/03-syntax.md, // operator_dispatch::OperandShape::Heterogeneous) on a // CONCRETE (non-generic, non-mono) `Nova_T*` receiver — the // narrowest, least-risky slice of the core channel // migration. Writes ONLY `resolved_callees` (which FnDecl // codegen should call) — the `res_rt` result-type inference // above is COMPLETELY untouched (audit POISON 6875 / D263 // guard: no shared state, no reordering, purely additive). // Homogeneous ops (`+ * / % & | ^`) are NOT covered — the // checker already guarantees a single Self-overload exists // for those (карта: канал там пустая формальность). // Comparisons (`== != < <= > >=`) are a SEPARATE protocol // chain (`@equal`/`@compare`) — deliberately out of scope // here (шаг 2). if matches!(op, BinOp::Sub | BinOp::Shl | BinOp::Shr) { if let Some(op_method) = crate::codegen::operator_dispatch::binop_method_name(*op) { if let Some(l_tr) = self.infer_expr_type(left, scope) { if let TypeRef::Named { path, generics, .. } = &l_tr { if path.len() == 1 && generics.is_empty() { let type_name = path[0].as_str(); if !is_primitive_recv_name(type_name) { if let Some(overloads) = self.method_overloads(type_name, op_method) { let compat: Vec<&FnDecl> = overloads .iter() .copied() .filter(|f| { matches!( f.receiver.as_ref(), Some(r) if r.kind == ReceiverKind::Instance && r.generics.is_empty() ) && f.generics.is_empty() && f.params.len() == 1 && !f.params[0].is_variadic && !matches!( self.assignable( right, &f.params[0].ty, gs, &fn_generic_scope(f), scope, ), Compat::Bad { .. } ) }) .collect(); if let [single] = compat.as_slice() { self.resolved_callees .borrow_mut() .insert(e.id, single.span); } } } } } } } } } } ExprKind::Unary { op, operand } => { // №367 (window p375-ptr2, D216/174.5 read-parity): consume // the assign-target marker BEFORE recursing (see // `assign_target_top`'s field doc) — a nested Deref reached // through `operand` below must see it already cleared. let is_assign_target_top = self.assign_target_top.replace(false); self.f1_expr(operand, gs, scope, errors); self.f4_check_value(operand, scope, errors); // №367: `*p` DEREF READ on a raw pointer — the READ-form // sibling of the already-closed WRITE retraction (`*p = v`, // №353, `check_target_readonly`'s Deref arm). `nova check` // never enforced this for the READ form — only codegen did // (emit_c.rs, C-type-string based) — so `x = *p` / any other // rvalue use of `*p` on a raw pointer passed `nova check` // clean. Skipped when THIS node is the immediate assignment // TARGET (`*p = v`) — already diagnosed (as a WRITE) by // `check_target_readonly`; the exclusion prevents a // redundant/misleadingly-worded second diagnostic on the // same span. if !is_assign_target_top && matches!(op, UnOp::Deref) { if let Some(operand_ty) = self.infer_expr_type(operand, scope) { if pointee_is_writable(&operand_ty).is_some() { errors.push(Diagnostic::new( "[E_POINTER_OP_USE_METHOD] operator `*p` (deref read) on \ raw pointer retired (Plan 174.5 §3/§9, D216 amend, №367 \ read-parity) — use `p.read()`" .to_string(), e.span, )); } } } // Plan 172.1.1 (U.4.5 — Unary arm): materialize the unary expr's resolved type. // `infer_expr_type(e)` dispatches by UnOp: Neg/Not → operand type; Deref → // pointee (strip Pointer wrapper); AddrOf/RawAddrOf → Pointer(operand_type). // 2026-07-02 (audit POISON 6897): fallback на channel-аннотацию операнда // (1) стал op-AWARE — тождество «тип результата = тип операнда» верно ТОЛЬКО // для Neg/Not; для AddrOf терялась Pointer-обёртка (`&b.v` : Scalar{int} // вместо TypedPtr), для Deref не снималась; (2) добавлен gs-gate как у // соседних арок (As/Is, Try/Bang, Coalesce) — голый Named{T} в generic-body // остаётся на legacy, не материализуется в канал. if e.id.is_set() { let op_identity = matches!( &e.kind, ExprKind::Unary { op: crate::ast::UnOp::Neg, .. } | ExprKind::Unary { op: crate::ast::UnOp::Not, .. } ); let tr_opt = self.infer_expr_type(e, scope).or_else(|| { if op_identity && operand.id.is_set() { self.resolved_types_buf.borrow().get(&operand.id) .and_then(|rt| Self::resolved_to_typeref(rt, e.span)) } else { None } }); if let Some(tr) = tr_opt { if !typeref_mentions_any(&tr, gs) { let rt = ResolvedType::from_type_ref(&tr); self.resolved_types_buf.borrow_mut().insert(e.id, rt); } } } } ExprKind::Try(inner) | ExprKind::Bang(inner) | ExprKind::RefArg(inner) => { // №253 [M-ok-or-bare-variant-misresolve-try-ice]: `<opt>.ok_or(<bare // variant>)?` — `ok_or`'s Err param `E` (`Option[T] @ok_or[E](err E) // -> Result[T,E]`, prelude/core.nv) is a METHOD-level generic bound // ONLY by this very argument (chicken-egg), so the general arg- // narrowing loop (`check_instance_overload`, ~L13038) deliberately // skips `materialize_literal_coercion` whenever `exp_ty` mentions the // callee's own unbound generic (avoids stamping an erased/wrong type // for the general case) — a bare enum-variant argument (`Empty`) never // gets a context type and falls to `infer_expr_type`'s last-resort // alphabetical-tie-break fallback (~L17737), picking whichever Sum // type declaring that variant name sorts first, NOT the function's own // error type. For the narrow `?`-immediately-after-`ok_or(..)` shape // the missing context IS recoverable: `?` propagates the Err arm // through THIS function's own declared return carrier // (`current_fn_return_ty`, `Result[_, E]`) — bind the bare-variant // argument against that concrete `E` before walking `inner`, so the // normal materialize/Ident-cache-lookup machinery picks it up. if matches!(e.kind, ExprKind::Try(_)) { if let ExprKind::Call { func, args, .. } = &inner.kind { if let ExprKind::Member { name: field, .. } = &func.kind { if field == "ok_or" && args.len() == 1 { if let Some(TypeRef::Named { path, generics, .. }) = self.current_fn_return_ty.borrow().clone() { if path.len() == 1 && path[0] == "Result" && generics.len() == 2 && !typeref_mentions_any(&generics[1], gs) { self.materialize_literal_coercion(args[0].expr(), &generics[1]); } } } } } } self.f1_expr(inner, gs, scope, errors); // Plan 174.2 Ф.B: cross-carrier `?` diagnostics. Only for `?` // (Try), not `!!` (Bang) — `!!` throws through Fail и не связан // с return-carrier. if matches!(e.kind, ExprKind::Try(_)) { self.check_try_carrier_match(inner, gs, scope, errors); } // Plan 172.1 §0a (Try/Bang): materialize the unwrapped type into the channel. // `infer_expr_type` now has Try/Bang arms: Result[T,E]→T, Option[T]→T. // gs-gated: a `T` inside a generic body stays on legacy. if e.id.is_set() { if let Some(tr) = self.infer_expr_type(e, scope) { if !typeref_mentions_any(&tr, gs) { let rt = ResolvedType::from_type_ref(&tr); self.resolved_types_buf.borrow_mut().insert(e.id, rt); } } } } ExprKind::Coalesce(a, b) => { self.f1_expr(a, gs, scope, errors); // [E_COALESCE_RETURN_FALLBACK] (D86 AMEND 2026-07-23): `b` может // быть `CoalesceReturnFallback` ТОЛЬКО как непосредственный правый // операнд `??` (парсер гарантирует это, см. ast::mod.rs doc). // Форма ВСЕГДА отвергается — не рекурсируем в f1_expr(b) (там нет // самостоятельной семантики), а сразу строим контекстную // диагностику. if let ExprKind::CoalesceReturnFallback(ret_value) = &b.kind { self.check_coalesce_return_fallback( a, ret_value, e.span, gs, scope, errors, ); } else { self.f1_expr(b, gs, scope, errors); } // Plan 172.1 §0a (Coalesce): type = unwrapped inner of `a` ≡ type of `b`. // `infer_expr_type` delegates to `b` as the canonical source. // gs-gated: generic fallback stays on legacy. if e.id.is_set() { if let Some(tr) = self.infer_expr_type(e, scope) { if !typeref_mentions_any(&tr, gs) { let rt = ResolvedType::from_type_ref(&tr); self.resolved_types_buf.borrow_mut().insert(e.id, rt); } } } } // [E_COALESCE_RETURN_FALLBACK]: reached only if `CoalesceReturnFallback` // somehow appears OUTSIDE the immediate `??` RHS position (never // constructed there by the parser) — defensive no-op walk of the // optional payload, no diagnostic (the Coalesce arm above is the only // legitimate detection point, since only it knows the LHS `X`). ExprKind::CoalesceReturnFallback(opt) => { if let Some(inner) = opt { self.f1_expr(inner, gs, scope, errors); } } ExprKind::Member { obj, name } => { let is_call_func = self.in_call_func.replace(false); self.f1_expr(obj, gs, scope, errors); // Plan 172.1 U.4.4: thread the Member expr's `ExprId` so the // field-found site can annotate a concrete primitive field type. self.f3_check_member_ctx(obj, name, e.span, e.id, scope, errors, is_call_func); // 172.1.2 (Member-мост, POST-ORDER): tuple-поле `.N` на obj, // видимом только КАНАЛУ (buf: Tuple-аннотация) — элемент кортежа. if e.id.is_set() && obj.id.is_set() && !self.resolved_types_buf.borrow().contains_key(&e.id) { if let Ok(ix) = name.parse::<usize>() { // Plan 196 Ф.4b: Tuple→элемент по индексу — через решатель // (`Constraint::Project` TupleField); byte-parity (`project` // = `items.get(ix)`, тот же инлайн-`match`). let container = self.resolved_types_buf.borrow().get(&obj.id).cloned(); let elem = container.and_then(|c| { Self::project_channel( &c, constraint_solver::ProjectKind::TupleField(ix), ) }); if let Some(rt) = elem { self.resolved_types_buf.borrow_mut().insert(e.id, rt); } } } // 2026-07-02: прежний P67-fallback («f3 и проба промахнулись → int, // byte-identical legacy») УДАЛЁН — это §1-анти-паттерн «авто-выводимый // неверный тип»: он ТРАВИЛ канал (`@map` use-поля generic-ресивера // аннотировалось Scalar-int), из-за чего codegen держал парный guard // «Member int = недоверие» и ронял ~1300 ЧЕСТНЫХ int-полей в legacy. // Незарезолвленный Member теперь БЕЗ аннотации → legacy-навигация; // канал содержит только доверенные типы, guard у потребителя снят. } ExprKind::Index { obj, index } => { // №367 (window p375-ptr2, D216/174.5 read-parity): consume // the assign-target marker BEFORE recursing — mirrors the // `ExprKind::Unary` arm (see `assign_target_top`'s field // doc and that arm's comment for the full rationale). let is_assign_target_top = self.assign_target_top.replace(false); self.f1_expr(obj, gs, scope, errors); self.f1_expr(index, gs, scope, errors); // Plan 152.1 Ф.1 (D249): `str` is NOT integer-indexable — codepoint // indexing a UTF-8 string is O(n) hiding behind `[i]`. Only the // byte-range slice `str[a..b]` (Range index) is valid. Discriminate // by the INDEX TYPE (not just a literal `a..b`) so `let r = 2..5; // s[r]` is also accepted. Element access goes through a lens. let index_is_range = matches!(index.kind, ExprKind::Range { .. }) || self.infer_expr_type(index, scope).as_ref() .and_then(Self::typeref_named_base) == Some("Range"); // [M-172.14-range-idx-postorder-retry] (канальный фикс, 2026-07-10, // регресс bcrypt): the EARLY Range-index whole-expr probe (Plan 172.1.1 // U.4.5 "Index probe", earlier in `f1_expr_inner` — before this match's // recursive `f1_expr(obj)`/`f1_expr(index)` above) runs PRE-ORDER — when // `obj`'s type is visible ONLY via the channel (`resolved_types_buf`, // populated POST-ORDER by a method call like `state.finalize()` with no // scope-visible type), `infer_expr_type(obj)` inside that early probe // fails (buf still empty at that point) → the WHOLE Range-index expr // (`state.finalize()[0..32]`) never gets a resolved_types_buf entry → // codegen's `infer_expr_c_type` Channel 2 misses it → falls to legacy → // stale `[N]T` C-type for the binding instead of the correct `[]T` // (checker's `infer_expr_type` Index-arm DOES compute `[]T` for a // FixedArray Range-slice — [M-fixed-array-value-semantics] — but that // fix never reaches the channel for this ordering). Retry HERE, // POST-ORDER (children just got annotated above) — idempotent no-op if // the early probe already succeeded (scope-typed `obj` case). if index_is_range && e.id.is_set() && !self.resolved_types_buf.borrow().contains_key(&e.id) { if let Some(tr) = self.infer_expr_type(e, scope) { let rt = Self::mark_type_params(ResolvedType::from_type_ref(&tr), gs); self.resolved_types_buf.borrow_mut().insert(e.id, rt); } } if !index_is_range { if std::env::var_os("NOVA_IDX_TRACE").is_some() { if let ExprKind::Ident(on) = &obj.kind { let sc = scope.contains_key(on); let bufh = obj.id.is_set() && self.resolved_types_buf.borrow().contains_key(&obj.id); let inferred = self.infer_expr_type(obj, scope).is_some(); if !inferred { eprintln!("[IDX-MISS] v={} scope={} buf={} fid={:?}", on, sc, bufh, e.span.file_id); } } } if let Some(obj_tr) = self.infer_expr_type(obj, scope) { // №367: `p[i]` INDEX READ on a raw pointer — the // READ-form sibling of the already-closed WRITE // retraction (`p[i] = v`, №353, `check_target_ // readonly`'s Index arm). Same rationale/exclusion // as the `ExprKind::Unary` Deref-read check above — // skipped when THIS node is the immediate // assignment TARGET (already diagnosed as a WRITE). if !is_assign_target_top && pointee_is_writable(&obj_tr).is_some() { errors.push(Diagnostic::new( "[E_POINTER_OP_USE_METHOD] operator `p[i]` (index read) on \ raw pointer retired (Plan 174.5 §3/§9, D216 amend, №367 \ read-parity) — use `p.read_at(i)`" .to_string(), e.span, )); } if Self::typeref_named_base(&obj_tr) == Some("str") { errors.push(Diagnostic::new( "[E_STR_NO_INT_INDEX] `str` cannot be indexed by an integer — \ codepoint indexing a UTF-8 string is O(n) masquerading as O(1) \ (D249). Use a lens: byte `s.bytes()[i]` (O(1)), or iterate \ codepoints with `for c in s.chars()` / `.chars().indices()` \ (positional `chars().nth(i)` was itself retracted, D260-амендмент, \ for the same O(n)-as-O(1) reason); the only `str[..]` form is the \ byte-range slice `s[a..b]`.".to_string(), e.span, )); } // Plan 172.1 U.4.4 (element half): materialize the ELEMENT type of an // `[]T`/`[N]T` index `arr[i]` into the checker channel — the convention // §1 «materialize the resolve, do not throw it» names BOTH field AND // element; field is done (Member), this is element. The element of a // STRUCTURAL array type is its `inner` (same extraction as // `infer_iter_elem_type`; structural, NOT a name-keyed `Vec` special-case // → §3-clean). Reuses the `obj_tr` already resolved above (no second // `infer_expr_type` — §2). GATE primitive (shared `primitive_gate`): the // consumer `infer_expr_c_type` is ALREADY authoritative (U.4.4b) → this is // a BEHAVIOR-CHANGE fixing the §1 `_=>nova_int` fallback for a // primitive-element index. A generic element (`[]T`, T a param) → // `Named{T}` is not primitive → rejected (mono/U.4.3(d) territory); Named // containers (`Vec[T]`/`HashMap[K,V]`) / `str` / custom `@index` carry no // `Array` TypeRef here → not annotated → legacy (minimal, sound). Range // index (`arr[a..b]`) is excluded above — it yields a slice, not an element. if e.id.is_set() { let elem: Option<&TypeRef> = match &obj_tr { TypeRef::Array(inner, _) | TypeRef::FixedArray(_, inner, _) => { Some(inner.as_ref()) } TypeRef::Readonly(inner, _) => match inner.as_ref() { TypeRef::Array(e2, _) | TypeRef::FixedArray(_, e2, _) => { Some(e2.as_ref()) } _ => None, }, _ => None, }; if let Some(elem) = elem { // 172.1.2: primitive-гейт снят — residual-параметр // помечается TypeParam (лоуэринг: instance-map/subst/Err). let rt = Self::mark_type_params( ResolvedType::from_type_ref(elem), gs); self.resolved_types_buf.borrow_mut().insert(e.id, rt); } else if let TypeRef::Named { path, .. } = &obj_tr { // 172.1.2 (Index @-слайс generic, 2026-07-03): scope["@"] // extension-метода на []T — элемент = TypeParam(T), // ПРЯМО в буфер (минуя TypeRef — урок утечки bare Named). if path.len() == 1 { if let Some(en) = path[0].strip_prefix("[]") { if gs.contains_key(en) { self.resolved_types_buf.borrow_mut().insert( e.id, ResolvedType::TypeParam(en.to_string()), ); } } } } } } else if e.id.is_set() && obj.id.is_set() { // 172.1.2 (Index-мост, POST-ORDER — дети уже аннотированы): // obj виден только КАНАЛУ (Шаг 2b: `@data` TypedPtr(T), // `@_buckets` Named{Vec,[..]}). СТРУКТУРНЫЕ формы only // (урок POISON 6453 — никакого user-@index здесь): // Vec[E]/[]E → E; *mut E (raw-buffer) → E. // Plan 196 Ф.4b: правило контейнер→элемент — в решателе // (`Constraint::Project`); byte-parity (`project` = та же // структурная раскладка, что был инлайн-`match`). let container = self.resolved_types_buf.borrow().get(&obj.id).cloned(); let elem = container.and_then(|c| { Self::project_channel(&c, constraint_solver::ProjectKind::Element) }); if let Some(rt) = elem { self.resolved_types_buf.borrow_mut().insert(e.id, rt); } } } } ExprKind::If { cond, then, else_ } => { self.f1_expr(cond, gs, scope, errors); self.f1_block(then, gs, scope, errors); if let Some(eb) = else_ { self.f1_else(eb, gs, scope, errors); } // Plan 172.1 U.4.4 (If-expr half): materialize the common primitive type of an // `if/else` EXPRESSION into the checker channel so codegen READS it instead of // re-deriving (§0/§1) — the If-parallel of the Match-arm flip. See // `infer_if_common_primitive` (maximally conservative: both branches empty-stmt // blocks with EQUAL non-unit primitive trailing). Consumer `infer_expr_c_type` // is ALREADY authoritative (U.4.4b) → byte-identical to legacy for the gated set. if e.id.is_set() { if let Some(rt) = self.infer_if_common_primitive(then, else_, scope) { self.resolved_types_buf.borrow_mut().insert(e.id, rt); } } } ExprKind::IfLet { pattern, scrutinee, then, else_, .. } => { self.f1_expr(scrutinee, gs, scope, errors); // Plan 124.2 (D221): pattern destructure priv-field check. let scrut_ty = self.infer_expr_type(scrutinee, scope); self.check_priv_pattern_recursive(pattern, scrut_ty.as_ref(), errors); // №279: resolve nested bare-variant sub-patterns against the // scrutinee's structural type (see fn doc). self.resolve_pattern_variant_types(pattern, scrut_ty.as_ref()); // [M-ro-launder-pattern-bind-not-enforced] (реестр 221.1 // №106, D34): extend `scope`/`ro_binding_names` with the // `if let` pattern's OWN binding (`if Some(x) = ...`) before // walking `then` — mirrors the `Match`-arm treatment // immediately below (SAME `match_arm_bindings` helper + // `pattern_bind_mutability` L1-launder registration; a bare // pattern-bind is D34-immutable, `mut x` is mut). Without the // `scope` extension, `then` never saw `x`'s TYPE at all // (a pre-existing, separate gap from the launder hole this // window closes — fixed here as its unavoidable // prerequisite: the launder check's scalar exemption, §72, // needs `infer_expr_type` to actually resolve the bound // name's type to tell a scalar bare-bind apart from a // heap-owning one). let binds = self.match_arm_bindings(pattern, scrut_ty.as_ref()); let mut saved: Vec<(String, Option<TypeRef>)> = Vec::new(); for (n, t) in &binds { saved.push((n.clone(), scope.insert(n.clone(), t.clone()))); } let ro_snapshot: std::collections::HashSet<String> = self.ro_binding_names.borrow().clone(); for (n, is_mut) in Self::pattern_bind_mutability(pattern) { let mut set = self.ro_binding_names.borrow_mut(); set.remove(&n); if !is_mut { set.insert(n); } } self.f1_block(then, gs, scope, errors); *self.ro_binding_names.borrow_mut() = ro_snapshot; for (n, prev) in saved { match prev { Some(t) => { scope.insert(n, t); } None => { scope.remove(&n); } } } if let Some(eb) = else_ { self.f1_else(eb, gs, scope, errors); } } ExprKind::Match { scrutinee, arms } => { self.f1_expr(scrutinee, gs, scope, errors); // Plan 124.2 (D221): each arm's pattern checked vs scrutinee type. let scrut_ty = self.infer_expr_type(scrutinee, scope); // generic-match-scope-gap fix (2026-07-21, see // `resolve_generic_bound_method_return` doc): when the general // inference above misses (a scrutinee that's a call to a method // on a GENERIC-PARAM receiver bound to a known protocol, e.g. // `@next()` on `I` bound `I Next[T]`), try the narrow bound- // aware fallback — used ONLY to widen the arm-bindings scope // extension below (`binds_scrut_ty`), NEVER for // `check_priv_pattern_recursive` or the channel materialization // further down (both keep using the original, general // `scrut_ty` — unchanged behavior there). let binds_scrut_ty: Option<TypeRef> = scrut_ty.clone().or_else(|| { if let ExprKind::Call { func, args, .. } = &scrutinee.kind { if args.is_empty() { if let ExprKind::Member { obj, name } = &func.kind { return self.resolve_generic_bound_method_return(scope, obj, name); } } } None }); for arm in arms { self.check_priv_pattern_recursive(&arm.pattern, scrut_ty.as_ref(), errors); // №279 [M-nested-err-pattern-shared-variant-wrong-enum-tag]: // resolve each nested bare-variant sub-pattern (e.g. // `Err(OnlySign)`'s `OnlySign`) against the scrutinee's // OWN structural type (`scrut_ty`, not the general // widened `binds_scrut_ty` used below for scope-only // purposes) — see `resolve_pattern_variant_types` doc. self.resolve_pattern_variant_types(&arm.pattern, scrut_ty.as_ref()); if let Some(g) = &arm.guard { self.f1_expr(g, gs, scope, errors); } // Plan 196.9 [M-primitive-concrete-overload-receiver-dispatch]: extend // `scope` with the arm's pattern-bound names (same conservative subset as // `match_arm_bindings`, reused from Plan 172.1 tally АТОМ 2a) BEFORE walking // the arm body. Without this, a method call on a pattern-bound receiver // (`Some(r) => r.clamp(lo, hi)`) reached `f1_check_call` -> // `check_instance_overload` with `r` ABSENT from scope — // `BoundCtx::infer_arg_ty` returned `None` and the whole function silently // no-op'd (no `[E_UNKNOWN_METHOD]`, no `resolved_callees` write) for EVERY // call on such a receiver. Codegen then had nothing to read and fell back to // name-keyed dispatch (last-registered same-name method wins regardless of // the receiver's real type) — e.g. an `i64` receiver silently mis-dispatching // into `f64 @clamp` (implicit int64_t<->double cast, precision loss at i64 // extremes, no CC-FAIL since both sides are scalar). let binds = self.match_arm_bindings(&arm.pattern, binds_scrut_ty.as_ref()); let mut saved: Vec<(String, Option<TypeRef>)> = Vec::new(); for (n, t) in &binds { saved.push((n.clone(), scope.insert(n.clone(), t.clone()))); } // [M-ro-launder-pattern-bind-not-enforced] (реестр 221.1 // №106, D34/D246-амендмент): a match-arm pattern binding // (`Ok(b0) => ...`) is L1-immutable per D34 ("bare-биндинг // конструктор-паттерна = immutable") — a bare pattern-bound // name gets the SAME ro-freeze the launder table already // gives an explicit `ro x = ...` local / non-`mut` param // (D246 ORACLE G); `mut b0` inside the pattern is mut, // exactly as `mut x = ...` is. Without this, `Ok(b0) => // { mut b = b0 }` on a heap-owning payload laundered // through the launder check MOLча (the check only ever // consulted `ro_binding_names`, never populated for // pattern binds). Snapshot/restore matches the SAME // `saved`/`scope` arm-body lifetime immediately above. let ro_snapshot: std::collections::HashSet<String> = self.ro_binding_names.borrow().clone(); for (n, is_mut) in Self::pattern_bind_mutability(&arm.pattern) { let mut set = self.ro_binding_names.borrow_mut(); set.remove(&n); if !is_mut { set.insert(n); } } match &arm.body { MatchArmBody::Expr(e) => { self.f1_expr(e, gs, scope, errors) } MatchArmBody::Block(b) => { self.f1_block(b, gs, scope, errors) } } *self.ro_binding_names.borrow_mut() = ro_snapshot; for (n, prev) in saved { match prev { Some(t) => { scope.insert(n, t); } None => { scope.remove(&n); } } } } // [M-match-arm-mixed-int-width-sentinel-coerce] fix (P1, 2026-07-21): // arms with GENUINELY incompatible int widths (neither safely widens // into the other) → hard error BEFORE the channel materialization // below, which otherwise bails silently and lets the legacy codegen // arm-type re-derivation pick an arbitrary (wrong) width. Safe-widening // mixes are NOT reported (see `check_match_arm_width_mismatch` doc). self.check_match_arm_width_mismatch(arms, scope, scrut_ty.as_ref(), errors); // Plan 172.1 U.4.4 (Match-arm half): materialize the common primitive // arm type into the checker channel so codegen READS the resolved match // type instead of re-deriving it (§0/§1). See // `infer_match_common_primitive`. The consumer `infer_expr_c_type` is // ALREADY authoritative (U.4.4b) → the annotation is consumed directly. // GATE non-unit primitive → byte-identical to legacy arm-inference for // the gated set (consolidates the consumer side onto the channel). if e.id.is_set() { if let Some(rt) = self.infer_match_common_primitive(arms, scope, scrut_ty.as_ref()) { self.resolved_types_buf.borrow_mut().insert(e.id, rt); } } } ExprKind::Block(b) => self.f1_block(b, gs, scope, errors), ExprKind::ArrayLit(elems) => { for el in elems { match el { ArrayElem::Item(e) | ArrayElem::Spread(e) => { self.f1_expr(e, gs, scope, errors) } } } // Plan 172.1 U.4.4 (composite Array): materialize the array's element type // from the FIRST `Item` (mirror the legacy ArrayLit arm's first-Item element) // into the channel as `Array(elem)` when that element is a concrete primitive // (shared `primitive_gate`). The consumer lowers it via resolved_type_to_c → // `resolved_array_to_c` (Vec-flip `Nova_Vec____<elem>*` + the SAME worklist / // instance-info side-effects as legacy) → byte-identical. // // The `current_array_elem_hint` int-literal→sized promotion ([M-138.2], a // `[]u8 = [1,2,3]` lowering to `Nova_Vec____nova_byte*`) does NOT need a gate // here: it is an EMISSION-side hint (set in emit_assign_typed around emit_expr), // and an ANNOTATED array takes its C-type from the ANNOTATION (authoritative), // routing AROUND this `infer_expr_c_type` consumer entirely. Proven empirically // (§7.2 detect-mode: 265 materializations over sized-int + broad dirs + the // direct `[]u8`/`[]u32` fixture cases — 0 DIFF). So an UN-annotated array (the // only context this channel is consumed for the array's own type) has no hint → // legacy yields the same primitive element → byte-identical. A non-primitive / // unresolvable first element (generic-`T`, user type, a nested `[]`/tuple, a // `Call`/`Member`/`Index`) bails to legacy (sound). Spread-first / no-`Item` // bails (legacy's spread element handling differs). `e.id` is direct. if e.id.is_set() { let first_item = elems.iter().find_map(|el| match el { ArrayElem::Item(x) => Some(x), _ => None, }); if let Some(x) = first_item { if let Some(tr) = self.infer_expr_type(x, scope) { let rt = ResolvedType::from_type_ref(&tr); if Self::primitive_gate(&rt) { // D239/D315 §0: channel the canonical NOMINAL `Vec[T]` // carrier (not the retired `R::Array` slice form). Lowers // byte-identically — `resolved_type_to_c(Named{Vec})` routes // to `resolved_array_to_c` via the `"Vec"` arm, exactly as // `R::Array` did. self.resolved_types_buf.borrow_mut().insert( e.id, ResolvedType::Named { name: "Vec".to_string(), module: Vec::new(), args: vec![rt], }, ); } } } } } ExprKind::MapLit { elems, .. } => { for (k, v) in crate::ast::MapElem::cloned_pairs(elems).iter() { self.f1_expr(k, gs, scope, errors); self.f1_expr(v, gs, scope, errors); } } ExprKind::TupleLit(elems) => { for el in elems { self.f1_expr(el, gs, scope, errors); } // Plan 172.1 U.4.4 (composite Tuple): materialize the tuple's // `ResolvedType` into the checker channel when EVERY element resolves // to a concrete PRIMITIVE (the shared `primitive_gate`). The consumer // lowers it via the already-byte-identical `resolved_type_to_c` Tuple // arm — which calls the SAME `register_mono_tuple` / // `compute_mono_tuple_c_name` helpers as the legacy // `infer_expr_c_type` TupleLit arm — so an all-primitive tuple is // byte-identical (both go MONO; a primitive element is "concrete" in // BOTH concreteness checks). The composite divergence U.4.2 flagged is // exactly the NON-primitive element (`infer_expr_c_type`'s `ety.is_empty()` // ≠ `resolved_type_to_c`'s `Nova_`-prefix schema on a type-param element): // any non-primitive / unresolvable element (generic-`T`, user type, a // nested aggregate, a `Call`/`Member`/`Index` the checker can't infer) // bails the WHOLE tuple to legacy (sound). `e.id` is available directly // (no plumbing). Same flip as the leaf atoms (Ident/Member/Index/Match). if e.id.is_set() && !elems.is_empty() { let mut elem_rts: Vec<ResolvedType> = Vec::with_capacity(elems.len()); let mut all_primitive = true; for el in elems { match self.infer_expr_type(el, scope) { Some(tr) => { let rt = ResolvedType::from_type_ref(&tr); if Self::primitive_gate(&rt) { elem_rts.push(rt); } else { all_primitive = false; break; } } None => { all_primitive = false; break; } } } if all_primitive { self.resolved_types_buf .borrow_mut() .insert(e.id, ResolvedType::Tuple(elem_rts)); } } } ExprKind::RecordLit { type_name, fields, .. } => { for f in fields { if let Some(v) = &f.value { self.f1_expr(v, gs, scope, errors); // №375 (window p375-ptr2): `Type { ptr_field: &ro_x }` // initializing a `*mut T` field from a `ro` source. if let Some(tn) = type_name.as_ref().and_then(|p| p.last()) { if let Some(field_ty) = self.record_fields_for(tn) .and_then(|fs| fs.iter().find(|rf| rf.name == f.name)) .map(|rf| rf.ty.clone()) { self.check_addrof_mut_from_ro_source(v, &field_ty, errors); } } } } } ExprKind::TaggedTemplate { tag, args, .. } => { self.f1_expr(tag, gs, scope, errors); for a in args { self.f1_expr(a, gs, scope, errors); } } ExprKind::InterpolatedStr { parts } => { for p in parts { if let InterpStrPart::Expr { expr: e, spec } = p { // Plan 221.1 (D186-амендмент): bare `"${x}"` (Display) // interpolation of a user record/sum/newtype/named-tuple // WITHOUT `#impl(Display)` used to silently reach // emit_c's numeric-cast last-resort fallback // (`nova_int_to_str((nova_int)(v))`) — prints the // object's address as garbage int. See // `check_interp_no_display` doc for full scope. self.check_interp_no_display(e, gs, scope, spec, errors); // Plan 221.1 followup (coordinator repro, 2026-07-21): // the SAME numeric-address garbage-fallback also fires // for bare `${x:?}` (Debug) on a type without // `#impl(Debug)` — `inject_synthesized_methods` // (auto_derive.rs) only synthesizes `@debug` for types // that literally list "Debug" in `impl_protocols` // (D229 §4: "Когда user type X помечен #impl(Debug)"), // contradicting the `gate_on_impl=false` "zero-friction, // no annotation needed" comment at emit_c.rs's Debug // branch (~42816) — that comment describes an intent // Plan 91.14's OWN default-body candidate search can // never fulfil (`Debug` protocol ships with NO default // body at all, D229 §2 "NO default body в protocol // decl" — the candidate list is always empty regardless // of the gate flag). D229 §7 already RESERVES // `E_DEBUG_PRINTABLE_NOT_IMPLEMENTED` for exactly this // "type doesn't impl Debug, no auto-synthesis possible" // case — it was never wired up. See // `check_interp_no_debug` doc for full scope. self.check_interp_no_debug(e, gs, scope, spec, errors); // №248 §0 channel-first: hand emit_c the resolved // bare Nova type name for this expr instead of // making it re-derive one from the C-type string // (string-parsing there dropped `NovaTuple_`). if e.id.is_set() { if let Some(tn) = self.resolve_interp_user_value_type(e, gs, scope) { self.resolved_types_buf.borrow_mut().entry(e.id) .or_insert(ResolvedType::Named { name: tn, module: vec![], args: vec![] }); } } self.f1_expr(e, gs, scope, errors); } } } ExprKind::Lambda { body, .. } => { self.check_ref_escape_capture(body, errors); self.f1_expr(body, gs, scope, errors) } ExprKind::ClosureLight { params, body, .. } => { match body { ClosureBody::Expr(be) => { self.check_ref_escape_capture(be, errors); self.f1_expr(be, gs, scope, errors) } ClosureBody::Block(b) => { self.check_ref_escape_capture_block(b, errors); self.f1_block(b, gs, scope, errors) } } // 2026-07-02 (tally АТОМ 3b): zero-param truthful-подмножество — // `|| body` без параметров: нечего гадать про params, ret = infer // тела. Гейт §1: primitive non-Unit ret + не упоминает gs. // Полный bidirectional-вывод сигнатур — фаза C6; param'ные // замыкания НЕ аннотируются (гадание params=int — ложь). if e.id.is_set() && params.is_empty() { if let ClosureBody::Expr(be) = body { if let Some(tr) = self.infer_expr_type(be, scope) { let rt = ResolvedType::from_type_ref(&tr); if rt != ResolvedType::Unit && Self::primitive_gate(&rt) && !typeref_mentions_any(&tr, gs) { self.resolved_types_buf.borrow_mut().insert( e.id, ResolvedType::Func { params: vec![], ret: Box::new(rt), effects: vec![], }, ); } } } } } ExprKind::ClosureFull(sb) => { match &sb.body { FnBody::Expr(be) => self.check_ref_escape_capture(be, errors), FnBody::Block(b) => self.check_ref_escape_capture_block(b, errors), FnBody::External => {} } self.f1_fn_sig_body(sb, gs, scope, errors); // 197.3 (Q3 B, channel-first migration): ClosureFull is fully // typed by grammar — every param carries an explicit `T`, // `return_type` is `-> R` or absent (= Unit) — unlike // ClosureLight, no body-inference is needed. Register // `ResolvedType::Func` for the closure's OWN id, mirroring // the ClosureLight zero-param arm above. Consumed by // emit_c.rs Channel 2 (`infer_expr_c_type`'s dedicated // ClosureLight/ClosureFull block) to type e.g. // `ro apply = fn(f fn(int)->int, x int)->int => f(x)` // without falling to the legacy ClosureFull arm there // (dead once this fires — removed alongside this change). // `mark_type_params` (same helper the HOF closure-arg // channel at ~7797 uses) reclassifies a bare in-scope // generic name (`gs`) as `ResolvedType::TypeParam` instead // of a naive `Named` — `resolved_type_to_c`'s TypeParam arm // consults `current_type_subst`, so a generic ClosureFull // resolves correctly PER mono-instantiation at emit time // (not just the fully-concrete case). if e.id.is_set() { let params_rt: Vec<ResolvedType> = sb.params.iter() .map(|p| Self::mark_type_params(ResolvedType::from_type_ref(&p.ty), gs)) .collect(); let ret_rt = Self::mark_type_params( sb.return_type.as_ref() .map(ResolvedType::from_type_ref) .unwrap_or(ResolvedType::Unit), gs, ); self.resolved_types_buf.borrow_mut().insert( e.id, ResolvedType::Func { params: params_rt, ret: Box::new(ret_rt), effects: vec![], }, ); } } ExprKind::Spawn(body) => { self.check_ref_escape_capture(body, errors); self.f1_expr(body, gs, scope, errors) } ExprKind::Detach(body) | ExprKind::Blocking(body) => self.f1_block(body, gs, scope, errors), ExprKind::Supervised { body, cancel, deadline, on_timeout } => { if let Some(c) = cancel { self.f1_expr(c, gs, scope, errors); } if let Some(_dl) = deadline { let _dl_e = &_dl.expr; self.f1_expr(_dl_e, gs, scope, errors); } if let Some(oh) = on_timeout { self.f1_expr(oh, gs, scope, errors); } self.f1_block(body, gs, scope, errors); } ExprKind::Forbid { body, .. } => { self.f1_block(body, gs, scope, errors) } ExprKind::Realtime { body, .. } => { self.f1_block(body, gs, scope, errors) } ExprKind::ParallelFor { pattern, iter, body, elem_type } => { self.f1_expr(iter, gs, scope, errors); if let Some(ann) = elem_type { self.f1_check_for_elem(iter, ann, gs, scope, errors); } // Plan 124.2 (D221): destructure pattern in parallel-for. let elem_ty = elem_type.clone() .or_else(|| self.infer_iter_elem_type(iter, scope)); self.check_priv_pattern_recursive(pattern, elem_ty.as_ref(), errors); // №279: resolve nested bare-variant sub-patterns (see fn doc). self.resolve_pattern_variant_types(pattern, elem_ty.as_ref()); // 172.1.2 (for-var в scope, 2026-07-03): loop-переменная типизируется // и БЕЗ явной аннотации — inferred elem_ty (тот же источник, что // D221-проверка выше). Закрывает Index:i:<v> кластер (v[i] в теле). self.f1_for_body(&elem_ty, pattern, body, gs, scope, errors); } ExprKind::For { pattern, iter, body, elem_type, .. } => { self.f1_expr(iter, gs, scope, errors); // Plan 87 Ф.3: явная аннотация типа элемента — checked // assertion против фактического типа элемента итератора. if let Some(ann) = elem_type { self.f1_check_for_elem(iter, ann, gs, scope, errors); } // Plan 124.2 (D221): destructure pattern in for-in loop. let elem_ty = elem_type.clone() .or_else(|| self.infer_iter_elem_type(iter, scope)); self.check_priv_pattern_recursive(pattern, elem_ty.as_ref(), errors); // №279: resolve nested bare-variant sub-patterns (see fn doc). self.resolve_pattern_variant_types(pattern, elem_ty.as_ref()); // 172.1.2 (for-var в scope, 2026-07-03): loop-переменная типизируется // и БЕЗ явной аннотации — inferred elem_ty (тот же источник, что // D221-проверка выше). Закрывает Index:i:<v> кластер (v[i] в теле). self.f1_for_body(&elem_ty, pattern, body, gs, scope, errors); } ExprKind::While { cond, body, .. } => { self.f1_expr(cond, gs, scope, errors); self.f1_block(body, gs, scope, errors); } ExprKind::WhileLet { pattern, scrutinee, body, .. } => { self.f1_expr(scrutinee, gs, scope, errors); // Plan 124.2 (D221): destructure pattern in while-let. let scrut_ty = self.infer_expr_type(scrutinee, scope); self.check_priv_pattern_recursive(pattern, scrut_ty.as_ref(), errors); // №279: resolve nested bare-variant sub-patterns (see fn doc). self.resolve_pattern_variant_types(pattern, scrut_ty.as_ref()); self.f1_block(body, gs, scope, errors); } ExprKind::Loop { body, .. } => { self.f1_block(body, gs, scope, errors) } ExprKind::Select { arms } => { for arm in arms { match &arm.op { SelectOp::Recv { chan, .. } => { self.f1_expr(chan, gs, scope, errors) } SelectOp::Send { chan, value } => { self.f1_expr(chan, gs, scope, errors); self.f1_expr(value, gs, scope, errors); } SelectOp::Default => {} } if let Some(g) = &arm.guard { self.f1_expr(g, gs, scope, errors); } self.f1_block(&arm.body, gs, scope, errors); } } ExprKind::Range { start, end, .. } => { if let Some(s) = start { self.f1_expr(s, gs, scope, errors); } if let Some(e) = end { self.f1_expr(e, gs, scope, errors); } } ExprKind::Throw(inner) => self.f1_expr(inner, gs, scope, errors), ExprKind::Interrupt(opt) => { if let Some(e) = opt { self.f1_expr(e, gs, scope, errors); } } ExprKind::With { body, .. } => { self.f1_block(body, gs, scope, errors) } ExprKind::Forall { range, body, .. } | ExprKind::Exists { range, body, .. } => { self.f1_expr(range, gs, scope, errors); self.f1_expr(body, gs, scope, errors); } // Plan 97 Ф.4 (D142): protocol-литерал — f1_expr walk // идентичен. ExprKind::HandlerLit { methods, .. } | ExprKind::ProtocolLit { methods, .. } => { for m in methods { match &m.body { HandlerMethodBody::Expr(e) => { self.f1_expr(e, gs, scope, errors) } HandlerMethodBody::Block(b) => { self.f1_block(b, gs, scope, errors) } } } } ExprKind::IntLit(_) | ExprKind::FloatLit(_) | ExprKind::BoolLit(_) | ExprKind::StrLit(_) | ExprKind::CharLit(_) | ExprKind::UnitLit | ExprKind::HexBlobLit(_) | ExprKind::NullPtrLit | ExprKind::Ident(_) | ExprKind::Path(_) | ExprKind::SelfAccess => {} } } fn f1_else( &self, eb: &ElseBranch, gs: &GenericScope, scope: &mut HashMap<String, TypeRef>, errors: &mut Vec<Diagnostic>, ) { match eb { ElseBranch::Block(b) => self.f1_block(b, gs, scope, errors), ElseBranch::If(e) => self.f1_expr(e, gs, scope, errors), } } fn f1_fn_sig_body( &self, sb: &FnSigBody, gs: &GenericScope, scope: &mut HashMap<String, TypeRef>, errors: &mut Vec<Diagnostic>, ) { match &sb.body { FnBody::Expr(e) => { self.f1_expr(e, gs, scope, errors); self.f4_check_value(e, scope, errors); } FnBody::Block(b) => self.f1_block(b, gs, scope, errors), FnBody::External => {} } } /// D246-амендмент ([M-ro-launder-via-mut-binding], Ф.1, 2026-07-23): /// determine the L2 content-view of a `let`-binding's TARGET, combining /// the explicit type modifier (if any) with the binding's own L1 /// mutability (P7 «bare `ro x` = freeze» / bare `mut x` under a /// no-modifier type = mut content-view). Shared between the annotated /// path (`f1_check_assign_let`) and the unannotated path (`f1_stmt` /// `Stmt::Let` — `mut b = a` with no `ann`). fn let_target_content_is_mut(ann: Option<&TypeRef>, binding_mut: bool) -> bool { match ann { Some(a) if a.is_readonly() => false, Some(a) if a.is_mut() => true, _ => binding_mut, } } /// D246-амендмент ([M-ro-launder-via-mut-binding], Ф.1, 2026-07-23): /// a `ro`-source (either L2 — `value`'s inferred type carries an /// explicit `ro T` modifier, OR L1 — `value` is a bare `Ident` bound /// through `ro_binding_names`: explicit `ro x = ...` local OR a /// non-`mut`/non-`consume` parameter, D176 default, P7 freeze) coerced /// into a mutable content-view TARGET is unsound: a write through the /// new mut-binding is visible to the original ro-bound name/param /// (P7 declares the freeze, but it did not survive re-binding before /// this fix — the exact hole `[M-ro-launder-via-mut-binding]` closes). /// Norm is STRICT (owner decision 2026-07-23, FINAL — «подтверждена /// доками+пробой» after a scalar-primitive exemption spike): applies to /// EVERY storage class EXCEPT the bare scalar primitives (`int`/`i8`../ /// `bool`/`char`/…) — see `is_bare_scalar_primitive` doc for the exact /// boundary (scalar-primitive ≠ value-record; a value-record even /// WITHOUT heap fields is still ❌, probe G/E remain neg — recursive /// field-fragility does not apply to a scalar because it has no fields /// AT ALL, whereas a record might recursively embed a heap field). fn check_readonly_source_coerce( &self, value: &Expr, target_content_is_mut: bool, scope: &HashMap<String, TypeRef>, errors: &mut Vec<Diagnostic>, ) { if !target_content_is_mut { return; } // L2 axis (existing, Plan 147 Ф.3): value's inferred TYPE carries an // explicit `ro T` modifier. if let Some(value_ty) = self.infer_expr_type(value, scope) { if value_ty.is_readonly() { errors.push(Diagnostic::new( format!( "[E_READONLY_COERCE] cannot coerce `readonly {}` to `{}`: \ removing readonly is not allowed. Use `.clone()` (D230) to \ get a mutable copy.", typeref_display(value_ty.strip_readonly()), if target_content_is_mut { "mut" } else { "ro" }, ), value.span, )); return; } } // L1 axis (D246-амендмент, [M-ro-launder-via-mut-binding]): value is // a bare identifier bound through a `ro` BINDING (local or param), // independent of its type's own L2 modifier. FINAL scalar-primitive // exemption (owner decision 2026-07-23): a bare scalar copy can // never alias (no fields at all — the bit-copy IS the independent // value), so it is exempt; every OTHER type (value-record included, // even without heap fields) stays covered. if let ExprKind::Ident(name) = &value.kind { let is_scalar = self.infer_expr_type(value, scope) .map_or(false, |t| is_fully_stack_value(&t, &self.types)); if !is_scalar && self.ro_binding_names.borrow().contains(name) { errors.push(Diagnostic::new( format!( "[E_READONLY_COERCE] источник `{name}` связан как `ro` (L1-binding \ — явный `ro {name} = ...` либо параметр без `mut`, D176-дефолт, \ P7 freeze), но присваивается в mutable content-view: запись через \ новый mut-binding была бы видна оригиналу/вызывающему (D246-\ амендмент, [M-ro-launder-via-mut-binding], Ф.1). Решения: \ (a) если `{name}` — параметр, объяви его `mut {name} T` (in-out \ ВСЕГДА — D326-ревизия §Р3: вызывающий увидит изменения после вызова); \ если локал — объяви его `mut {name} = ...` с самого начала — \ подходит, когда семантика реально in-out/мутируемая с начала; \ (b) скопируй явно — `.clone()` (D230) — если нужна НЕЗАВИСИМАЯ \ mutable-копия (используй ТОЛЬКО когда (a) не подходит — \ задокументируй одной строкой, почему); (c) оставь цель тоже `ro`, \ если запись не нужна." ), value.span, )); } } // Field-launder channel ([M-router-handler-mut-capture-escape- // soundness] §2, срочный пакет звучности mut/захватов, owner // decision 2026-08-01: "ВТОРОЙ канал той же дыры... ro-launder // ПОЛЕЙ — `mut lock = @lock` / `mut gauges = @gauges` из // ro-метода легально вымогают mut на разделяемое состояние"). // Same unsoundness as the L1-Ident axis above, one `Member`-hop // deeper: `mut x = @field` (self-field read inside a RO-receiver // method) or `mut x = obj.field` (bare-Ident `obj` bound `ro`, // L1, or typed `ro T`, L2). Scoped to these two live forms — // `metrics.nv`'s actual repro; a deeper chain (`a.b.field`) is a // documented, sound false-negative (consistent with this // checker's existing scope caveats elsewhere), not fixed here. if let ExprKind::Member { obj, .. } = &value.kind { let field_ty = self.infer_expr_type(value, scope); let field_is_stack = field_ty.as_ref() .map_or(false, |t| is_fully_stack_value(t, &self.types)); // #share exemption (D415): a copy of a `#share` type (AtomicInt & // co) is the SANCTIONED sharing form — mutation through the copy // is the whole point (fetch_add on the shared cell), not a // readonly-freeze escape. Mirrors the spawn-capture exemption. let field_is_share = field_ty.as_ref().map_or(false, |t| { crate::protocols::share_check::is_mut_alias_safe( &CapShareQuery(&self.types), t) }); if !field_is_stack && !field_is_share { let root_is_ro = match &obj.kind { ExprKind::SelfAccess => !self.current_recv_is_mut.get(), ExprKind::Ident(root_name) => { self.ro_binding_names.borrow().contains(root_name) || self.infer_expr_type(obj, scope) .map_or(false, |t| t.is_readonly()) } _ => false, }; if root_is_ro { let root_desc = match &obj.kind { ExprKind::SelfAccess => "receiver `@` (ro — метод объявлен \ без `mut @`, D176-дефолт)".to_string(), ExprKind::Ident(n) => format!( "`{n}` (ro — L1-binding или явный `ro T`, L2)" ), _ => "источник".to_string(), }; // №362 (p362-advice): suggestion (b) below used to claim // `.clone()` unconditionally, regardless of whether the // FIELD's own type actually provides one. `Clone` is opt-in // (D230 — auto-derive requires `#impl(Clone)`; a type that // never opted in has no `clone` method at all, record OR // named tuple alike), so a bare recommendation was actively // wrong for such a field (confirmed via nova-bignum's // `BigInt(sign, limbs)` — no `#impl(Clone)` — report // `docs/plans/wip/PROGRESS-p-bignum-tuple.md`, Defect A // Problem 1). Gate the wording on the SAME availability // check the general call-site validator uses (`t_provides_ // method` direct + `protocol_method_satisfiable_for` for a // not-yet-materialized auto-derive), so the hint never // dangles into a fresh `[E7320] no field or method clone`. let clone_tname: Option<String> = field_ty.as_ref() .and_then(|t| match t.strip_readonly() { TypeRef::Named { path, .. } => path.last().cloned(), _ => None, }); let clone_available = clone_tname.as_deref().is_some_and(|tn| { self.t_provides_method(tn, "clone") || self.protocol_method_satisfiable_for(tn, "clone") }); // №370 (p-diag, 2026-08-08, found by window p362-advice // 2026-08-06 as Finding B): the EARLIER text here (before // this fix) told the reader "вынеси код в приватный // хелпер с `mut`-параметром напрямую... передача поля // АРГУМЕНТОМ уже даёт независимую копию на границе // вызова" — that claim is FALSE. A `mut`-parameter is // ALWAYS by-pointer in-out (D326 R3, Plan 184 Р10; // solution (a) two lines below says the exact same thing // for THIS function's own params — "вызывающий увидит // изменения после вызова"), so extracting to a NEW // helper function changes nothing: the callee still // writes through a pointer to the SAME caller storage. // Confirmed by generated C + a run (nova-bignum-shaped // repro): `original.limbs.len()` and `copy.limbs.len()` // both moved from 3 to 4 after the "copy" helper pushed // once — there never was a second buffer. `nova check` // now rejects the call-argument shape too (see // `check_ro_field_into_mut_inout_arg`, same file), so // this text no longer points at a silently-broken // workaround. let solution_b = if clone_available { "(b) скопируй явно — `.clone()` (D230) — если нужна \ НЕЗАВИСИМАЯ mutable-копия; ".to_string() } else { format!( "(b) поле не реализует `Clone` (нет `#impl(Clone)` на \ `{}`) — `.clone()` здесь НЕ сработает (D230 — auto-\ derive только opt-in) — добавь `#impl(Clone)` на саму \ декларацию типа, если нужна НЕЗАВИСИМАЯ mutable-копия; \ приватный хелпер с `mut`-параметром НЕ ЗАМЕНЯЕТ \ `.clone()` — `mut`-параметр всегда by-pointer in-out \ (D326 R3), передача поля АРГУМЕНТОМ пишет через адрес \ в ТО ЖЕ хранилище, копии не даёт (№370); ", clone_tname.as_deref().unwrap_or("поля"), ) }; errors.push(Diagnostic::new( format!( "[E_READONLY_COERCE] поле, прочитанное с {root_desc}, \ присваивается в mutable content-view — поле не \ полностью-стековое (D246-амендмент §72 \ `is_fully_stack_value`), поэтому mut-binding над \ копией разделял бы кучевой storage с оригиналом: \ запись через новый mut-binding была бы видна \ оригиналу/вызывающему точно так же, как в L1-Ident \ канале ([M-router-handler-mut-capture-escape-\ soundness] §2, срочный пакет звучности, owner \ decision 2026-08-01). Решения: (a) сделай метод \ `mut @method` (receiver уже mut — launder не \ нужен, можно писать через поле напрямую); \ {solution_b}(c) оставь цель тоже \ `ro` (поле только читается)." ), value.span, )); } } } } /// Ф.1: проверить `let <name> <ann> = <value>` на совместимость. fn f1_check_assign_let( &self, value: &Expr, ann: &TypeRef, name: &str, binding_mut: bool, gs: &GenericScope, scope: &HashMap<String, TypeRef>, errors: &mut Vec<Diagnostic>, ) { // Plan 172.1 [literal-coercion channel] (§0/§1): materialize the sized coercion of // a context-typed literal into the channel (`ro x uint = 0x80` → the literal carries // `uint`, not the collapsed `nova_int`). DEFINITE site — this binding commits to // `ann` regardless of the `assignable` verdict below (an error aborts codegen). self.materialize_literal_coercion(value, ann); // [M-fn-value-binding-untyped-silent] (реестр 221.1 №101): a bare // fn-VALUE RHS against a non-func-compatible annotation (`ro t1 str = // test1`) — narrow, call-site-scoped check (see its own doc for why // this is NOT folded into `assignable`/`assignable_direct`). Checked // BEFORE `assignable` (mirrors `check_closure_scalar_return`'s own // ordering for the return-position sibling) — `assignable` would // otherwise silently accept it (`Func` → `Any` via `resolved_cat_of`). if self.check_fn_value_mismatch(value, ann, scope, errors) { return; } // [M-closure-param-fn-newtype-field-access-int-miscompile] (реестр // 221.1 №104): a closure LITERAL RHS with no explicit param/return // types (`|req| req.path`) relies entirely on emit_c's `fn_param_sigs` // legacy derivation for its OWN params' C types — that derivation only // recognized a BARE `fn(...) -> ...` let-annotation (`decl.ty` // structurally `TypeRef::Func`), silently falling to the // `nova_int`-for-every-param bootstrap default for a NAMED fn-newtype // annotation (`type Handler fn(ServerRequest) -> str`) instead — // `req.path` then miscompiled as an int-field access // (`(nova_int)(req.path)` / int_to_str). `infer_expr_type` has NO arm // for `ExprKind::Closure*` at all (never types a closure's OWN // ExprId), so this channel slot is otherwise VOID for a closure RHS — // filling it with the let-annotation's raw `ResolvedType` here is // ADDITIVE (nothing else ever writes this slot for a closure), not an // overwrite of any existing fact. The EXISTING emit_c channel-consumer // (`resolved_types.get(&decl.value.id)` → `fn_newtype_sigs` peel for a // `Named` fn-newtype, OR direct for a bare `Func` — Plan 228's // `[M-nested-fn-newtype-bind-then-call-broken]` unified HOF-binding // registration) already reads this exact slot; this just extends // WHICH RHS SHAPES populate it (Ident/Call already did; closures did // not). if value.id.is_set() && matches!(value.kind, ExprKind::ClosureLight { .. }) { self.resolved_types_buf.borrow_mut().insert(value.id, ResolvedType::from_type_ref(ann)); } // №375 (window p375-ptr2): `ro q *mut T = &b` over a readonly `b` — // checked at the annotation, independent of `assignable`'s structural // Ptr-category compat (which cannot see the mut/readonly pointee split). self.check_addrof_mut_from_ro_source(value, ann, errors); match self.assignable(value, ann, gs, gs, scope) { Compat::Bad { found } => { errors.push( Diagnostic::new( format!( "[E7301] cannot assign value of type `{}` to `{}` \ declared as `{}`", found, name, typeref_display(ann), ), value.span, ) .with_note_at( "type expected because of this annotation".to_string(), ann.span(), ), ); } // Plan 142 (D227): целочисленный литерал вне диапазона sized-типа. Compat::OutOfRange { msg } => { errors.push( Diagnostic::new( format!("[E_LIT_OUT_OF_RANGE] {msg}"), value.span, ) .with_note_at( "type expected because of this annotation".to_string(), ann.span(), ), ); } // [M-scalar-nonliteral-narrowing-not-enforced] (D54). Compat::Narrowing { from, to } => { errors.push( Diagnostic::new( format!( "[E_IMPLICIT_NARROWING] cannot assign value of type `{}` \ to `{}` of narrower type `{}` — implicit int narrowing \ loses range; use an explicit `... as {}` cast (D54)", from, name, to, to, ), value.span, ) .with_note_at( "type expected because of this annotation".to_string(), ann.span(), ), ); } // Plan 214.1 (D429 amend, R3'): ≥2 GENERIC `#coerce` patterns // unify to the SAME (I,O) pair at this position — `msg` is // already a fully-formed diagnostic (see `Compat::CoerceConflict` // doc). Compat::CoerceConflict { msg } => { errors.push(Diagnostic::new(msg, value.span)); } Compat::Ok | Compat::Unknown => {} } // D176 (Plan 108): `readonly T → T` is forbidden (E_READONLY_COERCE). // `T → readonly T` is allowed (auto-coerce, narrowing rights). // // **Plan 147 Ф.3 (D246):** coercion is decided by the target's L2 // *content-view*, which combines the explicit type modifier (L2) with // the binding (L1, P7 «bare `ro x` = freeze»): // - explicit `ro T` annotation → ro content-view (freeze) // - explicit `mut T` annotation → mut content-view // - bare `T` under a `ro` binding → ro content-view (P7 freeze) // - bare `T` under a `mut` binding → mut content-view // A ro source coerced into a mut content-view is `E_READONLY_COERCE`; // into a ro content-view it is OK. This makes the oracle row-D split // work: `-> ro Value` then `ro a Value = f()` ✅ (target frozen by the // `ro` binding) vs `mut a Value = f()` ❌ (mut content-view). let target_content_is_mut = Self::let_target_content_is_mut(Some(ann), binding_mut); // D246-амендмент ([M-ro-launder-via-mut-binding], Ф.1): checks BOTH // the L2 axis (value_ty.is_readonly()) and the L1 axis (value is a // bare Ident bound through `ro_binding_names`) — see the shared // helper's doc comment above for the full rationale. self.check_readonly_source_coerce(value, target_content_is_mut, scope, errors); } // ── Plan 124.2 (D221): pattern destructure priv-field check ─────────── // // Helper recursively walks a Pattern; when it encounters Pattern::Record // on a type with priv fields AND outside type-method scope, emits // E_PRIV_FIELD_PATTERN per priv field. Recurses into sub-patterns // (nested destructure) using the corresponding RecordField type. // /// Plan 124.6 (D224 §4): unified priv access predicate. Returns true if /// current fn body has priv access to `tname` (i.e. may read/write/ /// init/destructure priv-marked fields of that type). /// /// D224 §4 combined predicate — allowed when ANY of: /// 1. current_recv_type == tname — canonical type-method scope (D220). /// 2. in_test_block && same_module(current_module, type_defining_modules[tname]) /// — implicit test grant: any test block in the same module may access /// priv fields of types declared in that module (D224 §4 rule-2). /// 3. in_test_block && tname ∈ test_block_test_access /// — explicit `test "…" #test_access(T) { }` grant (D224 §4 rule-3). /// 4. current_recv_type ∈ type_pub_to[tname] /// — type-level friend via `#pub_to(FriendType)` on the type decl /// (D224 §4 rule-4). Per-field visible_to (rule for individual fields) /// is handled at call sites via `priv_field_access_allowed`. /// /// Legacy fn-level escape hatch (`#test_access(T)` on an `fn` decl, /// from current_fn_test_access) is also preserved as a sub-case of /// rule-3 for backward compat with D225. fn priv_access_allowed_base(&self, tname: &str) -> bool { // Rule 1: inside the type's own method body. let current_recv = self.current_recv_type.borrow(); if current_recv.as_deref() == Some(tname) { return true; } // Rule 2 + 3 (test-block rules). if self.in_test_block.get() { // Rule 2: implicit same-module grant inside a test block. let current_mod = self.current_module.borrow(); if !current_mod.is_empty() { if let Some(def_mod) = self.type_defining_modules.get(tname) { if def_mod.as_slice() == current_mod.as_slice() { return true; } } } // Rule 3a: explicit test_access list on the TestDecl itself. if self.test_block_test_access.borrow().iter().any(|t| t == tname) { return true; } } // Rule 3b (legacy): fn-level #test_access(T) escape hatch (D225). if self.current_fn_test_access.borrow().iter().any(|t| t == tname) { return true; } // Rule 4: type-level friend (`#pub_to(FriendType)` on tname's TypeDecl). if let Some(cur) = current_recv.as_deref() { if let Some(pub_to) = self.type_pub_to.get(tname) { if pub_to.iter().any(|f| f == cur) { return true; } } } false } /// Plan 160 (D281) Ф.2: true when the caller is in the SAME module as /// the type `tname` was declared in. Used for `priv` (module-private) boundary: /// fields with `priv_module_field=true` are allowed within the module. /// /// Conservative (deny) when: /// - `type_defining_modules` has no entry for `tname` (unknown origin), /// - `current_module` is empty (no module context set), /// - the modules differ. fn module_priv_access_allowed(&self, tname: &str, at: crate::diag::Span) -> bool { let Some(def_mod) = self.type_defining_modules.get(tname) else { return false; }; let current = self.current_module.borrow(); if !current.is_empty() && def_mod.as_slice() == current.as_slice() { return true; } // D281 follow-up (2026-07-08): код судим по модулю ЕГО файла, не по // current_module CU — тело generic-метода модуля-владельца, // перечитываемое в чужом CU (mono/prelude-merge), легально читает // module-private поля своего модуля. Без этого `type X priv {}` на // generic-типах ломал собственные итераторы (HashMapIter → buckets). match self.file_modules.borrow().get(&at.file_id) { Some(span_mod) => def_mod.as_slice() == span_mod.as_slice(), None => false, } } /// Plan 162 Ф.3: returns true iff `method_name` on type `type_name` is an /// **inherent** method — i.e. declared in the SAME module as `type_name`. /// /// Inherent: `type_method_map[type_name][method_name]` contains at least /// one module_name equal to `type_defining_modules[type_name]`. /// /// Extension: method declared in a DIFFERENT module (no overlap). /// /// Returns false (treated as extension / unknown) when: /// - type_name not in type_defining_modules (unknown origin), /// - method_name not in type_method_map[type_name], /// - no module_name in the method list matches the type's defining module. fn is_inherent_method(&self, type_name: &str, method_name: &str) -> bool { let Some(type_module) = self.type_defining_modules.get(type_name) else { return false; }; let Some(type_methods) = self.type_method_map.get(type_name) else { return false; }; let Some(method_modules) = type_methods.get(method_name) else { return false; }; method_modules.iter().any(|m| m == type_module) } /// Plan 162 Ф.5: extension method policy enforcement. /// /// Called when a method `method_name` IS found in `method_table` for type /// `type_name`. Emits: /// - `[E_EXTENSION_METHOD_NEEDS_IMPORT]` if the method is a pure extension /// (declared in a DIFFERENT module than the type) and no declaring module /// is imported in the current file. /// - `[E_METHOD_AMBIGUOUS]` if two or more extension modules providing this /// method are both imported. /// /// Conservative (no error) when: /// - `type_defining_modules` has no entry for the type (unknown origin), /// - `type_method_map` has no entry for the method (synthetic / no attribution), /// - at least one declaring module equals the type's defining module (inherent), /// - exactly one extension module is imported. fn check_extension_method_policy( &self, type_name: &str, method_name: &str, span: Span, errors: &mut Vec<Diagnostic>, ) { // Extension method policy only fires for call sites in ENTRY-MODULE // peer files. Checking imported module bodies (stdlib methods calling // other stdlib methods) would cause false positives because transitive // method bodies are not "user code" — their imports are not in scope. // Conservative: if entry_file_ids is empty (single-file legacy, no // peer_files), skip. if !self.entry_file_ids.is_empty() && !self.entry_file_ids.contains(&span.file_id) { return; } // Conservative: if we can't determine the type's defining module, allow. let Some(type_module) = self.type_defining_modules.get(type_name) else { return; }; // Extension method policy only applies to USER-DEFINED types. // Stdlib types (str, int, bool, Option, etc.) have methods spread // across multiple prelude/runtime modules — these are all part of the // standard library and implicitly available without explicit import. // Skip the check if the type's defining module starts with "std", // "prelude", or "runtime" (all stdlib conventional prefixes). // Additionally treat any method whose declaring module also starts // with "std"/"prelude"/"runtime" as inherent (see partition below). let is_stdlib_module = |m: &Vec<String>| { matches!(m.first().map(|s| s.as_str()), Some("std") | Some("prelude") | Some("runtime")) }; if is_stdlib_module(type_module) { return; } let Some(type_methods) = self.type_method_map.get(type_name) else { return; // no method attribution — could be synthetic; allow }; // Methods are stored in type_method_map with their original AST name, // which includes the leading '@' for instance methods (e.g. "@doubled"). // The call-site `method_name` arrives WITHOUT '@' (plain "doubled"). // Try both the bare name and the '@'-prefixed form. let bare_name = method_name.trim_start_matches('@'); let at_name = format!("@{bare_name}"); let method_modules = type_methods .get(bare_name) .or_else(|| type_methods.get(at_name.as_str())); let Some(method_modules) = method_modules else { return; // method not in type_method_map — synthetic or compiler-generated; allow }; // Partition into inherent (same module as type) and extension (different). // A method whose declaring module equals the type's defining module is // INHERENT. A method from a DIFFERENT module is EXTENSION. // Additional rule: a method from ANY stdlib module ("std", "prelude", // "runtime" first-segment) is treated as inherent regardless of exact // module match — stdlib methods are always implicitly available. let mut extension_modules: Vec<&Vec<String>> = Vec::new(); let mut has_inherent = false; for mod_name in method_modules { if mod_name == type_module || is_stdlib_module(mod_name) { has_inherent = true; } else { extension_modules.push(mod_name); } } // Inherent methods are always accessible when the type is in scope. if has_inherent { return; } // Pure extension: check which extension modules are imported. if extension_modules.is_empty() { return; // no attribution — allow } // Check import status by last segment of module path (or alias). // Use entry_imported_modules (direct imports of entry-module files only) // to exclude transitive imports from non-entry peers. let imported_ext: Vec<&Vec<String>> = extension_modules .iter() .filter(|m| { m.last().map_or(false, |last| self.entry_imported_modules.contains(last.as_str())) }) .cloned() .collect(); if imported_ext.is_empty() { // No extension module imported — require explicit import. let hint = extension_modules[0].last().map(|s| s.as_str()).unwrap_or("?"); errors.push(Diagnostic::new( format!( "[E_EXTENSION_METHOD_NEEDS_IMPORT] extension method `{}.{}()` \ requires the defining module to be imported. \ Hint: add `import ...{{{}}}` or import the module \ that declares this extension method.", type_name, bare_name, hint, ), span, )); } else if imported_ext.len() > 1 { // Multiple extension modules imported — ambiguous. let mod_names: Vec<String> = imported_ext .iter() .map(|m| m.join(".")) .collect(); errors.push(Diagnostic::new( format!( "[E_METHOD_AMBIGUOUS] extension method `{}.{}()` is ambiguous: \ defined in multiple imported modules ({}). \ Hint: import only one extension module that provides this method.", type_name, bare_name, mod_names.join(", "), ), span, )); } // Exactly one extension module imported — OK, no error. } /// Combines `priv_access_allowed_base(tname)` с per-field visible_to: /// true if access allowed, considering field's friend list. fn priv_field_access_allowed(&self, tname: &str, visible_to: &[String]) -> bool { if self.priv_access_allowed_base(tname) { return true; } let current_recv = self.current_recv_type.borrow(); if let Some(cur) = current_recv.as_deref() { if visible_to.iter().any(|t| t == cur) { return true; } } false } // Pattern::Or → recurses into all alternatives (same scrutinee_ty). // Pattern::Binding → recurses into inner. // Pattern::Tuple — Plan 124.4 covers tuple priv; here no-op (no type // info per-position without a concrete TupleType for ad-hoc tuples). // Pattern::Variant — sum-variant pattern; field privacy is encoded // inside variant ctor (out of scope V1). // // The `rest: bool` flag on Pattern::Record marks `..` syntactic // presence; it does NOT bind anything, so does NOT leak priv (D221 §3). /// №145 (D215/D221/D222 gap 3, owner ruling 2026-07-27): a bracket kind /// encodes HOW fields are addressed — round for POSITION, curly for /// NAME. A positional tuple (`type X(T, U)`, unnamed fields) destructures /// `ro (a, b) = t` only; a NAMED tuple (`type X(a T, b U)`, D215) HAS /// names, so it destructures `ro { a, b } = t` only — the round form is /// forbidden by the canon ("iначе вторая дверь и хрупкость: перестановка /// полей при рефакторинге молча меняет значения местами"). Before this /// check the round form on a named tuple compiled all the way to a /// CC-FAIL deep in codegen (`initializing '_NovaTuple2' with an /// expression of incompatible type 'NovaTuple_X'` — the legacy /// positional-tuple lowering, `_NovaTupleN`, was never taught about the /// `NovaTuple_X` named-tuple ABI) — an un-actionable internal-C leak /// instead of a clear Nova-level diagnostic pointing at the canon. /// Scope: the DIRECT `Stmt::Let` scrutinee only (mirrors /// `check_priv_pattern_recursive_inner`'s own `Stmt::Let`-only D411 /// `enforce_binding_rest` scoping) — nested nested Pattern::Tuple /// sub-patterns are `t_provides_field`-agnostic (no concrete element /// type per position for an ad-hoc tuple) and are left to the existing /// (permissive) sub-pattern walk, matching the pre-existing scope note /// on `check_priv_pattern_recursive` just above. fn check_positional_destructure_on_named_tuple( &self, pattern: &Pattern, scrutinee_ty: Option<&TypeRef>, errors: &mut Vec<Diagnostic>, ) { let Pattern::Tuple(_, span) = pattern else { return }; let tname_opt: Option<&str> = match scrutinee_ty { Some(TypeRef::Named { path, .. }) => path.last().map(|s| s.as_str()), Some(TypeRef::Readonly(inner, _)) => match inner.as_ref() { TypeRef::Named { path, .. } => path.last().map(|s| s.as_str()), _ => None, }, _ => None, }; let Some(tname) = tname_opt else { return }; let Some(td) = self.types.get(tname) else { return }; if let TypeDeclKind::NamedTuple(fields) = &td.kind { let field_list: Vec<&str> = fields.iter().map(|f| f.name.as_str()).collect(); errors.push(Diagnostic::new( format!( "[E_NAMED_TUPLE_POSITIONAL_DESTRUCTURE] named tuple `{ty}` \ cannot be destructured with the POSITIONAL form `(…)` — \ it has field names, so it destructures by NAME only \ (D215/D221/D222 canon: round brackets address by \ position, curly brackets address by name; a named \ tuple always has names). Hint: use `{{ {fields} }}` \ instead of `({ph})` (partial lists are allowed with an \ explicit `..`, e.g. `{{ {first}, .. }}`).", ty = tname, fields = field_list.join(", "), ph = field_list.iter().map(|_| "_").collect::<Vec<_>>().join(", "), first = field_list.first().copied().unwrap_or("field"), ), *span, )); } } fn check_priv_pattern_recursive( &self, pattern: &Pattern, scrutinee_ty: Option<&TypeRef>, errors: &mut Vec<Diagnostic>, ) { self.check_priv_pattern_recursive_inner(pattern, scrutinee_ty, false, errors) } /// D411 (Plan [M-d411-record-binding-destructuring]): same walk as /// `check_priv_pattern_recursive`, plus (when `enforce_binding_rest`) /// the record-binding `..`-partial rule: `ro {a, ..} = e` требует /// explicit `..` когда перечислена НЕ вся схема типа (иначе /// `E_RECORD_PATTERN_NEEDS_REST`). Только для `ro`/`mut` bindings /// (`Stmt::Let` walk, `enforce_binding_rest = true`) — match-arms / /// for-loop destructure (остальные call-sites) остаются permissive /// (`enforce_binding_rest = false`), не в зоне D411. /// /// Единая функция (не дубль): переиспользует уже посчитанный `metas` /// (field-schema резолв типа) вместо повторного лукапа `self.types`. fn check_priv_pattern_recursive_inner( &self, pattern: &Pattern, scrutinee_ty: Option<&TypeRef>, enforce_binding_rest: bool, errors: &mut Vec<Diagnostic>, ) { match pattern { Pattern::Record { type_path, fields, rest, span } => { let tname_opt: Option<String> = type_path .as_ref() .and_then(|p| p.last().cloned()) .or_else(|| match scrutinee_ty { Some(TypeRef::Named { path, .. }) => path.last().cloned(), Some(TypeRef::Readonly(inner, _)) => match inner.as_ref() { TypeRef::Named { path, .. } => path.last().cloned(), _ => None, }, _ => None, }); let Some(tname) = tname_opt else { return }; let Some(td) = self.types.get(tname.as_str()) else { return }; // Plan 124.2 + 124.4 + 124.6 (D221+D222+D224): unified priv // check (Record + NamedTuple). Per-field visible_to taken into // account (Plan 124.6). // Plan 160 (D281) Ф.2: priv_module_field added for module-boundary. struct FieldMeta { name: String, priv_field: bool, priv_module_field: bool, visible_to: Vec<String>, ty: TypeRef } let metas: Vec<FieldMeta> = match &td.kind { TypeDeclKind::Record(rec_fields) => rec_fields.iter().map(|f| FieldMeta { name: f.name.clone(), priv_field: f.priv_field, priv_module_field: f.priv_module_field, visible_to: f.visible_to.clone(), ty: f.ty.clone(), }).collect(), TypeDeclKind::NamedTuple(nt_fields) => nt_fields.iter().map(|f| FieldMeta { name: f.name.clone(), priv_field: f.priv_field, priv_module_field: f.priv_module_field, visible_to: f.visible_to.clone(), ty: f.ty.clone(), }).collect(), _ => return, }; let base_allowed = self.priv_access_allowed_base(tname.as_str()); if !base_allowed { for pf in fields { if let Some(meta) = metas.iter().find(|m| m.name == pf.name) { if meta.priv_field && !self.priv_field_access_allowed(tname.as_str(), &meta.visible_to) { // Plan 160 (D281) Ф.2: module-private pattern check. if meta.priv_module_field { if !self.module_priv_access_allowed(tname.as_str(), pf.span) { errors.push(Diagnostic::new( format!( "[E_FIELD_MODULE_PRIVATE] cannot destructure \ module-private field `{}.{}` in pattern from \ outside its module. Type declared with \ bare `priv` (Plan 160 / D281). Hint: use \ public accessor methods of `{}`.", tname, pf.name, tname, ), pf.span, )); } } else { errors.push(Diagnostic::new( format!( "[E_PRIV_FIELD_PATTERN] cannot destructure \ private field `{}.{}` в pattern outside type-\ method scope. Field marked `priv` (Plan 124 / \ D220/D221/D222). Hint: bind the value to a variable \ and access via public methods of `{}`, move \ destructure into a method of `{}`, or use \ `#test_access({})` (D225).", tname, pf.name, tname, tname, tname, ), pf.span, )); } } } } } // D411: `..`-partial rule — только для ro/mut binding-walk // (enforce_binding_rest). Частичный список полей (меньше, // чем полная схема типа) без `..` — нечестный сигнал «беру // всё», когда на деле берётся часть. `metas` уже несёт // полную схему (Record/NamedTuple), fields — то, что // перечислено в pattern; `*rest` — присутствие `..`. if enforce_binding_rest && !*rest && fields.len() < metas.len() { let listed = fields.iter().map(|f| f.name.as_str()) .collect::<Vec<_>>().join(", "); errors.push( Diagnostic::new( format!( "[E_RECORD_PATTERN_NEEDS_REST] record-pattern binding lists {} \ of {} field(s) of `{}` without `..` — a partial field list \ requires an explicit `..` to mark the rest as intentionally \ ignored (D411).", fields.len(), metas.len(), tname, ), *span, ) .with_note( "a FULL field list (every field of the type named) needs no `..`; \ a PARTIAL list must say so explicitly — this is not inferred from \ field count alone.", ) .with_note(format!( "add `..`: `{{ {}, .. }}`", listed, )), ); } // Recurse into sub-patterns with resolved sub-field type. for pf in fields { if let Some(sub) = &pf.pattern { let sub_ty = metas.iter() .find(|m| m.name == pf.name) .map(|m| m.ty.clone()); self.check_priv_pattern_recursive_inner( sub, sub_ty.as_ref(), enforce_binding_rest, errors, ); } } } Pattern::Or { alternatives, .. } => { for alt in alternatives { self.check_priv_pattern_recursive_inner( alt, scrutinee_ty, enforce_binding_rest, errors, ); } } Pattern::Binding { inner, .. } => { self.check_priv_pattern_recursive_inner( inner, scrutinee_ty, enforce_binding_rest, errors, ); } // Variant/Tuple/Array/Wildcard/Ident/Literal — no priv-record // semantics at this level (handled in Plan 124.4 для tuple form). // D411 note: a record pattern NESTED inside a Tuple binding // element (`ro (a, {x, ..}) = pair`) does not get the // `..`-rest check here — Tuple elem types aren't resolved at // this call site (out of scope for [M-d411-record-binding- // destructuring]; direct record-field nesting IS covered above). _ => {} } } /// Plan 87 Ф.2.2: пройти тело for-in. При заданной аннотации типа /// элемента (`for x TYPE in`) loop-переменная-`Ident` получает этот /// тип в scope тела (save/restore — for-body не утекает в окружающий /// scope). Без аннотации scope не трогаем — поведение 1:1 до Plan 87. fn f1_for_body( &self, // аннотация ЛИБО inferred elem-тип (172.1.2) — источник вычислен caller'ом elem_type: &Option<TypeRef>, pattern: &Pattern, body: &Block, gs: &GenericScope, scope: &mut HashMap<String, TypeRef>, errors: &mut Vec<Diagnostic>, ) { match (elem_type, pattern) { (Some(ann), Pattern::Ident { name, .. }) => { let saved = scope.insert(name.clone(), ann.clone()); self.f1_block(body, gs, scope, errors); match saved { Some(prev) => { scope.insert(name.clone(), prev); } None => { scope.remove(name); } } } _ => self.f1_block(body, gs, scope, errors), } } /// Plan 87 Ф.3: проверить, что аннотация типа loop-переменной /// (`for x TYPE in iter`) совместима с фактическим типом элемента /// итератора. Несовпадение → E7340. Если тип элемента уверенно /// вывести не удалось — проверка пропускается (Compat::Unknown- /// философия Plan 79: никаких ложных срабатываний). fn f1_check_for_elem( &self, iter: &Expr, ann: &TypeRef, gs: &GenericScope, scope: &HashMap<String, TypeRef>, errors: &mut Vec<Diagnostic>, ) { let Some(elem_tr) = self.infer_iter_elem_type(iter, scope) else { return; }; // U.5.2: structured category (`resolved_cat_of` + `cat_compatible_rt`), // mirroring `assignable`. Permissive на `Any` (generic-параметр / // неизвестное / protocol). Транзитный parity-ассерт снимается в Phase B. let ann_rt = self.resolved_cat_of(ann, gs); let elem_rt = self.resolved_cat_of(&elem_tr, gs); if !cat_compatible_rt(&elem_rt, &ann_rt) { errors.push( Diagnostic::new( format!( "[E7340] for-in loop variable annotated as `{}`, \ but the iterator yields elements of type `{}`", typeref_display(ann), typeref_display(&elem_tr), ), ann.span(), ) .with_note_at( "iterator element type comes from here".to_string(), iter.span, ), ); } } /// Plan 87 Ф.3: best-effort вывод типа элемента for-in итератора. /// `None` — вывести не удалось (проверка аннотации пропускается). fn infer_iter_elem_type( &self, iter: &Expr, scope: &HashMap<String, TypeRef>, ) -> Option<TypeRef> { match &iter.kind { // `a..b` / `a..=b` — элементы int. ExprKind::Range { .. } => Some(prim_ref("int", iter.span)), // Литерал массива — тип из первого выводимого не-spread элемента. ExprKind::ArrayLit(elems) => { for el in elems { if let ArrayElem::Item(e) = el { if let Some(t) = self.infer_expr_type(e, scope) { return Some(t); } } } None } // Прочее: если выражение имеет тип `[]T` / `[N]T` — элемент `T`. // D176 (Plan 108): `readonly []T` → elements are `T` (primitive copy). _ => match self.infer_expr_type(iter, scope)? { TypeRef::Array(inner, _) | TypeRef::FixedArray(_, inner, _) => Some(*inner), TypeRef::Readonly(inner, _) => match *inner { TypeRef::Array(elem, _) | TypeRef::FixedArray(_, elem, _) => Some(*elem), _ => None, }, _ => None, }, } } /// Plan 172.1 U.4.4: the shared PRIMITIVE gate for checker-channel annotations /// (`Ident` var / `Member` field / `Index` element). A resolved type is /// primitive-lowerable iff its C-type is ALWAYS a registered builtin /// (`nova_int`/`nova_bool`/`uint32_t`/`nova_str`/…) — no undeclared-identifier, /// mono, or generic-inference hazard — so annotating it into `resolved_types_buf` /// and letting codegen consume it (U.4.4b authoritative flip) is sound. Non-primitive /// (records / generics / `Vec` / pointers / enums) need codegen mono/typedef context /// the generic-level annotation cannot reproduce → they stay on the legacy path /// (U.4.3(d)/U.4.5 territory). View axes (L2 `readonly`) are peeled — `readonly T` /// lowers identically to `T`. fn primitive_gate(rt: &ResolvedType) -> bool { // U.1.3b: single source — delegates to `ResolvedType::is_primitive_lowerable`, // the SAME gate codegen consumes for Gap B extern-method return indexing (§0/§3). rt.is_primitive_lowerable() } /// Plan 172.13 Ф.2 (constraint-core, 2026-07-10): route a single /// concrete-type gate through the shared `constraint_solver` type-set /// language instead of an inline hand-rolled boolean — the literal- /// coercion family's migration proof (see `docs/plans/ /// 172.13-constraint-inference.md` Ф.0 inventory, group C). Both sides /// are already fully known here (no unbound solver variable needed); /// this still genuinely routes the DECISION through `Constraint:: /// MemberOf` + `Solver::solve`, replacing per-site duplicated gate code /// with the one shared predicate language (`TypeSet`). fn ts_member(rt: &ResolvedType, set: constraint_solver::TypeSet) -> bool { constraint_solver::Solver::new() .solve(&[constraint_solver::Constraint::MemberOf( constraint_solver::Ty::Concrete(rt.clone()), set, )]) .is_ok() } /// [M-option-fn-field-record-literal-elem-type-int] (реестр 221.1 №116): /// is `rt` "concrete enough" for `materialize_literal_coercion`'s ctor arm /// (`Some`/`Ok`/`Err`) to safely stamp the call node's `resolved_types_buf` /// with `expected`? The pre-existing per-site gate (`Union(Primitive, /// ConcreteNamedNoArgs)`, still used AS-IS via `ts_member` for every /// non-`Func` leaf) has NO case for `ResolvedType::Func` at all — a /// `fn(...)->...` generic argument (`Option[fn(A) -> B]`) therefore NEVER /// counted as concrete, so `Some(closure)` against `Option[fn(A) -> B]` /// silently skipped materialization: the Option's element-type mono /// defaulted to `nova_int` downstream, CC-FAIL `NovaOpt_nova_int` vs /// `NovaOpt_NovaClos_*` (record-literal field, bare `let`, AND free-fn /// return position all share this one gate). Recurses into a `Func`'s OWN /// params/return — a `fn(fn(int)->str) -> fn(int)->str` payload is still /// "concrete" as long as every leaf ultimately is. fn ctor_arg_concrete(rt: &ResolvedType) -> bool { match rt { ResolvedType::Func { params, ret, .. } => { params.iter().all(Self::ctor_arg_concrete) && Self::ctor_arg_concrete(ret) } _ => Self::ts_member( rt, constraint_solver::TypeSet::Union(vec![ constraint_solver::TypeSet::Primitive, constraint_solver::TypeSet::ConcreteNamedNoArgs, ]), ), } } /// Plan 196 Ф.4b (constraint-core, Project): project a channel container /// `ResolvedType` (from `resolved_types_buf`) into the type of its element /// via `Constraint::Project`. The §0-canonical container→element rule lives /// in the SOLVER (`constraint_solver::project`), no longer duplicated inline /// across the Index-мост / Member tuple-field producers (Ф.0-инвентарь #3/ /// #137). Byte-parity: `project` reproduces EXACTLY the prior inline /// structural `match` of those producers — the gate «this is an indexable /// container / a `.N` tuple-field» stays at the PRODUCER (contextual AST /// knowledge), as the numeric gate stayed at the producer in `Join` (Ф.4a). /// `None` = undetermined projection → producer leaves the node un-annotated /// (honest «no annotation» → legacy navigation). fn project_channel( container: &ResolvedType, kind: constraint_solver::ProjectKind, ) -> Option<ResolvedType> { use crate::types::constraint_solver::{Constraint, Solver, Ty, VarGen}; let mut g = VarGen::new(); let out = g.fresh(); Solver::new() .solve(&[Constraint::Project { out: Ty::Var(out), container: Ty::from_resolved(container), kind, }]) .ok() .and_then(|sol| sol.type_of(out)) } /// Plan 196 Ф.4c — lift a `ResolvedType` into a solver `Ty`, mapping the /// carrier-generic NAMES (`vars`) to their FRESH `TypeVar` identities. Same /// structural expansion as `Ty::from_resolved`, except a bare carrier /// (`Named{name,args:[]}` / `TypeParam(name)` with `name ∈ vars`) becomes a /// `Ty::Var` so `unify` can bind it. Anti-d119 by construction: the var is a /// numeric identity minted per-resolve, never the spelling (see the /// `constraint_solver` module doc). Anything unrepresentable (module≠empty /// Named, effectful Func, Readonly/Mut/Ptr) falls to a `Ty::Concrete` leaf — /// safe, because a carrier buried under such a leaf simply fails to unify and /// the caller's byte-parity gate then defers to legacy. /// 196.5 Stage-D wave-2 (дыра-2): does `rt` RE-SPELL any of the callee's own /// generic names (`vars` keys) as a bare `Named{name}`/`TypeParam(name)` leaf? /// Such a value is a leaked DECLARATION spelling (an unsubstituted `T`/`U`), /// not a concrete type — feeding it to the solver as a `Ty::Concrete` leaf is /// exactly the d119 spelling-collision hazard (see `constraint_solver` module /// doc). Used to reject poisoned arg-type candidates in /// `resolve_return_channel` (producer B). fn rt_respells_names( rt: &ResolvedType, vars: &HashMap<String, constraint_solver::TypeVar>, ) -> bool { use ResolvedType as R; match rt { R::TypeParam(n) => vars.contains_key(n), R::Named { name, args, module } => { (module.is_empty() && args.is_empty() && vars.contains_key(name)) || args.iter().any(|a| Self::rt_respells_names(a, vars)) } R::Tuple(items) => items.iter().any(|i| Self::rt_respells_names(i, vars)), R::Array(inner) => Self::rt_respells_names(inner, vars), R::Func { params, ret, .. } => { params.iter().any(|p| Self::rt_respells_names(p, vars)) || Self::rt_respells_names(ret, vars) } R::TypedPtr(_, inner) | R::Readonly(inner) => Self::rt_respells_names(inner, vars), _ => false, } } /// [M-196-ch-widen] SHADOW-ICE root fix: does `rt` contain NO residual bare-name /// leaf that could be an UNSUBSTITUTED generic-parameter spelling leaked from an /// ENCLOSING (still-abstract) generic body? Nova's `TypeRef` has no dedicated /// type-parameter carrier (see `infer_method_call_channel_type`'s "K"-class doc /// comment, ~7982: "`self.types.get(\"T\")` finds nothing; \"T\" is not a /// registered type") — so `ResolvedType::from_type_ref` lowers a bare /// `Named{path:["K"]}` the SAME way whether "K" is a genuine 0-arg concrete type /// or an unbound carrier from an OUTER FnDecl the solver never saw (it only mints /// vars for THIS call's own `recv.generics`/`method_names` — `rt_respells_names` /// above catches a same-call collision, but is BLIND to an outer scope's carrier /// arriving as a plain, not-in-`vars` `Ty::Named`). Repro (found via git-archaeology /// triage per capstone §4.4): `Lru[K,V]`'s `put` body (still generic, K/V abstract /// at CHECK time) calls `@order.len()` where `@order []K` — `resolve_return_channel` /// unifies Vec.len()'s OWN carrier var against `Vec[Named("K")]` (K not in `vars`, /// since `vars` only knows Vec.len()'s "T") and reports `T = Named("K")` as if /// concrete — `shadow_check_node_substs` caught the resulting mismatch against /// codegen's mono-correct `T = str` (`node_substs[…][T]` lowered to the erased stub /// `Nova_K*`). A bare `Named{name, args:[]}` (no module qualifier) is residual /// UNLESS `name` is a REGISTERED type (`self.types` — every Record/Sum/Protocol/ /// Effect/alias/newtype declaration, including std types like `Vec`/`HashMap`); /// `ResolvedType::TypeParam` is unconditionally residual (that is its entire /// purpose). Recurses structurally (mirrors `rt_respells_names`/`mentions_slot`) /// so a leak buried in `Vec[K]`/`(K,V)`/`fn(K)->V` is caught too. Same completeness- /// gate discipline as the length check below: an unclosed value means the WHOLE /// map (or `rt`) stays unwritten, caller falls back to legacy — never a silent /// wrong materialization (propose-then-verify, card §5). fn rt_is_closed(&self, rt: &ResolvedType) -> bool { use ResolvedType as R; match rt { R::TypeParam(_) => false, R::Named { name, module, args } => { if module.is_empty() && args.is_empty() && !self.types.contains_key(name) { return false; } args.iter().all(|a| self.rt_is_closed(a)) } R::Tuple(items) => items.iter().all(|i| self.rt_is_closed(i)), R::Array(inner) | R::Readonly(inner) | R::TypedPtr(_, inner) => { self.rt_is_closed(inner) } R::FixedArray(_, inner) => self.rt_is_closed(inner), R::Func { params, ret, .. } => { params.iter().all(|p| self.rt_is_closed(p)) && self.rt_is_closed(ret) } _ => true, } } fn ty_from_resolved_vars( rt: &ResolvedType, vars: &HashMap<String, constraint_solver::TypeVar>, ) -> constraint_solver::Ty { use constraint_solver::Ty; use ResolvedType as R; match rt { R::TypeParam(n) if vars.contains_key(n) => Ty::Var(vars[n]), R::Named { name, args, module } if module.is_empty() => { if args.is_empty() { if let Some(v) = vars.get(name) { return Ty::Var(*v); } } Ty::Named { name: name.clone(), args: args.iter().map(|a| Self::ty_from_resolved_vars(a, vars)).collect(), } } R::Tuple(items) => { Ty::Tuple(items.iter().map(|i| Self::ty_from_resolved_vars(i, vars)).collect()) } R::Array(inner) => Ty::Array(Box::new(Self::ty_from_resolved_vars(inner, vars))), R::Func { params, ret, effects } if effects.is_empty() => Ty::Func { params: params.iter().map(|p| Self::ty_from_resolved_vars(p, vars)).collect(), ret: Box::new(Self::ty_from_resolved_vars(ret, vars)), }, other => Ty::Concrete(other.clone()), } } /// Plan 196 Ф.4c (constraint-core, Resolve): resolve the DECLARED return type /// of a simple (single-overload, concrete-receiver) instance method call /// through the constraint solver's `resolve_return` primitive (§0). The /// carrier-binding rule — «unify the declared receiver pattern with the /// concrete receiver, then instantiate the return» — lives in the SOLVER (on /// FRESH `TypeVar`s, anti-d119), no longer only in the name-keyed /// `build_recv_subst` + `subst_type_ref_pub` pair. Mirror of Ф.4b /// `project_channel`: the PRODUCER keeps the contextual gate (which overload, /// instance-receiver, no residual method-generic — all decided by the caller) /// and this routes the binding DECISION through the constraint core. /// /// `Self` is pre-bound to the concrete receiver in TypeRef-land (byte-parity /// with the legacy `subst.insert("Self", peeled)`) since the primitive models /// only carrier unification. A return still mentioning a method-level generic /// (`[U]` — bound from the arguments, class-(b)) bails to `None`: the solver /// would treat the surviving name as a spurious concrete leaf. /// /// `None` = the solver could not resolve a fully-concrete return (incompatible /// shape, module/effect-carrying type it cannot represent, or a residual /// var/method-generic) → the caller keeps its legacy binding. The caller /// additionally accepts this result ONLY when it reproduces the legacy binding /// as a round-trippable concrete type, so parity holds regardless of the /// channel's fidelity (propose-then-verify). /// /// Plan 196.4 Stage-1a: extended so a return mentioning a METHOD-level generic /// (`[U]` — `fn Vec[T] @map[U](f (T)->U) -> Vec[U]`, bound by the CALL'S /// ARGUMENTS, not the receiver) no longer unconditionally bails (the old /// `9624` guard). `method_names` now ALSO mints fresh solver `Var`s (same /// numeric space as the carrier — anti-d119 by construction: a carrier `T` /// and a method `T` sharing a spelling never collide, each is a distinct /// `TypeVar`). When the caller supplies the call's arguments /// (`call_args_scope`), each non-variadic `(declared param, arg)` pair whose /// declared type mentions a method generic contributes a `Constraint::Eq` — /// the SAME structural walk `unify_type` performs for the free-fn /// generic-return arm (`f1_check_call` `10452`-`10492`), expressed on fresh /// `Var`s instead of a name-keyed `HashMap`. A method-generic var left /// unbound (no `call_args_scope`, no matching param, or the arg's type is /// itself unresolved) simply stays a free `Var` — `as_concrete_leaf` then /// honestly reports `None` for the whole return, exactly mirroring the old /// bail's "no annotation" contract for the erased/unresolved case. /// Plan 196.5 Stage-A: `method_names_ordered` mirrors `method_names` (same set) but /// preserves the DECLARATION order (`FnDecl.generics` order) — the `HashSet` the rest of /// this fn uses has no stable order, but `node_substs` (§6.1) needs positional order for /// mono-manging. Threaded in ADDITIVELY by the two callers below (they already build it /// from `f.generics`, right next to the existing `HashSet` construction). fn resolve_return_channel( &self, recv: &Receiver, recv_ty: &TypeRef, peeled: &TypeRef, ret: &TypeRef, method_names: &HashSet<String>, method_names_ordered: &[String], method_params: &[Param], call_args_scope: Option<(&[CallArg], &HashMap<String, TypeRef>)>, // [M-196-producer-b-turbofish] (Plan 196 Producer B): an explicit METHOD-level // turbofish on an INSTANCE call (`obj.method[U](args)`) — positional, aligned to // `method_names_ordered` (mirrors `f1_check_call`'s free-fn/static-ctor turbofish // overlay, D310, ~11691: "explicit annotation is ground truth"). `None` for every // pre-existing caller (inferred-only call-sites — unchanged behavior, additive // parameter). Carrier names (`names` below) are NEVER turbofish-supplied on an // instance call — the receiver VALUE already fixes them; turbofish on `obj.method[..]` // binds ONLY the method's OWN generics (Nova has no receiver-generic turbofish syntax // on an instance call — `Type[T].method(...)` is the DISTINCT static-ctor AST shape, // excluded by the caller before this fn is ever invoked). explicit_method_type_args: Option<&[TypeRef]>, ) -> Option<(ResolvedType, Vec<(String, ResolvedType)>)> { use constraint_solver::{Solver, Ty, TypeVar, VarGen}; // Carrier generic names — bare single-segment, no args (same extraction // `build_recv_subst` performs). A FRESH var per name (numeric identity). let names: Vec<String> = recv .generics .iter() .filter_map(|g| match g { TypeRef::Named { path, generics, .. } if path.len() == 1 && generics.is_empty() => { Some(path[0].clone()) } _ => None, }) .collect(); let mut g = VarGen::new(); let mut vars: HashMap<String, TypeVar> = HashMap::new(); for n in &names { vars.entry(n.clone()).or_insert_with(|| g.fresh()); } // Stage-1a: method-level generics mint fresh vars TOO — see doc comment. for n in method_names { vars.entry(n.clone()).or_insert_with(|| g.fresh()); } // Declared receiver pattern: prefer the FULL structured form // (`receiver_ty`, the same source `build_recv_subst` unifies), else the // flat `Named{type_name, generics}`. let recv_pattern_tr: TypeRef = recv.receiver_ty.clone().unwrap_or_else(|| TypeRef::Named { path: vec![recv.type_name.clone()], generics: recv.generics.clone(), span: recv.span, }); let recv_pattern = Self::ty_from_resolved_vars(&ResolvedType::from_type_ref(&recv_pattern_tr), &vars); let concrete_recv = Self::ty_from_resolved_vars(&ResolvedType::from_type_ref(peeled), &vars); // Return template: pre-bind `Self` to the concrete receiver in TypeRef-land // (byte-parity with legacy), then lift carriers (+ method generics) to vars. let mut self_subst: HashMap<String, TypeRef> = HashMap::new(); self_subst.insert("Self".to_string(), peeled.clone()); let ret_self = crate::const_fn_trampoline::subst_type_ref_pub(ret, &self_subst); let ret_template = Self::ty_from_resolved_vars(&ResolvedType::from_type_ref(&ret_self), &vars); // Plan 196.5 producers-widen: carrier unify moved UP (was after `extra_eqs` // construction) so the Class-1 closure-return-peek fallback below can read // back a CONCRETE carrier binding (`carrier_tr_subst`) to seed a closure // literal's own params — a closure body like `|x| x * 2` needs `x`'s // concrete type (e.g. `int`, from the receiver's `T`) to infer correctly, // not just `Self`. Pure reordering for the pre-existing `a_rt`-driven path: // it never touched `solver` while building `extra_eqs`, so moving WHEN the // carrier unify happens relative to that construction changes nothing for // it — `solver.unify(recv_pattern, concrete_recv)` still runs exactly once, // still strictly before `extra_eqs` is consumed (`solver.unify(a, b)` below). let mut solver = Solver::new(); solver.unify(&recv_pattern, &concrete_recv).ok()?; let mut carrier_tr_subst: HashMap<String, TypeRef> = HashMap::new(); carrier_tr_subst.insert("Self".to_string(), peeled.clone()); for n in &names { if let Some(v) = vars.get(n) { if let Some(prt) = solver.as_concrete_leaf(&solver.resolve(&Ty::Var(*v))) { if let Some(tr) = Self::resolved_to_typeref_tp(&prt, recv.span) { carrier_tr_subst.insert(n.clone(), tr); } } } } // [M-196-producer-b-turbofish] Explicit method-level turbofish overlay: unify each // `method_names_ordered[i]`'s FRESH var directly against the WRITTEN type-arg, // positionally — same ground-truth-overlay contract as the free-fn/static-ctor D310 // turbofish overlay (`f1_check_call` ~11691) and the legacy codegen seed // (`resolve_method_level_subst`'s `explicit_tf`, `emit_c.rs` ~21347). Runs BEFORE // `extra_eqs` (args/closure-derived) below — union-find `unify` is order-independent // (a later `solver.unify(a,b)` on the same var still merges cleanly), so this only // ADDS ground truth, never races the args-derived pass. Guarded by the SAME // `rt_respells_names` poison check the arg-derived path uses (~10475): a written // type-arg that bare-spells one of THIS call's own fresh-var names (nested generic // body re-using a carrier/method spelling) is unification poison, not information — // skip it, leave the var free, honest fall-through (unresolved → whole map stays // unwritten via the completeness gate at fn exit, same as an inference miss). if let Some(explicit) = explicit_method_type_args { for (n, ta) in method_names_ordered.iter().zip(explicit.iter()) { if let Some(v) = vars.get(n.as_str()) { let ta_rt = ResolvedType::from_type_ref(ta); if !Self::rt_respells_names(&ta_rt, &vars) { let leaf = Self::ty_from_resolved_vars(&ta_rt, &vars); let _ = solver.unify(&Ty::Var(*v), &leaf); } } } } // Stage-1a: Constraint::Eq param↔arg for METHOD-level generics — see doc // comment. Only the params whose DECLARED type mentions a method // generic are considered (matches the targeted scan `resolve_ // method_return_with_closure_args` performs for the closure sub-case). let mut extra_eqs: Vec<(Ty, Ty)> = Vec::new(); if !method_names.is_empty() { if let Some((args, scope)) = call_args_scope { for (p, a) in method_params.iter().zip(args.iter()) { if p.is_variadic { break; } if !typeref_mentions_any(&p.ty, method_names) { continue; } let p_self = crate::const_fn_trampoline::subst_type_ref_pub(&p.ty, &self_subst); let p_lifted = Self::ty_from_resolved_vars(&ResolvedType::from_type_ref(&p_self), &vars); let a_rt = self .infer_expr_type(a.expr(), scope) .map(|t| ResolvedType::from_type_ref(&t)) .or_else(|| { if !a.expr().id.is_set() { return None; } self.resolved_types_buf.borrow().get(&a.expr().id).cloned() }) // 196.5 Stage-D wave-2 (дыра-2, D2H): an arg type that RE-SPELLS // any of THIS callee's own generic names (`fn(T)->U` buffered for // a closure literal whose registration didn't substitute the // carrier — observed for user generic records: `D119Box[T]@map[U]` // arg buffered as `Func{[Named"T"], Named"U"}`) is unification // POISON, not information: `Ty::from_resolved` lifts the bare // `Named{T}` as a CONCRETE leaf (the d119 spelling hazard this // solver's fresh-var design exists to avoid), the pair-unify then // conflicts with the already-bound carrier var (`Var(T)=int` vs // `Named{T}`) and the WHOLE eq is atomically dropped — `U` never // binds, `as_concrete_leaf` honestly `None`s, and producer B // stays silent for the ENTIRE user-record method-generic class. // Rejecting the poisoned value falls through to the closure- // return peek below (`fpp_concrete` reseeded from the solver's // OWN carrier binding), which resolves the same call correctly. .filter(|rt| !Self::rt_respells_names(rt, &vars)); if let Some(a_rt) = a_rt { extra_eqs.push((p_lifted, Ty::from_resolved(&a_rt))); } else if let TypeRef::Func { params: fpp, return_type: Some(fr), .. } = &p_self { // [M-196.5 producers-widen, Class-1] closure-return-bound: // `p_self` is Self-substituted but still carrier-generic // (`self_subst` only carries "Self") — reseed `fpp` with the // CONCRETE carrier binding (`carrier_tr_subst`, resolved from // THIS solver above) before peeking, so a closure param typed // by the receiver's carrier (`.map(|x| …)` on `Vec[int]`, `x: // int`) infers correctly. `unresolved` stays `method_names` // only — carrier names are now concrete in `fpp_concrete`. let fpp_concrete: Vec<TypeRef> = fpp .iter() .map(|t| crate::const_fn_trampoline::subst_type_ref_pub(t, &carrier_tr_subst)) .collect(); if let Some(body_ty) = self.closure_arg_return_peek(&fpp_concrete, a.expr(), scope, method_names) { let fr_lifted = Self::ty_from_resolved_vars(&ResolvedType::from_type_ref(fr), &vars); let body_lifted = Self::ty_from_resolved_vars( &ResolvedType::from_type_ref(&body_ty), &vars); extra_eqs.push((fr_lifted, body_lifted)); } } } } } for (a, b) in &extra_eqs { // Best-effort per-pair (mirrors `f1_check_call`'s `let _ = // unify_type(..)`, `10477`): one param's structural conflict (an // unrepresentable shape collapsing to a mismatched `Ty::Concrete` // leaf) must not block a DIFFERENT method-generic from binding. let _ = solver.unify(a, b); } let resolved_ret = solver.resolve(&ret_template); let rt = solver.as_concrete_leaf(&resolved_ret)?; // [M-196-ch-widen] SHADOW-ICE fix: the return itself must be genuinely closed // too (see `rt_is_closed` doc) — an outer-scope carrier leaking into the // RETURN position is the same hazard as leaking into a per-name `ordered` // entry, just not observed empirically for `len()`-shaped returns (a fixed // primitive) — defense in depth, same gate. if !self.rt_is_closed(&rt) { return None; } // [M-196.5-node-substs] Producer B: `solver.subst` (via `solver.resolve`/ // `as_concrete_leaf` per-var) already has EVERY carrier + method-level binding — // Stage-1a minted a fresh `Var` for each (`vars`, above) and unified them. Read the // SAME solver, per declared name, instead of discarding the bindings with `solver` // at fn exit. Declaration order = carrier names (`names`, receiver order) then // method-level (`method_names_ordered`, caller-supplied `FnDecl.generics` order) — // mirrors rustc's `SubstsRef` (early carrier params, then late method params). // Same completeness gate as producer A: a residual (unresolved) var anywhere in the // declared set means the WHOLE map stays unwritten for this call-site (caller // decides — this fn only proposes the value, `&self` has no `call_id` here). let decl_order: Vec<&String> = names.iter().chain(method_names_ordered.iter()).collect(); let ordered: Vec<(String, ResolvedType)> = decl_order .iter() .filter_map(|n| { vars.get(n.as_str()).and_then(|v| { solver .as_concrete_leaf(&solver.resolve(&Ty::Var(*v))) .map(|prt| ((*n).clone(), prt)) }) }) .collect(); let ordered = if !decl_order.is_empty() && ordered.len() == decl_order.len() && ordered.iter().all(|(_, v)| self.rt_is_closed(v)) { ordered } else { Vec::new() }; Some((rt, ordered)) } /// Plan 172.1 U.4.4 (Match-arm half): the COMMON result type of a `match` /// expression's arms, materialized for the checker channel. `infer_expr_type` /// does NOT derive a match type (returns `None`), so the match's result type is /// re-derived in codegen (the `emit_match` `result_ty` pass + the legacy /// `infer_expr_c_type` Match arm) — §0 fragmentation. This is the bounded /// inference: a match's value type is the common type of its arm bodies; a /// diverging arm (`never`: `throw` / `interrupt`) does NOT constrain it. /// /// Returns `Some(rt)` iff EVERY non-diverging arm body resolves (via /// `infer_expr_type`, in the OUTER scope) to the SAME type AND that type is a /// NON-UNIT primitive (`primitive_gate`; `Unit` is excluded so a statement- /// position match keeps the legacy unit-domination path untouched). Maximally /// conservative: ANY arm whose body type is unresolvable here (a pattern-binding /// reference — bindings are absent from the outer scope; a block with statements; /// a generic / record / `Vec` result) → `None` → the whole match bails to legacy. /// Mirror of the `Ident` (U.4.4b) / `Member` / `Index` flips; for the gated set /// it is byte-identical to the legacy Match-arm inference (which already computes /// the same primitive via authoritative arm-body inference) — it consolidates the /// CONSUMER side (`infer_expr_c_type`) onto the channel. /// 2026-07-02 (tally АТОМ 2a): типы pattern-биндингов арма из scrut_ty — /// консервативное подмножество: `Some(x)`/`Ok(x)`/`Err(x)` при конкретных /// generic-args скрутини; одно-payload вариант non-generic user-sum /// (declared TypeRef); catch-all `v =>` (тип = scrut_ty). Всё прочее — /// пустой результат (арм-инференция в наружном scope, как раньше). /// generic-match-scope-gap fix (`[M-202-ident-x-module-alias-collision]` /// follow-up, 2026-07-21): narrow fallback used ONLY to feed /// `match_arm_bindings` when the general `infer_expr_type(scrutinee, ..)` /// returns `None` for a `match @method() { ... }` scrutinee whose /// receiver is an ABSTRACT generic type-param bound to a known protocol /// (e.g. `@next()` on receiver `I` inside `fn[I Next[T], T] I mut @min()`, /// `std/src/collections/vec_iter/core.nv`/`vec_lazy/core.nv`). Repro: a /// generic fn's body does `match @next() { Some(x) => x.compare(...) }` — /// `infer_expr_type`'s `Call` arm deliberately does NOT resolve a general /// instance-method-call return (see its own doc, ~15605), so `scrut_ty` /// was `None` and `x` never entered `scope` — any USER import whose last /// path segment happened to equal the bare bind-name (`x`) then made /// `f1_check_call`'s `ExprKind::Member` dispatch misread `x.compare(..)` /// as a call to a nonexistent free function `compare` in module `x` /// (false `[E7401]`). Renaming the bind (`x`→`cand`, done at the time) /// only hid the SYMPTOM; this closes the underlying scope-gap so ANY /// bind-name is safe, not just non-colliding ones. /// /// Resolution: `obj`'s scope-type (`self.infer_expr_type`) must be a bare /// `Named{path:[P], generics:[]}` where `P` is one of the CURRENT fn's own /// generic params (`current_fn_generics`, RAII-published by /// `f1_check_fn` — see field doc). Walk `P`'s declared bounds; the first /// bound that names a `TypeDeclKind::Protocol` declaring a 0-arg method /// matching `method` wins — its return type, with the PROTOCOL's own /// generics (`Next[T]`'s `T`) substituted by the bound's concrete type /// ARGS at this call-site (`subst_type_ref_pub`, `const_fn_trampoline.rs` /// — the same generic-param substitution every OTHER mono/subst consumer /// in this codebase uses). Multi-arg protocol methods, non-protocol /// bounds, and non-generic-param receivers all fall through to `None` /// (unsupported — no regression, just no additional coverage): this is /// intentionally narrow, scoped to the ONE consumer site /// (`ExprKind::Match`'s scrutinee) — does NOT touch `infer_expr_type`'s /// general Call-arm decoupling (per that fn's own doc, changing it there /// would ripple to every other consumer). fn resolve_generic_bound_method_return( &self, scope: &HashMap<String, TypeRef>, obj: &Expr, method: &str, ) -> Option<TypeRef> { let obj_ty = self.infer_expr_type(obj, scope)?; let TypeRef::Named { path, generics, .. } = &obj_ty else { return None }; if path.len() != 1 || !generics.is_empty() { return None; } let param_name = path[0].as_str(); let fn_generics = self.current_fn_generics.borrow(); let gp = fn_generics.iter().find(|g| g.name == param_name)?; for bound in &gp.bounds { let TypeRef::Named { path: bpath, generics: bargs, span: bspan } = bound else { continue }; if bpath.len() != 1 { continue; } // №514 fix: collision-aware lookup — see the sibling // `resolve_generic_bound_receiver_method`'s identical fix for the // full root-cause (a bound name like `Write` colliding across // modules; the collision-blind `self.types.get` silently picked // whichever same-named decl landed LAST in merge order). let Some(td) = self.types_get_for_file(bpath[0].as_str(), bspan.file_id) else { continue }; let TypeDeclKind::Protocol { methods, .. } = &td.kind else { continue }; let Some(m) = methods.iter().find(|m| { m.name == method || m.name.trim_start_matches('@') == method }) else { continue }; if !m.params.is_empty() || !m.generics.is_empty() { // Narrow case only — a parameterized/generic protocol method // would need arg-type-driven substitution too; unsupported here. continue; } let ret = m.return_type.clone()?; if td.generics.len() == bargs.len() && !td.generics.is_empty() { let subst: HashMap<String, TypeRef> = td.generics.iter() .map(|g| g.name.clone()) .zip(bargs.iter().cloned()) .collect(); return Some(crate::const_fn_trampoline::subst_type_ref_pub(&ret, &subst)); } return Some(ret); } None } /// [M-ro-launder-pattern-bind-not-enforced] (реестр 221.1 №106, D34): /// companion of `match_arm_bindings` — walks the SAME recognized pattern /// shapes (bare `Pattern::Ident` top-level; single-arg tuple-variant /// sub-`Ident`, e.g. `Ok(b)` / `Ok(mut b)`) collecting each binding's OWN /// `is_mut` flag (D34: bare pattern-bind = immutable/ro-freeze, `mut /// name` = mut) — independent of the scrutinee's type (unlike /// `match_arm_bindings`, there is no type to resolve here, just the /// pattern's own static shape). Deliberately kept in LOCKSTEP with /// `match_arm_bindings`'s shape recognition (same two arms, same /// guard) — a binding `scope` doesn't know about (an un-recognized /// pattern shape) gets no L1 launder-table entry either; registering one /// anyway would be a name in `ro_binding_names` with no matching `scope` /// entry — harmless in isolation, but an inconsistency with no upside. fn pattern_bind_mutability(pattern: &Pattern) -> Vec<(String, bool)> { match pattern { Pattern::Ident { name, is_mut, .. } => vec![(name.clone(), *is_mut)], Pattern::Variant { kind: VariantPatternKind::Tuple { patterns, rest }, .. } if !rest && patterns.len() == 1 => { match &patterns[0] { Pattern::Ident { name, is_mut, .. } => vec![(name.clone(), *is_mut)], _ => Vec::new(), } } _ => Vec::new(), } } fn match_arm_bindings( &self, pattern: &Pattern, scrut_ty: Option<&TypeRef>, ) -> Vec<(String, TypeRef)> { let mut out = Vec::new(); let Some(st) = scrut_ty else { return out }; match pattern { Pattern::Ident { name, .. } => out.push((name.clone(), st.clone())), Pattern::Variant { path, kind: VariantPatternKind::Tuple { patterns, rest }, .. } if !rest && patterns.len() == 1 => { if let Pattern::Ident { name: bind, .. } = &patterns[0] { if let TypeRef::Named { path: sp, generics, .. } = st { let variant = path.last().map(|s| s.as_str()).unwrap_or(""); let sum_name = sp.last().map(|s| s.as_str()).unwrap_or(""); let payload: Option<TypeRef> = match (sum_name, variant) { ("Option", "Some") if generics.len() == 1 => { Some(generics[0].clone()) } ("Result", "Ok") if generics.len() == 2 => { Some(generics[0].clone()) } ("Result", "Err") if generics.len() == 2 => { Some(generics[1].clone()) } _ if generics.is_empty() => { self.types.get(sum_name).and_then(|td| { if !td.generics.is_empty() { return None; } if let TypeDeclKind::Sum(variants) = &td.kind { variants.iter().find(|v| v.name == variant).and_then(|v| { match &v.kind { SumVariantKind::Tuple(tys) if tys.len() == 1 => { Some(tys[0].clone()) } _ => None, } }) } else { None } }) } _ => None, }; if let Some(p) = payload { out.push((bind.clone(), p)); } } } } _ => {} } out } /// №279 [M-nested-err-pattern-shared-variant-wrong-enum-tag]: recursively /// resolves each BARE (single-segment) `Pattern::Variant` in `pattern` /// against the STRUCTURAL scrutinee type reached by descending through /// enclosing tuple-variant payload positions — generalizes /// `match_arm_bindings`'s single-level Option[T]/Result[T,E]/general-sum /// payload derivation to (a) recurse into FURTHER nested /// `Pattern::Variant` sub-patterns (not just a terminal `Ident` bind) and /// (b) multi-field tuple variants. Writes each resolved pattern's OWN /// `span` → the sum type's simple name into `pattern_variant_types_buf`. /// /// Only writes when `scrutinee_ty` genuinely and unambiguously names a /// declared variant at that position (arity match for the multi-field /// case, declared-variant-name match for the unit case) — any /// uncertainty leaves the span unwritten, so codegen's existing /// `find_variant_compat` fallback chain is UNCHANGED for those cases /// (this pass only ADDS a higher-priority source of truth, never removes /// the fallback). fn resolve_pattern_variant_types(&self, pattern: &Pattern, scrutinee_ty: Option<&TypeRef>) { match pattern { Pattern::Variant { path, kind, span } => { // This pattern's OWN sum type — bare single-segment name // only; an explicit `Sum.Variant` path is already // unambiguous (codegen's `path.len() > 1` branch handles it // without this channel). // Option/Result variants deliberately EXCLUDED here: codegen // already has dedicated, well-exercised Option/Result tag // derivation (`is_opt`/`novares_ok_err`-driven) — this // channel targets USER sum types only (the actual bug class: // two different user enums sharing a variant name), so it // stays out of the way of the builtin paths entirely. if path.len() == 1 { if let Some(TypeRef::Named { path: sp, generics, .. }) = scrutinee_ty { let sum_name = sp.last().map(|s| s.as_str()).unwrap_or(""); let variant = path[0].as_str(); let resolved: Option<String> = if generics.is_empty() && sum_name != "Option" && sum_name != "Result" { self.types.get(sum_name).and_then(|td| { if let TypeDeclKind::Sum(variants) = &td.kind { variants.iter().any(|v| v.name == variant) .then(|| sum_name.to_string()) } else { None } }) } else { None }; if let Some(sn) = resolved { self.pattern_variant_types_buf.borrow_mut().insert(*span, sn); } } } // Recurse into sub-patterns with each field's STRUCTURAL // payload type (same derivation, per-position). if let VariantPatternKind::Tuple { patterns, rest } = kind { if !*rest { let variant = path.last().map(|s| s.as_str()).unwrap_or(""); let field_tys: Vec<Option<TypeRef>> = match scrutinee_ty { Some(TypeRef::Named { path: sp, generics, .. }) => { let sum_name = sp.last().map(|s| s.as_str()).unwrap_or(""); match (sum_name, variant) { ("Option", "Some") if generics.len() == 1 && patterns.len() == 1 => { vec![Some(generics[0].clone())] } ("Result", "Ok") if generics.len() == 2 && patterns.len() == 1 => { vec![Some(generics[0].clone())] } ("Result", "Err") if generics.len() == 2 && patterns.len() == 1 => { vec![Some(generics[1].clone())] } _ if generics.is_empty() => self .types .get(sum_name) .and_then(|td| { if !td.generics.is_empty() { return None; } if let TypeDeclKind::Sum(variants) = &td.kind { variants.iter().find(|v| v.name == variant).and_then( |v| match &v.kind { SumVariantKind::Tuple(tys) if tys.len() == patterns.len() => { Some( tys.iter() .map(|t| Some(t.clone())) .collect::<Vec<_>>(), ) } _ => None, }, ) } else { None } }) .unwrap_or_else(|| vec![None; patterns.len()]), _ => vec![None; patterns.len()], } } _ => vec![None; patterns.len()], }; for (i, p) in patterns.iter().enumerate() { let fty = field_tys.get(i).cloned().flatten(); self.resolve_pattern_variant_types(p, fty.as_ref()); } } } } Pattern::Or { alternatives, .. } => { for alt in alternatives { self.resolve_pattern_variant_types(alt, scrutinee_ty); } } Pattern::Binding { inner, .. } => { self.resolve_pattern_variant_types(inner, scrutinee_ty); } _ => {} } } /// [M-match-arm-mixed-int-width-sentinel-coerce] amend (found by the mega-CU /// gate on `d407_enum_payload_width.nv`, 2026-07-21): is `e` a BARE integer /// literal — `IntLit` or `Unary{Neg, IntLit}` (`-1`, per the SAME shape /// `assignable`'s D227 Rule 6 branch matches, ~14544) — with no cast / no /// fixed type of its own? Returns the raw value (i128, overflow-safe for /// negation of `i64::MIN`) if so. A bare literal is flexible: unlike a /// pattern-bound variable (whose type is FIXED by the payload it was /// extracted from), a literal arm has no type until placed in context — /// `d407W(_) => 1` alongside a `uint`-typed sibling arm must let `1` ADOPT /// `uint`, not be compared as a rigid `int` vs `uint` mismatch (D54 literal-fit, /// rustc precedent: an unsuffixed integer literal unifies with its context). fn bare_int_literal_value(e: &Expr) -> Option<i128> { match &e.kind { ExprKind::IntLit(v) => Some(*v as i128), ExprKind::Unary { op: UnOp::Neg, operand } => { if let ExprKind::IntLit(v) = &operand.kind { Some(-(*v as i128)) } else { None } } _ => None, } } /// [M-match-arm-mixed-int-width-sentinel-coerce] amend: does the raw literal /// value `v` fit `target` (int-family) WITHOUT loss — the SAME rules /// `assignable`'s `IntLit`/`Unary{Neg,IntLit}` branches already enforce for a /// literal reaching an ANNOTATED position (D227 Rule 3 sized range-check, /// Rule 6 negative-into-unsigned floor)? Wide-default (`int`/`uint`) targets /// have no upper range-check (`sized_int_name` → `None`, D227 Rule 1) — only /// the negative-floor applies (and only for `uint`, `int` is signed). fn literal_fits_scalar(v: i128, target: &ResolvedType) -> bool { let Some((_, signed)) = target.int_width_sign() else { return false }; if !signed && v < 0 { return false; // D227 Rule 6: a negative literal never fits an unsigned target } match target.sized_int_name() { Some(name) => lit_range_check(v, &name).is_none(), None => true, // wide-default int/uint: no upper range-check (D227 Rule 1) } } /// [M-match-arm-mixed-int-width-sentinel-coerce] (Plan 172.2 followup, fixed /// 2026-07-21): per-arm (span, ResolvedType, literal-value) extraction shared by /// `infer_match_common_primitive` (channel materialization) and /// `check_match_arm_width_mismatch` (diagnostic). Factored out so both /// consumers walk arms/bindings IDENTICALLY (§0/§3 — one arm-type derivation, /// not two copies that could drift). Diverging (`Never`) arms are OMITTED — /// they contribute no type constraint, same as the old inline `continue`. The /// third tuple element is `Some(v)` when the arm's value expression is a BARE /// integer literal (`bare_int_literal_value`) — the literal-fit amend above. fn match_arm_value_types( &self, arms: &[MatchArm], scope: &HashMap<String, TypeRef>, scrut_ty: Option<&TypeRef>, ) -> Option<Vec<(Span, ResolvedType, Option<i128>)>> { let mut out = Vec::new(); for arm in arms { // АТОМ 2a: расширить scope биндингами паттерна (типы из scrut_ty). // Пустой набор биндингов → армы работают в наружном scope как раньше. let binds = self.match_arm_bindings(&arm.pattern, scrut_ty); let ext_scope: Option<HashMap<String, TypeRef>> = if binds.is_empty() { None } else { let mut s = scope.clone(); for (k, v) in binds { s.insert(k, v); } Some(s) }; let scope: &HashMap<String, TypeRef> = ext_scope.as_ref().unwrap_or(scope); // 172.1.2 stmts-relax: канальный фоллбек (buf-аннотация сделана в // правильном scope при f1-рекурсии) — работаем в ResolvedType. let arm_rt = |t: &Expr| -> Option<ResolvedType> { if t.id.is_set() { if let Some(rt) = self.resolved_types_buf.borrow().get(&t.id).cloned() { return Some(rt); } } None }; let (span, t_expr, rt): (Span, &Expr, ResolvedType) = match &arm.body { MatchArmBody::Expr(e) => (e.span, e, match self.infer_expr_type(e, scope) { Some(tr) => ResolvedType::from_type_ref(&tr), None => arm_rt(e)?, }), MatchArmBody::Block(b) => { let t = b.trailing.as_deref()?; (t.span, t, if b.stmts.is_empty() { if let Some(tr) = self.infer_expr_type(t, scope) { ResolvedType::from_type_ref(&tr) } else { arm_rt(t)? } } else { // stmts-блок: scope-инференс нельзя (блок-локалы), канал — можно. arm_rt(t)? }) } }; if rt == ResolvedType::Never { continue; // diverging arm contributes no constraint } out.push((span, rt, Self::bare_int_literal_value(t_expr))); } Some(out) } /// [M-match-arm-mixed-int-width-sentinel-coerce] fix: does `b` need an EXPLICIT /// `as` to reach `a` (neither direction is a Nova-sanctioned safe int-widening, /// D54/`would_narrow_into`)? int-family (`Scalar`) ONLY — non-int disagreement /// (records/Float/etc.) is unchanged pre-existing behavior (bail silently, out /// of this marker's scope). `false` when EITHER side is non-int (permissive, /// mirrors `would_narrow_into`'s own "Non-int on either side ⇒ false" contract — /// the caller's `int_width_sign` gate already excludes those before calling). fn int_arms_incompatible(a: &ResolvedType, b: &ResolvedType) -> bool { b.would_narrow_into(a) && a.would_narrow_into(b) } fn infer_match_common_primitive( &self, arms: &[MatchArm], scope: &HashMap<String, TypeRef>, scrut_ty: Option<&TypeRef>, ) -> Option<ResolvedType> { let arm_types = self.match_arm_value_types(arms, scope, scrut_ty)?; let mut common: Option<ResolvedType> = None; let mut common_lit: Option<i128> = None; // Some iff `common` came from a bare literal arm for (_, rt, lit) in arm_types { match &common { None => { common = Some(rt); common_lit = lit; } Some(c) if *c == rt => {} Some(c) => { // [M-match-arm-mixed-int-width-sentinel-coerce] amend (mega-CU // gate found on d407_enum_payload_width.nv, 2026-07-21): a BARE // literal arm has no fixed type of its own — check literal-fit // (D227 Rules 1/3/6) BEFORE treating a disagreement as a real // width conflict. `d407W(_) => 1` alongside a `uint` sibling arm // must adopt `uint`, not collide with the literal's flexible // default (`int`). Whichever side is the literal, if it FITS the // other (concrete or previously-established) side, adopt that // side — no widen, no error. A negative literal that doesn't fit // an unsigned target correctly falls through to the mismatch // check below (D227 Rule 6 floor still applies). if let Some(v) = lit { if Self::literal_fits_scalar(v, c) { continue; // this arm's literal adopts `c` — common unchanged } } if let Some(cv) = common_lit { if Self::literal_fits_scalar(cv, &rt) { common = Some(rt); // established literal adopts the new, more concrete `rt` common_lit = None; continue; } } // [M-match-arm-mixed-int-width-sentinel-coerce] fix: int-family // arms that DISAGREE but are safe-widening-compatible (D54, // e.g. `u32` sentinel arm + `int` literal arm) unify to the // WIDER side instead of bailing — the wider type is what BOTH // arms can losslessly hold (mirrors the assignment-target // widening Nova already allows elsewhere; `would_narrow_into` // is the single source, `int_width_sign` gates int-family-only). let both_int = c.int_width_sign().is_some() && rt.int_width_sign().is_some(); if !both_int || Self::int_arms_incompatible(c, &rt) { return None; // genuine mismatch (or non-int) → bail, as before } // `rt` does NOT narrow into `c` ⇒ `c` is wide enough for `rt`, // keep it. Otherwise (not incompatible ⇒ the OTHER direction // must be safe) `c` narrows into `rt` ⇒ `rt` is the wider side. if !rt.would_narrow_into(c) { // c already wide enough — no change. } else { common = Some(rt); // upgrade to rt, the wider (or equal-C-type) side } common_lit = None; // common is now a concrete (non-literal-flexible) type } } } let rt = common?; // NON-UNIT primitive only (Unit excluded: a statement-position all-unit / // unit-dominated match must stay on the legacy path, §0-safe minimal subset). // 2026-07-02 (tally АТОМ 2c): гейт расширен зеркалом f3_check_member — // конкретный NON-generic declared value-тип (record/sum/newtype/named-tuple, // без type-args) детерминированно лоуэрится (Nova_X*/NovaValue_X) без // mono/subst-hazard; строгое равенство всех non-divergent армов сохранено. let concrete_value_named = matches!(&rt, ResolvedType::Named { name, args, .. } if args.is_empty() && self.types.get(name).map_or(false, |td| matches!(&td.kind, TypeDeclKind::Record(_) | TypeDeclKind::Sum(_) | TypeDeclKind::Newtype(_) | TypeDeclKind::NamedTuple(_)))); // 172.1.2 (2026-07-04): all-Unit match → Unit (byte-identical legacy). if rt == ResolvedType::Unit { return Some(ResolvedType::Unit); } if rt != ResolvedType::Unit && (Self::primitive_gate(&rt) || concrete_value_named) { Some(rt) } else { None } } /// [M-match-arm-mixed-int-width-sentinel-coerce] fix (Plan 172.2 followup, /// P1, 2026-07-21): a `match` whose arms hold GENUINELY incompatible int-family /// widths (neither side safe-widens into the other, D54/`would_narrow_into` — /// e.g. `u32` vs `i32` same-width-different-sign, or `u16` vs `i8`) used to bail /// SILENTLY out of `infer_match_common_primitive` into the legacy codegen /// arm-type re-derivation, which picks the first non-`nova_int` arm regardless /// of the OTHER arm's width/sign — silently reinterpreting a sentinel literal's /// bits under the wrong type (`None => -1` read back as `4294967295` when a /// sibling arm bound `u32`). Safe-widening-compatible mixes (`u32` sentinel arm /// + `int`/`i64` literal arm) are NOT reported here — `infer_match_common_primitive` /// now unifies those to the wider side (this function's own bail path is only /// entered when NEITHER direction is safe). D-amendment: spec/decisions/02-types.md. fn check_match_arm_width_mismatch( &self, arms: &[MatchArm], scope: &HashMap<String, TypeRef>, scrut_ty: Option<&TypeRef>, errors: &mut Vec<Diagnostic>, ) { let Some(arm_types) = self.match_arm_value_types(arms, scope, scrut_ty) else { return }; let mut common: Option<(Span, ResolvedType)> = None; let mut common_lit: Option<i128> = None; // Some iff `common` came from a bare literal arm for (span, rt, lit) in arm_types { let Some((c_span, c)) = &common else { common = Some((span, rt)); common_lit = lit; continue; }; if *c == rt { continue; } let both_int = c.int_width_sign().is_some() && rt.int_width_sign().is_some(); if !both_int { continue; // non-int disagreement — unchanged pre-existing behavior, out of scope } // [M-match-arm-mixed-int-width-sentinel-coerce] amend (mega-CU gate // found on d407_enum_payload_width.nv, 2026-07-21): literal-fit BEFORE // treating a disagreement as a real conflict — mirrors // `infer_match_common_primitive`'s own amend (see its doc for the full // rationale). A negative literal that doesn't fit an unsigned target // still falls through to the mismatch check (D227 Rule 6 floor). if let Some(v) = lit { if Self::literal_fits_scalar(v, c) { continue; // this arm's literal adopts `c` — common unchanged } } if let Some(cv) = common_lit { if Self::literal_fits_scalar(cv, &rt) { common = Some((span, rt)); // established literal adopts the new, concrete `rt` common_lit = None; continue; } } if !Self::int_arms_incompatible(c, &rt) { // safe-widening-compatible — track the wider side for any FURTHER arm, // mirroring `infer_match_common_primitive`'s own unify (so a 3rd arm is // compared against the correct running-wider type, not the first arm). // `rt` does NOT narrow into `c` ⇒ `c` stays the wider running type; // otherwise (not incompatible ⇒ the other direction is safe) `c` // narrows into `rt` ⇒ `rt` becomes the new wider running type. if !rt.would_narrow_into(c) { // c stays the wider running type — no change. } else { common = Some((span, rt)); } common_lit = None; // common is now a concrete (non-literal-flexible) type continue; } // Genuine mismatch: neither `int_name()` can be `None` here (both int-family). let c_name = c.int_name().unwrap_or("<int>"); let rt_name = rt.int_name().unwrap_or("<int>"); errors.push( Diagnostic::new( format!( "[E_MATCH_ARM_WIDTH_MISMATCH] match arms have incompatible integer \ widths: `{}` here vs `{}` — neither safely widens into the other; \ cast one arm explicitly (`... as {}` or `... as {}`)", rt_name, c_name, c_name, rt_name, ), span, ) .with_note_at( format!("this arm is typed `{}`", c_name), *c_span, ), ); return; // one diagnostic per match — avoid a cascade of repeats } } /// Plan 172.1 U.4.4 (If-expr half): the COMMON primitive value type of an /// `if cond { … } else { … }` EXPRESSION — the If-parallel of /// `infer_match_common_primitive`. `infer_expr_type` has no `If` arm (returns /// `None`), and legacy `infer_expr_c_type` RE-derives it (the `If` arm at /// emit_c.rs:38326 — divergence-aware then/else selection) — §0 fragmentation. /// Materializing it lets codegen READ the resolved If type instead of re-deriving. /// /// MAXIMALLY CONSERVATIVE (§0-safe minimal subset): returns `Some(rt)` ONLY when /// BOTH branches are EMPTY-stmt blocks (their value = the trailing expr; a stmt'd /// block's trailing may reference inner `let`s absent from `scope` — mirror the /// `match` Block-arm constraint) whose trailing types are EQUAL and a NON-UNIT /// primitive (`primitive_gate`). An `if` WITHOUT `else` (unit-valued), an `else if` /// CHAIN, a diverging branch (`Never` ≠ the other primitive), a non-primitive / /// unresolvable branch — all bail to `None` → legacy (which keeps its /// divergence-aware path for those). For the gated subset the legacy arm lands the /// SAME primitive (`then_ty`, no divergence) → byte-identical. fn infer_if_common_primitive( &self, then: &crate::ast::Block, else_: &Option<crate::ast::ElseBranch>, scope: &HashMap<String, TypeRef>, ) -> Option<ResolvedType> { let else_ = else_.as_ref()?; // 2026-07-02 (tally, If АТОМ 1b): post-order фоллбек — вызывается из f1 // If-арма ПОСЛЕ рекурсии в ветки, так что trailing-дети (Call/Binary/ // Some-ctor) уже аннотированы другими продюсерами; берём их из // resolved_types_buf, когда infer_expr_type сам не достаёт (паттерн // operand_rt Binary-арма). Гейты join не менялись. let branch_rt = |blk: &crate::ast::Block| -> Option<ResolvedType> { let t = blk.trailing.as_deref()?; // 172.1.2 stmts-relax: для блока СО stmts scope-инференс НЕЛЬЗЯ // (trailing может ссылаться на блок-локалы вне scope), но // buf-аннотация trailing'а сделана в ПРАВИЛЬНОМ scope при // f1-рекурсии → канал разрешён всегда. if blk.stmts.is_empty() { if let Some(tr) = self.infer_expr_type(t, scope) { return Some(ResolvedType::from_type_ref(&tr)); } } if t.id.is_set() { return self.resolved_types_buf.borrow().get(&t.id).cloned(); } None }; let then_rt = branch_rt(then)?; let else_rt = match else_ { crate::ast::ElseBranch::Block(b) => branch_rt(b)?, crate::ast::ElseBranch::If(_) => return None, }; // 172.1.2 (2026-07-04): both-Unit разрешён — legacy отвечает то же // nova_unit (statement-position if) → byte-identical. if then_rt == else_rt && then_rt == ResolvedType::Unit { return Some(ResolvedType::Unit); } if then_rt == else_rt && then_rt != ResolvedType::Unit && Self::primitive_gate(&then_rt) { Some(then_rt) } else { None } } /// [M-196-freefn-arity-overload-default-ret-mismatch] fix: when ≥2 /// free-fn overloads are BOTH arity+type-compatible for a call (only /// reachable when at least one of them needed a DEFAULT-arg fill to /// bind — a genuine same-arity/same-category collision, D84 axis 2, /// binds ALL its tied candidates with the SAME (zero) default-count, /// so it always still falls into the `None` tie branch below, /// unaffected), prefer the candidate needing the FEWEST defaults /// filled — equivalently, the fewest TOTAL declared params (`args.len()` /// is fixed across candidates, so minimizing `params.len()` minimizes /// defaults-needed). This generalizes the ordinary "exact arity wins /// over a default-filled longer overload" rule (every other language /// with default args applies it) to N-way default chains, and agrees /// with codegen's sibling Q1 rtbuf-producer (`types/mod.rs`'s Ident/Call /// arm feeding `resolved_types_buf`, mirrored by `emit_c.rs`'s /// `infer_call_ret_c`) whenever Q1's stricter literal /// `f.params.len() == args.len()` filter finds a unique zero-default /// match (the `defaults_used == 0` case is exactly Q1's own criterion) /// — but ALSO covers calls where the intended candidate itself needs /// SOME defaults and every sibling needs strictly more (Q1 has no /// tie-break for that shape at all, so it silently defers to codegen's /// arity-blind name-keyed `user_fn_sigs` — the same latent bug, wider /// shape). A genuine tie (≥2 candidates needing the SAME minimal /// default-count — D84 axis 2, or two default-chains of equal length) /// still defers, unchanged. Returns an INDEX into `compat` (not a /// reference) to sidestep the extra lifetime parameter a borrowed /// return would otherwise need — callers already hold `compat`. fn pick_no_default_overload( &self, compat: &[&&FnDecl], args: &[CallArg], ) -> Option<usize> { // Two-pass: collect (index, defaults_used) for every candidate that // still binds, THEN pick the minimum — a single pass that bails on // the first tie would wrongly stop at an early tie between two // high-default candidates instead of finding a later, strictly // better (fewer-defaults) one. let scored: Vec<(usize, usize)> = compat.iter().enumerate() .filter_map(|(i, c)| { let bindings = crate::argbind::bind_call_args(&c.params, args).ok()?; let defaults_used = bindings.iter() .filter(|b| matches!(b, crate::argbind::ArgBinding::Default)) .count(); Some((i, defaults_used)) }) .collect(); let min_defaults = scored.iter().map(|(_, d)| *d).min()?; let mut at_min = scored.iter().filter(|(_, d)| *d == min_defaults); let (idx, _) = at_min.next()?; if at_min.next().is_some() { return None; // ≥2 candidates tied at the minimum — genuine ambiguity. } Some(*idx) } /// Ф.1: проверить типы аргументов call-site против параметров callee. /// Plan 172.1 U.3.1: ARITY-AWARE compatibility probe for ONE overload. /// /// - `None` — the overload is not even ARITY-applicable (`bind_call_args` /// fails: wrong number of positional args, bad named arg, etc.). That is a /// wrong-arity / non-call situation reported elsewhere (BoundCtx), NOT a /// "no matching overload by type" — the caller must EXCLUDE it from the /// no-match decision (else a 0-arg / wrong-arity reference to a multi-overload /// fn would falsely fire E_NO_MATCHING_OVERLOAD). /// - `Some(true)` — binds AND every arg is category-compatible (no `Compat::Bad`). /// - `Some(false)` — binds but some arg is a category mismatch (the str↔int leak). /// /// Permissive by design — Narrowing/OutOfRange/Unknown/Ok all count as /// compatible, because codegen still performs the FINAL exact-C-type selection /// (U.3.4). The checker only enforces that AT LEAST ONE arity-applicable overload /// is category-compatible. Pure: emits no diagnostics. fn overload_applicability( &self, callee: &FnDecl, args: &[CallArg], gs: &GenericScope, scope: &HashMap<String, TypeRef>, ) -> Option<bool> { let bindings = crate::argbind::bind_call_args(&callee.params, args).ok()?; let callee_gs = fn_generic_scope(callee); for (pi, binding) in bindings.iter().enumerate() { let ai = match binding { crate::argbind::ArgBinding::Positional(i) | crate::argbind::ArgBinding::Named(i) => *i, _ => continue, }; let Some(param) = callee.params.get(pi) else { continue; }; if param.is_variadic { continue; } let Some(arg) = args.get(ai) else { continue; }; // [M-2223-generic-arity-overload-applicability] (реестр 221.1 // №130, (г)-слой №124): a `ClosureFull` literal argument's OWN // declared param COUNT vs a Func-shaped param's declared param // count — decidable BEFORE `assignable` below, which collapses // EVERY `TypeRef::Func`-shaped `expected` to `ResolvedType::Any` // (permissive-by-design, U.3.1 doc above) and therefore never // rejects an arity mismatch on its own. Without this, TWO (or // three) method-level-generic siblings differing ONLY in the // handler closure's OWN param arity (`fn(T1) -> R` vs `fn(T1, // T2) -> R` — Router-style extractor-count overloading, Plan // 222.3 §5's actual target shape) all register as "compatible" // for a single call site regardless of which arity the literal // closure argument actually declares — `compat_spans.len() > 1` // in `check_instance_overload` → no unique `resolved_callees` // entry → codegen's single-valued mono-dispatch last-wins → // `[E7001]` / wrong-arity C call. See `closure_arg_arity_ok`'s // doc for why an ARITY-only check (not the exact structural // `closure_args_match_concrete`, №124) is the right generality // here — that helper's `typeref_equal` would never match a // generic candidate's OWN typevars against the caller's concrete // closure types, even on the CORRECT sibling. if !self.closure_arg_arity_ok(¶m.ty, arg.expr()) { return Some(false); } if matches!( self.assignable(arg.expr(), ¶m.ty, gs, &callee_gs, scope), Compat::Bad { .. } ) { return Some(false); } } Some(true) } /// №309/№317 (221.1, окно p-ovl-channel, D84 mode-axis tiebreak). Checker-side /// mirror of `emit_c.rs::is_place_mutable` — is `e` a WRITABLE named place (so a /// `mut`-mode parameter may bind it)? `Ident` defers to `ro_binding_names` (the /// SAME oracle the checker already uses elsewhere for "is this binding mutable", /// e.g. lines ~21600/~17000) — a name absent from that set is `mut`-bound. /// `SelfAccess` defers to `current_recv_is_mut` (mirrors emit_c's /// `current_receiver_is_mut`). `Member`/`Index` recurse into the base place. /// Anything else (a temporary) is not a place at all. fn expr_mode_axis_mutable_place(&self, e: &Expr) -> bool { match &e.kind { ExprKind::Ident(name) => !self.ro_binding_names.borrow().contains(name), ExprKind::SelfAccess => self.current_recv_is_mut.get(), ExprKind::Member { obj, .. } => self.expr_mode_axis_mutable_place(obj), ExprKind::Index { obj, .. } => self.expr_mode_axis_mutable_place(obj), _ => false, } } /// №309/№317: checker-side mirror of `emit_c.rs::is_rvalue_temp`, EXTENDED to /// close [M-309-narrow-by-param-mode-binding-form] (the textual defect №309 /// records): a NAMED identifier whose CURRENT binding is `consume`-declared /// (`consume_binding_names`, populated by `Stmt::Let`/consume params — see field /// doc) is ALSO eligible for a `consume`-mode parameter, not only a syntactic /// rvalue temporary. Owner's rule (2026-08-03, verbatim): "temporary OR /// consuming — moves; everything else copies." `Member`/`Index`/`SelfAccess` /// stay ineligible (a sub-place is never itself a consume-bound name, mirrors /// emit_c exactly — no field/element-level consume tracking, D184 §1 scope). fn expr_mode_axis_consume_eligible(&self, e: &Expr) -> bool { match &e.kind { ExprKind::Ident(name) => self.consume_binding_names.borrow().contains(name), ExprKind::SelfAccess | ExprKind::Member { .. } | ExprKind::Index { .. } => false, _ => true, } } /// №309/№317 (221.1, окно p-ovl-channel): §0 channel-first tiebreak for an /// overload SET that is ambiguous by TYPE alone (`compat_fns.len() >= 2`, all /// already `overload_applicability`-compatible) because its members differ ONLY /// by the D84 parameter-MODE axis (Plan 184: `ro`/`mut`/`consume` on a param) /// and/or the receiver-mutability axis (Plan 135: `fn T @m` vs `fn T mut @m`) — /// two axes `emit_c.rs`'s legacy dispatch resolves with two SEQUENTIAL greedy /// filters (receiver-mut tiebreak, THEN `narrow_by_param_mode`) that can /// short-circuit each other (see PROGRESS-ovl.md "дефект 1" — the receiver-mut /// filter collapses the candidate pool to 1 BEFORE the param-mode filter ever /// runs, whenever the two axes happen to correlate, e.g. `mut @put(consume v /// int)` vs plain `@put(v int)`). Resolving the axes TOGETHER here, in the /// checker, and recording the unique winner in `resolved_callees` sidesteps the /// ordering bug entirely — `emit_c.rs`'s `channel_choice` already prefers the /// channel over its own legacy pool (§0, U.4.3 c2.2 precedent). /// /// Guarded to a GENUINE axis-only overload set: every candidate must have the /// SAME arity and STRUCTURALLY IDENTICAL param TypeRefs pairwise /// (`typeref_equal`, the same D84 duplicate-signature equality already used /// elsewhere in this file) — a real type-differentiated overload set is left to /// the pre-existing (unchanged) resolution. `obj` is the call's receiver /// expression (`None` for a free function — no receiver axis to consider). /// /// Selection: a candidate is INELIGIBLE (dropped, mirrors `narrow_by_param_mode`'s /// `continue 'cand`) when a `mut` axis it declares is not satisfied by the /// actual call site (`mut` receiver/param requires a mutable place; `consume` /// param requires `expr_mode_axis_consume_eligible`). Eligible candidates are /// scored by specificity (`consume` > `mut` > `ro`, same weights as /// `narrow_by_param_mode`) and the unique highest-scoring one wins. Zero /// eligible or a genuine tie → `None` (honest gap, falls through to the /// pre-existing legacy resolution — never a wrong silent guess). fn mode_axis_tiebreak( &self, obj: Option<&Expr>, compat_fns: &[&FnDecl], args: &[CallArg], ) -> Option<crate::diag::Span> { if compat_fns.len() < 2 { return None; } let first = compat_fns[0]; let same_shape = compat_fns.iter().all(|f| { f.params.len() == first.params.len() && f.params.iter().zip(first.params.iter()) .all(|(a, b)| typeref_equal(&a.ty, &b.ty)) }); if !same_shape { return None; } let recv_mut = |f: &FnDecl| f.receiver.as_ref().map(|r| r.mutable).unwrap_or(false); let axis_differs = compat_fns.windows(2).any(|w| { recv_mut(w[0]) != recv_mut(w[1]) || w[0].params.iter().zip(w[1].params.iter()) .any(|(a, b)| (a.consume, a.is_mut) != (b.consume, b.is_mut)) }); if !axis_differs { return None; } let obj_mut = obj.map(|o| self.expr_mode_axis_mutable_place(o)).unwrap_or(false); let mut scored: Vec<(&FnDecl, i32)> = Vec::new(); 'cand: for f in compat_fns { let mut score = 0i32; if recv_mut(f) { if !obj_mut { continue 'cand; } score += 2; } for (p, a) in f.params.iter().zip(args.iter()) { if p.consume { if !self.expr_mode_axis_consume_eligible(a.expr()) { continue 'cand; } score += 3; } else if p.is_mut { if !self.expr_mode_axis_mutable_place(a.expr()) { continue 'cand; } score += 2; } } scored.push((*f, score)); } let best = scored.iter().map(|(_, s)| *s).max()?; let mut at_best = scored.iter().filter(|(_, s)| *s == best); let (winner, _) = at_best.next()?; if at_best.next().is_some() { return None; // genuine tie — leave to the legacy resolution. } Some(winner.span) } /// Plan 172.1 U.3.3-instance (§6/§1). Resolve a value-receiver `obj.method(args)` /// call's overloads in the CHECKER and fire `[E_NO_MATCHING_OVERLOAD]` when a category /// mismatch is present — so it is a clean checker diagnostic, not a leaked `CC-FAIL`. /// /// Permissive, identical to the U.3.1 (free-fn) / U.3.3 (`Type.method`) rule: fires /// ONLY when ≥1 overload binds by arity (`overload_applicability` is `Some(_)`) but /// NONE is category-compatible (`Some(true)`). Narrowing/OutOfRange/Unknown count as /// compatible — codegen does the FINAL exact-C-type selection (U.3.4). Receiver type /// via the self-less `BoundCtx::infer_arg_ty`; unknown/complex receiver → skip (gap, /// not wrong). PRIMITIVE receivers gated (U.3.2): their external overloads live in /// codegen's `ExternalRegistry`, not the checker's `method_table`, so the checker's /// set is incomplete → would false-positive. De-risked in detect-mode (§7): 0 /// false-positives across 707K corpus calls (62K resolved-ok). /// Plan 177 Ф.3 [E_UNKNOWN_METHOD]: EXISTENCE-only mirror of the receiver-matching /// half of [`resolve_prefix_generic_method_return`] — does ANY prefix-generic / /// blanket receiver method named `method` apply to the concrete (peeled) receiver /// `peeled`? Covers `fn[T] T @method` / `fn[I Bound] I @method` (bare-typevar recv, /// matches any receiver) and `fn[T] []T @method` (slice-typevar recv, matches an /// Array/FixedArray receiver only). Used to KEEP such a call legitimate before the /// primitive unknown-method rejection (a blanket method is a real method on the /// primitive, even though it is registered under the typevar receiver key, not the /// primitive's name). Return-type resolvability is irrelevant here — pure existence. fn prefix_generic_method_exists(&self, peeled: &TypeRef, method: &str) -> bool { for (recv_key, methods) in self.sig.method_table.iter() { let Some(overloads) = methods.get(method) else { continue; }; for f in overloads { let Some(recv) = f.receiver.as_ref() else { continue; }; if !matches!(recv.kind, ReceiverKind::Instance) { continue; } // Bare typevar receiver (`fn[T] T @m` / blanket `fn[I Bound] I @m`): // recv_key IS a method-level generic param name → matches any receiver. if f.generics.iter().any(|g| &g.name == recv_key) { // №254 (221.1): mirror `resolve_prefix_generic_method_return`'s // bound-check (same scope: `Next`/`Iter` only, same "protocol // name lowercased = required method" convention). Without this, // a receiver that structurally fails a `Next`/`Iter` bound was // still treated as "has the method" — the call type-checked // clean, then crashed downstream as a codegen ICE // (`[P67-LEGACY] method call return type unknown`) instead of // an honest checker diagnostic (`bound_violation_message` // below fires the real, specific message for this case). if let Some(recv_g) = f.generics.iter().find(|g| &g.name == recv_key) { let concrete_name: Option<&str> = match peeled { TypeRef::Named { path, .. } => path.last().map(|s| s.as_str()), TypeRef::Array(_, _) => Some("Vec"), _ => None, }; let bound_ok = recv_g.bounds.iter().all(|b| { let TypeRef::Named { path: bpath, .. } = b else { return true; }; let Some(proto_name) = bpath.last() else { return true; }; if !matches!(proto_name.as_str(), "Next" | "Iter") { return true; } let required_method = proto_name.to_lowercase(); concrete_name .and_then(|cn| self.sig.method_table.get(cn)) .map(|m| m.contains_key(&required_method)) .unwrap_or(false) }); if !bound_ok { continue; } } return true; } // Single-level slice typevar (`fn[T] []T @m`): matches only an array // receiver (element binds T). if recv_key.starts_with("[]") && f.generics.iter().any(|g| g.name == recv_key[2..]) && matches!(peeled, TypeRef::Array(..) | TypeRef::FixedArray(..)) { return true; } } } false } /// №254 (221.1): when `prefix_generic_method_exists` above declines a /// `method` call SPECIFICALLY because the receiver fails a `Next`/`Iter` /// bound (not because no blanket by that name exists at all), produce a /// clear, dedicated diagnostic instead of falling through to the generic /// `[E7320] no field or method` — the owner's decision requires an honest /// bound-failure message here, not "method not found" (that phrasing /// would be actively misleading: the method name IS declared, just not /// for this receiver). fn bound_violation_message(&self, peeled: &TypeRef, method: &str) -> Option<String> { let concrete_name: Option<&str> = match peeled { TypeRef::Named { path, .. } => path.last().map(|s| s.as_str()), TypeRef::Array(_, _) => Some("Vec"), _ => None, }; let concrete_name = concrete_name?; for (recv_key, methods) in self.sig.method_table.iter() { let Some(overloads) = methods.get(method) else { continue; }; for f in overloads { let Some(recv) = f.receiver.as_ref() else { continue; }; if !matches!(recv.kind, ReceiverKind::Instance) { continue; } if !f.generics.iter().any(|g| &g.name == recv_key) { continue; } let Some(recv_g) = f.generics.iter().find(|g| &g.name == recv_key) else { continue }; for b in &recv_g.bounds { let TypeRef::Named { path: bpath, .. } = b else { continue }; let Some(proto_name) = bpath.last() else { continue }; if !matches!(proto_name.as_str(), "Next" | "Iter") { continue; } let required_method = proto_name.to_lowercase(); let satisfied = self.sig.method_table.get(concrete_name) .map(|m| m.contains_key(&required_method)) .unwrap_or(false); if !satisfied { return Some(format!( "[E_PROTOCOL_BOUND_NOT_SATISFIED] `{cn}.{m}(...)` — `{m}` \ requires `{cn}` to implement `{proto}` (needs a `{req}()` \ method), but `{cn}` has none. Hint: implement `#impl({proto}[..]) \ fn {cn} @{req}(...) -> ..` (or, for `Next`, call `.iter()` first \ if `{cn}` is a container with an `Iter`-satisfying `@iter()`).", cn = concrete_name, m = method, proto = proto_name, req = required_method, )); } } } } None } fn check_instance_overload( &self, obj: &Expr, method_name: &str, args: &[CallArg], gs: &GenericScope, scope: &HashMap<String, TypeRef>, span: crate::diag::Span, errors: &mut Vec<Diagnostic>, // Plan 172.1 U.4.3 (stage c1): call-site `ExprId` — key for the resolved-callee channel. call_id: crate::ast::ExprId, ) { // [M-samename-extension-method-recv-type-collision] (№92, channel-first // fix per Plan 196 doctrine — owner/integrator correction 2026-07-25): // `BoundCtx::infer_arg_ty` is a deliberately LIGHTWEIGHT, free-standing // (no `&self`) receiver-type probe — its own doc calls it "best-effort" // and it has NO arm for `ExprKind::Member` (field access), so a receiver // written `holder.buf.method()` returned `None` here and this whole // function (and the `resolved_callees` write below it feeds) never ran // for that call site. `emit_c.rs`'s Plan 196.7 checker-span dispatch // (`resolved_callees` → `fn_ret_by_span`) then had nothing to read and // fell through to the codegen-side single-key `method_receivers` // registry — LAST-WINS when ≥2 concrete receiver types register the // same method name (`[]u8 consume @into_body()` + `BodyReader consume // @into_body()`; confirmed live repro, not hypothetical — see the // window brief / `docs/plans/wip/196.5-facet-c-map.md`). Fall back to // the FULL `self.infer_expr_type` (this `TypeCheckCtx`'s own general // expression-type inference, `&self` — it already resolves `Member` // through the record-field schema, `self.types_get_for_file`/ // `subst_receiver_generics`, exactly what a field-access receiver // needs) ONLY when the lightweight probe misses AND `obj` is // SPECIFICALLY a field access (`ExprKind::Member`) — the narrow gap // this fix targets. Deliberately NOT a blanket fallback for every // shape `infer_arg_ty` returns `None` for: a first attempt (any-shape // `.or_else`) additionally started resolving `ExprKind::Call` // receivers (a chained mutable-`-> @`-return call, // `inc(inc(&p,10),20)`-shape) — `self.infer_expr_type`'s `Call` arm // and codegen's OWN (frozen, Plan 196.7-adjacent) chain-return // inference for that exact shape disagree on value-vs-pointer for a // `value` record's `Self`-return chain, producing a live regression // (`d326_value_record_fluent.nv`'s fluent-mut chain: codegen emitted // `(p2).x` — `.` on a `NovaValue_P1724Point*` POINTER local — CC-FAIL // "did you mean to use '->'"). `ExprKind::Member` has no such frozen // competing inference path (record-field access is a pure declared- // type lookup, not a call-return channel), so this narrower gate // fixes #92 with zero observed blast radius (mega-CU gate: 565→566 // PASS, 0 FAIL — see fix report) instead of the wider `.or_else` (1 // FAIL). Every call site `infer_arg_ty` already resolves — including // EVERY other shape it returns `None` for that ISN'T `Member` — stays // byte-identical (this fn `return`s early, same as before). let Some(recv_ty) = BoundCtx::infer_arg_ty(obj, scope).or_else(|| { if matches!(obj.kind, ExprKind::Member { .. }) { self.infer_expr_type(obj, scope) } else { None } }) else { return; }; // Plan 172.2: normalize the receiver to a single `type_name`, mapping // `[]T`/`[N]T` → "Vec" (D239 slice alias) and peeling `ro`/`mut`, so method // calls on a SLICE receiver (`out.push(x)` where `out: []u8`) resolve to Vec's // overloads — std's pervasive spelling — not only the `Vec[T]`-named form. let mut rt = &recv_ty; loop { match rt { TypeRef::Readonly(i, _) | TypeRef::Mut(i, _) => rt = i, _ => break, } } let type_name: String = match rt { TypeRef::Named { path, .. } if path.len() == 1 => path[0].clone(), TypeRef::Array(_, _) | TypeRef::FixedArray(_, _, _) => "Vec".to_string(), // [M-172.1-instance-complex-recv]: multi-segment path / generic / pointer receiver — // type_name extraction not implemented; overload/callee check skipped for these. // Covers: `a.b.method()` (path recv), `Vec[T]`-receiver in generic body (unnamed), // `*mut T`-receiver. A future pass can extract the base name for Vec[T]/Option[T]. _ => return, }; let type_name = type_name.as_str(); // Plan 200 П19 [E_UNKNOWN_METHOD]: a `[N]T` FixedArray receiver normalizes to // "Vec" just above (D239 spelling reuse for the SLICE `[]T` case), but `[N]T` is // NOT `Vec[T]` at the C level ([M-fixed-array-value-semantics] — inline `{ T // data[N]; }`, no heap pointer/len/cap header) and has ZERO real `.nv` methods // (Plan 200-19 architecture note — method-level const-generic `N` isn't in the // language). Before this gate, ANY method call on a FixedArray receiver — a genuine // typo (`arr.lenx()`) as much as the two now-synthesized accessors this Plan adds // (`@len`/`@ptr`, handled entirely upstream of `check_instance_overload` — see // `fixed_array_accessor_return`/Ш0 doc there) — fell through this permissive `Named`/ // `Array`/`FixedArray`-blind normalization straight to codegen, which has no correct // fallback for a non-Vec-shaped struct and panics `[P67-LEGACY]` (verified: `nova // test` on `a.lenx()` for `a [4]u8` ICEs at emit_c.rs, "method call return type // unknown"). Scoped to `FixedArray` ONLY (not `Array`/`[]T`, which genuinely IS // `Vec[T]` and keeps its existing, correct resolution below unchanged). if matches!(rt, TypeRef::FixedArray(..)) && !matches!(method_name, "len" | "ptr") { errors.push(Diagnostic::new( format!( "[E_UNKNOWN_METHOD] no method `{}` on fixed-array type `[N]T` — a \ `[N]T` receiver only has the compiler-synthesized `@len()`/`@ptr()` \ accessors (D440) plus indexing (`arr[i]`, D238); it is a distinct \ inline value type, not `Vec[T]` — Vec's own methods do not apply.\n \ fix: check the method name for a typo, or copy into a `[]T`/`Vec[T]` \ first if you need the fuller Vec surface.", method_name, ), span, )); return; } // Plan 177 Ф.3 [E_UNKNOWN_METHOD] (§0/§1/§6): a PRIMITIVE receiver whose method // resolves in NO channel — not a user/prelude method (`method_overloads`), not a // codegen builtin-intrinsic (`primitive_instance_method_known`: D109/D74/str/ // whitelist), and not a prefix-generic/blanket receiver (`fn[T] T @m`) — is a // genuine unknown-method call. Reject it HERE with a clean checker diagnostic // instead of letting it leak to codegen, which panics `[P67-LEGACY]` on an `int` // receiver (`emit_c.rs:39841` — «checker must annotate») and mis-types a `str` // receiver to `nova_int` → CC-FAIL; this leak also blocked the Plan 177 Ф.3 // `spec_tests/conformance/neg/` fixtures for the D325-retracted names. // // SCOPED to primitives (§7.3 permissive calibration): user / opaque / generic- // instance receivers keep their EXISTING resolution (`sig_complete_builtin` → // E7320 just below for extern builtins; user types stay codegen-resolved), where // the checker does NOT hold the full C-runtime method set and a rule here would // false-positive. For a REAL primitive the checker's knowledge IS complete once the // three channels above are consulted, so a miss is authoritative. // // `never` / `any` / `unit` are EXCLUDED (they are in `is_primitive_recv_name` but // are NOT method-bearing value primitives): a `never`-typed receiver means the // checker could NOT infer a concrete type (a divergent expression, or — the bulk // of the blast-radius survey — an opaque runtime type whose methods live outside // the checker's tables, e.g. a net `TcpListener`/`UdpSocket` receiver seen as // `never`). Flagging `never.close()` / `never.send_to()` would be a false positive; // `never`/`any`/`unit` therefore stay permissive (defer to codegen). §7.3. if is_primitive_recv_name(type_name) && !matches!(type_name, "never" | "any" | "unit") && self.method_overloads(type_name, method_name).is_none() && !crate::codegen::emit_c::CEmitter::primitive_instance_method_known( type_name, method_name, ) && !self.prefix_generic_method_exists(rt, method_name) { errors.push(Diagnostic::new( format!( "[E_UNKNOWN_METHOD] no method `{}` on primitive type `{}` — the \ receiver has no such instance method (checked: built-in primitive \ methods, prelude/protocol methods, and any generic `fn[T] T @{}` \ blanket).\n \ fix: check the method name for a typo, or `import` the stdlib module \ that provides it (e.g. `std.unicode` for char methods).", method_name, type_name, method_name, ), span, )); return; } // Plan 196.7 [M-174.1-to-str-name-collision-codegen-bug]: an Array/slice receiver // normalizes to "Vec" above (D239 `[]T ≡ Vec[T]`, for the pervasive Vec-overload // spelling), but a CONCRETE facade method is registered under the ELEMENT-spelling // key (`fn []u8 @to_str()` → method_table["[]u8"]), invisible to method_table["Vec"]. // Without this the checker records NO callee for `bytes.to_str()`, so codegen's // one-window dispatch has nothing to read and the bare-T `to_str()` blanket hijacks // the call → wrong C body (`Nova_Nova_Vec____nova_byte_method_to_str` returning `str` // vs the `[]u8 @to_str -> Result` body) → `->tag` on a `nova_str` CC-FAIL. Resolve // the concrete facade method HERE by the array spelling and record its `FnDecl.span` // (same c1/c2 single/unique-compatible rule as the Vec path below). GATED to methods // ABSENT from the "Vec" table so no Vec-method resolution changes (byte-identical); // fires only for the array-only facade family. D84 "concrete beats generic". // Element of an array/slice/`Vec[E]` receiver, in the `[]E` spelling the // facade method is registered under (both the `[]u8` source form and a // `Vec[u8]`-typed pattern/field binding must reach `[]u8 @to_str`). let array_elem_key: Option<String> = match rt { TypeRef::Array(inner, _) | TypeRef::FixedArray(_, inner, _) => Some(format!("[]{}", render_type_ref(inner))), TypeRef::Named { path, generics, .. } if path.len() == 1 && path[0] == "Vec" && generics.len() == 1 => Some(format!("[]{}", render_type_ref(&generics[0]))), _ => None, }; if let Some(array_key) = array_elem_key.filter(|_| self.sig.method_table.get(type_name) .map_or(true, |m| !m.contains_key(method_name))) { if let Some(ov) = self.sig.method_table.get(array_key.as_str()) .and_then(|m| m.get(method_name)) { let mut compat: Vec<crate::diag::Span> = Vec::new(); for f in ov { if let Some(true) = self.overload_applicability(f, args, gs, scope) { compat.push(f.span); } } let chosen = if ov.len() == 1 { Some(ov[0].span) } else if compat.len() == 1 { Some(compat[0]) } else { None }; if let Some(sp) = chosen { self.resolved_callees.borrow_mut().insert(call_id, sp); } return; } } // Plan 172.1.1 (U.3.2 probe): lift the primitive gate — `method_table.get` below returns // None for receivers absent from the checker's table (graceful no-op), so this is SAFE; it // RECORDS a callee only when the primitive's method sig IS present. Measures empirically // whether primitive method sigs reach `self.sig` (vs the comment's assumption they're // external-only). The consumer also requires `fn_ret_by_span` → un-channeled spans fall to // legacy regardless. (was: `if is_primitive_recv_name(type_name) { return; }`) let Some(methods) = self.sig.method_table.get(type_name) else { return; }; let Some(overloads) = methods.get(method_name) else { // [M-172.1-sync-extern-narrowing-migration] (U.1.3b, §6): для // SIG-COMPLETE builtin-типа (его ПОЛНЫЙ метод-набор приходит из // builtin_sig_modules — sync.nv externs) неизвестный метод — ЧИСТАЯ // checker-ошибка, а не P67-паника codegen'а глубже (паника in-process // убивает весь прогон раннера; §6 «C — не первичный чекер»). // Для остальных типов — прежний graceful no-op (их метод-набор в // sig-таблицах неполон: C-runtime-only методы, протоколы, blanket'ы). // Полнота = ПОЛНЫЙ метод-набор типа приходит из builtin_sig_modules // (у extern-типов вроде AtomicU64 нет Item::Type — только extern fns // с receiver'ом; свидетель полноты — наличие таких fns). // // [M-char-blanket-shadowed-by-sig-complete] (2026-07-17, стоп-волна // fix-runtime-lint-debt-регресс): "SIG-COMPLETE" — неправда для // примитива, у которого ЕСТЬ bare-T blanket (`fn[T] T @m`, D145) — // builtin_sig_modules описывает только КОНКРЕТНЫЕ receiver-методы, // blanket живёт в ОТДЕЛЬНОЙ книге (method_table keyed по generic- // parameter-имени, см. `prefix_generic_method_exists`). Как только // тип получает ЛЮБОЙ конкретный receiver-метод в builtin_sig_modules // (напр. `char @to_stringbuilder()` в string_builder.nv), sig_complete // становится true для ЭТОГО типа целиком — включая методы, реально // резолвящиеся ТОЛЬКО через blanket (`char.to_str()` → bare-T // `fn[T] T @to_str()`, std/runtime/string/core.nv). Без доп. проверки // — ложный E7320 (было: std/src/runtime/sync_test.nv CODEGEN-FAIL // после добавления `char @to_stringbuilder`). Тот же guard, что и на // раннем primitive-gate (line ~10599) — «конкретное после проверки // на blanket», не наоборот. let sig_complete_builtin = crate::codegen::external_registry::builtin_sig_modules() .iter() .flat_map(|m| m.items.iter()) .any(|it| matches!(it, Item::Fn(f) if f.receiver.as_ref().map(|r| r.type_name.as_str()) == Some(type_name))); // [M-char-blanket-shadowed-by-sig-complete] follow-up (2026-07-17, found // widening node_substs producers — d109_primitive_builtin_methods.nv // CODEGEN-FAIL blocking the WHOLE conformance mega-CU, char/eq/lt): the // just-landed fix above only re-checked the bare-T BLANKET channel // (`prefix_generic_method_exists`, D145 `.nv`-declared) — it missed the // THIRD channel the early primitive-gate above (~10655-10658) ALSO // consults: `primitive_instance_method_known` (D109 compiler-INTRINSIC // eq/lt/le/gt/ge/hash, emitted directly by `prim_builtin_method` in // emit_c.rs — never declared in ANY `.nv` source, so neither // `builtin_sig_modules` nor `prefix_generic_method_exists` can see it). // `char` becoming sig-complete (same trigger as the parent fix — // `char @to_stringbuilder()`) shadows `char.eq()`/`char.lt()`/etc the // SAME way it shadowed `char.to_str()` — same guard pattern, third // channel added for symmetry with the early gate. if sig_complete_builtin && !self.prefix_generic_method_exists(rt, method_name) && !crate::codegen::emit_c::CEmitter::primitive_instance_method_known( type_name, method_name, ) { errors.push(Diagnostic::new( format!( "[E7320] no field or method `{}` on type `{}`", method_name, type_name ), span, )); } return; }; // Plan 172.1 U.4.3 (c1/c2): record the chosen INSTANCE callee into the // resolved-callee channel — substrate for codegen consume (§0/§7.7: the checker // CHOOSES the overload by arg type, codegen lowers its own view by `FnDecl.span`). // c1: SINGLE-overload (unambiguous → byte-identical, codegen picks the same one). // c2: MULTI-overload → the UNIQUE type-compatible overload. Codegen's C-type // re-dispatch MIS-PICKS when the arg C-type does not EXACTLY match a param string // (narrowing arg like `u8`→`int`, with another overload declared first → strict // match empty → `pool.first()` = wrong overload → CC-FAIL); reading THIS choice for // dispatch (codegen consume, c2) FIXES it. (Generic instance methods take the mono // registration path and are NOT in codegen's span indexes, so the consume/assert // naturally scope to non-generic — see `fn_ret_by_span` / `c_name_by_span`.) let mut any_arity = false; let mut compat_spans: Vec<crate::diag::Span> = Vec::new(); // [M-concrete-instance-arity-overload-mangle] (реестр 221.1 №34): track // the compatible FnDecls alongside their spans — the comment above // ("Generic instance methods... NOT in codegen's span indexes, so the // consume/assert naturally scope to non-generic") assumed a // method-level-generic sibling (`@get[R](path str, f fn(int) -> R)`) // would never show up here ALONGSIDE a compatible concrete overload // (`@get(path str, h Handler)`) — but `assignable` structurally // coerces a closure/fn-newtype arg to a generic `fn(..) -> R` slot // just as readily as to the concrete named param type, so BOTH land // in `compat_spans` and the `len()==1` unique-choice rule below // silently produces NO record for a call that is, in fact, // unambiguous to a human (and already type-checks green). Codegen's // `has_sentinel_here` dispatch (emit_c.rs) then unconditionally // routes ANY call to that method name through the generic mono path // (its single-value `mono_method_decls` map cannot hold the concrete // sibling at all) — breaking calls whose argument is itself a `Call` // expression (E7001, the generic path's own closure-arg-return // inference only recognizes a bare `ClosureLight`). let mut compat_fns: Vec<&FnDecl> = Vec::new(); for f in overloads { match self.overload_applicability(f, args, gs, scope) { Some(true) => { any_arity = true; compat_spans.push(f.span); compat_fns.push(f); } Some(false) => any_arity = true, None => {} // arity-fail for this candidate } } let any_compat = !compat_spans.is_empty(); // D84 "concrete beats generic" (precedent already established at the // array-facade site above, ~12196): when a concrete (non-generic) // overload is compatible ALONGSIDE ≥1 method-level-generic sibling, // the concrete one is the unambiguous intended callee — a bare // generic-mono routing candidate is not a real alternative the // caller chose between (mirrors the `fn_span: None` sentinel design // codegen uses for the same reason, emit_c.rs ~16154). Exactly ONE // concrete match still wins deterministically even when several // generic siblings ALSO structurally apply. // // GATED to calls where NO argument is a bare `ClosureLight` literal // (`|x| ...`) directly at this call site. `assignable` is lenient // about a `ClosureLight` literal's OWN return type against a concrete // fn-newtype param (full body inference is deferred, not re-derived // here) — verified empirically: a closure whose body returns `int` // (`|n| n * 2`) still reports `Compat::Ok` against a concrete // `Handler = fn(int) -> str` param, so an unqualified tie-break would // WRONGLY prefer the concrete overload over the correct generic one // for the generic method's OWN legitimate call sites — confirmed live // (repro34c: routed to the `nova_str`-returning concrete mono, `int` // reinterpreted as a `nova_str` heap pointer → GC "Out of Memory"). A // `Call`-expression argument (the ACTUAL bug shape, [M-concrete- // instance-arity-overload-mangle]) has already been fully typed to a // concrete return type before `assignable` sees it, so no such // leniency gap exists there — the tie-break stays exact for the shape // it targets. // // [M-2223-closurefull-generic-overload-resolution] (реестр 221.1 // №124): `ClosureFull` (`fn(x T) -> U { ... }`) used to be lumped // into this SAME exemption by a stale over-broad comment — but its // grammar mandates every param type AND the return type spelled // explicitly (no leniency gap the `ClosureLight` rationale above // describes), so it does NOT need the exemption; it needs the // separate EXACT structural check below instead // (`closure_args_match_concrete`). Excluding it from the concrete // tie-break entirely used to force EVERY `ClosureFull`-argument call // through the generic-mono path even when an unrelated bound-generic // sibling merely happened to exist alongside a genuinely intended // concrete overload — `[E7001] cannot infer C type for closure-arg // return type` (Router-style extractor sugar's target shape, Plan // 222.3 §5 diagnosis). let has_bare_closurelight_arg = args.iter().any(|a| matches!( a.expr().kind, ExprKind::ClosureLight { .. } )); // [M-generic-mono-multi-instantiation-concrete-sibling-collision] // (реестр 221.1 №105): additionally require the concrete candidate's // OWN Func-shaped param(s) to genuinely accept the arg's ACTUAL return // type wherever that is statically decidable — see // `concrete_sibling_return_type_ok`'s doc for the full mechanism // (`assignable`'s bare-`Func`→`Any` collapse otherwise lets a // return-type-incompatible NAMED function value win this tie-break // over its real bound-generic target). `ClosureLight` stays exempt // via the `has_bare_closurelight_arg` short-circuit above (unchanged); // `ClosureFull` additionally requires an EXACT structural match // (`closure_args_match_concrete`, №124) — `concrete_sibling_return_ // type_ok` alone degrades PERMISSIVE for a bare closure literal (its // `infer_expr_type` call has no closure arm), which would silently // accept a signature-MISMATCHED `ClosureFull` too. let concrete_compat: Vec<crate::diag::Span> = if has_bare_closurelight_arg { Vec::new() } else { compat_fns.iter() .filter(|f| f.generics.is_empty()) .filter(|f| self.concrete_sibling_return_type_ok(f, args, scope)) .filter(|f| self.closure_args_match_concrete(f, args)) .map(|f| f.span) .collect() }; // Single-overload → codegen picks it regardless (c1). Multi-overload → record only // when EXACTLY ONE overload is type-compatible (the unambiguous choice); 0 or ≥2 // compatible = error / genuine ambiguity → leave to codegen (no record). let chosen_span = if overloads.len() == 1 { Some(overloads[0].span) } else if concrete_compat.len() == 1 { Some(concrete_compat[0]) } else if compat_spans.len() == 1 { Some(compat_spans[0]) } else if let Some(sp) = self.mode_axis_tiebreak(Some(obj), &compat_fns, args) { // №309/№317: ≥2 type-compatible candidates (the branches above all // missed) — try the D84 mode/receiver-mutability axis (see doc on // `mode_axis_tiebreak`) before giving up. `None` (not a pure axis // set, or a genuine tie) falls through unchanged. Some(sp) } else { None }; if let Some(sp) = chosen_span { self.resolved_callees.borrow_mut().insert(call_id, sp); } if any_arity && !any_compat { errors.push(Diagnostic::new( format!( "[E_NO_MATCHING_OVERLOAD] no overload of method `{}` on type `{}` \ matches the given argument types", method_name, type_name, ), span, )); } // Plan 172.2 [M-instance-method-arg-scalar-narrowing]: IMPLICIT NARROWING on a // method argument — the dispatch hole (single-overload scalar narrowing like // `vec_u32.push(int_var)` passed checker existence/arity + codegen single-overload // + C category, narrowing nobody). Run on the CHOSEN overload: substitute the // receiver's concrete type-args into each param (`Vec[T].push(value T)` + receiver // `Vec[u32]` → `u32`), then `assignable`; emit `[E_IMPLICIT_NARROWING]` only on // `Narrowing` (mirror the free-fn loop in `f1_check_call`, same `is_int_narrowing` // rule). Permissive on Bad/Unknown/unsubstituted-generic → 0 false positives (a Bad // arg is already E_NO_MATCHING_OVERLOAD above; a still-generic param resolves to // `Any` and skips). This makes the checker, not C, the soundness owner for method // args (§1). The receiver type via the same `infer_arg_ty` already gated to // non-primitive receivers (U.3.2) above; builtin `Vec`/user generics both covered. if let Some(sp) = chosen_span { if let Some(f) = overloads.iter().find(|o| o.span == sp) { let subst = f.receiver.as_ref() .map(|r| build_recv_subst(r, &recv_ty)) .unwrap_or_default(); let callee_gs: GenericScope = f.generics.iter().map(|g| (g.name.clone(), g.clone())).collect(); if let Ok(bindings) = crate::argbind::bind_call_args(&f.params, args) { for (pi, binding) in bindings.iter().enumerate() { let ai = match binding { crate::argbind::ArgBinding::Positional(i) | crate::argbind::ArgBinding::Named(i) => *i, _ => continue, }; let Some(param) = f.params.get(pi) else { continue }; if param.is_variadic { continue; } let Some(arg) = args.get(ai) else { continue }; // Plan 172.2: skip a compiler-synthesized arg with no real source // span (desugared `v[i] = x` index-writes, interpolation, etc.). Any // narrowing there belongs to the SOURCE construct, not a written // method arg; the dummy span misattributes the diagnostic (past-EOF) // and the desugar may not carry the user's intent. Conservative — the // user-visible `obj.method(arg)` form (real span) is what 172.2 targets. if arg.expr().span == crate::diag::Span::dummy() { continue; } let exp_ty = subst_typeref(¶m.ty, &subst); // Plan 172.1 [literal-coercion channel] (§0/§1): DEFINITE site — // CHOSEN overload with the receiver's concrete type-args substituted // in (`Vec[u32].push(0x80)` → `exp_ty = u32`), so the literal arg // carries the sized element type instead of collapsing to `nova_int`. // // [M-instance-method-closure-arg-generic-return] (mirrors the // [196.5 closure-lowering fix] applied to the free-fn/static-method // call site, ~11343 below): `subst` above is RECEIVER-level only // (`build_recv_subst`) — it never binds METHOD-level generics // (`f.generics`, e.g. `RetryPolicy @execute[T, E](body fn() Fail[E] // -> T)` on a non-generic receiver). A `exp_ty` that still mentions // one of `callee_gs` (`T`) is therefore NOT the truth for this call; // materializing against it stamps an ERASED `Func{ret: Named("T")}` // into `resolved_types` (`Named` with empty args PASSES the // `ConcreteNamedNoArgs` gate in `materialize_literal_coercion`'s // ClosureLight arm — structurally indistinguishable from a genuine // concrete no-arg record type at that layer) — codegen's channel- // first `closure_channel_ret_c` then trusts it and emits the lambda // body with a bogus `Nova_T*` C return (CC-FAIL: retry_test.nv, // `policy.execute(|| { ...; "success" })`, T=str — `Nova_T*` isn't // even a real generic-erasure convention here, just the catch-all // "unknown Named type" fallback in `resolved_named_to_c` colliding // with `Vec[T]`'s OWN erasure placeholder of the same letter). No // receiver-subst-style unification is attempted here (method-level // generics, not receiver-level) — absence is honest: skip // materialization so the consumer falls back to the legacy // body-walk (`infer_lambda_return_type_with_params`), which // correctly infers the concrete return by walking the closure's // own (possibly multi-statement) body. if !typeref_mentions_any(&exp_ty, &callee_gs) { self.materialize_literal_coercion(arg.expr(), &exp_ty); } // [M-d55-d108-sum-lift-map-literal-gaps] (реестр 221.1 №114 б): // a param whose DECLARED type mentions the RECEIVER's own generic // name (`insert(key K, val V)` on `HashMap[K,V]`) is exactly the // class `overload_applicability`'s earlier arity/category pre-check // (this fn, ~line 12468) could NOT validate — it ran `assignable` // against the RAW, unsubstituted `K`/`V` placeholder (not a real // declared type at all, so permissive/`Unknown`, never `Bad`) — the // narrowing-only check below's own doc-comment assumption ("a Bad // arg is already E_NO_MATCHING_OVERLOAD above") is FALSE for this // one class: a str passed where a substituted `JsonValue` is // expected (`fields.insert("sub", sub)` on `HashMap[str, // JsonValue]`) sailed through both checks silently, and only // surfaced as a codegen CC-FAIL. Scoped tightly — non-empty // `recv_generic_names` AND the param's OWN declared type actually // mentions one — every concrete (non-generic) param call site is // completely unaffected (zero blast radius, byte-identical). D55 // single-wrap sum-lift (`str` → `JsonValue.Str(..)`) is ALREADY // accepted here as `Compat::Ok` by `assignable`'s own fallback (the // same mechanism `try_wrap_leaf` materializes elsewhere) — this // does not flag those, only genuinely un-liftable mismatches. The // AST REWRITE (actually inserting the `JsonValue.Str(..)` wrapper // call so codegen sees a well-typed value) is NOT done here — this // closes the silent diagnostic hole only; see window report for the // honest remainder. // №375 (window p375-ptr2): same source-check, generic- // receiver instance-call arg path (this site's own // `Compat::CoerceConflict` surfacing below is gated to // `generic_param`-only — the source-check must NOT // depend on that gate). self.check_addrof_mut_from_ro_source(arg.expr(), &exp_ty, errors); let compat = self.assignable(arg.expr(), &exp_ty, gs, &callee_gs, scope); let recv_generic_names: HashSet<String> = subst.keys().cloned().collect(); let generic_param = !recv_generic_names.is_empty() && typeref_mentions_any(¶m.ty, &recv_generic_names); match compat { Compat::Bad { found } if generic_param => { errors.push( Diagnostic::new( format!( "[E7301] cannot pass value of type `{}` as \ argument `{}` of type `{}`", found, param.name, typeref_display(&exp_ty), ), arg.expr().span, ) .with_note_at( format!("parameter `{}` declared here", param.name), param.span, ), ); } Compat::CoerceConflict { msg } if generic_param => { errors.push( Diagnostic::new(msg, arg.expr().span).with_note_at( format!("parameter `{}` declared here", param.name), param.span, ), ); } Compat::Narrowing { from, to } => { // [M-172.1-sync-extern-narrowing-migration]: extern-callee // (builtin sync/atomics API) — enforcement отложен до // миграции корпуса (см. второй сайт + backlog). errors.push( Diagnostic::new( format!( "[E_IMPLICIT_NARROWING] cannot pass `{}` as argument \ `{}` of narrower type `{}` — implicit int narrowing \ loses range; use an explicit `{} as {}` cast (D54)", from, param.name, to, "<value>", to, ), arg.expr().span, ) .with_note_at( format!("parameter `{}` declared here", param.name), param.span, ), ); } _ => {} } } } } } } /// [M-fn-value-binding-untyped-silent] (реестр 221.1 №101 b): `f1_check_call`'s /// Ident-callee arm only resolves a callee through `sig.fn_decls` (global fn /// declarations) — a call through a LOCAL bound to a first-class `TypeRef::Func` /// value (HOF param, or a Plan-228 fn-value binding: `ro t1 = test1; t1(...)`) /// has no `FnDecl` to check against and previously skipped arg-checking /// entirely. `name` is not itself in `sig.fn_decls` (caller already checked) — /// look it up in `scope` instead; no-op (`Compat::Unknown`-equivalent skip) if /// it isn't bound to a `Func` there (an ordinary unresolved-name call, handled/ /// reported elsewhere). fn check_fn_value_call( &self, name: &str, args: &[CallArg], call_span: Span, gs: &GenericScope, scope: &HashMap<String, TypeRef>, call_id: crate::ast::ExprId, errors: &mut Vec<Diagnostic>, ) { let Some(TypeRef::Func { params, return_type, .. }) = scope.get(name) else { return; }; // Named/Spread args carry no per-param name/shape info to check against a // bare fn-value signature (no `FnDecl` param names/defaults to resolve them // against) — stay honest and skip rather than guess (mirrors the codebase's // general "undecidable → Unknown, not an error" discipline). if args.iter().any(|a| !matches!(a, CallArg::Item(_))) { return; } if args.len() != params.len() { errors.push(Diagnostic::new( format!( "[E_FN_VALUE_CALL_ARITY] `{}` (fn-value, `{}`) expects {} argument{}, \ found {}", name, typeref_display(&TypeRef::Func { params: params.clone(), effects: Vec::new(), return_type: return_type.clone(), extern_abi: None, span: call_span, }), params.len(), if params.len() == 1 { "" } else { "s" }, args.len(), ), call_span, )); return; } for (arg, param_ty) in args.iter().zip(params.iter()) { let arg_expr = arg.expr(); // №375 (window p375-ptr2): same source-check, fn-value call path. self.check_addrof_mut_from_ro_source(arg_expr, param_ty, errors); match self.assignable(arg_expr, param_ty, gs, gs, scope) { Compat::Bad { found } => { errors.push(Diagnostic::new( format!( "[E7301] cannot pass value of type `{}` as an argument of \ `{}` declared as `{}`", found, name, typeref_display(param_ty), ), arg_expr.span, )); } Compat::OutOfRange { msg } => { errors.push(Diagnostic::new( format!("[E_LIT_OUT_OF_RANGE] {msg}"), arg_expr.span, )); } Compat::Narrowing { from, to } => { errors.push(Diagnostic::new( format!( "[E_IMPLICIT_NARROWING] cannot pass value of type `{}` as an \ argument of narrower type `{}` — implicit int narrowing loses \ range; use an explicit `... as {}` cast (D54)", from, to, to, ), arg_expr.span, )); } Compat::CoerceConflict { msg } => { errors.push(Diagnostic::new(msg, arg_expr.span)); } Compat::Ok | Compat::Unknown => {} } } // Channel materialization (§0/§1), symmetric with the resolved-FnDecl call // sites below: annotate the call's own return type so downstream channel // consumers (codegen) read it instead of re-deriving. if call_id.is_set() { if let Some(ret) = return_type { if !typeref_mentions_any(ret, gs) { let rt = ResolvedType::from_type_ref(ret); self.resolved_types_buf.borrow_mut().insert(call_id, rt); } } } } fn f1_check_call( &self, func: &Expr, args: &[CallArg], trailing_present: bool, gs: &GenericScope, scope: &HashMap<String, TypeRef>, errors: &mut Vec<Diagnostic>, // Plan 172.1 U.3.4: call-site `ExprId` — key for the resolved-callee channel. call_id: crate::ast::ExprId, ) { // Trailing-форма перепривязывает последний param — пропускаем // (редко, и codegen всё равно проверяет). if trailing_present { return; } // [M-196.5 producer-A width fix, D310] Capture the explicit turbofish // type-args ALONGSIDE `base` — `producer=A` below (the free-fn/static- // method generic-return arm) used to derive `subst` PURELY from // structural `unify_type(param, arg)`, silently discarding an explicit // `func[T1, T2](...)` annotation. For a bare int literal arg // (`d310_twice[i64](10)`) `infer_expr_type` collapses to the default // `nova_int` width, so the unify-derived `T` lost the user's explicit // `i64` — a real bug (mismatch vs the legacy channel), not a gap. let explicit_type_args: Option<&Vec<TypeRef>> = match &func.kind { ExprKind::TurboFish { type_args, .. } => Some(type_args), _ => None, }; let base: &Expr = match &func.kind { ExprKind::TurboFish { base, .. } => base.as_ref(), _ => func, }; // Резолвим callee только однозначно (ровно один overload). let callee: &FnDecl = match &base.kind { ExprKind::Ident(n) => { // [Facet-B D307 §1/§3] `priv(file)` free-fn кандидаты видны ТОЛЬКО // из своего файла — фильтруем ПЕРЕД arity/type-compat, иначе чужой // file-private overload участвует в резолве call-site другого файла // (folder-CU bleed). let caller_file_id = base.span.file_id; // №534-фикс, ред. по замечанию координатора: единица // "своего" — МОДУЛЬ (folder co-equal files), а не файл. // `10-overloading.md` §«LLM-критерий»: «все перегрузки // имени должны быть в одном модуле» — это УЖЕ спека, не // новое правило; резолвер обязан её соблюдать, а не // изобретать file-level shadow (та черновая версия этой // правки ломала ЗАКОННУЮ перегрузку одного имени между // co-equal файлами ОДНОГО folder-модуля: `f(int)` в // `a.nv` + `f(str)` в `b.nv` того же `module foo` — // вызов из `a.nv` перестал бы видеть `f(str)` из `b.nv`). let caller_module: Option<Vec<String>> = self.file_modules.borrow() .get(&caller_file_id).cloned(); let is_own = |c: &&FnDecl| -> bool { if c.span.file_id == caller_file_id { return true; } match &caller_module { Some(cm) => self.file_modules.borrow() .get(&c.span.file_id) .map_or(false, |dm| dm == cm), None => false, // модуль неизвестен → падаем на file_id (уже false здесь) } }; let visible: Option<Vec<&FnDecl>> = self.sig.fn_decls.get(n).map(|v| { let filtered: Vec<&FnDecl> = v.iter() .filter(|c| !c.file_private || c.span.file_id == caller_file_id) .copied() .collect(); // №534 (class of №514 `types_get_for_file`, W2 specificity // principle "собственный носитель > делегат"): `fn_decls` is // a BARE-NAME, CU-wide table — an unqualified bare-Ident call // must NOT let an unrelated candidate declared in a DIFFERENT // MODULE dilute resolution of a same-named function the // CALLER'S OWN MODULE declares. Before this, a same-arity // same-named `fn` in a totally unrelated module (never // imported, no lexical relationship to the caller) competed // on equal footing via `overload_applicability` — landing on // 0-or-≥2 "compatible" candidates (arity ties) more easily, // which silently DROPS the call from `resolved_callees` // (checker gives up, gap → codegen-resolved). Codegen's own // call-emission mangles the SAME-MODULE callee correctly // regardless (unaffected), but `resolved_callees`' // ABSENCE was consumed downstream by // `collect_resolved_call_target_names_expr` (emit_c.rs) to // decide "is this bare Ident inside a `spawn` body a call // target (skip) or a variable (maybe capture)?" — an // unresolved call fell through to the flat, CU-wide // `var_types` slot and could pick up a COMPLETELY UNRELATED // same-named PARAMETER's type from yet another module, // emitting a spurious ctx-capture field assigned from the // (unmangled, nonexistent-as-a-value) bare call-target name // — C `use of undeclared identifier` (repro: // spec_tests/conformance/standalone/p534_routes_bare_name/). // Fix: a same-MODULE declaration SHADOWS every other-module // candidate outright — standard lexical-scoping "innermost // wins", mirroring `file_private`'s own caller-file // preference one level up (file → module, per D78/co-equal // files). Cross-module genuine multi-overload resolution // (imported free-fn) is untouched — this only narrows the // set when the caller's OWN module is itself among the // candidates. Within-module multi-file overloading // (`f(int)` in `a.nv` + `f(str)` in `b.nv`, same `module // foo`, per `10-overloading.md` §«LLM-критерий») stays // fully visible — `is_own` is true for BOTH declaring // files, so neither is filtered out. if filtered.iter().any(is_own) { filtered.into_iter().filter(|c| is_own(c)).collect() } else { filtered } }); match visible.as_deref() { Some([single]) => single, Some(multi) if !multi.is_empty() => { // Plan 172.1 U.3.1: resolve free-fn overloads in the // CHECKER (§0/§1). Consider ONLY arity-applicable overloads // (`overload_applicability` → Some(_)); if at least one // binds by arity but NONE is category-compatible, the call // is a type error that previously leaked to codegen/C // ("no matching overload" / CC-FAIL) — now a clean Nova // diagnostic. If NO overload binds by arity (wrong-arity // call or a non-call reference to the name), defer: that // arity error is reported by BoundCtx, not here. If ≥1 // overload is type-compatible, defer FINAL selection to // codegen's exact-C-type match (that move is U.3.4). let mut compat: Vec<&&FnDecl> = Vec::new(); let mut any_arity = false; for c in multi.iter() { match self.overload_applicability(c, args, gs, scope) { Some(true) => { any_arity = true; compat.push(c); } Some(false) => any_arity = true, None => {} } } if any_arity && compat.is_empty() { errors.push(Diagnostic::new( format!( "[E_NO_MATCHING_OVERLOAD] no overload of `{}` \ matches the given argument types", n, ), base.span, )); } // Plan 172.1 U.3.1 §0 extension: record UNIQUE arity+type-compat // overload in resolved_callees + resolved_types_buf. Mirrors the // U.3.2 multi-overload Path site and U.4.3 instance-method site. // Enables Call-channel for multi-overload free fns like // assert(bool)/assert(bool,str). // [M-172.1-free-fn-multi-overload-ambiguous]: 0 or ≥2 compat → codegen, // UNLESS `pick_no_default_overload` finds a unique zero-default // candidate among the ≥2 (see [M-196-freefn-arity-overload-default- // ret-mismatch] fix above) — a genuine same-arity/-category tie // (D84 axis 2) still defers, unchanged. let chosen_opt: Option<&&FnDecl> = match compat.len() { 0 => None, 1 => Some(compat[0]), _ => self.pick_no_default_overload(&compat, args).map(|i| compat[i]) .or_else(|| { // №309/№317: free-fn mode-axis tiebreak (no // receiver — `narrow_by_param_mode`'s mode-only // form, ADDITIONALLY binding-form-aware via // `mode_axis_tiebreak`/`expr_mode_axis_consume_ // eligible`). Only reached when the default-count // tiebreak above ALSO found no unique winner. let fns: Vec<&FnDecl> = compat.iter().map(|f| **f).collect(); self.mode_axis_tiebreak(None, &fns, args) .and_then(|sp| compat.iter().find(|f| f.span == sp).copied()) }), }; if let Some(chosen) = chosen_opt { self.resolved_callees.borrow_mut().insert(call_id, chosen.span); if call_id.is_set() { if let Some(ret_ty) = &chosen.return_type { let callee_gs_inner = fn_generic_scope(chosen); if !typeref_mentions_any(ret_ty, &callee_gs_inner) && !typeref_mentions_any(ret_ty, gs) { let rt = ResolvedType::from_type_ref(ret_ty); self.resolved_types_buf.borrow_mut().insert(call_id, rt); // [M-crossmodule-samename-typecheck-bleed] (221.1 №28): // stash the CALLEE's own return-type annotation span // (its declaring file) — see field doc. self.call_return_decl_span.borrow_mut() .insert(call_id, ret_ty.span()); } } } } return; } // [M-fn-value-binding-untyped-silent] fix (реестр 221.1 №101 b, // 2026-07-25): `n` names no VISIBLE global fn (or none at all) — // this whole match's ONLY Ident-callee path is fn_decls-keyed, so // a call through a LOCAL bound to a first-class `Func` value (a HOF // param, or the Plan-228 fn-value binding channel — `ro t1 = test1; // t1("oops")`) fell straight through to `return` with ZERO // arg-checking. Channel-parallel narrow check (own fn below) — // arity + per-arg `assignable`, symmetric with the resolved-FnDecl // arg-loop further down for an actual callee. _ => { self.check_fn_value_call(n, args, base.span, gs, scope, call_id, errors); return; } } } ExprKind::Path(parts) if parts.len() == 2 => { let overloads = self .method_overloads(&parts[0], &parts[1]) .map(|v| v.as_slice()); // Plan 91.8a.2 followup 2026-05-29: для receiver-types, // у которых ЧАСТЬ overload'ов лежит вне method_table // (external fn в другом stdlib-модуле, codegen builtins, // hidden D73 auto-derive paths) — single-overload arg-check // даёт ложные positives. Symptom: `let fill_s = str.from(fill)` // в std/runtime/string.nv падает с E7301 "cannot pass char as bool" // когда пользователь добавил `fn str.from(b bool) -> str` — // type-checker видит ЕДИНСТВЕННЫЙ overload (bool) и ругается // на arg типа char, не зная про external `str.from(c char)`. // // Фикс: для primitive-receiver'ов (str/int/char/bool/f*/u*/i*/uint) // **никогда** не делать arg-check на SINGLE-known-overload в // Path-форме. Codegen overload resolution в `external_registry` + // `method_overloads` корректно резолвит за нас. // // [M-str-primitive-static-arity-overload] AMEND (2026-07-17): // гейт НЕ распространяется на multi-known-overload — когда // чекеру видны ≥2 РЕАЛЬНЫХ кандидата (оба объявлены в одном // модуле, полный набор виден целиком — не "часть", риск // Plan 91.8a.2 неполноты сюда не относится), arity+type-compat // resolution в ветке `Some(multi)` ниже безопасна ТОЧНО так же, // как для non-primitive receiver'ов (тот же // `overload_applicability` + `resolved_callees` механизм, §0). // Без этого канала codegen (emit_c.rs) остаётся один на один со // строгим C-type-string `==`, слепым к Nova-типо-эквивалентным // разным сериализациям (`*u8` ro-pointee: `"nova_byte*"` arg vs // `"const nova_byte*"` param) — multi-overload primitive // Path-вызов (`str.new(buf, len)` рядом с 0-арг `str.new()`) // ложно проваливается в `E_UNKNOWN_STATIC_METHOD`. let is_primitive_recv = matches!( parts[0].as_str(), "str" | "int" | "char" | "bool" | "f32" | "f64" | "u8" | "u16" | "u32" | "u64" | "uint" | "i8" | "i16" | "i32" | "i64" ); // For primitive receivers: skip arg-check (false-positives from // external overloads unknown to checker), but still annotate // resolved_types_buf for the Call when we have an unambiguous // callee with a known return type (P67 anti-panic). // For primitive receivers: skip arg-check (false-positives from // external overloads unknown to checker). Annotation is done on the // codegen side via var_types fallback in infer_expr_c_type. if is_primitive_recv && !matches!(overloads, Some(m) if m.len() >= 2) { return; } // [M-196-facetc-generic-static-named-arg-misdispatch] (Plan 196 Facet C, // T.deserialize(d)-shape probe, last cell): `parts[0]` is a GENERIC // TYPE-PARAMETER in scope (`T.method(...)` static dispatch through a // protocol-bound type-param, D35 — e.g. serde's `T.deserialize(d)`) — // never a key in `method_table` (keyed by CONCRETE declared types only), // so `overloads` is unconditionally `None` here. `callnorm.rs`'s // `try_normalize_call` (facet-c-map §1, `Path(len==2)` arm) keys // `static_methods` by the SAME literal `type_name` and ALSO finds nothing // for "T" — so NEITHER layer can validate or reorder this call's args; a // `CallArg::Named` reaches codegen's Path-form static-dispatch emission // (`emit_c.rs` ~39268, the `method_overloads` branch) UNNORMALIZED, which // zips `args` positionally against the (by-then mono-resolved) concrete // signature WITHOUT consulting the Named label. Confirmed empirically: a // REORDERED named-arg call (`T.make(note: b, tag: a)` against `.make(tag // str, note str) -> Self`) silently returns the SWAPPED result (no // diagnostic, no crash) — even a misspelled arg NAME compiles clean. Plain // default-arg OMISSION (no Named args, just fewer positional args) already // fails LOUDLY at C-compile-time (arity mismatch) — an accepted "honest // arity error" outcome per this facet's established precedent (§6 // method-turbofish fix) — so this guard narrowly targets the SILENT case: // any `Named` arg through an unresolvable generic-type-param static // receiver. Zero blast radius on the existing corpus — std's only user of // this call shape (`serde.nv`'s `T.deserialize(d)`/`V.deserialize(sub)`) // never uses named args (196.5-facet-c-map.md §1/probe notes). if overloads.is_none() && gs.contains_key(parts[0].as_str()) && args.iter().any(|a| matches!(a, CallArg::Named { .. })) { errors.push(Diagnostic::new( format!( "[E_GENERIC_STATIC_NAMED_ARG_UNSUPPORTED] named arguments are \ not supported when calling a static method through a generic \ type-parameter receiver (`{}.{}(...)`) — the compiler cannot \ verify or reorder them until monomorphization, when the \ concrete type is known; pass positional arguments instead", parts[0], parts[1], ), base.span, )); return; } match overloads { // Reached by a primitive receiver only when `overloads` has // ≥2 entries (guard above) — `[single]` never matches that, // so this arm's behavior for non-primitive receivers is // untouched (byte-identical). Some([single]) => single, Some(multi) => { // Plan 172.1 U.3.3 + U.3.2 (172.1.1) [+ M-str-primitive- // static-arity-overload 2026-07-17: now also reached by // primitive receivers with ≥2 known overloads — same // resolution, no primitive-specific branching needed]: // arity-aware overload resolution in the CHECKER for // `Type.method(args)`. Fire E_NO_MATCHING_OVERLOAD // when ≥1 overload binds by arity but NONE is category-compatible (unchanged). // 172.1.1 ADDITION: RECORD the callee when EXACTLY ONE overload is type- // compatible (the unambiguous choice → resolved_callees → Call-channel, §0/§1 // «перегрузки резолвятся в чекере»); 0 or ≥2 compatible → codegen-resolved // (gap, not wrong). Final exact-C selection stays in codegen (U.3.4). let mut compat: Vec<&&FnDecl> = Vec::new(); let mut any_arity = false; for c in multi.iter() { match self.overload_applicability(c, args, gs, scope) { Some(true) => { any_arity = true; compat.push(c); } Some(false) => any_arity = true, None => {} } } if any_arity && compat.is_empty() { errors.push(Diagnostic::new( format!( "[E_NO_MATCHING_OVERLOAD] no overload of `{}.{}` \ matches the given argument types", parts[0], parts[1], ), base.span, )); return; } if compat.len() == 1 { compat[0] } else { return; } } // [M-p81-unknown-static-receiver-silent-p67] (окно №81, реестр 221.1): // `method_overloads` found NOTHING for this `Type.method(...)` — stays a // silent `return` (pre-existing behavior, byte-identical). INVESTIGATED: a // checker-side diagnostic here ("Type not declared/imported") was // prototyped to close the class of bug this arm actually hit (real root // cause below) but reverted — `nova check std/src` regressed 42 files // (false positives on: cross-module top-level `const` receivers like // `I64_MIN.to_nanos()` not covered by the LOCAL-only `const_types` map; // pure-runtime intrinsic namespaces with no `.nv` `type` decl at all — // `ChanReader.close_after(...)`, and siblings `Channel`/`CancelToken`/ // `StringBuilder`/`WriteBuffer`/`ReadBuffer` — resolved only via emit_c's // hardcoded B12*-class dispatch, invisible to the checker's registries). // A safe general fix needs a real cross-module const index + a checker- // visible intrinsic-namespace oracle first — out of scope for this window; // left as a finding, not implemented, to avoid the zero-tolerance // regression. Root cause of the REPORTED ICE fixed at the true source // instead: `std/src/net/stress_test.nv` called bare `Monotonic.now()` // with NO `import std.time.duration` anywhere in its module's peer-file // group — silently tolerated ONLY inside a huge multi-file compile unit // (spec_tests/conformance) that happens to ALSO contain an unrelated file // importing the real declaration (masking the missing import); an // isolated single-file/folder compile of just std/net has no such // neighbor, exposing the miss as this ICE. Fixed by adding the import. None => return, } } // Plan 81 Ф.2: module-qualified вызов `alias.func(...)` / // `mod.func(...)`. `obj` — alias/имя импортированного модуля, // `name` — свободная функция этого модуля. Раньше неизвестная // функция давала link-error (EXPECT_COMPILE_ERROR не ловил — // Plan 70.1 known-limitation); теперь — compile-error E7401. ExprKind::Member { obj, name } => { // Plan 172.1 U.3.3-instance (§6 «каждая ошибка понятна» / §1 «C — не // первичный чекер»): resolve `obj.method(args)` overloads in the checker // so a category mismatch (str↔int) is a clean `[E_NO_MATCHING_OVERLOAD]`, // NOT a leaked `CC-FAIL`. Same permissive rule as U.3.1/U.3.3 (fires only // when ≥1 arity-applicable overload but NONE category-compatible; codegen // keeps the FINAL exact-C selection, U.3.4). User-type receivers only — // primitive receivers gated (U.3.2). De-risked §7: 0 false-positives on // 707K corpus calls (62K resolved-ok), catches the crafted mismatch. self.check_instance_overload(obj, name, args, gs, scope, func.span, errors, call_id); let ExprKind::Ident(prefix) = &obj.kind else { return; }; // Локальная переменная перекрывает имя → это instance- // метод на значении, не module-call. if scope.contains_key(prefix) { return; } // Не импортированный модуль → instance-метод (codegen). if !self.imported_modules.contains(prefix) { return; } // Intrinsic namespace (gc / Time / Channel / ...) — // спец-dispatch в codegen, не обычная free fn. if is_intrinsic_namespace(prefix) { return; } match self.sig.fn_decls.get(name) { Some(overloads) => match overloads.as_slice() { [single] => single, // Plan 172.1.1 (U.3.2): multi-overload module/free-fn — record the UNIQUE // type-compatible overload (mirror the Type.method site above → Call-channel, // §0/§1); 0 or ≥2 compatible → codegen-resolved (gap, not wrong). many => { let mut compat: Vec<&&FnDecl> = Vec::new(); for c in many.iter() { if matches!( self.overload_applicability(c, args, gs, scope), Some(true) ) { compat.push(c); } } // [M-172.1-free-fn-multi-overload-ambiguous]: 0 or ≥2 compatible // overloads — checker cannot resolve unambiguously; codegen resolves. if compat.len() == 1 { compat[0] } else { return; } } }, None => { // Plan 162.2 Ф.3: cross-module fn known via sig_table — // suppress false-positive E7401 for functions that live in // a transitively imported module captured during the // signature pre-pass but not yet merged into fn_decls. if self.is_known_fn(name) { return; } errors.push(Diagnostic::new( format!( "[E7401] no function `{}` in module `{}`", name, prefix, ), base.span, )); return; } } } // прочие instance-методы (`obj.method` на значении) — // receiver-type inference ненадёжна в bootstrap; codegen // резолвит по type-info. _ => return, }; // Plan 172.1 U.3.4: record WHICH `FnDecl` this call resolved to (declaration // `Span` = stable cross-layer callee identity both layers hold). Codegen will READ // this (U.4.3) and lower THAT callee instead of re-resolving the overload (§0). Only // `.nv` callees the checker resolves UNAMBIGUOUSLY (free-fn single / `Type.method` / // module-fn — the arms above) land here; instance / multi-overload (`_ => return`) // stay codegen-resolved for now (gap, not wrong). ADDITIVE: written, not yet read → // byte-identical. self.resolved_callees.borrow_mut().insert(call_id, callee.span); // [196.5 closure-lowering fix] The literal-materialization arg-loop below must // NOT see a param type that still mentions callee generics (`f fn() -> T`): // `materialize_literal_coercion`'s ClosureLight arm gates on // `ConcreteNamedNoArgs`, which a bare `Named("T")` PASSES (any no-args name), // so the closure arg got stamped with an ERASED `Func{ret: Named("T")}` in // `resolved_types` — and codegen's channel-first `closure_channel_ret_c` then // emitted the lambda body with a `Nova_T*` C return (CC-FAIL, // d30_closure_return_generic.nv). Capture the producer-A unify subst here so // the arg-loop can substitute the CONCRETE instance types instead. let mut arg_subst: Option<HashMap<String, TypeRef>> = None; // Plan 172.1 §0 (call-return type channel): annotate the CALL expr's type from // the callee's declared return type — mirrors the check_instance_overload channel // (§3a/U.4.3). Guard with typeref_mentions_any to skip generic returns (they'd // need type-subst, not available here). Unit/never are also legal annotations. if call_id.is_set() { if let Some(ret_ty) = &callee.return_type { let callee_gs_inner = fn_generic_scope(callee); // Plan 196.5 producers-widen (Class-2): split out as a named bool — // reused below so the generic-subst arm (formerly `else if`, now an // unconditional sibling `if`) knows whether THIS branch already // materialized the return channel, to avoid a double/divergent write. let ret_is_concrete = !typeref_mentions_any(ret_ty, &callee_gs_inner) && !typeref_mentions_any(ret_ty, gs); if ret_is_concrete { // [M-172.1-d174-sync-consume-registry]: `-> Self` у static-метода // (`Mutex.new() -> Self`) обязан субституироваться в receiver-тип // ПЕРЕД материализацией — иначе канал несёт несуществующий // Named{Self}, а scope-регистрация let-binding'а травится // (д172_lm: channel=Self → codegen падал в name-keyed // `fn_ret_new` last-wins → чужой тип). Общий механизм: имя из // callee.receiver (§3 — декларация, не хардкод). let ret_owned; let ret_ty: &TypeRef = if let Some(recv) = &callee.receiver { let mut m: HashMap<String, TypeRef> = HashMap::new(); m.insert( "Self".to_string(), TypeRef::Named { path: vec![recv.type_name.clone()], generics: Vec::new(), span: callee.span, }, ); ret_owned = crate::const_fn_trampoline::subst_type_ref_pub(ret_ty, &m); &ret_owned } else { ret_ty }; let rt = ResolvedType::from_type_ref(ret_ty); self.resolved_types_buf.borrow_mut().insert(call_id, rt); // [M-crossmodule-samename-typecheck-bleed] (221.1 №28): stash // the CALLEE's own return-type annotation span (its declaring // file) — see field doc on `call_return_decl_span`. self.call_return_decl_span.borrow_mut().insert(call_id, ret_ty.span()); } // Plan 196.5 producers-widen (Class-2, d122-class gap): this used to be // `else if` — meaning a free-fn/static-method call whose DECLARED return // does NOT mention any callee generic (`fn f[K Bound](a K, b K) -> bool`) // never reached the unify-from-args block below at all, even though the // call unambiguously has generic ARGUMENTS the `node_substs` channel wants // (potential consumers just never got fed — see 196.5-perd-verification.md // §3 Класс 2). Lifted off the `else` so this block runs whenever the callee // is eligible (free fn / static method with generics), independent of // whether the return happens to mention them; the concrete-return // materialize sub-step inside stays gated on `!ret_is_concrete` (the branch // above already wrote `resolved_types_buf` for that call_id — this avoids a // second, possibly-divergent write for the SAME channel). The `node_substs` // write is genuinely unconditional now (Class-2's whole point). if (callee.receiver.is_none() // Plan 196.4 Stage-1b: a STATIC method (`fn Type[T].method(...)`, // called bare — `Type.method(args)`, no turbofish on `Type`) has NO // receiver VALUE to carrier-subst from (unlike an instance method — // that's `resolve_return_channel`'s job, 9594) and no explicit // type-args either (a turbofish call — `Type[T].method(args)` — is a // DIFFERENT AST shape, `Member{obj:TurboFish{..}}`, that never reaches // `f1_check_call`'s callee-resolution arms at all — see // `resolve_generic_static_return`, 13567, the Tier-1 channel for // THAT shape, D372). The only source of concrete type for the // receiver's carrier generics AND the method's own generics is the // SAME one a free fn has: the call's ARGUMENTS. Extend this arm to a // static receiver too — the `unify_type` walk below is receiver- // agnostic (`callee.params`/`args` line up 1:1 for a static call // exactly like a free fn, no implicit receiver param) and the // materialize-only-when-FULLY-resolved gate two lines down already // guards a `-> Self`-adjacent bounded-generic residual (e.g. // `ArrayGen[G Generator[T], T].default(elem G) -> ArrayGen[G, T]` — // `T` is not itself a param, only reachable through `G`'s bound; // structural `unify_type` leaves it unbound → residual → skip, // unchanged legacy fallback, exactly the erased-body contract below). || matches!( callee.receiver.as_ref().map(|r| &r.kind), Some(ReceiverKind::Static) )) && !callee_gs_inner.is_empty() { // [M-172.1-U4-freefn-generic-return] (Plan 177 Ф.2c): the callee's // return MENTIONS its own type-params — e.g. `sequence(items // []Result[T,E]) -> Result[[]T,E]`, `partition(...) -> ([]T,[]E)`. // The guard above DISCARDS such a return (§1 violation: the checker // knows the callee + args, yet leaves the concrete return un- // materialized → codegen Channel-2 miss → the divergent legacy // `value_aware_subst_to_ref` subst-mirror / an `fn_ret_<name>` // hijack). Instead INFER the type-args from the ARGUMENTS (structural // `unify_type` of each param TypeRef against the arg's inferred type — // the SAME unifier `build_recv_subst` uses for receivers) and // SUBSTITUTE, so the CONCRETE return (`Result[[]int,str]` / // `([]int,[]str)`) is materialized and codegen lowers it through the // SINGLE `resolved_type_to_c` (D315), not a subst-mirror. For a FREE // fn OR a STATIC method (Plan 196.4 Stage-1b — receiver-VALUE carrier // generics are the instance-method channel's job, 7202; a static // receiver has no value, only its declared carrier names, which this // arm now solves from args the same as a method-own generic) and only // when FULLY resolved for THIS caller (no residual type-param after // subst): an erased generic body caller (unbound `T` in scope) leaves // a residual → skip → legacy (mirrors the concrete-caller invariant // of the method-channel gs-gate). let mut subst: HashMap<String, TypeRef> = HashMap::new(); for (p, a) in callee.params.iter().zip(args.iter()) { if p.is_variadic { break; } if let Some(a_ty) = self.infer_expr_type(a.expr(), scope) { let _ = crate::const_fn_trampoline::unify_type( &p.ty, &a_ty, &callee_gs_inner, &mut subst, ); } } // [M-196.5 producers-widen, Class-1] closure-return-bound: a // fn-typed param whose arg is a closure LITERAL (`f fn() -> T`, // arg `|| 7`) never got a binding above — `infer_expr_type` has no // ClosureLight/ClosureFull arm (checker doesn't type closure bodies // generally), so `a_ty` was always `None` for it. Close any callee // generic still unbound after the structural pass by peeking the // closure literal's body return type instead — mirrors the legacy // codegen `resolve_mono_type_args` Source 2b (`emit_c.rs` ~19025, // C-string based) ported to the checker/TypeRef level (see // `closure_arg_return_peek`'s doc comment for the "no invention" // contract). `fp_seeded` substitutes what's ALREADY bound from the // pass above so a multi-param closure whose OTHER params depend on // an already-resolved generic still seeds correctly. for (p, a) in callee.params.iter().zip(args.iter()) { if p.is_variadic { break; } let TypeRef::Func { params: fpp, return_type: Some(fr), .. } = &p.ty else { continue; }; if !typeref_mentions_any(fr, &callee_gs_inner) { continue; } let fp_seeded: Vec<TypeRef> = fpp .iter() .map(|t| crate::const_fn_trampoline::subst_type_ref_pub(t, &subst)) .collect(); if let Some(body_ty) = self.closure_arg_return_peek( &fp_seeded, a.expr(), scope, &callee_gs_inner, ) { let _ = crate::const_fn_trampoline::unify_type( fr, &body_ty, &callee_gs_inner, &mut subst, ); } } // [M-196.5 producer-A width fix, D310] An explicit, COMPLETE // turbofish (`func[T1,...](...)`, arity == callee.generics) // is ground truth — overlay it on top of the unify-derived // `subst` per-generic. This is what fixes the fixed-width // loss: `d310_twice[i64](10)` now carries `T=i64` (the // explicit annotation), not the literal-arg's collapsed // default `nova_int`. Partial/absent turbofish (inference // call, e.g. `d310_twice(a)`) leaves the unify result // untouched — unchanged legacy behavior for that path. if let Some(tas) = explicit_type_args { if tas.len() == callee.generics.len() { for (g, ta) in callee.generics.iter().zip(tas.iter()) { subst.insert(g.name.clone(), ta.clone()); } } } // [196.5 closure-lowering fix] expose the resolved type-args to the // literal-materialization arg-loop below (see `arg_subst` decl above). arg_subst = Some(subst.clone()); if !subst.is_empty() { // Plan 196.5 producers-widen (Class-2): only attempt the // return-channel materialize when the DECLARED return actually // mentions a callee generic — `ret_is_concrete` means the // sibling branch above (formerly the `if` half of this `else // if`) already wrote `resolved_types_buf` for this call_id from // the UNMODIFIED declared return; re-deriving it here from the // unify-subst would be redundant at best and a DIVERGENT second // write at worst (this arm's `subst` is arg-derived, not the // Self-substitution the concrete branch performs). if !ret_is_concrete { let concrete = crate::const_fn_trampoline::subst_type_ref_pub(ret_ty, &subst); if !typeref_mentions_any(&concrete, &callee_gs_inner) && !typeref_mentions_any(&concrete, gs) { let rt = ResolvedType::from_type_ref(&concrete); self.resolved_types_buf.borrow_mut().insert(call_id, rt); } } // [M-196.5-node-substs] Producer A: `subst` above is the SAME map // just applied to the return — capture the VALUES themselves, // ordered by DECLARATION order (mono-mangling needs positional // order): receiver-carrier generics FIRST, then the fn's OWN // generics. // // [M-196-ch-static-ctor-node-substs] (Plan 196 Zone CH, producer): // a STATIC generic ctor (`fn Box[T].make(x T) -> Result[Box[T],E]`, // called bare `Box.make(v)`) carries its type-params on the // RECEIVER, so `callee.generics` is EMPTY — the old // `callee.generics`-only ordering skipped `node_substs` for the // WHOLE static-ctor class, even though `subst` (arg-unified above) // fully bound `T` and the `resolved_types` return-channel write two // blocks up ALREADY materialized the concrete `Result[Box[int],E]`. // Widen the positional-subst channel to match the return channel by // ordering over `receiver.generics ++ callee.generics` // (`callee_gs_inner` already contains BOTH — this only fixes the // ORDER + the completeness denominator). Additive + byte-parity-safe: // a free fn (no receiver) has `gen_names == callee.generics` → // byte-identical; the codegen consumer (`rt_slots_from_args`, keyed // BY NAME) treats a fresh entry as a channel HIT guarded by the // per-key byte-identity check (`subst_map_adopt_rt`) with a legacy // MISS-fallback, and `shadow_check_node_substs` (debug) asserts the // channel lowers to the exact legacy C-string. let mut gen_names: Vec<String> = Vec::new(); if let Some(recv) = &callee.receiver { for tr in &recv.generics { if let TypeRef::Named { path, .. } = tr { if path.len() == 1 && !gen_names.contains(&path[0]) { gen_names.push(path[0].clone()); } } } } for g in &callee.generics { if !gen_names.contains(&g.name) { gen_names.push(g.name.clone()); } } // Same materialize-only-when-fully-resolved gate as the // return-channel write above (per-param, via // `typeref_mentions_any`), plus a whole-map completeness gate // (`ordered.len() == gen_names.len()`): a residual (erased-body // caller leaving some param unbound) means the channel stays // UNWRITTEN for this call-site — same contract as `resolved_types_buf`. let ordered: Vec<(String, ResolvedType)> = gen_names .iter() .filter_map(|name| { subst.get(name).and_then(|tr| { if !typeref_mentions_any(tr, &callee_gs_inner) && !typeref_mentions_any(tr, gs) { Some((name.clone(), ResolvedType::from_type_ref(tr))) } else { None } }) }) .collect(); if !gen_names.is_empty() && ordered.len() == gen_names.len() { // [M-196.5-node-substs] Stage-A coverage trace (§9 acceptance: // "канал непуст на generic-формах"). Opt-in, mirrors NOVA_A1PP_TRACE. if std::env::var_os("NOVA_NODE_SUBSTS_TRACE").is_some() { eprintln!( "[NODE_SUBSTS] producer=A call_id={:?} callee={} n={}", call_id, callee.name, ordered.len() ); } self.node_substs.borrow_mut().insert(call_id, ordered); } } } } } let Ok(bindings) = crate::argbind::bind_call_args(&callee.params, args) else { // BindError уже репортит BoundCtx::check_call_argbind. return; }; let callee_gs = fn_generic_scope(callee); for (pi, binding) in bindings.iter().enumerate() { let ai = match binding { crate::argbind::ArgBinding::Positional(i) | crate::argbind::ArgBinding::Named(i) => *i, // Variadic собирается в []T, Default — нет arg-выражения. _ => continue, }; let Some(param) = callee.params.get(pi) else { continue; }; if param.is_variadic { continue; } let Some(arg) = args.get(ai) else { continue; }; // Plan 172.1 [literal-coercion channel] (§0/§1): DEFINITE site — the call // resolved to THIS callee (`resolved_callees`), so a context-typed literal arg // (`f(0x80)` with `f(x uint)`) carries `uint`, not the collapsed `nova_int`. // // [196.5 closure-lowering fix] A param type that mentions callee generics // (`f fn() -> T`) is NOT the truth for THIS call — materializing against it // stamped an ERASED closure annotation (`Func{ret: Named("T")}` passes the // `ConcreteNamedNoArgs` gate) into `resolved_types`, and codegen's // channel-first `closure_channel_ret_c` emitted the lambda body with a // `Nova_T*` C return. Substitute the call's resolved type-args (producer-A // unify, `arg_subst` above) first; a residual/absent subst → SKIP the // materialization entirely (absence is honest — consumers fall back to the // legacy body-walk; an erased stamp is a lie). Non-generic-mentioning // params keep the raw declared type — byte-identical legacy behavior. if typeref_mentions_any(¶m.ty, &callee_gs) { if let Some(s) = &arg_subst { let exp_ty = crate::const_fn_trampoline::subst_type_ref_pub(¶m.ty, s); if !typeref_mentions_any(&exp_ty, &callee_gs) { self.materialize_literal_coercion(arg.expr(), &exp_ty); } } } else { self.materialize_literal_coercion(arg.expr(), ¶m.ty); } // №375 (window p375-ptr2): `&ro_x` passed where a `*mut T` param // is declared — same source-check as the let-annotation site. self.check_addrof_mut_from_ro_source(arg.expr(), ¶m.ty, errors); match self.assignable(arg.expr(), ¶m.ty, gs, &callee_gs, scope) { Compat::Bad { found } => { errors.push( Diagnostic::new( format!( "[E7301] cannot pass `{}` as argument `{}` \ of type `{}`", found, param.name, typeref_display(¶m.ty), ), arg.expr().span, ) .with_note_at( format!("parameter `{}` declared here", param.name), param.span, ), ); } // Plan 142 (D227): литерал-аргумент вне диапазона sized-param. Compat::OutOfRange { msg } => { errors.push( Diagnostic::new( format!("[E_LIT_OUT_OF_RANGE] {msg}"), arg.expr().span, ) .with_note_at( format!("parameter `{}` declared here", param.name), param.span, ), ); } Compat::Narrowing { from, to } => { // [M-172.1-sync-extern-narrowing-migration] (2026-07-02): // narrowing-enforcement для EXTERN-callee отложен — merge // sync-сигнатур в чекер (U.1.3b) включил D54-проверку на // sized-atomic API (i32/u8-параметры), а корпус (atomics/ // sync, ~150 файлов) писался ДО enforcement'а с int-варами. // Снять gate после плановой миграции корпуса (backlog). { errors.push( Diagnostic::new( format!( "[E_IMPLICIT_NARROWING] cannot pass `{}` as argument \ `{}` of narrower type `{}` — implicit int narrowing \ loses range; use an explicit `{} as {}` cast (D54)", from, param.name, to, "<value>", to, ), arg.expr().span, ) .with_note_at( format!("parameter `{}` declared here", param.name), param.span, ), ); } } // Plan 214.1 (D429 amend, R3'): see `Compat::CoerceConflict` doc. Compat::CoerceConflict { msg } => { errors.push( Diagnostic::new(msg, arg.expr().span).with_note_at( format!("parameter `{}` declared here", param.name), param.span, ), ); } Compat::Ok | Compat::Unknown => { // [M-generic-arg-type-mismatch-silent] The Ty/TyCat lowering // folds every int width into one `TyCat::Int` and drops a // named type's generic arguments, so a generic value passed // with a different concrete primitive type-argument (e.g. // `Vec[int]`→`Vec[u32]`, or a user `Stack[int]`→`Stack[u32]`) // slips past `assignable`. That is a pointer reinterpretation — // a generic instantiation is a distinct monomorphized struct // (`Nova_Vec____nova_int*` 8-byte slots vs // `Nova_Vec____uint32_t*` 4-byte slots), so reading one as the // other misreads element width/stride and yields garbage like // `(hi<<32)|lo`. NOT Vec-specific — fires for ANY generic type whose // base name + arity match but a concrete type-argument differs. // Scalar coercion (`int`→`u32` OUTSIDE a generic, e.g. `push(int)` // into a `Vec[u32]`) is a value-level truncation and is intentionally // NOT flagged. U.5.3: the mismatch DECISION is now made on the // lossless structured `ResolvedType` (`distinct_mono`); `generic_args` // is kept only to iterate the arg pairs + display their `TypeRef`s in // the message (a thin accessor, not a parallel type engine). if let (Some((base_p, args_p)), Some(arg_tr)) = ( generic_args(¶m.ty), self.infer_expr_type(arg.expr(), scope), ) { if let Some((base_a, args_a)) = generic_args(&arg_tr) { // Same generic type (base name + arity) — compare each // type-argument; flag the first definite mismatch. if base_p == base_a && args_p.len() == args_a.len() { for (pa, aa) in args_p.iter().zip(args_a.iter()) { if distinct_mono( &self.resolved_cat_of(pa, &callee_gs), &self.resolved_cat_of(aa, gs), ) { errors.push( Diagnostic::new( format!( "[E_ARG_ELEM_TYPE_MISMATCH] cannot \ pass `{}` as argument `{}` of type \ `{}` — generic type argument \ differs (`{}` vs `{}`); a generic \ instantiation is a distinct \ monomorphized type, so this would \ reinterpret its contents", typeref_display(&arg_tr), param.name, typeref_display(¶m.ty), typeref_display(aa), typeref_display(pa), ), arg.expr().span, ) .with_note_at( format!( "parameter `{}` declared here", param.name, ), param.span, ), ); break; } } } } } } } } } /// Ф.3: проверить существование поля/метода `name` у `obj`. /// /// Консервативно: проверяется только когда тип `obj` уверенно /// резолвится в concrete record **без embed'ов** (`use`-поля /// проксируют члены — резолв слишком сложен). Метод ИЛИ поле — /// обе формы валидны (`obj.field`, `obj.method`, `obj.method()`). fn f3_check_member_ctx( &self, obj: &Expr, name: &str, span: Span, // Plan 172.1 U.4.4 (primitive-Member flip): the `ExprId` of the Member // expression itself (threaded from the `f1_expr` caller). When a CONCRETE // primitive-typed field is found below, its resolved type is written into the // checker channel (`resolved_types_buf[member_id]`) instead of being thrown // away (the validation-only walk computed `field.ty` for the privacy check but // discarded it — conventions §1/§10 anti-pattern «materialize the resolve, do // not throw it»). Codegen reads it AUTHORITATIVELY (consumer flipped in U.4.4b). member_id: crate::ast::ExprId, scope: &HashMap<String, TypeRef>, errors: &mut Vec<Diagnostic>, // 172.1.2 Шаг 2: Member — func-позиция Call (метод-вызов, не field-read). is_call_func: bool, ) { // Plan 132 Ф.1: bound method value `obj.@method` removed (D-block Plan 11). // `Type.@method` (unbound fn-pointer) is still allowed. // Heuristic identical to emit_c.rs emit_method_value_typed: a bare Ident // that starts with uppercase OR is a primitive type name = unbound (keep). // Everything else = bound (E_BOUND_METHOD_REMOVED). if name.starts_with('@') { let is_type_name = match &obj.kind { ExprKind::Ident(n) => { n.chars().next().map(|c| c.is_ascii_uppercase()).unwrap_or(false) || matches!( n.as_str(), "int" | "i8" | "i16" | "i32" | "i64" | "u8" | "u16" | "u32" | "u64" | "f32" | "f64" | "bool" | "char" | "str" ) } ExprKind::Path(parts) if parts.len() == 1 => true, _ => false, }; if !is_type_name { errors.push(Diagnostic::new( format!( "[E_BOUND_METHOD_REMOVED] bound method value `obj.{}` \ removed (Plan 132). Bound method values that capture \ receiver were removed because they conflicted with \ field/method same-name design. Migration: \ use a lambda `|| obj.{}(...)` for a captured call, \ or use unbound `Type.{}` for an fn-pointer.", name, &name[1..], // strip leading @ name, ), span, )); return; } // Unbound Type.@method — fall through, normal checks apply. return; } let Some(obj_tr) = self.infer_expr_type(obj, scope) else { return; }; // Plan 118 D216 §5: auto-deref one level для typed pointer `*T` — // permissive в type-checker (skip f3 not-found error so codegen может // attempt arrow `p->field`). Full proper auto-deref resolution // с codegen integration (emit `p->field` C consistently для both // record and primitive pointee) — Ф.4 followup work. V1: skip check // для pointer types — permissive (codegen may produce CC-FAIL if // arrow codegen path не handles *T properly yet). // Plan 118.5: pointer-like = Pointer | Mut | Unsafe (all auto-deref candidates). if matches!( obj_tr, TypeRef::Pointer(_, _) | TypeRef::Mut(_, _) | TypeRef::Uninit(_, _) ) { return; // permissive — defer to codegen + Ф.4 full integration } // Plan 172.1 U.4.4 generic-Member: capture the receiver's type-args (`recv_type_args`) // so a generic field's type can be substituted before the channel gate (see the hook // at the field-found site below). `[]` for a non-generic receiver → no substitution. let TypeRef::Named { path, generics: recv_type_args, span: obj_tr_span } = &obj_tr else { return; }; let Some(tname) = path.last() else { return; }; // Plan 152.1 Ф.4 (D249): str's bare length / codepoint-access / byte-access // accessors are retired for the lens model. Targeted error (fires BEFORE the // priv-field-read path below — `str` has a priv `len` FIELD — and before the // generic "no method" error) guiding to the lens replacement. `@byte_len()` // (O(1) byte-length shortcut) stays. EXEMPTION: a bare self-access `@len` // (obj == SelfAccess) is the legitimate backing-field read inside str's own // `@byte_len() => @len` (D117 carve-out) — only NON-self receivers (external // `s.len()` / `s.char_at(i)` / …) are diagnosed. if tname == "str" && !matches!(obj.kind, ExprKind::SelfAccess) { // NB: `get` is NOT listed — `str @get(r Range) -> Option[str]` is the // valid safe-slice accessor (slice.nv). The retired `get(int)` simply // fails arg-resolution against `get(Range)` (no `str.get(int)` sites remain). let hint = match name { "len" => Some("`byte_len()` (byte length, O(1)) or `chars().count()` (codepoint count, O(n))"), "char_len" => Some("`chars().count()` (codepoint count, O(n))"), "char_at" => Some("`for c in chars()` / `.chars().indices()` (i-th codepoint via \ target iteration — positional `chars().nth(i)` was itself \ retracted, D260-амендмент)"), "byte_at" => Some("`bytes()[i]` (i-th byte, O(1), bounds-checked)"), _ => None, }; if let Some(hint) = hint { errors.push(Diagnostic::new( format!( "[E_STR_NO_LEN] `str.{}()` was retired (D249 lens model): a `str` \ has three diverging lengths, so length / element access goes through \ a representation lens — use {}.", name, hint ), span, )); return; } } // [M-blanket-method-resolve]: a blanket method `fn[T] T @m` (receiver is // one of the fn's own type-params) applies to ANY concrete type, but lives // in `method_table` under the param key ("T"), not under `tname`. The // per-type resolution below keys on `tname` and would miss it, firing a // false [E7320] on `str`/user types (primitives slip through the // `self.types.get` early-return). Accept the blanket method here. if self.blanket_method_names.contains(name) { return; } // [M-198-f4c-1-privfile-type-not-discriminated]: file-aware lookup — // `span` is the member-access expr's own span, so `span.file_id` is // the USE-SITE's file; a colliding `priv(file) type` resolves to ITS // OWN file's shape instead of whichever peer-file decl won the global // `types` slot (mirrors 2d5f64e91's caller-file fn-candidate filter). // // [M-crossmodule-samename-typecheck-bleed] (221.1 №28, 2026-07-23): // prefer `obj_tr`'s OWN span (`obj_tr_span`) over the member-access // expr's span — a value whose type flows in from ANOTHER file (a // function call's return, an imported const, etc.) must resolve // `tname` against the type's OWN declaring-file scope, not the // ACCESSING file's — otherwise a same-named type declared in an // unrelated peer module of the same CU can win the name-only global // `self.types` fallback (false E7320). Safe: `types_get_for_file` // itself still falls back to the SAME global map when the given // file has no local/group/import match, so this can only ever // sharpen resolution (never regress a case that worked before). let Some(td) = self.types_get_for_file(tname, obj_tr_span.file_id) else { return; }; match &td.kind { TypeDeclKind::Record(fields) => { // embed (`use`) проксирует поля/методы вложенного типа — резолв // слишком сложен для надёжной проверки, пропускаем такой тип. if fields.iter().any(|f| f.is_embed) { // 172.1.2 (2026-07-03): САМО embed-поле (`@map` при // `use map HashMap[T,()]`) — прямое поле с declared-типом; // аннотируем (subst+mark), валидацию по-прежнему пропускаем. if member_id.is_set() && !is_call_func { if let Some(field) = fields.iter().find(|f| f.name == name) { let field_ty = self.subst_receiver_generics( &field.ty, &td.generics, recv_type_args); let tparams: std::collections::HashSet<String> = td.generics.iter().map(|g| g.name.clone()).collect(); let rt = Self::mark_type_params( ResolvedType::from_type_ref(&field_ty), &tparams); self.resolved_types_buf.borrow_mut().insert(member_id, rt); } } return; } // Plan 139.1 (lang-item str): same-name field/method resolution. // `str` has BOTH a priv field `len` AND a method `@len()`. A // member access `s.len(...)` is a METHOD call, not a priv-field // read — codegen resolves it to the method. If a method with this // name exists on the type, prefer it (return) BEFORE the priv-field // check, so `s.len()` is not mis-flagged E_PRIV_FIELD_READ. A bare // field read like `s.ptr` (no method `ptr`) still falls through to // the field block below and fires privacy correctly. This matches // the documented field/method same-name design (see E_BOUND_METHOD // heuristic above) и поведение codegen method-resolution. let has_same_name_method = self.t_provides_method(tname, name); if has_same_name_method { // Plan 162 Ф.5: extension method policy — check even for // method/field same-name early return (e.g. str.len() case). self.check_extension_method_policy(tname, name, span, errors); // 172.1.2 Шаг 2 (same-name fix): `@len` как FIELD-READ (не func // вызова) на типе с одноимённым методом (Vec: поле len + @len()) // раньше возвращался ДО материализации → .len/.cap падали в // legacy на КАЖДОЙ mono-эмиссии. Для не-call позиции // материализуем тип ПОЛЯ теми же гейтами, что основной блок // ниже (subst + primitive/concrete-named). Call-позиция — // ни в коем случае (аннотация поля на метод-вызове = ложь). if !is_call_func { if let Some(field) = fields.iter().find(|f| f.name == name) { if member_id.is_set() { let field_ty = self.subst_receiver_generics( &field.ty, &td.generics, recv_type_args); let tparams: std::collections::HashSet<String> = td.generics.iter().map(|g| g.name.clone()).collect(); let rt = Self::mark_type_params( ResolvedType::from_type_ref(&field_ty), &tparams); self.resolved_types_buf.borrow_mut().insert(member_id, rt); } // [M-173-priv-field-samename-bypass] (владелец, 2026-07-10): // a bare NON-CALL access `obj.name` (no parens) resolves to // the RAW STRUCT FIELD — the same-named method requires // explicit call syntax `obj.name()` and is a completely // different codegen path. Previously this branch returned // right after materializing the field's type, WITHOUT ever // running the priv-field gate below — so `raw.len` on // `Vec[T]` (priv field `len` + method `len()`) silently // compiled from any module and emitted a direct C struct // field read. Mirror the plain-field priv check (below) // here so the same-name-method fast path can't be used to // smuggle a priv/module-priv field read past the checker. // Plan 124 (D220) + Plan 160 (D281) Ф.2. if field.priv_field && !self.priv_field_access_allowed(tname.as_str(), &field.visible_to) { if field.priv_module_field { if !self.module_priv_access_allowed(tname.as_str(), span) { errors.push(Diagnostic::new( format!( "[E_FIELD_MODULE_PRIVATE] cannot read \ module-private field `{}.{}` from \ outside its module (a same-named method \ `{}()` exists but requires call syntax — \ bare `{}.{}` still reads the raw field). \ Type declared with bare `priv` (Plan 160 / \ D281). Hint: call `{}.{}()` instead.", tname, name, name, tname, name, tname, name, ), span, )); } } else { errors.push(Diagnostic::new( format!( "[E_PRIV_FIELD_READ] cannot read private field \ `{}.{}` outside type-method scope (a same-named \ method `{}()` exists but requires call syntax — \ bare `{}.{}` still reads the raw field). Field \ marked `priv` (Plan 124 / D220). Hint: call \ `{}.{}()` instead.", tname, name, name, tname, name, tname, name, ), span, )); } } } } return; } if let Some(field) = fields.iter().find(|f| f.name == name) { // Plan 172.1 U.4.4 (primitive-Member flip, mirror of U.4.4b Ident): // the checker found the field and knows `field.ty` (it needs it for the // privacy/E7320 checks below) — MATERIALIZE that resolve into the // channel instead of discarding it (§1/§10). Codegen reads it // AUTHORITATIVELY (consumer flipped in U.4.4b), so this is a // BEHAVIOR-CHANGE: where legacy `infer_expr_c_type` mis-fell-back to // `nova_int` for a `bool`/sized-int field it now emits the correct type. // GATE to PRIMITIVE resolved types only (same as U.4.4b): their C-type // is ALWAYS a registered builtin — no undeclared-identifier / mono / // generic-inference hazard. A GENERIC field (`field.ty = T` on a generic // record) lowers to `from_type_ref(T) = Named{T}` which the gate REJECTS // (not primitive) — that subset needs receiver type-arg substitution // (mono-hazard, gated on U.4.3(d)), out of scope for this atom. A // concrete primitive field (`count int`) is type-arg-independent → safe. if member_id.is_set() { // Plan 172.1 U.4.4 generic-Member (unblocked by U.4.3(d) generic- // receiver inference): substitute the receiver's type-args into the // field type, THEN gate on the SUBSTITUTED type. A concrete field // (`count int`) substitutes to itself → the primitive-Member behavior // is byte-identical; a generic field `v T` on a now-typed receiver // `b: GMBox[int]` materializes `b.v: int`; a generic field substituting // to a NON-primitive (`[]T` → `[]int`) is rejected by the gate → legacy // (mono-hazard avoided, same as the pre-U.4.3(d) generic-field skip). let field_ty = self.subst_receiver_generics(&field.ty, &td.generics, recv_type_args); // 172.1.2 Шаг 2b: residual-параметры → ЯВНЫЙ TypeParam; // аннотация всегда (правда о шаблоне); лоуэринг подставит // mono-subst или отдаст Err → legacy (Шаг 1). let tparams: std::collections::HashSet<String> = td.generics.iter().map(|g| g.name.clone()).collect(); let rt = Self::mark_type_params( ResolvedType::from_type_ref(&field_ty), &tparams); // Plan 172.1 U.4.5 P2 (Member widening): annotate a CONCRETE field type. // Primitive (was U.4.4) OR a NON-generic declared value-type (record / sum / // newtype / named-tuple, no type-args) — its C-type is DETERMINISTIC // (`Nova_X*` / `NovaValue_X`) with no type-param / mono-instance hazard, so // codegen lowers it via `resolved_type_to_c` without needing // `current_type_subst`. A generic-arg'd type (`Pair[int]`) or a residual // type-param is STILL rejected → legacy (mono-hazard, gated on 172.1.2). let concrete_value_named = matches!(&rt, ResolvedType::Named { name, args, .. } if args.is_empty() && self.types.get(name).map_or(false, |td| matches!(&td.kind, TypeDeclKind::Record(_) | TypeDeclKind::Sum(_) | TypeDeclKind::Newtype(_) | TypeDeclKind::NamedTuple(_)))); let _ = concrete_value_named; // gate снят Шагом 2b (TypeParam-носитель) if std::env::var_os("NOVA_MEMBER_INT_TRACE").is_some() && matches!(&rt, ResolvedType::Scalar { wide_default: true, signed: true, .. }) { eprintln!("[MEMBER-INT f3-record] id={:?} span={:?} tname={} field={} fty={:?}", member_id, span, tname, name, field_ty); } self.resolved_types_buf.borrow_mut().insert(member_id, rt); } // Plan 124 (D220) + 124.6 (D225): priv field READ access check. // Allowed: own type-method, или fn с #test_access(tname), // или current_recv ∈ field.visible_to (friend). if field.priv_field && !self.priv_field_access_allowed(tname.as_str(), &field.visible_to) { // Plan 160 (D281) Ф.2: distinguish module-private from // type-private. priv_module_field=true → Module default; // allow if same module, otherwise E_FIELD_MODULE_PRIVATE. if field.priv_module_field { if !self.module_priv_access_allowed(tname.as_str(), span) { errors.push(Diagnostic::new( format!( "[E_FIELD_MODULE_PRIVATE] cannot read \ module-private field `{}.{}` from \ outside its module. Type declared with \ bare `priv` (Plan 160 / D281). \ Hint: add a public accessor method on \ `{}`, or move the caller into the same \ module.", tname, name, tname, ), span, )); } } else { errors.push(Diagnostic::new( format!( "[E_PRIV_FIELD_READ] cannot read private field \ `{}.{}` outside type-method scope. Field marked \ `priv` (Plan 124 / D220). Hint: add public \ getter method on `{}`, or move accessing code \ into a method of `{}`, or use `#test_access({})` \ на test fn (escape hatch — D224).", tname, name, tname, tname, tname, ), span, )); } } return; } // Метод? Имена операторных методов могут храниться с ведущим `@`. // Plan 186 [bug-1 audit-197 fix]: `t_provides_method` is a bare // `tname`-keyed lookup — for a slice receiver reconstructed from // the CHANNEL (`obj_tr` round-tripped through `ResolvedType`, // which canonicalizes `[]T` → `Named{Vec,[T]}`, D239/Stage-1a // comment above :14074-14087), `tname` is the bare "Vec" alias // with the element carried separately in `recv_type_args` — a // CONCRETE-slice-receiver method (`fn []str @join(sep) -> str`, // `std/text.nv` — registered under the LITERAL "[]str" key, same // class as the `[]<elem>` `slice_key` fallback already used by // `resolve_instance_method_return_arity`, 13816-13849) is invisible // to a plain "Vec" lookup. Retry with the reconstructed literal // "[]<elem>" key when the receiver carries exactly one concrete // element type-arg. let slice_elem_has_method = if tname == "Vec" { match recv_type_args.as_slice() { [TypeRef::Named { path: ep, generics: eg, .. }] if ep.len() == 1 && eg.is_empty() => { self.t_provides_method(&format!("[]{}", ep[0]), name) } _ => false, } } else { false }; // [M-vec-ext-method-untyped-let-breaks-chain-dispatch]: a THIRD slice // method-registration convention, sibling to the two above — a // PREFIX-GENERIC slice-extension method (`fn[T] []T @method(...)`, the // pervasive std/user idiom, e.g. `vec_seq.nv`'s own `@map[U]`/`@filter`/ // `@fold[Acc]`) registers in `method_table` under the LITERAL key // "[]T" — the DECLARATION's own generic param name, not a concrete // element name — so neither the bare `t_provides_method(tname, name)` // ("Vec" key) NOR `slice_elem_has_method` ("[]<concrete-elem>" key, // built for `fn []str @join`-style CONCRETE receivers) above finds it. // A receiver that reaches THIS branch as `Named{"Vec", [elem]}` is // reconstructed from the CHANNEL — an unannotated `ro x = v.map(f)` // binding materializes its type via `ResolvedType::from_type_ref`'s // D239 canonicalization (`[]T` → `Named{Vec,[T]}`, see `f1_stmt`'s // `chain_ty`) — a genuine `TypeRef::Array` receiver (e.g. a DIRECT // annotation `ro x []int = ...`) never reaches here at all (bails at // the `TypeRef::Named` destructure above), so this gap fired ONLY for // the channel-sourced shape: any subsequent extension-method call on // such an unannotated binding (`x.filter(...)`) fell straight through // to the false [E7320] below. Reuse `prefix_generic_method_exists` // (the existing tested existence-check for exactly this receiver // class, Plan 177 Ф.3 — 0 false-positives across a 707K-call corpus) // by reconstructing the `TypeRef::Array` shape it expects from the // single concrete element carried in `recv_type_args`. let prefix_generic_slice_method = tname == "Vec" && recv_type_args.len() == 1 && self.prefix_generic_method_exists( &TypeRef::Array(Box::new(recv_type_args[0].clone()), span), name, ); let has_method = self.t_provides_method(tname, name) || slice_elem_has_method || prefix_generic_slice_method; if has_method { // Plan 162 Ф.5: extension method policy. self.check_extension_method_policy(tname, name, span, errors); return; } // `into` / `try_into` синтексируются компилятором из `From` / // `TryFrom` (D73/D77) — их нет в method_table, но они валидны // для любого типа-источника конверсии. if matches!(name, "into" | "try_into") { return; } // Plan 91.8a.2 [M-91.8a.2-default-body-general] 2026-05-29: // generalized protocol default-body satisfiability. Replaces prior // hardcoded equals/fmt MVP. Walks ALL protocols, finds methods named // `name` with `default_body`, and for each checks whether the body's // top-level method/free-fn calls resolve for T (i.e. T provides every // method/overload referenced by the body). If at least one protocol // is satisfied → accept the bare call; codegen general synthesizer // emits the concrete Nova_<T>_method_<name> on first use. if self.protocol_method_satisfiable_for(tname, name) { return; } // №254 (221.1): `name` exists as SOME blanket declared under this // name (that's why we got this far without an earlier `has_method` // accept) but the receiver fails its `Next`/`Iter` bound — surface // the specific bound-failure instead of falling to generic E7320. if let Some(msg) = self.bound_violation_message( &TypeRef::Named { path: vec![tname.to_string()], generics: vec![], span }, name, ) { errors.push(Diagnostic::new(msg, span)); return; } // Plan 114.4.1 (D200): assoc const detection — если `name` matches // одну из assoc consts типа, hint user про namespace access. let is_assoc_const = self.types.get(tname) .map(|td| td.assoc_consts.iter().any(|ac| ac.name == name)) .unwrap_or(false); if is_assoc_const { errors.push(Diagnostic::new( format!( "[E_CONST_INSTANCE_ACCESS] cannot access associated \ constant `{}.{}` через instance — assoc constants \ live на type-level (zero storage в instance). \ Use `{}.{}` namespace access instead (Plan 114.4.1 D200).", tname, name, tname, name, ), span, )); return; } let avail: Vec<&str> = fields.iter().map(|f| f.name.as_str()).collect(); let mut diag = Diagnostic::new( format!( "[E7320] no field or method `{}` on type `{}`", name, tname, ), span, ); if !avail.is_empty() { diag = diag.with_note(format!( "`{}` has field{}: {}", tname, if avail.len() == 1 { "" } else { "s" }, avail.join(", "), )); } errors.push(diag); } TypeDeclKind::NamedTuple(fields) => { // Plan 120 (D215): named tuple — named access only (Q120-positional-access-on-named Option B) if name.chars().all(|c| c.is_ascii_digit()) { errors.push(Diagnostic::new( format!( "[E_TUPLE_POSITIONAL_ACCESS_ON_NAMED] \ named tuple `{}` does not support positional field access `.{}`; \ use named access `.field_name` instead", tname, name ), span, )); return; } if let Some(field) = fields.iter().find(|f| f.name == name) { // Plan 172.1 RANK 2 (named-priority de-collapse): the Record arm (above) // materializes a substituted concrete field type into the channel, but this // NamedTuple arm discarded it (§1 validation-discard) → a GENERIC NamedTuple // field (`Pair[u8].a`) fell to the codegen `nova_int` fallback, because the // legacy generic-template substitution path handles only `Record`, not // NamedTuple. Mirror the Record arm's U.4.4/U.4.5 block so the authoritative // consumer emits the correct narrow C type (u8→nova_byte) independent of that // legacy gap. Same primitive_gate/concrete_value_named gate → only DETERMINISTIC // C-types channeled (no mono/generic-inference hazard); a NON-generic NamedTuple // primitive field stays byte-identical (legacy already resolves it via the // self-describing `NovaTuple_X` schema), `int`/`uint` fields stay nova_int/nova_uint. if member_id.is_set() { let field_ty = self.subst_receiver_generics(&field.ty, &td.generics, recv_type_args); // 172.1.2 Шаг 2b: residual-параметры → ЯВНЫЙ TypeParam; // аннотация всегда (правда о шаблоне); лоуэринг подставит // mono-subst или отдаст Err → legacy (Шаг 1). let tparams: std::collections::HashSet<String> = td.generics.iter().map(|g| g.name.clone()).collect(); let rt = Self::mark_type_params( ResolvedType::from_type_ref(&field_ty), &tparams); let concrete_value_named = matches!(&rt, ResolvedType::Named { name, args, .. } if args.is_empty() && self.types.get(name).map_or(false, |td| matches!(&td.kind, TypeDeclKind::Record(_) | TypeDeclKind::Sum(_) | TypeDeclKind::Newtype(_) | TypeDeclKind::NamedTuple(_)))); let _ = concrete_value_named; // gate снят Шагом 2b (TypeParam-носитель) if std::env::var_os("NOVA_MEMBER_INT_TRACE").is_some() && matches!(&rt, ResolvedType::Scalar { wide_default: true, signed: true, .. }) { eprintln!("[MEMBER-INT f3-ntuple] id={:?} span={:?} tname={} field={} fty={:?}", member_id, span, tname, name, field_ty); } self.resolved_types_buf.borrow_mut().insert(member_id, rt); } // Plan 124.4 (D222) + 124.6 (D225): priv field READ check // для named tuple — uniform allowance с Record. if field.priv_field && !self.priv_field_access_allowed(tname.as_str(), &field.visible_to) { // Plan 160 (D281) Ф.2: module-private vs type-private. if field.priv_module_field { if !self.module_priv_access_allowed(tname.as_str(), span) { errors.push(Diagnostic::new( format!( "[E_FIELD_MODULE_PRIVATE] cannot read \ module-private field `{}.{}` from \ outside its module. Named-tuple type \ declared with bare `priv` (Plan 160 / \ D281). Hint: add a public accessor \ method on `{}`.", tname, name, tname, ), span, )); } } else { errors.push(Diagnostic::new( format!( "[E_PRIV_FIELD_READ] cannot read private field \ `{}.{}` outside type-method scope. Named-tuple \ field marked `priv` (Plan 124 / D220 / D222). \ Hint: add public getter method on `{}`, move \ accessing code into a method of `{}`, или use \ `#test_access({})` на test fn (D225).", tname, name, tname, tname, tname, ), span, )); } } return; } let has_method = self.t_provides_method(tname, name); if has_method { // Plan 162 Ф.5: extension method policy. self.check_extension_method_policy(tname, name, span, errors); return; } if matches!(name, "into" | "try_into") { return; } if self.protocol_method_satisfiable_for(tname, name) { return; } let avail: Vec<&str> = fields.iter().map(|f| f.name.as_str()).collect(); let mut diag = Diagnostic::new( format!( "[E7320] no field or method `{}` on named tuple `{}`", name, tname, ), span, ); if !avail.is_empty() { diag = diag.with_note(format!( "`{}` has field{}: {}", tname, if avail.len() == 1 { "" } else { "s" }, avail.join(", "), )); } errors.push(diag); } TypeDeclKind::Newtype(TypeRef::Tuple(_, _)) => { // Positional tuple: named field access (`.x`) is invalid if !name.chars().all(|c| c.is_ascii_digit()) { let has_method = self.t_provides_method(tname, name); if has_method { // Plan 162 Ф.5: extension method policy. self.check_extension_method_policy(tname, name, span, errors); return; } if matches!(name, "into" | "try_into") { return; } errors.push(Diagnostic::new( format!( "[E_TUPLE_NAMED_ACCESS_ON_POSITIONAL] \ positional tuple `{}` does not have named fields; \ use positional access `.0`, `.1`, … instead", tname ), span, )); } } _ => {} // Sum, Effect, Protocol, Alias, Opaque, etc. — conservative, skip } } /// Plan 120 (D215): validate tuple construction calls. /// Checks direct construction `TypeName(args...)` where TypeName is a /// known named or positional tuple type. Conservative: skips if callee is /// not a plain Ident or if the name is shadowed by a local variable. fn f5_check_tuple_construct( &self, func: &Expr, args: &[CallArg], span: Span, scope: &HashMap<String, TypeRef>, errors: &mut Vec<Diagnostic>, ) { let ExprKind::Ident(name) = &func.kind else { return; }; if scope.contains_key(name.as_str()) { return; } let Some(td) = self.types.get(name.as_str()) else { return; }; match &td.kind { TypeDeclKind::NamedTuple(fields) => { // Plan 124.4 (D222) + 124.6 (D225): if any priv field exists, // for each named-arg targeting priv field check access allowance. let has_priv = fields.iter().any(|f| f.priv_field); let base_allowed = self.priv_access_allowed_base(name.as_str()); for arg in args { if let CallArg::Named { name: field_name, value: arg_value } = arg { let field_decl = fields.iter().find(|f| &f.name == field_name); if field_decl.is_none() { let avail: Vec<&str> = fields.iter().map(|f| f.name.as_str()).collect(); let mut diag = Diagnostic::new( format!( "[E_TUPLE_UNKNOWN_FIELD] named tuple `{}` has no field `{}`", name, field_name, ), span, ); if !avail.is_empty() { diag = diag.with_note(format!( "`{}` has field{}: {}", name, if avail.len() == 1 { "" } else { "s" }, avail.join(", "), )); } errors.push(diag); } else if let Some(fd) = field_decl { // №145 (D102, `03-syntax.md:5327`): a REQUIRED // param — one WITHOUT a default — binds // POSITIONALLY only; keyword form is reserved // for OPTIONAL (defaulted) params. This mirrors // `check_keyword_only`'s free-fn/static-method // rule (Plan 50) — the named-tuple constructor // call (`TypeName(...)`) never routed through // that check at all (it resolves via // `self.sig.fn_decls`/`method_table`, and a // type name is neither), so this exact class of // violation compiled silently until now // (measured: `Complex(re: 0.0, im: 1.0)` for a // no-defaults `Complex(re f64, im f64)` passed // clean). D215/D222's own doc examples show a // named-arg construction call for a type with // NO defaults (`Vec3(x: 1.0, y: 2.0, z: 3.0)`) // — those are spec bugs, not counter-evidence // (owner ruling 2026-07-27: canon is positional // `Complex(0.0, 1.0)`; the doc examples need a // D-amendment, tracked separately, NOT fixed by // loosening this rule). if fd.default.is_none() { errors.push(Diagnostic::new( format!( "[E_TUPLE_NAMED_ARG_NO_DEFAULT] named tuple \ `{ty}`'s field `{f}` has no default — it is a \ REQUIRED param and must be passed positionally, \ not by name (D102: keyword args are reserved \ for optional/defaulted params). Hint: pass \ `{f}`'s value positionally instead of `{f}: …`.", ty = name, f = field_name, ), arg_value.span, )); } } if has_priv && !base_allowed { if let Some(fd) = fields.iter().find(|f| &f.name == field_name) { if fd.priv_field && !self.priv_field_access_allowed(name.as_str(), &fd.visible_to) { // Plan 160 (D281) Ф.2: module-private init. if fd.priv_module_field { if !self.module_priv_access_allowed(name.as_str(), arg_value.span) { errors.push(Diagnostic::new( format!( "[E_FIELD_MODULE_PRIVATE] cannot initialize \ module-private field `{}.{}` via named-tuple \ constructor from outside its module. Type \ declared with bare `priv` (Plan 160 / D281). \ Hint: use factory method `{}.new(...)`.", name, field_name, name, ), arg_value.span, )); } } else { errors.push(Diagnostic::new( format!( "[E_PRIV_FIELD_INIT] cannot initialize private \ field `{}.{}` via named-tuple constructor outside \ type-method scope. Field marked `priv` (Plan 124 / \ D220 / D222). Hint: use factory method like \ `{}.new(...)`, or use `#test_access({})` on test fn.", name, field_name, name, name, ), arg_value.span, )); } } } } } } // D215 amend: fields with defaults are optional at call site. let required = fields.iter().filter(|f| f.default.is_none()).count(); let total = fields.len(); if args.len() < required || args.len() > total { let range_desc = if required == total { format!("exactly {} argument{}", total, if total == 1 { "" } else { "s" }) } else { format!("{}-{} arguments", required, total) }; errors.push(Diagnostic::new( format!( "[E_TUPLE_CONSTRUCT_ARITY_MISMATCH] named tuple `{}` expects \ {} but {} {} provided", name, range_desc, args.len(), if args.len() == 1 { "was" } else { "were" }, ), span, )); } } TypeDeclKind::Newtype(TypeRef::Tuple(elem_types, _)) => { if args.iter().any(|a| matches!(a, CallArg::Named { .. })) { errors.push(Diagnostic::new( format!( "[E_TUPLE_CONSTRUCT_NAMED_ON_POSITIONAL] \ positional tuple `{}` does not accept named arguments; \ pass values by position instead", name, ), span, )); } if args.len() != elem_types.len() { errors.push(Diagnostic::new( format!( "[E_TUPLE_CONSTRUCT_ARITY_MISMATCH] positional tuple `{}` expects \ {} argument{} but {} {} provided", name, elem_types.len(), if elem_types.len() == 1 { "" } else { "s" }, args.len(), if args.len() == 1 { "was" } else { "were" }, ), span, )); } } _ => {} } } /// Ф.4: имя типа в value-позиции (`let c = Foo`, `Foo + 1`) → E7330. /// /// Флагится только bare `Ident`, разрешающийся в **непустой** /// Plan 91.8a.2 [M-91.8a.2-default-body-general] 2026-05-29. /// /// Generalized check: does type `tname` satisfy SOME protocol's method /// named `method_name` through its `default_body`? Walks ALL protocols /// in `self.types`, finds methods of that name with default_body, and /// for each tries to verify body's referenced calls resolve for T. /// /// Implementation: a small AST visitor (`default_body_calls_satisfy_for`) /// recursively walks the body and checks each `obj.method(...)` / /// `Type.method(...)` call that depends on Self or @ — verifies T /// provides a matching method or overload. /// /// Returns true if at least one protocol satisfies. False if no protocol /// has a matching default body OR every candidate has unsatisfiable /// dependencies. /// Plan 91.9 (D186): verify that type T satisfies every protocol listed /// в its `#impl(P1 + P2 + ...)` annotation. For each P: /// 1. P must be a known protocol (else E_UNKNOWN_PROTOCOL). /// 2. T must provide every required method of P: /// - Either explicit `fn T @method(...)` declaration (in method_table), OR /// - P's method has a `default_body` whose calls resolve for T /// (`default_body_calls_satisfy_for` walker — same checker used /// для bare-call satisfiability). /// Missing methods → E_IMPL_MISSING_METHODS со списком и hint'ом /// (как реализовать). /// Plan 126 (D230) Ф.4: bridge TypeCheckCtx → auto_derive::DeriveQuery. /// Allows synthesize_method to query type registry and method coverage /// without coupling auto_derive module к full type-checker structure. fn as_derive_query(&'a self) -> AutoDeriveQueryBridge<'a> { AutoDeriveQueryBridge { ctx: self } } /// Plan 137 (D237): protocols renamed; emit helpful E_PROTOCOL_RENAMED. const RENAMED_PROTOCOLS: &'static [(&'static str, &'static str)] = &[ ("Hashable", "Hash"), ("Equatable", "Equal"), ("Comparable", "Compare"), ("Cloneable", "Clone"), ("Printable", "Display"), ("DebugPrintable", "Debug"), ]; /// Plan 161 Ф.2 (D355 §4): ≤1-impl invariant for `Next[T]`-shaped /// protocols (any protocol with exactly ONE generic param and exactly /// ONE required method — the shape blanket-dispatch, D355, relies on; /// `Next[T]` is the only stdlib instance today, but the check is not /// hardcoded to that name). Detected STRUCTURALLY (independent of /// `#impl(...)` — that annotation is opt-in per D186/D268, and its /// absence is never itself an error, see `verify_method_impl_protocols` /// doc-comment above): if a type declares 2+ overloads of the /// protocol's method whose signatures instantiate the protocol's /// generic param to DIFFERENT concrete types, the type would /// simultaneously implement `Proto[A]` and `Proto[B]` (A ≠ B) — an /// irreconcilable double-impl (blanket dispatch could never pick a /// single T) → `E_DUPLICATE_PROTOCOL_IMPL`. fn check_duplicate_protocol_impl(&self, errors: &mut Vec<Diagnostic>) { let candidate_protocols: Vec<(&str, &str, &crate::ast::EffectMethod)> = self .types .values() .filter_map(|td| { if let TypeDeclKind::Protocol { methods, .. } = &td.kind { if td.generics.len() == 1 && methods.len() == 1 { return Some((td.name.as_str(), td.generics[0].name.as_str(), &methods[0])); } } None }) .collect(); if candidate_protocols.is_empty() { return; } for (type_name, methods_by_name) in self.sig.method_table.iter() { for (proto_name, generic_name, proto_method) in &candidate_protocols { if type_name == proto_name { continue; } let Some(overloads) = methods_by_name.get(&proto_method.name) else { continue; }; if overloads.len() < 2 { continue; } // Infer, per overload, the concrete binding of the protocol's // generic param. `None` = this overload doesn't structurally // match the protocol shape at some FIXED (non-generic) // position — silently not a candidate impl (e.g. an // unrelated same-named method with a different signature). let mut distinct: Vec<(String, &FnDecl)> = Vec::new(); for fd in overloads { let Some(binding) = infer_protocol_generic_binding(fd, proto_method, generic_name, type_name) else { continue; }; if !distinct.iter().any(|(b, _)| *b == binding) { distinct.push((binding, fd)); } } if distinct.len() >= 2 { let variants = distinct .iter() .map(|(b, _)| format!("`{}[{}]`", proto_name, b)) .collect::<Vec<_>>() .join(", "); // Point at the LAST conflicting declaration (the one that // actually introduces the conflict; the first stands), // matching the convention of the neighboring dup-checks. let span = distinct.last().unwrap().1.span; errors.push(Diagnostic::new( format!( "[E_DUPLICATE_PROTOCOL_IMPL] тип `{}` реализует протокол `{}` \ сразу для нескольких разных типовых аргументов: {} — метод \ `@{}` объявлен с разными конкретными T. Тип не может \ реализовывать `{}[T]` для двух разных T одновременно (D355 §4). \ Оставь ровно одну реализацию `@{}` (для второго варианта — \ отдельный newtype).", type_name, proto_name, variants, proto_method.name, proto_name, proto_method.name, ), span, )); } } } } /// Plan 161 Ф.2 (D355 §5 / D285 §5b): two blanket methods /// (`fn[I Proto[T]] I @name`) declared for the SAME protocol with the /// SAME method name conflict — dispatch (D285 §3) scans for "a" blanket /// candidate by name and cannot tell which of two applies once a /// receiver type satisfies the shared bound → `E_BLANKET_CONFLICT`. /// A blanket decl is detected structurally (mirrors /// `blanket_method_names` detection in `TypeCheckCtx::build` above): /// receiver present AND receiver's type-name IS one of the fn's own /// generic params, that param carrying a protocol bound. Grouping key is /// `(protocol base name, method name)` — NOT the literal bound-typevar /// spelling (`fn[I Next[T]] I @m` and `fn[J Next[T]] J @m` are the SAME /// conflict even though "I" ≠ "J"; the generic key-based duplicate- /// signature check elsewhere in this file keys on the literal receiver /// name and misses exactly this case). fn check_blanket_conflict(&self, module: &Module, errors: &mut Vec<Diagnostic>) { let mut seen: HashMap<(String, String), &FnDecl> = HashMap::new(); for item in &module.items { let Item::Fn(fd) = item else { continue; }; let Some(recv) = &fd.receiver else { continue; }; let Some(gp) = fd.generics.iter().find(|g| g.name == recv.type_name) else { continue; }; let Some(bound) = gp.first_bound() else { continue; }; let Some(proto_base) = Self::typeref_named_base(bound) else { continue; }; let key = (proto_base.to_string(), fd.name.clone()); match seen.get(&key) { Some(prev) if prev.span != fd.span => { errors.push(Diagnostic::new( format!( "[E_BLANKET_CONFLICT] два blanket-метода `@{}` объявлены для \ одного протокола `{}` — конфликт (D355 §5): диспетч `x.{}()` \ для любого типа, реализующего `{}[T]`, не может однозначно \ выбрать между двумя blanket-реализациями. Переименуй один из \ методов или объедини реализации в одну.", fd.name, proto_base, fd.name, proto_base, ), fd.span, )); } Some(_) => {} None => { seen.insert(key, fd); } } } } fn verify_impl_protocols(&self, td: &TypeDecl, errors: &mut Vec<Diagnostic>) { for proto_name in &td.impl_protocols { // Plan 164 Ф.1: proto_name may be "Next[T]" — extract bare name for lookups. let proto_base = impl_spec_base_name(proto_name); // Plan 137 (D237): check for renamed protocols first, emit helpful message. if let Some((_, new_name)) = Self::RENAMED_PROTOCOLS.iter() .find(|(old, _)| *old == proto_base) { errors.push(Diagnostic::new( format!( "[E_PROTOCOL_RENAMED] protocol `{}` was renamed to `{}` (D237). \ Use `#impl({})` instead.", proto_name, new_name, new_name, ), td.span, )); continue; } let proto_decl = match self.types.get(proto_base) { Some(td) => td, None => { // Plan 162.1 Step 3: suppress E_UNKNOWN_PROTOCOL when the // protocol is known via the cross-module sig_table. In the // lazy-resolution scenario the protocol may be from a // transitively-imported module not yet inline-merged into // module.items. is_known_type checks both self.types (local) // and self.sig_table (cross-module). if self.is_known_type(proto_base) { // Known via sig_table — skip full verification for now. // (Full check would require fetching the protocol decl // from the sig_table, which is a Step 4 / lazy-body task.) continue; } errors.push(Diagnostic::new( format!( "[E_UNKNOWN_PROTOCOL] type `{}` has `#impl({})` but \ `{}` is not a known type. Did you forget to import \ it, or misspell the protocol name?", td.name, proto_name, proto_name, ), td.span, )); continue; } }; let proto_methods = match &proto_decl.kind { TypeDeclKind::Protocol { methods, .. } => methods, _ => { errors.push(Diagnostic::new( format!( "[E_IMPL_NOT_PROTOCOL] type `{}` has `#impl({})` but \ `{}` is not a protocol — it's a different kind of type. \ `#impl(...)` only accepts protocol names.", td.name, proto_name, proto_name, ), td.span, )); continue; } }; // Per-method check: T provides explicit method OR synthesizable from default body. // Plan 91.9 also enforces signature match для explicit methods — // E_IMPL_WRONG_SIGNATURE если T provides method с wrong arity / // param types / return type vs protocol declaration. let mut missing: Vec<String> = Vec::new(); let mut wrong_sig: Vec<(String, String, String)> = Vec::new(); // Plan 108.4 Ф.2: receiver mutability mismatch errors. // Each entry: (method_name, error_code, proto_qualifier, fix_hint). let mut wrong_recv: Vec<(String, &'static str, String, String)> = Vec::new(); // Plan 126 (D230) Ф.4: built-in protocol auto-derive eligibility. // Если protocol auto-derive-able AND user не предоставил explicit // method AND нет default body — пытаемся synthesize. Synth-success // → method считается satisfied (не добавляется в `missing`). // Synth-error → emit E_AUTO_DERIVE_* diagnostic. // Plan 164 Ф.1: build a generic substitution map from the impl-spec // args, e.g. "Next[U]" → {"T": "U"} (proto param → impl arg). // Used in check_signature_match_with_subst below. let proto_arg_subst: Vec<(String, String)> = { let args_text = impl_spec_args_text(proto_name); if args_text.is_empty() || proto_decl.generics.is_empty() { vec![] } else { // Strip outer `[` and `]` and split by `,` at depth 0. let inner = &args_text[1..args_text.len().saturating_sub(1)]; let mut parts: Vec<String> = Vec::new(); let mut depth = 0usize; let mut cur = String::new(); for ch in inner.chars() { match ch { '[' | '(' => { depth += 1; cur.push(ch); } ']' | ')' => { depth -= 1; cur.push(ch); } ',' if depth == 0 => { parts.push(cur.trim().to_string()); cur = String::new(); } _ => { cur.push(ch); } } } if !cur.trim().is_empty() { parts.push(cur.trim().to_string()); } proto_decl.generics.iter() .zip(parts.iter()) .map(|(gp, arg)| (gp.name.clone(), arg.clone())) .collect() } }; let is_auto_derivable = crate::protocols::auto_derive::is_builtin_protocol(proto_base); let mut auto_derive_errors: Vec<crate::protocols::auto_derive::DeriveError> = Vec::new(); for m in proto_methods { let has_explicit = self.t_provides_method(&td.name, &m.name); let has_default = if let Some(body) = &m.default_body { self.default_body_calls_satisfy_for(body, &td.name) } else { false }; if has_explicit { // Compare signature. Find T's fn for method name. if let Some(t_method) = self.find_method_decl(&td.name, &m.name) { if let Some(reason) = check_signature_match_with_subst(t_method, m, &proto_arg_subst) { wrong_sig.push(( m.name.clone(), render_method_sig(&m.name, &m.params, &m.return_type), reason, )); } // Plan 108.4 Ф.2: receiver mutability match check. if let Some(recv_err) = check_receiver_mut_match(t_method, m, proto_base, &td.name) { wrong_recv.push(recv_err); } } } else if !has_default { // Plan 126 Ф.4: попытка auto-derive перед reporting missing. let mut synthesized_ok = false; if is_auto_derivable { // Match method name к protocol's expected method. let expected_method = crate::protocols::auto_derive::builtin_protocol_method(proto_base); if expected_method.map_or(false, |em| em == m.name.as_str()) { let bridge = self.as_derive_query(); let mut derive_ctx = crate::protocols::auto_derive::AutoDeriveCtx::new(&bridge); match crate::protocols::auto_derive::synthesize_method( &mut derive_ctx, td, proto_base, ) { Ok(_fn_decl) => { // Synthesis succeeded — method satisfied via auto-derive. // V1: synthesized FnDecl не register'ится в method_table // immediately — Plan 126 Ф.5+ extension покрывает codegen // integration. Здесь только suppress'им E_IMPL_MISSING_METHODS, // подтверждая что фактическая synthesis возможна. synthesized_ok = true; } Err(derive_err) => { auto_derive_errors.push(derive_err); } } } } if !synthesized_ok { missing.push(render_method_sig(&m.name, &m.params, &m.return_type)); } } } // Plan 126 Ф.4: emit auto-derive diagnostics (E_AUTO_DERIVE_*). for derive_err in &auto_derive_errors { errors.push(Diagnostic::new( derive_err.diagnostic_message(), td.span, )); } for (name, expected, reason) in &wrong_sig { errors.push(Diagnostic::new( format!( "[E_IMPL_WRONG_SIGNATURE] type `{}` has method `{}` but its \ signature does not match the requirement from `#impl({})`. \ Expected: `{}`. {}\n \ note: protocol method signatures must match exactly \ (arity, param types, return type — modulo Self ↔ {}).", td.name, name, proto_name, expected, reason, td.name, ), td.span, )); } // Plan 108.4 Ф.2: emit receiver-mutability mismatch errors. for (mname, error_code, proto_qual, fix_hint) in &wrong_recv { errors.push(Diagnostic::new( format!( "[{}] type `{}` implements `{}` but method `{}` has wrong \ receiver mutability. Protocol `{}` declares `{} @{}(...)`. \ {}", error_code, td.name, proto_name, mname, proto_name, proto_qual, mname, fix_hint, ), td.span, )); } if !missing.is_empty() { let hint = missing.iter() .map(|s| format!(" - {}", s)) .collect::<Vec<_>>() .join("\n"); errors.push(Diagnostic::new( format!( "[E_IMPL_MISSING_METHODS] type `{}` claims `#impl({})` but \ is missing required method(s):\n{}\n \ note: implement these directly, e.g. `fn {} @<method>(...) -> ... => ...`, \ or ensure dependencies for a default body (e.g. `@compare` enables \ Equatable.equals via default).", td.name, proto_name, hint, td.name, ), td.span, )); } } } /// Plan 154.1 (D268): verify a METHOD-level `#impl(P1 + P2 + ...)` annotation /// on `fn T @m(...)`. Unlike the type-level pass (`verify_impl_protocols`, /// which checks T provides ALL of P's methods), this checks that THIS method /// `@m` legitimately implements a method OF P: /// 1. P is a known protocol type → else E_IMPL_UNKNOWN_PROTOCOL /// 2. `@m` ∈ P's declared methods → else E_IMPL_NOT_A_PROTOCOL_METHOD /// 3. signature (params + return + receiver → else E_IMPL_SIGNATURE_MISMATCH /// mutability) matches P's `@m`, modulo `Self` ↔ T /// /// Opt-in: the binding `type_impl_protocols[T] += P` happens in codegen /// (emit_c.rs ~2063, mirrored for method-level `#impl`). Absence of `#impl` /// is never an error — structural conformance still satisfies `[T P]` bounds. fn verify_method_impl_protocols(&self, fd: &FnDecl, errors: &mut Vec<Diagnostic>) { // `#impl` on a function requires a receiver — a protocol method is always // a method on a type. A free function can never BE a protocol method. let recv = match &fd.receiver { Some(r) => r, None => { errors.push(Diagnostic::new( format!( "[E_IMPL_NOT_A_PROTOCOL_METHOD] free function `{}` has `#impl(...)` \ but `#impl` on a `fn` requires a receiver (`fn T @{}(...)`). \ Protocol methods are always methods on a type.", fd.name, fd.name, ), fd.span, )); return; } }; for proto_name in &fd.impl_protocols { // Plan 164 Ф.1: proto_name may be "Next[U]" — extract bare name for lookups. let proto_base = impl_spec_base_name(proto_name); // Plan 137 (D237): renamed protocols — helpful redirect. if let Some((_, new_name)) = Self::RENAMED_PROTOCOLS.iter() .find(|(old, _)| *old == proto_base) { errors.push(Diagnostic::new( format!( "[E_PROTOCOL_RENAMED] protocol `{}` was renamed to `{}` (D237). \ Use `#impl({})` instead.", proto_name, new_name, new_name, ), fd.span, )); continue; } let proto_decl = match self.types.get(proto_base) { Some(td) => td, None => { errors.push(Diagnostic::new( format!( "[E_IMPL_UNKNOWN_PROTOCOL] method `{} @{}` has `#impl({})` but \ `{}` is not a known type. Did you forget to import it, or \ misspell the protocol name?", recv.type_name, fd.name, proto_name, proto_base, ), fd.span, )); continue; } }; let proto_methods = match &proto_decl.kind { TypeDeclKind::Protocol { methods, .. } => methods, _ => { errors.push(Diagnostic::new( format!( "[E_IMPL_UNKNOWN_PROTOCOL] method `{} @{}` has `#impl({})` but \ `{}` is not a protocol — it's a different kind of type. \ `#impl(...)` only accepts protocol names.", recv.type_name, fd.name, proto_name, proto_base, ), fd.span, )); continue; } }; // Plan 164 Ф.1: build a generic substitution map from the impl-spec args, // e.g. "Next[U]" → {"T": "U"} (proto generic param → impl type arg name). let proto_arg_subst: Vec<(String, String)> = { let args_text = impl_spec_args_text(proto_name); if args_text.is_empty() || proto_decl.generics.is_empty() { vec![] } else { let inner = &args_text[1..args_text.len().saturating_sub(1)]; let mut parts: Vec<String> = Vec::new(); let mut depth = 0usize; let mut cur = String::new(); for ch in inner.chars() { match ch { '[' | '(' => { depth += 1; cur.push(ch); } ']' | ')' => { depth -= 1; cur.push(ch); } ',' if depth == 0 => { parts.push(cur.trim().to_string()); cur = String::new(); } _ => { cur.push(ch); } } } if !cur.trim().is_empty() { parts.push(cur.trim().to_string()); } proto_decl.generics.iter() .zip(parts.iter()) .map(|(gp, arg)| (gp.name.clone(), arg.clone())) .collect() } }; // `@m` must be one of P's declared methods. let proto_method = match proto_methods.iter().find(|m| m.name == fd.name) { Some(m) => m, None => { let avail = proto_methods.iter() .map(|m| format!("`@{}`", m.name)) .collect::<Vec<_>>() .join(", "); errors.push(Diagnostic::new( format!( "[E_IMPL_NOT_A_PROTOCOL_METHOD] method `@{}` is not declared by \ protocol `{}`. `{}` declares: {}. Did you mean one of those, \ or a different protocol in `#impl(...)`?", fd.name, proto_base, proto_base, avail, ), fd.span, )); continue; } }; // Signature: params + return type, modulo `Self` ↔ T and generic subst. if let Some(reason) = check_signature_match_with_subst(fd, proto_method, &proto_arg_subst) { errors.push(Diagnostic::new( format!( "[E_IMPL_SIGNATURE_MISMATCH] method `{} @{}` has `#impl({})` but its \ signature does not match protocol `{}`'s `@{}`. Expected: `{}`. {}\n \ note: protocol method signatures must match exactly (arity, param \ types, return type — modulo Self ↔ {}).", recv.type_name, fd.name, proto_name, proto_base, fd.name, render_method_sig(&proto_method.name, &proto_method.params, &proto_method.return_type), reason, recv.type_name, ), fd.span, )); continue; } // Receiver mutability (ro / mut / consume) must match P's `@m`. if let Some((_, code, proto_qual, fix)) = check_receiver_mut_match(fd, proto_method, proto_base, &recv.type_name) { errors.push(Diagnostic::new( format!( "[E_IMPL_SIGNATURE_MISMATCH] method `{} @{}` has `#impl({})` but its \ receiver mutability does not match protocol `{}`'s `{} @{}(...)`. {} \ (detail: {})", recv.type_name, fd.name, proto_name, proto_name, proto_qual, fd.name, fix, code, ), fd.span, )); continue; } } } fn protocol_method_satisfiable_for(&self, tname: &str, method_name: &str) -> bool { // Plan 91.9 (D186) gate: bare-call satisfiability требует `#impl(P)` // opt-in. Only protocols в T's impl_protocols list considered. // Без `#impl` — bare call к default-body-synthesized method даёт // E7320 normally (opt-in nominal layer над structural protocols). // Plan 164 Ф.1: impl_protocols may contain "Next[U]" — extract bare names // for the opted_in set which is compared against self.types keys (bare names). let opted_in: HashSet<&str> = self.types.get(tname) .map(|td| td.impl_protocols.iter().map(|s| impl_spec_base_name(s)).collect()) .unwrap_or_default(); for (proto_name, td) in &self.types { if !opted_in.contains(proto_name.as_str()) { continue; } if let TypeDeclKind::Protocol { methods, .. } = &td.kind { for m in methods { if m.name == method_name && m.default_body.is_some() { let body = m.default_body.as_ref().unwrap(); if self.default_body_calls_satisfy_for(body, tname) { return true; } } } } } false } /// №384 (Plan p383-bounds, единый вход): whether every method reference / /// free-fn call on Self/@ inside `body` resolves for `tname`. Thin /// delegator to the free-standing `default_body_calls_satisfy_for` /// walker below (shared with `BoundCtx::check_satisfaction_against_methods` /// — decl-site `#impl(P)` verification and call-site generic-bound /// satisfaction now run through the SAME walker, not two copies of it; /// `self` supplies the `DefaultBodyProbe` backend for this ctx). fn default_body_calls_satisfy_for(&self, body: &Block, tname: &str) -> bool { default_body_calls_satisfy_for(body, tname, self) } /// T has method `name` (instance or static, with-or-without `@` prefix). fn t_provides_method(&self, tname: &str, name: &str) -> bool { // U.2.3.3: base (sig) ∪ synth overlay (this is the F1 helper that collapses // the inline `method_table.get(t).keys().any(trim @)` sites). let has = |tbl: Option<&HashMap<String, Vec<&'a FnDecl>>>| { tbl.map_or(false, |m| m.keys().any(|k| k.trim_start_matches('@') == name)) }; has(self.sig.methods_of(tname)) || has(self.synth_methods.get(tname)) } /// Find the FnDecl of T's method with given name. Returns first match /// (overloads not typical для protocol methods — strict 1-to-1 match). fn find_method_decl(&self, tname: &str, name: &str) -> Option<&FnDecl> { // U.2.3.3: search base (sig) then synth overlay. let search = |tbl: Option<&HashMap<String, Vec<&'a FnDecl>>>| -> Option<&'a FnDecl> { let methods = tbl?; for (k, fns) in methods.iter() { if k.trim_start_matches('@') == name { return fns.first().copied(); } } None }; search(self.sig.methods_of(tname)).or_else(|| search(self.synth_methods.get(tname))) } fn t_provides_field(&self, tname: &str, name: &str) -> bool { if let Some(td) = self.types.get(tname) { if let TypeDeclKind::Record(fields) = &td.kind { return fields.iter().any(|f| f.name == name); } } false } /// `str.from(T)` satisfied if T has either: /// - `fn str.from(T) -> str` overload registered, OR /// - `fn T @into() -> str` (D73 chain). fn t_satisfies_str_from(&self, tname: &str) -> bool { // Path 1: explicit `fn str.from(T) -> str` overload. let str_from = self.method_overloads("str", "from") .map_or(false, |fns| fns.iter().any(|f| { f.params.len() == 1 && matches!(&f.params[0].ty, TypeRef::Named { path, .. } if path.last().map_or(false, |s| s == tname)) })); if str_from { return true; } // Path 2: T has `@into() -> str`. self.method_overloads(tname, "into") .or_else(|| self.method_overloads(tname, "@into")) .map_or(false, |fns| fns.iter().any(|f| { matches!(&f.return_type, Some(TypeRef::Named { path, .. }) if path.len() == 1 && path[0] == "str") })) } /// Plan 221.1 [M-interp-numeric-fallback-silent-garbage]: mirrors the /// D410 fallback emit_c.rs checks (~42887-42904) right before its /// numeric-cast last resort — `str.from(T)` overload OR `T.to_str()` /// INSTANCE method. Either routes bare `${x}` to a real string /// conversion instead of the garbage numeric cast, so a type /// satisfying this predicate must NOT get `E_INTERP_NO_DISPLAY`. /// Deliberately narrower than `t_satisfies_str_from` above (which also /// accepts `@into() -> str` — the retracted D73 Into[str] auto-derive /// path, no longer consulted by emit_c's interpolation fallback). fn interp_display_via_str_from_or_to_str(&self, tname: &str) -> bool { let str_from = self.method_overloads("str", "from") .map_or(false, |fns| fns.iter().any(|f| { f.params.len() == 1 && matches!(&f.params[0].ty, TypeRef::Named { path, .. } if path.last().map_or(false, |s| s == tname)) })); if str_from { return true; } self.method_overloads(tname, "to_str") .map_or(false, |fns| fns.iter().any(|f| f.receiver.is_some())) } /// Plan 221.1 (D186-амендмент) [M-interp-numeric-fallback-silent-garbage]: /// bare `"${x}"` (Display: `spec` is `None` or a non-Debug rich `Spec`) /// interpolation of a user value-type WITHOUT `#impl(Display)` used to /// silently reach emit_c's LAST-RESORT numeric-cast fallback /// (`nova_int_to_str((nova_int)(v))`, emit_c.rs ~42903) — prints the /// object's heap address as a decimal integer with zero diagnostic. /// D186 gate_on_impl=true already means "no `#impl(Display)` ⇒ no /// display method" for the synthesis path (`try_synthesize_default_ /// method`, gate_on_impl=true for Display) — this pass turns that /// already-established absence into an honest compile error instead of /// letting codegen degrade to silent garbage. rustc precedent: `Display` /// is never auto-derived; a missing impl is a compile error, not a /// best-effort fallback. /// /// Scope — deliberately narrow, non-generic value types only /// (`td.generics.is_empty()`): /// - `Record` / `Sum` / `NamedTuple` / `Newtype` declared types. /// /// Explicitly OUT of scope (left silent here — handled elsewhere or a /// distinct, already-covered concern): /// - primitives / `str` / `char` — never reach emit_c's fallback at /// all (early-out at emit_c.rs ~42775-42778); /// - typed pointers (`&v`, `*p`, `e as *T`) — already banned via /// `E_PTR_NO_DISPLAY_USE_DEBUG_STR` (`UnsafeCtx::walk_expr` above, /// Plan 91.14/118 D216 §15); `*T` Debug auto-derive is a SEPARATE /// open item ([M-91.14-ptr-auto-derive]), not touched here; /// - generic type-parameters in scope (`gs.contains(name)`) — bound- /// satisfiability is a mono-time concern the checker cannot decide /// structurally pre-monomorphization; /// - generic (parametrized) declared types — `Vec[T]`, builtin /// `Option`/`Result`, or a user `Box[T]` (`!td.generics.is_empty()`) /// — routed in emit_c via `try_generic_mono_interp_dispatch` / /// the Option-Result `DeclaredBody` special-case, neither of which /// is visible to this pre-mono pass; flagging here risks a false /// positive against a type that DOES resolve fine post-mono. /// Shared scoping logic between `check_interp_no_display` and /// `check_interp_no_debug` — resolves `ex`'s static type and returns its /// name IFF it's a candidate this pass should judge at all: a /// non-generic `Record`/`Sum`/`NamedTuple`/`Newtype` declared type. /// Returns `None` for everything the gap-closure deliberately leaves /// alone (see the two callers' doc comments for the itemized list — /// primitives, generic type-params, generic declared types, unknown /// names, `CharLit` literals). fn resolve_interp_user_value_type( &self, ex: &Expr, gs: &GenericScope, scope: &HashMap<String, TypeRef>, ) -> Option<String> { // CharLit is never a user type — mirrors emit_c's // `!matches!(e.kind, ExprKind::CharLit(_))` gate at ~42775/~42690. if matches!(ex.kind, ExprKind::CharLit(_)) { return None; } let tname = match self.infer_expr_type(ex, scope) { Some(TypeRef::Named { path, generics, .. }) if generics.is_empty() => { path.last()?.clone() } _ => return None, }; if gs.contains_key(&tname) { return None; // generic type-param in scope — mono-time concern. } if matches!( tname.as_str(), "int" | "float" | "f32" | "f64" | "bool" | "char" | "str" ) { return None; } let td = self.types.get(&tname)?; // unknown/builtin name — leave to other passes. if !td.generics.is_empty() { return None; // generic declared type (incl. Option/Result) — mono-time concern. } let is_user_value_type = matches!( td.kind, TypeDeclKind::Record(_) | TypeDeclKind::Sum(_) | TypeDeclKind::NamedTuple(_) | TypeDeclKind::Newtype(_) ); if !is_user_value_type { return None; } Some(tname) } fn check_interp_no_display( &self, ex: &Expr, gs: &GenericScope, scope: &HashMap<String, TypeRef>, spec: &crate::ast::FormatSpec, errors: &mut Vec<Diagnostic>, ) { // Only the bare `FormatSpec::None` shape reaches emit_c's numeric // fallback (~42903) — a rich `Spec` (even Display-kind) is emitted // via the SEPARATE `emit_format_spec_value` lowering, which already // errors honestly (`E_BAD_FORMAT_SPEC`) when no Display/Debug method // resolves (emit_c.rs ~43295-43302) — no garbage-fallback gap there. if !matches!(spec, crate::ast::FormatSpec::None) { return; } let Some(tname) = self.resolve_interp_user_value_type(ex, gs, scope) else { return; }; if self.find_method_decl(&tname, "display").is_some() { return; // explicit `@display` OR gate-satisfied auto-derive synth. } if self.interp_display_via_str_from_or_to_str(&tname) { return; // D410 `str.from(T)` / `T.to_str()` fallback still applies. } errors.push(Diagnostic::new( format!( "[E_INTERP_NO_DISPLAY] type `{}` has no `#impl(Display)` — bare \ string interpolation `\"${{...}}\"` would otherwise silently \ print a numeric cast of the value instead of a real \ representation (Plan 91.9 D186: gate_on_impl=true — Display is \ opt-in, never auto-derived; rustc precedent: missing impl is a \ compile error, not a best-effort fallback). Fix: add \ `#impl(Display)` above `type {}` plus a `fn {} @display(mut f \ Fmt)` method, or use `${{x:?}}` (requires its OWN `#impl(Debug)`, \ D229 §4 — not automatic either).", tname, tname, tname, ), ex.span, )); } /// Plan 221.1 followup [M-interp-numeric-fallback-silent-garbage]: /// D410-style fallback for Debug — `str.from_debug(T)` overload routes /// bare `${x:?}` to a real conversion instead of the numeric-cast last /// resort (emit_c.rs ~42865/42887, `from_method = "from_debug"` when /// `is_debug`). Unlike Display, there is NO `to_str()`-instance /// equivalent fallback for Debug in emit_c — only `str.from_debug`. fn interp_debug_via_str_from_debug(&self, tname: &str) -> bool { self.method_overloads("str", "from_debug") .map_or(false, |fns| fns.iter().any(|f| { f.params.len() == 1 && matches!(&f.params[0].ty, TypeRef::Named { path, .. } if path.last().map_or(false, |s| s == tname)) })) } /// Plan 221.1 followup (coordinator repro, 2026-07-21) /// [M-interp-numeric-fallback-silent-garbage]: bare `${x:?}` (Debug — /// spec is exactly `FormatSpec::Debug`, the bare form; a rich `Spec` /// with `Kind::Debug` goes through the separate `emit_format_spec_value` /// lowering, which already errors honestly via `E_BAD_FORMAT_SPEC` when /// no `@debug` resolves) on a user value-type WITHOUT `#impl(Debug)` /// used to silently reach the SAME numeric-cast last-resort fallback as /// the Display case (`nova_int_to_str((nova_int)(v))`, emit_c.rs /// ~42903) — printing the object's heap address as a decimal integer. /// /// This closes a gap DISTINCT from (but sibling to) `E_INTERP_NO_ /// DISPLAY`: `emit_c.rs`'s Debug branch (~42816) calls /// `try_synthesize_default_method_with_gate(..., gate_on_impl=false)` /// with a comment claiming "zero-friction... no annotation needed" — /// but `Debug`'s protocol declaration ships with **no default body at /// all** (D229 §2), so that candidate-search-based synthesis path can /// NEVER find a match regardless of the gate flag; it is dead code for /// Debug specifically. The REAL Debug synthesis mechanism actually used /// everywhere in this codebase is `inject_synthesized_methods` /// (auto_derive.rs, hand-written memberwise-body generator), and IT /// gates on `td.impl_protocols` containing `"Debug"` literally (mirrored /// in the type-checker's own `register_synthesized_methods`, which is /// what `find_method_decl` below actually observes) — i.e. Debug DOES /// require `#impl(Debug)` in the ACTUAL, currently-shipping /// implementation, matching D229 §4 ("Когда user type X помечен /// `#impl(Debug)`") and its own §7 error-code table, which already /// RESERVES `E_DEBUG_PRINTABLE_NOT_IMPLEMENTED` for exactly "type /// doesn't impl Debug, no auto-synthesis possible" — that code was /// never actually wired to a check anywhere in the compiler before this. /// /// Scope mirrors `check_interp_no_display` exactly (see /// `resolve_interp_user_value_type`): non-generic `Record`/`Sum`/ /// `NamedTuple`/`Newtype` only; primitives (which DO have unconditional /// `@debug` bodies in `std/prelude/protocols.nv` per D229 §5), typed /// pointers (separately covered by `E_PTR_NO_DISPLAY_USE_DEBUG_STR`), /// generic type-params, and generic declared types are left alone. fn check_interp_no_debug( &self, ex: &Expr, gs: &GenericScope, scope: &HashMap<String, TypeRef>, spec: &crate::ast::FormatSpec, errors: &mut Vec<Diagnostic>, ) { if !matches!(spec, crate::ast::FormatSpec::Debug) { return; } let Some(tname) = self.resolve_interp_user_value_type(ex, gs, scope) else { return; }; if self.find_method_decl(&tname, "debug").is_some() { return; // explicit `@debug` OR gate-satisfied (`#impl(Debug)`) auto-derive synth. } if self.interp_debug_via_str_from_debug(&tname) { return; // D410-style `str.from_debug(T)` fallback still applies. } errors.push(Diagnostic::new( format!( "[E_DEBUG_PRINTABLE_NOT_IMPLEMENTED] type `{}` has no \ `#impl(Debug)` — bare `\"${{...:?}}\"` would otherwise \ silently print a numeric cast of the value instead of a \ real representation (Plan 91.14 D229 §4/§7: Debug \ auto-derive requires `#impl(Debug)` same as Display requires \ `#impl(Display)` — it is NOT automatic for every record/sum \ type). Fix: add `#impl(Debug)` above `type {}` (compiler \ synthesizes a memberwise `{} {{ field1: ..., field2: ... }}` \ body via `inject_synthesized_methods`), or write an explicit \ `fn {} @debug(mut f Fmt)` method.", tname, tname, tname, tname, ), ex.span, )); } /// record/sum-тип. Пустые типы (unit), эффекты (handler — значение), /// протоколы/newtype/alias/opaque, а также имена, перекрытые /// локальной переменной — пропускаются (валидно либо неоднозначно). fn f4_check_value( &self, expr: &Expr, scope: &HashMap<String, TypeRef>, errors: &mut Vec<Diagnostic>, ) { let ExprKind::Ident(name) = &expr.kind else { return; }; // Локальная переменная / параметр перекрывает имя типа. if scope.contains_key(name) { return; } let Some(td) = self.types.get(name) else { return; }; let kind = match &td.kind { TypeDeclKind::Record(fields) if !fields.is_empty() => "record type", TypeDeclKind::Sum(variants) if !variants.is_empty() => "sum type", // empty record/sum (unit), effect/protocol/newtype/alias/opaque — // не value-misuse (либо валидно как значение, либо неоднозначно). _ => return, }; let hint = match &td.kind { TypeDeclKind::Record(_) => format!( "construct a value: `{} {{ ... }}` or a constructor `{}.new(...)`", name, name, ), TypeDeclKind::Sum(_) => { format!("use one of `{}`'s variants", name) } _ => String::new(), }; errors.push( Diagnostic::new( format!("[E7330] `{}` is a {}, not a value", name, kind), expr.span, ) .with_note(hint), ); } /// Plan 172.1 [literal-coercion channel] (§0/§1, 2026-06-30): materialize the /// CONTEXT-COERCED type of an integer literal (D55) into the `resolved_types_buf` /// channel so codegen emits it with the sized C-type (`((uint)NU)`) instead of /// collapsing to `nova_int`. The checker ALREADY computes this coercion in /// `assignable` (the IntLit-vs-sized-`expected` arm) and DISCARDS it after the /// range-check — this is the §1 «материализуй резолв, не выбрасывай» fix: the /// resolved type is WRITTEN to the channel for the consumer (`emit_expr` IntLit via /// `channel_int_c_type`), not re-derived in codegen. /// /// Recurses through the literal-bearing constructor wrappers so a literal NESTED in /// `Some(_)` / `Ok(_)` / `Err(_)` / a tuple / an array reaches its sized element /// type — the single target form that subsumes the per-position codegen coercions /// (`[M-172.1-some-target-coerce]` / `-tuple-destructure-annot` / `-default-arg-typed`). /// Call ONLY at DEFINITE coercion sites (annotated `let`, the CHOSEN call's args, /// `return`) — NEVER on a SPECULATIVE overload probe (`overload_applicability`), which /// would annotate a literal against a rejected overload's param. /// /// Conservative by construction: only a literal whose `expected` lowers to a SIZED /// `Scalar` (NOT the wide-default `int` — a no-op vs the seed; NOT a generic param → /// `Named` — left to mono) is annotated; everything else keeps the `number_exprs` /// seed (sound fallback). The consumer's `is_typed_integer` gate is the final filter. /// 172.1.2: аннотировать узел expected-типом, если тот КОНКРЕТЕН /// (Named: без args — примитив/объявленный; с args — все args /// primitive/Str/concrete-named). §1-гейт против записи невыведенного. fn annotate_expected_concrete(&self, value: &Expr, expected: &TypeRef) { if !value.id.is_set() { return; } let TypeRef::Named { path, generics, .. } = expected else { return }; let Some(last) = path.last() else { return }; // Plan 172.13 Ф.2 (constraint-core): gate routed through the shared // `TypeSet` language (`ts_member`) instead of duplicated inline // `matches!` booleans. `primitive_gate` already covers Str/Bool // (both `TypeSet::Primitive` members), so the former separate // `matches!(rt, Str | Bool)` was redundant with it — byte-identical // set, one shared predicate. `self.types.contains_key` (declared-type // lookup) needs checker state outside `ResolvedType` — stays a // direct Rust `||`, not folded into `TypeSet`. let concrete = if generics.is_empty() { let rt = ResolvedType::from_type_ref(expected); Self::ts_member(&rt, constraint_solver::TypeSet::Primitive) || self.types.contains_key(last) } else { generics.iter().all(|g| { let rt = ResolvedType::from_type_ref(g); Self::ts_member( &rt, constraint_solver::TypeSet::Union(vec![ constraint_solver::TypeSet::Primitive, constraint_solver::TypeSet::ConcreteNamedNoArgs, ]), ) }) }; if concrete { let rt = ResolvedType::from_type_ref(expected); self.resolved_types_buf.borrow_mut().insert(value.id, rt); } } fn materialize_literal_coercion(&self, value: &Expr, expected: &TypeRef) { // Strip compile-time-only type modifiers — they do not change the C width. match expected { TypeRef::Readonly(inner, _) | TypeRef::Mut(inner, _) | TypeRef::Uninit(inner, _) => { self.materialize_literal_coercion(value, inner); return; } _ => {} } match &value.kind { ExprKind::IntLit(_) => { if value.id.is_set() { let rt = ResolvedType::from_type_ref(expected); // Plan 172.13 Ф.2: gate routed through `TypeSet:: // ScalarNotWideDefaultInt` — any SIZED scalar except // exactly `int` (never a no-op wide-default `int`, never // a non-scalar). NOTE the byte-parity subtlety this // preserves: `uint` (unsigned wide-default) still PASSES // — it differs from the `int` literal seed's C type even // though both are "wide" (see the TypeSet doc-comment). if Self::ts_member(&rt, constraint_solver::TypeSet::ScalarNotWideDefaultInt) { self.resolved_types_buf.borrow_mut().insert(value.id, rt); } } } // 2026-07-02 (tally, кластер VariantsMatchClosure АТОМ 1): bare `None` // против expected `Option[T]` → аннотировать САМ Ident типом Option[T]. // infer_expr_type не резолвит варианты generic-sum'ов (Option), а legacy // отвечал по function-LEVEL current_fn_return_ty независимо от позиции // (call-arg/let — ложь по построению) с дефолтом NovaOpt_nova_int. // Гейт против лжи (§1): T обязан быть КОНКРЕТНЫМ — primitive_gate || // concrete_value_named (зеркало f3_check_member); typevar `T` (Named, не // в self.types) отвергается обоими → без аннотации → legacy-навигация. ExprKind::Ident(n) if n == "None" => { if value.id.is_set() { if let TypeRef::Named { path, generics, .. } = expected { if path.last().map(|s| s.as_str()) == Some("Option") && generics.len() == 1 { let arg_rt = ResolvedType::from_type_ref(&generics[0]); let concrete_value_named = matches!(&arg_rt, ResolvedType::Named { name, args, .. } if args.is_empty() && self.types.get(name).map_or(false, |td| matches!(&td.kind, TypeDeclKind::Record(_) | TypeDeclKind::Sum(_) | TypeDeclKind::Newtype(_) | TypeDeclKind::NamedTuple(_)))); if Self::ts_member(&arg_rt, constraint_solver::TypeSet::Primitive) || concrete_value_named { let rt = ResolvedType::from_type_ref(expected); self.resolved_types_buf.borrow_mut().insert(value.id, rt); } } } } } // [M-208-fmtkind-bare-variant-shadow] (2026-07-17): general sibling of // the `None`/`Option[T]` arm above, for any OTHER bare enum-variant // Ident against a concrete (non-generic) user Sum type. Root cause: // `infer_expr_type`'s bare-variant "last resort" fallback // ([M-hashmap-order-bare-variant-flake], 2026-07-13) picks among ALL // sum types that declare a variant of this NAME via a context-free // lexicographic-smallest-typename tie-break — correct only when // there's truly no better signal. Plan 208 Ф.2 added `FmtKind` (variant // `Oct`, std/runtime/fmt_buf.nv) which happens to sort before the // pre-existing `Month` (also variant `Oct`, std/time/civil) — so any // bare `Oct` passed as a call arg/return/let-init/match-arm against an // expected `Month` silently mis-resolved to `FmtKind.Oct` // (`nth_sunday_epoch_day(y, Oct)` etc, std/time/civil/tz.nv → E7301). // Here we DO have a better signal: `expected` (this exact position) // names a concrete Sum type; if THAT type owns a unit-variant spelled // exactly `n`, the bare Ident denotes THAT variant unambiguously // (mirrors Rust's own expected-type-directed enum-variant resolution). // Materializing into `resolved_types_buf` here fixes the resolution AT // THE SOURCE for every consumer — the `assignable` compat check AND // codegen's own lowering both read `resolved_types_buf` FIRST, before // `infer_expr_type`'s ambiguous fallback ever runs — instead of only // papering over the downstream E7301 symptom. ExprKind::Ident(n) if n != "None" => { if value.id.is_set() { if let TypeRef::Named { path, generics, .. } = expected { if generics.is_empty() { if let Some(tname) = path.last() { if let Some(td) = self.types.get(tname) { if td.generics.is_empty() { if let TypeDeclKind::Sum(variants) = &td.kind { if variants.iter().any(|v| &v.name == n) { let rt = ResolvedType::from_type_ref(expected); self.resolved_types_buf.borrow_mut().insert(value.id, rt); } } } } } } } } } // `Some(inner)` / `Ok(inner)` / `Err(inner)` against `Option[T]` / `Result[T,E]`. ExprKind::Call { func, args, .. } if args.len() == 1 => { if let ExprKind::Ident(ctor) = &func.kind { if let Some(elem) = ctor_payload_expected(ctor, expected) { self.materialize_literal_coercion(args[0].expr(), elem); // 172.1.2 (контекстная типизация ctor, 2026-07-03): сам // Call аннотируется expected-типом (армы if/match в // return-позиции: Ok(x)/Err(e) при известном Result[T,E]). // Гейт §1: ВСЕ args expected конкретны (как None-арм) — // иначе без аннотации. if value.id.is_set() { if let TypeRef::Named { generics, .. } = expected { // [M-option-fn-field-record-literal-elem-type-int] // (реестр 221.1 №116): `ctor_arg_concrete` (not the bare // `Union(Primitive, ConcreteNamedNoArgs)` gate) — a // `fn(...)->...` generic arg (`Option[fn(A) -> B]`) is // "concrete" too when its own params/return are, so // `Some(closure)` against `Option[fn(A) -> B]` now // materializes instead of silently skipping (root cause // of the Option-mono defaulting to `nova_int`). let all_concrete = !generics.is_empty() && generics.iter().all(|g| { let rt = ResolvedType::from_type_ref(g); Self::ctor_arg_concrete(&rt) }); if all_concrete { let rt = ResolvedType::from_type_ref(expected); self.resolved_types_buf.borrow_mut().insert(value.id, rt); } } } } } } // 172.1.2 АТОМ 3a (2026-07-03): `ro f (int)->int = |x| x+1` — // closure против Func-аннотации: посев параметров + Func-аннотация // самого замыкания (потребитель Channel 2 clos_struct_name готов; // источник = ДЕКЛАРИРОВАННАЯ аннотация — рассинхрона нет). ExprKind::ClosureLight { params: cl_params, .. } => { if value.id.is_set() { if let TypeRef::Func { params: fp, return_type, .. } = expected { if cl_params.len() == fp.len() { let ps: Vec<ResolvedType> = fp.iter().map(ResolvedType::from_type_ref).collect(); let ret_rt = match return_type { Some(r) => ResolvedType::from_type_ref(r), None => ResolvedType::Unit, }; // [№TBD, реестр 221.1 №403] `ConcreteNamedNoArgs` is PURELY // structural (`constraint_solver.rs`: any bare `Named{args: // []}` passes) — it cannot tell a genuinely declared type // apart from a leaked generic-scope letter (`T`/`U`/`E`/...) // that never went through `mark_type_params` before reaching // this ("172.1.2 АТОМ 3a") arm. Two OTHER `materialize_ // literal_coercion` call sites already hit and fixed the SAME // class (`[196.5 closure-lowering fix]` ~L14269, // `[M-instance-method-closure-arg-generic-return]` ~L15162) // via `typeref_mentions_any` against the callee's generic // scope — this simpler, `expected`-only arm has no callee/`gs` // to consult, so it takes a cheaper, equally honest route: // require a bare Named name to be an ACTUAL declared type // (`self.types`) — a real record/sum/protocol always IS // one; a bare generic-parameter letter never is. Concretely: // `Option[T].filter(pred fn(T) -> bool)` called on a concrete // `Option[int]` (`a.filter(|x| x % 2 == 0)`) used to stamp raw // `Named{"T"}` straight into `resolved_types` here (`T` PASSES // `ConcreteNamedNoArgs` — indistinguishable from a real // no-generics record at this purely-structural layer) — // codegen's channel then mangled it to a bogus, never-defined // `Nova_T*` C type (CC-FAIL, // `plan200_14_option_result_flat_map_filter.nv`'s // `Option.filter` tests, only surfaced once a SEPARATE fix — // making the Func-return-type conversion succeed for a bare // `bool`/`int` — let this arm's registration reach codegen // instead of bailing earlier on the UNRELATED missing-primitive // gap it used to hit first). `char`, a `Named{args:[]}`-shaped // primitive (`constraint_solver.rs` `TypeSet::Primitive`'s own // special case), is checked FIRST and short-circuits before the // `type_decls` gate — never affected by this change. let concrete = ps.iter().chain(std::iter::once(&ret_rt)).all(|r| { if Self::ts_member(r, constraint_solver::TypeSet::Primitive) { return true; } match r { ResolvedType::Named { name, args, .. } if args.is_empty() => self.types.contains_key(name.as_str()), _ => false, } }); if concrete { self.resolved_types_buf.borrow_mut().insert( value.id, ResolvedType::Func { params: ps, ret: Box::new(ret_rt), effects: vec![], }, ); } } } } } // `(a, b, …)` against `(Ta, Tb, …)` → coerce element-wise. ExprKind::TupleLit(elems) => { if let TypeRef::Tuple(tys, _) = expected { if tys.len() == elems.len() { for (el, ty) in elems.iter().zip(tys) { self.materialize_literal_coercion(el, ty); } } } } // `[a, b, …]` against `[]T` / `Vec[T]` → coerce each element to `T`. ExprKind::ArrayLit(items) => { if let Some(elem) = array_elem_type(expected) { for it in items { if let ArrayElem::Item(x) = it { self.materialize_literal_coercion(x, elem); } } } } // [M-option-fn-field-record-literal-elem-type-int] (реестр 221.1 №116): // `Type { field: value, ... }` against a concrete (non-generic) declared // `Record` type — recurse into EACH field's value against the FIELD's // OWN declared type (mirrors the `TupleLit`/`ArrayLit` element-wise // recursion just above). Without this arm a record literal's field // values NEVER reached this materializing walk — a field carries its // OWN nested expected-typed sub-position (`Type { hook: Some(closure) }` // where `hook Option[fn(A) -> B]` needs `closure` materialized against // `fn(A) -> B`, not against the outer record's own type — the `Call` // arm above already unwraps `Some(..)`/`Ok(..)`/`Err(..)` against // `Option[T]`/`Result[T,E]`, it just never got THIS field's `T` handed // to it before). Anonymous literals (D55 record-coercion) are handled // identically — `expected` alone decides the field schema, `type_name` // is not consulted. Scoped to non-generic declared records (mirrors // this function's other `td.generics.is_empty()`-gated arms) — // a generic record's field types would need receiver-generic // substitution first, out of scope here. ExprKind::RecordLit { fields, .. } => { if let TypeRef::Named { path, generics, .. } = expected { if generics.is_empty() { if let Some(name) = path.last() { if let Some(td) = self.types.get(name) { if td.generics.is_empty() { if let TypeDeclKind::Record(decl_fields) = &td.kind { for f in fields { if let Some(v) = &f.value { if let Some(fd) = decl_fields.iter().find(|df| df.name == f.name) { self.materialize_literal_coercion(v, &fd.ty); } } } } } } } } } } // Value-position control flow: each branch's TAIL value is in the SAME // expected-typed position (an `if`/`match`/block as a value coerces its // result, so each branch result coerces). `=> if c { 0x80 } else { 5 }` and // `{ …; match k { _ => 0x80 } }` reach their literal leaves this way. ExprKind::If { then, else_, .. } | ExprKind::IfLet { then, else_, .. } => { self.materialize_block_tail(then, expected); if let Some(eb) = else_ { match eb { ElseBranch::Block(b) => self.materialize_block_tail(b, expected), ElseBranch::If(e) => self.materialize_literal_coercion(e, expected), } // 172.1.2 (2026-07-03): if-С-else в коэрцируемой позиции — // тип выражения = expected (гейт annotate_expected_concrete). self.annotate_expected_concrete(value, expected); } } ExprKind::Match { arms, .. } => { // 172.1.2 (2026-07-03): match в коэрцируемой позиции — тип = // expected (гейт: конкретный, annotate_expected_concrete). self.annotate_expected_concrete(value, expected); for arm in arms { match &arm.body { MatchArmBody::Expr(e) => self.materialize_literal_coercion(e, expected), MatchArmBody::Block(b) => self.materialize_block_tail(b, expected), } } } ExprKind::Block(b) => self.materialize_block_tail(b, expected), _ => {} } } /// [literal-coercion channel]: coerce a block's TRAILING (tail value) expression to /// `expected` — the block's value position. (Statements are not tail values; explicit /// `return`s inside the block are handled by `materialize_returns_in_block`.) fn materialize_block_tail(&self, b: &Block, expected: &TypeRef) { if let Some(t) = &b.trailing { self.materialize_literal_coercion(t, expected); } } /// [literal-coercion channel]: materialize every explicit `return <expr>` in a function /// body against the declared return type, recursing through nested control flow (the /// implicit tail return is handled separately at the `FnBody` site). DEFINITE — every /// `return` in this body commits to `ret`. Covers the common block-bearing constructs; /// exotic bodies (spawn/select/handler) simply are not walked (sound fallback). fn materialize_returns_in_block(&self, b: &Block, ret: &TypeRef) { for s in &b.stmts { self.materialize_returns_in_stmt(s, ret); } if let Some(t) = &b.trailing { self.materialize_returns_in_expr(t, ret); } } fn materialize_returns_in_stmt(&self, s: &Stmt, ret: &TypeRef) { match s { Stmt::Return { value: Some(e), .. } => self.materialize_literal_coercion(e, ret), Stmt::Expr(e) => self.materialize_returns_in_expr(e, ret), Stmt::Let(d) => self.materialize_returns_in_expr(&d.value, ret), Stmt::Const(d) => self.materialize_returns_in_expr(&d.value, ret), Stmt::Defer { body, .. } => self.materialize_returns_in_expr(body, ret), Stmt::ConsumeScope { body, .. } => self.materialize_returns_in_block(body, ret), _ => {} } } fn materialize_returns_in_expr(&self, e: &Expr, ret: &TypeRef) { match &e.kind { ExprKind::If { then, else_, .. } | ExprKind::IfLet { then, else_, .. } => { self.materialize_returns_in_block(then, ret); if let Some(eb) = else_ { match eb { ElseBranch::Block(b) => self.materialize_returns_in_block(b, ret), ElseBranch::If(x) => self.materialize_returns_in_expr(x, ret), } } } ExprKind::Match { arms, .. } => { for arm in arms { match &arm.body { MatchArmBody::Expr(x) => self.materialize_returns_in_expr(x, ret), MatchArmBody::Block(b) => self.materialize_returns_in_block(b, ret), } } } // Same-fn control flow only — a `return` here IS the enclosing fn's return. // Deliberately NOT recursing into `detach`/`parallel for`/`spawn`/closures: // a `return` there belongs to a DIFFERENT execution context, so coercing it // to THIS fn's return type would be wrong (sound fallback: leave it seeded). ExprKind::While { body, .. } | ExprKind::WhileLet { body, .. } | ExprKind::Loop { body, .. } | ExprKind::For { body, .. } => self.materialize_returns_in_block(body, ret), ExprKind::Block(b) => self.materialize_returns_in_block(b, ret), _ => {} } } // ======================================================================== // [M-closure-trailing-scalar-coercion-no-typecheck] fix (2026-07-10, // found during the `toml-fail-fix` investigation — see // docs/plans/backlog-followups.md). // // Neither `assignable` (call-arg / annotated-let positions only) nor the // `materialize_returns_in_*` walk above (which coerces LITERALS, it does // not REJECT mismatches) ever rejected a closure literal (`|| body` / // `|x| body` / `fn(...) ...`) reaching a return position whose declared // type is a scalar (`bool`/`int`/sized-int/float/`char`). Codegen lowers // a closure to a function pointer; without this check the pointer is // silently bit-reinterpreted as the scalar (an always-truthy `bool`, // garbage int, …) with NO diagnostic. Repro (mis-parsed multiline `||` // with a leading `||` on the continuation line — parser deliberately does // NOT continue an `||`-expression across a leading `||`, so the second // line becomes its OWN statement, parsed as the zero-arg closure literal // `|| (...)`, silently becoming the block's trailing expression): // // fn f(c char) -> bool { // ro n = c as int // (n >= 65 && n <= 90) // || (n >= 97 && n <= 122) // parses as a NEW stmt: closure-literal trailing // } // // Mirrors the return-position walk of `materialize_returns_in_block`/ // `_in_expr` above (same reachable-position set — same-fn control flow // only, NOT into `detach`/`spawn`/`parallel for`/nested closures, whose // `return` belongs to a different execution context). // ======================================================================== /// Проверяет ОДНУ return-позицию (trailing блока, arrow-body, `return X`): /// closure-литерал против скалярного `expected` — новый стабильный код /// `E_CLOSURE_SCALAR_RETURN`. fn check_closure_scalar_return(&self, value: &Expr, expected: &TypeRef, errors: &mut Vec<Diagnostic>) { if !is_closure_literal_expr(value) { return; } // Peel compile-time-only modifiers — same as `materialize_literal_coercion`. let mut ty = expected; loop { match ty { TypeRef::Readonly(inner, _) | TypeRef::Mut(inner, _) | TypeRef::Uninit(inner, _) => { ty = inner; } _ => break, } } // Expected IS itself a fn-type (HOF return) — a closure literal is legal there. if matches!(ty, TypeRef::Func { .. }) { return; } let rt = ResolvedType::from_type_ref(ty); if !resolved_type_is_scalar_like(&rt) { return; } errors.push(Diagnostic::new( format!( "[E_CLOSURE_SCALAR_RETURN] closure-литерал (`|| ...` / `|x| ...` / \ `fn(...) ...`) не может быть значением скалярного типа `{}`: closure \ лежит в памяти как указатель на функцию — коэрсия к скаляру молча \ бит-реинтерпретирует указатель (напр. `bool` всегда true), без этой \ диагностики. Частый триггер — случайный closure-литерал из-за ведущего \ `||`/`|x|` в начале continuation-строки многострочного выражения: \ проверьте, не должна ли эта строка ПРОДОЛЖАТЬ предыдущее выражение той \ же строкой (02-types.md D-блок «Closure-литерал против скалярного \ return-типа»).", render_type_ref(ty) ), value.span, )); } /// [M-fn-value-binding-untyped-silent] (реестр 221.1 №101, 2026-07-25): does /// `value`'s inferred type resolve to `TypeRef::Func` while `expected` does /// NOT itself denote a func-compatible position (bare `Func` or a declared /// fn-newtype/alias chain, `typeref_is_func_compatible`)? Pushes `[E7301]` /// and returns `true` if so. /// /// Deliberately NOT folded into the shared `assignable`/`assignable_direct` /// (tried first, reverted) — those run for EVERY call-argument/let/return /// check in the whole codebase, and `infer_expr_type`'s Plan-228 fn_decls /// fallback resolves an out-of-scope bare name that HAPPENS to also name a /// free fn elsewhere in a huge merged compile unit (spec_tests/conformance + /// transitively-imported std is one multi-hundred-file CU) to that unrelated /// fn's `Func` shape. That mistyping was already latent (Plan 228) but /// harmless everywhere else (`resolved_cat_of` collapses `Func` → `Any`, /// permissive) — folding a Func-mismatch check into the shared path turned /// it into a false-positive `[E7301]` on a genuinely-unrelated `int` /// argument (repro: `offset`, a std/fs-family local name colliding with an /// unrelated conformance helper `fn offset(int,int,int)`). Called ONLY from /// the two positions this window actually needs — an annotated `let` RHS /// (`f1_check_assign_let`) and a fn-value-typed call argument /// (`check_fn_value_call`) — where a `Func`-shaped value being the exact /// wrong thing IS the point of the check, not an incidental side effect of /// a call-argument loop this window never asked to touch. fn check_fn_value_mismatch( &self, value: &Expr, expected: &TypeRef, scope: &HashMap<String, TypeRef>, errors: &mut Vec<Diagnostic>, ) -> bool { let Some(found_tr) = self.infer_expr_type(value, scope) else { return false; }; if !matches!(found_tr, TypeRef::Func { .. }) { return false; } if self.typeref_is_func_compatible(expected) { return false; } errors.push(Diagnostic::new( format!( "[E7301] cannot assign value of type `{}` to `{}`: a fn-value \ cannot be coerced into a non-fn-compatible type (use a bare \ `fn(...) -> ...` annotation or a declared fn-newtype instead)", typeref_display(&found_tr), typeref_display(expected), ), value.span, )); true } fn check_closure_scalar_return_in_block(&self, b: &Block, ret: &TypeRef, errors: &mut Vec<Diagnostic>) { for s in &b.stmts { self.check_closure_scalar_return_in_stmt(s, ret, errors); } if let Some(t) = &b.trailing { self.check_closure_scalar_return_in_expr(t, ret, errors); } } fn check_closure_scalar_return_in_stmt(&self, s: &Stmt, ret: &TypeRef, errors: &mut Vec<Diagnostic>) { match s { Stmt::Return { value: Some(e), .. } => self.check_closure_scalar_return(e, ret, errors), Stmt::Expr(e) => self.check_closure_scalar_return_in_expr(e, ret, errors), Stmt::Let(d) => self.check_closure_scalar_return_in_expr(&d.value, ret, errors), Stmt::Const(d) => self.check_closure_scalar_return_in_expr(&d.value, ret, errors), Stmt::Defer { body, .. } => self.check_closure_scalar_return_in_expr(body, ret, errors), Stmt::ConsumeScope { body, .. } => self.check_closure_scalar_return_in_block(body, ret, errors), _ => {} } } /// Ищет ВЛОЖЕННЫЕ explicit `return X` (не саму `e` — `e` тут контейнер для /// поиска, не return-значение; зеркалит `materialize_returns_in_expr`). fn check_closure_scalar_return_in_expr(&self, e: &Expr, ret: &TypeRef, errors: &mut Vec<Diagnostic>) { match &e.kind { ExprKind::If { then, else_, .. } | ExprKind::IfLet { then, else_, .. } => { self.check_closure_scalar_return_in_block(then, ret, errors); if let Some(eb) = else_ { match eb { ElseBranch::Block(b) => self.check_closure_scalar_return_in_block(b, ret, errors), ElseBranch::If(x) => self.check_closure_scalar_return_in_expr(x, ret, errors), } } } ExprKind::Match { arms, .. } => { for arm in arms { match &arm.body { MatchArmBody::Expr(x) => self.check_closure_scalar_return_in_expr(x, ret, errors), MatchArmBody::Block(b) => self.check_closure_scalar_return_in_block(b, ret, errors), } } } ExprKind::While { body, .. } | ExprKind::WhileLet { body, .. } | ExprKind::Loop { body, .. } | ExprKind::For { body, .. } => self.check_closure_scalar_return_in_block(body, ret, errors), ExprKind::Block(b) => self.check_closure_scalar_return_in_block(b, ret, errors), _ => {} } } /// D246-амендмент ([M-ro-launder-via-mut-binding], Ф.1б, 2026-07-23): /// THIRD position of the norm — RETURN. Mirrors `check_readonly_source_coerce` /// (Let-init) but for a function's own return value: returning a bare /// `Ident` that is L1-ro-bound (bare local `ro x = …` OR a non-`mut` /// param, D176 default, P7 freeze) under a return type that is NOT /// itself `-> ro T` (i.e. mut-default per D184 — the CALLER's binding /// decides) launders the freeze out: the callee hands back the SAME /// object it only had read-only access to, under an implicitly-mut /// contract — `mut w = f()` at the caller then aliases the callee's own /// frozen source. `-> ro T` is exempt (the return itself is frozen /// regardless of what's inside). Scalar-primitive exemption applies /// identically to the other two positions (see `is_bare_scalar_primitive`). fn check_ro_launder_return( &self, value: &Expr, ret: &TypeRef, scope: &HashMap<String, TypeRef>, errors: &mut Vec<Diagnostic>, ) { if ret.is_readonly() { return; } if let ExprKind::Ident(name) = &value.kind { let is_scalar = self.infer_expr_type(value, scope) .map_or(false, |t| is_fully_stack_value(&t, &self.types)); if !is_scalar && self.ro_binding_names.borrow().contains(name) { errors.push(Diagnostic::new( format!( "[E_READONLY_COERCE] возврат `{name}` — источник связан как `ro` \ (L1-binding — явный `ro {name} = ...` либо параметр без `mut`, \ D176-дефолт, P7 freeze), но объявленный тип возврата НЕ `ro` \ (mut-дефолт биндинга у caller'а, D184): `mut w = f()` у caller'а \ дал бы запись, видимую внутреннему источнику `{name}` (D246-\ амендмент, [M-ro-launder-via-mut-binding], Ф.1б). Решения: \ (a) если `{name}` должен быть mut-параметром — in-out по D326-\ ревизии §Р3, сама функция это НЕ решает изнутри для собственного \ параметра; поменяй сигнатуру функции на `mut {name} T`, если это \ правильная семантика; (b) верни явную независимую копию — \ `{name}.clone()` (D230); (c) объяви возврат `-> ro T`, если источник \ действительно должен остаться заморожен и у caller'а." ), value.span, )); } } } /// Traversal companion of `check_ro_launder_return` — walks a block for /// its own trailing tail AND every nested explicit `return X`, mirroring /// `check_closure_scalar_return_in_block`'s structure. fn check_ro_launder_return_in_block( &self, b: &Block, ret: &TypeRef, scope: &HashMap<String, TypeRef>, errors: &mut Vec<Diagnostic>, ) { for s in &b.stmts { self.check_ro_launder_return_in_stmt(s, ret, scope, errors); } if let Some(t) = &b.trailing { self.check_ro_launder_return_in_expr(t, ret, scope, errors); } } fn check_ro_launder_return_in_stmt( &self, s: &Stmt, ret: &TypeRef, scope: &HashMap<String, TypeRef>, errors: &mut Vec<Diagnostic>, ) { match s { Stmt::Return { value: Some(e), .. } => self.check_ro_launder_return(e, ret, scope, errors), Stmt::Expr(e) => self.check_ro_launder_return_in_expr(e, ret, scope, errors), Stmt::Defer { body, .. } => self.check_ro_launder_return_in_expr(body, ret, scope, errors), Stmt::ConsumeScope { body, .. } => self.check_ro_launder_return_in_block(body, ret, scope, errors), _ => {} } } /// Ищет ВЛОЖЕННЫЕ explicit `return X` (зеркалит /// `check_closure_scalar_return_in_expr`'s traversal scope/limits). fn check_ro_launder_return_in_expr( &self, e: &Expr, ret: &TypeRef, scope: &HashMap<String, TypeRef>, errors: &mut Vec<Diagnostic>, ) { match &e.kind { ExprKind::If { then, else_, .. } | ExprKind::IfLet { then, else_, .. } => { self.check_ro_launder_return_in_block(then, ret, scope, errors); if let Some(eb) = else_ { match eb { ElseBranch::Block(b) => self.check_ro_launder_return_in_block(b, ret, scope, errors), ElseBranch::If(x) => self.check_ro_launder_return_in_expr(x, ret, scope, errors), } } } ExprKind::Match { arms, .. } => { for arm in arms { match &arm.body { MatchArmBody::Expr(x) => self.check_ro_launder_return_in_expr(x, ret, scope, errors), MatchArmBody::Block(b) => self.check_ro_launder_return_in_block(b, ret, scope, errors), } } } ExprKind::While { body, .. } | ExprKind::WhileLet { body, .. } | ExprKind::Loop { body, .. } | ExprKind::For { body, .. } => self.check_ro_launder_return_in_block(body, ret, scope, errors), ExprKind::Block(b) => self.check_ro_launder_return_in_block(b, ret, scope, errors), _ => {} } } /// Ф.1: совместимо ли `expr` с типом `expected`? /// /// `expr_gs` — generic-scope места, где написан `expr`; `exp_gs` — /// generic-scope, в котором объявлен `expected` (для arg↔param это /// разные scope: caller vs callee). Числовые литералы полиморфны /// (D44): целый литерал совместим с любым числовым типом. /// Plan 221.1 №286/№143 (окно p-chan): if `obj`'s statically-known type /// is a CONCRETE `ChanReader[T]`/`ChanWriter[T]` (turbofish-declared via /// `Channel[T].new` — see `channel_new_turbofish_elem` — OR a plain /// function-parameter/local annotated `ChanReader[T]`/`ChanWriter[T]` /// directly, which `scope` already carries for free, D91 capability /// types, `docs/guide/channels.md` §"Passing to functions"), returns /// `T`. `None` for anything else — most importantly a bare, untracked /// `Channel.new(cap)` (`generics` empty) — callers MUST treat `None` as /// "fall through to pre-existing legacy/erased behavior", never as an /// error: this whole window is a STRICTLY ADDITIVE extension of the /// checker channel (§0/196) — it only narrows what used to be silently /// erased to `nova_int` (№286/№143), it never rejects code that /// compiled before this window. fn channel_elem_type(&self, obj: &Expr, scope: &HashMap<String, TypeRef>) -> Option<TypeRef> { let rt = self.infer_expr_type(obj, scope)?; match rt { TypeRef::Named { path, mut generics, .. } if generics.len() == 1 => { match path.last().map(String::as_str) { Some("ChanReader") | Some("ChanWriter") => Some(generics.remove(0)), _ => None, } } _ => None, } } /// Plan 221.1 №286 residual gap (window p286): honor the /// `docs/guide/channels.md` promise that `T` is "inferred from the /// first `send`/`recv`" for a BARE `Channel.new(cap)` binding (no /// turbofish, no `ChanWriter[T]`/`ChanReader[T]` annotation) — a /// promise window p-chan (№143/№286's core fix) had explicitly and /// honestly documented as never having held, rather than implement it. /// Measured this window that the gap is not benign: an untyped /// channel's erased `recv()` result still lets `.method()` calls /// through to the legacy name-only codegen fallback, so a co-present /// unrelated type sharing a method name can silently win (repro: /// `docs/plans/repros/p286-bare-channel-erased/bare_len_probe.nv`, /// RED without this pass — `EmbeddedDir.len()` silently picked over /// `Vec.len()`). /// /// Single forward walk of `b`'s TOP-LEVEL statements (no recursion into /// nested blocks/branches — deliberately conservative, matches the /// common linear-code shape the docs promise is written for). Maintains /// its own `sim_scope`, seeded from the real `scope` at block entry and /// advanced for each simple `let`/`ro`/`mut` binding walked so a `send` /// argument declared EARLIER IN THIS SAME BLOCK (`mut v []int = ...; /// v.push(...); tx.send(v)` — the ordinary shape) resolves correctly, /// rather than missing because the real `f1_stmt` walk (which runs /// AFTER this pre-pass) hasn't reached that `let` yet. For every /// untracked `Channel.new` destructure found, tracks its writer name as /// "pending" until either its first `.send`/`.try_send` call is found /// (writes the inferred `T` into `channel_bare_send_elem_hint`, keyed /// by the `Channel.new` call's own `ExprId`) or the block ends (stays /// untracked — identical to pre-p286 behavior, fully additive). fn seed_channel_bare_send_hints(&self, b: &Block, scope: &HashMap<String, TypeRef>) { let mut sim_scope = scope.clone(); let mut pending: Vec<(String, ExprId)> = Vec::new(); for s in &b.stmts { if let Stmt::Let(d) = s { if is_channel_new_call(&d.value) && d.value.id.is_set() && channel_new_turbofish_elem(&d.value).is_none() { let tx_name = match &d.pattern { Pattern::Tuple(pats, _) if pats.len() == 2 => match pats.first() { Some(Pattern::Ident { name, .. }) => Some(name.clone()), _ => None, }, Pattern::Record { fields, .. } => fields.iter().find_map(|f| { if f.name != "tx" { return None; } match &f.pattern { Some(Pattern::Ident { name, .. }) => Some(name.clone()), None => Some(f.name.clone()), _ => None, } }), _ => None, }; if let Some(tx_name) = tx_name { pending.push((tx_name, d.value.id)); } } } if !pending.is_empty() { let call_expr = match s { Stmt::Expr(e) => Some(e), Stmt::Let(d2) => Some(&d2.value), _ => None, }; if let Some(e) = call_expr { if let ExprKind::Call { func, args, .. } = &e.kind { if let ExprKind::Member { obj, name } = &func.kind { if matches!(name.as_str(), "send" | "try_send") { if let ExprKind::Ident(recv_name) = &obj.kind { if let Some(pos) = pending.iter().position(|(n, _)| n == recv_name) { if let Some(arg) = args.first() { if let Some(t) = self.infer_expr_type(arg.expr(), &sim_scope) { let (_, eid) = pending.remove(pos); self.channel_bare_send_elem_hint .borrow_mut() .insert(eid, t); } } } } } } } } } // Best-effort sim_scope advance — only what's needed to resolve // typical `send` arguments, NOT a full scope walk (the real // walk is `f1_stmt`, run right after this pre-pass returns). if let Stmt::Let(d) = s { if let Pattern::Ident { name, .. } = &d.pattern { match d.ty.clone().or_else(|| self.infer_expr_type(&d.value, &sim_scope)) { Some(t) => { sim_scope.insert(name.clone(), t); } None => { sim_scope.remove(name); } } } } } } /// Plan 200 (sql-autoconv) D55 amend entry-point: `assignable_direct` /// (the structural EXACT check, unchanged) PLUS — when direct fails — /// the "obvious single-wrapper coercion" fallback (D55 §Sum/newtype /// auto-wrap): `expected` is a declared sum-with-one-matching-unary- /// variant or a newtype, and `expr`'s own kind unambiguously matches /// exactly ONE candidate's inner type. Exactly one level deep — the /// fallback re-checks candidates via `assignable_direct` only (never /// `assignable` again), so a chain (int→UserId→Wrapper) is NOT /// auto-wrapped; only the single declared wrapper is. fn assignable( &self, expr: &Expr, expected: &TypeRef, expr_gs: &GenericScope, exp_gs: &GenericScope, scope: &HashMap<String, TypeRef>, ) -> Compat { let direct = self.assignable_direct(expr, expected, expr_gs, exp_gs, scope); if let Compat::Bad { .. } = &direct { let lookup = |n: &str| self.types.get(n).map(|td| td.kind.clone()); let candidates = single_wrap_candidates(&lookup, expected); if !candidates.is_empty() { let found_kind = self.wrap_kind_of_expr(expr, scope); if let Some(found_kind) = found_kind { let matches: Vec<&(WrapTarget, TypeRef)> = candidates .iter() .filter(|(_, inner_ty)| { wrap_kind_of(inner_ty, &lookup, 0) == found_kind }) .collect(); // Ambiguous (≥2 candidates match, e.g. an `int` literal // would match BOTH an `I(i64)` and — if it existed — a // second int-family variant) or no match (0) → no // auto-wrap, fall through to the direct verdict. if matches.len() == 1 { let (_, inner_ty) = matches[0]; return self.assignable_direct(expr, inner_ty, expr_gs, exp_gs, scope); } } } } // Plan 214 (D429): `#coerce` fallback — tried AFTER the single-wrapper // fallback above (design note: "single-wrapper проверяется ПЕРВЫМ"). // The two never actually compete at one call site: R11 rejects any // `#coerce` declaration whose (I,O) pair is already covered by the // single-wrapper mechanism, so by construction at most one of the two // ever claims a given pair — this ordering is defensive symmetry with // that invariant, not a live tie-break. R5 (exact > coercion) already // holds structurally: we only reach here after `direct` failed, i.e. // no exact match exists at this position. if let Compat::Bad { .. } = &direct { if let Some(input_name) = self.coerce_expr_input_name(expr, scope) { if let Some(pairs) = self.coerce_pairs.get(&input_name) { let exp_key = coerce_type_key(expected); if pairs.iter().any(|p| p.output_key == exp_key) { return Compat::Ok; } } } } // Plan 214.1 (D429 amend): GENERIC `#coerce` pattern fallback — tried // ONLY after the CONCRETE `coerce_pairs` lookup above misses (design // §2 pseudocode: concrete lookup first, patterns second — the hot // str/[]u8 concrete path never even reaches `named_base_and_args`). if let Compat::Bad { .. } = &direct { if let Some(verdict) = self.generic_coerce_lookup(expr, expected, scope) { return verdict; } } direct } /// Plan 214.1 (D429 amend): GENERIC `#coerce` pattern accept-path. /// `None` — no applicable pattern, caller falls through to `direct`'s /// mismatch. `Some(Compat::Ok)` — EXACTLY one pattern unifies to /// `expected`'s canonical key. `Some(Compat::CoerceConflict{..})` — R3': /// ≥2 patterns unify to the SAME (I,O) pair at this position (see that /// variant's doc for why this can only be caught here, not at decl time). /// /// R13' (anti-self-recursion): a pattern whose `decl_span` equals /// `current_coerce_decl_span` (i.e. we are checking THAT SAME /// declaration's own body) is excluded from candidates — `Json[T] @data() /// -> T => @` must stay an honest type mismatch, not rewrite `@` into /// `@.data()` (infinite recursion). fn generic_coerce_lookup( &self, expr: &Expr, expected: &TypeRef, scope: &HashMap<String, TypeRef>, ) -> Option<Compat> { let input_ty = self.coerce_expr_input_shape(expr, scope)?; let (base, concrete_args) = named_base_and_args(&input_ty)?; let patterns = self.generic_coerce_patterns.get(&base)?; let exp_key = coerce_type_key(expected); let self_span = *self.current_coerce_decl_span.borrow(); let matches: Vec<&GenericCoercePattern> = patterns .iter() .filter(|p| Some(p.decl_span) != self_span) .filter(|p| { unify_coerce_receiver(&p.params, &concrete_args) .map(|bindings| { coerce_type_key(&substitute_coerce_shape(&p.ret_shape, &bindings)) == exp_key }) .unwrap_or(false) }) .collect(); match matches.len() { 0 => None, 1 => Some(Compat::Ok), _ => { let decls: Vec<String> = matches .iter() .map(|p| format!("`{base}[..] @{}()`", p.method_name)) .collect(); Some(Compat::CoerceConflict { msg: format!( "[E_COERCE_DUPLICATE_PAIR] ≥2 generic `#coerce` patterns on `{base}` \ unify to the same pair `{base}[..] → {exp_key}` at this position \ (D429 R3'): {}. At most one `#coerce` may provide a given (I,O) pair \ — remove or rename one of the declarations.", decls.join(", ") ), }) } } } /// Plan 214.1: like `coerce_expr_input_name`, but returns the FULL /// `TypeRef` (generics included) — a generic pattern lookup needs both /// the base name AND the concrete generic args to unify against `params`. /// Same literal fast-path as `coerce_expr_input_name` (a str literal is /// always plain `str`, no generics — irrelevant to any generic pattern, /// but kept for symmetry/consistency with that function). fn coerce_expr_input_shape(&self, expr: &Expr, scope: &HashMap<String, TypeRef>) -> Option<TypeRef> { if matches!(&expr.kind, ExprKind::StrLit(_) | ExprKind::InterpolatedStr { .. }) { return Some(TypeRef::Named { path: vec!["str".to_string()], generics: Vec::new(), span: Span::dummy() }); } self.infer_expr_type(expr, scope) } /// Plan 214 (D429): the concrete type NAME a `#coerce` lookup should use /// for `expr` — the I-side key into `self.coerce_pairs`. Literal string /// forms (`StrLit`/`InterpolatedStr`) are ALWAYS `str`-typed regardless of /// content, so they resolve directly (no inference needed, mirrors /// `wrap_kind_of_expr`'s literal fast-path); anything else goes through /// the real scope-aware `infer_expr_type` (handles `Ident` var lookups, /// method-chain results, etc. — more general than the AST-rewrite pass's /// leaf-only `var_types`, since the accept-path here just needs a yes/no /// verdict, not a rewritable node). fn coerce_expr_input_name(&self, expr: &Expr, scope: &HashMap<String, TypeRef>) -> Option<String> { if matches!(&expr.kind, ExprKind::StrLit(_) | ExprKind::InterpolatedStr { .. }) { return Some("str".to_string()); } let ty = self.infer_expr_type(expr, scope)?; simple_named_type_name(&ty) } /// Plan 200 (sql-autoconv) D55 amend: best-effort `WrapKind` of `expr` — /// used ONLY to disambiguate wrap candidates in `assignable` (never to /// decide plain assignability, which stays `assignable_direct`'s job). /// Literal kinds are read directly off the AST (cheap, exact); anything /// else falls back to `infer_expr_type` (covers `Ident` var references — /// the `${n}` case — plus any other expr whose type is inferable). fn wrap_kind_of_expr(&self, expr: &Expr, scope: &HashMap<String, TypeRef>) -> Option<WrapKind> { match &expr.kind { ExprKind::IntLit(_) => Some(WrapKind::IntFamily), ExprKind::Unary { op: UnOp::Neg, operand } if matches!(operand.kind, ExprKind::IntLit(_)) => { Some(WrapKind::IntFamily) } ExprKind::FloatLit(_) => Some(WrapKind::Float), ExprKind::BoolLit(_) => Some(WrapKind::Bool), ExprKind::StrLit(_) | ExprKind::InterpolatedStr { .. } => Some(WrapKind::Str), _ => { let tr = self.infer_expr_type(expr, scope)?; let lookup = |n: &str| self.types.get(n).map(|td| td.kind.clone()); Some(wrap_kind_of(&tr, &lookup, 0)) } } } /// [M-fn-value-binding-untyped-silent] (реестр 221.1 №101): does `tr` denote /// a func-compatible position — a bare `TypeRef::Func`, a fn-POINTER type /// (`*fn(...)` / `*unsafe fn(...)` / `*uninit fn(...)`, D216/D353 — /// `Pointer(Func{..})`, possibly Uninit-wrapped), OR a `Named` reference to /// a declared fn-newtype/alias chain that ULTIMATELY resolves to `Func` /// (`type Handler fn(ServerRequest) -> str`, D52)? Peels the compile-time- /// only view modifiers first (mirrors `check_closure_scalar_return`'s /// peel). Depth-guarded against a pathological alias cycle (mirrors /// `resolved_cat_of_depth`'s guard) — generics-carrying Named refs are NOT /// peeled (a fn-newtype has none; bail conservatively to `false` rather /// than mis-resolve a parametric unrelated type of the same name). fn typeref_is_func_compatible(&self, tr: &TypeRef) -> bool { self.typeref_is_func_compatible_depth(tr, 0) } fn typeref_is_func_compatible_depth(&self, tr: &TypeRef, depth: u32) -> bool { if depth > 16 { return false; } match tr { TypeRef::Func { .. } => true, TypeRef::Readonly(inner, _) | TypeRef::Mut(inner, _) | TypeRef::Uninit(inner, _) | TypeRef::Ref(inner, _) | TypeRef::Pointer(inner, _) => self.typeref_is_func_compatible_depth(inner, depth + 1), TypeRef::Named { path, generics, .. } if generics.is_empty() => { let Some(n) = path.last() else { return false; }; match self.types.get(n).map(|td| &td.kind) { Some(TypeDeclKind::Newtype(inner)) | Some(TypeDeclKind::Alias(inner)) => { self.typeref_is_func_compatible_depth(inner, depth + 1) } _ => false, } } _ => false, } } /// [M-generic-mono-multi-instantiation-concrete-sibling-collision] /// (реестр 221.1 №105): peel `tr` the SAME way `typeref_is_func_compatible_ /// depth` does (view-wrappers + a declared fn-newtype/alias chain) down to a /// bare `TypeRef::Func`, and return its DECLARED return type (cloned; inner /// `None` = implicit `()`). Outer `None` — `tr` is not (transitively) /// Func-shaped at all — undecidable, caller stays permissive (old /// behavior). Companion to `typeref_is_func_compatible` (bool-only); this /// one needs the actual return-type payload. fn peel_func_return_depth(&self, tr: &TypeRef, depth: u32) -> Option<Option<TypeRef>> { if depth > 16 { return None; } match tr { TypeRef::Func { return_type, .. } => Some(return_type.as_deref().cloned()), TypeRef::Readonly(inner, _) | TypeRef::Mut(inner, _) | TypeRef::Uninit(inner, _) | TypeRef::Ref(inner, _) | TypeRef::Pointer(inner, _) => self.peel_func_return_depth(inner, depth + 1), TypeRef::Named { path, generics, .. } if generics.is_empty() => { let n = path.last()?; match self.types.get(n).map(|td| &td.kind) { Some(TypeDeclKind::Newtype(inner)) | Some(TypeDeclKind::Alias(inner)) => { self.peel_func_return_depth(inner, depth + 1) } _ => None, } } _ => None, } } /// [M-generic-mono-multi-instantiation-concrete-sibling-collision] /// (реестр 221.1 №105): does concrete candidate `f`'s OWN declared /// Func-shaped param(s) genuinely accept the paired arg's ACTUAL return /// type, wherever that return type is statically KNOWN? `assignable` / /// `cat_compatible_rt` collapse EVERY bare `TypeRef::Func` (and, through /// it, every fn-newtype param like `Handler = fn(int) -> str`) to /// `ResolvedType::Any` — a deliberate permissiveness for the closure- /// LITERAL case (D84 comment above: "codegen does the FINAL exact-C-type /// selection"), because a literal's own return type needs a full body-walk /// this layer doesn't do. That SAME escape hatch also swallows a NAMED /// FUNCTION VALUE (or any other non-closure fn-typed expr) whose return /// type IS fully resolvable right here — e.g. `.get(path, handler_int)` /// where `handler_int: fn(int) -> int` against a concrete `Handler = /// fn(int) -> str` sibling param — silently treating a definite /// return-type MISMATCH as "compatible", so the D84 tie-break below /// (caller) wins the concrete overload over the call's actual target (a /// bound-generic sibling with a DIFFERENT `R`) purely because "not a bare /// closure literal" was the only guard. Runtime effect: codegen dispatches /// to the CONCRETE C symbol and bit-reinterprets the wrong-typed return /// value (confirmed live: `probe105_router6c` — `int`/`bool`-returning /// named handlers routed through the `str`-returning concrete mono, empty/ /// garbage `nova_str` output, no compile error). Conservative both ways: /// only a param/arg pair where EITHER side isn't Func-shaped, or the /// return-type category itself is undecidable (`Any`, a generic param, /// …), stays permissive (`true`) — byte-identical for every existing /// shape (closures stay exempt via the caller's own `has_bare_closure_arg` /// gate; the #34 fixture's `wrap_handler(...)` `Call`-expr — a REAL /// `Handler`-typed return — still matches exactly, `str`==`str`). fn concrete_sibling_return_type_ok( &self, f: &FnDecl, args: &[CallArg], scope: &HashMap<String, TypeRef>, ) -> bool { let Ok(bindings) = crate::argbind::bind_call_args(&f.params, args) else { return true; }; for (pi, binding) in bindings.iter().enumerate() { let ai = match binding { crate::argbind::ArgBinding::Positional(i) | crate::argbind::ArgBinding::Named(i) => *i, _ => continue, }; let Some(param) = f.params.get(pi) else { continue; }; if param.is_variadic { continue; } let Some(arg) = args.get(ai) else { continue; }; let Some(param_ret) = self.peel_func_return_depth(¶m.ty, 0) else { continue; }; // [M-105 follow-up] `infer_expr_type` has no general "free-fn CALL // returning a bare fn-VALUE" arm (its `Call` handling targets typed // callee/receiver returns, not this narrower shape) — a factory call // like `make_int_handler()` (`-> fn(int) -> int`) came back `None` // here, silently falling through to the SAME permissive gap this // whole helper exists to close. Fallback: an UNAMBIGUOUS (single- // overload) free-fn callee's OWN declared return type, read directly // off `self.sig` — bypasses `infer_expr_type` entirely, no body-walk. let arg_ty = self.infer_expr_type(arg.expr(), scope).or_else(|| { let ExprKind::Call { func, .. } = &arg.expr().kind else { return None; }; let ExprKind::Ident(fname) = &func.kind else { return None; }; let overloads = self.sig.free_fns(fname)?; match overloads.as_slice() { [only] => only.return_type.clone(), _ => None, } }); let Some(arg_ty) = arg_ty else { continue; }; let Some(arg_ret) = self.peel_func_return_depth(&arg_ty, 0) else { continue; }; let ok = match (¶m_ret, &arg_ret) { (None, None) => true, (Some(p), Some(a)) => cat_compatible_rt( &self.resolved_cat_of(a, &HashMap::new()), &self.resolved_cat_of(p, &HashMap::new()), ), // one side implicit `()`, the other a real value type — definite mismatch. _ => false, }; if !ok { return false; } } true } /// [M-2223-closurefull-generic-overload-resolution] (реестр 221.1 №124): /// peel `tr` the SAME way `peel_func_return_depth`/`typeref_is_func_ /// compatible_depth` do (view-wrappers + a declared fn-newtype/alias /// chain) down to a bare `TypeRef::Func`, returning a CLONE of that /// FULL Func shape (params AND return type — `peel_func_return_depth` /// only keeps the return half). `None` — `tr` is not (transitively) /// Func-shaped at all. fn peel_func_shape_depth(&self, tr: &TypeRef, depth: u32) -> Option<TypeRef> { if depth > 16 { return None; } match tr { TypeRef::Func { .. } => Some(tr.clone()), TypeRef::Readonly(inner, _) | TypeRef::Mut(inner, _) | TypeRef::Uninit(inner, _) | TypeRef::Ref(inner, _) | TypeRef::Pointer(inner, _) => self.peel_func_shape_depth(inner, depth + 1), TypeRef::Named { path, generics, .. } if generics.is_empty() => { let n = path.last()?; match self.types.get(n).map(|td| &td.kind) { Some(TypeDeclKind::Newtype(inner)) | Some(TypeDeclKind::Alias(inner)) => { self.peel_func_shape_depth(inner, depth + 1) } _ => None, } } _ => None, } } /// [M-2223-closurefull-generic-overload-resolution] (реестр 221.1 №124): /// does concrete candidate `f`'s OWN declared Func-shaped param(s), paired /// with a `ClosureFull` LITERAL argument (`fn(x T) -> U { ... }`) at this /// call site, match the closure's own signature EXACTLY? A `ClosureFull` /// literal's grammar mandates every param type AND the return type spelled /// explicitly — unlike `ClosureLight` (`|x| ...`, D84's original exemption /// rationale: its return type needs a full body-walk this layer doesn't /// do), so an exact structural comparison (`typeref_equal`) is decidable /// right here with zero inference. This is the companion `f`-decl-shape /// check to `concrete_sibling_return_type_ok` (#105) — that helper only /// re-derives a RETURN type via `infer_expr_type`, which has NO arm for a /// bare closure literal (`ClosureFull` included) and silently degrades to /// permissive (`None` → `continue`) for one; calling it alone on a /// `ClosureFull` arg would therefore treat ANY signature — matching or /// not — as compatible, the exact silent-miscompile failure mode #105 /// closed for named function values. Reading the literal's OWN AST /// annotations directly (no `infer_expr_type` involved) avoids that gap. /// Permissive (`true`) whenever either side isn't decidable structurally /// (candidate param not Func-shaped, or the paired arg isn't a /// `ClosureFull`) — a generic sibling's OWN legitimate `ClosureFull` call /// site (whose shape does NOT structurally match the unrelated concrete /// sibling's fixed param type) correctly returns `false` here, so /// `concrete_compat` (caller) excludes the concrete candidate and the /// generic path still wins for it. fn closure_args_match_concrete(&self, f: &FnDecl, args: &[CallArg]) -> bool { let Ok(bindings) = crate::argbind::bind_call_args(&f.params, args) else { return true; }; for (pi, binding) in bindings.iter().enumerate() { let ai = match binding { crate::argbind::ArgBinding::Positional(i) | crate::argbind::ArgBinding::Named(i) => *i, _ => continue, }; let Some(param) = f.params.get(pi) else { continue; }; if param.is_variadic { continue; } let Some(arg) = args.get(ai) else { continue; }; let ExprKind::ClosureFull(sb) = &arg.expr().kind else { continue; }; let Some(param_func) = self.peel_func_shape_depth(¶m.ty, 0) else { continue; }; let arg_func = TypeRef::Func { params: sb.params.iter().map(|p| p.ty.clone()).collect(), effects: Vec::new(), return_type: sb.return_type.clone().map(Box::new), extern_abi: None, span: sb.span, }; if !typeref_equal(¶m_func, &arg_func) { return false; } } true } /// [M-2223-generic-arity-overload-applicability] (реестр 221.1 №130): /// single-`(param, arg)` companion to `closure_args_match_concrete` /// (№124) — that helper's `typeref_equal` full-structural check is /// correct ONLY for a CONCRETE candidate (every param type is a real, /// caller-independent type), never for a method-level-GENERIC candidate, /// whose Func-shaped param mentions the callee's OWN typevars (`fn(T1) -> /// R`) — those can never structurally equal a call site's fully-concrete /// closure literal (`fn(int) -> str`), even when arity/binding is /// otherwise exactly right. Called from `overload_applicability` /// (§ U.3.1/U.4.3, shared by free-fn/static-method/instance-method /// overload resolution alike) — BEFORE the `assignable`-based category /// check, which structurally cannot decide this (`TypeRef::Func` expected /// types collapse to `ResolvedType::Any` there, permissive by design). /// /// What IS decidable regardless of genericity: a `ClosureFull` literal's /// grammar mandates every param spelled explicitly, so its own declared /// param COUNT is exact — comparing counts (not types) disambiguates /// generic siblings that differ ONLY in how many params their handler /// closure takes (Router-style extractor-count overloading, Plan 222.3 /// §5's target shape: `@get[R](path, h fn(T1) -> R)` vs `@get[R](path, h /// fn(T1, T2) -> R)`). /// /// Permissive (`true`) whenever either side isn't decidable this way /// (`arg_expr` isn't a `ClosureFull` literal, or `param_ty` isn't /// Func-shaped at all) — mirrors `closure_args_match_concrete`'s own /// permissive fallback, zero blast radius for every other arg/param /// shape. fn closure_arg_arity_ok(&self, param_ty: &TypeRef, arg_expr: &Expr) -> bool { let ExprKind::ClosureFull(sb) = &arg_expr.kind else { return true; }; let Some(param_func) = self.peel_func_shape_depth(param_ty, 0) else { return true; }; let TypeRef::Func { params, .. } = ¶m_func else { return true; }; params.len() == sb.params.len() } fn assignable_direct( &self, expr: &Expr, expected: &TypeRef, expr_gs: &GenericScope, exp_gs: &GenericScope, scope: &HashMap<String, TypeRef>, ) -> Compat { // U.5.2: category через структурный `ResolvedType` (lossless width/sign) — // `resolved_cat_of` зеркалит `cat_of` (alias/Vec/Named/Any), но int-семья несёт // точные (width, signed) вместо `TyCat::Int`-коллапса. let exp_rt = self.resolved_cat_of(expected, exp_gs); // Generic-параметр / any / func / tuple — проверить нельзя. if matches!(exp_rt, ResolvedType::Any) { // [M-checker-protocol-typed-arg-any-bypass] fix (zero-tolerance, backlog- // followups.md): `resolved_cat_of`/`resolved_cat_of_depth` map EVERY protocol // EXPECTED type to `Any` — mirrors legacy `cat_of`'s "protocol/effect/opaque // permissive" collapse, which pre-dates the structural-protocol machinery // (D42/D53/D72/D142) and was never revisited once that machinery existed. That // blanket `Any` used to ALSO skip structural verification entirely for a PLAIN // (non-generic) protocol-typed parameter — `fn f(w Fmt)` — the documented // "type value / existential" surface (`TypeDeclKind::Protocol` doc-comment). // A `[T Bound]` GENERIC bound was NEVER affected by this hole — it is checked // separately by `BoundCtx::check_satisfaction`/`check_satisfaction_against_methods` // via `check_call_bounds`/`check_method_call_bounds` (D53/D72/D142), which never // routes through `resolved_cat_of`. Symptom: `x.debug(sb)` with // `sb: StringBuilder` (implements `Write` only, NOT the wider `Fmt`) type-checked // clean, then silently type-confused the C pointer at runtime (`Nova_StringBuilder*` // where `Nova_FmtCtx*` was expected — both structs happen to start with a pointer // at offset 0, so nothing crashed, it just produced wrong output instead of a // compile error). `protocol_mismatch_found` runs the SAME structural check (method- // table presence + `use`-embed-flatten (D145) + default-body fallback (D183)) the // generic-bound path already uses, narrowly gated to cases it can decide (a Named // reference to a declared `Protocol`, or an inline anonymous `TypeRef::Protocol`, // with an inferable concrete Named/Array/non-primitive/non-generic argument type) // — anything undecidable stays exactly as permissive as before this fix. if let Some(found) = self.protocol_mismatch_found(expr, expected, exp_gs, scope) { return Compat::Bad { found }; } // №45 (221.1) [M-fn-type-expected-any-bypass]: same-shaped narrow re-check, // this time for the fn-typed source of the `Any` collapse (see doc comment // on `fn_type_mismatch_found`). if let Some(found) = self.fn_type_mismatch_found(expr, expected, expr_gs, exp_gs, scope) { return Compat::Bad { found }; } return Compat::Ok; } // Литералы: тип адаптируется к контексту (D44). match &expr.kind { // Plan 200 (sql-autoconv) D55 amend: `[a, b, …]` against `[]T` / // `Vec[T]` — coerce EACH element independently (recursing through // the full `assignable`, so a heterogeneous literal like // `[1, "alice", true]` against `[]SqlValue` wrap-coerces every // element on its own — this is what unblocks bare `${1}`/`${n}` // tag-template interpolation: the parser desugars `${…}` into // exactly this shape, an `ArrayLit` argument at a known `[]T` // call-arg position). Only when every element is a plain `Item` // (no spreads) — a spread's source is itself a whole `[]T`, not // a per-element value, so it stays on the legacy `infer_expr_type` // path below (unaffected by this arm). ExprKind::ArrayLit(items) if !items.is_empty() && items.iter().all(|it| matches!(it, ArrayElem::Item(_))) => { if let Some(elem_ty) = array_elem_type(expected) { for it in items { let ArrayElem::Item(item_expr) = it else { unreachable!() }; match self.assignable(item_expr, elem_ty, expr_gs, exp_gs, scope) { Compat::Ok => {} Compat::Bad { found } => { return Compat::Bad { found: format!("[]{found}") }; } other => return other, } } return Compat::Ok; } } // №373 (p-diag, 2026-08-08, `[M-ctor-payload-lit-out-of-range-no-diag]`): // `ro a Option[u8] = Some(300)` used to type-check silently and wrap // (300 -> 44), asymmetric with the DIRECT `ro a u8 = 300` position two // arms below, which correctly hard-errors `[E_LIT_OUT_OF_RANGE]`. // Root: `materialize_literal_coercion`'s OWN ctor arm (same file, // `Some`/`Ok`/`Err` against `Option[T]`/`Result[T,E]`) already threads // the literal's TYPE through this exact position — the literal really // IS typed `u8` here — it just never VALIDATED the range while doing // so; `assignable_direct` had no ctor-payload arm at all, so the // range-check two arms below was simply never reached for this shape. // Recurses the FULL `assignable` (reusing that exact same range-check // logic, hex-reinterpret and all — no duplicated arithmetic) ONLY on a // syntactically bare literal payload, and ONLY acts on its // `Compat::OutOfRange` result — any other outcome (in-range literal, // non-scalar elem, non-ctor call, a variable/expr payload) falls // through unchanged to the pre-existing structural check below, so // this cannot narrow what already type-checks, only add the missing // diagnostic for a shape that used to pass silently. ExprKind::Call { func, args, .. } if args.len() == 1 => { if let ExprKind::Ident(ctor) = &func.kind { if let Some(elem) = ctor_payload_expected(ctor, expected) { let payload = args[0].expr(); let is_bare_lit = matches!(&payload.kind, ExprKind::IntLit(_)) || matches!(&payload.kind, ExprKind::Unary { op: UnOp::Neg, operand } if matches!(operand.kind, ExprKind::IntLit(_))); if is_bare_lit { if let Compat::OutOfRange { msg } = self.assignable(payload, elem, expr_gs, exp_gs, scope) { return Compat::OutOfRange { msg }; } } } } } ExprKind::IntLit(v) => { return match &exp_rt { ResolvedType::Scalar { .. } => { // Plan 142 (D227 Rule 3): context-coercion целого // литерала к sized-типу — hard range-check. Default // `int`/`uint` (wide) → sized_int_name = None → Ok. // Имя для range-check берём из DIRECT-типа // (`from_type_ref`, БЕЗ alias-резолва) — byte-identical // c legacy `sized_int_name(expected)` (alias → None, // `uint` ≠ `u64`; `wide_default` несёт это различие). if let Some(name) = ResolvedType::from_type_ref(expected).sized_int_name() { // Hex literals > i64::MAX are stored as wrapping // i64 by the lexer (spec: bit-identical). For // unsigned target types, reinterpret as u64 so // 0xCBF29CE484222325 (FNV offset etc.) is valid. let v128 = if matches!(name.as_str(), "u8"|"u16"|"u32"|"u64") && *v < 0 { (*v as u64) as i128 } else { *v as i128 }; if let Some(msg) = lit_range_check(v128, &name) { return Compat::OutOfRange { msg }; } } Compat::Ok } ResolvedType::Float { .. } => Compat::Ok, _ => Compat::Bad { found: "int".to_string() }, }; } // Plan 142 (D227 Rule 6): unary-minus над целым литералом — // `-1`, `-200` etc. `-1` это Unary{Neg, IntLit(1)}, не часть // литерала, поэтому проверяем здесь (negative-in-unsigned → // hard error; negative вне i-диапазона → тоже range-check). ExprKind::Unary { op: UnOp::Neg, operand } if matches!(operand.kind, ExprKind::IntLit(_)) => { let ExprKind::IntLit(v) = operand.kind else { unreachable!() }; return match &exp_rt { ResolvedType::Scalar { .. } => { let exp_direct = ResolvedType::from_type_ref(expected); // i128 negation — без overflow даже для i64::MIN. let neg = -(v as i128); // D227 amend (Rule 6, fixed 2026-06-20; spec 03-syntax.md; regress // detect172/d227): a NEGATIVE literal into ANY unsigned target is a // sign-domain error. The wide-default skip (D227 Rule 1) relaxes only // the UPPER bound — never the floor 0. Keyed on the GENERAL // `signed == false` property (not a `uint` special-case), this closed // the `uint = -1` hole vs `u64 = -1`; byte-identical for sized unsigned // (same `< T.MIN (0)` message). NOT an open marker — bug is fixed. if matches!(exp_direct, ResolvedType::Scalar { signed: false, .. }) && neg < 0 { if let Some(name) = exp_direct.int_name() { return Compat::OutOfRange { msg: format!("{neg} < {name}.MIN (0)"), }; } } if let Some(name) = exp_direct.sized_int_name() { if let Some(msg) = lit_range_check(neg, &name) { return Compat::OutOfRange { msg }; } } Compat::Ok } ResolvedType::Float { .. } => Compat::Ok, _ => Compat::Bad { found: "int".to_string() }, }; } ExprKind::FloatLit(_) => { return match &exp_rt { ResolvedType::Float { .. } => Compat::Ok, _ => Compat::Bad { found: "f64".to_string() }, }; } ExprKind::BoolLit(_) => { return if matches!(exp_rt, ResolvedType::Bool) { Compat::Ok } else { Compat::Bad { found: "bool".to_string() } }; } // [M-d55-str-literal-coercion-name-gated] fix (2026-07-17, generalized): // D55 amend (spec/decisions/02-types.md §Str-литерал→[]u8) — a bare // str-LITERAL at an expected `[]u8` position coerces (compile-time-known // UTF-8 bytes, zero-copy — the SAME "obvious literal" carve-out as the // numeric/single-wrapper literal coercions elsewhere in this fn). This is // the type-directed ACCEPT-side half; the previous implementation gated // entirely on the method being literally named `write` — a §3 name-keyed // anti-pattern that ALSO left a checker asymmetry: a protocol-erased // receiver (`w Fmt`) type-checked (permissive `overload_applicability` // skip) while a CONCRETE receiver (`sb StringBuilder`) hit // `[E_NO_MATCHING_OVERLOAD]` for the identical `w.write("lit")` shape. // Reached from EVERY position `assignable`/`assignable_direct` already // gates — call-arg (any method/free-fn, `overload_applicability`/ // `f1_check_call`), let/const annotation (`f1_check_assign_let`), and // array-element (the `ArrayLit` arm above, which recurses element-wise // through `assignable`) — no separate per-position wiring needed. Return // position is NOT checked via `assignable` at all (documented gap, // unrelated to this fix — see the `materialize_returns_in_*` comments). // `ExprKind::Ident` (a str VARIABLE) never reaches this arm — D176 still // requires an explicit `.bytes()` for a non-literal str value. ExprKind::StrLit(_) => { if matches!(exp_rt, ResolvedType::Str) { return Compat::Ok; } if is_bytes_slice_rt(&exp_rt) { return Compat::Ok; } return Compat::Bad { found: "str".to_string() }; } ExprKind::InterpolatedStr { .. } => { return if matches!(exp_rt, ResolvedType::Str) { Compat::Ok } else { Compat::Bad { found: "str".to_string() } }; } ExprKind::CharLit(_) => { // `char` riding as `Named { name: "char", .. }` — mirrors legacy `TyCat::Char`. return if matches!(&exp_rt, ResolvedType::Named { name, .. } if name == "char") { Compat::Ok } else { Compat::Bad { found: "char".to_string() } }; } ExprKind::UnitLit => { return if matches!(exp_rt, ResolvedType::Unit) { Compat::Ok } else { Compat::Bad { found: "()".to_string() } }; } // Plan 134: null ptr literal — only assignable к *() or pointer types // (ResolvedType::Ptr). Mismatch flagged as *()-not-X. ExprKind::NullPtrLit => { return if matches!(exp_rt, ResolvedType::Ptr) { Compat::Ok } else { Compat::Bad { found: "*()".to_string() } }; } _ => {} } // [M-208-fmtkind-bare-variant-shadow-v2] (2026-07-17, regression follow-up): // universal bare-enum-variant disambiguation, applied HERE — the ONE // canonical choke point every call shape (free fn, method, static ctor // `Type.new(...)`, return, let-init, match-arm) funnels through for its // compatibility check — instead of only at the specific call-arg-binding // site that `materialize_literal_coercion` patches. That earlier fix // ([M-208-fmtkind-bare-variant-shadow], same date) covered ordinary // free-function calls (`nth_sunday_epoch_day(y, Oct)`) but a regression // report showed `Date.new(2026, Oct, 4)` (a STATIC method/ctor call — // different callee-resolution path, never reaches that materialize call // site) still mis-resolved `Oct` to `FmtKind.Oct` — proving per-call-site // coverage is fundamentally incomplete; fixing the ONE shared checkpoint // both paths already converge on removes the whack-a-mole entirely. // // Root cause recap: `infer_expr_type`'s bare-variant "last resort" // fallback ([M-hashmap-order-bare-variant-flake], 2026-07-13) picks among // ALL sum types sharing a variant NAME via a context-free // lexicographic-smallest-typename tie-break — deterministic, but blind to // the actual call-site `expected` type, so it is deterministically WRONG // whenever the alphabetically-smaller candidate isn't the one the // position actually wants ("FmtKind" < "Month", both declare `Oct`). // `expected` — RIGHT HERE, as this function's own parameter — is exactly // the missing signal. If it names a concrete (non-generic) Sum type that // owns a unit-variant spelled exactly like this bare Ident, the value // denotes THAT variant unambiguously, full stop — no tie-break needed, // no dependence on how many other files/types happen to be in this // compile-unit or in what order they got registered into `self.types`. // Also seed `resolved_types_buf` so codegen's OWN later lookup (which // reads that cache FIRST, before ever re-deriving via `infer_expr_type`) // sees the same correct answer — a passing check alone, without this, // would just move the wrong-type bug from a compile error to a silent // runtime type-confusion. if let ExprKind::Ident(name) = &expr.kind { if !scope.contains_key(name) { if let TypeRef::Named { path, generics, .. } = expected { if generics.is_empty() { if let Some(tname) = path.last() { if let Some(td) = self.types.get(tname) { if td.generics.is_empty() { if let TypeDeclKind::Sum(variants) = &td.kind { if variants.iter().any(|v| &v.name == name) { if expr.id.is_set() { let rt = ResolvedType::from_type_ref(expected); self.resolved_types_buf.borrow_mut().insert(expr.id, rt); } return Compat::Ok; } } } } } } } } } // Не-литерал: вывести тип; не вышло → Unknown (skip, не ошибка). let Some(found_tr) = self.infer_expr_type(expr, scope) else { return Compat::Unknown; }; // [M-fn-value-binding-untyped-silent] (реестр 221.1 №101): the Func- // mismatch check that used to live HERE was REVERTED — folded into the // shared `assignable_direct` (used by every call-argument/let/return // check in the whole codebase), `infer_expr_type`'s Plan-228 fn_decls // fallback resolved an out-of-scope bare name that coincidentally also // names a free fn ELSEWHERE in the huge merged compile unit to that // unrelated fn's `Func` shape — previously harmless (collapsed to `Any` // downstream), this turned a pre-existing, unrelated scope-threading gap // into a false-positive `[E7301]` (repro: `offset`, a std/fs-family local // colliding with an unrelated conformance helper `fn offset(int,int,int)`). // The narrow, call-site-scoped replacement is `check_fn_value_mismatch` // (near `check_closure_scalar_return`) — invoked ONLY from the two // positions this window actually needs (an annotated `let`, a fn-value // call argument), not from every `assignable` call site in the codebase. // Plan 125.1 (Ф.1) — never-subtype-of-T per spec D25: `never` assignable // to any expected type (bottom type). `from_type_ref` mirrors `ty_of_ref` // (both map `never` → `Never`, neither resolves aliases). // U.5.5(a): peel the L2 `readonly` view so `readonly never` is still bottom. if matches!(ResolvedType::from_type_ref(&found_tr).peel_view(), ResolvedType::Never) { return Compat::Ok; } let found_rt = self.resolved_cat_of(&found_tr, expr_gs); // [M-scalar-nonliteral-narrowing-not-enforced] (D54): a NON-LITERAL int // value coerced into a narrower / value-range-unsafe int position must // use an explicit `as`. U.5.2: narrowing decided on the DIRECT // (`from_type_ref`) types via `would_narrow_into` — the SINGLE source that // folds the former raw-TypeRef `is_int_narrowing` second pass. Direct-only // width (no alias resolve) preserves the legacy `int_width_rank` semantics // byte-identically; widening stays implicit. if ResolvedType::from_type_ref(&found_tr) .would_narrow_into(&ResolvedType::from_type_ref(expected)) { return Compat::Narrowing { from: typeref_display(&found_tr), to: typeref_display(expected), }; } if cat_compatible_rt(&found_rt, &exp_rt) { Compat::Ok } else { Compat::Bad { found: typeref_display(&found_tr) } } } /// [M-checker-protocol-typed-arg-any-bypass] fix: `expected` (already found to /// resolve to `ResolvedType::Any` by the caller — `assignable_direct`'s check right /// above this call) may denote a PROTOCOL — either a `TypeRef::Named` naming a /// declared `type X protocol { ... }`, or an inline anonymous `TypeRef::Protocol /// { methods, .. }` (D142, e.g. `fn f(x protocol { @close() -> () })`). If so, /// best-effort check whether `expr`'s INFERRED type structurally satisfies it — the /// SAME rule the generic-bound path already enforces /// (`BoundCtx::check_satisfaction_against_methods`, D53/D72/D142): every required /// method (name + arity) must be present, directly or via a `default_body` fallback /// (D183), transitively through `use`-embeds (D145). /// /// Returns `None` when undecidable (`expected` does not denote a protocol at all, the /// arg's type is not inferable to a concrete nominal Named/Array type, the concrete /// name is a passthrough generic-param of the ENCLOSING scope, or a primitive with no /// `method_table` registry — mirrors `check_satisfaction`'s own skips) — the caller /// keeps the OLD permissive `Compat::Ok` in every such case, so this fix touches /// ONLY the case it can actually decide. `Some(msg)` is a DEFINITE structural /// mismatch, formatted for the `Compat::Bad { found }` slot callers already build /// `[E7301]`/`[E_NO_MATCHING_OVERLOAD]` diagnostics around (this fn intentionally does /// NOT invent a new error code — every existing `Compat::Bad` call site already /// produces a clean, well-tested compile error from `found`). fn protocol_mismatch_found( &self, expr: &Expr, expected: &TypeRef, exp_gs: &GenericScope, scope: &HashMap<String, TypeRef>, ) -> Option<String> { // Peel the same transparent view-wrappers `resolved_cat_of_depth` recurses // through en route to its Protocol/Any arms — `ro`/`mut`/`uninit`/`ref` views of a // protocol-typed position collapse to Any exactly like the bare form there, so the // structural check must see through them here too. let mut peeled = expected; loop { peeled = match peeled { TypeRef::Readonly(inner, _) | TypeRef::Mut(inner, _) | TypeRef::Uninit(inner, _) | TypeRef::Ref(inner, _) => inner.as_ref(), _ => break, }; } enum Req<'x> { // [M-fmt-write-protocol-collision-cycle-adjacent]: carries the // REFERRING TypeRef's own `span.file_id` alongside the bare name — // `protocol_missing_methods` below needs it to re-resolve the SAME // (not a same-named collision from a different module) protocol // decl when walking `use`-embeds / missing-method lists. Named(String, crate::diag::FileId), Anon(&'x [EffectMethod]), } let req = match peeled { TypeRef::Named { path, span, .. } => { let name = path.last()?; // A generic type-param of the ENCLOSING position — `resolved_cat_of_depth` // checks this FIRST too (before ever consulting `self.types`), so a name // shadowing both a real protocol and a local type-param always resolves as // the type-param there; mirror that precedence here. if exp_gs.contains_key(name) { return None; } // [M-fmt-write-protocol-collision-cycle-adjacent] (2026-07-21): // `self.types.get(name)` alone is the CU-wide last-write-wins slot // (see `file_local_types`'s doc) — TWO different modules declaring // their own same-named `export type X protocol` (e.g. `std.io.Write` // vs `std.prelude.protocols.Write`) collapse onto ONE winner there, // decided by transitive-import merge order (an import cycle // elsewhere in the graph can flip it with zero change at either // declaration). `types_get_for_file` disambiguates using THIS // TypeRef's OWN declaring file (`span.file_id` — the file that // actually WROTE `sink Write`, e.g. `protocols.nv` itself, which // always has its OWN `Write` in the per-file overlay) before // falling back to the CU-wide slot for the (common) non-colliding // case — order-independent, no fmt_buf-specific branch. let td = self.types_get_for_file(name, span.file_id)?; if !matches!(td.kind, TypeDeclKind::Protocol { .. }) { return None; // effect/type-set/opaque/etc. — different Any-source, untouched } Req::Named(name.clone(), span.file_id) } TypeRef::Protocol { methods, .. } => Req::Anon(methods), _ => return None, // Any/never/Self, func, tuple, unresolved name, generic-param, ... }; let found_tr = self.infer_expr_type(expr, scope)?; let concrete_name = match &found_tr { TypeRef::Named { path, .. } => path.last()?.clone(), // D239 `[]T ≡ Vec[T]` — align with `check_satisfaction_against_methods`'s own // Array-as-Vec receiver treatment (checks Vec's method_table). TypeRef::Array(_, _) => "Vec".to_string(), _ => return None, // Tuple/Func/etc. — composite arg types, undecidable here }; // Passthrough type-param (the arg's OWN type is a generic param of the enclosing // fn, still erased at this call site) — undecidable, enforced at the eventual // concrete call site instead (mirrors `check_satisfaction`'s // `current_fn_gs` skip in `BoundCtx` — Plan 176 Ф.1 note: THIS skip is // for the `Any`-typed-position channel, a different mechanism, and was // left unchanged by that fix — out of its scope). if exp_gs.contains_key(&concrete_name) { return None; } // Built-in primitives have no `method_table` registry here (their real methods // live in codegen's `ExternalRegistry`) — best-effort permissive, mirrors // `check_satisfaction`/`check_satisfaction_against_methods`'s identical skip. if matches!(concrete_name.as_str(), "int" | "i8" | "i16" | "i32" | "i64" | "u8" | "u16" | "u32" | "u64" | "uint" | "f32" | "f64" | "bool" | "char" | "str" | "any" | "never") { return None; } // The "concrete" name is ITSELF a declared Protocol — the arg is an already- // erased existential value (D142 `protocol Name { ... }` literal, or simply a // protocol-typed variable/param forwarded to another protocol-typed position), // NOT a nominal type with a `method_table` entry. A `protocol Writer4 { write(v) // {...} }` literal's inline methods are captured into a per-literal vtable at // construction (`emit_protocol_lit`) — validated THERE against every required // method (see the D142 literal-completeness check) — they are never registered // under the type name "Writer4" in `sig.method_table`, so `method_overloads` // would (wrongly) report every method missing. Same-protocol forwarding // (`w Writer4` flowing into another `Writer4`-expected position) trivially // satisfies by identity; a DIFFERENT protocol name would need protocol-to- // protocol subset checking, out of scope here — skip (permissive, unchanged // from before this fix) rather than false-positive. if self.types.get(&concrete_name) .map(|td| matches!(td.kind, TypeDeclKind::Protocol { .. })) .unwrap_or(false) { return None; } let missing = match &req { Req::Named(proto_name, use_file_id) => { let mut seen: HashSet<String> = HashSet::new(); self.protocol_missing_methods(&concrete_name, proto_name, *use_file_id, &mut seen) } Req::Anon(methods) => self.protocol_required_missing(&concrete_name, methods), }; if missing.is_empty() { return None; } let proto_display = match &req { Req::Named(n, _) => n.clone(), Req::Anon(_) => "<anonymous protocol>".to_string(), }; Some(format!( "{} (does not satisfy `{}`; missing: {})", concrete_name, proto_display, missing.join(", ") )) } /// №45 (221.1 bug-sweep) `[M-fn-type-expected-any-bypass]`: `resolved_cat_of_depth` /// maps EVERY fn-typed EXPECTED position to `Any` (`TypeRef::Tuple(_) | /// TypeRef::Func { .. } => R::Any` — a deliberate, general category collapse, not /// something to touch broadly here) — so `assignable_direct`'s `Any` early-return /// used to accept ANY value at a `fn(...)-> ...`-typed position with ZERO structural /// verification of the callee's actual signature: `fn(int) -> str` could be handed /// where `fn(Req) -> Resp` was expected and the checker stayed silent (found live on /// real code — №50: `handle_connection(stream, Router)` called where the parameter /// had been re-typed to `fn([]u8) -> ServerResponse`, only caught by the C compiler). /// Mirrors `protocol_mismatch_found` immediately above: a SEPARATE, narrowly-gated /// re-check for the ONE Any-source `resolved_cat_of_depth` collapses without /// verifying, not a change to the shared category collapse itself (which other /// callers legitimately rely on staying permissive for erased/generic fn-types). /// Conservative like its sibling: `None` (permissive, unchanged behavior) whenever /// EITHER side is not a decidable, concrete `TypeRef::Func` — an erased/generic /// fn-type, a fn-newtype, a closure-light literal whose type doesn't resolve here, /// etc. all stay exactly as permissive as before this fix. Only fires on a param- /// count mismatch or a per-param/return category mismatch that `cat_compatible_rt` /// (the same permissive category-compat predicate `assignable` uses everywhere else) /// confidently reports as incompatible — nested fn/tuple/generic params legitimately /// collapse to `Any` on BOTH sides via the SAME `resolved_cat_of` and stay permissive /// (`cat_compatible_rt`'s `(Any, _) | (_, Any) => true` arm), so this cannot regress /// any currently-accepted erased-generic fn-type call site. fn fn_type_mismatch_found( &self, expr: &Expr, expected: &TypeRef, expr_gs: &GenericScope, exp_gs: &GenericScope, scope: &HashMap<String, TypeRef>, ) -> Option<String> { fn peel(mut t: &TypeRef) -> &TypeRef { loop { t = match t { TypeRef::Readonly(inner, _) | TypeRef::Mut(inner, _) | TypeRef::Uninit(inner, _) | TypeRef::Ref(inner, _) => inner.as_ref(), _ => return t, }; } } let TypeRef::Func { params: exp_params, return_type: exp_ret, .. } = peel(expected) else { return None; // Tuple/other Any-source — untouched, unrelated to this fix }; let found_tr = self.infer_expr_type(expr, scope)?; let found_peeled = peel(&found_tr); let TypeRef::Func { params: found_params, return_type: found_ret, .. } = found_peeled else { return None; // arg's own type isn't a decidable fn-type here — undecidable, permissive }; if exp_params.len() != found_params.len() { return Some(Self::typeref_display(found_peeled)); } for (ep, fp) in exp_params.iter().zip(found_params.iter()) { let ecat = self.resolved_cat_of(ep, exp_gs); let fcat = self.resolved_cat_of(fp, expr_gs); if !cat_compatible_rt(&fcat, &ecat) { return Some(Self::typeref_display(found_peeled)); } } match (exp_ret.as_deref(), found_ret.as_deref()) { (Some(er), Some(fr)) => { let ecat = self.resolved_cat_of(er, exp_gs); let fcat = self.resolved_cat_of(fr, expr_gs); if !cat_compatible_rt(&fcat, &ecat) { return Some(Self::typeref_display(found_peeled)); } } (None, None) => {} // unit-returning vs value-returning — a genuine, decidable mismatch. _ => return Some(Self::typeref_display(found_peeled)), } None } /// [M-checker-protocol-typed-arg-any-bypass] fix: does `type_name` provide every /// method `required` lists (name + arity — `method_overloads` reads the base /// `sig.method_table` ∪ this `TypeCheckCtx`'s own synth/auto-derive overlay, U.2.3.3)? /// A method with a `default_body` (D183) counts as satisfied even when `type_name` /// has no override — same rule as `BoundCtx::check_satisfaction_against_methods`. /// Returns the list of missing method signatures (empty ⇒ satisfied). fn protocol_required_missing(&self, type_name: &str, required: &[EffectMethod]) -> Vec<String> { let mut missing = Vec::new(); for req in required { let found = self .method_overloads(type_name, &req.name) .map(|fns| fns.iter().any(|f| f.params.len() == req.params.len())) .unwrap_or(false); if !found { if req.default_body.is_some() { continue; } let sig = render_method_sig(&req.name, &req.params, &req.return_type); let prefix = if req.is_static { "." } else { "" }; missing.push(format!("{}{}", prefix, sig)); } } missing } /// [M-checker-protocol-typed-arg-any-bypass] fix: `protocol_required_missing` for a /// NAMED protocol, transitively flattened through `use`-embeds (D145) — mirrors the /// `flatten_dfs` DFS `BoundCtx::build` runs to populate `protocol_specs`, re-walked /// here because `TypeCheckCtx` has no precomputed flattened registry of its own (a /// different struct, built in a different phase — see the type's own doc-comment). /// `seen` DFS-guards embed cycles (diagnosed separately, `E_PROTOCOL_EMBED_CYCLE`) — /// a cycle just stops contributing further methods, never infinite-loops. fn protocol_missing_methods( &self, type_name: &str, proto_name: &str, // [M-fmt-write-protocol-collision-cycle-adjacent] (2026-07-21): the // declaring file_id of the REFERENCING TypeRef (threaded from // `protocol_mismatch_found`'s `Req::Named` for the TOP-level call; // from THIS fn's own resolved `td.span.file_id` for the recursive // embed calls below) — resolves `proto_name` via `types_get_for_file` // instead of the CU-wide last-write-wins `self.types`, so a // same-named protocol collision from an unrelated module (e.g. // `std.io.Write` vs `std.prelude.protocols.Write`) cannot substitute // the WRONG decl's method list here. use_file_id: crate::diag::FileId, seen: &mut HashSet<String>, ) -> Vec<String> { if !seen.insert(proto_name.to_string()) { return Vec::new(); } let Some(td) = self.types_get_for_file(proto_name, use_file_id) else { return Vec::new(); }; let TypeDeclKind::Protocol { methods, embeds } = &td.kind else { return Vec::new(); }; let mut missing = self.protocol_required_missing(type_name, methods); // An embed clause (`use Write` inside `Fmt`) is textually written // INSIDE `Fmt`'s own declaring file — NOT necessarily the file of // whatever call site originally referenced `Fmt` by name (e.g. a // `fn D374Pair @display(mut f Fmt)` signature written in a THIRD // file that neither declares `Fmt` nor `Write` itself). Re-anchor to // `td.span.file_id` (this protocol's OWN file) before recursing, so // the embed name resolves relative to where it's actually written, // not relative to the top-level caller's file (which may have no // local override for the embed name at all, silently falling back to // the very CU-wide collision slot this fix exists to avoid). for e in embeds { if let TypeRef::Named { path, .. } = e { if let Some(emb_name) = path.last() { missing.extend( self.protocol_missing_methods(type_name, emb_name, td.span.file_id, seen), ); } } } missing } /// 172.1.2 Шаг 1: пометить residual generic-параметры ЯВНЫМ носителем. /// `Named{name, module:[], args:[]}` с name ∈ params → `TypeParam(name)`; /// рекурсивно по args/Tuple/Array/TypedPtr/Readonly/Func. Идемпотентно. fn mark_type_params(rt: ResolvedType, params: &impl GenericNameSet) -> ResolvedType { use ResolvedType as R; match rt { R::Named { name, module, args } => { if module.is_empty() && args.is_empty() && params.has_generic_name(&name) { R::TypeParam(name) } else { R::Named { name, module, args: args.into_iter().map(|a| Self::mark_type_params(a, params)).collect(), } } } R::Array(inner) => R::Array(Box::new(Self::mark_type_params(*inner, params))), // [M-fixed-array-value-semantics]: recurse into `[N]T`'s element like `Array` // (was silently caught by the `other => other` wildcard below — a real gap for // `type Foo[T] { arr [4]T }`'s `T`, now closed). R::FixedArray(n, inner) => R::FixedArray(n, Box::new(Self::mark_type_params(*inner, params))), R::Tuple(items) => { R::Tuple(items.into_iter().map(|i| Self::mark_type_params(i, params)).collect()) } R::TypedPtr(m, inner) => R::TypedPtr(m, Box::new(Self::mark_type_params(*inner, params))), R::Readonly(inner) => R::Readonly(Box::new(Self::mark_type_params(*inner, params))), R::Func { params: ps, ret, effects } => R::Func { params: ps.into_iter().map(|p| Self::mark_type_params(p, params)).collect(), ret: Box::new(Self::mark_type_params(*ret, params)), effects, }, other => other, } } } /// [№TBD, реестр 221.1 №403] `impl ResolvedType` block for `resolved_to_typeref` — see /// its own doc for why this lives here (split out of the single giant `impl<'a> /// TypeCheckCtx<'a>` block above/below) instead of alongside `from_type_ref` up at line /// ~207: keeping it textually adjacent to its OLD call sites (all still `Self:: /// resolved_to_typeref(...)` inside `TypeCheckCtx`, now resolved via the one-line delegate /// right after this block) made the diff minimal and easy to audit — Rust does not require /// a type's `impl` blocks to be contiguous or singular. impl ResolvedType { /// Plan 172.1 §0a helper: convert a ResolvedType back to a TypeRef for use as the /// return value of `infer_expr_type`. This is a best-effort conversion — Ptr / Any / /// TypedPtr / Func → None (uncommon in field / return-type positions; not needed for /// the Coalesce/Binary/If consumer chain). Scalar and Named → concrete TypeRef. /// /// [№TBD, реестр 221.1, окно p403-linux-runfail] Moved from a `TypeCheckCtx`-private /// method to `impl ResolvedType` (mirrors `from_type_ref`'s own placement/naming /// symmetry — this is its inverse) so `codegen/emit_c.rs`'s `resolved_type_to_typeref_ /// named` can reuse this FULL primitive/composite coverage (Scalar/Bool/Float/Str/ /// Named-with-generics/Array/Tuple/TypedPtr) instead of its own much narrower /// Named/Func/Unit/Readonly-only base case. That narrower base case silently dropped /// an entire `ResolvedType::Func{..}` conversion the moment EITHER its params OR its /// return type was a bare primitive (`R::Scalar`/`R::Bool`/`R::Float`/`R::Str` — the /// overwhelmingly common case for a closure's return type) — the `?`-propagation on /// the recursive per-param/per-return call turned one missing primitive arm into a /// `None` for the WHOLE surrounding `Func`, discarding an otherwise fully-resolved /// signature. Concretely: a `ro f fn(Req) -> int = |r| { ... }` closure literal (`Req` /// a >16-byte value-record past the auto-by-ref threshold, Plan 172.14) — the checker /// DOES register `resolved_types[closure_id] = Func{params:[Named{Req}], ret:Scalar{ /// int64}}` (`f1_check_assign_let`, unconditionally, for ANY arity) — but `closure_ /// channel_param_tys` (emit_c.rs) peels it via `resolved_type_to_typeref_named`, whose /// OLD `_ => None` catch-all bailed on the `int` return the instant it recursed into /// it, so the closure's OWN C signature fell all the way to the hardcoded /// `nova_int`-per-param bootstrap default while the CALL SITE (driven by a completely /// separate, already-correct computation, the `Stmt::Let` `fn_param_sigs` registration /// a few thousand lines away in emit_c.rs, which already knew to call `type_ref_to_c` /// on the let's OWN annotation) built the correct `NovaValue_Req`-by-value cast — a /// caller/callee C-signature MISMATCH (`nova_lambda_N_body(void*, nova_int)` declared, /// `nova_bool(*)(void*, NovaValue_Req)` called through). Both x86-64 ABIs happen to /// pass an oversized-struct differently (SysV: MEMORY-class push-to-stack; Microsoft /// x64: always by hidden reference) — the SAME wrong C therefore silently reads /// garbage register/stack content as if it were the intended arg on BOTH platforms /// (confirmed by a byte-identical repro on Windows, wrong VALUE, no crash — `nova /// build`), but only SysV's stack-vs-register split reliably corrupts far enough to /// SEGV downstream (`spec_tests/conformance/standalone/m2217_26_generic_static_method_ /// value_arg_addr_mismatch`'s `via_closure` test, Linux-only RUN-FAIL after its first /// two PASS lines — 221.1 №403). This function's OWN internal recursive calls (`Self:: /// resolved_to_typeref` → now `Self::resolved_to_typeref`, unchanged spelling, just a /// different enclosing `impl` block) are untouched, so every EXISTING caller inside /// `TypeCheckCtx` (now a one-line delegate, see below) keeps its exact prior behavior. pub fn resolved_to_typeref(rt: &ResolvedType, span: Span) -> Option<TypeRef> { use ResolvedType as R; Some(match rt { // 172.1.2 Шаг 1: residual параметр без subst-контекста невосстановим — None. R::TypeParam(_) => return None, // 172.12 A1′: transitional debt carrier (a `current_type_subst` C-name) — it // is read ONLY as a C-string (never reconstructed to a `TypeRef`), so like a // residual type-param it is not recoverable here → None. R::Raw(_) => return None, R::Scalar { width, signed, wide_default } => { let name = match (width, signed, wide_default) { (8, true, _) => "i8", (16, true, _) => "i16", (32, true, _) => "i32", (64, true, true) => "int", (64, true, false) => "i64", (8, false, _) => "u8", (16, false, _) => "u16", (32, false, _) => "u32", (64, false, true) => "uint", (64, false, false) => "u64", _ => return None, }; prim_ref(name, span) } R::Float { width: 32 } => prim_ref("f32", span), R::Float { width: 64 } => prim_ref("f64", span), R::Float { .. } => return None, R::Bool => prim_ref("bool", span), R::Str => prim_ref("str", span), R::Never => prim_ref("never", span), R::Named { name, module, args } => { let mut path = module.clone(); path.push(name.clone()); TypeRef::Named { path, generics: args.iter().filter_map(|a| Self::resolved_to_typeref(a, span)).collect(), span, } } R::Array(elem) => { TypeRef::Array( Box::new(Self::resolved_to_typeref(elem, span)?), span, ) } // [M-fixed-array-value-semantics]: round-trip `[N]T` losslessly (N carried). R::FixedArray(n, elem) => { TypeRef::FixedArray( *n, Box::new(Self::resolved_to_typeref(elem, span)?), span, ) } // Plan 172.1 D86b: tuple Ok-payload in Result[(T1,T2),E] must survive the // resolved_to_typeref round-trip so Coalesce infer_expr_type can extract // generics[0] = (T1,T2) instead of the shifted Err type E. R::Tuple(elems) => { let converted: Vec<TypeRef> = elems.iter() .filter_map(|e| Self::resolved_to_typeref(e, span)) .collect(); if converted.len() != elems.len() { return None; } TypeRef::Tuple(converted, span) } // [M-per-file-check-no-prelude-protocol-scope] follow-on (Plan 172.13 // batch 4): reconstruct a typed raw pointer round-trip. `from_type_ref` // (above, ~L238-257) losslessly encodes `*T`/`*mut T`/`*unsafe T` as // `TypedPtr(modifier, inner)`, but this reverse direction used to give // up (`return None`) on EVERY `TypedPtr` — so any Call-channel-typed // static-method-return that happened to be a pointer (`RawMem.alloc(n) // -> *mut u8`) came back out of the channel as an untyped `None`. A // let-binding with no explicit annotation (`ro buf = RawMem.alloc(n)`) // then registered NO type for `buf` in scope, so a later `buf[n] = 0` // index-write couldn't see `pointee_is_writable` (needs a `TypeRef:: // Pointer`) and fell through to the ro-binding L1/L2 freeze — // E_READONLY_CONTENT despite `*mut u8` being writable-pointee by // construction. Mirrors `from_type_ref`'s own construction 1:1 (Mut/ // Unsafe wrap the pointee, Ro is the bare `Pointer`). R::TypedPtr(modifier, inner) => { let base = Self::resolved_to_typeref(inner, span)?; let wrapped = match modifier { crate::ast::PointerModifier::Ro => base, crate::ast::PointerModifier::Mut => TypeRef::Mut(Box::new(base), span), crate::ast::PointerModifier::Uninit => TypeRef::Uninit(Box::new(base), span), }; TypeRef::Pointer(Box::new(wrapped), span) } // [M-result-ok-unit-inference-mismatch] (2026-07-16): `R::Unit` used to // fall into the catch-all `return None` below. That is SAFE at the TOP // level (an un-annotatable unit-typed expression — callers treat `None` // as "leave unbound", never wrong), but the SAME `resolved_to_typeref` // is also called PER-ARG inside the `R::Named` arm below via // `.filter_map(...)`, to rebuild a generic type's arg LIST positionally // (`Result[T, E]` → `args: [T, E]`). `filter_map` SILENTLY DROPS any arg // that resolves to `None` — so a `Result[(), E]` (`args: [R::Unit, // R::Named(E)]`) round-tripped to `TypeRef::Named{Result, generics: // [E]}` — a ONE-ARG list with `E` SHIFTED INTO THE `T` SLOT. Downstream // generic substitution (`build_recv_subst`/`.ok()`'s `Result[T,E] -> // Option[T]`) then bound `T→E` (arity-mismatch on the structural path // falls back to a positional zip, which just pairs `T` with the only // surviving arg) — mistyping `.ok()` on unit-ok `Result` as `Option[E]` // instead of `Option[()]` (CC-FAIL: codegen's OWN mono correctly picks // `NovaOpt_nova_unit`, checker names the binding `NovaOpt_<E>`). // `()` IS representable (`TypeRef::Unit`), so round-trip it instead of // dropping it — preserves position for `Named`/`Tuple` args without // touching the top-level bail behavior other callers rely on (a // top-level `Some(Unit)` merely lets a `()`-typed expr bind `()` in // scope, which is correct, not a regression). R::Unit => TypeRef::Unit(span), R::Readonly(_) | R::Any | R::Ptr | R::Func { .. } => return None, }) } } impl<'a> TypeCheckCtx<'a> { /// Thin delegate — the actual conversion now lives on `impl ResolvedType` /// (see its doc, 221.1 №403) so `codegen/emit_c.rs` can reuse it too. /// Kept here, same name/signature, so every pre-existing `Self:: /// resolved_to_typeref(...)` call site inside `TypeCheckCtx` stays byte-identical. fn resolved_to_typeref(rt: &ResolvedType, span: Span) -> Option<TypeRef> { ResolvedType::resolved_to_typeref(rt, span) } /// Ф.1: best-effort вывод типа выражения (для не-литералов). fn infer_expr_type( &self, expr: &Expr, scope: &HashMap<String, TypeRef>, ) -> Option<TypeRef> { match &expr.kind { ExprKind::Ident(name) => { if let Some(tr) = scope.get(name).cloned() { return Some(tr); } // Ident not in scope: try resolved_types_buf first. // // [M-fn-value-binding-untyped-silent] fix (реестр 221.1 №101, // 2026-07-25): `f1_expr_inner`'s Ident-annotation producer // (~L7985) runs on THIS SAME Ident BEFORE this fn is reached // from `f1_check_assign_let`/`assignable` — it calls `infer_ // expr_type` itself (recursing through the fn_decls fallback // below, successfully), then LOSSY-round-trips the result // through `ResolvedType::from_type_ref` → cache → // `resolved_to_typeref`. That reverse conversion honestly // gives up (`return None`) on `Func`/`Any`/`Ptr`/`TypeParam`/ // `Raw` (irreconstructible without extra context) — but the // OLD code here `return`ed that `None` UNCONDITIONALLY, // discarding it as "Unknown, skip" and never trying the // fn_decls/bare-variant fallbacks below AT ALL once a (lossy) // cache entry existed. A bare free-fn reference (`ro t1 str = // test1`) round-trips to `None` this way on EVERY subsequent // lookup — silently un-typing itself right when a caller // (`check_fn_value_mismatch`, near `check_closure_scalar_return`) // needed the real `Func` shape most. Only skip the fallbacks on // an actual cache HIT (`Some(tr)`); a round-trip MISS falls // through instead of short-circuiting. Low blast radius: a // `None`-vs-`Func` difference is inert for every OTHER existing // consumer of `infer_expr_type` — `resolved_cat_of` collapses // `Func` → `Any` just like the old `None`-driven `Compat:: // Unknown` was permissive, so nothing downstream newly rejects // on this alone (confirmed: folding a Func-mismatch check // straight into the shared `assignable_direct` — tried first — // DID regress a same-named free-fn/out-of-scope-local collision // elsewhere in the huge merged CU; reverted in favor of the // narrow, call-site-scoped `check_fn_value_mismatch`). if expr.id.is_set() { if let Some(rt) = self.resolved_types_buf.borrow().get(&expr.id) { if let Some(tr) = Self::resolved_to_typeref(rt, expr.span) { return Some(tr); } } } // [Plan 228 Ф.2(a) producer, реестр 221.1 №94-v2] Bare reference // to a free fn BY NAME, not a call (`ro m Mid = identity_mw`) — // the sixth legacy arm this plan targets (`fn_returns_fn_sig`'s // Ident-RHS propagation, emit_c.rs) exists because `m`'s value // literally IS `identity_mw` — a first-class fn value whose type // is `identity_mw`'s OWN declared `Func` shape. Single // non-generic, receiver-less overload only (same single- // candidate discipline as the free-fn-CALL producer, `f1_expr_ // inner`'s Ident/Call arm ~8925) — an ambiguous or generic name // is honestly left unresolved (`None`) rather than guessing. if let Some(overloads) = self.sig.fn_decls.get(name) { let candidates: Vec<&&FnDecl> = overloads.iter() .filter(|f| f.receiver.is_none() && f.generics.is_empty()) .collect(); if let [f] = candidates.as_slice() { let params: Vec<TypeRef> = f.params.iter().map(|p| p.ty.clone()).collect(); return Some(TypeRef::Func { params, effects: f.effects.clone(), return_type: f.return_type.clone().map(Box::new), extern_abi: None, span: expr.span, }); } } // Last resort: if name = a unit-variant of a known sum type, infer as that type. // Covers bare enum variants (`D52Red`, `None`) used in expression position. // [M-hashmap-order-bare-variant-flake] (2026-07-13): `self.types` is a // `HashMap<String, TypeDecl>` — Rust's default `RandomState` hasher reseeds // every process, so iterating it directly picked a DIFFERENT candidate on // every `nova test`/`nova build` invocation whenever ≥2 sum types in the CU // declare a unit-variant of the same bare name (corpus example: // `d406_enum_kind_token.nv`'s `D406Color` and `d52_type_forms.nv`'s // `D52Color` both declare `Green`; a bare `ro c = Green` resolved to // whichever type the hash iteration surfaced first that run). When the // colliding candidates have INCOMPATIBLE payload shapes elsewhere in a // large corpus, a wrong-candidate pick is a genuine type-confusion bug, not // just cosmetic — this is the root cause of the `spec_tests/conformance` // segfault flake (RUN-FAIL ~1-in-4..5, byte-different `.c` across separate // compiles of the SAME source, confirmed via bisection + generated-C diff). // Fix: collect every candidate, sort by name, always pick the same // (lexicographically smallest) one — same source now ALWAYS produces the // same C, so a genuinely-ambiguous corpus fails (or passes) the SAME way on // every run instead of flaking. let mut candidates: Vec<&String> = self.types.iter() .filter_map(|(type_name, td)| { if let TypeDeclKind::Sum(variants) = &td.kind { if td.generics.is_empty() && variants.iter().any(|v| &v.name == name) { return Some(type_name); } } None }) .collect(); candidates.sort(); if let Some(type_name) = candidates.into_iter().next() { return Some(TypeRef::Named { path: vec![type_name.clone()], generics: Vec::new(), span: expr.span, }); } None } ExprKind::RecordLit { type_name: Some(name), .. } => { Some(TypeRef::Named { path: name.clone(), generics: Vec::new(), span: expr.span, }) } ExprKind::As(_, ty) => Some(ty.clone()), // Plan 172.1 §0a: Try (`expr?`) unwraps Result[T,E]→T or Option[T]→T. // Bang (`expr!!`) unwraps Option[T]→T or Result[T,E]→T. // Conservative: only when the inner type resolves and is a known container. ExprKind::Try(inner) | ExprKind::Bang(inner) | ExprKind::RefArg(inner) => { // [M-try-map-err-chain-loses-payload-type] (владелец, 2026-07-31): // `X.map_err(f)?` — возврат `@map_err[F]` стирается (typevar F // не подставлен), сама цепочка часто вовсе не резолвится, но // Ok-payload map_err НЕ меняет: тип = Ok-тип receiver'а X. // Хук ДО общей развязки — receiver резолвится и тогда, когда // цепочка нет (Result[T,E] у X → T). if let ExprKind::Call { func, .. } = &inner.kind { if let ExprKind::Member { obj, name } = &func.kind { if name == "map_err" { if let Some(TypeRef::Named { path: op, generics: og, .. }) = self.infer_expr_type(obj, scope) { let ob = op.last().map(String::as_str); if (ob == Some("Result") || ob == Some("Option")) && !og.is_empty() { return Some(og[0].clone()); } } } } } let inner_tr = self.infer_expr_type(inner, scope)?; if let TypeRef::Named { path, generics, .. } = &inner_tr { let base = path.last().map(String::as_str); if base == Some("Result") || base == Some("Option") { if !generics.is_empty() { return Some(generics[0].clone()); } // Plan 173.1 [M-bare-result-try-annotation] (2026-07-09): // BARE `Result`/`Option` (erased, no type args — `fn f() // -> Result`). The unwrapped T is NOT knowable here; // falling through to «return inner unchanged» annotated // `ro v = f()?` as the WHOLE Result → codegen declared // `NovaRes_…* v = payload.Ok._0` → pointer arithmetic on // an int payload (`v + 1` scaled by struct size: 42 + // sizeof instead of 43 — supervised_errors.nv SECTION 2, // uncovered when the concurrency folder first compiled // past its Ф.2-era CC-FAILs). Honest None → no channel // annotation → legacy codegen navigation types the // unwrap correctly. return None; } } // Unknown container (user type, generic T) → return inner type unchanged. Some(inner_tr) } // `x as T` — result type is always T (the declared target). ExprKind::As(_, target_ty) => Some(target_ty.clone()), // Plan 172.1 §0a: Coalesce (`a ?? b`) — type is the unwrapped inner T from `a: // Option[T]`. The fallback `b` must be compatible but DOES NOT determine the result // type (a literal `0` is `int` but `v.first() ?? 0` on `Vec[uint]` must be `uint`). // Unwrap by the same logic as Try/Bang: inner[0] of Option[T]/Result[T,E]. ExprKind::Coalesce(a, _) => { let a_tr = self.infer_expr_type(a, scope)?; if let TypeRef::Named { path, generics, .. } = &a_tr { let base = path.last().map(String::as_str); if (base == Some("Option") || base == Some("Result")) && !generics.is_empty() { return Some(generics[0].clone()); } } // Unknown: return the outer type of `a` unchanged. Some(a_tr) } // A block expression's value is its trailing expr. Conservative: // only infer when the block has NO statements, so the trailing // cannot reference inner let-bindings (which are absent from the // outer `scope`) → no risk of resolving a name to the wrong outer // binding. Covers `ro p = unsafe { … as *mut T }` so `p` infers as // `*mut T` — required by the raw-pointer index-write carve-out in // `check_target_readonly` (169.2 [M-169.2-ptr-index-ro-binding]). // Blocks with statements stay `None` as before. ExprKind::Block(b) if b.stmts.is_empty() => { b.trailing.as_ref().and_then(|t| self.infer_expr_type(t, scope)) } // Block with statements and no trailing expr → unit. Trailing is inferred above. ExprKind::Block(b) if b.trailing.is_none() => Some(TypeRef::Unit(expr.span)), // Plan 172.1 §0a: `if cond { then } else { … }` expression type = type of // then-branch trailing expr (branches must be type-compatible per checker). // Gate: else_ must exist (otherwise If = unit, no value type). // Conservative: use outer `scope` for trailing infer — if the trailing expr // references a let-binding introduced inside the block, infer_expr_type returns // None → no annotation → safe legacy fallback. No false annotations possible. // // D275 unit-domination MIRROR (2026-07-02, infer↔emit симметрия R2/R4): // если then-хвост — реальный value-тип (fluent `-> @` push → Vec*), а // ДРУГАЯ не-расходящаяся ветка даёт unit — ВЕСЬ if коэрсится в unit // (discard-позиция; emit_if_expr так и эмитит). Без зеркала f1-preamble // аннотировал if типом fluent-хвоста → канал перекрывал emit-фоллбек → // `tmp(Vec*) = NOVA_UNIT` CC-FAIL (cgfix_fluent_tail_if/chain — регресс // D275, пойман 2026-07-02). Never (расходящаяся ветка) unit НЕ форсит // (Plan 125 приоритет divergence сохранён); неразрешимый else — старое // поведение (then-тип). ExprKind::If { then, else_: Some(eb), .. } => { // Divergence-aware (Plan 125): расходящаяся ветка исключается из выбора // типа (та же логика, что emit_if_expr / legacy infer_If — R3 симметрия). // `block_diverges` ловит и stmt-форму (`{ throw "..." }` без trailing), // которую типовой инференс видел бы как Unit. let then_div = block_diverges(then); // 2026-07-02 (tally, If АТОМ 1a): then без trailing и без divergence = // Unit — точное зеркало else-стороны (ниже, None => Unit); прежняя // асимметрия роняла statement-form цепочки (`if c { v.push(x) } // else { ... }`) в legacy. Join-правила НЕ менялись (D275-mirror). let then_t = match &then.trailing { Some(t) => self.infer_expr_type(t, scope), None => Some(TypeRef::Unit(expr.span)), }; let (else_div, else_t): (bool, Option<TypeRef>) = match eb { crate::ast::ElseBranch::Block(b) => ( block_diverges(b), match &b.trailing { Some(t) => self.infer_expr_type(t, scope), None => Some(TypeRef::Unit(expr.span)), }, ), crate::ast::ElseBranch::If(e) => { (expr_diverges(e), self.infer_expr_type(e, scope)) } }; if then_div && !else_div { return else_t; // тип if = тип не-расходящейся else-ветки } if else_div && !then_div { return then_t; } if then_div && else_div { return Some(prim_ref("never", expr.span)); } // Обе ветки не расходятся: let is_unit = |t: &TypeRef| matches!(ResolvedType::from_type_ref(t), ResolvedType::Unit); match (&then_t, &else_t) { // Ровно одна — unit, другая — value: D275 unit-доминирование // ([M-codegen-fluent-tail-if-unify]) → весь if = unit. (Some(tt), Some(te)) if (is_unit(tt) && !is_unit(te)) || (is_unit(te) && !is_unit(tt)) => { Some(TypeRef::Unit(expr.span)) } // Обе разрешились И совпали → этот тип. (Some(tt), Some(te)) if ResolvedType::from_type_ref(tt) == ResolvedType::from_type_ref(te) => { Some(tt.clone()) } // Типы разошлись ЛИБО сторона не разрешилась → НЕ аннотировать // (None → legacy). Прежнее lax-«then-tail побеждает» ядовито: fluent // Vec*-хвост в then + неразрешимый else-if аннотировал if как Vec*, // канал перекрывал emit-фоллбек D275 → `tmp(Vec*) = NOVA_UNIT` CC-FAIL // (cgfix_fluent_tail_if/chain, pre-existing регресс, пойман 2026-07-02). _ => None, } } // If without else — always unit (condition may fail, no value produced). ExprKind::If { else_: None, .. } => Some(TypeRef::Unit(expr.span)), // Plan 172.1 §0a: Unary op — result type by operator: // Neg/Not: result = operand type (arithmetic/bool identity). // Deref(*p): result = pointee T (strip Pointer wrapper, peel Mut/Readonly inner). // AddrOf/RawAddrOf: result = *operand (wrap in Pointer — conservative, AddrOf // is rarely used as an expression type source; included for completeness). ExprKind::Unary { op, operand } => { use crate::ast::UnOp; let op_tr = self.infer_expr_type(operand, scope)?; match op { // Plan 234 Ф.2 (D46-амендмент): `~x` — тип результата = // тип операнда (как `-x`; на пользовательских типах // `@bitnot() -> Self` обычно, но эта channel-инфра не // проверяет саму сигнатуру, только структурное эхо типа). UnOp::Neg | UnOp::Not | UnOp::BitNot => Some(op_tr), UnOp::Deref => match op_tr { TypeRef::Pointer(inner, _) => Some(match *inner { TypeRef::Mut(t, _) | TypeRef::Readonly(t, _) => *t, t => t, }), _ => None, }, // **Plan 118.6 RESTORED (D216 §4 AMEND / D246 «§V2.6 // частично отменено», owner decision 2026-08-06, window // p375-ptr2, spec commit 96100421e):** `&x` from a // `mut`-bound SOURCE infers the writable `*mut T` // automatically; from a `ro`-bound source (or any // non-simple-place operand — Member/Index roots also // qualify via `assign_root_ident`, anything else stays // conservative) it infers the readonly `*T` default. The // ANNOTATED type still wins wherever one is present (`ro // p *T = &mut_x` stays a readonly-pointee `p` — checked // structurally elsewhere, this arm only supplies the // UNANNOTATED inferred shape). Guarantee (№375): a // writable pointer can never be materialized from a // `ro`-bound source via ANY path (this inference default, // an explicit annotation, a param/field/return position, // or an `as`-cast — see `check_addrof_mut_from_ro_source` // and the `ExprKind::As` same-pointee-cast retraction). UnOp::AddrOf | UnOp::RawAddrOf => { let mut_source = Self::assign_root_ident(operand) .map_or(false, |root| !self.ro_binding_names.borrow().contains(root)); let pointee = if mut_source { TypeRef::Mut(Box::new(op_tr), expr.span) } else { op_tr }; Some(TypeRef::Pointer(Box::new(pointee), expr.span)) } } } // Plan 172.1 §0a: Member `obj.field` — type = field type in the record declaration. // Gate: obj must infer to a Named type; record must be non-generic (no type_params). // Generic records are excluded — field types may reference type params that the // bare Named{name} annotation cannot reproduce without generic mono args. ExprKind::Member { obj, name } => { // [Plan 228 Ф.1(b), реестр 221.1 №94-v2 mechanism (b)] Method-value // expression `Type.@method` (UNBOUND form — the bound form `x.@len` // was retracted Plan 132, D35 §6081/§6082, so `obj` here is always a // TYPE name, never a value). Before this, `infer_expr_type` had no // arm for it at all: the general Member arm below unconditionally // recurses `infer_expr_type(obj, scope)` first, and a bare type name // (`MvInferNum`, `int`, `str`) is never scope-bound → that recursion // returns `None` (a bare-sum-variant fallback doesn't apply either) → // the WHOLE Member (and therefore the method-value arg it wraps, e.g. // `a.map(MvInferNum.@to_str)`) stayed untyped. That silence is why the // arg-loops in `f1_check_call`/`resolve_return_channel` (`infer_expr_ // type(a.expr(), scope)`, used to `unify_type`/`Constraint::Eq` a // method-level generic against the arg) never saw a method-value arg's // shape — `node_substs` stayed unwritten and emit_c's legacy Step2m // (`resolve_method_level_subst`/`resolve_instance_call_subst`/ // `infer_method_level_return_for_sum_inner`) re-derived it standalone. // Typing the Member here as the method's callable `Func` shape // (`fn(Recv, params...) -> Ret`) lets the EXISTING arg-loops unify it // structurally — no new inference engine, same sig-registry // (`method_overloads`) `method_value_lookup_sig` (emit_c) already // mirrors for the SAME selection (first-declared overload; ambiguity // resolution via `as fn(...)` annotation is out of scope here exactly // as it is there — Plan 11 Ф.5). // // Gate: `name` carries the `@`-prefix marker (parser, `TokenKind::At` // arm) AND `obj` is a bare `Ident` that is NOT scope-shadowed (a local // variable literally named like a type wins — matches // `method_value_lookup_sig`'s own `var_types`-first check) AND names a // known user type or a primitive. A blanket method (`fn[T] T @m`, // D145) is deliberately NOT covered — `method_overloads` is keyed by // CONCRETE type name; a blanket's receiver is the fn's own generic // param name (`blanket_method_names`, a separate registry), so // `self.method_overloads("int", "to_str")` genuinely misses `int.@to_str` // (prelude's `fn[T] T @to_str() -> str`) — honest miss, not a bug (this // IS №82's answer: bonus mechanism does NOT close it, confirmed // structurally, matching the mvinfer fixtures' own doc comment). if let Some(method_bare) = name.strip_prefix('@') { if let ExprKind::Ident(tn) = &obj.kind { if !scope.contains_key(tn) && (self.is_known_type(tn) || Self::is_primitive_type_name(tn)) { if let Some(f) = self.method_overloads(tn, method_bare) .and_then(|overloads| overloads.first()) { if let Some(recv) = &f.receiver { let recv_ty = recv.receiver_ty.clone().unwrap_or_else(|| { TypeRef::Named { path: vec![recv.type_name.clone()], generics: recv.generics.clone(), span: recv.span, } }); let mut self_subst: HashMap<String, TypeRef> = HashMap::new(); self_subst.insert("Self".to_string(), recv_ty.clone()); let mut params: Vec<TypeRef> = vec![recv_ty]; params.extend(f.params.iter().map(|p| { crate::const_fn_trampoline::subst_type_ref_pub(&p.ty, &self_subst) })); let ret = f.return_type.clone() .map(|t| crate::const_fn_trampoline::subst_type_ref_pub(&t, &self_subst)) .unwrap_or(TypeRef::Unit(expr.span)); return Some(TypeRef::Func { params, effects: Vec::new(), return_type: Some(Box::new(ret)), extern_abi: None, span: expr.span, }); } } } } } let obj_tr = self.infer_expr_type(obj, scope)?; // 172.1.2: позиционное tuple-поле `t.0` / `t.1` — элемент кортежа. if let TypeRef::Tuple(tys, _) = &obj_tr { if let Ok(ix) = name.parse::<usize>() { return tys.get(ix).cloned(); } } if let TypeRef::Named { path, generics, .. } = &obj_tr { if let Some(type_name) = path.last() { // [M-198-f4c-1-privfile-type-not-discriminated]: same // file-aware lookup as f3_check_member_ctx — `expr` is // this Member access itself, `expr.span.file_id` its // use-site file, so a colliding `priv(file) type` // yields ITS OWN file's field shape/types for // inference (not whichever peer-file decl won `types`). if let Some(td) = self.types_get_for_file(type_name, expr.span.file_id) { if let TypeDeclKind::Record(fields) = &td.kind { if td.generics.is_empty() && generics.is_empty() { return fields .iter() .find(|f| &f.name == name) .map(|f| f.ty.clone()); } // ice-p67-http (2026-07-08): generic record с ИЗВЕСТНЫМИ // type-args ресивера (`@map: HashMap[K, V]` внутри // HashMapIter[K, V]) — поле резолвится подстановкой // receiver-args в объявленный тип поля (тот же // subst_receiver_generics, что в f3_check_member_ctx). // Раньше generic-ресивер целиком выпадал в None → // f3_check_member_ctx молча пропускал НЕСУЩЕСТВУЮЩЕЕ // поле (`@map._buckets` после переименования) → чекер // PASS + P67-ICE в codegen (Index element type unknown). if !td.generics.is_empty() && td.generics.len() == generics.len() { return fields .iter() .find(|f| &f.name == name) .map(|f| self.subst_receiver_generics( &f.ty, &td.generics, generics)); } } // №362 (p362-advice): mirror the Record arm immediately // above for `TypeDeclKind::NamedTuple` (D215) — this Member // arm previously had NO NamedTuple case at all, so // `infer_expr_type(@field, scope)` returned `None` whenever // the ENCLOSING/receiver type of `@field` (e.g. `Self` for a // self-field read) was itself a named tuple, independent of // the field's own type. That silent `None` breaks EVERY // caller that chains a further `.member`/`.method(...)` off // the result — most importantly `f3_check_member_ctx` // (~15042, `let Some(obj_tr) = self.infer_expr_type(obj, // scope) else { return; }`), which exists specifically to // validate that chained access. The gap made `@field. // anything` (a bogus field, a bogus method, or the // E_READONLY_COERCE-suggested `.clone()` on a field type with // NO `#impl(Clone)`) type-check with ZERO diagnostics — // confirmed via minimal repro (`nova check` accepted // `@mant.nonexistent_field_xyz` on a NamedTuple `Self` // outright) and matches nova-bignum's field report // (docs/plans/wip/PROGRESS-p-bignum-tuple.md, Defect A // Problem 2: `E_RECV_METHOD_MISMATCH` misresolving `.div_rem` // on the result of `@mant.clone()` to an unrelated type — // downstream of THIS same untyped chain, not a `.clone()`- // specific bug). Same generic-substitution shape as the // Record arm (bare vs receiver-typed fields). if let TypeDeclKind::NamedTuple(fields) = &td.kind { if td.generics.is_empty() && generics.is_empty() { return fields .iter() .find(|f| &f.name == name) .map(|f| f.ty.clone()); } if !td.generics.is_empty() && td.generics.len() == generics.len() { return fields .iter() .find(|f| &f.name == name) .map(|f| self.subst_receiver_generics( &f.ty, &td.generics, generics)); } } } } } None } // Plan 172.1 §0a: Binary expr type inference. // Comparison/logical ops → bool (always safe). // Arithmetic/bitwise: infer from left ONLY if left is NOT an IntLit/FloatLit/CharLit // (bare literals default to int/f64/char which may mismatch the declared context, §5). // Non-literal left (Ident, Member, Call, As, etc.) → its inferred type is reliable. ExprKind::Binary { op, left, .. } => { use crate::ast::BinOp; match op { BinOp::Eq | BinOp::Neq | BinOp::Lt | BinOp::Le | BinOp::Gt | BinOp::Ge | BinOp::And | BinOp::Or | BinOp::Implies | BinOp::Iff => { Some(prim_ref("bool", expr.span)) } _ => { // Skip if left is a bare literal (→ int/f64 default, context-unaware). let left_is_lit = matches!( &left.kind, ExprKind::IntLit(_) | ExprKind::FloatLit(_) | ExprKind::CharLit(_) | ExprKind::Unary { .. } // -N is also a literal pattern ); if left_is_lit { None } else { self.infer_expr_type(left, scope) } } } } ExprKind::IntLit(_) => Some(prim_ref("int", expr.span)), ExprKind::FloatLit(_) => Some(prim_ref("f64", expr.span)), ExprKind::BoolLit(_) => Some(prim_ref("bool", expr.span)), ExprKind::StrLit(_) | ExprKind::InterpolatedStr { .. } => { Some(prim_ref("str", expr.span)) } ExprKind::CharLit(_) => Some(prim_ref("char", expr.span)), // D412 (Plan 186): hex-blob / embed → `[]u8`. ExprKind::HexBlobLit(_) => Some(TypeRef::Array( Box::new(prim_ref("u8", expr.span)), expr.span, )), // Plan 134: null ptr literal → *() = TypeRef::Pointer(TypeRef::Unit). ExprKind::NullPtrLit => Some(TypeRef::Pointer( Box::new(TypeRef::Unit(expr.span)), expr.span, )), // D176 (Plan 108): SelfAccess → look up "@" in scope (injected by f1_check_fn). ExprKind::SelfAccess => scope.get("@").cloned(), // Plan 125.1 (Ф.2): `throw expr` — divergent expression. По спеке // D25 throw имеет тип `never` (bottom). Без этого `let x = throw e` // / `f(throw e)` падают в Compat::Unknown даже при наличии // never-subtype hookpoint'а из Ф.1. ExprKind::Throw(_) => Some(prim_ref("never", expr.span)), // Plan 125.1 (Ф.2): `interrupt v?` — D61 досрочное завершение // with-блока, тип позиции = `never` (control-flow leaves enclosing // expression context). Аналогично Throw — divergent. ExprKind::Interrupt(_) => Some(prim_ref("never", expr.span)), // Plan 115 D214 [M-115-newtype-constructor]: `Type(value)` call where // Type is a known Newtype/Alias → infer as Named(Type). Without // this, `ro h = SqHandle(raw)` binds `h` без типа в scope, и // assignable() для `close_sqlite(h)` падает в Compat::Unknown // (E7301 не fires при passing PngHandle к fn(SqHandle)). // // Plan 125.1 (Ф.2): never-returning builtins + user fns whose // return_type resolves to `Ty::Never` → propagate `never`. ExprKind::Call { func, args: outer_call_args, .. } => { // Plan 200 П19: `[N]T @len()`/`@ptr()` — compiler-synthesized FixedArray // accessors, checked FIRST (§3 one-window structural test — same peel as // `check_instance_overload`, D238-family). Must run before every other Call // arm below: a FixedArray receiver has ZERO real `.nv` methods (architecture // note, Plan 200-19), so nothing here can shadow a genuine declaration — // and this is what lets `unsafe { p.write(...) }` on a `mut`-receiver // `arr.ptr()` type-check as `*mut T` (E_POINTER_RO_ASSIGN reads THIS // function's return via `infer_expr_type(obj)`, not the codegen channel). if let ExprKind::Member { obj, name } = &func.kind { if matches!(name.as_str(), "len" | "ptr") && outer_call_args.is_empty() { if let Some(obj_ty) = self.infer_expr_type(obj, scope) { if let Some((n, inner)) = Self::peel_fixed_array(&obj_ty) { let is_mut = !self.is_through_ro_binding(obj); if let Some(rt) = Self::fixed_array_accessor_return( n, inner, name, is_mut, expr.span, ) { return Some(rt); } } } } } // №368 (window p375-ptr2): the SAME structural gap as the // FixedArray `len`/`ptr` special case just above, for the // DYNAMIC `[]T`/`Vec[T]` receiver's `.ptr()` — `mut p = // buf.ptr()` with NO annotation left `p` UNTYPED in scope // (this general Call arm has no instance-method-return // resolution path for an ordinary `.nv`-declared method — // `resolve_instance_method_return` exists but has no live // caller; wiring it in generally is a much bigger, riskier // change than this bug needs). Every LATER checker gate // keyed off `infer_expr_type(p)` (E_POINTER_RO_ASSIGN, №349, // №353, №367's read-form checks, №375's // `check_addrof_mut_from_ro_source`) silently no-opped for // this idiom while codegen (a separate, C-type-string-based // inference) still classified `p` correctly — exactly the // asymmetry №368 reports. Mirrors Vec[T]'s real two-overload // `@ptr()`/`mut @ptr()` pair (access.nv:258/266) // structurally: `array_elem_type` normalizes BOTH the `[]T` // sugar and explicit `Vec[T]` (D239 alias) to the same // element type, so this covers both spellings in one arm. if let ExprKind::Member { obj, name } = &func.kind { if name == "ptr" && outer_call_args.is_empty() { if let Some(obj_ty) = self.infer_expr_type(obj, scope) { if let Some(inner) = array_elem_type(&obj_ty) { let is_mut = !self.is_through_ro_binding(obj); let pointee = if is_mut { TypeRef::Mut(Box::new(inner.clone()), expr.span) } else { inner.clone() }; return Some(TypeRef::Pointer(Box::new(pointee), expr.span)); } } } } // Plan 221.1 №286/№143 (окно p-chan, real Channel[T] mono): // `.recv()`/`.try_recv()` on a receiver with a STATICALLY // known element `T` (`channel_elem_type` — `None` for an // untracked bare `Channel.new`, falls through unchanged) → // `Option[T]`, same `TypeRef::Named{path:["Option"], // generics:[T]}` shape used by every other Option-producing // site in this fn (e.g. `closure_if_ctor_peek`). `.share()` // (ChanWriter only, D91/Plan 201) returns the SAME // `ChanWriter[T]` as its receiver — needed so a chained // `tx2 = tx.share()` also gets a typed element (`tx2.send(v)` // must be checked exactly like `tx.send(v)`). if let ExprKind::Member { obj, name } = &func.kind { if matches!(name.as_str(), "recv" | "try_recv") && outer_call_args.is_empty() { if let Some(elem_t) = self.channel_elem_type(obj, scope) { return Some(TypeRef::Named { path: vec!["Option".to_string()], generics: vec![elem_t], span: expr.span, }); } } else if name == "share" && outer_call_args.is_empty() { if let Some(obj_ty) = self.infer_expr_type(obj, scope) { if matches!(&obj_ty, TypeRef::Named { path, generics, .. } if generics.len() == 1 && path.last().map(String::as_str) == Some("ChanWriter")) { return Some(obj_ty); } } } } // [M-183-int-to-str-module-method-collision] (§0 checker-primary): // EFFECT-OPERATION call — `Time.now_monotonic_ns()` / `Clock.tick()` — has a // STATICALLY-KNOWN declared return type (the op's `return_type` in the // `type E effect { op(...) -> R }` block). The checker previously did NOT // infer it → `ro x = Time.now_monotonic_ns()` left `x` UNTYPED in scope → // `check_instance_overload`'s `infer_arg_ty(x)` returned None → the primitive // unknown-method gate was SKIPPED → a stray `x.to_str()` leaked past the // checker and codegen coarse-by-name (`method_receivers` last-wins) // mis-dispatched the `nova_int` receiver to a same-named FOREIGN method // (`NetError.to_str` / any type's `to_str`) → silent type-confused C (the int // passed as an enum/record pointer → SEGV). Resolving the op's declared return // type restores `x: int` → the existing `[E_UNKNOWN_METHOD]` gate fires cleanly // (int owns no `to_str`). Mirrors codegen's authoritative `effect_schemas` // dispatch (emit_c `infer_expr_c_type`). The effect-op call surfaces as EITHER // `func = Path([E, op])` (`E.op()` at statement level) OR `func = Member{obj: // Ident(E), name: op}` — cover both. Effect must be in `self.types` (declared // in this module or a merged peer); imported-only effects fall through (None) // → legacy behaviour, no regression. let eff_op: Option<(&str, &str)> = match &func.kind { ExprKind::Path(parts) if parts.len() == 2 => { Some((parts[0].as_str(), parts[1].as_str())) } ExprKind::Member { obj, name } => match &obj.kind { ExprKind::Ident(n) => Some((n.as_str(), name.as_str())), _ => None, }, _ => None, }; if let Some((eff, op_name)) = eff_op { if let Some(td) = self.types.get(eff) { if let TypeDeclKind::Effect(ops) = &td.kind { if let Some(op) = ops.iter().find(|m| m.name == op_name) { return Some(match &op.return_type { Some(rt) => rt.clone(), None => TypeRef::Unit(expr.span), }); } } } } // 196.5 Stage-D wave-2 (дыра-1, D1H1): a static call on a PRIMITIVE // type name (`str.from(x)`, `int.parse(...)`) parses to // `ExprKind::Path(["str", "from"])`, NOT `Member{obj:Ident, name}` — // primitive type names are reserved tokens, not plain `Ident`s (mirrors // the SAME Path-shape check `f1_check_call` already relies on at // `~12290`: `parts[0] == "str" && parts[1] == "from"`). The Member-keyed // static-return arm below (`tyname`/`ctor`, gated on `self.types`/ // `is_primitive_type_name`) therefore never sees this call shape — // `closure_arg_return_peek`'s `infer_expr_type(body, ..)` on a closure // literal whose body is `str.from(x)` returned `None`, so `Option[T] // @map[U](fn(T)->U)`/`Result[T,E]@map[U]` calls with such a closure // co-missed BOTH `resolved_types` (Channel 2) and `node_substs` (same // propose-then-verify block in `resolve_return_channel`/ // `resolve_instance_method_return_arity`, both gated on this same // return-type resolution) and fell to legacy // `infer_method_level_return_for_sum` (emit_c.rs). Mirror ONLY the // primitive-receiver concrete-return sub-case (no `self.types`/generics // lookup needed — primitives are never generic) — single-overload, // static receiver, concrete Named/Self return, same gate as `13741`. if let ExprKind::Path(parts) = &func.kind { if parts.len() == 2 && Self::is_primitive_type_name(parts[0].as_str()) { if let Some(overloads) = self.method_overloads(&parts[0], &parts[1]) { // `str.from` etc. commonly have MULTIPLE static overloads // (one per source primitive: char/bool/f64/f32/int/..., // `std/runtime/string/from_scalar.nv` + `std/runtime/ // char.nv`) — same arg-type dispatch the instance-method // multi-overload arm already performs (`~14382`): same // arity, single overload whose CONCRETE (non-generic, // primitive-or-Str/Bool) param0 matches the first arg's // inferred type. let static_candidates: Vec<&FnDecl> = overloads.iter() .filter(|s| s.receiver.as_ref() .map_or(false, |r| matches!(r.kind, ReceiverKind::Static)) && s.generics.is_empty()) .copied() .collect(); let picked: Option<&FnDecl> = match static_candidates.as_slice() { [] => None, [one] => Some(*one), many => { let a0 = outer_call_args.first().map(|a| a.expr()); a0.and_then(|a0| { let a0_rt = self.infer_expr_type(a0, scope) .map(|t| ResolvedType::from_type_ref(&t))?; let mut hit: Option<&FnDecl> = None; for cand in many { let Some(p0) = cand.params.first() else { continue }; let p0_rt = ResolvedType::from_type_ref(&p0.ty); let concrete = Self::primitive_gate(&p0_rt) || matches!(&p0_rt, ResolvedType::Str | ResolvedType::Bool) || matches!(&p0_rt, ResolvedType::Named { args, .. } if args.is_empty()); if !concrete || p0_rt != a0_rt { continue; } if hit.is_some() { return None; } // ambiguous hit = Some(*cand); } hit }) } }; if let Some(f) = picked { if let Some(ret) = &f.return_type { let concrete = match ret { TypeRef::Named { path, generics, .. } if path.len() == 1 && generics.is_empty() => { if path[0] == "Self" { Some(parts[0].clone()) } else if self.types.contains_key(&path[0]) || Self::is_primitive_type_name(&path[0]) { Some(path[0].clone()) } else { None } } _ => None, }; if let Some(n) = concrete { return Some(TypeRef::Named { path: vec![n], generics: Vec::new(), span: expr.span, }); } if ret.is_pointer_or_wraps_pointer() || ret.is_readonly() { return Some(ret.clone()); } // 196.5 Stage-D wave-2 (дыра-1, D1H1 follow-up): a // NON-bare `Self`-mentioning return (`fn u64. // try_from(s str) -> Result[Self, TryFromIntError]`, // `std/runtime/*`) falls through the bare-Named // `concrete` match above (`generics.is_empty()` is // false for `Result[Self,E]`) — the whole call // (`u64.try_from(a)`) stayed untyped, so a CHAINED // `.ok()`/`.unwrap_or`/`??` on it (`std/src/data/ // semver.nv`: `u64.try_from(a).ok() ?? 0`) could not // resolve ITS OWN receiver type either → co-miss // cascades one level up (`Result.ok`, same channel/ // node_substs block). `f.generics.is_empty()` was // already gated above, so `Self` is the ONLY name // that can be a residual placeholder here — substitute // it (mirrors `resolve_return_channel`'s `self_subst` // pattern, `~9770`) and accept iff NOTHING still // mentions `Self` afterward (recursive, catches nested // generics — no new inference, one substitution pass). let mut self_subst: HashMap<String, TypeRef> = HashMap::new(); self_subst.insert("Self".to_string(), TypeRef::Named { path: vec![parts[0].clone()], generics: Vec::new(), span: expr.span, }); let substituted = crate::const_fn_trampoline::subst_type_ref_pub( ret, &self_subst); let mut self_only: HashSet<String> = HashSet::new(); self_only.insert("Self".to_string()); if !typeref_mentions_any(&substituted, &self_only) { return Some(substituted); } } } } } } // [M-vec-elem-type-mismatch-silent] generic constructor: // `Type[T..].new(...)` / `.with_capacity` / `.from` / `.default` // / `.filled` → `Named{Type, generics:[T..]}`. Preserves the // element type (unlike `infer_value_type`, which collapses to the // bare name) so an inferred `mut v = Vec[int].with_capacity(n)` // binds `v: Vec[int]` in scope — letting the container // element-mismatch arg-check fire on inferred bindings, not only // explicitly-annotated ones. AST shape: Call{ func: Member{ obj: // TurboFish{ base: Ident(Type), type_args }, name: ctor } }. if let ExprKind::Member { obj, name: ctor } = &func.kind { // Plan 172.2: slice bare-ctor `[]T.new()/.with_capacity()/…` — the // dominant std spelling (`mut out = []u8.with_capacity(n)`). In expr // position `[]T` parses to `Path(["__array", <elem>])`; infer it to // `Array(<elem>)` so the binding records the concrete container type in // scope (mirror of the `Vec[T].ctor` TurboFish arm below). Without this // the receiver type is unknown → the method-arg narrowing check skips. if let ExprKind::Path(parts) = &obj.kind { if parts.len() == 2 && parts[0] == "__array" && matches!( ctor.as_str(), // Plan 196.2 W1 step2 [CAP-A enabler]: `of` added. `[]T.of(a,b,…)` // (variadic slice literal) returns `[]T` just like `from`; its // omission left `ro sv = []int.of(...)` UNRESOLVED → `sv` absent // from scope → any method on sv (e.g. `sv.iter()`) could not resolve // its receiver → NOT materialized into `resolved_types` → codegen // fell to the legacy `infer_call_ret_c` B07 arm. The TurboFish twin // `Vec[T].of` already resolved via `resolve_generic_static_return`. "new" | "with_capacity" | "from" | "default" | "filled" | "of" ) { return Some(TypeRef::Array( Box::new(TypeRef::Named { path: vec![parts[1].clone()], generics: Vec::new(), span: expr.span, }), expr.span, )); } } if let ExprKind::TurboFish { base, type_args } = &obj.kind { if let ExprKind::Ident(tyname) = &base.kind { // Plan 172.1 U.4.3(d) [M-172.1-U4.3d-generic-recv-infer]: // resolve the generic static-method's DECLARED return type // from the registry (§0/§3 general mechanism). This SUBSUMES // the former name-keyed ctor hardcode below — a Self-returning // ctor (`new`/…) resolves here to the SAME `Type[Targs]`, and // ANY other static method (`of`, user ctors) now infers too. if let Some((rt, ordered, fn_span)) = self.resolve_generic_static_return( tyname, ctor, type_args, expr.span, ) { // [M-196.5-node-substs] Producer C write: this call shape // (`Member{obj:TurboFish}`) never reaches ANY legacy return- // channel producer (see the comment at f1_check_call ~10622), // so there is no independent propose-then-verify pair here — // `ordered` IS the value THIS fn computed the return from // (same `subst` map). SHADOW verification (emit_c, // `shadow_check_node_substs`) is the independent cross-check. if expr.id.is_set() && !ordered.is_empty() { if std::env::var_os("NOVA_NODE_SUBSTS_TRACE").is_some() { eprintln!( "[NODE_SUBSTS] producer=C-static-turbofish \ call_id={:?} type={} method={} n={}", expr.id, tyname, ctor, ordered.len(), ); } self.node_substs.borrow_mut().insert(expr.id, ordered); } // Реестр 221.1 №137 (`[M-reflect-generic-static-dispatch- // collision]`): additionally materialize `resolved_callees` // (WHICH FnDecl — the single overload `resolve_generic_static_ // return` already resolved above, `fn_span`) + `resolved_types` // (the call's OWN substituted return type) for this call shape. // Closes a channel gap: this producer previously fed ONLY // `node_substs` (used defensively by codegen's OWN independent // dispatch, e.g. the `Vec`/`HashMap` turbofish-static arm in // emit_c.rs, which recomputes structurally and only // shadow-verifies against `node_substs`) — a receiver EXCLUDED // from codegen's generic-type-instance machinery for unrelated // representation reasons (`Option`/`Result`, Plan 62.A: value // repr `NovaOpt_<T>`/`NovaRes_<..>`, not the heap // `Nova_<Type>____<T>*` convention) had NO dispatch route at // all — codegen fell through to the coarse name-only // `method_receivers` fallback (collision with any other // same-named zero-arg static). Writing BOTH channels here lets // codegen READ the resolved callee + return type directly // instead of re-deriving them by scanning its own registry by // name — the §0/196 channel-first contract. if expr.id.is_set() { self.resolved_callees.borrow_mut().insert(expr.id, fn_span); self.resolved_types_buf.borrow_mut() .insert(expr.id, ResolvedType::from_type_ref(&rt)); } return Some(rt); } // Intrinsic fallback: Self-returning ctors whose methods live // in Rust (no `.nv` decl) so the registry can't resolve them — // `Vec[T].new`/`.with_capacity`/`.from`/`.default`/`.filled`. // (User `.nv` ctors of the same names already resolved above.) if matches!( ctor.as_str(), "new" | "with_capacity" | "from" | "default" | "filled" ) { return Some(TypeRef::Named { path: vec![tyname.clone()], generics: type_args.clone(), span: expr.span, }); } } } // [M-172.1-d174-sync-consume-registry] (Gap B checker-side): STATIC // method return on a CONCRETE non-generic KNOWN type — `Mutex.new()` → // `Mutex`, `Once.new()` → `Once` (sync builtin decls merged via // builtin_sig_modules). Требуется, чтобы `mut mu = Mutex.new()` попал в // scope → `mu.lock()` резолвится → `consume g` типизирован через КАНАЛ // (fail-path эмиссия defer-тел читает канал, не var_types-тайминг). // Консервативно: single-overload static, non-generic тип, конкретный // Named/Self return без generic-упоминаний. // 196.5 Stage-D wave-2 (дыра-1, D1H1): `self.types` — только // ПОЛЬЗОВАТЕЛЬСКИЕ `type X {..}` декларации; builtin-примитивы // (`str`/`int`/...) в ней НЕ зарегистрированы, хотя они законные // non-generic static-receiver'ы с `method_overloads`-записями // (`str.from(x)` — Plan 35/73). Гейт раньше ронял ВЕСЬ арм для // примитивного receiver'а → `closure_arg_return_peek` (types/mod.rs // ~14993, `infer_expr_type` на теле closure-литерала) не мог // вывести тип тела `str.from(x)` → `Option[T]@map[U]`/`Result[T,E] // @map[U]` с ТАКИМ closure-телом падали в co-miss (Channel-2 И // node_substs — оба продюсера пишут в ОДНОМ блоке, гейтированном // ИМЕННО этим условием) → легаси `infer_method_level_return_for_sum` // (emit_c.rs). Расширение на `is_primitive_type_name` — тот же // паттерн, что уже используется чуть ниже (`13759`) для bare-Named // return-конкретизации; НЕ новый инференс, тот же `method_overloads` // lookup, тот же single-overload/static/non-generic гейт. if let ExprKind::Ident(tyname) = &obj.kind { let non_generic_known_recv = self.types.get(tyname.as_str()) .map_or(false, |td| td.generics.is_empty()) || Self::is_primitive_type_name(tyname); if non_generic_known_recv { if let Some(overloads) = self.method_overloads(tyname, ctor) { if let [f] = overloads.as_slice() { if f.receiver.as_ref() .map_or(false, |r| matches!(r.kind, ReceiverKind::Static)) && f.generics.is_empty() { if let Some(ret) = &f.return_type { let concrete = match ret { TypeRef::Named { path, generics, .. } if path.len() == 1 && generics.is_empty() => { if path[0] == "Self" { Some(tyname.clone()) } else if self.types.contains_key(&path[0]) || Self::is_primitive_type_name(&path[0]) { Some(path[0].clone()) } else { None } } _ => None, }; if let Some(n) = concrete { return Some(TypeRef::Named { path: vec![n], generics: Vec::new(), span: expr.span, }); } // [M-per-file-check-no-prelude-protocol-scope] // follow-on (Plan 172.13 batch 4): STATIC-method // sibling of the free-fn oracle-row-D propagation // above (Plan 147 Ф.3, D246) — a single-overload // static method returning a POINTER (`RawMem.alloc` // → `*mut u8`) or a `ro`-wrapped value was falling // through this whole `Member{Ident,ctor}` arm // untyped (the `concrete` match above only handles // bare `Named`/`Self`), leaving `ro buf = // RawMem.alloc(n)` unregistered in `scope` — the // later `buf[n] = 0` index-write check then found // no type for `buf`, couldn't see it's a writable // `*mut T` pointee (`pointee_is_writable`), and // fell through to the ro-binding L1/L2 freeze → // spurious E_READONLY_CONTENT. This normally never // surfaced because the check only fires for // ENTRY-file code (see `is_target_in_entry` below) // and this exact call shape had never been the // entry before (it's an internal `std.runtime.*` // helper) — a per-file `nova check // std/runtime/string/core.nv` makes the file the // entry and exposes it. Mirror the free-fn arm // exactly: pointer-or-wraps-pointer / readonly // return shapes propagate as-is. if ret.is_pointer_or_wraps_pointer() || ret.is_readonly() { return Some(ret.clone()); } } } } } } } // Plan 186 [bug-1 audit-197 fix] field-of-func-type call: // `obj.field(args)` / `@field(args)` where `field` is a plain // RECORD FIELD holding a first-class function value (D42 Model-A // DI-by-field, e.g. `type Repo[T] { ro to_columns fn(T) -> []ColumnValue }` // then `@to_columns(value)`) — NOT a declared method // (`method_overloads` has no entry for it). The Member-arm above // (this same fn, `ExprKind::Member{obj,name}`) already resolves // such a field's declared `TypeRef` correctly, INCLUDING generic- // receiver substitution (`subst_receiver_generics`) — recurse into // it via `func` itself (which IS that Member expr) and, if it // resolves to `TypeRef::Func`, the call's type is its return type. // Guarded on NO method of this name existing on the receiver, so // this can only fire on the field case — it is structurally unable // to change the outcome of any instance-METHOD call (a name can't // be both a field and a method on the same type), keeping this // disjoint from the deliberate method-call decoupling below. // Without this, the RHS of `ro cols = @to_columns(value)` resolved // to `None` unconditionally (this whole arm never considered a // field-call), so `cols` was never registered into `scope` // (`f1_stmt`'s `Stmt::Let` binding falls to `scope.remove`) — // every later `cols.<method>(...)` then had an unknown receiver, // surfacing downstream as the codegen `[P67-LEGACY] method call // \`.map\` return type unknown` ICE (examples/real_world/orm_demo.nv // `@insert`/`@update`/`@update_versioned`, Plan 197 audit finding #1). if let Some(mut peeled) = self.infer_expr_type(obj, scope) { loop { match peeled { TypeRef::Readonly(i, _) | TypeRef::Mut(i, _) => peeled = *i, _ => break, } } if let TypeRef::Named { path, .. } = &peeled { if path.len() == 1 && self.method_overloads(&path[0], ctor).is_none() { if let Some(TypeRef::Func { return_type: Some(rt), .. }) = self.infer_expr_type(func, scope) { return Some(*rt); } } } } // Plan 172.1.2: GENERAL instance method-call return inference is DECOUPLED // from `infer_expr_type` (it must NOT change the inline checks fed by this // function — a resolved method-chain receiver unblocks a false-positive // extension-policy check [self_nested], and an inline-rippled binding type // flips the global `[]T`/`Vec[T]` spelling, perturbing a GC-sensitive layout // [plan154]). The CHANNEL annotation uses `infer_method_call_channel_type` // instead; this arm stays return-type-free for method calls. } if let ExprKind::Ident(name) = &func.kind { if let Some(td) = self.types.get(name) { // Plan 128.1 Ф.2 — D215 NamedTuple constructor (`Vec3(1,2,3)`) // is type-producing наряду с Newtype/Alias. Без этого // assignable() для NamedTuple bindings (e.g. `let v Vec3 = Vec3(...)`) // падает в default-int fallback, ломая infer элемента в // array-literals `[Vec3(1,2,3), Vec3(4,5,6)]`. // // Sum-variant constructors `Red(1)` НЕ попадают в этот arm: // sum variants хранятся внутри TypeDeclKind::Sum(...) (см. // walk_typeref @ 3082), а не как top-level types — поэтому // `self.types.get("Red")` → None, fallthrough preserved. if matches!( td.kind, TypeDeclKind::Newtype(_) | TypeDeclKind::Alias(_) | TypeDeclKind::NamedTuple(_) ) { return Some(TypeRef::Named { path: vec![name.clone()], generics: Vec::new(), span: expr.span, }); } } // Plan 125.1 (Ф.2): never-returning builtins (D13). // Same set as `expr_diverges` ниже + `unreachable`. if matches!(name.as_str(), "panic" | "exit" | "abort" | "unreachable") { return Some(prim_ref("never", expr.span)); } // Plan 125.1 (Ф.2): propagate `never` для user fns whose // declared return_type resolves к `never`. Requires ВСЕ // overloads divergent — иначе ambiguous (call resolution // ещё не выполнена на этом этапе, безопаснее fallback к // `None` чем выбрать random overload). U.5.4: `from_type_ref` // mirrors the deleted `ty_of_ref` for the `never` check. if let Some(decls) = self.sig.fn_decls.get(name) { if !decls.is_empty() && decls.iter().all(|d| { d.return_type.as_ref().map_or(false, |tr| { // U.5.5(a): peel the L2 `readonly` view (`readonly never` // is still bottom) so the never-propagation is unchanged. matches!(ResolvedType::from_type_ref(tr).peel_view(), ResolvedType::Never) }) }) { return Some(prim_ref("never", expr.span)); } // **Plan 147 Ф.3 (D246, oracle row D):** propagate a // `ro`-wrapped RETURN type (`-> ro Value`) so the // E_READONLY_COERCE content-view check can reject // `mut a Value = f()` while accepting `ro a Value = // f()`. Requires ALL overloads to declare a `Readonly` // return (call resolution is not yet done here, so a // mixed set is ambiguous → fall through to None). The // returned `ro` type drives only the readonly-coerce // gate; it is intentionally NOT a general return-type // inference (no monomorphization here). if !decls.is_empty() && decls.iter().all(|d| { d.return_type.as_ref().map_or(false, |tr| tr.is_readonly()) }) { // Safe: the `all` guard proved the first decl has a // readonly return_type. if let Some(rt) = decls[0].return_type.as_ref() { return Some(rt.clone()); } } // **Plan 147 Ф.3 (D246, L3, oracle row D):** propagate a // POINTER return type (`-> *T` / `-> *mut T`) so the // E_POINTER_RO_ASSIGN check on `*a = v` (where // `a = f()`) can read the pointee capability from the // type. Requires ALL overloads to agree on a pointer // return shape (else ambiguous → None). Only the FIRST // matching decl's type is returned — its pointee L3 is // what the deref-write gate consults; this is not a // general return-type inference. if !decls.is_empty() && decls.iter().all(|d| { d.return_type .as_ref() .map_or(false, |tr| tr.is_pointer_or_wraps_pointer()) }) { if let Some(rt) = decls[0].return_type.as_ref() { return Some(rt.clone()); } } } } // Plan 172.1 §0a: Call return type — if f1_check_call already resolved this // call's return type into resolved_types_buf (single-overload path), read it // back so Coalesce/Try/Bang/Binary wrapping this Call can propagate the type. // Safe: resolved_types_buf is written BEFORE this f1_expr recursive descent // for the outer expression (the walker visits inner exprs first). Only lookup, // no write; borrow() does not conflict with outer borrow_mut() since the outer // insert is for a DIFFERENT ExprId (the outer expr, not this Call expr). if expr.id.is_set() { if let Some(rt) = self.resolved_types_buf.borrow().get(&expr.id) { // [M-crossmodule-samename-typecheck-bleed] (221.1 №28): // prefer the CALLEE's own return-type annotation span // (its declaring file) over this call EXPRESSION's span // (the caller's file) — a same-named type declared in // BOTH the callee's module and some unrelated peer // module of the same CU must resolve against the // callee's own module, not whichever one happens to // win the name-only global `self.types` slot. let decl_span = self.call_return_decl_span.borrow() .get(&expr.id).copied(); return Self::resolved_to_typeref(rt, decl_span.unwrap_or(expr.span)); } } None } // Plan 172.1 §0a / D263 (2026-07-02): ArrayLit — read BACK the channel annotation // written by f1_expr's ArrayLit arm (single source, §1 «материализуй резолв» — no // re-derive here). f1_expr runs on the let-RHS BEFORE Stmt::Let's scope // registration calls infer_expr_type, so the buf is already populated. This binds // `ro a = [1, 2, 3]` as `a: Vec[int]` in the checker scope — required for // operator-overload Binary inference (`a + b` → left operand type, D263 `@plus`). // // D373 fallback (ex-D185, 2026-07-02): the f1 annotation is primitive-gated (emission-side // byte-compat), so a record-element literal (`[D373Score{…}]`) has NO buf entry — // yet the SCOPE needs the binding type (`mut scores = [Rec{…}]` → `[]Rec`), or the // checker cannot resolve prefix-generic methods on it (`scores.sort_of()`, // `fn[T Compare] []T @sort_of`) and codegen panics [P67-LEGACY]. Infer the element // from the FIRST Item when it resolves to a CONCRETE single-segment non-generic // Named type declared in `self.types` (record/sum/newtype — same conservatism as // `resolve_prefix_generic_method_return`'s receiver guard). Scope-only knowledge: // the literal's own emission stays legacy (no channel write here). // Un-annotated otherwise (empty / spread-first / generic elem) → None (legacy). ExprKind::ArrayLit(elems) => { if expr.id.is_set() { if let Some(rt) = self.resolved_types_buf.borrow().get(&expr.id) { return Self::resolved_to_typeref(rt, expr.span); } } let first_item = elems.iter().find_map(|el| match el { ArrayElem::Item(x) => Some(x), _ => None, })?; let elem_tr = self.infer_expr_type(first_item, scope)?; if let TypeRef::Named { path, generics, .. } = &elem_tr { if path.len() == 1 && generics.is_empty() && self.types.get(&path[0]).map_or(false, |td| td.generics.is_empty()) { return Some(TypeRef::Array(Box::new(elem_tr), expr.span)); } } None } // D238: `obj[i]` on user type with `@index` method → return type of @index. // Also handles Array/FixedArray element type for the infer_expr_type path // (f1_expr handles primitives via resolved_types_buf; this covers non-primitive // and user-defined @index which f1_expr does not annotate). ExprKind::Index { obj, index } => { let obj_tr = self.infer_expr_type(obj, scope)?; // Range index: slice result = same type as obj. // [M-fixed-array-value-semantics] (172.14, чинит регрессию // §18а-миграции hmac): срез `[N]T[a..b]` имеет РАНТАЙМ-длину → // результат `[]T`, не статически-размерный `[N]T` (правило // писалось до value-класса [N]T, когда obj был всегда []T). if matches!(index.kind, ExprKind::Range { .. }) { if let TypeRef::FixedArray(_, inner, sp) = &obj_tr { return Some(TypeRef::Array(inner.clone(), *sp)); } if let TypeRef::Readonly(ro_inner, _) = &obj_tr { if let TypeRef::FixedArray(_, inner, sp) = ro_inner.as_ref() { return Some(TypeRef::Array(inner.clone(), *sp)); } } return Some(obj_tr); } match &obj_tr { TypeRef::Array(inner, _) | TypeRef::FixedArray(_, inner, _) => { return Some(inner.as_ref().clone()); } TypeRef::Readonly(inner, _) => match inner.as_ref() { TypeRef::Array(e2, _) | TypeRef::FixedArray(_, e2, _) => { return Some(e2.as_ref().clone()); } _ => {} }, TypeRef::Named { path, .. } => { // 172.1.2 (Index @-слайс, 2026-07-03): scope["@"] extension-метода // на `[]int`/`[]T` хранит ИМЯ "[]int" как Named — элемент = // суффикс имени (Named{elem}); typevar пометит потребитель. if path.len() == 1 { if let Some(elem_name) = path[0].strip_prefix("[]") { // ТОЛЬКО конкретный элемент: примитив или объявленный // тип; голый typevar ([]T) → None (утекал bare Named{T} // в TypeRef-потоки → фабрикация Nova_T* в erased-телах). let concrete = ResolvedType::from_type_ref(&TypeRef::Named { path: vec![elem_name.to_string()], generics: vec![], span: expr.span, }); let is_concrete = Self::primitive_gate(&concrete) || self.types.contains_key(elem_name); if !elem_name.is_empty() && is_concrete { return Some(TypeRef::Named { path: vec![elem_name.to_string()], generics: vec![], span: expr.span, }); } return None; } } // User-defined @index(key K) -> V: look up @index method on type. // 2026-07-02 (audit POISON 6453): декларированный return клонировался // ДОСЛОВНО без подстановки generic-аргументов ресивера (`Vec[str][i]` // аннотировался голым Named{T}) и без учёта overload-произвола // find_method_decl (fns.first() из трёх @index Vec). Гейт: ТОЛЬКО // non-generic ресивер И конкретный (не Self, не typevar) return — // иначе None → legacy-навигация (subst — mono-канал 172.1.2). if let Some(type_name) = path.last() { let recv_is_generic = self .types .get(type_name) .map_or(true, |td| !td.generics.is_empty()); if recv_is_generic { return None; } if let Some(fd) = self.find_method_decl(type_name, "index") { if let Some(ret) = &fd.return_type { if let TypeRef::Named { path: rp, generics: rg, .. } = ret { if rp.len() == 1 && rg.is_empty() && rp[0] == "Self" { return None; } } return Some(ret.clone()); } } } } _ => {} } None } // Plan 172.1 P67 D45: Match expr type = first arm-body type that resolves. // Pattern bindings are not added to scope here; bodies referencing them get None // and we skip to the next arm. SelfAccess / scope-Ident arms resolve correctly. ExprKind::Match { arms, .. } => { for arm in arms { let ty = match &arm.body { MatchArmBody::Expr(e) => self.infer_expr_type(e, scope), MatchArmBody::Block(b) => b.stmts.iter().rev().find_map(|s| { if let Stmt::Expr(e) = s { self.infer_expr_type(e, scope) } else { None } }), }; if let Some(t) = ty { return Some(t); } } None } // Plan 172.1 P67 D45: If expr type. // Without else → Unit (statement-form if). With else → try then-block tail, // then else-branch. Guards against the checker annotating unit-if with a wrong type. ExprKind::If { then, else_, .. } => { if else_.is_none() { return Some(TypeRef::Unit(expr.span)); } // Try then-block tail first. let then_ty = then.stmts.iter().rev().find_map(|s| { if let Stmt::Expr(e) = s { self.infer_expr_type(e, scope) } else { None } }); if let Some(t) = then_ty { return Some(t); } // Fall through to else branch. match else_ { Some(ElseBranch::Block(b)) => b.stmts.iter().rev().find_map(|s| { if let Stmt::Expr(e) = s { self.infer_expr_type(e, scope) } else { None } }), Some(ElseBranch::If(e)) => self.infer_expr_type(e, scope), None => unreachable!(), } } // Plan 172.1 P67 D45: Block expr type = tail stmt type (last Expr stmt). ExprKind::Block(b) => b.stmts.iter().rev().find_map(|s| { if let Stmt::Expr(e) = s { self.infer_expr_type(e, scope) } else { None } }), _ => None, } } /// Plan 172.1 U.4.3(d) [M-172.1-U4.3d-generic-recv-infer]: resolve the DECLARED /// return type of a generic static-method call `Type[Targs].method(args)` from the /// signature registry, substituting the receiver's carrier generics (`Targs`) and /// `Self` → the concrete receiver type. This is the §0/§3 GENERAL mechanism that /// SUBSUMES the former name-keyed ctor hardcode `{new,with_capacity,from,default, /// filled}`: those Self-returning ctors resolve here to the SAME `Type[Targs]`, and /// ANY other static method (`of`, user ctors, non-Self returns) now resolves /// correctly too — no name list. Materialized into scope by the let-binding /// inference (`Stmt::Let` → `infer_expr_type`), it lets `ro b = GBox[int].of(0)` /// bind `b: GBox[int]`, unblocking the instance overload-dispatch channel /// (`check_instance_overload` → `resolved_callees`) for constructor-inferred /// receivers — previously gated on this; explicit-typed receivers already worked /// (bd1e5d66). /// /// PERMISSIVE (§7.3 calibration — zero false positives on the positive corpus): /// returns `None` (caller falls back to the intrinsic hardcode / `None`) on anything /// ambiguous — method not in the registry (intrinsic `Vec`), ≥2 overloads, non-static /// receiver, no declared return, turbofish/receiver arity mismatch, or a substituted /// return that still mentions an UNBOUND method-level generic (`fn Box[T].wrap[U](u U) /// -> U` — `U` comes from the arg, not the turbofish). /// Plan 196.5 Stage-B4: return also carries the ordered turbofish subst (§6.1 /// `node_substs` — Producer C). Declaration order = the receiver's carrier generic /// names (`recv.generics`, turbofish-supplied) THEN this static method's OWN /// method-level generics (`f.generics`) — mirrors `resolve_return_channel`'s /// `names.chain(method_names_ordered)` convention (9777) even though THIS call /// shape (`Type[T].method(args)`, ONE bracket, on the type) never supplies /// method-level values — see the completeness-gate comment at the tail of this fn. /// Реестр 221.1 №137 (`[M-reflect-generic-static-dispatch-collision]`): /// return tuple gained `Span` (the resolved single-overload `f`'s decl /// span) — codegen needs it to select the SAME `FnDecl` via /// `resolved_callees` (channel), instead of re-deriving "which overload" /// itself by a name-only scan of its own parallel registry /// (`generic_type_methods`). Additive: existing 2-tuple callers still /// destructure the first two elements; span is free (already in scope /// as `f.span`). fn resolve_generic_static_return( &self, tyname: &str, method: &str, type_args: &[TypeRef], span: Span, ) -> Option<(TypeRef, Vec<(String, ResolvedType)>, Span)> { // Synth-aware (base ∪ auto-derive overlay), same source the dispatch channel reads. let overloads = self.method_overloads(tyname, method)?; // Single overload only — ≥2 is permissive (codegen / hardcode fallback handle it, // byte-identical for the Self-returning ctor case). let [f] = overloads.as_slice() else { return None; }; let recv = f.receiver.as_ref()?; if !matches!(recv.kind, ReceiverKind::Static) { return None; } if recv.type_name != tyname { return None; } let ret = f.return_type.as_ref()?; // The turbofish must bind exactly the receiver's carrier generics. if type_args.len() != recv.generics.len() { return None; } let recv_ty = TypeRef::Named { path: vec![tyname.to_string()], generics: type_args.to_vec(), span, }; let mut subst: HashMap<String, TypeRef> = HashMap::new(); subst.insert("Self".to_string(), recv_ty); // Plan 196.5 Stage-B4: `carrier_names` — DECLARATION-order twin of the `subst` // insertions below (the `HashMap` has no stable order; `node_substs` needs // positional order for mono-manging, same rationale as `resolve_return_channel`'s // `names`, 9684). let mut carrier_names: Vec<String> = Vec::new(); for (i, g) in recv.generics.iter().enumerate() { if let TypeRef::Named { path, generics, .. } = g { if path.len() == 1 && generics.is_empty() { if let Some(ta) = type_args.get(i) { subst.insert(path[0].clone(), ta.clone()); carrier_names.push(path[0].clone()); } } } } let out = crate::const_fn_trampoline::subst_type_ref_pub(ret, &subst); // Don't materialize a HALF-resolved type: a method-level generic (`[U]`) of `f` // left in the result after substitution would bind the local to an unresolved // typevar — permissive bail. let bound: HashSet<&str> = subst.keys().map(String::as_str).collect(); let unbound: HashSet<String> = f .generics .iter() .map(|g| g.name.clone()) .filter(|n| !bound.contains(n.as_str())) .collect(); if !unbound.is_empty() && typeref_mentions_any(&out, &unbound) { return None; } // [M-196.5-node-substs] Producer C (Stage-B4): the turbofish `type_args` themselves // ARE the subst — read directly off `subst`, no inference (unlike Producer A's // args-based `unify_type` or Producer B's constraint solver). Declaration order = // `carrier_names` then `f.generics` (method-level) — but `subst` only HAS carrier // bindings (this AST shape has one bracket, on the TYPE; a static method's OWN // generics are never turbofish-supplied here). Same whole-map completeness gate as // Stage-A (`ordered.len() == decl_order.len()`): the map only reaches full // decl-order length when `f.generics` is empty — a static method that ALSO // declares method-level generics leaves the channel unwritten for this call-site // (genuine gap: filling it needs arg-based inference, out of THIS producer's // scope — mirrors the erased/partial-caller "stays unwritten" contract). let decl_order: Vec<&String> = carrier_names.iter().chain(f.generics.iter().map(|g| &g.name)).collect(); let ordered: Vec<(String, ResolvedType)> = decl_order .iter() .filter_map(|n| { subst.get(n.as_str()).map(|ta| ((*n).clone(), ResolvedType::from_type_ref(ta))) }) .collect(); // [M-196-ch-widen] SHADOW-ICE fix (see `rt_is_closed` doc): `type_args` is the // CALL-SITE's own turbofish literal (`Vec[K].new()`) — this fn has no `gs` // (enclosing FnDecl's generic scope) to check against, so a turbofish written // INSIDE a still-generic body (`Vec[K].new()` where `K` is the ENCLOSING // method's own abstract carrier, e.g. `Lru[K,V]`'s own body) would otherwise // materialize `K` as if concrete — the SAME hazard `resolve_return_channel` // had (Producer B). Same registry-based closedness gate, same degrade-to- // unwritten contract. let ordered = if !decl_order.is_empty() && ordered.len() == decl_order.len() && ordered.iter().all(|(_, v)| self.rt_is_closed(v)) { ordered } else { Vec::new() }; Some((out, ordered, f.span)) } /// [реестр 221.1 №126, `[M-static-generic-method-path-call-p67-panic]`] Sibling of /// `resolve_generic_static_return` for a DIFFERENT AST/generic axis: `Type.method[T] /// (args)` — the explicit turbofish sits on the METHOD name, binding the static /// method's OWN generics (`f.generics`), not the receiver TYPE's carrier generics /// (`Type[T].ctor()` — `resolve_generic_static_return`'s territory, the structurally /// different `Member{obj:TurboFish}` AST shape vs this call's `TurboFish{base:Member}`). /// Before this producer existed, NEITHER channel covered the call: `resolve_instance_ /// method_return_arity` (the generic instance-call path `infer_method_call_channel_type` /// otherwise reaches for a `TurboFish{base:Member}` call) bails unconditionally on a /// `ReceiverKind::Static` receiver (instance-only by design), and `resolve_generic_ /// static_return` bails on the carrier-generics arity mismatch (0 carrier generics vs /// the method's own). Codegen's legacy `infer_call_ret_c` has no bucket for this shape /// either → unconditional `[P67-LEGACY]` panic. Narrowed to a receiver type with NO /// carrier generics of its own (`recv.generics.is_empty()`) so the turbofish arg count /// unambiguously binds `f.generics` — a generic RECEIVER type ADDITIONALLY carrying its /// own generic static method (`Type[G].method[M]`, a double-turbofish call) is a /// distinct, unhandled shape, left to legacy (permissive bail, no regression). fn resolve_generic_static_method_own_return( &self, tyname: &str, method: &str, type_args: &[TypeRef], span: Span, ) -> Option<(TypeRef, Vec<(String, ResolvedType)>, Span)> { // [реестр 221.1 №126, harden] primitives have their own dedicated static- // method resolution/rejection pipeline (E_UNKNOWN_STATIC_METHOD et al.) — // never let a user-type generic-static producer speak for a primitive name // (would risk resurrecting a RETRACTED primitive static, e.g. `str.from`, // D410, if `method_overloads` ever mis-keys a blanket entry under it). if is_primitive_recv_name(tyname) { return None; } let overloads = self.method_overloads(tyname, method)?; let [f] = overloads.as_slice() else { return None; }; let recv = f.receiver.as_ref()?; if !matches!(recv.kind, ReceiverKind::Static) { return None; } if recv.type_name != tyname { return None; } if !recv.generics.is_empty() { return None; // ambiguous double-turbofish shape — decline, leave to legacy. } if type_args.len() != f.generics.len() { return None; } let recv_ty = TypeRef::Named { path: vec![tyname.to_string()], generics: Vec::new(), span, }; let mut subst: HashMap<String, TypeRef> = HashMap::new(); subst.insert("Self".to_string(), recv_ty); let mut method_names_ordered: Vec<String> = Vec::new(); for (g, ta) in f.generics.iter().zip(type_args.iter()) { subst.insert(g.name.clone(), ta.clone()); method_names_ordered.push(g.name.clone()); } // No declared `->` (implicit-Unit body, e.g. `fn Type.show[T](x T) { ... }`) — // the call's return is simply Unit, no subst needed at all. let ret = match f.return_type.as_ref() { Some(r) => r.clone(), None => return Some((TypeRef::Unit(span), Vec::new(), f.span)), }; let out = crate::const_fn_trampoline::subst_type_ref_pub(&ret, &subst); // Don't materialize a half-resolved type: bail if the substituted return still // mentions any of this method's own generics (should be unreachable given the // arity match above — defensive, mirrors the sibling fn's own gate). let bound: HashSet<&str> = subst.keys().map(String::as_str).collect(); let unbound: HashSet<String> = f.generics.iter() .map(|g| g.name.clone()) .filter(|n| !bound.contains(n.as_str())) .collect(); if !unbound.is_empty() && typeref_mentions_any(&out, &unbound) { return None; } let ordered: Vec<(String, ResolvedType)> = method_names_ordered.iter() .filter_map(|n| subst.get(n.as_str()).map(|ta| (n.clone(), ResolvedType::from_type_ref(ta)))) .collect(); let ordered = if !ordered.is_empty() && ordered.iter().all(|(_, v)| self.rt_is_closed(v)) { ordered } else { Vec::new() }; Some((out, ordered, f.span)) } /// Plan 200 П19: peel `ro`/`mut` TYPE wrappers off a receiver `TypeRef` and, if the /// core shape is `[N]T` (`TypeRef::FixedArray`), return `(N, &elem)`. Mirror of the /// peel-loop `resolve_instance_method_return_arity`/`check_instance_overload` already /// run before their "Vec" name-normalization (§3 one-window: same structural test, not /// a second copy) — kept as its own tiny fn because the `@len`/`@ptr` synthesis below is /// called from two sites (inline `infer_expr_type` + the channel producer) that each do /// their OWN peel for unrelated reasons first. fn peel_fixed_array(ty: &TypeRef) -> Option<(usize, &TypeRef)> { match ty { TypeRef::FixedArray(n, inner, _) => Some((*n, inner.as_ref())), TypeRef::Readonly(i, _) | TypeRef::Mut(i, _) => Self::peel_fixed_array(i), _ => None, } } /// Plan 200 П19 (`[N]T @len()` / `@ptr()`): compiler-SYNTHESIZED FixedArray accessors — /// same class of magic as the D238 `arr[i]` FixedArray read (checker `ExprKind::Index` /// arm ~8991, codegen `parse_mono_fixed_array_name` ~32415): no `.nv` `FnDecl` exists for /// either method (method-level const-generic `N` is not in the language — Ш0 probe /// `fn [4]u8 @probe() -> int => 4` fails to parse, "expected identifier, got int /// literal" — confirmed 2026-07-21), so the return type is synthesized STRUCTURALLY from /// `(n, inner)` here rather than looked up in `method_table`. `is_mut` is the call-site /// receiver's mutability (`!is_through_ro_binding(obj)`, same predicate D175/D326 already /// use) — selects `*T` (ro, D246 default) vs `*mut T` for `@ptr()`, mirroring Vec's real /// `@ptr()`/`mut @ptr()` overload pair (`access.nv:262/270`). Returns `None` for any /// other method name (falls through unchanged to the normal Vec-normalized dispatch — /// zero interference with real Vec/Array methods, which stay on their existing path). fn fixed_array_accessor_return( n: usize, inner: &TypeRef, method: &str, is_mut: bool, span: crate::diag::Span, ) -> Option<TypeRef> { match method { "len" => Some(TypeRef::Named { path: vec!["int".to_string()], generics: Vec::new(), span, }), "ptr" => { let _ = n; // N doesn't affect the accessor's TYPE, only codegen's C body. let pointee = if is_mut { TypeRef::Mut(Box::new(inner.clone()), span) } else { inner.clone() }; Some(TypeRef::Pointer(Box::new(pointee), span)) } _ => None, } } /// Plan 172.1.2 [M-172.1-U4-recv-infer]: resolve the DECLARED return type of an /// INSTANCE method call `obj.method(...)` from the signature registry, given the /// receiver's already-resolved type `recv_ty`. Substitutes the receiver's carrier /// generics (`Vec[int]` → `T`=`int`) and `Self` → the concrete receiver. This is the /// §0/§3 receiver-inference substrate: woven into `infer_expr_type`'s Call arm it makes /// method-call types RECURSIVE, so a chain `a.b().c()` resolves and a /// `let v = obj.method()` binds the real type into scope. /// /// PERMISSIVE (§7.3 — zero false positives on the positive corpus): returns `None` on /// anything ambiguous — receiver not a single named/slice type, method absent from the /// registry (intrinsic with no `.nv` decl), ≥2 overloads (multi-overload arg-dispatch /// is step 2), a STATIC receiver, no declared return, or a substituted return still /// mentioning an UNBOUND method-level generic (`fn V[T].map[U](f) -> Vec[U]` — `U` comes /// from the arg, not the receiver). Mirror of `resolve_generic_static_return` for the /// instance side. fn resolve_instance_method_return( &self, recv_ty: &TypeRef, method: &str, ) -> Option<TypeRef> { // No live caller today (kept for API symmetry with `resolve_generic_static_return` // — see that fn's doc); no enclosing `gs` to thread, so the Plan 196 gs-bounds // fallback below is a no-op here (empty scope), not a regression. let empty_gs: GenericScope = HashMap::new(); self.resolve_instance_method_return_arity(recv_ty, method, None, None, None, None, &empty_gs) } /// 172.1.2 (2026-07-03): arity-aware вариант — при >1 перегрузке выбирает /// ЕДИНСТВЕННУЮ с совпадающим числом параметров (StringBuilder.append и др.). fn resolve_instance_method_return_arity( &self, recv_ty: &TypeRef, method: &str, arity: Option<usize>, // 172.1.2 arg-type dispatch (2026-07-04): (args, scope) для // дизамбигуации same-arity перегрузок по типу первого аргумента. args_scope: Option<(&[CallArg], &HashMap<String, TypeRef>)>, // Plan 196.5 Stage-A: call-site `ExprId`, threaded down ONLY so the producer-B // `node_substs` write (inside the method-generic-residual branch AND the pure-carrier // branch below) has a key. `None` from the 0-arity wrapper above (no call-site to key // by — that wrapper has no callers today, kept for API symmetry with // `resolve_generic_static_return`). call_id: Option<crate::ast::ExprId>, // [M-196-producer-b-turbofish] (Plan 196 Producer B): explicit METHOD-level // turbofish from an `obj.method[U](args)` call-site (positional, aligned to // `f.generics` declaration order) — threaded through to every // `resolve_return_channel` call below as ground-truth overlay (see that fn's doc). // `None` from every pre-existing caller (inferred-only instance calls). explicit_type_args: Option<&[TypeRef]>, // Plan 196 gs-bounds: the CALLER's own generic-scope (bounds-carrying, not just // names) — consulted ONLY as the LAST fallback below, when the receiver turns out // to be a bare generic-param-in-scope with no real method_table/self.types entry // (a genuine typevar, e.g. `v: T` inside `Option[T Debug]@debug`'s body calling // `v.debug(f)`). Threading an EMPTY scope is always safe (no false positives — // just misses the new fallback, same as before this param existed). gs: &GenericScope, ) -> Option<TypeRef> { // Normalize receiver: peel ro/mut views; `[]T`/`[N]T` → "Vec" (D239 slice alias), // so a slice receiver resolves Vec's methods (std's pervasive spelling). Mirror of // `check_instance_overload`'s normalization. let mut peeled = recv_ty; loop { match peeled { TypeRef::Readonly(i, _) | TypeRef::Mut(i, _) => peeled = i, _ => break, } } let type_name: String = match peeled { TypeRef::Named { path, .. } if path.len() == 1 => { // 2026-07-03: scope["@"] extension-метода на слайсе хранит имя // "[]T"/"[]int" — D239-нормализация к Vec для method-резолва. if path[0].starts_with("[]") { "Vec".to_string() } else { path[0].clone() } } TypeRef::Array(_, _) | TypeRef::FixedArray(_, _, _) => "Vec".to_string(), _ => return None, }; // Plan 174.1 (2026-07-08) [M-174.1-concrete-slice-recv-method-resolve]: // a CONCRETE-slice-receiver method (`fn []u8 @to_str()`, `fn []str // @join(..)`, `fn []int @sum()` — std/sort.nv, std/text.nv, // std/runtime/string) is registered in `method_table` under its LITERAL // spelling ("[]u8"), while the "Vec" normalization above discards the // element. The "Vec" lookup therefore misses and the checker never // annotated `resolved_types` — surfacing as the long-standing // `[P67-LEGACY] method call return type unknown` ICE in fn-main CUs // (test-CU builds masked it via codegen's name-keyed `fn_ret_<method>` // fallback). Reconstruct the literal "[]<elem>" key from the receiver's // element type and retry, so the CHECKER resolves + annotates (§0). let slice_key: Option<String> = match peeled { TypeRef::Named { path, .. } if path.len() == 1 && path[0].starts_with("[]") => { Some(path[0].clone()) } // №252 [M-bytes-to-str-chain-misresolves-universal-str-conv]: a // `[]T`-typed scope value (e.g. `"hi".bytes()`'s result) is // stored canonicalized as `Named{"Vec", [T]}` (D239 alias), not // the raw `Array`/FixedArray` shape below — mirror // `check_instance_overload`'s `array_elem_key` (~L12734-12736) // so a chain call (`bytes.to_str().map_err(..)`) finds the SAME // `[]u8`-spelled concrete facade method // (`string/core.nv:274`) that `check_instance_overload` already // resolves for a bare/match-scrutinee use of the same call — // without this arm the retry below silently misses and falls // through to the generic `fn[T] T @to_str() -> str` blanket. TypeRef::Named { path, generics, .. } if path.len() == 1 && path[0] == "Vec" && generics.len() == 1 => { Some(format!("[]{}", render_type_ref(&generics[0]))) } TypeRef::Array(inner, _) | TypeRef::FixedArray(_, inner, _) => { let mut e: &TypeRef = inner; loop { match e { TypeRef::Readonly(i, _) | TypeRef::Mut(i, _) => e = i, _ => break, } } match e { TypeRef::Named { path, generics, .. } if path.len() == 1 && generics.is_empty() => { Some(format!("[]{}", path[0])) } _ => None, } } _ => None, }; let overloads = match self.method_overloads(&type_name, method).or_else(|| { slice_key .as_deref() .filter(|k| *k != type_name.as_str()) .and_then(|k| self.method_overloads(k, method)) }) { Some(o) => o, None => { // 196.5 Stage-D wave-2 (дыра-1, D1H1 шаг 3): `unwrap`-семья на // Option/Result — РЕТРАКТИРОВАННЫЕ методы (D85/D86, prelude // `[M-unwrap-twins-retraction]`): деклараций в .nv НЕТ, но вызовы // живут (conformance d119_*, m196_*; desugar map-spread СИНТЕЗИРУЕТ // `.get(k).unwrap()`) и обслуживаются исключительно codegen-легаси // хардкодом (`emit_c.rs` B11q/B11r match-армы: `"unwrap" | // "unwrap_or" | "unwrap_or_else" => elem_ty/ok_c`). Продюсер канала // работает от деклараций → для undeclared метода канал co-miss // ПО ПОСТРОЕНИЮ. Зеркалируем тот же фундаментальный факт // (`Option[T]→T`, `Result[T,E]→T`) на уровне чекера — НЕ язык- // меняющее (поведение уже такое), просто материализация в канал. // Гейт: конкретный (не param) первый generic — residual пометит // mark_type_params на Call-арме, как у всех продюсеров. if matches!(method, "unwrap" | "unwrap_or" | "unwrap_or_else") { if let TypeRef::Named { path, generics, .. } = peeled { if path.len() == 1 && ((path[0] == "Option" && generics.len() == 1) || (path[0] == "Result" && generics.len() == 2)) { return Some(generics[0].clone()); } } } // 172.1.2 (protocol-ресивер, 2026-07-03): `w Writer` — сигнатура // метода живёт в декларации ПРОТОКОЛА (D53), не в method_table. // Konkretный declared return (без generics/Self) — фундаментальный // факт контракта; residual → None (честно). if let Some(td) = self.types.get(&type_name) { if let TypeDeclKind::Protocol { methods, .. } = &td.kind { let mut it = methods.iter().filter(|m| { m.name == method || m.name.trim_start_matches('@') == method }); if let (Some(m), None) = (it.next(), it.next()) { if m.generics.is_empty() { let ret = match &m.return_type { Some(r) => r.clone(), None => TypeRef::Unit(m.span), }; let mentions_self = matches!(&ret, TypeRef::Named { path, .. } if path.len() == 1 && path[0] == "Self"); if !mentions_self { return Some(ret); } return Some(peeled.clone()); } } } } // Plan 172.1 D145/D282: prefix-generic receiver fallback. if let Some(ret) = self.resolve_prefix_generic_method_return(peeled, method) { return Some(ret); } // Plan 196 gs-bounds (generalizes `resolve_generic_bound_method_return`, // the match-scrutinee-only sibling below, to EVERY instance-call receiver // now that `gs` carries bounds, not just names — see that fn's doc for the // shared root cause, `docs/plans/196-one-truth-closeout.md` B11q/B11r). return self.resolve_generic_bound_receiver_method(peeled, method, arity, gs); } }; // Single overload; при >1 — arity-фильтр (172.1.2): единственная // перегрузка с params.len()==arity выбирается однозначно. let f: &FnDecl = match overloads.as_slice() { [one] => one, many => { // Унисон-правило (2026-07-03): если ВСЕ перегрузки — fluent `-> @` // (StringBuilder.append(str/int/char/…)), возврат идентичен // независимо от выбора → тип эха = ресивер, дизамбигуация не нужна. if many.iter().all(|f| { f.returns_receiver && f.receiver.as_ref().map_or(false, |r| { matches!(r.kind, ReceiverKind::Instance) }) }) { return Some(peeled.clone()); } let a = arity?; let cands: Vec<&&FnDecl> = many.iter().filter(|f| f.params.len() == a).collect(); match cands.as_slice() { [] => return None, [one] => *one, same_arity => { // 172.1.2 arg-type dispatch: тип ПЕРВОГО аргумента против // КОНКРЕТНОГО param0 кандидатов — ровно одно совпадение. let (cargs, cscope) = args_scope?; let a0 = cargs.first()?.expr(); let a0_rt = self .infer_expr_type(a0, cscope) .map(|t| ResolvedType::from_type_ref(&t)) .or_else(|| { if !a0.id.is_set() { return None; } self.resolved_types_buf.borrow().get(&a0.id).cloned() })?; let mut hit: Option<&FnDecl> = None; for f in same_arity { let Some(p0) = f.params.first() else { continue }; let p0_rt = ResolvedType::from_type_ref(&p0.ty); // только КОНКРЕТНЫЙ param0 (примитив/Str/Bool/Named // без args) — generic-кандидаты не участвуют. let concrete = Self::primitive_gate(&p0_rt) || matches!(&p0_rt, ResolvedType::Str | ResolvedType::Bool) || matches!(&p0_rt, ResolvedType::Named { args, .. } if args.is_empty()); if !concrete { continue; } if p0_rt == a0_rt { if hit.is_some() { return None; } // неоднозначно hit = Some(f); } } hit? } } } }; let recv = f.receiver.as_ref()?; if !matches!(recv.kind, ReceiverKind::Instance) { return None; } // 172.1.2 (fluent-return, 2026-07-03): `-> @` (returns_receiver) — тип // эха = тип ресивера (D132/D181-факт); return_type при этом None. // // [M-196-ch-coverage2, Producer B-fluent-generic] The `-> @` echo above // is BY CONSTRUCTION independent of whether `f` ALSO declares its OWN // method-level generics (`Vec[T] mut @append[S AsSlice[T]](other S) -> // @`) — `S` is bound purely from the call's ARGUMENTS, never from the // return. Before this fix, that meant every fluent-with-own-generics // call-site NEVER reached `resolve_return_channel` at all (this fn // returned two lines below, before any method-generic resolution ran) // — a permanent, structural `node_substs` miss for the WHOLE class, // regardless of how concrete the call-site actually was. Empirically // confirmed the dominant Stage-B2 (`resolve_method_level_subst`) // `reason=miss` class on the std/collections corpus (`docs/plans/ // wip/196-ch-coverage2-notes.md` baseline): `Vec[byte].append` // alone ~300/311 raw fallback hits. Route through the SAME // constraint-solver channel (`resolve_return_channel`) the non-fluent // branches below already use, passing `peeled` itself as the `ret` // template — its own role is only to seed `ret_template` (unused here, // `rt` is discarded: this fn's return stays the unconditional receiver // echo, byte-parity preserved) so the SAME solver call also unifies // `extra_eqs` (method-generic param↔arg) and resolves `ordered` — the // per-declared-name (carrier ++ method) map `resolve_method_level_subst` // needs. Gate: `f.generics` non-empty (nothing to gain otherwise) + // args available (`args_scope` — the 0-arity wrapper has no args to // bind from, mirrors every other producer's contract) + the solver // INDEPENDENTLY closes EVERY carrier AND method-level name // (`resolve_return_channel`'s own whole-map + `rt_is_closed` gates, // `ordered` stays empty on ANY residual — propose-then-verify, no new // trust surface, zero risk to the unconditional echo below). if f.returns_receiver { if !f.generics.is_empty() { if let (Some(cid), Some(_)) = (call_id, args_scope) { let method_names: HashSet<String> = f.generics.iter().map(|g| g.name.clone()).collect(); let method_names_ordered: Vec<String> = f.generics.iter().map(|g| g.name.clone()).collect(); if let Some((_, ordered)) = self.resolve_return_channel( recv, recv_ty, peeled, peeled, &method_names, &method_names_ordered, &f.params, args_scope, explicit_type_args, ) { if !ordered.is_empty() { if std::env::var_os("NOVA_NODE_SUBSTS_TRACE").is_some() { eprintln!( "[NODE_SUBSTS] producer=B-fluent-generic call_id={:?} \ method={} n={}", cid, f.name, ordered.len() ); } self.node_substs.borrow_mut().insert(cid, ordered); } } } } return Some(peeled.clone()); } // [196.5 Stage-D волна-4] B11ag producer (external_registry feeds the // checker): an `extern "nova"` INSTANCE method declared WITHOUT a `->` // return annotation (`extern "nova" fn Once mut @call_once(body fn() // -> ())`) returns Unit — that IS its declared return. The bare // `f.return_type.as_ref()?` below would BAIL to None on it (co-miss → // legacy `infer_call_ret_c` B11ag extern-registry arm). Materialize the // Unit the declaration states, so Channel 2 covers it. Gated on // `f.is_external` to stay scoped to the extern-registry class (a // non-extern user method with an implicit-Unit body is left to legacy — // narrow byte-identity blast radius); `returns_receiver` was already // handled above, so reaching here with `return_type == None` on an // extern method is unambiguously the Unit case. if f.return_type.is_none() { if f.is_external { return Some(TypeRef::Unit(f.span)); } return None; } let ret = f.return_type.as_ref()?; // Subst: receiver carrier generics (`Vec[int]` → T=int) + `Self` → concrete receiver. let mut subst = build_recv_subst(recv, recv_ty); subst.insert("Self".to_string(), peeled.clone()); let out = crate::const_fn_trampoline::subst_type_ref_pub(ret, &subst); // Don't materialize a HALF-resolved type: an unbound method-level generic (`[U]`) // left after substitution → permissive bail (its binding comes from the args, which // arg-type-inference would resolve — step 2 territory). // Plan 172.1.2 — FULLY-CONCRETE gate (alias_tagged/Pair6 + plan154 fixes): bail if the // substituted return STILL mentions ANY receiver-carrier (`recv.generics`) or method-level // (`f.generics`) generic. This catches BOTH (a) an UNBOUND generic — `build_recv_subst` // had no concrete arg (a `Pair6{…}` RecordLit receiver inferred WITHOUT generics → `-> B` // leaks as codegen `Nova_B*`, a §1 wrong-type CC-FAIL — str-equality routing skipped → // "invalid operands"), AND (b) a VACUOUSLY-bound one — a generic-typed receiver binds // `T`→`T`, so a `-> *mut T` return stays `*mut T` → codegen erased-stub `Nova_T**` // (plan154's `into_raw` on a buffer whose element type the checker left as the carrier). // Only a return with NO residual type-param is context-independent enough to channel // (codegen lowers it without `current_type_subst`); the original guard checked merely the // METHOD generics against `subst.keys()`, missing both carrier classes. Carrier names are // extracted the SAME way `build_recv_subst` does (bare single-segment, no nested generics). // 172.1.2 Шаг 3.2: гейт РАСЩЕПЛЁН. (a) METHOD-level generic (`[U]` — биндинг // из аргументов) — Plan 196.4 Stage-1a больше НЕ безусловный bail (см. ветку // ниже): args-driven унификация пробует связать `U` из вызова, прежде чем // сдаться на legacy. (b) RECEIVER-carrier residual (`-> T`, `-> *mut T` при // vacuous T→T) → БОЛЬШЕ НЕ bail: имя ∈ gs объемлющего тела → Call-арм // пометит его mark_type_params → TypeParam(T) → лоуэринг // receiver-instance-map/subst или Err→legacy (Шаг 1/2b инфраструктура). let method_names: HashSet<String> = f.generics.iter().map(|g| g.name.clone()).collect(); // Plan 196.5 Stage-A: declaration-order twin of `method_names` (see // `resolve_return_channel`'s doc comment) — `node_substs` needs positional order, // the `HashSet` above does not preserve it. let method_names_ordered: Vec<String> = f.generics.iter().map(|g| g.name.clone()).collect(); if !method_names.is_empty() && typeref_mentions_any(&out, &method_names) { // Plan 196.4 Stage-1a: `out` (carrier+`Self` only, `build_recv_subst`) // still carries an unbound METHOD-level generic — bound by the CALL'S // ARGUMENTS, which `build_recv_subst` never sees (the Tier-2 gap // `docs/plans/196.4-call-resolvedtype-channel.md` §1/§8 identifies: // `Result[T,E].map[U](...)`-shaped instance-method returns). Bind it // the SAME way the free-fn generic-return arm does (`f1_check_call` // `10452`-`10492`): structural `unify_type` per non-variadic // (declared param, arg) pair whose declared type mentions a method // generic, seeded with the carrier+`Self` subst already computed // above. Needs the call's arguments — absent (`args_scope: None`, // the 0-arity `resolve_instance_method_return` wrapper) → honest // bail, unchanged legacy behavior. let Some((call_args, call_scope)) = args_scope else { return None }; let mut full_subst = subst.clone(); // [M-196-producer-b-turbofish] Seed from an explicit method-level turbofish // (`obj.method[U](args)`) BEFORE the args/closure-derived pass below — ground // truth, positional against `f.generics` (mirrors the free-fn D310 overlay and // the legacy codegen `explicit_tf` seed, `emit_c.rs` ~21347). Handles the class // `unify_type` structurally CANNOT: a method generic that appears ONLY in // RETURN position (no param mentions it, e.g. `fn Reg @empty[T]() -> Vec[T]`) — // the args loop below has nothing to unify against, so without this seed // `out_full` stays unresolved and the whole branch honestly bails, exactly the // `m176_method_return_turbofish` regression class this mirrors the FIX for. // `unify_type` never overwrites an already-bound name (see its "already bound — // must match" arm) — a param that ALSO mentions this generic can only confirm or // silently no-op on conflict, never diverge from the explicit annotation. if let Some(explicit) = explicit_type_args { for (g, ta) in f.generics.iter().zip(explicit.iter()) { full_subst.entry(g.name.clone()).or_insert_with(|| ta.clone()); } } for (p, a) in f.params.iter().zip(call_args.iter()) { if p.is_variadic { break; } if !typeref_mentions_any(&p.ty, &method_names) { continue; } let p_seeded = crate::const_fn_trampoline::subst_type_ref_pub(&p.ty, &subst); if let Some(a_ty) = self.infer_expr_type(a.expr(), call_scope) { let _ = crate::const_fn_trampoline::unify_type( &p_seeded, &a_ty, &method_names, &mut full_subst); } else if let TypeRef::Func { params: fpp, return_type: Some(fr), .. } = &p_seeded { // [M-196.5 producers-widen, Class-1] closure-return-bound: the // COMMON `.map(|x| …)`/`Option.map`/`Result.map` shape — arg is a // closure LITERAL, `infer_expr_type` has no arm for it (`a_ty` is // always `None`). `p_seeded` is already carrier+`Self`-concrete // (from `subst` above), so `fpp` only still names UNRESOLVED // method-level generics (checked by `closure_arg_return_peek` // itself) — peek the closure body's return type and unify just // the return position (`fr`, typically a bare method generic like // `U`) against it. Mirrors the free-fn arm's identical fallback in // `f1_check_call` (~10738). if let Some(body_ty) = self.closure_arg_return_peek(fpp, a.expr(), call_scope, &method_names) { let _ = crate::const_fn_trampoline::unify_type( fr, &body_ty, &method_names, &mut full_subst); } } } let out_full = crate::const_fn_trampoline::subst_type_ref_pub(ret, &full_subst); if typeref_mentions_any(&out_full, &method_names) { return None; // still unresolved from the args — honest bail } // Gate: require the constraint-solver channel (`resolve_return_ // channel`, Stage-1a-extended with `Constraint::Eq` param↔arg on // fresh vars — anti-d119) to INDEPENDENTLY reproduce this binding // before materializing. This class had NO legacy value before // Stage-1a (this branch previously ALWAYS bailed to `None`) — the // solver's agreement IS the correctness gate (propose-then-verify), // checked in EVERY build (not a debug-only assert): there is // nothing else to trust a release build's materialization against. let channel = self.resolve_return_channel( recv, recv_ty, peeled, ret, &method_names, &method_names_ordered, &f.params, args_scope, explicit_type_args); return match channel { Some((rt, ordered)) if rt == ResolvedType::from_type_ref(&out_full) => { // [M-196.5-node-substs] SHADOW-adjacent write: the solver // INDEPENDENTLY re-derived this binding (propose-then-verify gate // above just confirmed `rt == out_full`) — the per-param `ordered` // map is the SAME solver's per-var bindings, so it inherits the // same verified trust. `call_id` absent (0-arity wrapper) → skip. if let Some(cid) = call_id { if !ordered.is_empty() { if std::env::var_os("NOVA_NODE_SUBSTS_TRACE").is_some() { eprintln!( "[NODE_SUBSTS] producer=B-method-residual call_id={:?} method={} n={}", cid, f.name, ordered.len() ); } self.node_substs.borrow_mut().insert(cid, ordered); } } Some(out_full) } _ => None, }; } // Plan 172.1.2 (plan154 / self_nested / flatten fix): bail when the return lowers to a // mono-instantiated CONTAINER/slice/tuple (`Array` / `Named`-with-generics / `Tuple`). // Resolving such a return (a) for the CHANNEL registers a FRESH monomorphization via // `resolved_type_to_c` side-effects, perturbing the binary layout — and a latent // conservative-GC root-finding bug is layout-sensitive (plan154's deterministic segfault // in an aggregated sibling test), and (b) for INLINE use types a method-chain receiver // (`v.iter()` → `VecIter[int]`) that then unblocks a FALSE-POSITIVE extension-method // policy check in `f3_check_member` (self_nested). It also catches a mis-substituted // NESTED receiver (`Vec[Vec[T]].flatten` flat-zips `T`→`Vec[str]` → wrong `Vec[Vec[str]]`, // a `Named`-with-generics). KEEP only primitive / concrete NON-generic value-type returns // (context-independent C-spelling, no `____`-mono) — the conservative §0/§1 receiver- // inference win the plan targets; container returns wait for the typed-IR mono path. let mut peeled_out = &out; loop { match peeled_out { TypeRef::Readonly(i, _) | TypeRef::Mut(i, _) => peeled_out = i, _ => break, } } match peeled_out { // Plan 172.1 Session C ARM 2: Tuple channeling (bail снят, R::Tuple-арм all_concrete-guard). // ARM 3/4 (Array/FixedArray): ЭКСПЕРИМЕНТ — bail снят для энумерации mono-subst gaps через // NOVA_BAIL_GAPLOG (proba D). Array/FixedArray падают в `_ => {}` → Some(out) (каналятся). // Если segfault (Boehm layout) / регресс — gaps закрываются рецепт-портом (emit_c 6131-6143) // ПЕРЕД финальным снятием. build_recv_subst flatten-fix (15371) корректит nested receiver. // Plan 172.1 Session C ARM 1 (RE-ATTEMPT после build_recv_subst flatten-fix): Named-with- // generics container-return КАНАЛИЗИРУЕТСЯ. Теперь SAFE — build_recv_subst (15371) // структурно унифицирует nested receiver (`Vec[Vec[T]]`→`T→str`, не `Vec[str]`), так что // `out` корректен (flatten больше не даёт `Vec[Vec[str]]`). Разблокирует P4b/Ф.3 для Named. _ => {} } // Plan 196 Ф.4c: route this simple (single-overload, concrete-receiver) // carrier-binding + return-instantiation through the constraint solver's // `resolve_return` primitive (§0) — the Ф.4b `project_channel` mirror for // the resolve-family. The EMITTED ANNOTATION for this call is // `from_type_ref(out)`, which the solver channel independently reproduces: // the carrier-binding rule now lives in the SOLVER (fresh `TypeVar`s, anti- // d119), no longer only in the name-keyed `build_recv_subst`. We keep the // legacy TypeRef `out` as the returned CARRIER (not the solver's resolved // type reconstructed back to a TypeRef) precisely because `from_type_ref` // is NON-injective (`[]T ↦ Vec[T]`, `mut`/`ref` transparent): a rebuilt // TypeRef could pick a chain-divergent receiver spelling for an OUTER // `a.b().c()` (this fn's result feeds the recursive receiver resolution). // Byte-identical BY CONSTRUCTION — `out` is returned unchanged; the solver // is consulted read-only and its agreement asserted. // // Plan 196.4 Stage-1a: this pure-carrier path is DELIBERATELY kept SHADOW // (not flipped to return a solver-reconstructed TypeRef) — `ResolvedType:: // from_type_ref` canonicalizes `[]T` to `Named{Vec,[T]}` (D239, `168`-`218` // above), which is NON-invertible: reconstructing a TypeRef from the // channel's `ResolvedType` for a slice-shaped carrier binding would // relabel `[]T` spelling to `Vec[T]`, risking the LITERAL `"[]elem"`-keyed // concrete-slice method lookup a CHAIN receiver resolve can depend on // (`13617`-`13650`, D174.1) — exactly the hazard this comment already // flags. Since `out` and the channel already AGREE here (the assert below // never trips on the corpus), swapping buys no new coverage and only // reintroduces that documented regression class; the flip is applied // instead where it unlocks NEW coverage — the method-generic-residual // branch above, gated on independent solver agreement (not a debug-only // assert), which is Stage-1a's actual Tier-2 deliverable. let channel = self.resolve_return_channel( recv, recv_ty, peeled, ret, &method_names, &method_names_ordered, &f.params, args_scope, explicit_type_args); debug_assert!( channel .as_ref() .map_or(true, |(s, _)| *s == ResolvedType::from_type_ref(&out)), "Ф.4c: resolve_return diverged from build_recv_subst binding" ); // [M-196.5-node-substs] Same channel/solver as the debug_assert above — write // node_substs ONLY when the return independently agrees with legacy `out` (mirrors // the assert's parity contract; checked unconditionally, not debug-only, since this // is a brand-new additive channel nothing reads yet — costs nothing in release and // keeps both channels' trust level identical). if let (Some(cid), Some((rt, ordered))) = (call_id, &channel) { if !ordered.is_empty() && *rt == ResolvedType::from_type_ref(&out) { if std::env::var_os("NOVA_NODE_SUBSTS_TRACE").is_some() { eprintln!( "[NODE_SUBSTS] producer=B-carrier call_id={:?} method={} n={}", cid, method, ordered.len() ); } self.node_substs.borrow_mut().insert(cid, ordered.clone()); } } let _ = channel; Some(out) } /// Plan 196 gs-bounds: resolve an instance-method call whose RECEIVER is a bare /// generic-parameter-in-scope (`v: T`) via `T`'s declared PROTOCOL bound — e.g. /// `v.debug(f)` inside `Option[T Debug]@debug`'s own body (`std/src/prelude/ /// protocols.nv`), where `v`'s type is the plain typevar `T`, not a real type, so /// neither `method_overloads` nor `self.types.get(type_name)` above find anything. /// /// This is the SAME pattern as [`resolve_generic_bound_method_return`] below — /// which is scoped, by its own doc, to ONLY the match-scrutinee call site (reading /// `current_fn_generics`, a `RefCell<Vec<GenericParam>>` populated for exactly this /// narrow need) — generalized to ANY instance-call receiver, now that `gs` itself /// carries bounds (Plan 196 gs-migration) instead of bare names. Root cause and /// scope measured in `docs/plans/wip/196-gs-spike.md` / `196-one-truth-closeout.md` /// (B11q/B11r `Nova_Option_method_debug_*` legacy dispatch): before this fallback /// existed, ANY protocol-bound-dispatched call on a bare generic receiver missed /// Channel 2 entirely and fell to codegen's name-pattern-matching legacy arms. /// /// Narrow, permissive, additive-only — reached ONLY after every concrete-type /// resolution path above (and [`resolve_prefix_generic_method_return`]) has /// already missed, so it cannot divert an existing concrete resolution: /// - first matching bound (bounds are a conjunction — D145 multi-bound — but the /// first protocol that declares the method wins, mirroring the sibling fn); /// - arity must match when the caller supplied one (0-arity match-scrutinee siblng /// passes `None` via its own call path — not this one); /// - a method-level generic on the protocol method itself is unsupported (→ try the /// next bound, else `None` — no regression, legacy still covers it). fn resolve_generic_bound_receiver_method( &self, peeled: &TypeRef, method: &str, arity: Option<usize>, gs: &GenericScope, ) -> Option<TypeRef> { let TypeRef::Named { path, generics, .. } = peeled else { return None }; if path.len() != 1 || !generics.is_empty() { return None; } let gp = gs.get(path[0].as_str())?; for bound in &gp.bounds { let TypeRef::Named { path: bpath, generics: bargs, span: bspan } = bound else { continue }; if bpath.len() != 1 { continue; } // №514 fix: a bound name like `Write` can collide across modules // (`std.io.Write` — byte-sink, `Result`-returning — vs // `std.prelude.protocols.Write` — text-sink, `()`-returning; same // collision class as `[M-fmt-write-protocol-collision-cycle-adjacent]`, // `file_local_types`'s own doc a few hundred lines above). The bare // `self.types.get(name)` global slot silently picks whichever same- // named decl landed LAST in the merged transitive-import order — // observed live: `write_all[W Write]`'s own bound resolved to the // WRONG `Write` (the prelude text-sink, `-> ()`), so `w.write(rest)` // used as a match scrutinee got typed `nova_unit` instead of // `Result[int, IoError]` — invalid C at the mono call site // (`nova_unit tmp = Nova_TcpStream_method_write(...)`). Resolve via // the collision-aware `types_get_for_file`, keyed by the BOUND's own // declaration file (`bspan.file_id` — the file that WROTE `[W Write]`, // where the intended `Write` is unambiguously in scope, imported or // same-module) — mirrors every other bound-name resolution in this // checker (`f3_check_member_ctx` et al.) instead of the collision- // blind global slot. let Some(td) = self.types_get_for_file(bpath[0].as_str(), bspan.file_id) else { continue }; let TypeDeclKind::Protocol { methods, .. } = &td.kind else { continue }; // [M-196-gs-bounds-parametric-bound-hazard] a PARAMETRIC protocol bound // (`D355Source[T]` — Plan 161/D355 blanket dispatch, // `spec_tests/conformance/d355_blanket_protocol.nv`) writes its own type-arg // (`T`) as a BARE name that is NOT necessarily a key of the enclosing `gs` // (D355's `T` is inferred from the bound, never a real entry of `fd.generics` // — see that fixture's own §1 comment). Substituting the protocol method's // return through such a bound can leave a residual `Named("T")` that // `mark_type_params` (gated on `gs.contains_key`) then FAILS to recognize as a // type-param, so it gets channeled as a bogus CONCRETE type — confirmed via // repro (this exact fixture CC-FAILed: `NovaOpt_nova_int` initialized from // `NovaOpt_nova_str`, a cross-mono-instance mixup) when this guard was // missing. Restricting to NON-parametric bounds (`Debug`/`Display` — zero own // generics, D com D422) is safe: their method's return type can only mention // names ALREADY in the enclosing `gs` (nothing new is introduced by the // bound), so `mark_type_params` covers it correctly. Parametric-bound // dispatch (D355-style) is intentionally NOT handled here — out of this // window's scope (see report); still resolved by whatever path already // handles the D355 fixture (unaffected — this fn simply declines it, `None`). if !td.generics.is_empty() { continue; } let Some(m) = methods.iter().find(|m| { m.name == method || m.name.trim_start_matches('@') == method }) else { continue }; if let Some(a) = arity { if m.params.len() != a { continue; } } if !m.generics.is_empty() { // Method-level generic on the protocol method — would need arg-type- // driven substitution too; unsupported here (mirrors the sibling fn). continue; } if !bargs.is_empty() { // Non-parametric protocol (`td.generics` empty, just excluded above) but // the bound itself was WRITTEN with type-args (`T Debug[X]` — malformed/ // unsupported spelling) — decline rather than guess. continue; } let ret = match &m.return_type { Some(r) => r.clone(), None => TypeRef::Unit(m.span), }; // [M-196-gs-bounds-self-in-protocol-return] a protocol method may return // `Self` (`Deserializer.enter_field(key str) -> Result[Self, DeError]`, // `std/src/encoding/serde/serde.nv`) — the implementor's OWN concrete type, // not a real declared type named "Self". A first version of this fn // substituted `Self` -> `peeled` unconditionally (mirroring the sibling // `mentions_self` handling a few lines up in this same fn) — but that // regressed the flagship (`nova-polaris` build, `[E_RECV_METHOD_MISMATCH]` // on the `.deser_int`-family dispatch reached through an `enter_field(..)?` // chain): substituting Self INSIDE a compound carrier (`Result[Self, E]`) // interacts with the `?`-operator's carrier-unwrap machinery in a way this // window did not fully chase down (receiver miscategorized as `[]T`, then // as `DeError`, depending on the exact substituted shape — a live, deeper // bug in that pipeline, not something a bounds-only carrier fix should // paper over). Narrowed instead: only substitute a BARE `-> Self` return // (mirrors the direct-receiver branch's OWN restriction to non-compound // returns); a `Self` nested inside a carrier is declined (`continue` to the // next bound / fall through to legacy) — exactly the SAME (safe, already- // working) codepath these calls took before this fn existed at all. Zero // regression: this only narrows what the NEW fallback covers, it does not // change any pre-existing resolution. let ret = match &ret { TypeRef::Named { path: rp, generics: rg, .. } if rp.len() == 1 && rp[0] == "Self" && rg.is_empty() => { peeled.clone() } other if typeref_mentions_any(other, &Self::self_only_gs()) => continue, other => other.clone(), }; return Some(ret); } None } /// Plan 196 gs-bounds: singleton `GenericScope`-shaped `{"Self": ...}` marker used /// ONLY as a `typeref_mentions_any` probe (name-membership check) — never read for /// its (dummy) bound content. Avoids a second, HashSet-typed overload of the same /// probe just for this one caller. fn self_only_gs() -> GenericScope { std::iter::once(( "Self".to_string(), GenericParam::unbounded("Self".to_string(), Span::dummy()), )).collect() } /// Plan 172.1 D145/D282: prefix-generic receiver fallback for /// [`resolve_instance_method_return`]. Called when no concrete-type overload /// is registered under the caller's `type_name`. Scans `method_table` for a /// **prefix-generic receiver** declaration: /// /// - Bare typevar: `fn[T] T @method` — recv_key IS the typevar name. Any /// concrete receiver type matches; T binds to `peeled`. /// - Single-level slice typevar: `fn[T] []T @method` — recv_key == "[]T". /// Concrete receiver must be `TypeRef::Array`; T binds to the element type. /// - Blanket-protocol: `fn[I Proto[T]] I mut @method` — also matched by the /// bare-typevar arm (bound checking is deferred to the full type-checker). /// /// Returns `None` if no matching declaration is found or the return type still /// mentions an unbound method-level generic after substitution. fn resolve_prefix_generic_method_return( &self, peeled: &TypeRef, method: &str, ) -> Option<TypeRef> { // Guard: only apply this fallback for CONCRETE NON-PARAMETRIC receivers — // a Named type with NO type-args (e.g. `int`, `str`, `D282IntCount`) or an // Array whose element is concrete (e.g. `[]int` → for slice-typevar receivers). // Generic-instance receivers (`EnumerateIter[VecIter[int], int]`, `Vec[str]`, // etc.) have their own resolution path; matching a blanket `fn[I Iter[T]] I` // on them here would produce half-substituted types (T still in out) that // resolved_type_to_c silently collapses to nova_int, breaking downstream // member accesses. // 172.1.2 Шаг 3.3 ПРОБОВАЛСЯ И ОТКАЧЕН (2026-07-02): снятие запрета // generic-instance ресиверов дало CC-FAIL «member base nova_int» // (blanket-матч на generic-instance подставляет не тот углеродный тип; // yield был всего −11). Residual до arg-binding inference / typed-IR. // // Plan 221.1 №111 (D239 `[]T` ≡ `Vec[T]` spelling parity): a `Vec[X]` // receiver with exactly ONE CONCRETE (non-generic) type-arg is the // SAME slice-typevar receiver as `[]X` in the other legal spelling — // it must be equally eligible for the slice-typevar blanket-method // branch below (`fn[T Bound] []T @method`, e.g. serde.nv's container- // conformance `@serialize`). Excluding it here (as any OTHER // non-empty-generics `Named` receiver — `Option[T]`, // `EnumerateIter[VecIter[int], int]`, etc. — legitimately is, per the // comment above) meant a struct field declared `items Vec[Item]` // (explicit generic syntax) never reached the slice-typevar match // arm that `items []Item` (array-sugar syntax) does, leaving the // `.serialize`/`.deserialize` call return type unresolved and // surfacing downstream as `[P67-LEGACY] method call return type // unknown` (emit_c.rs) for the EXPLICIT spelling only — confirmed via // minimal repro: `[]Item` builds clean, `Vec[Item]` ICEs, byte- // identical struct otherwise. Only this single-level concrete-Vec // shape is carved out; `Vec[Vec[T]]`/`Vec[Option[T]]` (element itself // generic) still fall through to the generic-instance channel // unchanged (mirrors the Array arm's own `inner` concreteness gate). let peeled_ok = match peeled { TypeRef::Named { path, generics, .. } if path.len() == 1 && path[0] == "Vec" && generics.len() == 1 => { matches!(&generics[0], TypeRef::Named { generics: ig, .. } if ig.is_empty()) } TypeRef::Named { generics, .. } => generics.is_empty(), TypeRef::Array(inner, _) => matches!(inner.as_ref(), TypeRef::Named { generics, .. } if generics.is_empty()), _ => false, }; if !peeled_ok { return None; } // №254 (221.1) specificity: when a concrete receiver satisfies BOTH a // direct `Next[T]`-bound blanket (it owns `@next` itself) AND the // `Iter[I]`-delegate blanket (design point 4 — it also has `@iter()`, // e.g. a user type mirroring Rust's `impl Iterator + IntoIterator<IntoIter // = Self>`), the delegate must NOT win: dispatching through it calls // `@iter()` (== self) then re-resolves the SAME ambiguity on the // "new" receiver — an infinite loop (confirmed empirically: a type // implementing both directly recurses to fiber-stack-overflow before // this fix). HashMap iteration order over `sig.method_table` is // unspecified, so the loop below USED TO pick whichever candidate a // given hash-seed visited first — silently non-deterministic even // when it didn't outright recurse. Fix: visit candidates in a STABLE // order that always tries a non-`Iter`-bound candidate (the direct // `Next[T]` carrier, or anything else) before an `Iter`-bound one — // "own carrier beats delegate". A genuine tie (two candidates both // NOT bound by `Iter`, or both bound by `Iter`) still resolves by // incidental HashMap order — full `E_AMBIGUOUS` diagnostics for that // residual case is the broader #260/#262 class, out of scope here. let mut ordered: Vec<(&String, &HashMap<String, Vec<&FnDecl>>)> = self.sig.method_table.iter().collect(); ordered.sort_by_key(|(recv_key, methods)| { let is_iter_delegate = methods.get(method) .and_then(|overloads| match overloads.as_slice() { [f] => Some(f), _ => None, }) .and_then(|f| f.generics.iter().find(|g| &g.name == *recv_key)) .map(|recv_g| recv_g.bounds.iter().any(|b| { matches!(b, TypeRef::Named { path, .. } if path.last().map(|s| s.as_str()) == Some("Iter")) })) .unwrap_or(false); is_iter_delegate as u8 }); for (recv_key, methods) in ordered { let Some(overloads) = methods.get(method) else { continue; }; let [f] = overloads.as_slice() else { continue; }; let Some(recv) = f.receiver.as_ref() else { continue; }; if !matches!(recv.kind, ReceiverKind::Instance) { continue; } // Identify the typevar name and produce the concrete binding. let (typevar_name, t_binding): (String, TypeRef) = if f.generics.iter().any(|g| &g.name == recv_key) { // Bare typevar receiver (`fn[T] T @m` or `fn[I Bound] I @m`). (recv_key.clone(), peeled.clone()) } else if recv_key.starts_with("[]") && f.generics.iter().any(|g| g.name == &recv_key[2..]) { // Single-level slice typevar: `fn[T] []T @m`. Plan 221.1 // №111: `Vec[X]` (explicit generic spelling, `peeled_ok` // above already restricted it to a single CONCRETE // type-arg) binds the SAME typevar from its sole generic // arg — `[]X` and `Vec[X]` are one receiver shape (D239). match peeled { TypeRef::Array(inner, _) => { (recv_key[2..].to_string(), (**inner).clone()) } TypeRef::Named { path, generics, .. } if path.len() == 1 && path[0] == "Vec" && generics.len() == 1 => { (recv_key[2..].to_string(), generics[0].clone()) } _ => continue, } } else { continue; }; // №254 (221.1) bound-check: a bare-typevar receiver blanket // (`fn[I Bound] I @m`) used to bind `I := peeled` UNCONDITIONALLY // — no check that `peeled` actually satisfies `Bound`. Codegen's // OWN protocol-aware blanket dispatch (Plan 164 Ф.3, emit_c.rs // `protocols_match`) already refuses a receiver lacking the bound // protocol and falls into the single-key `method_receivers` // last-wins fallback — the checker/codegen disagreement is // exactly backlog #262 ("checker-codegen mutual permissive // ring"). Concretely: `entries.collect()` (entries: `Vec[u8]`) // let the checker accept `I := Vec[u8]` against `collect`'s // `I Next[T]` bound though `Vec` has no `next()` method at all — // codegen then rejected it, producing E_RECV_METHOD_MISMATCH // instead of an honest bound-failure (or, once a genuine // alternate candidate exists — the `Iter[I]`-delegate blanket — // silently picking whichever candidate a HashMap iteration // happened to visit first). // // Reject a candidate whose receiver typevar carries a `Next`/ // `Iter` protocol bound the concrete receiver does not // structurally satisfy. Scoped to these two protocols ONLY: // both are documented (prelude/collections.nv) to use the exact // "protocol name, lowercased, is the required method name" // convention (`Next[T]` → `next`, `Iter[I]` → `iter`) — reusing // that convention here is precise, not a heuristic guess. // Widening this to protocol-bounds in general is the broader // backlog #260/#262 class, out of scope for this window. if typevar_name == *recv_key { if let Some(recv_g) = f.generics.iter().find(|g| &g.name == recv_key) { let concrete_name: Option<&str> = match &t_binding { TypeRef::Named { path, .. } => path.last().map(|s| s.as_str()), TypeRef::Array(_, _) => Some("Vec"), _ => None, }; let bound_ok = recv_g.bounds.iter().all(|b| { let TypeRef::Named { path: bpath, .. } = b else { return true; }; let Some(proto_name) = bpath.last() else { return true; }; if !matches!(proto_name.as_str(), "Next" | "Iter") { return true; // out of scope — permissive (unchanged behavior) } let required_method = proto_name.to_lowercase(); concrete_name .and_then(|cn| self.sig.method_table.get(cn)) .map(|m| m.contains_key(&required_method)) .unwrap_or(false) }); if !bound_ok { continue; } } } let Some(ret) = f.return_type.as_ref() else { continue; }; let mut subst: HashMap<String, TypeRef> = HashMap::new(); subst.insert(typevar_name.clone(), t_binding); subst.insert("Self".to_string(), peeled.clone()); // [M-next-collect-value-record] (checker-side mirror of the codegen // emit_c.rs fallback of the same name): when the receiver typevar // carries a PROTOCOL bound with its OWN inner typevar (`I: Next[T]` // — `T` is the iterator's element type, distinct from `I` itself), // binding only `I -> peeled` above leaves `T` dangling in `ret` // (e.g. blanket `Vec[T]` from `fn[I Next[T]] I mut @collect() -> // Vec[T]`). A GENERIC receiver resolves `T` via the generic- // instance channel elsewhere; a CONCRETE, non-generic `Next[T]` // implementor (`SplitIter`/`RSplitIter`/`CharsIter` — a plain // `value` record whose `@next` carries a fixed method-level // `#impl(Next[<elem>])`) has no generic-instance channel to fall // back on, so `T` stays free and `resolved_type_to_c` later erases // it to `nova_int` downstream (CC-FAIL, or a silently wrong element // type when two mono `Vec`s happen to share layout). Resolve `T` // here from the peeled type's own `#impl(Next[<elem>])` binding. if let TypeRef::Named { path: peeled_path, generics: peeled_gens, .. } = peeled { if peeled_gens.is_empty() { if let Some(concrete_name) = peeled_path.last() { if let Some(recv_g) = f.generics.iter().find(|g| &g.name == recv_key) { for bound in &recv_g.bounds { let TypeRef::Named { path: bpath, generics: bgens, .. } = bound else { continue }; if bgens.is_empty() { continue; } let Some(proto_name) = bpath.last() else { continue }; let Some(concrete_methods) = self.sig.method_table.get(concrete_name) else { continue }; let mut elem_arg: Option<String> = None; 'find_elem: for cand_overloads in concrete_methods.values() { for cand in cand_overloads { for spec in &cand.impl_protocols { if impl_spec_base_name(spec) == proto_name.as_str() { let inner = impl_spec_args_text(spec) .trim_start_matches('[') .trim_end_matches(']') .split(',') .next() .unwrap_or("") .trim(); if !inner.is_empty() { elem_arg = Some(inner.to_string()); } break 'find_elem; } } } } if let Some(arg) = elem_arg { if let Some(TypeRef::Named { path: bp, generics: bg, .. }) = bgens.first() { if bg.is_empty() { if let Some(tv) = bp.last() { subst.insert(tv.clone(), TypeRef::Named { path: vec![arg], generics: vec![], span: Span::dummy(), }); } } } } } } } } } let out = crate::const_fn_trampoline::subst_type_ref_pub(ret, &subst); // Bail if the substituted return still mentions any unbound method-level // generic (e.g. `fn[T] []T @map[U](…) -> []U` — U is unresolved here). let unbound: HashSet<String> = f .generics .iter() .filter(|g| g.name != typevar_name) .map(|g| g.name.clone()) .collect(); if !unbound.is_empty() && typeref_mentions_any(&out, &unbound) { continue; } return Some(out); } None } /// Plan 172.1.2 [M-172.1-U4-recv-infer]: CHANNEL-only method-call return inference, /// DECOUPLED from `infer_expr_type` (which feeds inline soundness checks — see the decouple /// rationale at the reverted Call arm: a resolved method-chain receiver unblocks a /// false-positive extension-policy check, and an inline-rippled binding type flips the global /// `[]T`/`Vec[T]` spelling, perturbing a GC-sensitive layout). Resolves `obj.method(...)`'s /// return type for the codegen `resolved_types` channel ONLY, recursing into itself for chain /// receivers (`a.b().c()`) and falling back to `infer_expr_type` for a LEAF receiver /// (Ident/SelfAccess/literal). Returns None for non-method-call exprs, ctor forms (handled by /// `infer_expr_type`'s ctor arms), and anything `resolve_instance_method_return` bails on /// (container/unbound-carrier/multi-overload/static). fn infer_method_call_channel_type( &self, e: &Expr, scope: &HashMap<String, TypeRef>, // Plan 196 gs-bounds: threaded down to `resolve_instance_method_return_arity`'s // new bound-receiver fallback (see that fn's doc) — the enclosing fn/type-decl's // OWN generic-scope, so a call on a bare generic receiver (`v.debug(f)`, `v: T`) // can dispatch by `T`'s protocol bound instead of missing Channel 2 entirely. gs: &GenericScope, ) -> Option<TypeRef> { let ExprKind::Call { func, args: call_args, .. } = &e.kind else { return None; }; // [реестр 221.1 №126, `[M-static-generic-method-path-call-p67-panic]`] // `Type.method[T](args)` — turbofish on the METHOD name (a STATIC method's // OWN generics), parses to `TurboFish{base, type_args}` where `base` is // EITHER `Member{obj:Ident(tyname), name}` (a lowercase-first-continuation // parse) OR — the ACTUAL shape for a PascalCase type name like our probe's // `Nzp126Utils.show[int](42)` — `Path([tyname, name])` (parser/mod.rs // `starts_uppercase` loop, ~9115: greedily folds `Type.method` into a // 2-segment Path BEFORE the postfix stage ever sees a Dot to build a // Member node). Checked FIRST (before either AST shape is destructured by // the arms below) so both encodings reach the SAME dedicated static-own- // generic producer: none of the instance-oriented `rt`-inference sources // further down can ever resolve a bare TYPE name as a VALUE type (by // design — `resolve_instance_method_return_arity` bails on // `ReceiverKind::Static`), and the sibling `resolve_generic_static_return` // covers ONLY a turbofish on the type's own CARRIER generics (`Type[T]. // ctor()`, the different `Member{obj:TurboFish}` shape) — this call form // had NO producer at all before, hence the unconditional `[P67-LEGACY]` // panic. A miss (name shadowed by a variable, ambiguous double-turbofish // receiver, non-static/overloaded method, …) falls through unchanged to // the existing resolution below. if let ExprKind::TurboFish { base, type_args } = &func.kind { let tyname_name: Option<(&str, &str)> = match &base.kind { ExprKind::Member { obj, name } => match &obj.kind { ExprKind::Ident(t) if !scope.contains_key(t.as_str()) => { Some((t.as_str(), name.as_str())) } _ => None, }, ExprKind::Path(parts) if parts.len() == 2 => { Some((parts[0].as_str(), parts[1].as_str())) } _ => None, }; if let Some((tyname, method_name)) = tyname_name { if let Some((static_rt, ordered, fn_span)) = self .resolve_generic_static_method_own_return( tyname, method_name, type_args, e.span, ) { if e.id.is_set() { if !ordered.is_empty() { self.node_substs.borrow_mut().insert(e.id, ordered); } self.resolved_callees.borrow_mut().insert(e.id, fn_span); } return Some(static_rt); } } } // Plan 200 П19: `[N]T @len()`/`@ptr()` — same compiler-synthesized FixedArray // accessors as the early check in `infer_expr_type`'s Call arm (§3 one-window: one // shared `fixed_array_accessor_return`, two producer call-sites — this fn feeds the // codegen `resolved_types` CHANNEL, the other feeds inline soundness checks; see that // fn's own doc for why they're decoupled instead of merged). Checked first so it // short-circuits before the const-receiver/turbofish AST-shape dispatch below. if let ExprKind::Member { obj, name } = &func.kind { if matches!(name.as_str(), "len" | "ptr") && call_args.is_empty() { if let Some(obj_ty) = self.infer_expr_type(obj, scope) { if let Some((n, inner)) = Self::peel_fixed_array(&obj_ty) { let is_mut = !self.is_through_ro_binding(obj); if let Some(rt) = Self::fixed_array_accessor_return( n, inner, name, is_mut, e.span, ) { return Some(rt); } } } } } // МЕРЖ ДВУХ ВОЛН 2026-07-20 (интегратор): entry принимает ТРИ AST-shape'а. // [M-196-producer-b-turbofish] (Plan 196 Producer B): `obj.method[U](args)` — // explicit METHOD-level turbofish on an INSTANCE receiver parses to // `TurboFish{base: Member{obj,name}, type_args}` (parser/mod.rs ~8230). Structurally // distinct from a static-ctor turbofish (`Type[T].new()` — TurboFish wraps the // RECEIVER) — excluded below unchanged (`obj.kind == TurboFish` guard). Explicit // type-args overlay `resolve_return_channel`'s solver as ground truth (D310 mirror). // [M-p67-path-call-const-receiver-method-ice]: `Path([CONST_NAME, method])` — // a `const NAME TYPE = value` receiver in conventional SCREAMING_SNAKE_CASE: the // parser's PascalCase path-collector (`starts_uppercase` gate) folds `NAME.method` // into a 2-segment Path — spelling-only — instead of the `Member` shape a // lowercase-first variable receiver gets. `const_types` (built once in // `TypeCheckCtx::build`) recognizes the receiver and routes it through the // IDENTICAL resolution — one channel, one behavior, any surface spelling. let (recv_ty, name, explicit_type_args): (TypeRef, &String, Option<&[TypeRef]>) = match &func.kind { ExprKind::Member { .. } | ExprKind::TurboFish { .. } => { let (member_expr, explicit_ta): (&Expr, Option<&[TypeRef]>) = match &func.kind { ExprKind::Member { .. } => (func.as_ref(), None), ExprKind::TurboFish { base, type_args } if matches!(&base.kind, ExprKind::Member { .. }) => { (base.as_ref(), Some(type_args.as_slice())) } _ => return None, }; let ExprKind::Member { obj, name } = &member_expr.kind else { return None; }; if name.starts_with('@') { return None; } // Exclude static-ctor forms (`Type[T].new()`, `[]T.new()`): their return is // inferred by `infer_expr_type`'s ctor arms / `resolve_generic_static_return`, // not the instance path. if matches!(&obj.kind, ExprKind::TurboFish { .. }) { return None; } if let ExprKind::Path(parts) = &obj.kind { if parts.len() == 2 && parts[0] == "__array" { return None; } } // Receiver type: chain-aware (recurse for a Call receiver), else leaf via // infer_expr_type. let rt = self .infer_method_call_channel_type(obj, scope, gs) .or_else(|| self.infer_expr_type(obj, scope)) // 172.1.2 (Call:.len closure): третий источник — КАНАЛ: Member-ресиверы // generic-тел (`@_buckets`, `@data`) аннотированы Шагом 2b как // Named/TypeParam, но TypeRef-инференс их не видит. Восстановление // TypeRef — ЛОКАЛЬНОЕ (TypeParam(n) → Named{n} только для // method-резолюции; residual в out заново пометит Call-арм). .or_else(|| { if !obj.id.is_set() { return None; } let buf = self.resolved_types_buf.borrow(); let rt = buf.get(&obj.id)?.clone(); drop(buf); Self::resolved_to_typeref_tp(&rt, e.span) })?; (rt, name, explicit_ta) } // [M-p67-path-call-const-receiver-method-ice]: `parts[0]` unambiguously a // const (types/consts are disjoint Nova DECL namespaces) — a real // static/type-namespace Path (`Monotonic.now()`, `Channel.new()`) never // collides with a name present in `const_types`. ExprKind::Path(parts) if parts.len() == 2 => { let rt = self.const_types.get(parts[0].as_str())?.clone(); (rt, &parts[1], None) } // [M-assoc-const-chained-method-call-p67] (окно №73): `Type.CONST.method()` // — a chained method call directly on a bare out-of-body assoc-const // receiver, no intermediate binding. Same parser fold as the 2-segment // top-level-const arm above (PascalCase path-collector, ~8797), one // segment deeper: `parts[0..2]` unambiguously names a declared assoc- // const (`assoc_const_types`, built once in `TypeCheckCtx::build` from // `TypeDecl.assoc_consts`) — a genuine 3-segment module/type static Path // (`mod.Type.static_method()`) never collides, since `assoc_const_types` // only ever contains PAIRS that are an actual declared `const Type.NAME`. ExprKind::Path(parts) if parts.len() == 3 => { let rt = self.assoc_const_types .get(&(parts[0].clone(), parts[1].clone()))? .clone(); (rt, &parts[2], None) } _ => return None, }; let call_arity = call_args.len(); // Plan 196.5 Stage-A: this call-site's `ExprId` is the `node_substs` key producer B // writes under (propagated through `resolve_instance_method_return_arity` → // `resolve_return_channel`'s caller-side insert). let call_id = if e.id.is_set() { Some(e.id) } else { None }; self.resolve_instance_method_return_arity( &recv_ty, name, Some(call_arity), Some((call_args.as_slice(), scope)), call_id, explicit_type_args, gs) .or_else(|| { // [M-196-producer-b-turbofish] An explicit turbofish call-site skips the // closure-arg-inference fallback: `explicit_type_args` already fixes every // method-level generic by declaration, so a miss above means a DIFFERENT // reason (arity/overload/carrier residual) — closure-return-peek exists to // discover a generic that has NO other source, which doesn't apply once one // was written explicitly (mirrors the free-fn D310 overlay's ground-truth // contract: an explicit annotation is never a trigger for a fallback INFERENCE // source). if explicit_type_args.is_some() { return None; } // 172.1.2 arg-binding (2026-07-03): method-level generic (`map[U]`) // выводится из closure-аргумента при известном carrier-subst. self.resolve_method_return_with_closure_args(&recv_ty, name, call_args, scope, call_id) }) .or_else(|| { // [M-named-tuple-field-accessor-on-call-ice] (ICE-пачка п.1): a // zero-arg call-syntax field read (`obj.field()`) on a Record/ // NamedTuple receiver whose type has NO declared method named // `field` (checked twice above: `resolve_instance_method_return_ // arity` and the closure-arg fallback both missed) previously // fell all the way through to codegen's P67-LEGACY terminal // panic — `f3_check_member_ctx`'s field-match arm (below, in // this same impl) annotates the INNER Member sub-expr's // `ExprId` unconditionally (no `is_call_func` gate on the // plain-field branch, unlike the same-name-method branch a few // lines above it, which explicitly documents why call-position // must NEVER get the field annotation when a method exists) — // but nothing annotated the OUTER Call's own `ExprId`, so // `resolved_types`/`resolved_callees` had no entry for the call // node itself and codegen's return-type cascade panicked // (`obj_ty="NovaTuple_X"`/`"Nova_X*"`, `obj=Call`/`Ident(..)`). // A bare `.field` (no parens) already worked (`f3_check_member_ // ctx`'s field arm covers it) — only the call-syntax form was // unreachable. No spec sanction exists for `.field()` as a // GENERAL field-read sugar (D117 reserves call-syntax for // EXPLICITLY DECLARED accessor methods like `cap`/`len`), but // the checker already silently accepted the call-syntax parse // (no diagnostic) before this fix — turning it into a hard // error now would be a new user-facing regression on a form // that at least *parses*; annotating the channel here keeps // the parse-accepted surface working, consistent with §4а // (no ICE on any accepted input) and the brief's stated fix // channel ("checker annotation"). if call_arity != 0 || explicit_type_args.is_some() { return None; } self.field_zero_arg_call_return(&recv_ty, name) }) } /// See the `[M-named-tuple-field-accessor-on-call-ice]` producer above: /// resolves `obj.field()` (zero-arg call syntax) to `field`'s OWN type when /// `recv_ty`'s underlying declared type is a Record or NamedTuple with a /// field literally named `name` and no colliding same-named method (the /// caller already tried real method resolution and missed). Mirrors the /// substitution the bare-`.field` `f3_check_member_ctx` arm performs /// (`subst_receiver_generics` over the type's own generics), so a generic /// record/named-tuple field substitutes the SAME concrete type either way. fn field_zero_arg_call_return(&self, recv_ty: &TypeRef, name: &str) -> Option<TypeRef> { let mut peeled = recv_ty; loop { match peeled { TypeRef::Readonly(i, _) | TypeRef::Mut(i, _) => peeled = i, _ => break, } } let TypeRef::Named { path, generics: args, .. } = peeled else { return None }; let type_name = path.last()?; if self.t_provides_method(type_name, name) { // A real method with this name exists — never shadow it with a // field annotation from a call-position (mirrors the same-name // guard in `f3_check_member_ctx`). return None; } let td = self.types.get(type_name)?; let field_ty: &TypeRef = match &td.kind { TypeDeclKind::Record(fields) => &fields.iter().find(|f| f.name == name)?.ty, TypeDeclKind::NamedTuple(fields) => &fields.iter().find(|f| f.name == name)?.ty, _ => return None, }; // Exclude a Func-typed field (a stored closure/fn-pointer field IS // meant to be invoked with call syntax, args aside — codegen's own // dispatch cascade may still have a legitimate route for that we must // not shadow here; only a PLAIN-VALUE field masquerading as a call is // this producer's target). if matches!(field_ty, TypeRef::Func { .. }) { return None; } Some(self.subst_receiver_generics(field_ty, &td.generics, args)) } /// 172.1.2 C6(a): посев типов closure-параметров из СУБСТИТУИРОВАННОЙ /// сигнатуры callee (тот же путь, что arg-binding): для каждого arg-индекса /// с ClosureLight против Func-параметра — карта (имя → конкретный TypeRef). /// Параметры, требующие невыведенный method-generic, не сеются (честно). fn closure_arg_param_seeds( &self, e: &Expr, scope: &HashMap<String, TypeRef>, ) -> Vec<(usize, Vec<(String, TypeRef)>)> { let mut out = Vec::new(); let ExprKind::Call { func, args, .. } = &e.kind else { return out }; let ExprKind::Member { obj, name } = &func.kind else { return out }; if name.starts_with('@') || matches!(&obj.kind, ExprKind::TurboFish { .. }) { return out; } if !args.iter().any(|a| matches!(&a.expr().kind, ExprKind::ClosureLight { .. })) { return out; } let Some(recv_ty) = self .infer_expr_type(obj, scope) .or_else(|| { if !obj.id.is_set() { return None; } let buf = self.resolved_types_buf.borrow(); let rt = buf.get(&obj.id)?.clone(); drop(buf); Self::resolved_to_typeref_tp(&rt, e.span) }) else { return out }; let mut peeled = &recv_ty; loop { match peeled { TypeRef::Readonly(i, _) | TypeRef::Mut(i, _) => peeled = i, _ => break, } } let type_name: String = match peeled { TypeRef::Named { path, .. } if path.len() == 1 => path[0].clone(), TypeRef::Array(_, _) | TypeRef::FixedArray(_, _, _) => "Vec".to_string(), _ => return out, }; let Some(overloads) = self.method_overloads(&type_name, name) else { return out }; let [f] = overloads.as_slice() else { return out }; let Some(recv) = f.receiver.as_ref() else { return out }; if !matches!(recv.kind, ReceiverKind::Instance) { return out; } let method_names: HashSet<String> = f.generics.iter().map(|g| g.name.clone()).collect(); let mut subst = build_recv_subst(recv, &recv_ty); subst.insert("Self".to_string(), peeled.clone()); for (i, (pd, a)) in f.params.iter().zip(args.iter()).enumerate() { let TypeRef::Func { params: fp, .. } = &pd.ty else { continue }; let ExprKind::ClosureLight { params: cp, .. } = &a.expr().kind else { continue }; if cp.len() != fp.len() { continue; } let mut binds = Vec::new(); let mut ok = true; for (cpar, fpt) in cp.iter().zip(fp.iter()) { let t = crate::const_fn_trampoline::subst_type_ref_pub(fpt, &subst); if typeref_mentions_any(&t, &method_names) { ok = false; break; } if cpar.name != "_" { binds.push((cpar.name.clone(), t)); } } if ok && !binds.is_empty() { out.push((i, binds)); } } out } /// Plan 196.5 producers-widen (Class-1, closure-return-bound fallback): /// peek a closure-literal ARG's body return type, seeding the closure's OWN /// declared params with `fp` (caller-substituted — must ALREADY be concrete /// for every name NOT in `unresolved`, e.g. carrier/`Self` already bound by /// the caller). Mirrors the legacy codegen `resolve_mono_type_args` Source 2b /// (`emit_c.rs` ~19025 — C-string based) at the checker/TypeRef level, so the /// SAME class of closure (identity/computed `Expr` body, or an empty-stmts /// `Block` trailing expr) the legacy codegen path already lowers correctly /// can ALSO populate the `node_substs`/return-channel — not inventing new /// inference, porting the existing legacy heuristic one layer up (checker /// phase, ahead of codegen). Honest `None` (no annotation, no guess) /// whenever: arity mismatch, a closure param's declared type still mentions /// an `unresolved` name (nothing concrete to seed it with — refuses to guess /// with a bare generic), or the body itself doesn't resolve via /// `infer_expr_type` (stmts-block, unsupported expr shape, etc.) — the same /// "no invention" contract `infer_expr_type` already holds everywhere else /// in this file. /// [M-196-builtin-producer] Closure-body-block peek gate: a block is safe to /// peek (return its `trailing` expr's type under the UNCHANGED closure-param /// `cscope`) when every leading statement is a NON-BINDING side-effect /// (`Stmt::Expr`/`Stmt::Assign`/`Stmt::TupleAssign` — none of these introduce a /// new name into scope, so `trailing`'s free variables are exactly the /// closure's own params, unaffected by what preceded it). `Stmt::Let`/ /// `Stmt::Const` (new bindings `trailing` could reference) and control-flow /// (`Return`/`Break`/`Continue`/`Throw`/…) are conservatively EXCLUDED — this /// is a peek (read-only best-effort type materialization feeding a /// propose-then-verify solver, `resolve_return_channel`), not a real /// interpreter; the narrower the accepted shape, the safer the guarantee /// that "trailing's type doesn't depend on anything peek can't see". /// /// Closes a real corpus gap: `Option[T]@flat_map[U](f fn(T)->Option[U])`-style /// combinators are naturally written `|x| { some_side_effect; Some(f(x)) }` /// (a mutation flag in a test, a log call, etc.) — before this, ANY non-empty /// leading statement bailed the whole peek (`_ => None`), permanently denying /// `U` a binding source and leaving the call un-channeled (legacy /// `infer_method_level_return_for_sum`/B11q/B11r pick it up instead). /// `spec_tests/conformance/plan200_14_option_result_flat_map_filter.nv` /// (`n.flat_map(|x| { called = true; Some(x + 1) })`, /// `r.flat_map(|x| { called = true; p200_14_check_positive(x) })`) is the /// exact shape this closes. fn closure_block_stmts_are_peek_safe(stmts: &[Stmt]) -> bool { stmts.iter().all(|s| matches!(s, Stmt::Expr(_) | Stmt::Assign { .. } | Stmt::TupleAssign { .. })) } /// [M-196-closeout-if-body-peek] Narrow per-branch peek used ONLY by /// `closure_if_ctor_peek` below — recognizes the builtin Option/Result ctor /// shapes that `infer_expr_type` structurally cannot resolve without an /// `expected` type (bare `None`, single-arg `Some`/`Ok`/`Err` calls — see /// `materialize_literal_coercion`'s ctor arm, ~13724, which is expected-type- /// driven, the REVERSE direction from this peek). Falls back to /// `infer_expr_type` (unchanged) for anything else, so already-working shapes /// (e.g. a nested call that itself returns a concrete `Option[T]`/`Result[T,E]`) /// keep working exactly as before. fn closure_if_ctor_branch_peek( &self, e: &Expr, scope: &HashMap<String, TypeRef>, ) -> Option<ClosureIfCtorBranch> { if let ExprKind::Ident(n) = &e.kind { if n == "None" { return Some(ClosureIfCtorBranch::Option(None)); } } if let ExprKind::Call { func, args, .. } = &e.kind { if args.len() == 1 { if let ExprKind::Ident(ctor) = &func.kind { let inner = self.infer_expr_type(args[0].expr(), scope); match ctor.as_str() { "Some" => return Some(ClosureIfCtorBranch::Option(inner)), "Ok" => return Some(ClosureIfCtorBranch::Result(inner, None)), "Err" => return Some(ClosureIfCtorBranch::Result(None, inner)), _ => {} } } } } Self::typeref_as_ctor_branch(self.infer_expr_type(e, scope)?) } /// A concrete `Option[T]`/`Result[T,E]` (already fully resolved, e.g. by a /// nested call) also counts as a known branch — both slots known. fn typeref_as_ctor_branch(t: TypeRef) -> Option<ClosureIfCtorBranch> { if let TypeRef::Named { path, generics, .. } = &t { match (path.last().map(String::as_str), generics.as_slice()) { (Some("Option"), [ty]) => { return Some(ClosureIfCtorBranch::Option(Some(ty.clone()))); } (Some("Result"), [ty, ety]) => { return Some(ClosureIfCtorBranch::Result(Some(ty.clone()), Some(ety.clone()))); } _ => {} } } None } /// [M-196-closeout-if-body-peek] Closes the last producer gap documented by /// the builtin-producer wave (`docs/plans/wip/196-builtin-notes.md` §2): /// `ClosureBody::Expr(If{..})` combinator bodies like /// `|x| if x == 0 { None } else { Some(x) }` — REAL corpus site, /// `spec_tests/conformance/plan200_14_option_result_flat_map_filter.nv:44` /// (`a.flat_map(|x| if x == 0 { None } else { Some(x) })`, test "f itself can /// return None (real bind, not just map)"). `infer_expr_type`'s existing /// `ExprKind::If` arm (Plan 125/172.1, D275 unit-domination, unchanged here) /// bails on this shape because NEITHER branch alone resolves without ctor-aware /// peeking (`None` bare-Ident is generic-Option-excluded, `Some(x)` has no /// expected-type context) — deliberately scoped to closure-peek callers only /// (NOT folded into shared `infer_expr_type`, which has 249 unrelated /// consumers; a narrow local helper keeps blast radius to zero elsewhere). /// /// Gate: `If{then, else_: Some(ElseBranch::Block)}` only (no elif chains — the /// real corpus shape is a plain two-way branch; conservative, matches the /// "peek is read-only best-effort, not an interpreter" contract of the sibling /// gates in this file). Both branch blocks must be stmt-empty-or-peek-safe /// (`closure_block_stmts_are_peek_safe`) with SOME trailing expr. Each /// branch's trailing expr is peeked via `closure_if_ctor_branch_peek`; the two /// results must agree on the SAME sum (Option xor Result) and every generic /// slot known by either side must not conflict — else bail (`None`, safe /// legacy fallback, exactly the pre-existing behavior). fn closure_if_ctor_peek( &self, e: &Expr, scope: &HashMap<String, TypeRef>, ) -> Option<TypeRef> { let ExprKind::If { then, else_: Some(crate::ast::ElseBranch::Block(eb)), .. } = &e.kind else { return None; }; if !Self::closure_block_stmts_are_peek_safe(&then.stmts) || !Self::closure_block_stmts_are_peek_safe(&eb.stmts) { return None; } let then_e = then.trailing.as_deref()?; let else_e = eb.trailing.as_deref()?; let a = self.closure_if_ctor_branch_peek(then_e, scope)?; let b = self.closure_if_ctor_branch_peek(else_e, scope)?; let merge_slot = |x: Option<TypeRef>, y: Option<TypeRef>| -> Option<Option<TypeRef>> { match (x, y) { (Some(tx), Some(ty)) => { if ResolvedType::from_type_ref(&tx) == ResolvedType::from_type_ref(&ty) { Some(Some(tx)) } else { None // conflicting concrete types on the same slot — bail } } (Some(t), None) | (None, Some(t)) => Some(Some(t)), (None, None) => Some(None), } }; match (a, b) { (ClosureIfCtorBranch::Option(ta), ClosureIfCtorBranch::Option(tb)) => { let t = merge_slot(ta, tb)??; Some(TypeRef::Named { path: vec!["Option".to_string()], generics: vec![t], span: e.span }) } (ClosureIfCtorBranch::Result(ta, ea), ClosureIfCtorBranch::Result(tb, eb2)) => { let t = merge_slot(ta, tb)??; let et = merge_slot(ea, eb2)??; Some(TypeRef::Named { path: vec!["Result".to_string()], generics: vec![t, et], span: e.span, }) } _ => None, // mismatched sum kinds (Option vs Result) — bail, legacy fallback } } fn closure_arg_return_peek( &self, fp: &[TypeRef], arg_expr: &Expr, outer_scope: &HashMap<String, TypeRef>, unresolved: &impl GenericNameSet, ) -> Option<TypeRef> { match &arg_expr.kind { ExprKind::ClosureLight { params: cp, body } => { if cp.len() != fp.len() { return None; } let mut cscope = outer_scope.clone(); for (cpar, fpt) in cp.iter().zip(fp.iter()) { if typeref_mentions_any(fpt, unresolved) { return None; } if cpar.name != "_" { cscope.insert(cpar.name.clone(), fpt.clone()); } } match body { ClosureBody::Expr(be) => self.infer_expr_type(be, &cscope) .or_else(|| self.closure_if_ctor_peek(be, &cscope)), ClosureBody::Block(b) if Self::closure_block_stmts_are_peek_safe(&b.stmts) => { b.trailing.as_deref().and_then(|t| { self.infer_expr_type(t, &cscope) .or_else(|| self.closure_if_ctor_peek(t, &cscope)) }) } _ => None, } } ExprKind::ClosureFull(sb) => { // Fully typed by grammar (Q3 B, 197.3) — no body-inference needed, // the declared return type IS the answer (when present and it // doesn't itself still mention an unresolved name). sb.return_type.as_ref().and_then(|rt| { if typeref_mentions_any(rt, unresolved) { None } else { Some(rt.clone()) } }) } _ => None, } } /// 172.1.2 arg-binding: `v.map(|x| x*2)` при v: Vec[int] — параметр метода /// `f (T)->U`: closure-параметры типизируются СУБСТИТУИРОВАННОЙ сигнатурой /// (T→int из ресивера), тело инферится → U связывается результатом. Гейты: /// один overload, Instance-ресивер, U — голый method-generic в return-позиции /// fn-параметра; невыведенный U → None (legacy). Closure: Expr-тело или /// пустой stmts-блок (сложные тела — C6). fn resolve_method_return_with_closure_args( &self, recv_ty: &TypeRef, method: &str, args: &[CallArg], scope: &HashMap<String, TypeRef>, // [M-196.5-node-substs, Zone CH B1] call-site `ExprId` — key for the // `node_substs` write at the tail of this fn (mirrors the threading // `resolve_instance_method_return_arity`/`resolve_return_channel` already do). // `None` (no call-site, e.g. a hypothetical future 0-arity wrapper) makes the // write below a no-op — byte-identical to this fn's behavior before the // parameter existed. call_id: Option<crate::ast::ExprId>, ) -> Option<TypeRef> { let mut peeled = recv_ty; loop { match peeled { TypeRef::Readonly(i, _) | TypeRef::Mut(i, _) => peeled = i, _ => break, } } let type_name: String = match peeled { TypeRef::Named { path, .. } if path.len() == 1 => { // 2026-07-03: scope["@"] extension-метода на слайсе хранит имя // "[]T"/"[]int" — D239-нормализация к Vec для method-резолва. if path[0].starts_with("[]") { "Vec".to_string() } else { path[0].clone() } } TypeRef::Array(_, _) | TypeRef::FixedArray(_, _, _) => "Vec".to_string(), _ => return None, }; let f: &FnDecl = match self.method_overloads(&type_name, method) { Some(overloads) => match overloads.as_slice() { [f] => *f, // [M-2223-generic-arity-overload-applicability] (№130): ≥2 // generic siblings of the SAME name (arity-differing handler // closures, `@get[R](h fn(T1)->R)` vs `@get[R](h fn(T1,T2) // ->R)`) used to bail here unconditionally — but // `check_instance_overload` (types/mod.rs, same pass, runs // FIRST via `f1_check_call`) already disambiguated THIS // call-site's sibling via `closure_arg_arity_ok` and wrote it // to `resolved_callees`; reuse that choice instead of // discarding the closure-arg return-type inference entirely. multi => match call_id.and_then(|cid| self.resolved_callees.borrow().get(&cid).copied()) .and_then(|sp| multi.iter().find(|cand| cand.span == sp)) { Some(f) => *f, None => return None, }, }, // NARROWED (post-bisect, conformance regression on // c_keyword_ident_mangling): the original scan ALSO matched a // "bare typevar receiver" candidate (`fn[T] T @m[U](...)`) for // ANY `TypeRef::Named` `peeled` — far too permissive (`method` // is just a name; an UNRELATED bare-typevar decl sharing the // same method name anywhere in the reachable corpus could match // and feed the closure-binding logic below a WRONG `f`/`recv`, // silently mistyping some OTHER closure-arg call — the observed // regression). Scope this fallback to EXACTLY the slice-sugar // shape this fix targets (`[]T @map[U]`-style, vec_seq.nv): // `peeled` must be a genuine `Array`/`FixedArray`, and the // candidate's recv_key must be the LITERAL "[]<typevar>" form. None if matches!(peeled, TypeRef::Array(_, _) | TypeRef::FixedArray(_, _, _)) => { self.sig.method_table.iter().find_map(|(recv_key, methods)| { let overloads = methods.get(method)?; let [cand] = overloads.as_slice() else { return None }; let crecv = cand.receiver.as_ref()?; if !matches!(crecv.kind, ReceiverKind::Instance) { return None; } if recv_key.starts_with("[]") && cand.generics.iter().any(|g| g.name == recv_key[2..]) { Some(*cand) } else { None } })? } None => return None, }; let recv = f.receiver.as_ref()?; if !matches!(recv.kind, ReceiverKind::Instance) { return None; } if f.returns_receiver { return Some(peeled.clone()); } let ret = f.return_type.as_ref()?; let method_names: HashSet<String> = f.generics.iter().map(|g| g.name.clone()).collect(); if method_names.is_empty() { return None; // покрыто базовой resolve_instance_method_return } // [M-196.5-node-substs] Declaration-order twin of `method_names` (mirrors // `resolve_return_channel`'s `method_names_ordered`) — the `HashSet` above has // no stable order, but the `node_substs` write at the tail needs positional order. let method_names_ordered: Vec<String> = f.generics.iter().map(|g| g.name.clone()).collect(); let mut subst = build_recv_subst(recv, recv_ty); // Plan 186 [bug-1 audit-197 fix]: `build_recv_subst` extracts the // carrier-binding names ONLY from `recv.generics` (the `Type[T,U]` // bracket-carrier slots) — for a PREFIX-generic `[]T` slice // receiver `recv.generics` is EMPTY (the parser never populates // `generics_first_decl` on the `[]T` fast-path, `parser/mod.rs` // ~3009-3049, so the Receiver's own `.generics` Vec stays empty; // the carrier typevar lives ONLY inside the structured // `recv.receiver_ty` — `Array(Named T)`). `build_recv_subst` // early-returns an EMPTY subst in that case (its `receiver_ty` // structural-unify branch is gated behind `!names.is_empty()`), // leaving `T` unbound — every closure-param seed below then still // mentions `T` (`typeref_mentions_any`) and bails, so `U` never // gets inferred. Retry the structural unify here directly, using // THIS decl's own generics (`f.generics` — receiver-carrier + // method-level names combined, `parser/mod.rs:3169`-3174) as the // bindable set: sound because `f`/`recv` are the SAME decl // `build_recv_subst` was just called for. if recv.generics.is_empty() { if let Some(decl_ty) = &recv.receiver_ty { let mut t = recv_ty; loop { match t { TypeRef::Readonly(i, _) | TypeRef::Mut(i, _) => t = i, _ => break, } } let mut s: HashMap<String, TypeRef> = HashMap::new(); if crate::const_fn_trampoline::unify_type(decl_ty, t, &method_names, &mut s) .is_ok() { for (k, v) in s { subst.entry(k).or_insert(v); } } } } subst.insert("Self".to_string(), peeled.clone()); for (pd, a) in f.params.iter().zip(args.iter()) { let arg_expr = a.expr(); let TypeRef::Func { params: fp, return_type: Some(fr), .. } = &pd.ty else { continue; }; // [M-closurefull-own-generic-sibling-return-infer-gap] (реестр // 221.1 №127, window p130): a `ClosureFull` literal (`fn(x T) -> // U { ... }`) declares its return type EXPLICITLY in the // grammar — unlike `ClosureLight` below, which has none and // needs the body-walk/`cscope`-seed machinery to derive one. // Before this arm this fn only matched `ClosureLight` and fell // through to `continue` for EVERY `ClosureFull` arg, leaving `R` // permanently unbound in `subst` — `out` below still mentioned // it → this whole channel bailed (`None`) for the shape Plan // 222.3 §5's extractor sugar actually needs, even with ZERO // sibling competition (confirmed: fails identically for a // single, wholly-unambiguous generic method). if let ExprKind::ClosureFull(sb) = &arg_expr.kind { if sb.params.len() == fp.len() { if let (Some(body_tr), TypeRef::Named { path: rp, generics: rg, .. }) = (sb.return_type.clone(), fr.as_ref()) { if rp.len() == 1 && rg.is_empty() && method_names.contains(&rp[0]) { subst.entry(rp[0].clone()).or_insert(body_tr); } } } continue; } let ExprKind::ClosureLight { params: cp, body } = &arg_expr.kind else { continue; }; if cp.len() != fp.len() { continue; } // Типизируем closure-параметры субституированной сигнатурой. let mut cscope = scope.clone(); let mut seed_ok = true; for (cpar, fpt) in cp.iter().zip(fp.iter()) { let t = crate::const_fn_trampoline::subst_type_ref_pub(fpt, &subst); if typeref_mentions_any(&t, &method_names) { seed_ok = false; // параметр сам требует U — не выведем break; } cscope.insert(cpar.name.clone(), t); } if !seed_ok { continue; } // [M-196-builtin-producer] mirrors `closure_arg_return_peek`'s // peek-safe gate (same doc there) — same non-binding-leading- // statements class, kept consistent so both closure-arg producers // agree on what a "peekable" block looks like. let body_tr: Option<TypeRef> = match body { ClosureBody::Expr(be) => self.infer_expr_type(be, &cscope) .or_else(|| self.closure_if_ctor_peek(be, &cscope)), ClosureBody::Block(b) => { if Self::closure_block_stmts_are_peek_safe(&b.stmts) { b.trailing.as_deref().and_then(|t| { self.infer_expr_type(t, &cscope) .or_else(|| self.closure_if_ctor_peek(t, &cscope)) }) } else { None } } }; let Some(body_tr) = body_tr else { continue }; // fr должен быть ГОЛЫМ method-generic (Named{U}) → U := тип тела. if let TypeRef::Named { path: rp, generics: rg, .. } = fr.as_ref() { if rp.len() == 1 && rg.is_empty() && method_names.contains(&rp[0]) { subst.entry(rp[0].clone()).or_insert(body_tr); } } } let out = crate::const_fn_trampoline::subst_type_ref_pub(ret, &subst); if typeref_mentions_any(&out, &method_names) { return None; // U так и не выведен → честный legacy } // [M-196.5-node-substs, Zone CH B1] Producer B-closure-arg: `subst` above is // EXACTLY the per-name substitution this fn already used to materialize `out` // (carrier via `build_recv_subst`/receiver_ty unify, method-level generics via // the closure-body-return peek a few lines up) — reading it here is not new // inference. This fn exists BECAUSE `resolve_return_channel` (Producer // B-method-residual, `f1_check_call`-adjacent) sometimes bails for a closure- // LITERAL arg (`infer_expr_type` has no arm for it, and the `rt_respells_names` // poison-guard drops a re-spelled name rather than risk a wrong bind) — exactly // the `sum=Result method=map resolved=false` class documented at // `infer_method_level_return_for_sum` (emit_c.rs). Until now THIS fn's success // fed Channel 2 (`resolved_types`, via the caller in `f1_expr_inner`) but never // `node_substs` — a real coverage gap for per-name consumers (Zone GEN: // `resolve_mono_type_args_ch` / `resolve_method_level_subst`), not merely an // intentional conservative bail. Declaration order = carrier names // (`recv.generics`, receiver order) then method-level (`method_names_ordered`, // `f.generics` order) — mirrors `resolve_return_channel`'s `names.chain( // method_names_ordered)` / `resolve_generic_static_return`'s `carrier_names` // convention. Whole-map completeness gate (same contract as every other // producer in this file): a residual/unbound name anywhere in the declared set // leaves the channel UNWRITTEN for this call-site. if let Some(cid) = call_id { let carrier_names: Vec<String> = recv .generics .iter() .filter_map(|g| match g { TypeRef::Named { path, generics, .. } if path.len() == 1 && generics.is_empty() => { Some(path[0].clone()) } _ => None, }) .collect(); let decl_order: Vec<&String> = carrier_names.iter().chain(method_names_ordered.iter()).collect(); let ordered: Vec<(String, ResolvedType)> = decl_order .iter() .filter_map(|n| { subst.get(n.as_str()) .map(|tr| ((*n).clone(), ResolvedType::from_type_ref(tr))) }) .collect(); // [M-196-ch-widen] SHADOW-ICE fix (see `rt_is_closed` doc): `subst` is seeded // from the RECEIVER's actual type at the call-site (`build_recv_subst`/ // structural unify) — same class of hazard as `resolve_return_channel` // (Producer B-method-residual): if the receiver's carrier is itself an // ENCLOSING generic body's own still-abstract type-param (e.g. `@order.map(..)` // where `@order []K` inside a generic `Lru[K,V]` method), `subst` would carry // that bare name as if concrete. This fn has no `gs` either — same registry- // based closedness gate. if !decl_order.is_empty() && ordered.len() == decl_order.len() && ordered.iter().all(|(_, v)| self.rt_is_closed(v)) { if std::env::var_os("NOVA_NODE_SUBSTS_TRACE").is_some() { eprintln!( "[NODE_SUBSTS] producer=B-closure-arg call_id={:?} method={} n={}", cid, method, ordered.len() ); } self.node_substs.borrow_mut().insert(cid, ordered); } } Some(out) } /// 172.1.2: resolved_to_typeref + TypeParam(n)→Named{n} — ТОЛЬКО для /// method-резолюции ресивера (см. вызов выше); НЕ для прямого лоуэринга. fn resolved_to_typeref_tp(rt: &ResolvedType, span: Span) -> Option<TypeRef> { use ResolvedType as R; match rt { R::TypeParam(n) => Some(TypeRef::Named { path: vec![n.clone()], generics: vec![], span, }), R::Named { name, module, args } => { let mut path = module.clone(); path.push(name.clone()); let generics = args .iter() .map(|a| Self::resolved_to_typeref_tp(a, span)) .collect::<Option<Vec<_>>>()?; Some(TypeRef::Named { path, generics, span }) } R::Array(inner) => Some(TypeRef::Array( Box::new(Self::resolved_to_typeref_tp(inner, span)?), span, )), R::Readonly(inner) => Some(TypeRef::Readonly( Box::new(Self::resolved_to_typeref_tp(inner, span)?), span, )), other => Self::resolved_to_typeref(other, span), } } /// Plan 172.1 U.4.4 generic-Member: substitute a record's declared generic params /// (`type_params`, from the `TypeDecl`) with the receiver's concrete type-args /// (`type_args`, from the materialized receiver type `Named{T, [args]}`) inside a /// field/member type. A concrete field (`count int`) carries no param name → returned /// unchanged (no-op, so the primitive-Member behavior is byte-identical); a generic /// field (`v T`) on a typed receiver `GBox[int]` becomes `int`. Returns the type /// unchanged on a non-generic receiver (`type_params` empty) or an arity mismatch /// (permissive — gate downstream rejects an unresolved param as non-primitive). fn subst_receiver_generics( &self, field_ty: &TypeRef, type_params: &[GenericParam], type_args: &[TypeRef], ) -> TypeRef { if type_params.is_empty() || type_params.len() != type_args.len() { return field_ty.clone(); } let mut subst: HashMap<String, TypeRef> = HashMap::new(); for (p, a) in type_params.iter().zip(type_args.iter()) { subst.insert(p.name.clone(), a.clone()); } crate::const_fn_trampoline::subst_type_ref_pub(field_ty, &subst) } // ── D175/D176 (Plan 108): readonly enforcement helpers ───────────────── /// Resolve record fields for a type name. Returns None if not a Record type. fn record_fields_for<'t>(&'t self, type_name: &str) -> Option<&'t Vec<RecordField>> { match self.types.get(type_name)?.kind { TypeDeclKind::Record(ref fields) => Some(fields), _ => None, } } /// Returns `true` if `expr` is an access path that goes through a readonly field, /// meaning any mutation through this path is forbidden (D175 transitivity). /// Plan 124.8 (D175 amend): returns true if `expr` is a path rooted at /// an Ident that is `ro`-bound. Walks through Member/Index chains к /// корневому Ident. fn is_through_ro_binding(&self, expr: &Expr) -> bool { match &expr.kind { ExprKind::Ident(name) => self.ro_binding_names.borrow().contains(name), ExprKind::Member { obj, .. } => self.is_through_ro_binding(obj), ExprKind::Index { obj, .. } => self.is_through_ro_binding(obj), _ => false, } } /// **Plan 147 Ф.3 (D246):** walk a `.field`/`[i]` access path down to its /// root `Ident` name, used to look up the binding's declared L2 /// content-view type for the R2-split override (`ro r mut Point` → /// content-writable). Returns `None` for non-Ident roots (deref / self / /// call results), which carry no simple binding to consult. fn assign_root_ident(expr: &Expr) -> Option<&str> { match &expr.kind { ExprKind::Ident(name) => Some(name.as_str()), ExprKind::Member { obj, .. } => Self::assign_root_ident(obj), ExprKind::Index { obj, .. } => Self::assign_root_ident(obj), _ => None, } } /// **№375 (D246 amend, Plan 221 п.12, window p375-ptr2):** given `expr` is /// about to be materialized at a position that EXPECTS a `*mut T` /// (writable-pointee) pointer, does it root in a direct `&place` / /// `raw &place` address-of over a `ro`-bound SOURCE binding? Returns the /// root name for the diagnostic when so. /// /// This checks the SOURCE, not `&x`'s own inferred type — `&x` (D246, /// intentional) always infers as the READONLY `*T` regardless of `x`'s L1 /// binding (L1 does not leak into L3), so a structural type-compat check /// alone can never see this violation: `Pointer(bare T)` and /// `Pointer(Mut(T))` both collapse to the same `Ptr` category in /// `resolved_cat_of`. The predicate is therefore invariant to whichever /// way a FUTURE separate decision on the `&x` INFERENCE default (A: stays /// always-`*T`: B: mut-source→`*mut T`, ro-source→`*T`) resolves — either /// way, a WRITABLE pointer may only be materialized from a `mut`-bound /// source, checked HERE at the point the address-expr meets the expected /// pointer type (annotation / param / field / `as`-cast target / return). /// /// Deliberately NOT recursive through `ExprKind::As` — an intermediate /// cast is its own separate checkpoint (the `ExprKind::As` arm in /// `f1_expr_inner` calls this same helper with the cast's OWN inner/target /// pair), so peeling here would double-report `(&ro_x) as *mut T` once at /// the As-node itself and again at whatever outer position consumes the /// cast's result. Only the DIRECT `&place`/`raw &place` shape roots here — /// an opaque pointer-returning call (`buf.ptr() as *mut T`) is a different, /// unrelated question the L3 pointee-writability axis already governs. fn addrof_mut_ro_source_root<'e>(&self, expr: &'e Expr) -> Option<&'e str> { match &expr.kind { ExprKind::Unary { op: UnOp::AddrOf | UnOp::RawAddrOf, operand } => { if self.is_through_ro_binding(operand) { Self::assign_root_ident(operand) } else { None } } _ => None, } } /// **№375:** shared enforcement — `expected` is the pointer type `value` /// is about to be materialized INTO (let/param/field annotation, `as`-cast /// target, or a fn's declared return type). Fires only when `expected`'s /// pointee is WRITABLE and `value` is a direct `&`/`raw &` over a /// `ro`-bound source (`addrof_mut_ro_source_root`). New diagnostic — /// closes the ro-guarantee bypass: previously `ro q *mut T = &b` (readonly /// `b`) passed `nova check` clean and the write executed at runtime. fn check_addrof_mut_from_ro_source( &self, value: &Expr, expected: &TypeRef, errors: &mut Vec<Diagnostic>, ) { if pointee_is_writable(expected) != Some(true) { return; } if let Some(root) = self.addrof_mut_ro_source_root(value) { errors.push(Diagnostic::new( format!( "[E_POINTER_MUT_FROM_RO_SOURCE] cannot materialize a writable \ `*mut T` pointer from `&{root}` — `{root}` is a `ro`-bound \ source (L1). A `*mut T` pointer may only be taken from a \ `mut`-bound binding (D246 amend, Plan 221 п.12 / №375); the \ bare `&x` DEFAULT stays the readonly `*T` regardless of `x`'s \ binding (L1 does not leak into L3) — this is checked at the \ SOURCE, where the address-expression meets the expected \ writable-pointer type. Hint: declare `mut {root} = ...`.", ), value.span, )); } } fn is_readonly_path(&self, expr: &Expr, scope: &HashMap<String, TypeRef>) -> bool { match &expr.kind { ExprKind::Member { obj, name: field_name } => { // Check if this field is readonly on obj's type. let obj_ty = self.infer_expr_type(obj, scope); if let Some(tr) = obj_ty { let type_name = match tr.strip_readonly() { TypeRef::Named { path, .. } => path.last().map(|s| s.as_str()), _ => None, }; if let Some(tname) = type_name { if let Some(fields) = self.record_fields_for(tname) { if fields.iter().any(|f| f.name == *field_name && f.readonly) { return true; } } } } // Transitivity: if the path to obj is itself readonly, propagate. self.is_readonly_path(obj, scope) } _ => false, } } /// Plan 172.5 (D326 R10): a `mut ref` borrow lives only for the synchronous /// call — it must NOT escape into a closure / `spawn` / `parallel` / /// `supervised` / `detach` body (whose lifetime can outlive the call → /// dangling pointer). Reject capturing any `mut ref` parameter of the /// enclosing function. Conservative (references, not shadow-aware) — an /// over-report is sound; the escape ban is what keeps the no-lifetimes model /// safe. Cheap no-op when the enclosing fn has no `mut ref` params. fn check_ref_escape_capture(&self, body: &Expr, errors: &mut Vec<Diagnostic>) { if self.mut_ref_param_names.borrow().is_empty() { return; } let mut names = std::collections::HashSet::new(); crate::alpha_rename::collect_names_expr(body, &mut names); self.report_ref_escape(&names, body.span, errors); } fn check_ref_escape_capture_block(&self, body: &Block, errors: &mut Vec<Diagnostic>) { if self.mut_ref_param_names.borrow().is_empty() { return; } let mut names = std::collections::HashSet::new(); crate::alpha_rename::collect_names_block(body, &mut names); self.report_ref_escape(&names, body.span, errors); } fn report_ref_escape( &self, captured: &std::collections::HashSet<String>, span: crate::diag::Span, errors: &mut Vec<Diagnostic>, ) { let mr = self.mut_ref_param_names.borrow(); for name in captured { if mr.contains(name) { errors.push(Diagnostic::new( format!( "[E_REF_ESCAPE_CAPTURE] `mut ref`-параметр `{}` захвачен \ замыканием/`spawn`/`parallel`/`supervised`/`detach` (D326 R10). \ `ref` — borrow на время СИНХРОННОГО вызова, у него нет \ лайфтайма; захват мог бы пережить вызов → висячий указатель. \ Скопируй нужное значение в локал перед захватом.", name ), span, )); } } } /// Plan 172.5 (D326 R2): a `mut ref` argument (`f(ref <place>)`) writes into /// the caller's storage, so the borrowed place must be mutable — reject a /// `ro`-bound local, and (for field/index paths) a `ro`/`priv` field or a /// `ro` collection. Reuses the assignment-target readonly checker: a mut-ref /// borrow is a potential write to exactly this target. fn check_ref_marker_mutability( &self, place: &Expr, scope: &HashMap<String, TypeRef>, errors: &mut Vec<Diagnostic>, ) { // Bare `ro`-bound local (`ro x = ...; f(ref x)`) — mutation forbidden. if let ExprKind::Ident(name) = &place.kind { if self.ro_binding_names.borrow().contains(name) { errors.push(Diagnostic::new( format!( "[E_REF_ARG_NOT_MUT] `mut ref` пишет в caller-сторадж, но `{}` \ связан через `ro` (immutable, L1 — D246 / D326 R2). Объяви его \ `mut {} = ...`, иначе мутация через `ref` невозможна.", name, name ), place.span, )); return; } } // Field / index paths — delegate to the assignment-target readonly // checker (handles `ro`-binding domination, `ro`/`priv` fields, `ro` // collections). A plain mutable Ident needs no further check. if matches!(place.kind, ExprKind::Member { .. } | ExprKind::Index { .. }) { self.check_target_readonly(place, scope, errors); } } /// Check if an assignment target is readonly and emit an error if so. /// Handles D175 (readonly fields) and D176 (readonly T index writes). fn check_target_readonly( &self, target: &Expr, scope: &HashMap<String, TypeRef>, errors: &mut Vec<Diagnostic>, ) { match &target.kind { ExprKind::Member { obj, name: field_name } => { // **№349 fix (D246 L3, Ф.3-companion):** `p.field = v` where // `p`'s type is a raw pointer is auto-deref sugar for // `(*p).field = v` (D216 §5) — a WRITE THROUGH THE POINTER. // Pointee-writability is an L3 property of the POINTER'S // TYPE (`*mut T` vs bare `*T ≡ *ro T`), independent of L1 // binding (`mut p` does NOT grant a writable pointee) and // independent of any L2 view wrapper on `p` — mirror the // `*p = v` Deref arm and `p[i] = v` Index arm below, which // already gate on `pointee_is_writable` and explicitly // ignore the binding. Before this fix `p.field = v` fell // through ALL of the checks below undetected: the L1/L2 // checks target VALUE bindings (their `scope.get(root)` // lookups see the pointer's own — unrelated — type), and // the direct-field lookup further down matches on // `tr.strip_readonly()` which never yields `Named` for a // `Pointer` type, so `type_name` was silently `None`. Net // effect: `mut p *Counter; p.v = 5` passed `nova check` // clean and only the generated C caught it downstream // (`read-only variable is not assignable`) with no Nova // diagnostic at all. Gate here FIRST and `return` in both // branches so the unrelated value-binding logic never runs // for a pointer receiver. if let Some(obj_ty) = self.infer_expr_type(obj, scope) { if let Some(writable) = pointee_is_writable(&obj_ty) { if !writable { errors.push(Diagnostic::new( format!( "[E_POINTER_RO_ASSIGN] cannot assign to `{}` \ through a readonly pointer — `*T` is a \ readonly pointee (the L3 default is `ro`: \ `*T ≡ *ro T`, Plan 147 / D246). `{}.{} = ...` \ is auto-deref sugar for `(*{}).{} = ...`; a \ writable pointee requires the `*mut T` opt-in \ on the pointer's TYPE. Pointer reassignability \ (`mut {}`) does NOT make the pointee writable.", field_name, Self::assign_root_ident(obj).unwrap_or("p"), field_name, Self::assign_root_ident(obj).unwrap_or("p"), field_name, Self::assign_root_ident(obj).unwrap_or("p"), ), target.span, )); return; } // Writable pointee (`*mut T`): the pointer indirection // itself is clear to write through, but the POINTEE's // own record type may still declare this specific // field `ro`/`priv` (D175/D220) — check those against // the pointee's named type, not the pointer's. if let Some(pointee_named) = pointee_named_type(&obj_ty) { self.check_field_write_attrs( &pointee_named, field_name, target, errors, ); } return; } } // **Plan 147 Ф.3 (D246, R2-split):** an EXPLICIT `mut T` // content-view (L2) on the binding overrides the bare-ro // freeze — `ro r mut Point` permits `r.x = v` (content ✅) // while `r = X` stays forbidden (reassign ❌, handled by D36). // This is the deliberate split of the two axes (L1 binding vs // L2 content-view); the bare-`ro r` freeze (P7) below only // applies when the binding does NOT carry an explicit `mut` // content-view. let root_view_is_mut_type = Self::assign_root_ident(obj) .and_then(|root| scope.get(root)) .map_or(false, |t| t.is_mut()); // **Plan 147 Ф.3 (D246, R2-split MIRROR):** an EXPLICIT `ro T` // content-view (L2) on the binding FREEZES the owned-graph even // under a `mut` L1 binding — `mut r ro Point` permits `r = X` // (reassign ✅, L1) but forbids `r.x = v` (content ❌, L2). This // is the symmetric counterpart of `root_view_is_mut_type`: // L2 content-view dominates field-writes independently of L1. // The freeze is along the owned-graph only — a `*` deref is a // separate target arm (L3 `pointee_is_writable`), so the L2 // wall-at-`*` (P4) is respected. The bare `ro r` freeze (P7) is // handled by `is_through_ro_binding` below; this catches the // mut-binding-with-ro-type-view case it cannot see. let root_view_is_ro_type = Self::assign_root_ident(obj) .and_then(|root| scope.get(root)) .map_or(false, |t| t.is_readonly()); if root_view_is_ro_type { errors.push(Diagnostic::new( format!( "[E_READONLY_FIELD] cannot mutate `{}` through a `ro` \ content-view binding (L2 freeze — `ro T` type-view \ dominates field-writes even under a `mut` binding; \ Plan 147 / D246 R2-split). Hint: drop the `ro` type \ modifier (`mut name Point`) if content-mutation is needed.", field_name ), target.span, )); return; } // Plan 124.8 (D175 amend): binding dominates — если корень // path = Ident бинд'енный через `ro`, любой write блокируется // даже если field имеет `mut` модификатор. Rust-style правило: // `let x = ...; x.field = ...` ❌ vs `let mut x = ...; x.field = ...` ✅. if !root_view_is_mut_type && self.is_through_ro_binding(obj) { errors.push(Diagnostic::new( format!( "[E_READONLY_FIELD] cannot mutate `{}` через `ro`-binding \ (binding dominates даже над `mut field` markers — Plan \ 124.8 / D175 amend). Hint: используй `mut binding-name = ...` \ если нужна мутация.", field_name ), target.span, )); return; } // Transitivity check: mutation through a ro field (Plan 114 D184). if self.is_readonly_path(obj, scope) { errors.push(Diagnostic::new( format!( "[E_READONLY_FIELD] cannot mutate `{}` through a `ro` field path", field_name ), target.span, )); return; } // Direct check: is this specific field readonly? if let Some(tr) = self.infer_expr_type(obj, scope) { self.check_field_write_attrs(&tr, field_name, target, errors); } } // D176 / Plan 114 D184: index write `arr[i] = x` — forbid if arr has `ro` type. ExprKind::Index { obj, .. } => { let obj_ty = self.infer_expr_type(obj, scope); // Plan 147 Ф.3 (D246, L3 pointee-capability) — RAW-POINTER index write. // `p[i] = v` ≡ `*(p+i) = v` is a write THROUGH the pointer: for a raw // pointer the pointee-mutability is read FROM THE TYPE (L3), binding- // independent — mirror the `*p = v` Deref rule below. So `ro p *mut T` // permits `p[i] = v` (oracle row C); the L1/L2 binding-freeze (which is // for VALUE collections like `[]int`/Vec) must NOT apply to raw pointers. // `pointee_is_writable` returns Some(..) only for TypeRef::Pointer (None // for value collections), so this carve-out is pointer-exact. (169.2: // P7 was over-strict on `*mut T` index-writes — [M-169.2-ptr-index-ro-binding].) if let Some(ty) = &obj_ty { if pointee_is_writable(ty).is_some() { // **№353 fix (D216 amend, Plan 174.5 §3/§9 retraction, // checker-channel companion of the codegen-side // `E_POINTER_OP_USE_METHOD` guard in `emit_c.rs`):** // `p[i] = v` — the index-write OPERATOR form on a raw // pointer is retired, full stop, regardless of the // pointee's L3 writability — mirror the Deref arm // above. Same rationale: `nova check` never reached // the codegen-only retraction check, so this form // passed clean on a writable `*mut T` pointee. Use // `p.write_at(i, v)` instead. errors.push(Diagnostic::new( "[E_POINTER_OP_USE_METHOD] operator `p[i] = v` (index \ write) on raw pointer retired (Plan 174.5 §3/§9, \ D216 amend) — use `p.write_at(i, v)`" .to_string(), target.span, )); return; } } if let Some(tr) = &obj_ty { if tr.is_readonly() { errors.push(Diagnostic::new( "[E_READONLY_CONTENT] cannot write through index on `ro` array".to_string(), target.span, )); return; } } // Plan 147 Ф.7 [M-147-ro-binding-index-freeze] + [M-147-param-index-freeze]: // L1/L2 freeze (D246 P7): a `ro`-bound local or a non-mut param is frozen — // even an explicitly-`mut`-typed content view does NOT allow index writes if // the ROOT binding is ro. Mirror the Member-field check above (line ~7878). // // `ro r mut []int` → `r[0] = x` ❌ (binding ro dominates; L1 freeze) // but `ro r mut Point` → `r.field = v` ✅ was already the Member rule. // The only carve-out: explicit `mut` content-view type on the object // (TypeRef::Mut) allows content writes regardless of binding (R2-split P6). // We check: if root is ro-bound AND the *declared type* is NOT TypeRef::Mut // (explicit mut content-view) → fire. // Plan 147 Ф.7: only enforce the ro-binding freeze for // assignments in user's own code (entry-module file_id). Imported // prelude / std functions may legitimately use `ro buf = ...` locals // that they then write through (e.g. WriteBuffer internals). Their // correctness is established when their own module is compiled; we // must not re-check them here against the user module's entry guard. let is_target_in_entry = if self.entry_file_ids.is_empty() { target.span.file_id == 0 } else { self.entry_file_ids.contains(&target.span.file_id) }; if is_target_in_entry && self.is_through_ro_binding(obj) { let explicit_mut_view = obj_ty.map_or(false, |t| t.is_mut()); if !explicit_mut_view { errors.push(Diagnostic::new( "[E_READONLY_CONTENT] cannot write through index on a `ro`-bound \ binding or non-mut param — L1/L2 freeze (Plan 147 / D246 P7). \ The binding is readonly; use `mut binding = ...` (local) or \ `mut param T` (param) to allow index writes. An explicit `mut T` \ content-view type (`ro r mut []int`) would also permit it \ (R2-split, P6)." .to_string(), target.span, )); } } } // **№353 fix (D216 amend, Plan 174.5 §3/§9 retraction, checker- // channel companion to the codegen-side `E_POINTER_OP_USE_METHOD` // guard in `emit_c.rs`):** `*p = v` — the deref-write OPERATOR // form on a raw pointer is retired, full stop, regardless of the // pointee's L3 writability. `nova check` ran no codegen and so // never reached the codegen-only retraction check — this form // passed `nova check` clean on a writable `*mut T` pointee, and // on a readonly `*T` pointee it surfaced the WRONG diagnostic // (`E_POINTER_RO_ASSIGN`, which implies the operator form would // be legal on a `*mut T` — it is not). The retraction check must // therefore fire FIRST, before any writability question, and // `return` so the (now unreachable for this arm) L3 branch below // never masks it. Use `p.write(v)` instead. ExprKind::Unary { op: UnOp::Deref, operand } => { if let Some(ty) = self.infer_expr_type(operand, scope) { if pointee_is_writable(&ty).is_some() { errors.push(Diagnostic::new( "[E_POINTER_OP_USE_METHOD] operator `*p = v` (deref \ write) on raw pointer retired (Plan 174.5 §3/§9, \ D216 amend) — use `p.write(v)`" .to_string(), target.span, )); } } } _ => {} } } /// Shared field-attribute check for a WRITE target `<obj>.<field_name>`, /// given `obj`'s already-resolved receiver type `tr` — a record's own /// `ro`/`priv` field markers (D175/D220/D281), independent of how the /// receiver was reached (owned value, or through a `*mut T` pointer — /// №349: the pointer-receiver branch of `check_target_readonly` calls /// this with the POINTEE's named type after clearing the L3 /// pointee-writability gate). Split out of the direct value-field arm /// so both receiver kinds share one implementation instead of drifting. fn check_field_write_attrs( &self, tr: &TypeRef, field_name: &str, target: &Expr, errors: &mut Vec<Diagnostic>, ) { let type_name = match tr.strip_readonly() { TypeRef::Named { path, .. } => path.last().map(|s| s.as_str()), _ => None, }; let Some(tname) = type_name else { return }; let Some(fields) = self.record_fields_for(tname) else { return }; let Some(f) = fields.iter().find(|f| f.name == *field_name) else { return }; if f.readonly { errors.push(Diagnostic::new( format!( "[E_READONLY_FIELD] cannot assign to `ro` field `{}` of type `{}`", field_name, tname ), target.span, )); } // Plan 124 (D220) + 124.6 (D225): priv field WRITE check. // Plan 160 (D281) Ф.2: module-private write. if f.priv_field && !self.priv_field_access_allowed(tname, &f.visible_to) { if f.priv_module_field { if !self.module_priv_access_allowed(tname, target.span) { errors.push(Diagnostic::new( format!( "[E_FIELD_MODULE_PRIVATE] cannot write to \ module-private field `{}.{}` from outside \ its module. Type declared with bare `priv` \ (Plan 160 / D281). Hint: add a public mutator \ method on `{}`.", tname, field_name, tname, ), target.span, )); } } else { errors.push(Diagnostic::new( format!( "[E_PRIV_FIELD_WRITE] cannot write to private \ field `{}.{}` outside type-method scope. \ Field marked `priv` (Plan 124 / D220). \ Hint: add public mutator method on `{}` \ (e.g. `export fn {} mut @set_{}(v T)`), \ move accessing code into a method of `{}`, \ or use `#test_access({})` (D225).", tname, field_name, tname, tname, field_name, tname, tname, ), target.span, )); } } } // ── End D175/D176 helpers ────────────────────────────────────────────── /// Plan 172.1 U.5.4: the SOLE structured category resolver — the LOSSLESS replacement /// of the deleted lossy `cat_of`/`cat_of_depth`/`TyCat` parallel engine. SAME /// resolution as the old `cat_of` (alias-transparent, `Vec[T]`→`Array`, /// newtype/record/sum/named-tuple→`Named` + generic args, unknown/protocol/effect/ /// `any`/`never`/generic-param→`Any`, `char`→`Named { "char" }`) but int-family /// carries EXACT `(width, signed, wide_default)`, float carries width, and `Named` /// carries generic args — instead of the `TyCat`-collapse. Feeds `cat_compatible_rt` /// (`assignable`/`f1_check_for_elem`) and `distinct_mono` (generic-arg identity). /// /// Like the old `cat_of` (and UNLIKE `from_type_ref`), int-family names resolve to /// `Scalar` REGARDLESS of generics (`int[X]`→`Scalar`) — name match, not /// `ty_of_ref`'s generics-guard. fn resolved_cat_of(&self, tr: &TypeRef, gs: &GenericScope) -> ResolvedType { self.resolved_cat_of_depth(tr, gs, 0) } fn resolved_cat_of_depth( &self, tr: &TypeRef, gs: &GenericScope, depth: u32, ) -> ResolvedType { use ResolvedType as R; if depth > 16 { return R::Any; // mirrors cat_of_depth depth-guard → Other } match tr { TypeRef::Named { path, generics, .. } => { let Some(name) = path.last() else { return R::Any; }; if gs.contains_key(name) { return R::Any; // generic type-param → permissive } // Int-family — UNGUARDED on generics (mirrors cat_of, not from_type_ref). if let Some(s) = ResolvedType::scalar_from_int_name(name) { return s; } // Generic type-arguments (lossless, U.5.3) — recursed for the nominal // arms below so `Stack[int]` ≠ `Stack[u32]` in the type-identity check. let rt_args = || -> Vec<R> { generics .iter() .map(|g| self.resolved_cat_of_depth(g, gs, depth + 1)) .collect() }; // [M-172.1-U5-nv-type-name-hardcode] §3 DEBT (carried from legacy // `cat_of`, NOT introduced here): `str` (Plan 139 lang-item // `type str value {...}` in core.nv) and `Vec` (vec_seq.nv / D239) ARE // `.nv`-DECLARED types, so name-keying their category here violates §3 // ("resolve from the declaration, not a Rust name-match"). The principled // fix derives their category from the import-resolved registry / lang-item // (U.1/U.6), NOT a wider `ResolvedType` change — U.5 only collapses the // REPRESENTATION (Ty+TyCat→ResolvedType), carrying this debt byte-identically. // `char`/`f32`/`f64`/`bool` are genuine compiler PRIMITIVES (NO `.nv` type // decl — `char.nv` declares only METHODS on the primitive, like `int`) → // sanctioned §3 exception, same class as `scalar_from_int_name`. match name.as_str() { "f32" => R::Float { width: 32 }, "f64" => R::Float { width: 64 }, "bool" => R::Bool, "str" => R::Str, // U.5.5(a): category resolver — keyed on `name` only, so `module` // stays `[]` (the C-lowering identity comes from `from_type_ref`). "char" => R::Named { name: "char".to_string(), module: Vec::new(), args: Vec::new() }, // D239 `Vec[T] ≡ []T` — same `Array(elem)` category as the sugar. "Vec" if generics.len() == 1 => { R::Array(Box::new(self.resolved_cat_of_depth(&generics[0], gs, depth + 1))) } // (legacy bare `ptr` name arm removed, U.5.4 — Plan 134; `ptr` now // resolves as an unknown name → `Any` via the `other` arm below. // `TypeRef::Pointer` (typed `*T`) still → `Ptr` further down.) "any" | "never" | "Self" => R::Any, other => match self.types.get(other) { Some(td) => match &td.kind { // Alias transparent (D52); newtype/record/sum/named-tuple // nominal by name (+ generic args); protocol/effect/opaque permissive. TypeDeclKind::Alias(inner) => { self.resolved_cat_of_depth(inner, gs, depth + 1) } TypeDeclKind::Newtype(_) | TypeDeclKind::Record(_) | TypeDeclKind::Sum(_) | TypeDeclKind::NamedTuple(_) => { R::Named { name: other.to_string(), module: Vec::new(), args: rt_args() } } TypeDeclKind::Protocol { .. } | TypeDeclKind::Effect(_) | TypeDeclKind::TypeSet(_) // Plan 172.3 (D310): bound-only, no runtime category | TypeDeclKind::Opaque => R::Any, }, None => R::Any, // unknown name → permissive }, } } TypeRef::Array(inner, _) | TypeRef::FixedArray(_, inner, _) => { R::Array(Box::new(self.resolved_cat_of_depth(inner, gs, depth + 1))) } TypeRef::Tuple(_, _) | TypeRef::Func { .. } => R::Any, TypeRef::Protocol { .. } => R::Any, TypeRef::Unit(_) => R::Unit, TypeRef::Readonly(inner, _) => self.resolved_cat_of_depth(inner, gs, depth + 1), TypeRef::Pointer(_, _) => R::Ptr, TypeRef::Mut(inner, _) | TypeRef::Uninit(inner, _) => { self.resolved_cat_of_depth(inner, gs, depth + 1) } // Plan 184: `ref T` категориально = цель (Р5 чтение = T; Р6 heap ≡ H). TypeRef::Ref(inner, _) => self.resolved_cat_of_depth(inner, gs, depth + 1), } } // [M-generic-arg-type-mismatch-silent] U.5.3: the raw-TypeRef // `generic_arg_mismatch` (+ `concrete_primitive_name` / `is_type_param_name`) was // folded into the structured `distinct_mono` over the lossless `ResolvedType` and // deleted. `generic_args` (a thin TypeRef accessor) is retained for the f1_check_call // arg-pair iteration + message display. This removed the LAST `cat_of` consumer. } /// Ф.1: результат проверки совместимости. enum Compat { /// Совместимо. Ok, /// Тип выражения не выводится — проверку пропускаем (не ошибка). Unknown, /// Несовместимо; `found` — отображение типа выражения. Bad { found: String }, /// Plan 142 (D227): целочисленный литерал вне диапазона sized-типа. /// `msg` — готовый суффикс диагностики, e.g. «300 > u8.MAX (255)». OutOfRange { msg: String }, /// [M-scalar-nonliteral-narrowing-not-enforced] (D54): неявное сужение /// НЕ-литерала из более широкого int-типа в более узкий (или /// value-range-unsafe sign-flip / cross). Требует явного `as`. `from`/`to` — /// отображения исходного и целевого типов для диагностики. Narrowing { from: String, to: String }, /// Plan 214.1 (D429 amend, R3'): a GENERIC `#coerce` pattern lookup found /// ≥2 candidates unifying to the SAME (I,O) pair at this exact position — /// `msg` is a fully-formed `[E_COERCE_DUPLICATE_PAIR]`-prefixed /// diagnostic text (mirrors `OutOfRange`'s "ready-made suffix" shape, /// pushed by callers verbatim). Distinct from a plain `Bad` mismatch: the /// position DOES have a valid coercion, just a non-deterministic CHOICE /// of which declaration provides it (see `generic_coerce_lookup` doc for /// why decl-time R3 dedup can't catch this for generic patterns). CoerceConflict { msg: String }, } /// Plan 142 (D227 Rule 3/6): диапазон `[min, max]` для sized-int типа. /// Возвращает `None` для не-sized-типов (`int`/`uint` — wide defaults /// per D227 Rule 1, диапазон не проверяется; всё остальное — не int). /// Используется `i128`, чтобы корректно сравнить `u64::MAX` и негативные /// значения с границами (литерал хранится как `i64`). fn sized_int_bounds(name: &str) -> Option<(i128, i128)> { let (min, max): (i128, i128) = match name { "u8" => (0, u8::MAX as i128), "u16" => (0, u16::MAX as i128), "u32" => (0, u32::MAX as i128), "u64" => (0, u64::MAX as i128), "i8" => (i8::MIN as i128, i8::MAX as i128), "i16" => (i16::MIN as i128, i16::MAX as i128), "i32" => (i32::MIN as i128, i32::MAX as i128), "i64" => (i64::MIN as i128, i64::MAX as i128), // `int` (= i64) и `uint` (= u64) — wide defaults (D227 Rule 1): // голый литерал держится без range-check. Не-числовые имена — // тоже None (сюда не дойдём: проверяем только TyCat::Int). _ => return None, }; Some((min, max)) } /// Plan 172.1 U.5.2: the former `sized_int_name(&TypeRef)`, `int_width_rank` and /// `is_int_narrowing` standalone helpers were folded into `ResolvedType` /// (`sized_int_name()` method + `would_narrow_into`, the single narrowing source) and /// deleted here — the raw-TypeRef second pass over `assignable` is gone. /// /// Plan 142 (D227 Rule 3/6): диапазон-проверка значения `val` (i128) /// против sized-типа `name`. `Some(msg)` — литерал вне диапазона, /// `msg` — суффикс диагностики; `None` — в пределах (или не sized). fn lit_range_check(val: i128, name: &str) -> Option<String> { let (min, max) = sized_int_bounds(name)?; if val > max { Some(format!("{val} > {name}.MAX ({max})")) } else if val < min { Some(format!("{val} < {name}.MIN ({min})")) } else { None } } /// Plan 172.1 [literal-coercion channel]: the expected payload type for a builtin /// sum constructor `Some(_)` / `Ok(_)` / `Err(_)` against a concrete `Option[T]` / /// `Result[T,E]` expected type — the type a literal payload coerces to. `None` for any /// other constructor / non-matching expected (the literal then keeps its seed). Matched /// by the LAST path segment so `std.Option`-qualified forms resolve identically (§3 — /// no name-key special-casing of one builtin; this is the standard sum-ctor shape). /// [M-196-closeout-if-body-peek] Partial knowledge about which builtin sum an /// If-branch's trailing expr constructs, per-generic-slot (`Option` has one slot, /// `Result` has two — T and E, either possibly still unknown from THIS branch /// alone, e.g. bare `None`/`Err(e)` doesn't reveal T). See /// `closure_if_ctor_peek`/`closure_if_ctor_branch_peek` (`~16916`). enum ClosureIfCtorBranch { Option(Option<TypeRef>), Result(Option<TypeRef>, Option<TypeRef>), } fn ctor_payload_expected<'a>(ctor: &str, expected: &'a TypeRef) -> Option<&'a TypeRef> { let TypeRef::Named { path, generics, .. } = expected else { return None; }; match (ctor, path.last().map(String::as_str)) { ("Some", Some("Option")) | ("Ok", Some("Result")) => generics.first(), ("Err", Some("Result")) => generics.get(1), _ => None, } } /// Plan 172.1 [literal-coercion channel]: the element type of an array/slice/Vec /// expected type (`[]T` / `[N]T` / `Vec[T]`) — what each array-literal element coerces /// to. `None` for any non-sequence expected. fn array_elem_type(expected: &TypeRef) -> Option<&TypeRef> { match expected { TypeRef::Array(inner, _) | TypeRef::FixedArray(_, inner, _) => Some(inner), TypeRef::Named { path, generics, .. } if generics.len() == 1 && path.last().map(String::as_str) == Some("Vec") => { generics.first() } _ => None, } } /// [M-d55-str-literal-coercion-name-gated] fix: is the (already category- /// resolved) `rt` the `[]u8` category — i.e. `Array(Scalar{width:8, /// signed:false})`? `resolved_cat_of`/`resolved_cat_of_depth` canonicalize /// BOTH `[]u8` sugar and `Vec[u8]` to this exact shape (D239 `[]T ≡ /// Vec[T]`), so a single structural check here covers both spellings — /// the key every str-literal→`[]u8` coercion site (`assignable_direct`'s /// `StrLit` arm) compares against, replacing the retired name-gate on the /// method literally spelled `write`. fn is_bytes_slice_rt(rt: &ResolvedType) -> bool { matches!( rt, ResolvedType::Array(inner) if matches!(**inner, ResolvedType::Scalar { width: 8, signed: false, .. }) ) } /// Plan 200 (sql-autoconv) D55 amend — "obvious single-wrapper coercion": /// how a bare value at an `expected`-typed position gets wrapped. Exactly /// ONE level (the candidate's OWN inner type is never re-unwrapped) — a /// chain like `int → UserId → Wrapper` stays rejected, only the single /// declared wrapper auto-wraps. See `single_wrap_candidates` / `assignable`. #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) enum WrapTarget { /// `type X Y` (D52 newtype) — wrap via `expr as X`. Reuses the existing /// `as`-cast codegen verbatim (incl. numeric width conversion), no new /// codegen needed. Newtype(String), /// `type X enum … | Variant(Y) | …` (D406) with `Variant` the sum's /// ONLY unary (single-payload) constructor accepting `Y` — wrap via /// `X.Variant(expr)`, same shape as the explicit form authors already /// write (`SqlValue.I(1)`) — reuses existing ctor-call codegen. SumVariant(String, String), } /// D55 amend: every syntactically eligible `(wrap_target, required inner /// type)` pair for `expected`. Zero candidates ⟹ `expected` isn't a /// declared non-generic newtype/sum — no auto-wrap possible. The CALLER /// disambiguates by matching the value's own `WrapKind` against each /// candidate's inner-type `WrapKind`: exactly one match ⟹ unambiguous /// wrap; zero or ≥2 ⟹ no auto-wrap (ambiguous, or plain mismatch — the /// existing error stands). /// /// Bootstrap-scoped to NON-GENERIC **newtype** wrappers (`generics` must be /// empty for the `Newtype` arm) — `UserId`-shaped declarations; a /// parameterized newtype (`Wrapper[T]`) stays out of scope (mirrors D55's /// existing generic-arg coercion gaps elsewhere in this checker; the `as`- /// cast codegen this arm feeds needs the DECLARED inner type verbatim, and a /// generic newtype's declared inner type is itself a bare type-param name — /// substituting it correctly is unimplemented, not merely untested). /// /// [M-generic-sumlift-mono-missing-variant-wrap] fix (реестр 221.1 №320, /// Plan p320): the **Sum** arm is NOT so scoped — `expected`'s own generics /// (e.g. `Node[K, V]`) are irrelevant to the decision here. The disambiguation /// this function feeds (`wrap_kind_of`) compares a candidate's inner type by /// NAME only for a `Named` kind (generics of THAT inner type are never /// consulted — see `wrap_kind_of`'s `Named(name.clone())` arm) — so a /// variant's declared payload `Wrap[K, V]` (using the SUM's own, still- /// abstract type-param names) already carries everything the match needs: /// `WrapKind::Named("Wrap")`, identical to what `w`'s own concrete-at-the- /// call-site type `Wrap[K, V]` resolves to. The wrap materializes as an /// ordinary constructor call `Node.Leaf(w)` (`try_wrap_leaf`), which the /// SAME generic-inference machinery every hand-written `Node.Leaf(w)` call /// already goes through — no substitution needed here, only the previously- /// blanket bail removed. Before this fix, `single_wrap_candidates` returned /// EMPTY for any generic-instantiated `expected` regardless of arm, so a /// generic sum's single-unary-variant wrap silently produced NO auto-wrap: /// the checker never flagged the resulting return-position mismatch either /// (return-value compat against the declared return type is NOT enforced via /// `assignable`/`assignable_direct` — see `check_fn`'s `FnBody::Expr`/ /// `FnBody::Block` arms, "return-type compat is checked elsewhere" — in /// practice for a bare leaf it is checked NOWHERE, only ever materialized /// correctly via this rewrite), so codegen received the RAW un-wrapped value /// and emitted `return w;` into a function whose C return type is the sum's /// own pointer/struct type — ICE (see probe fixtures /// `spec_tests/conformance/p320_sumlift_generic_*`). /// `lookup` — resolve a type NAME to its declared `TypeDeclKind` (owned — /// callers hold it either as `&TypeDecl` refs, borrowed-`HashMap<String, /// TypeDecl>`, or a fresh scan; a closure decouples this shared rule from /// any one context's concrete map shape, so BOTH `assignable` — the /// ACCEPT side — and the `annotate_sum_wrap` REWRITE pass — the emit side /// — call the exact same decision function). fn single_wrap_candidates( lookup: &impl Fn(&str) -> Option<TypeDeclKind>, expected: &TypeRef, ) -> Vec<(WrapTarget, TypeRef)> { let mut ty = expected; loop { match ty { TypeRef::Readonly(inner, _) | TypeRef::Mut(inner, _) | TypeRef::Uninit(inner, _) => { ty = inner; } _ => break, } } let TypeRef::Named { path, generics, .. } = ty else { return Vec::new() }; let Some(name) = path.last() else { return Vec::new() }; match lookup(name) { // Newtype arm stays generic-free (see doc comment above). Some(TypeDeclKind::Newtype(inner)) if generics.is_empty() => { vec![(WrapTarget::Newtype(name.clone()), inner)] } // Sum arm: `expected`'s own generics are irrelevant here (№320 fix, // see doc comment above) — no `generics.is_empty()` gate. Some(TypeDeclKind::Sum(variants)) => variants .iter() .filter_map(|v| match &v.kind { SumVariantKind::Tuple(tys) if tys.len() == 1 => Some(( WrapTarget::SumVariant(name.clone(), v.name.clone()), tys[0].clone(), )), _ => None, }) .collect(), _ => Vec::new(), } } /// D55 amend: coarse structural "kind" of a type, for wrap-candidate /// disambiguation ONLY. Deliberately STRICTER than `cat_compatible_rt` /// (which permissively treats int↔float as mutually assignable, D44): an /// `int` value must be eligible for an `I(i64)` wrap candidate and NEVER /// simultaneously for an `F(f64)` one, or `sql\`${1}\`` would be flagged /// ambiguous instead of wrapping to `I(1)`. Aliases transparently resolve /// to their underlying kind (depth-guarded). #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) enum WrapKind { IntFamily, Float, Bool, Str, Array(Box<WrapKind>), Named(String), Other, } fn wrap_kind_of(ty: &TypeRef, lookup: &impl Fn(&str) -> Option<TypeDeclKind>, depth: u32) -> WrapKind { if depth > 8 { return WrapKind::Other; } match ty { TypeRef::Readonly(inner, _) | TypeRef::Mut(inner, _) | TypeRef::Uninit(inner, _) => { wrap_kind_of(inner, lookup, depth + 1) } TypeRef::Array(inner, _) | TypeRef::FixedArray(_, inner, _) => { WrapKind::Array(Box::new(wrap_kind_of(inner, lookup, depth + 1))) } TypeRef::Named { path, generics, .. } => { let Some(name) = path.last() else { return WrapKind::Other }; match name.as_str() { "int" | "uint" | "i8" | "i16" | "i32" | "i64" | "u8" | "u16" | "u32" | "u64" => { WrapKind::IntFamily } "f32" | "f64" => WrapKind::Float, "bool" => WrapKind::Bool, "str" => WrapKind::Str, "Vec" if generics.len() == 1 => { WrapKind::Array(Box::new(wrap_kind_of(&generics[0], lookup, depth + 1))) } _ => match lookup(name) { Some(TypeDeclKind::Alias(inner)) => wrap_kind_of(&inner, lookup, depth + 1), _ => WrapKind::Named(name.clone()), }, } } _ => WrapKind::Other, } } // ─── Plan 214 (D429): `#coerce` — declarative implicit zero-cost conversions ─── /// One validated `#coerce` pair, ready for accept/rewrite/lint consumption. /// Built ONLY by [`collect_coerce_pairs`] — never constructed ad hoc — so the /// three consumers (checker accept-path `assignable`, AST-rewrite /// `MapLitAnnotator::try_coerce_leaf`, lint `W_COERCE_EXPLICIT_REDUNDANT`) /// read structurally IDENTICAL data (R9 "one window"). Fully OWNED (no `'a` /// borrow off the source `Module`) — cheap to build twice (once in /// `MapLitCtx::build` for diagnostics + rewrite, once in `TypeCheckCtx::build` /// for the accept-path), mirroring this file's existing `wrap_types`/`types` /// parallel-scan convention (see `MapLitCtx::wrap_types` doc). #[derive(Debug, Clone)] pub(crate) struct CoercePairEntry { /// O — canonical string key (see `coerce_type_key`): identifies the pair /// together with the `by_input` map's key (I). Strips `ro`/`mut`/`uninit` /// wrappers and canonicalizes `[]T`/`Vec[T]` (D239) so both spellings hit /// the same key. pub output_key: String, /// Method name to call on a value of type I (`bytes`, `into_str`, /// `into_bytes`, …) — what the rewrite splices in: `x` → `x.method_name()`. pub method_name: String, /// finalize (consume-receiver, owning return) vs view (non-consume, `ro` /// return) lane (D429 R2). Feeds the R7-mandated use-after-consume /// diagnostic text ("consumed by implicit #coerce finalization"). pub is_finalize: bool, /// Declaring `#coerce fn`'s span — R3/R11 dup-pair diagnostics + (future) /// R7 "inserted here" pointer. pub decl_span: Span, } /// Plan 214 (D429) R6: canonical string key for a `#coerce` pair's I or O /// type — strips `ro`/`mut`/`uninit` wrappers (a pair's identity doesn't /// depend on the view-return's `ro`) and canonicalizes `[]T`/`Vec[T]` (D239) /// to the SAME key so both spellings dedupe/match identically. Deliberately a /// plain structural stringifier (not `resolved_cat_of`, which needs a /// generic-scope `exp_gs` this call site never has) — sufficient because R14 /// already rejects every generic `#coerce` declaration, so every I/O this /// function ever sees is fully concrete. fn coerce_type_key(ty: &TypeRef) -> String { match ty { TypeRef::Readonly(inner, _) | TypeRef::Mut(inner, _) | TypeRef::Uninit(inner, _) => { coerce_type_key(inner) } TypeRef::Array(inner, _) | TypeRef::FixedArray(_, inner, _) => { format!("[]{}", coerce_type_key(inner)) } TypeRef::Named { path, generics, .. } => { let base = path.last().cloned().unwrap_or_default(); if base == "Vec" && generics.len() == 1 { format!("[]{}", coerce_type_key(&generics[0])) } else if generics.is_empty() { base } else { format!( "{}[{}]", base, generics.iter().map(coerce_type_key).collect::<Vec<_>>().join(",") ) } } // Tuple/Func/Protocol/etc — out of scope for a #coerce O type (would need // method-level generics to express meaningfully, themselves unsupported — // see `collect_coerce_pairs`'s method-level-generic reject); a stable-enough // fallback for a key that will simply never match anything real. Plan 214.1: // by the time a GENERIC pattern's `ret_shape` reaches this function, it has // already been run through `substitute_coerce_shape` — every `TypeRef` this // function ever sees (concrete pair OR substituted generic pattern) is fully // concrete, so this stringifier never needs to resolve a bare type-param name. other => format!("{other:?}"), } } /// Plan 214 (D429): bare concrete type NAME of `ty`, for I-side lookup — /// strips `ro`/`mut`/`uninit`, returns `Some(name)` only for a non-generic /// `TypeRef::Named` (covers `str`, `StringBuilder`, `WriteBuffer`, any /// non-generic user type — every legal V1 `#coerce` receiver type). `None` /// for anything else (array/tuple/generic-named/…) — a value of such a type /// can never be an I-side match in V1 (receiver types are never array/tuple- /// shaped in the seed pairs, and a generic-named value can't identify a /// concrete pair without unification, which V1 deliberately doesn't do — /// mirrors R14/R16's "concrete lookup, not unification" framing). fn simple_named_type_name(ty: &TypeRef) -> Option<String> { match ty { TypeRef::Readonly(inner, _) | TypeRef::Mut(inner, _) | TypeRef::Uninit(inner, _) => { simple_named_type_name(inner) } TypeRef::Named { path, generics, .. } if generics.is_empty() => path.last().cloned(), _ => None, } } // ─── Plan 214.1 (D429 amend): generic `#coerce` — R14 RETRACTED ─────────── // // Design (docs/plans/214.1-generic-coerce.md §2-3): a SECOND, separate // registry of `GenericCoercePattern`s (keyed by receiver BASE name, e.g. // `Json`), checked ONLY when the concrete `coerce_pairs` lookup misses // (`Json[User] → User` never touches the hot str/[]u8 concrete path). The // receiver's OWN carrier-bracket generics (`Json[T]`) are the ONLY pattern // variables V1 supports; unification is ONE-DIRECTIONAL and shallow (R4 // "без рекурсии вглубь" — binds whole top-level slots, never recurses INTO // a slot's own nested generics), so `Json[Json[T]]` at a `User`-expected // position binds `?T := Json[User]` (one level), NOT `?T := User` (two // levels) — the double-unwrap R4 forbids never happens because nothing // EVER chains a second lookup. /// Plan 214.1: one validated GENERIC `#coerce` pattern. Built ONLY by /// [`collect_coerce_pairs`] (same collector, same validation posture as /// `CoercePairEntry` — see that struct's doc for the "one window" rationale, /// identical here). No decl-time (I,O) IDENTITY exists for a pattern (the /// true concrete pair only crystallizes once its `params` are bound at a /// SPECIFIC call site) — this is exactly why R3' collision detection lives /// at APPLICATION time (`generic_coerce_lookup`), unlike R3's decl-time /// `seen_pairs` for concrete pairs. #[derive(Debug, Clone)] pub(crate) struct GenericCoercePattern { /// Positional carrier-generic parameter NAMES: `Json[T]` → `["T"]`, /// `Pair[K, V]` → `["K", "V"]`. Index *i* corresponds to the receiver's /// *i*-th generic ARG slot — `unify_coerce_receiver` binds these names /// against a concrete value's own generic args at the SAME positions /// (arity mismatch ⇒ no match). Validated at collection time to be bare /// single-segment `TypeRef::Named` references (see `collect_coerce_pairs` /// `E_COERCE_GENERIC_PATTERN_UNSUPPORTED` reject) — never a nested or /// concrete shape. pub params: Vec<String>, /// Declared return type AS WRITTEN — may reference `params` names bare /// (`T`) or nested (`Vec[T]`). Substituted via `substitute_coerce_shape` /// once `params` are bound by a specific unification, then keyed via /// `coerce_type_key` for the exp-key compare — byte-identical /// canonicalization to the concrete-pair path once substitution has made /// it fully concrete. pub ret_shape: TypeRef, /// Method name to call on the receiver (`x` → `x.method_name()`), same /// role as `CoercePairEntry::method_name`. pub method_name: String, /// finalize vs view lane (D429 R2) — same role as `CoercePairEntry`. pub is_finalize: bool, /// Declaring `#coerce fn`'s span — R13' self-exclusion (compared against /// `current_coerce_decl_span`) + future diagnostic "declared here" notes. pub decl_span: Span, } /// Plan 214.1: like `simple_named_type_name`, but ALSO returns the type's /// generic ARGS at any arity (possibly empty) — a GENERIC `#coerce` pattern /// lookup needs both the base name (`"Json"`, to index `generic_coerce_ /// patterns`) AND the concrete args (`[User]`, to unify against a pattern's /// `params`), which the concrete-only `simple_named_type_name` (empty- /// generics-only) can't supply. fn named_base_and_args(ty: &TypeRef) -> Option<(String, Vec<TypeRef>)> { match ty { TypeRef::Readonly(inner, _) | TypeRef::Mut(inner, _) | TypeRef::Uninit(inner, _) => { named_base_and_args(inner) } TypeRef::Named { path, generics, .. } => Some((path.last()?.clone(), generics.clone())), _ => None, } } /// Plan 214.1: one-directional structural unify of a GENERIC `#coerce` /// pattern's receiver-generic SLOTS (`params`, each a bare carrier /// type-param name by construction) against a concrete value's ACTUAL /// generic args at the SAME positions. Deliberately shallow (R4 "без /// рекурсии вглубь", design §2): each slot binds to whatever concrete /// `TypeRef` sits at that position WHOLESALE — no unification INTO that /// arg's own nested structure. The same param name repeated at ≥2 positions /// (e.g. a hypothetical `Pair[T, T]` receiver) must bind to a STRUCTURALLY /// IDENTICAL concrete arg (compared via `coerce_type_key`) or the whole /// unify fails — consistency, not silent aliasing. Arity mismatch (`params. /// len() != concrete_args.len()`) — e.g. a 1-param pattern against a value /// whose base name happens to collide at a different arity — is simply no /// match, not an error (arity mismatches are exceedingly unlikely in /// practice: the pattern's arity is the DECLARING type's own arity). fn unify_coerce_receiver( params: &[String], concrete_args: &[TypeRef], ) -> Option<HashMap<String, TypeRef>> { if params.len() != concrete_args.len() { return None; } let mut bindings: HashMap<String, TypeRef> = HashMap::new(); for (name, arg) in params.iter().zip(concrete_args) { match bindings.get(name) { Some(existing) => { if coerce_type_key(existing) != coerce_type_key(arg) { return None; } } None => { bindings.insert(name.clone(), arg.clone()); } } } Some(bindings) } /// Plan 214.1: substitute BARE carrier type-param references (per /// `bindings`, produced by `unify_coerce_receiver`) into a GENERIC `#coerce` /// pattern's `ret_shape`, producing the CONCRETE output type for one /// specific unification (`Json[T] -> T` + `?T := User` → `User`; `Json[T] -> /// Vec[T]` + `?T := User` → `Vec[User]`). Unlike `unify_coerce_receiver` /// (deliberately shallow, R4 — a MATCH/search that must stay bounded), /// this recurses freely through nested `generics`/`Array`/wrapper shapes — /// it is a plain mechanical replace with no search or ambiguity, so /// unbounded depth here carries none of R4's risk. fn substitute_coerce_shape(ty: &TypeRef, bindings: &HashMap<String, TypeRef>) -> TypeRef { match ty { TypeRef::Readonly(inner, sp) => { TypeRef::Readonly(Box::new(substitute_coerce_shape(inner, bindings)), *sp) } TypeRef::Mut(inner, sp) => { TypeRef::Mut(Box::new(substitute_coerce_shape(inner, bindings)), *sp) } TypeRef::Uninit(inner, sp) => { TypeRef::Uninit(Box::new(substitute_coerce_shape(inner, bindings)), *sp) } TypeRef::Array(inner, sp) => { TypeRef::Array(Box::new(substitute_coerce_shape(inner, bindings)), *sp) } TypeRef::FixedArray(n, inner, sp) => { TypeRef::FixedArray(*n, Box::new(substitute_coerce_shape(inner, bindings)), *sp) } TypeRef::Named { path, generics, span: _ } if path.len() == 1 && generics.is_empty() => { bindings.get(&path[0]).cloned().unwrap_or_else(|| ty.clone()) } TypeRef::Named { path, generics, span } => TypeRef::Named { path: path.clone(), generics: generics.iter().map(|g| substitute_coerce_shape(g, bindings)).collect(), span: *span, }, // Tuple/Func/Protocol/Unit/etc — a bound type-param can't legally // appear bare inside these shapes for a V1 #coerce ret_shape (the // receiver-shape validation only ever binds `params` at Named-arg // positions); pass through unchanged. other => other.clone(), } } /// Plan 214 (D429) Ф.1 / Plan 214.1 (D429 amend): scan `module` (entry items /// + peer_files) for `#coerce`-attributed `fn`s, validate them (R1 /// unarity/receiver-form, R2 zero-cost view/finalize shape, R3 /// one-decl-per-pair for CONCRETE pairs, R11 single-wrapper primacy, R12 /// effect-freedom, and — Plan 214.1 — the generic-PATTERN shape gate that /// replaced R14's blanket reject), and return THREE things: the concrete /// accept/rewrite/lint-shared registry (`I type name → applicable pairs`, /// UNCHANGED byte-for-byte from Plan 214), the NEW generic-pattern registry /// (`I base type name → applicable patterns`, Plan 214.1), and any /// validation diagnostics. /// /// `lookup` — resolve a type NAME to its declared `TypeDeclKind`, EXACTLY the /// closure shape `single_wrap_candidates`/`wrap_kind_of` already take (R11 /// reuses `single_wrap_candidates` verbatim to detect a pair already covered /// by the built-in newtype/sum wrapper). /// /// Called from THREE independent build sites (`MapLitCtx::build` — surfaces /// the diagnostics via `MapLitCtx::check_module` — `TypeCheckCtx::build` — /// consumes both registries, diagnostics discarded to avoid double-reporting /// — and the finalize-lane use-after-consume scan, concrete-pairs-only, /// generic patterns discarded there: no fixture requires a generic+finalize /// combination in Plan 214.1's scope) — same parallel-scan shape as /// `wrap_types`/`types` elsewhere in this file; deterministic over the same /// `module`, so all copies agree byte-for-byte (R9 "one window" is about the /// DATA being identical, not about a single shared mutable instance — see /// `CoercePairEntry` doc). fn collect_coerce_pairs( module: &Module, lookup: &impl Fn(&str) -> Option<TypeDeclKind>, ) -> ( HashMap<String, Vec<CoercePairEntry>>, HashMap<String, Vec<GenericCoercePattern>>, Vec<Diagnostic>, ) { let mut by_input: HashMap<String, Vec<CoercePairEntry>> = HashMap::new(); let mut generic_patterns: HashMap<String, Vec<GenericCoercePattern>> = HashMap::new(); let mut errors: Vec<Diagnostic> = Vec::new(); let mut seen_pairs: HashMap<(String, String), Span> = HashMap::new(); // Dedup by declaration span: `module.items` (fully-merged view) and // `module.peer_files[*].items_here` (per-peer copies, Plan 42.4/52 Ф.7) // overlap for a folder-module's OWN declarations — a #coerce fn declared // in `std/runtime/string/core.nv` shows up in BOTH, which without dedup // would self-collide against R3's `seen_pairs` (a fn "duplicating" its // own pair with itself, found empirically: `str @bytes()` tripped // E_COERCE_DUPLICATE_PAIR against its OWN declaration on the very first // #coerce smoke run). `Span` is `Eq+Hash` (file_id+start+end) — the same // source declaration always carries the identical span in both copies. let mut seen_spans: HashSet<Span> = HashSet::new(); let mut fns: Vec<&FnDecl> = Vec::new(); for item in &module.items { if let Item::Fn(f) = item { if seen_spans.insert(f.span) { fns.push(f); } } } for pf in &module.peer_files { for item in &pf.items_here { if let Item::Fn(f) = item { if seen_spans.insert(f.span) { fns.push(f); } } } } for f in fns { if !f.coerce_attr { continue; } // R1: unarity + V1 form gate. let Some(recv) = &f.receiver else { errors.push(Diagnostic::new( format!( "[E_COERCE_NOT_UNARY] `#coerce fn {}` — a free function (no receiver) is \ not a recognized `#coerce` shape (D429 R1). Declare it as a receiver \ method: `#coerce fn Type @method() -> ro O` (view) or `#coerce fn Type \ consume @method() -> O` (finalize).", f.name ), f.span, )); continue; }; match recv.kind { ReceiverKind::Static => { if f.params.len() == 1 { errors.push(Diagnostic::new( format!( "[E_COERCE_RECEIVER_FORM_DEFERRED] `#coerce fn {}.{}` — the \ receiver-form (static one-param constructor) is DEFERRED from V1 \ (D429 R1/§Форма): zero carriers, and the `ro Self` form check alone \ doesn't guarantee zero-cost (a constructor may clone internally). \ Declare the pair as a method on the SOURCE type instead: `#coerce \ fn {} @method() -> ro Self` (or `consume @method()` for finalize).", recv.type_name, f.name, recv.type_name ), f.span, )); } else { errors.push(Diagnostic::new( format!( "[E_COERCE_NOT_UNARY] `#coerce fn {}.{}` — a static form must take \ EXACTLY one parameter to be unary (D429 R1); got {}.", recv.type_name, f.name, f.params.len() ), f.span, )); } continue; } ReceiverKind::Instance => { if !f.params.is_empty() { errors.push(Diagnostic::new( format!( "[E_COERCE_NOT_UNARY] `#coerce fn {} @{}` — an instance-method \ `#coerce` form must take ZERO parameters (I = receiver type, D429 \ R1); got {}.", recv.type_name, f.name, f.params.len() ), f.span, )); continue; } } } let input_name = recv.type_name.clone(); // Plan 214.1 (D429 amend): R14 RETRACTED — a receiver carrying its // OWN carrier-bracket generics (`Json[T]`) is now a supported GENERIC // PATTERN (see `GenericCoercePattern`/`generic_coerce_lookup`), not a // blanket reject. Still LOUDLY rejected (same "attribute without // effect doesn't exist" posture as every other R-check here — never // a silent skip): // - a method-level type parameter (`fn[U] Type @method()`) — // nothing at the receiver determines `U` at an IMPLICIT // call-site (no turbofish on an implicit insertion); // - a receiver generic ARG that isn't a bare carrier-param // reference (nested shape `Json[[]T]`, or a concrete arg // `Json[int]`) — the shallow one-directional unifier // (`unify_coerce_receiver`, R4 "без рекурсии вглубь") only binds // bare top-level slots. if !f.generics.is_empty() { errors.push(Diagnostic::new( format!( "[E_COERCE_GENERIC_PATTERN_UNSUPPORTED] `#coerce fn {} @{}` — a \ method-level type parameter is not a supported generic `#coerce` shape \ (D429 §generic-образцы, Plan 214.1): nothing at the receiver determines \ it at an implicit call-site. Only the receiver's OWN carrier-bracket \ generics (`{}[T] @{}()`) can be pattern variables.", input_name, f.name, input_name, f.name ), f.span, )); continue; } let is_generic_recv = !recv.generics.is_empty(); let mut generic_params: Vec<String> = Vec::new(); if is_generic_recv { // FIX (probe found `OneBox[Vec[T]]` accepted silently): the // original gate looped `recv.generics`, which the parser ALWAYS // flattens to a bare-typevar list (`generic_params_to_type_refs` // emits `Named{path:[name], generics:[]}` per slot; Plan 153.5's // nested-carrier support in `parse_generic_decl_params_inner` // harvests `Vec[T]`'s free typevar `T` INTO that same flat list) // — so the premise "`Type[Vec[T]]` is a parse error" was wrong, // it parses fine, the nesting just never survives into // `recv.generics`. The TRUE per-slot structural shape survives // separately in `recv.receiver_ty` (Plan 153.5/D263: populated // whenever the receiver carries ANY carrier generics, flat or // nested) — validate ITS top-level slot list instead. A nested // slot (`Vec[T]`) is `Named{path:["Vec"], generics:[T]}` there — // `generics` non-empty — so the existing bare-shape match arm // now correctly rejects it without any other change. let structural_slots: &[TypeRef] = match &recv.receiver_ty { Some(TypeRef::Named { generics, .. }) => generics, // Defense-in-depth, not currently reachable: `receiver_ty` is // built from the SAME carrier slots that populate // `recv.generics` (parser/mod.rs `parse_fn`), so it is always // `Some` here. A future parser change dropping that // invariant must not silently accept an unvalidated shape. _ => &[], }; let mut shape_ok = !structural_slots.is_empty(); for g in structural_slots { match g { // Bare single-segment Named with no generics of its own // — BUT (former P3 gap, now actually enforced instead of // merely flagged) a bare name that happens to COINCIDE // with a real declared/primitive type (`D429Box[int] // @m()`) is syntactically indistinguishable from a true // pattern variable at this layer; resolve it via `lookup` // (user types) + the primitive-name table and reject // rather than silently treat a pinned instantiation as // polymorphic. TypeRef::Named { path, generics: gg, .. } if path.len() == 1 && gg.is_empty() && lookup(path[0].as_str()).is_none() && !TypeCheckCtx::is_primitive_type_name(&path[0]) => { generic_params.push(path[0].clone()); } _ => { shape_ok = false; break; } } } if !shape_ok { errors.push(Diagnostic::new( format!( "[E_COERCE_GENERIC_PATTERN_UNSUPPORTED] `#coerce fn {}[..] @{}` — \ receiver generic args must be BARE carrier type parameters \ (`{}[T] @{}()`), not a nested/concrete shape or a name that collides \ with a real declared/primitive type (D429 §generic-образцы, Plan \ 214.1 — R4 \"без рекурсии вглубь\": the unifier only binds top-level \ slots against a TRUE type parameter).", input_name, f.name, input_name, f.name ), f.span, )); continue; } } // R12: effect-free. if !f.effects.is_empty() { errors.push(Diagnostic::new( format!( "[E_COERCE_EFFECTFUL] `#coerce fn {} @{}` declares a non-empty effect row — \ an implicitly-inserted call must be effect-free (D429 R12): a hidden \ effect at a bare-value call-site breaks the strict-effects contract \ (\"effect visible in signature AND at the call-site\").", input_name, f.name ), f.span, )); continue; } let Some(ret_ty) = &f.return_type else { errors.push(Diagnostic::new( format!( "[E_COERCE_NOT_ZERO_COST] `#coerce fn {} @{}` has no declared return type — \ a `#coerce` pair must produce the O value (D429 R2).", input_name, f.name ), f.span, )); continue; }; // R2: zero-cost view/finalize shape. let is_finalize = recv.consume; if recv.mutable { errors.push(Diagnostic::new( format!( "[E_COERCE_NOT_ZERO_COST] `#coerce fn {} mut @{}` — a `mut`-receiver form is \ neither the view lane (non-consume + `ro` return) nor the finalize lane \ (`consume` + owning return); implicitly mutating the source at an unrelated \ coercion call-site would be a hidden side-effect (D429 R2).", input_name, f.name ), f.span, )); continue; } if !is_finalize && !matches!(ret_ty, TypeRef::Readonly(..)) { errors.push(Diagnostic::new( format!( "[E_COERCE_NOT_ZERO_COST] `#coerce fn {} @{}` — the non-`consume` (view) \ form must return `ro O` (zero-cost view, D429 R2); the declared return type \ is not `ro`-wrapped. Either wrap the return in `ro`, or mark the receiver \ `consume` to declare a finalize (owning move) pair instead.", input_name, f.name ), f.span, )); continue; } if is_finalize && matches!(ret_ty, TypeRef::Readonly(..)) { errors.push(Diagnostic::new( format!( "[E_COERCE_NOT_ZERO_COST] `#coerce fn {} consume @{}` returns `ro O` — a \ finalize (`consume`) pair must return an OWNING value (D429 R2 finalize \ lane), not a view into the receiver it just consumed.", input_name, f.name ), f.span, )); continue; } let output_ty = ret_ty.clone(); let output_key = coerce_type_key(&output_ty); // R11: pair already covered by the built-in single-wrapper coercion // (newtype-over-I / sum-with-one-unary-I-variant) — reuse the SAME // decision function `assignable`'s single-wrap fallback calls. let wrap_candidates = single_wrap_candidates(lookup, &output_ty); if let Some((target, _inner)) = wrap_candidates .iter() .find(|(_, inner)| simple_named_type_name(inner).as_deref() == Some(input_name.as_str())) { let what = match target { WrapTarget::Newtype(n) => format!("newtype `type {n} {input_name}`"), WrapTarget::SumVariant(t, v) => format!("sum variant `{t}.{v}({input_name})`"), }; errors.push(Diagnostic::new( format!( "[E_COERCE_DUPLICATE_PAIR] `#coerce fn {input_name} @{}` duplicates a pair \ already covered by the built-in single-wrapper coercion — {what} (D429 \ R11). Two implicit mechanisms on the same (I,O) pair would make the \ insertion non-deterministic for the reader; the wrapper already coerces \ this pair, remove the `#coerce` declaration.", f.name ), f.span, )); continue; } // Plan 214.1: a GENERIC pattern has no decl-time (I,O) identity (the // true pair only crystallizes once `params` are bound at a specific // call site) — R3's `seen_pairs` decl-time dedup is a CONCRETE-only // mechanism; the generic analogue (R3') is detected at APPLICATION // time instead (`generic_coerce_lookup`), by design (see that // function's doc + the design note in `GenericCoercePattern`'s doc). if is_generic_recv { generic_patterns.entry(input_name).or_default().push(GenericCoercePattern { params: generic_params, ret_shape: output_ty, method_name: f.name.clone(), is_finalize, decl_span: f.span, }); continue; } // R3: at most one `#coerce` per (I,O) pair, program-wide. let key = (input_name.clone(), output_key.clone()); if let Some(prev_span) = seen_pairs.get(&key) { errors.push( Diagnostic::new( format!( "[E_COERCE_DUPLICATE_PAIR] duplicate `#coerce` pair `{input_name} → \ {output_key}` (D429 R3: at most one `#coerce` per (I,O) pair \ program-wide, both declaration directions count).", input_name = input_name, output_key = output_key ), f.span, ) .with_note_at("first declared here", *prev_span), ); continue; } seen_pairs.insert(key, f.span); by_input.entry(input_name).or_default().push(CoercePairEntry { output_key, method_name: f.name.clone(), is_finalize, decl_span: f.span, }); } (by_input, generic_patterns, errors) } // Plan 172.1 U.5.4: the lossy `TyCat` category enum (and `cat_of`/`cat_of_depth`/ // `cat_compatible`) was deleted — `ResolvedType` + `resolved_cat_of` + `cat_compatible_rt` // are the single lossless category representation. (`Ty`/`ty_of_ref` remain pending the // U.5.4 never-check migration.) /// Plan 172.1 U.5.2: structured mirror of `cat_compatible` over `ResolvedType` /// (replaces the `TyCat` version for the `assignable` / `f1_check_for_elem` category /// gate). Permissive on `Any` (= old `Other`) and — exactly like the old `(Int,Int)` — /// on int WIDTH/SIGN: `(Scalar,Scalar)` is ALWAYS compatible here. Narrowing is decided /// SEPARATELY by `would_narrow_into` on the DIRECT (`from_type_ref`) types BEFORE this /// call, preserving the `int_width_rank`-vs-`cat_of` alias asymmetry byte-identically /// (an alias-to-int must NOT be flagged as narrowing — `int_width_rank` does not resolve /// aliases, so the width gate stays direct-only). `char` rides as `Named("char")`, so the /// old `(Char,Char)` case is covered by the `(Named,Named)` arm. fn cat_compatible_rt(found: &ResolvedType, expected: &ResolvedType) -> bool { use ResolvedType as R; match (found, expected) { (R::Any, _) | (_, R::Any) => true, // Permissive on int width/sign AND float width (assignability) — the // generic-arg type-identity check is what compares those exactly (U.5.3). (R::Scalar { .. }, R::Scalar { .. }) | (R::Float { .. }, R::Float { .. }) | (R::Scalar { .. }, R::Float { .. }) | (R::Float { .. }, R::Scalar { .. }) => true, (R::Bool, R::Bool) | (R::Str, R::Str) | (R::Unit, R::Unit) | (R::Ptr, R::Ptr) => true, // NAME only — generic args ignored here (permissive); `char` rides as `Named`. (R::Named { name: a, .. }, R::Named { name: b, .. }) => a == b, (R::Array(a), R::Array(b)) => cat_compatible_rt(a, b), _ => false, } } /// U.5.3 ([M-generic-arg-type-mismatch-silent], structured): `char` rides as /// `Named { name: "char", .. }`, so it must be treated as a primitive LEAF here, not a /// nominal record (mirrors the legacy `concrete_primitive_name` which lists `char`). fn is_char_rt(t: &ResolvedType) -> bool { matches!(t, ResolvedType::Named { name, .. } if name == "char") } /// U.5.3 — is `t` a concrete builtin scalar primitive leaf (the /// `concrete_primitive_name` set: int-family, `f32`/`f64`, `char`, `bool`, `str`)? Used /// by `distinct_mono` to decide where width/sign/float-width must match EXACTLY. fn is_prim_leaf_rt(t: &ResolvedType) -> bool { use ResolvedType as R; matches!(t, R::Scalar { .. } | R::Float { .. } | R::Bool | R::Str) || is_char_rt(t) } /// U.5.3 — structured replacement of `generic_arg_mismatch`: are two `ResolvedType` /// type-arguments of the SAME generic a DEFINITE mismatch (two distinct /// monomorphizations, a pointer-reinterpretation footgun)? Conservative — `true` ONLY /// when confident, mirroring the legacy raw-TypeRef rules on the now-lossless type: /// 1. `Any` (generic-param / unknown / protocol) either side → permissive (`false`); /// 2. both concrete primitives → mismatch iff NOT structurally equal (int names via /// `wide_default`, `f32`≠`f64` via width, `char`≠`int`, …); /// 3. both same-base generic (`Array`/`[]`, or a user `Named { args }`) → recurse on /// each nested type-argument; mismatch if any nested pair is; /// 4. both concrete NAMED records (alias-resolved via `resolved_cat_of`) with /// different names → mismatch; /// 5. a primitive vs a record (or `ptr`/`unit`/mixed) → permissive (`false`). fn distinct_mono(p: &ResolvedType, a: &ResolvedType) -> bool { use ResolvedType as R; match (p, a) { (R::Any, _) | (_, R::Any) => false, // (1) (R::Array(x), R::Array(y)) => distinct_mono(x, y), // (3) Vec/[]T // (4) both nominal records (NOT `char`, which is a primitive leaf) → name + // recurse args. (R::Named { name: np, args: ap, .. }, R::Named { name: na, args: aa, .. }) if !is_char_rt(p) && !is_char_rt(a) => { np != na || ap.len() != aa.len() || ap.iter().zip(aa.iter()).any(|(x, y)| distinct_mono(x, y)) } // (2) both concrete primitives → exact structural identity. _ if is_prim_leaf_rt(p) && is_prim_leaf_rt(a) => p != a, // (5) primitive vs record / ptr / unit / mixed → permissive. _ => false, } } /// Ф.1: generic-scope функции — её параметры + generics receiver-типа. /// Plan 196 gs-bounds: value is the FULL `GenericParam` (bounds/default/consume_bound), /// not just the bare name — see `GenericScope`'s doc. `fd.generics` is the source of /// truth for bounds; the receiver-generics loop below only ever ADDS bare names already /// declared there (dup-insert is a no-op) — `GenericParam::unbounded` is a fallback ONLY /// for the (undeclared-typevar-in-receiver diagnostic's own target) case where a /// receiver names a typevar `fn_generic_scope`'s OWN caller never validated against /// `fd.generics`, so we must not panic/clobber: synthesize a bound-less entry rather /// than assume one already exists. fn fn_generic_scope(fd: &FnDecl) -> GenericScope { let mut gs: GenericScope = HashMap::new(); for g in &fd.generics { gs.insert(g.name.clone(), g.clone()); } if let Some(r) = &fd.receiver { for tr in &r.generics { if let TypeRef::Named { path, span, .. } = tr { if path.len() == 1 { // Plan 196 gs-bounds: `fn Option[T Debug] @debug(...)` writes its // bound in the RECEIVER's carrier brackets, not a `fn[...]` prefix — // `Receiver.carrier_bounds` (`ast/mod.rs`) is the SEPARATE field that // carries it (`r.generics` here is just the bare name list). A prior // version of this fn always synthesized `GenericParam::unbounded` // for a receiver-generic name, silently dropping carrier-bracket // bounds — confirmed via repro (`[GSBOUND] gp.bounds=[]` trace) that // this was the reason `Option[T Debug]@debug`'s OWN `T` never // resolved through `resolve_generic_bound_receiver_method`. gs.entry(path[0].clone()).or_insert_with(|| { r.carrier_bounds.iter() .find(|cb| cb.name == path[0]) .cloned() .unwrap_or_else(|| GenericParam::unbounded(path[0].clone(), *span)) }); } } } } gs } /// Plan 81 Ф.2: имена-namespace со специальным dispatch в codegen /// (`gc.collect()`, `Time.sleep()`, ...) — не обычные module-qualified /// вызовы свободных функций. Совпадает со списком guard'а в /// `emit_c.rs` (Member-rewrite Plan 70.1). // Plan 172.1 U.1.4: делегирует в ЕДИНЫЙ `crate::is_intrinsic_namespace` // (раньше список был захардкожен здесь И в codegen emit_c.rs — hand-synced; // теперь один источник). Тонкий wrapper сохранён, чтобы не трогать call-sites. fn is_intrinsic_namespace(name: &str) -> bool { crate::is_intrinsic_namespace(name) } /// Plan 172.1 U.3.3-instance gate: a PRIMITIVE receiver name whose methods are (partly) /// external (`ExternalRegistry`, not the checker's `method_table`) → excluded from the /// instance-method overload rule until U.1/U.2 give the checker the full set (same gate /// as U.3.2). NOT a §3 hardcode of stdlib NAMES — it is the language primitive set (the /// §2-sanctioned width/sign/builtin exception, mirrors `scalar_from_int_name` + the /// non-int primitives), used only to DEFER, never to resolve. fn is_primitive_recv_name(name: &str) -> bool { matches!( name, "int" | "uint" | "i8" | "i16" | "i32" | "i64" | "u8" | "u16" | "u32" | "u64" | "f32" | "f64" | "str" | "bool" | "char" | "byte" | "unit" | "any" | "never" ) } /// Ф.1: syntactic ptr-detection для arithmetic-ban check в /// BoundCtx::walk_expr (где нет full type-inference). Покрывает literal /// (`null ptr`), explicit cast (`x as *()` or `x as ptr` legacy), /// scope-binding с typed ptr. /// Plan 134: `*()` = TypeRef::Pointer(TypeRef::Unit); legacy "ptr" Named still /// matched for transition period until all call-sites migrated. /// Recursion в Ident lookup безопасна — scope содержит resolved TypeRef'ы. fn expr_is_ptr_typed(e: &Expr, scope: &HashMap<String, TypeRef>) -> bool { fn typeref_is_ptr(ty: &TypeRef) -> bool { match ty { // Plan 134: *() = TypeRef::Pointer(TypeRef::Unit) = void*. TypeRef::Pointer(inner, _) => matches!(inner.as_ref(), TypeRef::Unit(_)), // Legacy "ptr" named type (transition period). TypeRef::Named { path, .. } => path.last().map_or(false, |s| s == "ptr"), _ => false, } } match &e.kind { ExprKind::NullPtrLit => true, ExprKind::As(_, ty) => typeref_is_ptr(ty), ExprKind::Ident(name) => scope.get(name).map_or(false, typeref_is_ptr), ExprKind::SelfAccess => scope.get("@").map_or(false, typeref_is_ptr), _ => false, } } /// **Plan 150 / D248:** is this type definitively `bool` or `unit`? Relational /// operators (`<` `<=` `>` `>=`) require an ORDERED operand category; `bool` /// ordering is method-only via `@compare` (D183), and `unit` has no order at /// all. Transparent over L1/L2 modifier wrappers (`ro bool` is still bool). /// Conservative: callers pass a definitively-inferred type (from /// `infer_arg_ty`) and treat `None`/other as permissive (don't fire). fn typeref_is_bool_or_unit(ty: &TypeRef) -> bool { match ty { TypeRef::Unit(_) => true, TypeRef::Named { path, .. } => path.last().map_or(false, |s| s == "bool"), TypeRef::Readonly(inner, _) | TypeRef::Mut(inner, _) => typeref_is_bool_or_unit(inner), _ => false, } } /// Владелец 2026-07-21 (D-амендмент, spec/decisions/02-types.md, рядом с /// D55/str-блоками): is this type definitively `str`? Transparent over /// L1/L2 modifier wrappers, как `typeref_is_bool_or_unit`. Conservative: /// permissive (returns `false`) on unknown/generic types — same convention /// as the sibling helpers above (не ловит T-параметры generic-функций, /// резолвящиеся в `str` только на mono). fn typeref_is_str(ty: &TypeRef) -> bool { match ty { TypeRef::Named { path, .. } => path.last().map_or(false, |s| s == "str"), TypeRef::Readonly(inner, _) | TypeRef::Mut(inner, _) => typeref_is_str(inner), _ => false, } } fn prim_ref(name: &str, span: Span) -> TypeRef { TypeRef::Named { path: vec![name.to_string()], generics: Vec::new(), span, } } /// Plan 172.1 U.4.3(d): does `ty` mention any single-name type from `names` /// (an unbound generic-param set)? Used by `resolve_generic_static_return` to /// reject a half-substituted return type (one still carrying a method-level /// typevar the turbofish did not bind). Recurses through every TypeRef shape /// that can carry a generic name. fn typeref_mentions_any(ty: &TypeRef, names: &impl GenericNameSet) -> bool { match ty { TypeRef::Named { path, generics, .. } => { (path.len() == 1 && names.has_generic_name(&path[0])) || generics.iter().any(|g| typeref_mentions_any(g, names)) } TypeRef::Array(inner, _) | TypeRef::FixedArray(_, inner, _) | TypeRef::Readonly(inner, _) | TypeRef::Mut(inner, _) | TypeRef::Uninit(inner, _) | TypeRef::Pointer(inner, _) | TypeRef::Ref(inner, _) => typeref_mentions_any(inner, names), TypeRef::Tuple(elems, _) => elems.iter().any(|e| typeref_mentions_any(e, names)), TypeRef::Func { params, return_type, .. } => { params.iter().any(|p| typeref_mentions_any(p, names)) || return_type .as_ref() .map_or(false, |r| typeref_mentions_any(r, names)) } TypeRef::Protocol { .. } | TypeRef::Unit(_) => false, } } /// Strip `ro`/`mut`/`uninit` wrappers and return `(последний сегмент пути, /// generics)` для `Named`-типа; `None` для остальных форм. Как /// `check_try_carrier_match`'s `typeref_named_base`, но свободная функция, /// дополнительно возвращающая generics (нужны для сравнения `E`-типа /// `Result[T,E]` в [`coalesce_return_fallback_advice`]). fn typeref_carrier_and_generics(t: &TypeRef) -> Option<(&str, &[TypeRef])> { match t { TypeRef::Named { path, generics, .. } => { path.last().map(|s| (s.as_str(), generics.as_slice())) } TypeRef::Readonly(inner, _) | TypeRef::Mut(inner, _) | TypeRef::Uninit(inner, _) => typeref_carrier_and_generics(inner), _ => None, } } /// [E_COALESCE_RETURN_FALLBACK] / `W_MANUAL_COALESCE` (D86 AMEND 2026-07-23): /// какой канон предложить взамен `X ?? return R` (или зеркально, ручного /// `match X { Ok(v) => v, Err(_) => return R }`) — **одна decision-функция**, /// переиспользуемая и чекером (Ф.2, `check_coalesce_return_fallback`), и /// линтом (Ф.3, `W_MANUAL_COALESCE` в `lints.rs`). Чисто-функциональна: на /// входе только типы `X` и return-типа enclosing fn, никакого AST текста — /// сама подсказка (нужен ли embed произвольного under-`Err(..)` выражения) /// строится вызывающей стороной по `Suggestion`-политике (см. таблицу Ф.2 в /// брифе): первые три исхода не требуют embed'а исходного текста (голая /// замена суффикса `?? return ...` на фиксированную строку — machine- /// applicable), `MapErr`/`OkOr` требуют embed error-выражения, которого у /// чекера нет в виде текста (нет source-map в этом контексте) — эти два /// исхода получают `HasPlaceholders`, не `MachineApplicable` (честная /// applicability, а не бланкетная). pub(crate) enum CoalesceReturnAdvice { /// `X?` — тот же носитель наружу (Option→Option, или Result→Result с /// совпадающим `E`), D85. SameCarrier, /// `X.ok()?` — операнд `Result`, функция возвращает `Option`. ResultToOptionFn, /// `X.map_err(fn(_ E) -> F => <ошибка>)?` — Result→Result, но `E` /// меняется на `F`. Closure-full параметр ОБЯЗАН быть типизирован (`_ E`, /// не голый `_` — D22/closure-full grammar), поэтому носим оба имени: /// `source_err_display` — читаемое имя `E` (операнда), `target_err_display` /// — читаемое имя `F` (return-типа). MapErr { source_err_display: String, target_err_display: String }, /// `X.ok_or(<ошибка>)?` — операнд `Option`, функция возвращает `Result`. OkOr, /// Оба типа известны и НИ один не `Option`/`Result` — обёртки для /// проброса нет, explicit `match` законен (D86-остаток, `glob.nv`-класс). NoBridgeKnown, /// Тип операнда/return не выведен, либо несёт generic-параметр (`gs`) — /// консервативное молчание (нет базы для конкретной подсказки). Unknown, } pub(crate) fn coalesce_return_fallback_advice( op_ty: Option<&TypeRef>, ret_ty: Option<&TypeRef>, gs: &GenericScope, ) -> CoalesceReturnAdvice { let (op_ty, ret_ty) = match (op_ty, ret_ty) { (Some(a), Some(b)) => (a, b), _ => return CoalesceReturnAdvice::Unknown, }; if typeref_mentions_any(op_ty, gs) || typeref_mentions_any(ret_ty, gs) { return CoalesceReturnAdvice::Unknown; } let op = match typeref_carrier_and_generics(op_ty) { Some(v @ ("Option", _)) | Some(v @ ("Result", _)) => v, Some(_) => return CoalesceReturnAdvice::NoBridgeKnown, None => return CoalesceReturnAdvice::Unknown, }; let ret = match typeref_carrier_and_generics(ret_ty) { Some(v @ ("Option", _)) | Some(v @ ("Result", _)) => v, Some(_) => return CoalesceReturnAdvice::NoBridgeKnown, None => return CoalesceReturnAdvice::Unknown, }; match (op.0, ret.0) { ("Option", "Option") => CoalesceReturnAdvice::SameCarrier, ("Result", "Result") => { match (op.1.get(1), ret.1.get(1)) { (Some(oe), Some(re)) if typeref_equal(oe, re) => CoalesceReturnAdvice::SameCarrier, (Some(oe), Some(re)) => CoalesceReturnAdvice::MapErr { source_err_display: typeref_display(oe), target_err_display: typeref_display(re), }, // E-тип одной из сторон не виден (malformed Result-generics) — // не выведено, консервативное молчание. _ => CoalesceReturnAdvice::Unknown, } } ("Result", "Option") => CoalesceReturnAdvice::ResultToOptionFn, ("Option", "Result") => CoalesceReturnAdvice::OkOr, _ => unreachable!("op/ret restricted to Option|Result above"), } } /// Общий рендер `CoalesceReturnAdvice` → (note-текст, опциональный /// `Suggestion`) — переиспользуется ОБОИМИ потребителями decision-функции: /// чекером (Ф.2, `check_coalesce_return_fallback`, `advice` из реального /// инференса) и линтом (Ф.3, `W_MANUAL_COALESCE` в `lints.rs`, `advice` из /// синтаксической эвристики над declared-типами — см. `lints.rs` /// doc-комментарий у вызывающей стороны). `suggestion_span` — куда положить /// `Suggestion` (у чекера это суффикс `?? return ...`; у линта — весь /// match-выражение, т.к. форма замены другая — линт переписывает match /// целиком, не суффикс). pub(crate) fn coalesce_advice_render( advice: &CoalesceReturnAdvice, suggestion_span: Span, ) -> (String, Option<crate::diag::Suggestion>) { use crate::diag::{Applicability, Suggestion}; match advice { CoalesceReturnAdvice::SameCarrier => ( "`?` пробрасывает ту же обёртку наружу (D85) — тот же носитель что и \ return-тип функции." .to_string(), Some(Suggestion { message: "замени на `X?`".to_string(), span: suggestion_span, replacement: "?".to_string(), applicability: Applicability::MachineApplicable, }), ), CoalesceReturnAdvice::ResultToOptionFn => ( "выражение даёт `Result`, функция возвращает `Option` — мост `.ok()` \ перед `?`." .to_string(), Some(Suggestion { message: "замени на `X.ok()?`".to_string(), span: suggestion_span, replacement: ".ok()?".to_string(), applicability: Applicability::MachineApplicable, }), ), CoalesceReturnAdvice::MapErr { source_err_display, target_err_display } => ( format!( "меняется тип ошибки (наружу `{}`) — канон `.map_err` (D85 отклонил \ авто-`From` ради явности).", target_err_display ), Some(Suggestion { message: format!( "замени на `X.map_err(fn(_ {}) -> {} => <ошибка>)?` — впиши исходное \ error-выражение вместо плейсхолдера (closure-full параметр типизирован \ обязательно — голый `fn(_)` не парсится)", source_err_display, target_err_display ), span: suggestion_span, replacement: format!( ".map_err(fn(_ {}) -> {} => /* исходное выражение ошибки */)?", source_err_display, target_err_display ), applicability: Applicability::HasPlaceholders, }), ), CoalesceReturnAdvice::OkOr => ( "выражение даёт `Option`, функция возвращает `Result` — мост \ `.ok_or(<ошибка>)`." .to_string(), Some(Suggestion { message: "замени на `X.ok_or(<ошибка>)?` — впиши исходное \ error-выражение вместо плейсхолдера" .to_string(), span: suggestion_span, replacement: ".ok_or(/* исходное выражение ошибки */)?".to_string(), applicability: Applicability::HasPlaceholders, }), ), CoalesceReturnAdvice::NoBridgeKnown => ( "обёртки для проброса нет — здесь явный `match` законен и читается \ лучше: `match x { Some(v) => v, None => return None }` (или \ `Ok(v)`/`Err(_)` аналогично для `Result`) — например \ `std/src/path/glob.nv` (`return false` / `return (false, pi)` из \ `bool`/кортеж-функций, D86-остаток)." .to_string(), None, ), CoalesceReturnAdvice::Unknown => ( "тип операнда или return-тип функции не выведен (или несёт \ generic-параметр) — недостаточно контекста для конкретной \ подсказки; замени вручную по семантике: `?` / `.ok()?` / \ `.map_err(..)?` / `.ok_or(..)?`, либо явный `match`, если обёртки \ для проброса нет." .to_string(), None, ), } } /// **Plan 147 Ф.3 (D246, L3 pointee-capability):** given the type of a /// pointer-valued expression `p`, determine whether writing through it /// (`*p = v`) is allowed by the pointee capability (L3) — read FROM THE TYPE, /// independent of the binding (L1). /// /// Returns: /// - `Some(true)` — pointee is writable (`*mut T` = `Pointer(Mut(..))`, or /// the composed `*mut uninit T` = `Pointer(Mut(Uninit(..)))` — the `Mut` /// arm matches regardless of what it wraps). /// - `Some(false)` — pointee is readonly (a bare `*T ≡ *ro T` = `Pointer(T)`, /// OR a bare `*uninit T` = `Pointer(Uninit(..))` with no `mut` opt-in — see /// the №358 note below). /// - `None` — `p` is not a pointer type (no L3 capability to enforce; /// non-pointer deref is handled elsewhere / a no-op here). /// /// Outer binding-level / value-level modifier wrappers (`Readonly` / `Mut` / /// `Uninit`) around the WHOLE type are transparent — they belong to L1/L2 (the /// name/value view), not to L3 (the pointee). A `*()` (void pointer, opaque /// pointee) carries no writable element, so it is treated as readonly. /// /// **№358 fix (D246 amend):** a bare `*uninit T` (no explicit `mut`) used to /// return `Some(true)` here — the possibly-uninit CONTRACT (init/layout, a /// READ-side UB concern, §21 item 6/`E_UNSAFE_T_READ_REQUIRES_WRAP`) was /// conflated with the L3 WRITE-capability axis. D246 states the write-cap /// opt-in is `*mut T`, full stop — `uninit` is an orthogonal axis, not a /// second way to earn it. Writing Nova-side into possibly-uninit memory /// (`p.write(v)` / `*p = v`) now requires the explicit COMPOSED opt-in `*mut /// uninit T` (`Pointer(Mut(Uninit(T)))`, §V2.2 `02-types.md:10648` amend) — /// the `TypeRef::Mut(..)` arm below already matches that shape regardless of /// what it wraps, so only the stale `TypeRef::Uninit(..)` disjunct needed /// removing (folds into the `_ => false` default). A bare `*uninit T` stays /// perfectly legal for FFI out-params where the FOREIGN side writes (the /// canonical `os_read(fd, buf *uninit u8, n)` — the OS/C side fills it, not /// checked Nova code) and for `.read()` (still `Some`-any-pointee, unaffected /// — reading is unsafe-gated separately, not L3-gated). fn pointee_is_writable(ty: &TypeRef) -> Option<bool> { match ty { // Transparent over outer L1/L2 modifier wrappers — the pointee // capability lives on the inner Pointer, not on these wrappers. TypeRef::Readonly(inner, _) | TypeRef::Mut(inner, _) | TypeRef::Uninit(inner, _) => pointee_is_writable(inner), TypeRef::Pointer(pointee, _) => Some(match pointee.as_ref() { // `*mut T` (and the composed `*mut uninit T`) → writable pointee // (L3 opt-in). Matches regardless of what `Mut` wraps. TypeRef::Mut(..) => true, // `*()` = void pointer — opaque, no writable element. TypeRef::Unit(_) => false, // Bare `*T ≡ *ro T`, and bare `*uninit T` (no `mut` opt-in, №358) // → readonly pointee (L3 default). _ => false, }), _ => None, } } /// №349 companion to `pointee_is_writable`: given the type of a /// pointer-valued expression `p`, return the pointee's own type with any /// `Mut`/`Uninit`/`Readonly` L3 wrapper stripped — the type record-field /// lookups (`ro`/`priv` markers, D175/D220) should be checked against. /// `None` when `ty` is not a pointer type at all (mirrors /// `pointee_is_writable`'s `None` case). fn pointee_named_type(ty: &TypeRef) -> Option<TypeRef> { match ty { TypeRef::Readonly(inner, _) | TypeRef::Mut(inner, _) | TypeRef::Uninit(inner, _) => pointee_named_type(inner), TypeRef::Pointer(pointee, _) => Some(match pointee.as_ref() { TypeRef::Mut(inner, _) | TypeRef::Uninit(inner, _) | TypeRef::Readonly(inner, _) => { (**inner).clone() } other => other.clone(), }), _ => None, } } /// Ф.1: человекочитаемое отображение TypeRef для диагностик. pub(crate) fn typeref_display(tr: &TypeRef) -> String { match tr { TypeRef::Named { path, generics, .. } => { let base = path.join("."); if generics.is_empty() { base } else { let inner: Vec<String> = generics.iter().map(typeref_display).collect(); format!("{}[{}]", base, inner.join(", ")) } } TypeRef::Array(inner, _) => format!("[]{}", typeref_display(inner)), TypeRef::FixedArray(n, inner, _) => { format!("[{}]{}", n, typeref_display(inner)) } TypeRef::Tuple(elems, _) => { let inner: Vec<String> = elems.iter().map(typeref_display).collect(); format!("({})", inner.join(", ")) } TypeRef::Func { params, return_type, .. } => { let ps: Vec<String> = params.iter().map(typeref_display).collect(); let rt = return_type .as_ref() .map(|t| typeref_display(t)) .unwrap_or_else(|| "()".to_string()); format!("fn({}) -> {}", ps.join(", "), rt) } // Plan 97 Ф.2 (D142): анонимный protocol — компактное отображение // через сигнатуры. В диагностике пользователю важно отличить // anon-protocol от других видов типа. TypeRef::Protocol { methods, .. } => { let sigs: Vec<String> = methods .iter() .map(|m| { let prefix = if m.is_static { "." } else { "" }; let ps: Vec<String> = m .params .iter() .map(|p| typeref_display(&p.ty)) .collect(); let rt = m .return_type .as_ref() .map(|t| format!(" -> {}", typeref_display(t))) .unwrap_or_default(); format!("{}{}({}){}", prefix, m.name, ps.join(", "), rt) }) .collect(); format!("protocol {{ {} }}", sigs.join("; ")) } TypeRef::Unit(_) => "()".to_string(), // D176 (Plan 108): readonly T — display as "readonly T" TypeRef::Readonly(inner, _) => format!("ro {}", typeref_display(inner)), // Plan 118 D216 §1 + Plan 118.5: typed pointer `*T` family — new // canonical syntax with each modifier as its own variant. Pointer is // the only sigil-introducing variant; Mut/Unsafe are pre-modifiers // that wrap an inner (typically `Pointer(T)`, producing `mut *T`/ // `unsafe *T`). Display recursively to preserve nesting (e.g. // `Mut(Pointer(Readonly(Pointer(T))))` → `mut *ro *T`). TypeRef::Pointer(inner, _) => format!("*{}", typeref_display(inner)), TypeRef::Mut(inner, _) => format!("mut {}", typeref_display(inner)), // §10a rename (Plan 174.5, 2026-07-11): see `Self::typeref_display` // above — `Uninit` wrapping `Func` keeps the `unsafe` spelling // (D216 §10 legacy fn-pointer shape), else `uninit`. TypeRef::Uninit(inner, _) => { let kw = if matches!(inner.as_ref(), TypeRef::Func { .. }) { "unsafe" } else { "uninit" }; format!("{} {}", kw, typeref_display(inner)) } TypeRef::Ref(inner, _) => format!("ref {}", typeref_display(inner)), } } /// Ф.2: построить диагностику E7310 о неверной арности type-аргументов. /// Вызывается только когда аргументы УКАЗАНЫ (`actual > 0`) — опущенные /// аргументы легальны (выводятся из контекста), это не arity-ошибка. fn arity_diag(name: &str, info: &ArityInfo, actual: usize, span: Span) -> Diagnostic { let plural = |n: usize| if n == 1 { "" } else { "s" }; let werewas = |n: usize| if n == 1 { "was" } else { "were" }; let msg = if info.count == 0 { format!( "[E7310] type `{}` is not generic — it takes no type arguments, \ but {} {} provided", name, actual, werewas(actual), ) } else { format!( "[E7310] type `{}` expects {} type argument{}, but {} {} provided", name, info.count, plural(info.count), actual, werewas(actual), ) }; let diag = Diagnostic::new(msg, span); match info.decl_span { Some(ds) => diag.with_note_at(format!("type `{}` declared here", name), ds), None => diag, } } /// Plan 101.4 (D145 Ред. 5): protocol composition validation. /// /// Проверяет три инварианта на `type X protocol { use Y use Z ... }`: /// 1. **E_PROTOCOL_EMBED_NOT_PROTOCOL** — target типа `use TypeName` /// объявлен, но это НЕ `TypeDeclKind::Protocol` (effect/record/sum/ /// alias/newtype). Embed работает только между protocol'ами. /// 2. **E_PROTOCOL_EMBED_UNKNOWN** — target не объявлен ни как protocol, /// ни как любой другой тип (typo / forgotten import). /// 3. **E_PROTOCOL_EMBED_CYCLE** — `A use B use C use A` — циклическая /// композиция. Detect через DFS. /// 4. **E_PROTOCOL_EMBED_DUPLICATE** — после flatten'а ≥2 метода с /// одинаковым (name, arity) сигнатурой пришли из разных embed-путей /// (или direct + embedded). Разрешено если строго совпадают; иначе /// ambiguity, должна быть resolved direct-override'ом (V1 — error; /// override-механизм — V2/D145 Ред. 6). /// `sig_table` — optional cross-module signature table (Plan 162.1 Step 3). /// When present, `E_PROTOCOL_EMBED_UNKNOWN` is suppressed for type names that /// are declared in a transitively-imported module captured in the sig_table. fn check_protocol_embeds( module: &Module, sig_table: Option<&crate::imports::ModuleSigTable>, errors: &mut Vec<Diagnostic>, ) { use std::collections::{HashMap, HashSet}; // Collect protocol declarations + map of all type names → kind hint. let mut proto_map: HashMap<String, (&Vec<EffectMethod>, &Vec<TypeRef>, Span)> = HashMap::new(); let mut type_kinds: HashMap<String, &'static str> = HashMap::new(); for item in &module.items { if let Item::Type(t) = item { let kind_name = match &t.kind { TypeDeclKind::Protocol { .. } => "protocol", TypeDeclKind::Effect(_) => "effect", TypeDeclKind::Record(_) => "record", TypeDeclKind::Sum(_) => "sum", TypeDeclKind::Alias(_) => "alias", TypeDeclKind::Newtype(_) => "newtype", TypeDeclKind::Opaque => "opaque", TypeDeclKind::NamedTuple(_) => "named_tuple", TypeDeclKind::TypeSet(_) => "type_set", // Plan 172.3 (D310) }; type_kinds.insert(t.name.clone(), kind_name); if let TypeDeclKind::Protocol { methods, embeds } = &t.kind { proto_map.insert(t.name.clone(), (methods, embeds, t.span)); } } } // 1+2: validate each embed reference. for (proto_name, (_methods, embeds, _span)) in &proto_map { for emb in embeds.iter() { let TypeRef::Named { path, span: emb_span, .. } = emb else { errors.push(Diagnostic::new( format!( "[E_PROTOCOL_EMBED_NOT_NAMED] `use` in protocol `{}` body \ requires a named protocol type (e.g. `use Reader`); \ complex type expressions are not allowed here", proto_name ), emb.span(), )); continue; }; let Some(emb_name) = path.last() else { continue }; // Self-embed `use Self` или `use <SelfName>` — circular trivially. if emb_name == proto_name { errors.push(Diagnostic::new( format!( "[E_PROTOCOL_EMBED_CYCLE] protocol `{}` cannot embed itself \ (`use {}`)", proto_name, emb_name ), *emb_span, )); continue; } match type_kinds.get(emb_name) { None => { // Plan 162.1 Step 3: suppress E_PROTOCOL_EMBED_UNKNOWN // when the type is known via the cross-module sig_table. // This handles the lazy-resolution scenario where a // transitively imported module's types are not yet // inline-merged into module.items. let known_via_sig_table = sig_table .map(|st| !st.find_type_modules(emb_name).is_empty()) .unwrap_or(false); if !known_via_sig_table { errors.push(Diagnostic::new( format!( "[E_PROTOCOL_EMBED_UNKNOWN] unknown type `{}` in \ `use {}` (protocol `{}` body) — type not declared \ in module or via import", emb_name, emb_name, proto_name ), *emb_span, )); } } Some(&"protocol") => { /* OK */ } Some(other) => { errors.push(Diagnostic::new( format!( "[E_PROTOCOL_EMBED_NOT_PROTOCOL] `use {}` in protocol \ `{}` body — `{}` is a {}, not a protocol. Protocol \ composition (D145 Ред. 5) requires `use <Protocol>`", emb_name, proto_name, emb_name, other ), *emb_span, )); } } } } // 3: cycle detection via DFS coloring (white/gray/black). #[derive(Clone, Copy, PartialEq)] enum Color { White, Gray, Black } let mut color: HashMap<String, Color> = proto_map.keys() .map(|n| (n.clone(), Color::White)).collect(); fn dfs_cycle( node: &str, proto_map: &HashMap<String, (&Vec<EffectMethod>, &Vec<TypeRef>, Span)>, color: &mut HashMap<String, Color>, path: &mut Vec<String>, errors: &mut Vec<Diagnostic>, ) { let cur = *color.get(node).unwrap_or(&Color::White); if cur == Color::Black { return; } if cur == Color::Gray { // Cycle: path contains `node` earlier. let cycle_start = path.iter().position(|n| n == node).unwrap_or(0); let cycle_names: Vec<String> = path[cycle_start..].to_vec(); let cycle_str = format!("{} → {}", cycle_names.join(" → "), node); // Use embed-span of first protocol in cycle if available. let span = proto_map.get(&cycle_names[0]) .map(|(_, _, s)| *s).unwrap_or_default(); errors.push(Diagnostic::new( format!( "[E_PROTOCOL_EMBED_CYCLE] cyclic protocol composition: {}", cycle_str ), span, )); return; } color.insert(node.to_string(), Color::Gray); path.push(node.to_string()); if let Some((_, embeds, _)) = proto_map.get(node) { for emb in embeds.iter() { if let TypeRef::Named { path: p, .. } = emb { if let Some(n) = p.last() { if proto_map.contains_key(n) { dfs_cycle(n, proto_map, color, path, errors); } } } } } path.pop(); color.insert(node.to_string(), Color::Black); } // Iterate sorted for deterministic diagnostic order. let mut names: Vec<String> = proto_map.keys().cloned().collect(); names.sort(); for name in &names { if *color.get(name).unwrap_or(&Color::White) == Color::White { let mut path = Vec::new(); dfs_cycle(name, &proto_map, &mut color, &mut path, errors); } } // 4: duplicate-method detection after flatten. // Flatten без cycle (cycle уже reported) — guard через max-depth. fn flatten_with_origin( name: &str, proto_map: &HashMap<String, (&Vec<EffectMethod>, &Vec<TypeRef>, Span)>, seen: &mut HashSet<String>, out: &mut Vec<(String, String, usize)>, // (origin_proto, method_name, arity) origin: &str, ) { if !seen.insert(name.to_string()) { return; } let Some((methods, embeds, _)) = proto_map.get(name) else { return; }; for m in methods.iter() { out.push((origin.to_string(), m.name.clone(), m.params.len())); } for emb in embeds.iter() { if let TypeRef::Named { path: p, .. } = emb { if let Some(n) = p.last() { flatten_with_origin(n, proto_map, seen, out, n); } } } } for (proto_name, (local_methods, _, span)) in &proto_map { let mut entries = Vec::new(); let mut seen = HashSet::new(); flatten_with_origin(proto_name, &proto_map, &mut seen, &mut entries, proto_name); // Group by (name, arity). >1 distinct origins → duplicate. let mut sig_origins: HashMap<(String, usize), Vec<String>> = HashMap::new(); for (orig, mname, arity) in entries { sig_origins.entry((mname, arity)).or_default().push(orig); } // Plan 91.8a (D183): local override allowed — если метод есть и // локально в proto_name, и из embed'ов, локальная декларация // считается override embedded-default'а (НЕ duplicate). Это // используется напр. в `Comparable.equals` default body для // embedded `Equatable.equals`. let local_sigs: HashSet<(String, usize)> = local_methods.iter() .map(|m| (m.name.clone(), m.params.len())) .collect(); for ((mname, arity), origins) in sig_origins { // Уникальные источники (один и тот же origin >1 раз не считается). let unique: HashSet<String> = origins.iter().cloned().collect(); if unique.len() > 1 { // Override-by-local case: если метод объявлен локально И // также приходит из embed'а — local wins, skip duplicate. if local_sigs.contains(&(mname.clone(), arity)) { continue; } let mut sources: Vec<String> = unique.into_iter().collect(); sources.sort(); errors.push(Diagnostic::new( format!( "[E_PROTOCOL_EMBED_DUPLICATE] method `{}/{}` in protocol \ `{}` is provided by multiple embedded protocols: {}. \ Protocol composition (D145 Ред. 5) does not yet support \ override; remove one embed or define the method directly \ (Plan 91.8a D183: declaring the method locally в `{}` \ overrides embedded default).", mname, arity, proto_name, sources.join(", "), proto_name ), *span, )); } } } } /// Plan 101.3 (D145 Ред. 5): валидация bound-имён в declaration /// generic-параметров. Для каждого `[T A + B + C]` проверяем, что /// каждое имя bound'а — это объявленный protocol, либо well-known /// stdlib-alias (Hashable/Eq/Ord/Display/Equatable/Comparable/ToStr/ /// TryFrom/TryInto), либо primitive-имя (Q-representation-bound future). /// Если имя — record/sum/effect → error E_BOUND_NOT_PROTOCOL. /// Если имя вообще unknown → error E_BOUND_UNKNOWN. /// /// `sig_table` — optional cross-module signature table (Plan 162.1 Step 3). /// When present, `E_BOUND_UNKNOWN` is suppressed for type names that are /// declared in a transitively-imported module captured in the sig_table. fn check_generic_bound_declarations( module: &Module, sig_table: Option<&crate::imports::ModuleSigTable>, errors: &mut Vec<Diagnostic>, ) { use std::collections::HashMap; // Карта известных type-имён → kind hint. let mut type_kinds: HashMap<String, &'static str> = HashMap::new(); for item in &module.items { if let Item::Type(t) = item { let kind_name = match &t.kind { TypeDeclKind::Protocol { .. } => "protocol", TypeDeclKind::Effect(_) => "effect", TypeDeclKind::Record(_) => "record", TypeDeclKind::Sum(_) => "sum", TypeDeclKind::Alias(_) => "alias", TypeDeclKind::Newtype(_) => "newtype", TypeDeclKind::Opaque => "opaque", TypeDeclKind::NamedTuple(_) => "named_tuple", TypeDeclKind::TypeSet(_) => "type_set", // Plan 172.3 (D310) }; type_kinds.insert(t.name.clone(), kind_name); } } // Well-known stdlib alias names (D237: renamed + new protocols). // D237: Hashable→Hash, Equatable→Equal, Comparable→Compare, Cloneable→Clone, // Printable→Display, DebugPrintable→Debug. let stdlib_aliases: &[&str] = &[ "Ord", "Eq", "ToStr", "TryFrom", "TryInto", "Hash", "Display", "Equal", "Compare", "Clone", "Debug", "Iterable", "From", "Into", ]; // Primitive-имена (Q-representation-bound future): let primitives: &[&str] = &[ "int", "i8", "i16", "i32", "i64", "u8", "u16", "u32", "u64", "uint", "f32", "f64", "bool", "char", "str", "any", "never", ]; // Plan 172.3 (D310): validate type-set DECLARATIONS — members must be concrete // types (not protocol/effect/another type-set), and a single set must not mix // signed/unsigned integers (u64.MAX ∉ i64; Q6). Membership/use-site checks live // в BoundCtx.check_satisfaction; this is the declaration-time soundness lock (§5). { let signed_ints: &[&str] = &["i8", "i16", "i32", "i64", "int"]; let unsigned_ints: &[&str] = &["u8", "u16", "u32", "u64", "uint"]; for item in &module.items { let Item::Type(t) = item else { continue; }; let TypeDeclKind::TypeSet(members) = &t.kind else { continue; }; let mut signed_seen: std::collections::HashSet<&str> = std::collections::HashSet::new(); let mut unsigned_seen: std::collections::HashSet<&str> = std::collections::HashSet::new(); for m in members { let TypeRef::Named { path, span, .. } = m else { continue; }; let Some(mname) = path.last() else { continue; }; let mn = mname.as_str(); // Member-concreteness: reject protocol/effect/another type-set. if let Some(&kind) = type_kinds.get(mname) { if matches!(kind, "protocol" | "effect" | "type_set") { errors.push(Diagnostic::new( format!( "[E_TYPE_SET_MEMBER_NOT_CONCRETE] member `{}` of type-set `{}` \ is a {}, not a concrete type — type-set members must be concrete \ (primitives or declared record/newtype/named-tuple/sum types) (D310).", mn, t.name, kind ), *span, )); } } if let Some(&s) = signed_ints.iter().find(|&&s| s == mn) { signed_seen.insert(s); } if let Some(&s) = unsigned_ints.iter().find(|&&s| s == mn) { unsigned_seen.insert(s); } } // Signedness uniformity (Q6, D310) — amended by Plan 206 (D423): // a PARTIAL signed/unsigned mix stays incompatible-value-domains // unsound (`u64.MAX = 2^64-1 ∉ i64`). A FULL union (every signed // member ∧ every unsigned member, no gaps — exactly `SignedInt ∪ // UnsignedInt`) is exempted: per-member monomorphization already // resolves `T.MAX`/`T.MIN` per-instance (D310 §«Семантика тела»), // and sign-agnostic comparisons (`rhs < 0`) are well-defined // (constant-false) for every unsigned member — no cross-domain // value ever needs to compare across the signed/unsigned split. // This is the same case the D310 text's own illustrative // `AnyNumber` example (02-types.md) assumed legal. `Ints` // (protocols.nv) is the stdlib instance of this exemption. let is_full_union = signed_seen.len() == signed_ints.len() && unsigned_seen.len() == unsigned_ints.len(); if !signed_seen.is_empty() && !unsigned_seen.is_empty() && !is_full_union { errors.push(Diagnostic::new( format!( "[E_TYPE_SET_MIXED_SIGNEDNESS] type-set `{}` mixes signed and unsigned \ integer members PARTIALLY — a single body cannot be sound for a partial mix \ (u64.MAX = 2^64-1 ∉ i64). Split into separate signed/unsigned sets (e.g. \ SignedInt / UnsignedInt), or list the FULL union (all of i8/i16/i32/i64/int \ + all of u8/u16/u32/u64/uint — exempted, D310 amend D423).", t.name ), t.span, )); } } } let check_bound = |b: &TypeRef, errors: &mut Vec<Diagnostic>| { let TypeRef::Named { path, span, .. } = b else { return; }; let Some(name) = path.last() else { return; }; // Если у имени префикс (`std.collections.Iter`), берём последний. // Allowed: protocol, alias, primitive. if stdlib_aliases.contains(&name.as_str()) { return; } if primitives.contains(&name.as_str()) { return; } match type_kinds.get(name) { Some(&"protocol") => { /* OK */ } // Plan 172.3 (D310): type-set is a valid generic bound (D72 amended). // Membership (T ∈ set) is checked at INSTANTIATION (check_satisfaction), // not here; this site only validates the bound NAME is a legal kind. Some(&"type_set") => { /* OK */ } Some(&kind) => { errors.push(Diagnostic::new( format!( "[E_BOUND_NOT_PROTOCOL] `{}` is a {}, not a protocol — \ generic bounds must be protocol-types (D72). Consider \ declaring `type {} protocol {{ ... }}` if structural \ contract is intended.", name, kind, name ), *span, )); } None => { // Plan 162.1 Step 3: suppress E_BOUND_UNKNOWN when the type // is known via the cross-module sig_table (lazy resolution). let known_via_sig_table = sig_table .map(|st| !st.find_type_modules(name).is_empty()) .unwrap_or(false); if !known_via_sig_table { errors.push(Diagnostic::new( format!( "[E_BOUND_UNKNOWN] unknown type `{}` used as generic bound — \ not a declared protocol, stdlib alias, or primitive. \ Did you forget to declare/import it?", name ), *span, )); } } } }; // Plan 172.3 (D310): at most ONE type-set per bound-list (`[T A + B]` with two // type-sets A,B is rejected — a single membership axis keeps semantics clear; // protocol intersection stays unbounded). let check_single_type_set = |g: &GenericParam, errors: &mut Vec<Diagnostic>| { let n_sets = g.bounds.iter().filter(|b| { matches!(b, TypeRef::Named { path, .. } if path.last().map(|n| type_kinds.get(n) == Some(&"type_set")).unwrap_or(false)) }).count(); if n_sets > 1 { errors.push(Diagnostic::new( format!( "[E_MULTIPLE_TYPE_SETS] generic parameter `{}` has {} type-set bounds — \ at most one type-set is allowed per bound-list (protocol bounds may be \ combined freely via `+`) (D310).", g.name, n_sets ), g.span, )); } }; for item in &module.items { match item { Item::Fn(f) => { for g in &f.generics { for b in &g.bounds { check_bound(b, errors); } check_single_type_set(g, errors); } } Item::Type(t) => { for g in &t.generics { for b in &g.bounds { check_bound(b, errors); } check_single_type_set(g, errors); } } _ => {} } } } /// №384 (Plan p383-bounds, единый вход): the small "does T provide X" /// backend a `default_body_calls_satisfy_for` walk needs. Two callers, two /// contexts with different data available: /// - `TypeCheckCtx` (decl-site `#impl(P)` verification, `verify_impl_protocols`) /// — has the full synth/auto-derive overlay AND a per-type field table. /// - `BoundCtx` (call-site generic-bound satisfaction, /// `check_satisfaction_against_methods`) — only the base `sig.method_table`, /// no synth overlay, no field table. /// The WALKER (the part that actually matters — recursing through a default /// body and deciding which sub-expressions are dependency calls that must /// resolve) is ONE implementation, free-standing below; each ctx supplies /// its own answers to the three primitive queries through this trait, /// rather than duplicating the walk. trait DefaultBodyProbe { fn provides_method(&self, tname: &str, name: &str) -> bool; fn provides_field(&self, tname: &str, name: &str) -> bool; fn satisfies_str_from(&self, tname: &str) -> bool; } impl<'a> DefaultBodyProbe for TypeCheckCtx<'a> { fn provides_method(&self, tname: &str, name: &str) -> bool { self.t_provides_method(tname, name) } fn provides_field(&self, tname: &str, name: &str) -> bool { self.t_provides_field(tname, name) } fn satisfies_str_from(&self, tname: &str) -> bool { self.t_satisfies_str_from(tname) } } /// №384: recursively walks `body` (a protocol default_body Block) and checks /// whether every method reference / free-fn call on Self/@ resolves to a /// method available on `tname` (directly or via well-known auto-derive: /// `str.from(T)` overload OR `T.@into() -> str`). /// /// Conservative: unknown patterns return `true` (assume satisfiable). /// Codegen general synthesizer does the precise check at emission. /// /// Единый вход: this is the SAME walker both `TypeCheckCtx::verify_impl_protocols` /// (decl-site `#impl(P)`) and `BoundCtx::check_satisfaction_against_methods` /// (call-site `[T P]` bound satisfaction) call — previously only the /// decl-site path ran this walk; the call-site path unconditionally /// accepted ANY `default_body.is_some()` without checking the body's own /// dependencies, which is exactly how `a.equal(b)` on a type without /// `@compare` slipped through to a resolver falling back onto an unrelated /// same-named method elsewhere in the program (UB, №384 Корень Б). fn default_body_calls_satisfy_for(body: &Block, tname: &str, probe: &dyn DefaultBodyProbe) -> bool { let mut ok = true; walk_default_body_block(body, tname, &mut ok, probe); ok } fn walk_default_body_block(b: &Block, tname: &str, ok: &mut bool, probe: &dyn DefaultBodyProbe) { for s in &b.stmts { walk_default_body_stmt(s, tname, ok, probe); if !*ok { return; } } if let Some(t) = &b.trailing { walk_default_body_expr(t, tname, ok, probe); } } fn walk_default_body_stmt(s: &Stmt, tname: &str, ok: &mut bool, probe: &dyn DefaultBodyProbe) { match s { Stmt::Expr(e) => walk_default_body_expr(e, tname, ok, probe), Stmt::Return { value: Some(e), .. } => walk_default_body_expr(e, tname, ok, probe), Stmt::Let(d) => walk_default_body_expr(&d.value, tname, ok, probe), Stmt::Const(_) => {} Stmt::Assign { target, value, .. } => { walk_default_body_expr(target, tname, ok, probe); walk_default_body_expr(value, tname, ok, probe); } _ => {} } } fn walk_default_body_expr(e: &Expr, tname: &str, ok: &mut bool, probe: &dyn DefaultBodyProbe) { if !*ok { return; } match &e.kind { ExprKind::Call { func, args, .. } => { // Check the call target. Two patterns matter: // 1. Member call `obj.method(...)` where `obj` is `@` (SelfAccess) // → require T provides the method. // 2. Path call `Type.method(arg)` where one arg is `@` → handle // well-known auto-derive: `str.from(@)` accepts if T has // `str.from(T)` overload OR `T.@into() -> str`. if let ExprKind::Member { obj, name } = &func.kind { if matches!(obj.kind, ExprKind::SelfAccess) { if !probe.provides_method(tname, name) { *ok = false; return; } } } else if let ExprKind::Path(parts) = &func.kind { if parts.len() == 2 && parts[0] == "str" && parts[1] == "from" { if args.iter().any(|a| matches!(a.expr().kind, ExprKind::SelfAccess)) && !probe.satisfies_str_from(tname) { *ok = false; return; } } } walk_default_body_expr(func, tname, ok, probe); for a in args { walk_default_body_expr(a.expr(), tname, ok, probe); } } ExprKind::Member { obj, name } => { if matches!(obj.kind, ExprKind::SelfAccess) { // Bare access `@method` (method-value, not call) — require T provides. if !probe.provides_method(tname, name) && !probe.provides_field(tname, name) { *ok = false; return; } } walk_default_body_expr(obj, tname, ok, probe); } ExprKind::Binary { left, right, .. } => { walk_default_body_expr(left, tname, ok, probe); walk_default_body_expr(right, tname, ok, probe); } ExprKind::Unary { operand, .. } => walk_default_body_expr(operand, tname, ok, probe), ExprKind::Try(inner) | ExprKind::Bang(inner) | ExprKind::RefArg(inner) => walk_default_body_expr(inner, tname, ok, probe), ExprKind::Coalesce(a, b) => { walk_default_body_expr(a, tname, ok, probe); walk_default_body_expr(b, tname, ok, probe); } ExprKind::As(inner, _) | ExprKind::Is(inner, _) => { walk_default_body_expr(inner, tname, ok, probe); } ExprKind::If { cond, then, else_ } => { walk_default_body_expr(cond, tname, ok, probe); walk_default_body_block(then, tname, ok, probe); if let Some(eb) = else_ { match eb { crate::ast::ElseBranch::Block(b) => walk_default_body_block(b, tname, ok, probe), crate::ast::ElseBranch::If(i) => walk_default_body_expr(i, tname, ok, probe), } } } ExprKind::Block(b) => walk_default_body_block(b, tname, ok, probe), _ => {} } } /// Plan 15 (D72): registry для bound enforcement. /// /// `protocol_specs`: для каждого `type Foo protocol { ... }` — список /// required methods (TypeDeclKind::Effect; в Nova protocol/effect единая /// форма по D62). /// /// `fn_decls`: top-level fn-декларации (для resolve вызова по имени). /// /// `method_table`: для каждого concrete-типа — методы (по имени), для /// проверки "type T satisfies protocol P". struct BoundCtx<'a> { /// Plan 15 D53 strict: только protocol-kind типов. Effect-kind /// сюда не попадает — effects не разрешены как D72 bounds. /// /// Plan 101.4 (D145 Ред. 5): значение — **flattened** список методов: /// direct + recursive embedded protocol methods. Поэтому owned `Vec`, /// а не borrow в AST (синтетическая копия после embed-expansion). /// Flatten построен в `BoundCtx::build` через DFS с cycle-protection. protocol_specs: HashMap<String, Vec<EffectMethod>>, /// Plan 15 D53 strict: effect-kind типы. Рспользуется для /// дифференциированного error-сообщения, если их пытаются /// использовать как bound («`Db` is an effect, not a protocol»). effect_decls: HashMap<String, &'a TypeDecl>, /// Plan 172.1 U.2.3.2: shared base signature registry — replaces the local /// `fn_decls`/`method_table` build loop (§0 single source). Read via /// `self.sig.fn_decls` / `self.sig.method_table` (same nested shapes, base only). /// D84 overload semantics unchanged: `Vec<&FnDecl>` per name; bound-checker /// filters by arity, codegen does full type-based resolve. sig: &'a crate::sig_registry::SigRegistry<'a>, /// Plan 53: имена sum-variant'ов (для refutability check let-pattern). /// `type Color | Red | Green` → {"Red", "Green"}. Рспользуется чтобы /// отличить `let Color.Red { x } = obj` (refutable, error) от /// `let Pair { x, y } = p` (irrefutable record). sum_variant_names: std::collections::HashSet<String>, /// Plan 162 Ф.3: type name -> declaring module name segments. /// Mirrors TypeCheckCtx.type_defining_modules; built from peer_files. type_defining_modules: HashMap<String, Vec<String>>, /// Plan 162 Ф.3: TypeMethodMap - type_name -> method_name -> /// list of module_names that declared this method. type_method_map: HashMap<String, HashMap<String, Vec<Vec<String>>>>, /// Plan 172.3 (D310): type-set name → member type-refs. Used at instantiation /// to check `T ∈ set` (E_TYPE_NOT_IN_SET). Built from module.items + peer_files. type_sets: HashMap<String, Vec<TypeRef>>, /// Plan 180: type name → declared `#impl(...)` protocol list. Lets the bound /// checker accept `[T P]` for a `#impl(P)` auto-derivable type whose P-method /// is compiler-synthesized (absent from the base `sig.method_table`). impl_protocol_types: HashMap<String, Vec<String>>, /// Plan 184 Р10: names of VALUE types — value-records (`type X value {}`, /// `AllocKind::Value`) and named tuples (`type X(a A, b B)`). A `mut x T` /// param of such a type is a by-pointer in-out reference (arg must be a /// mutable, addressable lvalue → `E_MUT_ARG_NOT_MUTABLE`). Primitives are /// recognized inline. Heap records / protocols / typevars are absent here, /// so their `mut` params keep the handle ABI unconstrained (Р6). value_type_names: std::collections::HashSet<String>, /// №386 (Plan p386-bound-doors, "4th door"): name → `TypeDecl`, so the /// declaration's OWN generic bounds (`type Box[K Equal + Hash] { … }`) /// can be checked against concrete type-args wherever a WRITTEN /// `TypeRef` names a generic type (`check_typeref_bounds`) — previously /// `BoundCtx` had no type-declaration lookup at all (only /// `TypeCheckCtx` did, for arity/existence, a separate pass this /// checker cannot call into). Scanned local + peer files, same pattern /// as `impl_protocol_types`/`value_type_names` above. type_decls: HashMap<String, &'a TypeDecl>, /// №388 (Plan p386-bound-doors) supplement: `#coerce` (D429) receiver-type /// key -> declared output `TypeRef`, for the SAME `str -> bytes() -> []u8` /// bridge `str@bytes()` provides — `AsSlice[u8]` is satisfied by `[]u8`, /// NOT literally by `str`. Before this field, `check_satisfaction_*` /// closed the gap with a blanket "any primitive vacuously satisfies any /// bound" skip that happened to also cover `str`'s real `#coerce` case /// as an unprincipled side effect (verified: removing the primitive /// blanket-skip broke `probes-p383/p383_coerce_asslice_pos/main.nv`'s /// `v.append("hello")`, a DESIGNED/verified-passing `#coerce` case from /// the prior p383 window, until this field was added). `#coerce` is now /// its OWN explicit satisfaction path (task's own framing: "#coerce — /// отдельный законный путь, оформить явно"), tried in /// `check_satisfaction_against_methods` for ANY concrete type — not /// primitive-only — after the direct structural check fails, before /// reporting "missing". Deliberately NOT the full D429 R1-R15 /// validated `collect_coerce_pairs` registry (that emits `E_COERCE_*` /// diagnostics and needs a `TypeCheckCtx`-only `lookup` closure this /// separate pass doesn't have) — a lightweight, best-effort re-scan of /// `#coerce`-attributed fns' receiver -> return type, matching the /// permissive "best-effort, not the source of truth" character of /// every other lookup in this struct (`type_decls`, `impl_protocol_types`, /// …). Malformed `#coerce` declarations are still caught by /// `TypeCheckCtx`'s own `E_COERCE_*` diagnostics independently. coerce_output: HashMap<String, TypeRef>, /// [D52-амендмент, ОКНО-5, M-newtype-over-fn-type-unsupported / /// M-alias-of-fn-type-not-callable]: name of a declared `type X fn(A) -> /// B` newtype (one level — the grammar itself disallows chaining) OR /// `type X alias fn(A) -> B` (any alias chain, resolved fully /// transitively — D52 alias-transparency) → the underlying `Func` /// shape. Lets `check_call_callee_not_local_shadow` treat `X`-typed /// locals as callable (call-through, 222.3 §5а). Mirrors codegen's own /// independent `fn_newtype_sigs` pre-scan in `emit_c.rs` (same design, /// no shared state — checker and codegen run as separate passes). fn_type_names: HashMap<String, TypeRef>, /// [M-property-testing-rot] (Plan 172.13 батч 3), upgraded Plan 176 Ф.1 /// ([M-176-io-forward-bounded-generic]): generic-scope (Plan 196 gs-bounds — /// name → full `GenericParam`, bounds included) of the fn whose body is /// currently being walked. A nested call inside a bounded generic body /// forwards the enclosing fn's typevar (`property[G Generator[T], T]` body → /// `property_with(gen, ...)` binds callee `G := caller G`; io-core's /// `read_to_string[R Read]` forwarding into `read_to_end[R2 Read]`) — the /// satisfaction check must recognize the PASSTHROUGH typevar. Was a bare /// `HashSet<String>` (172.13): matched by NAME only, so it skipped /// unconditionally whenever the concrete arg's name happened to be some /// generic-param of the caller — even an UNBOUNDED one (`fn f[R](r R) { /// read_to_end(r) }`, no bound on `R` at all), silently letting a genuinely /// unsatisfied forward through instead of the honest "does not satisfy" /// diagnostic (verified regression: `nova check` on that shape passed /// clean pre-fix). Now the full `GenericScope` — `check_satisfaction` reads /// the caller's OWN declared bound(s) for that name and only skips when one /// of them actually satisfies the callee's required bound (exact-name match, /// or a protocol-hierarchy superset via `protocol_specs`'s own DFS-flattened /// method list — see `generic_bound_covers`); otherwise falls through to the /// normal structural check, which correctly reports the gap (the typevar has /// no `method_table` entry, so every required method is "missing"). current_fn_gs: std::cell::RefCell<GenericScope>, /// №303 (221.1) [M-receiver-carrier-bound-self-passthrough]: the SYMBOLIC /// receiver type (`Named{type_name, generics: r.generics}`, e.g. /// `MapItP[I, T, U]` with bare typevar generics) of the fn currently /// being walked, `None` for a static fn / free fn / test block. Mirrors /// `current_fn_gs` (same set/clear discipline, same "current fn" scope). /// Needed because THIS walker (unlike `TypeCheckCtx`, `types/mod.rs` /// ~7594) never seeds a `"@"` scope entry — a nested, unresolved `Self` /// inside a param's declared type (`g FiltItP[Self, U]`, /// `repro_param.nv`) would otherwise reach `check_satisfaction` as a /// literal type NAMED "Self" (never found in `method_table`), producing /// a false bound-violation diagnostic — found verifying /// `check_receiver_carrier_bounds` against the real corpus. current_recv_ty: std::cell::RefCell<Option<TypeRef>>, } /// №384 (Plan p383-bounds): `BoundCtx`'s `DefaultBodyProbe` backend — base /// `sig.method_table` only, no synth/auto-derive overlay (that overlay is /// TypeCheckCtx-private) and no per-type field table (`provides_field` /// conservatively `false`). Strictly a SUBSET of what `TypeCheckCtx`'s own /// impl can answer, never a superset — this can only make the call-site /// bound-check MORE conservative (occasionally still reports "missing" for /// a method that IS really synth-derivable), never less conservative, so it /// carries no risk of masking a real gap the way the old unconditional /// `default_body.is_some() => satisfied` did. impl<'a> DefaultBodyProbe for BoundCtx<'a> { fn provides_method(&self, tname: &str, name: &str) -> bool { self.sig.methods_of(tname) .map_or(false, |m| m.keys().any(|k| k.trim_start_matches('@') == name)) } fn provides_field(&self, _tname: &str, _name: &str) -> bool { false } fn satisfies_str_from(&self, tname: &str) -> bool { let str_from = self.sig.methods_of("str") .and_then(|m| m.get("from")) .map_or(false, |fns| fns.iter().any(|f| { f.params.len() == 1 && matches!(&f.params[0].ty, TypeRef::Named { path, .. } if path.last().map_or(false, |s| s == tname)) })); if str_from { return true; } self.sig.methods_of(tname).map_or(false, |m| { m.get("into").or_else(|| m.get("@into")).map_or(false, |fns| fns.iter().any(|f| { matches!(&f.return_type, Some(TypeRef::Named { path, .. }) if path.len() == 1 && path[0] == "str") })) }) } } impl<'a> BoundCtx<'a> { fn build(module: &'a Module, sig: &'a crate::sig_registry::SigRegistry<'a>) -> Self { // Plan 101.4: direct = name → (own methods, embed-typerefs). // Используется для flatten DFS ниже. let mut direct: HashMap<String, (Vec<EffectMethod>, Vec<TypeRef>)> = HashMap::new(); // U.2.3.2: fn_decls/method_table no longer built here — read from `sig`. let mut sum_variant_names: std::collections::HashSet<String> = std::collections::HashSet::new(); let mut effect_decls: HashMap<String, &TypeDecl> = HashMap::new(); // Plan 172.3 (D310): type-set name → member type-refs (membership at instantiation). let mut type_sets: HashMap<String, Vec<TypeRef>> = HashMap::new(); // Scan local items + peer files (so cross-module/stdlib type-sets like // SignedInt resolve when the generic fn lives in another file of the package). let type_set_scan = |items: &[Item], type_sets: &mut HashMap<String, Vec<TypeRef>>| { for item in items { if let Item::Type(t) = item { if let TypeDeclKind::TypeSet(members) = &t.kind { type_sets.insert(t.name.clone(), members.clone()); } } } }; type_set_scan(&module.items, &mut type_sets); for pf in &module.peer_files { type_set_scan(&pf.items_here, &mut type_sets); } // Plan 180: collect `#impl(...)` declarations per type (local + peers). let mut impl_protocol_types: HashMap<String, Vec<String>> = HashMap::new(); let impl_scan = |items: &[Item], m: &mut HashMap<String, Vec<String>>| { for item in items { if let Item::Type(t) = item { if !t.impl_protocols.is_empty() { m.insert(t.name.clone(), t.impl_protocols.clone()); } } } }; impl_scan(&module.items, &mut impl_protocol_types); for pf in &module.peer_files { impl_scan(&pf.items_here, &mut impl_protocol_types); } // Plan 184 Р10: collect VALUE-type names (value-records + named tuples). let mut value_type_names: std::collections::HashSet<String> = std::collections::HashSet::new(); let value_scan = |items: &[Item], m: &mut std::collections::HashSet<String>| { for item in items { if let Item::Type(t) = item { let is_named_tuple = matches!(t.kind, TypeDeclKind::NamedTuple(_)); if t.allocation.is_stack_value() || is_named_tuple { m.insert(t.name.clone()); } } } }; value_scan(&module.items, &mut value_type_names); for pf in &module.peer_files { value_scan(&pf.items_here, &mut value_type_names); } // №386 (Plan p386-bound-doors): name -> TypeDecl (local + peers), see // field doc on `type_decls`. let mut type_decls: HashMap<String, &'a TypeDecl> = HashMap::new(); for item in &module.items { if let Item::Type(t) = item { type_decls.insert(t.name.clone(), t); } } for pf in &module.peer_files { for item in &pf.items_here { if let Item::Type(t) = item { type_decls.insert(t.name.clone(), t); } } } // №388 (Plan p386-bound-doors) supplement: `#coerce fn Type @method() // -> O` -> receiver-type-name -> O (see `coerce_output`'s field doc). // Lightweight re-scan, NOT the validated D429 registry. let mut coerce_output: HashMap<String, TypeRef> = HashMap::new(); let coerce_scan = |items: &[Item], m: &mut HashMap<String, TypeRef>| { for item in items { let Item::Fn(f) = item else { continue }; if !f.coerce_attr { continue; } let Some(recv) = &f.receiver else { continue }; let Some(rt) = &f.return_type else { continue }; m.entry(recv.type_name.clone()).or_insert_with(|| rt.clone()); } }; coerce_scan(&module.items, &mut coerce_output); for pf in &module.peer_files { coerce_scan(&pf.items_here, &mut coerce_output); } // [D52-амендмент, ОКНО-5]: callable-fn-type names (newtype-over-fn / // alias-of-fn) — see `fn_type_names` doc above. let mut fn_type_names: HashMap<String, TypeRef> = HashMap::new(); { let mut newtype_raw: HashMap<String, TypeRef> = HashMap::new(); let mut alias_raw: HashMap<String, TypeRef> = HashMap::new(); let fn_type_scan = |items: &[Item], newtype_raw: &mut HashMap<String, TypeRef>, alias_raw: &mut HashMap<String, TypeRef>| { for item in items { if let Item::Type(t) = item { match &t.kind { TypeDeclKind::Newtype(inner @ TypeRef::Func { .. }) => { newtype_raw.insert(t.name.clone(), inner.clone()); } TypeDeclKind::Alias(inner) => { alias_raw.insert(t.name.clone(), inner.clone()); } _ => {} } } } }; fn_type_scan(&module.items, &mut newtype_raw, &mut alias_raw); for pf in &module.peer_files { fn_type_scan(&pf.items_here, &mut newtype_raw, &mut alias_raw); } fn resolve_fn_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_fn_chain(n2, newtype_raw, alias_raw, depth + 1); } } _ => {} } } None } for name in newtype_raw.keys().chain(alias_raw.keys()) { if let Some(f) = resolve_fn_chain(name, &newtype_raw, &alias_raw, 0) { fn_type_names.insert(name.clone(), f); } } } for item in &module.items { match item { Item::Type(t) => { // Plan 15 D53 strict: protocol-kind → eligible как // bound (D72); effect-kind → отдельный registry для // диагностики «used as bound but it's an effect». match &t.kind { TypeDeclKind::Protocol { methods, embeds } => { direct.insert( t.name.clone(), (methods.clone(), embeds.clone()), ); } TypeDeclKind::Effect(_) => { effect_decls.insert(t.name.clone(), t); } // Plan 53: sum-variants для refutability check. TypeDeclKind::Sum(variants) => { for v in variants { sum_variant_names.insert(v.name.clone()); } } _ => {} } } _ => {} } } // Plan 101.4 flatten: для каждого protocol'а собираем полный // список методов = direct ∪ recursively-embedded. Cycle-protection // через `seen` — если протокол повторно встречается в DFS, его // методы НЕ добавляются повторно (silent skip; error diagnostic — // в `check_protocol_embeds` отдельно). Duplicate-method конфликты // тоже только в check_protocol_embeds; здесь — bag-union. fn flatten_dfs( name: &str, direct: &HashMap<String, (Vec<EffectMethod>, Vec<TypeRef>)>, seen: &mut std::collections::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_dfs(emb_name, direct, seen, out); } } } } let mut protocol_specs: HashMap<String, Vec<EffectMethod>> = HashMap::new(); for name in direct.keys() { let mut out = Vec::new(); let mut seen = std::collections::HashSet::new(); flatten_dfs(name, &direct, &mut seen, &mut out); protocol_specs.insert(name.clone(), out); } // Plan 162 Ф.3: build type_defining_modules and type_method_map for BoundCtx. // Same logic as TypeCheckCtx.build; BoundCtx needs them for is_inherent_method // used in resolve_instance_method. let mut type_defining_modules: HashMap<String, Vec<String>> = HashMap::new(); let mut type_method_map: HashMap<String, HashMap<String, Vec<Vec<String>>>> = HashMap::new(); if module.peer_files.is_empty() { for item in &module.items { match item { Item::Type(td) => { type_defining_modules.entry(td.name.clone()).or_insert_with(|| module.name.clone()); } Item::Fn(f) => { if let Some(recv) = &f.receiver { let module_list = type_method_map.entry(recv.type_name.clone()).or_default().entry(f.name.clone()).or_default(); if !module_list.contains(&module.name) { module_list.push(module.name.clone()); } } } _ => {} } } } else { for pf in &module.peer_files { for item in &pf.items_here { match item { Item::Type(td) => { type_defining_modules.entry(td.name.clone()).or_insert_with(|| pf.module_name.clone()); } Item::Fn(f) => { if let Some(recv) = &f.receiver { let module_list = type_method_map.entry(recv.type_name.clone()).or_default().entry(f.name.clone()).or_default(); if !module_list.contains(&pf.module_name) { module_list.push(pf.module_name.clone()); } } } _ => {} } } } } BoundCtx { protocol_specs, effect_decls, sig, sum_variant_names, type_defining_modules, type_method_map, type_sets, impl_protocol_types, value_type_names, type_decls, coerce_output, fn_type_names, current_fn_gs: std::cell::RefCell::new(HashMap::new()), current_recv_ty: std::cell::RefCell::new(None) } } /// Plan 162 Ф.3: returns true iff method_name on type type_name is inherent /// (declared in the same module as the type). fn is_inherent_method(&self, type_name: &str, method_name: &str) -> bool { let Some(type_module) = self.type_defining_modules.get(type_name) else { return false; }; let Some(type_methods) = self.type_method_map.get(type_name) else { return false; }; let Some(method_modules) = type_methods.get(method_name) else { return false; }; method_modules.iter().any(|m| m == type_module) } fn check_module(&self, module: &Module, errors: &mut Vec<Diagnostic>) { // Plan 56 Ф.2.7 reverted (2026-05-20, D122 amended): эффекты в // protocol-методах РАЗРЕШЕНЫ. Под mono-dispatch (bootstrap) эффект // protocol-метода пробрасывается как у любой effectful-функции; // прежний запрет касался только true-vtable dispatch (Plan 03 — // там effectful-protocol bounds обязаны mono-dispatch'иться). // Пример: `type TryFrom[T,E] protocol { try_from(t T) Fail[E] -> Self }`. for item in &module.items { match item { Item::Fn(f) => { let mut scope: HashMap<String, TypeRef> = HashMap::new(); // Регистрируем параметры функции с их типами. for p in &f.params { scope.insert(p.name.clone(), p.ty.clone()); } // [M-property-testing-rot], upgraded Plan 176 Ф.1: publish the fn's // full generic-scope (Plan 196 `fn_generic_scope` — name + bounds) so // `check_satisfaction` can recognize a passthrough typevar AND verify // its declared bound actually covers the callee's required one. *self.current_fn_gs.borrow_mut() = fn_generic_scope(f); // №303: symbolic receiver type for "Self" passthrough // resolution (see `current_recv_ty`'s own doc). *self.current_recv_ty.borrow_mut() = f.receiver.as_ref().map(|r| TypeRef::Named { path: vec![r.type_name.clone()], generics: r.generics.clone(), span: r.span, }); // №386 (Plan p386-bound-doors): check the DECLARATION-level // bounds of every generic type named in a param/return // annotation (`fn f(m IndexMap[NoMethods, int])`) — after // `current_fn_gs` is set above, so a param whose type-arg is // the enclosing fn's OWN typevar correctly uses the // passthrough-coverage path `check_satisfaction` already has, // instead of a false positive on an unresolved name. for p in &f.params { self.check_typeref_bounds(&p.ty, errors); } if let Some(rt) = &f.return_type { self.check_typeref_bounds(rt, errors); } self.walk_fn_body(f, &mut scope, errors); self.current_fn_gs.borrow_mut().clear(); *self.current_recv_ty.borrow_mut() = None; } Item::Test(t) => { // Plan 15: тесты тоже могут содержать generic-вызовы // c bounds — обходим их body со свежим scope. let mut scope: HashMap<String, TypeRef> = HashMap::new(); self.walk_block(&t.body, &mut scope, errors); } // №386 (Plan p386-bound-doors): a record/named-tuple/sum-variant // FIELD declared with a concrete generic type-arg // (`type Foo { m IndexMap[NoMethods, int] }`) is a written // `TypeRef` just like a param/return type — check it once here // at the declaration (not per instantiation: a concrete field // type is a fixed fact of the declaration, independent of how // many times `Foo` itself gets instantiated). `current_fn_gs` is // seeded with THIS type's own generics so a field reusing `Foo`'s // own type-param (`type Foo[T] { m IndexMap[T, int] }`) is a // passthrough, not a false positive — same mechanism `fn_generic_scope` // gives function bodies above. Item::Type(t) => { let mut gs: GenericScope = HashMap::new(); for g in &t.generics { gs.insert(g.name.clone(), g.clone()); } *self.current_fn_gs.borrow_mut() = gs; match &t.kind { TypeDeclKind::Record(fields) => { for f in fields { self.check_typeref_bounds(&f.ty, errors); } } TypeDeclKind::NamedTuple(fields) => { for f in fields { self.check_typeref_bounds(&f.ty, errors); } } TypeDeclKind::Sum(variants) => { for v in variants { match &v.kind { SumVariantKind::Tuple(tys) => { for ty in tys { self.check_typeref_bounds(ty, errors); } } SumVariantKind::Record(fields) => { for f in fields { self.check_typeref_bounds(&f.ty, errors); } } SumVariantKind::Unit => {} } } } TypeDeclKind::Newtype(inner) | TypeDeclKind::Alias(inner) => { self.check_typeref_bounds(inner, errors); } TypeDeclKind::Protocol { .. } | TypeDeclKind::Effect(_) | TypeDeclKind::TypeSet(_) | TypeDeclKind::Opaque => {} } self.current_fn_gs.borrow_mut().clear(); } _ => {} } } } fn walk_fn_body(&self, f: &FnDecl, scope: &mut HashMap<String, TypeRef>, errors: &mut Vec<Diagnostic>) { match &f.body { FnBody::Expr(e) => self.walk_expr(e, scope, errors), FnBody::Block(b) => self.walk_block(b, scope, errors), FnBody::External => {} } } fn walk_block(&self, b: &Block, scope: &mut HashMap<String, TypeRef>, errors: &mut Vec<Diagnostic>) { // Сохраняем snapshot для bindings которые let'аются в этом блоке — // чтобы вернуть scope после блока (block-out shadowing semantics). let mut snapshot: Vec<(String, Option<TypeRef>)> = Vec::new(); for s in &b.stmts { if let Stmt::Let(d) = s { if let Some(name) = pattern_simple_name(&d.pattern) { snapshot.push((name.clone(), scope.get(&name).cloned())); } } } for s in &b.stmts { self.walk_stmt(s, scope, errors); } if let Some(t) = &b.trailing { self.walk_expr(t, scope, errors); } // Восстановим shadowed bindings (block-out). for (n, prev) in snapshot { match prev { Some(t) => { scope.insert(n, t); } None => { scope.remove(&n); } } } } fn walk_stmt(&self, s: &Stmt, scope: &mut HashMap<String, TypeRef>, errors: &mut Vec<Diagnostic>) { match s { Stmt::Expr(e) => self.walk_expr(e, scope, errors), Stmt::Let(d) => { self.walk_expr(&d.value, scope, errors); // №386 (Plan p386-bound-doors): explicit `ro x: T = …` / `mut x: T = …` // annotation is a WRITTEN TypeRef (`ro m: IndexMap[NoMethods, int] = …`). if let Some(ty) = &d.ty { self.check_typeref_bounds(ty, errors); } // Plan 53: refutable pattern в `let` — compile error. // Допустимы только irrefutable patterns (Ident, Wildcard, // Tuple, plain-Record). Refutable (Literal, Variant, Or, // Array, Record-к-sum-variant) ловим здесь — codegen и // interp ассамят irrefutable. self.check_let_pattern_irrefutable(&d.pattern, errors); // Регистрируем simple-Ident pattern с inferred типом. if let Some(name) = pattern_simple_name(&d.pattern) { let inferred = d.ty.clone() .or_else(|| Self::infer_arg_ty(&d.value, scope)); if let Some(t) = inferred { scope.insert(name, t); } } } // Plan 114.4 Ф.2: scope-local const — pass-through (no-op for now). Stmt::Const(_) => {} Stmt::Assign { target, value, .. } => { self.walk_expr(target, scope, errors); self.walk_expr(value, scope, errors); } Stmt::Return { value, .. } => { if let Some(v) = value { self.walk_expr(v, scope, errors); } } Stmt::Throw { value, .. } => self.walk_expr(value, scope, errors), Stmt::Break(_) | Stmt::Continue(_) => {} // D90 Plan 20 Ф.2: body парсится, walk'аем — bound-checker // получит call'ы внутри body. Body-constraint проверки // (no Fail, no suspend, no exit-control) добавляются в Ф.3. Stmt::Defer { body, .. } => { self.walk_expr(body, scope, errors); } // Plan 110 D188: walk init + body (scaffold). Stmt::ConsumeScope { init, body, .. } => { self.walk_expr(init, scope, errors); for s in &body.stmts { self.walk_stmt(s, scope, errors); } if let Some(t) = &body.trailing { self.walk_expr(t, scope, errors); } } // Plan 33.2 Ф.8: assert_static — walk expr. Stmt::AssertStatic { expr, .. } | Stmt::Assume { expr, .. } => self.walk_expr(expr, scope, errors), // Ф.4.1: apply — ghost statement, args walk'аем (для name resolution). Stmt::Apply { args, .. } => { for a in args { self.walk_expr(a, scope, errors); } } // Ф.4.2: calc — ghost, шаги walk'аем. Stmt::Calc { steps, .. } => { for step in steps { self.walk_expr(&step.expr, scope, errors); } } // Plan 33.9 Ф.2: reveal — ghost, name resolution в pipeline. Stmt::Reveal { .. } => {} // Plan 136: tuple destructuring assignment. Stmt::TupleAssign { lhs, rhs, .. } => { for e in lhs { self.walk_expr(e, scope, errors); } for e in rhs { self.walk_expr(e, scope, errors); } } } } fn walk_expr(&self, e: &Expr, scope: &mut HashMap<String, TypeRef>, errors: &mut Vec<Diagnostic>) { // Проверяем сам call перед рекурсией в args (порядок не важен). self.check_call_bounds(e, scope, errors); // Plan 221.1 №88 (i): structural receiver shape enforcement // (`Vec[int].flatten()` → E_RECV_SHAPE_MISMATCH). Own top-level hook // (not folded into `check_call_bounds`'s Member-branch above, which // `return`s early after `check_method_call_bounds` — bound-checking // and shape-checking are independent concerns). if let ExprKind::Call { func, .. } = &e.kind { if let ExprKind::Member { obj, name: method_name } = &func.kind { self.check_receiver_shape_match(obj, method_name, e.span, scope, errors); // №303 (221.1): receiver carrier-bound enforcement // (`Holder[Plain].show()` where `show` needs `T Display`) — // own hook, same independence rationale as the shape-match // hook right above (bound-checking and shape-checking are // separate concerns, both best-effort). self.check_receiver_carrier_bounds(obj, method_name, e.span, scope, errors); } } // Plan 46 (D102): argument binding diagnostics. self.check_call_argbind(e, scope, errors); // Plan 207 cmpxchg-lint волна B (D425 амендмент): compare_exchange(_weak) // failure-ordering hard-error (E_CAS_FAILURE_ORDER_INVALID). self.check_cas_ordering(e, scope, errors); // Plan 97.1 hardening: `box.method()` для protocol-typed var — // method обязан быть в protocol_specs[<Proto>]. self.check_protocol_method_call(e, scope, errors); // [nova_fn_len-local-shadow-fix] (cigreen-fix, CI std-green batch): // call-callee resolution must give a local/param binding PRIORITY // over free-fn resolution (D29-style shadow rule). Without this, // `ro len = value.byte_len(); ...; len()` (bare Ident callee that // shadows a non-function local) fell through undetected to // codegen's free-fn fallback (`free_fn_c_name`), silently emitting // a call to a phantom `nova_fn_len` C symbol — only failing much // later at LINK time ("undefined symbol nova_fn_len"), far from // the root cause. See std/testing/property.nv (the actual typo — // fixed separately) for the triggering pattern. self.check_call_callee_not_local_shadow(e, scope, errors); match &e.kind { ExprKind::Call { func, args, trailing } => { self.walk_expr(func, scope, errors); for a in args { self.walk_expr(a.expr(), scope, errors); } if let Some(t) = trailing { match t { crate::ast::Trailing::Block(b) => self.walk_block(b, scope, errors), crate::ast::Trailing::LegacyBlockWithParams(tb) => { self.walk_block(&tb.body, scope, errors) } crate::ast::Trailing::Fn(sb) => { // Trailing-fn body: Expr или Block. match &sb.body { FnBody::Expr(e) => self.walk_expr(e, scope, errors), FnBody::Block(b) => self.walk_block(b, scope, errors), FnBody::External => {} } } } } } ExprKind::TurboFish { base, type_args } => { self.walk_expr(base, scope, errors); // №386 (Plan p386-bound-doors): explicit turbofish type-args // naming a generic TYPE declaration — `IndexMap[NoMethods, // int].new()`, `Box[NoMethods]?`, any `Type[ConcreteArgs]` // construction. Free-function turbofish (`f[T](args)`) also // reaches this arm; `type_decls.get` simply misses (fn names // aren't in that map), so this is a no-op there — same // best-effort skip as `check_typeref_bounds`. if let Some(name) = Self::turbofish_base_name(base) { if let Some(td) = self.type_decls.get(name.as_str()) { if td.generics.len() == type_args.len() { let gs = self.current_fn_gs.borrow(); for (gp, concrete) in td.generics.iter().zip(type_args.iter()) { if gp.bounds.is_empty() || Self::is_passthrough_typevar(concrete, &gs) { continue; } for bound in &gp.bounds { self.check_satisfaction(concrete, bound, &gp.name, name.as_str(), e.span, errors); } } } } for t in type_args { self.check_typeref_bounds(t, errors); } } } ExprKind::Binary { left, right, op } => { // Plan 115 D214: ptr arithmetic banned (E_PTR_ARITHMETIC_BANNED). // V1: only comparison (Eq/Neq) и cast (handled separately) // разрешены на ptr. Все остальные binary ops — forbidden. let is_arith_or_rel = !matches!(op, BinOp::Eq | BinOp::Neq); if is_arith_or_rel { let l_is_ptr = expr_is_ptr_typed(left, scope); let r_is_ptr = expr_is_ptr_typed(right, scope); if l_is_ptr || r_is_ptr { errors.push(Diagnostic::new( format!( "[E_PTR_ARITHMETIC_BANNED] арифметика и сравнения \ порядка на `ptr` запрещены (Plan 115 D214 V1): \ опаковый pointer не поддерживает `{:?}`. \ Используйте `==` / `!=` для null-check'ов; для integer-\ арифметики сделайте `(p as u64) <op> ...`.", op ), e.span, )); } } // Plan 150 / D248: relational operators require an ORDERED operand // type. `bool` / `unit` are not ordered (bool ordering is // method-only via `@compare`, D183) — `flag < 5` / `true < false` // is the silent-coercion footgun. `==`/`!=` on bool/unit stay legal // (not relational, hence excluded here). Conservative: fires only // when an operand type is definitively known to be bool/unit // (permissive on unknown/generic — does not break inference). if matches!(op, BinOp::Lt | BinOp::Le | BinOp::Gt | BinOp::Ge) { let is_bool_unit = |x: &Expr| { Self::infer_arg_ty(x, scope) .map_or(false, |t| typeref_is_bool_or_unit(&t)) }; if is_bool_unit(left) || is_bool_unit(right) { errors.push(Diagnostic::new( "[E_RELATIONAL_OPERAND_NOT_ORDERED] `bool` / `unit` is not \ an ordered type — the relational operators `<` `<=` `>` \ `>=` require operands of an ordered category (int, float, \ str, char, or a type carrying `@compare`). Boolean ordering \ is method-only via `@compare` (D183 / Plan 150 D248), not \ the `<` operator. Use `==` / `!=` for boolean equality." .to_string(), e.span, )); } } // Владелец 2026-07-21 (D-амендмент, spec/decisions/02-types.md, // рядом с D55/str-блоками): string `+` is NOT part of the // language — `str` не специфицирован в спеке как имеющий // арифметические операторы; ранее это была несанкционированная // фича (codegen Plan 13 Ф.9.2 молча лоуэрил `BinOp::Add` на // `nova_str` в `Nova_str_method_concat`). Закрытие дыры: // hard error, ЕДИНСТВЕННЫЙ канон — интерполяция // (`"${a}${b}"`) или явный `@concat`/`StringBuilder.append` // (в цикле). Gate — конкретно `BinOp::Add` (Sub/Mul/Div/Mod // на str и так не имеют смысла/недостижимы — нет // `@minus`/`@times` и т.п.); permissive на unknown/generic-T // (та же конвенция, что и E_MIXED_WIDTH_ARITH ниже — не // ловит generic-параметр, резолвящийся в `str` только на // mono). `@concat` (str/transform.nv) остаётся явным методом // — НЕ ретрактирован, просто operator-sugar `+` над ним // больше не существует. if matches!(op, BinOp::Add) { let is_str = |x: &Expr| { Self::infer_arg_ty(x, scope).map_or(false, |t| typeref_is_str(&t)) }; if is_str(left) || is_str(right) { errors.push(Diagnostic::new( "[E_STR_CONCAT_PLUS] string `+` is not part of the \ language; use string interpolation \ (`\"${a}${b}\"`) instead — or, inside a loop, a \ `StringBuilder` + `.append` (repeated `+` is \ O(n²)). `@concat` remains available as an \ explicit method (`a.concat(b)`)." .to_string(), e.span, )); } } // Plan 172.1 D405: mixed-width integer arithmetic is a compile error // (E_MIXED_WIDTH_ARITH). Two SIZED (non-wide-default) integer operands // of DIFFERENT widths may not be combined: `u8 + u16` → CC-error; use // explicit `as` casts to a common width first. Permissive: fires only when // BOTH operand types are definitively known (infer_arg_ty non-None). // Does NOT fire for Shl/Shr (shift-amount asymmetry is conventional) or // for relational/equality operators (already handled elsewhere). let is_arith = matches!( op, BinOp::Add | BinOp::Sub | BinOp::Mul | BinOp::Div | BinOp::Mod | BinOp::BitAnd | BinOp::BitOr | BinOp::BitXor ); if is_arith { use ResolvedType as R; let sized_width = |x: &Expr| -> Option<u8> { let tr = Self::infer_arg_ty(x, scope)?; match ResolvedType::from_type_ref(&tr) { R::Scalar { width, wide_default: false, .. } => Some(width), _ => None, } }; if let (Some(lw), Some(rw)) = (sized_width(left), sized_width(right)) { if lw != rw { errors.push(Diagnostic::new( format!( "[E_MIXED_WIDTH_ARITH] mixed-width integer arithmetic \ is not allowed: left operand is {}-bit, right is \ {}-bit (D405). Use explicit `as` casts to a common \ width before the operation.", lw, rw ), e.span, )); } } } self.walk_expr(left, scope, errors); self.walk_expr(right, scope, errors); } ExprKind::Unary { op, operand } => { // Plan 234 (владелец 2026-07-27, гейт Ф.2): унарное семейство // `!`/`-`/`~` × неподдерживаемый тип операнда — честная ошибка // ЧЕКЕРА, не CC-FAIL с текстом clang. `~` определён ТОЛЬКО для // целочисленных (D46-амендмент §D); заодно закрыт весь список // неподдерживаемых комбинаций для `-`/`!` (владелец: "и прочие // комбинации — пройтись единообразно одной волной"). Conservative // (как E_MIXED_WIDTH_ARITH/E_RELATIONAL_OPERAND_NOT_ORDERED выше): // фиксируем ТОЛЬКО когда тип операнда достоверно известен (bool/ // str/f32/f64/int-family/пользовательский) — permissive ТОЛЬКО на // unknown/generic (custom-типы БЕЗ нужного @neg/@bitnot ловятся // ниже по конвейеру существующим codegen CC-FAIL). // // D46-AMEND 2026-08-02 (окно p-op-channel, владелец): `@not` // RETRACTED — `!` больше НЕ перегружаемый, единственный // допустимый операнд-тип строго `bool` (было: permissive на // custom-типах ради `@not()`-диспетча). `UnOp::Not` теперь // помечает bad для ЛЮБОГО достоверно известного не-`bool` типа, // включая пользовательские record/sum (раньше пропускались — // ушли бы на `@not`-dispatch, которого больше нет). if let Some(operand_ty) = Self::infer_arg_ty(operand, scope) { use ResolvedType as R; let rt = ResolvedType::from_type_ref(&operand_ty); let is_bool = matches!(rt, R::Bool); let is_str = typeref_is_str(&operand_ty); let is_float = matches!(rt, R::Float { .. }); let is_numeric = matches!(rt, R::Scalar { .. }) || is_float; let bad = match op { UnOp::Neg if is_bool || is_str => Some(( "-", "числовой (int/float-семейство) или пользовательский \ тип с `@neg()`", )), UnOp::Not if !is_bool => Some(( "!", "`bool` (D46-AMEND 2026-08-02: `@not` retracted — `!` \ больше не перегружаемый)", )), UnOp::BitNot if is_bool || is_float || is_str => Some(( "~", "целочисленный (`i8`..`i64`/`u8`..`u64`/`int`/`uint`) \ или пользовательский тип с `@bitnot()`", )), _ => None, }; if let Some((op_src, expected)) = bad { errors.push(Diagnostic::new( format!( "[E_UNARY_OPERAND_TYPE] operator `{}` requires {} — \ operand type here is not compatible (D46-амендмент \ 2026-07-27 / план 234: `~` — целочисленный-only, `!` — \ логический, `-` — числовой; type coercion между этими \ семьями не выполняется).", op_src, expected ), e.span, )); } } self.walk_expr(operand, scope, errors); } ExprKind::Try(inner) | ExprKind::Bang(inner) | ExprKind::RefArg(inner) => self.walk_expr(inner, scope, errors), ExprKind::Coalesce(a, b) => { self.walk_expr(a, scope, errors); self.walk_expr(b, scope, errors); } ExprKind::As(e, _) => self.walk_expr(e, scope, errors), ExprKind::Is(e, _) => self.walk_expr(e, scope, errors), ExprKind::Member { obj, .. } => self.walk_expr(obj, scope, errors), ExprKind::Index { obj, index } => { self.walk_expr(obj, scope, errors); self.walk_expr(index, scope, errors); } ExprKind::If { cond, then, else_ } => { self.walk_expr(cond, scope, errors); self.walk_block(then, scope, errors); if let Some(eb) = else_ { match eb { ElseBranch::Block(b) => self.walk_block(b, scope, errors), ElseBranch::If(e) => self.walk_expr(e, scope, errors), } } } ExprKind::IfLet { scrutinee, then, else_, .. } => { self.walk_expr(scrutinee, scope, errors); self.walk_block(then, scope, errors); if let Some(eb) = else_ { match eb { ElseBranch::Block(b) => self.walk_block(b, scope, errors), ElseBranch::If(e) => self.walk_expr(e, scope, errors), } } } ExprKind::Match { scrutinee, arms } => { self.walk_expr(scrutinee, scope, errors); for arm in arms { if let Some(g) = &arm.guard { self.walk_expr(g, scope, errors); } match &arm.body { MatchArmBody::Expr(e) => self.walk_expr(e, scope, errors), MatchArmBody::Block(b) => self.walk_block(b, scope, errors), } } } ExprKind::Block(b) => self.walk_block(b, scope, errors), ExprKind::ArrayLit(elems) => { for el in elems { match el { ArrayElem::Item(e) | ArrayElem::Spread(e) => self.walk_expr(e, scope, errors), } } } ExprKind::MapLit { elems, .. } => { let pairs = crate::ast::MapElem::cloned_pairs(&elems); for (k, v) in pairs.iter() { self.walk_expr(k, scope, errors); self.walk_expr(v, scope, errors); } } ExprKind::TupleLit(elems) => { for e in elems { self.walk_expr(e, scope, errors); } } ExprKind::RecordLit { type_name, fields, .. } => { for f in fields { if let Some(v) = &f.value { self.walk_expr(v, scope, errors); } } // №386 (Plan p386-bound-doors): a generic type's own bounds // instantiated purely by FIELD-LITERAL inference (`Box { k: // NoMethods{x:1} }`, no explicit `Box[NoMethods]` anywhere for // `check_typeref_bounds` to see — own call site, own doc). if let Some(name) = type_name { self.check_record_lit_decl_bounds(name, fields, e.span, scope, errors); } } ExprKind::TaggedTemplate { tag, args, .. } => { self.walk_expr(tag, scope, errors); for a in args { self.walk_expr(a, scope, errors); } } ExprKind::InterpolatedStr { parts } => { for p in parts { if let InterpStrPart::Expr { expr: e, spec: _ } = p { self.walk_expr(e, scope, errors); } } } ExprKind::Lambda { body, .. } => self.walk_expr(body, scope, errors), // Plan 19, C5: BoundCtx обходит тело closure-light / // closure-full для генерик-bound проверок. Полный // bidirectional inference — фаза C6; здесь — только walk. ExprKind::ClosureLight { body, .. } => match body { crate::ast::ClosureBody::Expr(e) => self.walk_expr(e, scope, errors), crate::ast::ClosureBody::Block(b) => self.walk_block(b, scope, errors), }, ExprKind::ClosureFull(sb) => match &sb.body { FnBody::Expr(e) => self.walk_expr(e, scope, errors), FnBody::Block(b) => self.walk_block(b, scope, errors), FnBody::External => {} }, ExprKind::Spawn(body) => self.walk_expr(body, scope, errors), ExprKind::Detach(body) | ExprKind::Blocking(body) => self.walk_block(body, scope, errors), ExprKind::Supervised { body, cancel, deadline, on_timeout } => { if let Some(c) = cancel { self.walk_expr(c, scope, errors); } if let Some(_dl) = deadline { self.walk_expr(&_dl.expr, scope, errors); } if let Some(oh) = on_timeout { self.walk_expr(oh, scope, errors); } self.walk_block(body, scope, errors); } ExprKind::Forbid { body, .. } => self.walk_block(body, scope, errors), ExprKind::Realtime { body, .. } => self.walk_block(body, scope, errors), ExprKind::ParallelFor { iter, body, .. } => { self.walk_expr(iter, scope, errors); self.walk_block(body, scope, errors); } ExprKind::For { iter, body, .. } => { self.walk_expr(iter, scope, errors); self.walk_block(body, scope, errors); } ExprKind::While { cond, body, .. } => { self.walk_expr(cond, scope, errors); self.walk_block(body, scope, errors); } ExprKind::WhileLet { scrutinee, body, .. } => { self.walk_expr(scrutinee, scope, errors); self.walk_block(body, scope, errors); } ExprKind::Loop { body, .. } => self.walk_block(body, scope, errors), ExprKind::Select { arms } => { for arm in arms { match &arm.op { SelectOp::Recv { chan, .. } => self.walk_expr(chan, scope, errors), SelectOp::Send { chan, value } => { self.walk_expr(chan, scope, errors); self.walk_expr(value, scope, errors); } SelectOp::Default => {} } if let Some(g) = &arm.guard { self.walk_expr(g, scope, errors); } self.walk_block(&arm.body, scope, errors); } } ExprKind::Range { start, end, .. } => { if let Some(s) = start { self.walk_expr(s, scope, errors); } if let Some(e) = end { self.walk_expr(e, scope, errors); } } ExprKind::Throw(e) => self.walk_expr(e, scope, errors), ExprKind::Interrupt(opt) => { if let Some(e) = opt { self.walk_expr(e, scope, errors); } } // [E_COALESCE_RETURN_FALLBACK]: checker-rejected in `f1_expr_inner` // before this pass; walked defensively. ExprKind::CoalesceReturnFallback(opt) => { if let Some(e) = opt { self.walk_expr(e, scope, errors); } } ExprKind::With { body, .. } => self.walk_block(body, scope, errors), // D.1.3: квантор — только в контрактах; обходим range и body. ExprKind::Forall { range, body, .. } | ExprKind::Exists { range, body, .. } => { self.walk_expr(range, scope, errors); self.walk_expr(body, scope, errors); } // Plan 97 Ф.4 (D142): protocol-литерал — structural-check // относительно объявленного протокола (instance-only). ExprKind::ProtocolLit { proto_name, methods } => { self.check_protocol_lit(proto_name, methods, e.span, errors); } // Литералы / ident'ы / handler-литералы — без рекурсии в bound-проверке. ExprKind::IntLit(_) | ExprKind::FloatLit(_) | ExprKind::BoolLit(_) | ExprKind::StrLit(_) | ExprKind::CharLit(_) | ExprKind::UnitLit | ExprKind::HexBlobLit(_) | ExprKind::NullPtrLit | ExprKind::Ident(_) | ExprKind::Path(_) | ExprKind::SelfAccess | ExprKind::HandlerLit { .. } => {} } } /// Plan 97 Ф.4 (D142): структурная проверка protocol-литерала. /// /// 1. Resolve `proto_name` в registered protocol через `protocol_specs`. /// Если не найден — error (unknown protocol). /// 2. Каждый impl-метод должен соответствовать **instance**-методу /// протокола (по имени + arity). Реализация **static**-метода /// (декларированного с `.method`) в protocol-литерале запрещена /// (static — `Type.method` D35, у литерала нет «своего типа»). /// 3. Каждый instance-метод протокола должен быть реализован — иначе /// «missing method» error. fn check_protocol_lit( &self, proto_name: &[String], methods: &[HandlerMethod], span: Span, errors: &mut Vec<Diagnostic>, ) { let name = match proto_name.last() { Some(n) => n.clone(), None => return, }; let Some(spec_methods) = self.protocol_specs.get(&name) else { // Unknown protocol — diagnostic с hint'ом про D142. // Permissive если effect (effect-литерал, не protocol-литерал). if !self.effect_decls.contains_key(&name) { errors.push(Diagnostic::new( format!( "unknown protocol `{}` in protocol-literal — must be a declared \ `type {} protocol {{ ... }}` (D142 / Plan 97 Ф.4). \ If you meant an effect-literal, use `effect {} {{ ... }}` instead.", name, name, name), span, )); } return; }; // Static-method-impl rejection (Ф.4.3). for spec_m in spec_methods.iter() { if spec_m.is_static { // Если literal реализует static-метод (по имени), diagnostic. if methods.iter().any(|im| im.name == spec_m.name) { errors.push(Diagnostic::new( format!( "static method `.{}` cannot be implemented in protocol-literal \ — static methods belong to a type (D35: `fn Type.{}(...)`), \ not to an instance. Declare a named `type Impl {{ ... }}` with \ `fn Impl.{}(...)` and pass an instance of `Impl` instead.", spec_m.name, spec_m.name, spec_m.name), span, )); } } } // Structural-match: каждый instance-метод протокола должен быть реализован. let mut missing: Vec<String> = Vec::new(); for spec_m in spec_methods.iter() { if spec_m.is_static { continue; } let found = methods.iter().any(|im| im.name == spec_m.name && im.params.len() == spec_m.params.len()); if !found { missing.push(format!( "{}({})", spec_m.name, spec_m.params.iter().map(|p| p.name.clone()).collect::<Vec<_>>().join(", "))); } } if !missing.is_empty() { errors.push(Diagnostic::new( format!( "protocol-literal `protocol {} {{ ... }}` is missing required instance methods: {}. \ The protocol contract declared `{}` requires every instance method to be \ implemented (D142 / Plan 97 Ф.4 structural conformance).", name, missing.join(", "), name), span, )); } // Extra-method warning: реализация unknown-имени. for im in methods { let in_proto = spec_methods.iter().any(|s| s.name == im.name); if !in_proto { errors.push(Diagnostic::new( format!( "protocol-literal implements method `{}` not declared in protocol `{}` \ (D142 / Plan 97 Ф.4). Method names must match the contract.", im.name, name), im.span, )); } } } /// Plan 97.1 hardening (D142): Nova-side enforcement для /// `obj.method(args)` где `obj` — переменная типа named protocol. /// Метод должен быть в `protocol_specs[<Proto>]`; иначе compile error /// (раньше эта ошибка ловилась только на C-side как /// `no member named 'X' in struct NovaVtable_<Proto>`). /// /// Закрывает silent miscompile риск для пользовательской опечатки /// `l.nonexistent()` на protocol-typed value. fn check_protocol_method_call( &self, e: &Expr, scope: &HashMap<String, TypeRef>, errors: &mut Vec<Diagnostic>, ) { let ExprKind::Call { func, .. } = &e.kind else { return; }; // Снять turbofish если есть. let func = match &func.kind { ExprKind::TurboFish { base, .. } => base.as_ref(), _ => func.as_ref(), }; let (obj, method_name, member_span) = match &func.kind { ExprKind::Member { obj, name } => (obj.as_ref(), name.clone(), func.span), _ => return, }; // Resolve obj-тип через scope (только для простых Ident'ов; deeper // resolution — это задача codegen-уровня inference). let obj_ty = match &obj.kind { ExprKind::Ident(n) => match scope.get(n) { Some(t) => t.clone(), None => return, }, _ => return, }; // Extract protocol-name (named type, не generic-bound here). let proto_name = match &obj_ty { TypeRef::Named { path, generics, .. } if generics.is_empty() && path.len() == 1 => { path[0].clone() } _ => return, }; // Skip non-protocol type bindings. let Some(spec_methods) = self.protocol_specs.get(&proto_name) else { return; }; // Method обязан быть в protocol-spec. let known: bool = spec_methods.iter().any(|m| m.name == method_name); if known { return; } // Compose listing of known methods для R5.3 hint. let known_methods: Vec<String> = spec_methods.iter().map(|m| m.name.clone()).collect(); let listing = if known_methods.is_empty() { "<no methods>".to_string() } else { known_methods.join(", ") }; errors.push(Diagnostic::new( format!( "unknown method `.{}()` on protocol-typed value (declared protocol `{}` \ has no such method). Declared methods: [{}].\n \ fix: rename call to one of the declared methods, or add `{}` to the protocol \ declaration (`type {} protocol {{ ... }}`).\n \ (Plan 97.1 hardening — D142 / [M-protocol-method-name-shadowing] enforcement.)", method_name, proto_name, listing, method_name, proto_name), member_span, )); } /// [nova_fn_len-local-shadow-fix] (cigreen-fix): a bare-`Ident` call /// callee (`name(...)`) must resolve to the in-scope LOCAL/parameter /// binding `name` first (D29-style shadow rule — a local always shadows /// a same-named free function). If that local has a DEFINITIVELY KNOWN, /// non-function type, calling it is a genuine type error — flag it here /// instead of silently falling through to codegen's free-fn fallback /// (which mangles to `nova_fn_<name>` and only fails much later at LINK /// time with a confusing "undefined symbol" error, far from the actual /// mistake). Conservative like the sibling checks in this walker /// (E_MIXED_WIDTH_ARITH above): fires only when the local's type is /// definitively known AND not itself a function type — permissive on /// unknown/generic bindings to avoid false positives. fn check_call_callee_not_local_shadow( &self, e: &Expr, scope: &HashMap<String, TypeRef>, errors: &mut Vec<Diagnostic>, ) { // A local/param is "callable" if it's a fn-type, a fn-POINTER // (`*fn`/`*unsafe fn` = `Pointer(Func)` / `Pointer(Uninit(Func))`), or // any of those under a ro/mut/uninit/ref wrapper — peel and re-check. // (Bug fix: the bare `TypeRef::Func` guard mis-fired on `*unsafe fn(...)` // locals, which ARE callable via `fp(...)` — repro // spec_tests/conformance/d216_uninit_rename_174_5.nv:32.) // // [D52-амендмент, ОКНО-5, M-newtype-over-fn-type-unsupported / // M-alias-of-fn-type-not-callable]: a NAMED type also counts as // callable when it's a name registered in `self.fn_type_names` — // (a) `type X fn(A) -> B` (D52 newtype над fn-типом) — call-through: // the ONLY sensible operation on a function-shaped newtype is // calling it (222.3 §5а «why fn-newtype форвардит вызов, а // int-newtype — нет»); or (b) `type X alias fn(A) -> B` (any alias // chain) — D52 alias-transparency requires `X` behave exactly like // the aliased fn-type, INCLUDING being callable (`[M-alias-of-fn- // type-not-callable]` fix). See `fn_type_names` doc (`BoundCtx` // field) for the pre-scan that builds this set. fn is_callable_local_ty( ty: &TypeRef, fn_type_names: &HashMap<String, TypeRef>, depth: u32, ) -> bool { if depth > 16 { return false; // cycle guard (mirrors wrap_kind_of's own depth guard) } match ty { TypeRef::Func { .. } => true, TypeRef::Pointer(inner, _) | TypeRef::Uninit(inner, _) | TypeRef::Readonly(inner, _) | TypeRef::Mut(inner, _) | TypeRef::Ref(inner, _) => is_callable_local_ty(inner, fn_type_names, depth + 1), TypeRef::Named { path, generics, .. } if generics.is_empty() => { path.last().map(|n| fn_type_names.contains_key(n)).unwrap_or(false) } _ => false, } } let ExprKind::Call { func, .. } = &e.kind else { return; }; let ExprKind::Ident(name) = &func.kind else { return; }; let Some(ty) = scope.get(name) else { return; }; if is_callable_local_ty(ty, &self.fn_type_names, 0) { return; // callable local (closure / fn-typed param / fn-pointer / newtype-over-fn / alias-of-fn). } errors.push(Diagnostic::new( format!( "[E_CALL_NOT_CALLABLE] `{name}` names a local variable of type `{}` here, \ not a function — `{name}(...)` cannot call it. A local/parameter binding \ always shadows a same-named free function; if you meant to call a free \ function named `{name}`, rename the local. If you just meant to use the \ value, drop the `()`.", typeref_display(ty), ), func.span, )); } /// №386 (Plan p386-bound-doors): is `ty` a BARE name currently in scope /// as an abstract type-parameter (`gs` — `current_fn_gs`, seeded either /// with the enclosing fn's generics+receiver-generics, or — while /// walking a type declaration's own fields — that type's own /// generics)? An in-scope typevar is NOT a "concrete" instantiation for /// the door-4/5 declaration-bound checks below — it is an UNRESOLVED /// parameter of the CURRENT declaration, still waiting for an external /// caller to supply a real type. Whether THAT external caller's choice /// satisfies the bound is checked at ITS OWN instantiation site (the /// other `check_typeref_bounds`/`check_record_lit_decl_bounds`/turbofish /// call sites already wired) — checking it AGAIN here, against an /// abstract name, would be either redundant (if bounded) or a FALSE /// POSITIVE against std's existing, DOCUMENTED under-constrained /// generics (verified regression, `nova check std/src`): /// `HashMap[K, V].new()` returns `HashMap[K, V]` without restating the /// type's own `K Hash`; `type HashMapIter[K, V] { map HashMap[K, V] }` /// doesn't restate it either; `type Set[T] { use map HashMap[T, ()] }` /// is EXPLICITLY documented (`set/core.nv` header comment, spec /// `Q-bounds`) as bound-free by design, deferring enforcement to /// `Set[T]`'s OWN use sites. This guard scopes the 4 new doors to /// GENUINELY concrete instantiations, leaving that documented /// under-constrained-generic gap exactly as open as it already was — /// not silently narrower, not silently wider. fn is_passthrough_typevar(ty: &TypeRef, gs: &GenericScope) -> bool { matches!(ty, TypeRef::Named { path, generics, .. } if generics.is_empty() && path.len() == 1 && gs.contains_key(&path[0])) } /// №386 (Plan p386-bound-doors): base-identifier of a turbofish /// (`Type[Args]` / `f[T]`) — last path segment of `Ident`/`Path`, `None` /// for any other base shape (already-resolved value expr, etc). fn turbofish_base_name(base: &Expr) -> Option<String> { match &base.kind { ExprKind::Ident(n) => Some(n.clone()), ExprKind::Path(p) => p.last().cloned(), _ => None, } } /// №386 (Plan p386-bound-doors, "4th door"): recursively walk a WRITTEN /// `TypeRef` and, for every `Named` node whose generics are non-empty /// AND whose name resolves to a locally-known generic `TypeDecl`, check /// the DECLARATION's own bounds (`type Box[K Equal + Hash] { … }`) /// against the concrete type-args actually written — delegating to the /// SAME `check_satisfaction` primitive doors 1-3 (free fn / method / /// method-own typevar, №383) already funnel into via /// `check_generic_bounds_for_call`. This is a FOURTH call site into that /// one satisfaction-check, not a parallel re-implementation. /// /// Recurses into `generics` FIRST (mirrors `TypeCheckCtx::walk_typeref`'s /// own order), so a nested generic (`Box[Box[NoMethods]]`) is checked at /// EVERY nesting level automatically — no separate nested-generic case /// needed. /// /// Best-effort, matching every other path in this checker: an arity /// mismatch against `td.generics.len()` is silently skipped (that is /// `E_TYPE_ARITY_MISMATCH`'s job, in `TypeCheckCtx::walk_typeref` — a /// separate pass this checker cannot call into, see `type_decls`'s field /// doc); a name absent from `type_decls` (unresolved / not a generic /// type) is skipped too. /// /// Call sites (the actual "instantiation surfaces" for a WRITTEN /// TypeRef): `let`/`ro`/`mut` annotations, fn param/return types, and /// record/named-tuple field declarations — wired at each in /// `check_module`/`walk_stmt`. A generic instantiated WITHOUT an /// explicit annotation (`Box { k: NoMethods{x:1} }`, K inferred from the /// field literal, no `TypeRef` anywhere in the source spells `Box[ /// NoMethods]`) is NOT reachable through this walker at all — that is /// `check_record_lit_decl_bounds`'s separate job (own call site, /// same `check_satisfaction` primitive). fn check_typeref_bounds(&self, tr: &TypeRef, errors: &mut Vec<Diagnostic>) { match tr { TypeRef::Named { path, generics, span } => { for g in generics { self.check_typeref_bounds(g, errors); } let Some(name) = path.last() else { return; }; if generics.is_empty() { return; } let Some(td) = self.type_decls.get(name.as_str()) else { return; }; if td.generics.len() != generics.len() { return; // arity mismatch — E_TYPE_ARITY_MISMATCH's job, not ours. } let gs = self.current_fn_gs.borrow(); for (gp, concrete) in td.generics.iter().zip(generics.iter()) { if gp.bounds.is_empty() || Self::is_passthrough_typevar(concrete, &gs) { continue; } for bound in &gp.bounds { self.check_satisfaction(concrete, bound, &gp.name, name, *span, errors); } } } TypeRef::Array(inner, _) | TypeRef::FixedArray(_, inner, _) => { self.check_typeref_bounds(inner, errors); } TypeRef::Tuple(items, _) => { for it in items { self.check_typeref_bounds(it, errors); } } TypeRef::Func { params, return_type, .. } => { for p in params { self.check_typeref_bounds(p, errors); } if let Some(rt) = return_type { self.check_typeref_bounds(rt, errors); } } TypeRef::Readonly(inner, _) | TypeRef::Pointer(inner, _) | TypeRef::Mut(inner, _) | TypeRef::Uninit(inner, _) | TypeRef::Ref(inner, _) => self.check_typeref_bounds(inner, errors), TypeRef::Protocol { .. } | TypeRef::Unit(_) => {} } } /// №386 (Plan p386-bound-doors): a record literal whose generic type-args /// are inferred PURELY from field-literal values (`Box { k: /// NoMethods{x:1} }` — no explicit `Box[NoMethods]` anywhere in the /// source for `check_typeref_bounds` to walk) is a SEPARATE /// instantiation surface from any written `TypeRef`; own call site into /// the same `check_satisfaction` primitive. Mirrors /// `TypeCheckCtx::f1_expr_inner`'s RecordLit `gen_args` inference (same /// all-or-nothing rule per param: an unresolvable field skips just THAT /// generic param, not the whole literal) — reimplemented here because /// `BoundCtx` is a wholly separate pass with no access to /// `TypeCheckCtx`'s internals (`resolved_types_buf` etc.). fn check_record_lit_decl_bounds( &self, type_name: &[String], fields: &[RecordLitField], span: Span, scope: &HashMap<String, TypeRef>, errors: &mut Vec<Diagnostic>, ) { let Some(last) = type_name.last() else { return; }; let Some(td) = self.type_decls.get(last.as_str()) else { return; }; if td.generics.iter().all(|g| g.bounds.is_empty()) { return; } let TypeDeclKind::Record(field_decls) = &td.kind else { return; }; for gp in &td.generics { if gp.bounds.is_empty() { continue; } // Concrete type-arg for this generic param = inferred type of the // field literal whose DECLARED type is exactly this bare param // (same match `TypeCheckCtx::f1_expr_inner`'s `gen_args` uses). let concrete = field_decls.iter().find_map(|fd| { if let TypeRef::Named { path, generics: fg, .. } = &fd.ty { if fg.is_empty() && path.join("_") == gp.name { return fields.iter() .find(|f| f.name == fd.name) .and_then(|f| f.value.as_ref()) .and_then(|v| Self::infer_arg_ty(v, scope)); } } None }); let Some(concrete) = concrete else { continue }; // not inferable — best-effort skip. if Self::is_passthrough_typevar(&concrete, &self.current_fn_gs.borrow()) { continue; // abstract param of the CURRENT decl — see doc on `is_passthrough_typevar`. } for bound in &gp.bounds { self.check_satisfaction(&concrete, bound, &gp.name, last, span, errors); } } } /// Plan 15 Ф.3: проверить bound'ы на конкретном call-site. /// /// Если callee — top-level fn с generics+bounds, и есть turbofish /// type_args (или возможна простая inference из args) — проверить /// что concrete-T удовлетворяет bound'у. fn check_call_bounds( &self, e: &Expr, scope: &HashMap<String, TypeRef>, errors: &mut Vec<Diagnostic>, ) { let ExprKind::Call { func, args, .. } = &e.kind else { return; }; // Распакуем turbofish, чтобы добраться до базового идентификатора. let (base, type_args): (&Expr, &[TypeRef]) = match &func.kind { ExprKind::TurboFish { base, type_args } => (base, type_args.as_slice()), _ => (func.as_ref(), &[][..]), }; // Plan 101.2 (D145 Ред. 5) / №383 (Plan p383-bounds): method-call // bound enforcement — `xs.desc()` где xs : []NoShow, desc объявлен // как `fn[T Showable_] []T @desc` (receiver-linked typevar), ИЛИ // `h.combine(v)` где `combine` объявлен как `fn Box @combine[S // Clone](other S)` (method-OWN typevar, №383 Корень А). Оба случая // ныне проходят через ОДИН и тот же `check_generic_bounds_for_call` // (см. его doc) — `check_method_call_bounds` лишь готовит // receiver-linked binding, когда он применим, и делегирует туда же, // куда и свободная функция ниже. `args` передаются дальше — раньше // метод их вообще не получал, поэтому method-own generics не // проверялись НИКОГДА (не «расхождение check/build», а полное // отсутствие механизма). if let ExprKind::Member { obj, name: method_name } = &base.kind { self.check_method_call_bounds(obj, method_name, args, e.span, scope, errors); return; } let fn_name = match &base.kind { ExprKind::Ident(n) => n.clone(), _ => return, }; // D84: fn_decls — Vec<&FnDecl>. Резолв overload по arity (то, что // bound-checker может определить без full type-inference). // Если несколько overloads подходят по arity — bound-checker не // делает разрешение (это работа codegen, у которого есть type-info). // Bound-проверка пропускается; codegen ловит ambiguity на своём // уровне. let Some(overloads) = self.sig.fn_decls.get(&fn_name) else { return; }; let arity_matches: Vec<&&FnDecl> = overloads.iter() .filter(|f| f.params.len() == args.len()) .collect(); let callee: &FnDecl = match arity_matches.as_slice() { [single] => *single, _ => return, // нет однозначной overload по arity — пропускаем }; self.check_generic_bounds_for_call( callee, &fn_name, type_args, args, None, e.span, scope, errors, ); } /// №383 (Plan p383-bounds, единый вход): проверка bound'ов generic /// type-param'ов на call-site — ОБЩАЯ для свободной функции /// (`check_call_bounds`) и метода (`check_method_call_bounds`). /// Владелец: «метод это та же функция» — до этого фикса это было /// технически ДВЕ реализации (свободная функция делала turbofish + /// arg-based inference по `args`; метод делал ТОЛЬКО receiver- /// substitution первого generic'а и `args` даже не получал), т.е. /// фактический `if is_method {...} else {...}`, просто разнесённый по /// разным функциям вместо явного if. Теперь обе формы проходят один и /// тот же bindings-цикл; единственная асимметрия — `recv_binding`, /// которым МОЖЕТ (но не обязан) воспользоваться вызывающий, когда у /// callee есть receiver-linked typevar (Plan 101 `fn[T Bound] []T /// @method`/`fn[T Bound] T @method`); метод-OWN generics (`fn Recv /// @method[S Bound](args)`, №383 Корень А) — как и generics свободной /// функции — связываются исключительно arg-based inference'ом ниже, /// receiver в этом не участвует вообще. fn check_generic_bounds_for_call( &self, callee: &FnDecl, callee_name: &str, type_args: &[TypeRef], args: &[CallArg], recv_binding: Option<(String, TypeRef)>, span: Span, scope: &HashMap<String, TypeRef>, errors: &mut Vec<Diagnostic>, ) { // Bounds присутствуют? if !callee.generics.iter().any(|g| !g.bounds.is_empty()) { return; } // Сматчим concrete-тип для каждого typevar'а. Источники (по // приоритету — первый выигрывает, дальнейшие не перезаписывают): // 1. receiver-linked substitution (только методы с Plan 101 // receiver-формой — `recv_binding`, если передан). // 2. turbofish — explicit type_args[i] для callee.generics[i]. // 3. simple inference: для каждого param с TypeRef::Named{path:[T]} // где T — generic-param, тип arg'а на той же позиции = concrete T // (работает одинаково для свободной функции И метода — тот же // цикл по `callee.params`/`args`, включая method-own generics // вроде `S` в `@combine[S Clone](other S)`). let mut bindings: HashMap<String, TypeRef> = HashMap::new(); if let Some((name, ty)) = recv_binding { bindings.insert(name, ty); } if !type_args.is_empty() { for (i, gp) in callee.generics.iter().enumerate() { if let Some(t) = type_args.get(i) { bindings.entry(gp.name.clone()).or_insert_with(|| t.clone()); } } } for (i, param) in callee.params.iter().enumerate() { let Some(call_arg) = args.get(i) else { continue; }; let arg_expr = call_arg.expr(); if let Some(t_name) = Self::param_generic_name(¶m.ty, &callee.generics) { if !bindings.contains_key(&t_name) { if let Some(arg_ty) = Self::infer_arg_ty(arg_expr, scope) { bindings.insert(t_name, arg_ty); } } } } // Для каждого bounded generic — проверить. // Plan 101.3: multi-bound `[T A + B]` — ALL bounds должны быть // satisfied (conjunction). check_satisfaction вызывается на // каждом bound отдельно — каждый missing-метод выдаст diagnostic. for gp in &callee.generics { if gp.bounds.is_empty() { continue; } let Some(concrete) = bindings.get(&gp.name) else { // Inference не удалась — пропускаем (best-effort). // Strict-mode мог бы требовать explicit turbofish. continue; }; for bound in &gp.bounds { self.check_satisfaction( concrete, bound, &gp.name, callee_name, span, errors, ); } } } /// Plan 101.2 (D145 Ред. 5): bound enforcement для method-call /// `obj.method(args)` где method объявлен с receiver-generic prefix /// `fn[T Bound] []T @method` (или `fn[T Bound] T @method`). Inferим /// concrete T из obj-type, для каждого bound checking satisfaction. /// /// **Surface**: только `fn[T] []T @method` (array-receiver) и /// `fn[T] T @method` (bare-T receiver) — Plan 101.1 формы. /// Tuple/Func/Map receivers — followup (V2 если нужно). /// /// **Best-effort**: если obj-type не resolvable или method /// неоднозначен по arity — skip (silent, как и check_call_bounds /// для free-fn'ов; codegen/runtime поймает на своём уровне). fn check_method_call_bounds( &self, obj: &Expr, method_name: &str, args: &[CallArg], span: Span, scope: &HashMap<String, TypeRef>, errors: &mut Vec<Diagnostic>, ) { // Inferим obj-type. let Some(obj_ty) = Self::infer_arg_ty(obj, scope) else { return; }; // Peel ro/mut/uninit wrappers — mirrors `check_receiver_shape_match`'s // own peel loop; the DECLARED receiver shape never carries these. let mut peeled = &obj_ty; loop { match peeled { TypeRef::Readonly(i, _) | TypeRef::Mut(i, _) | TypeRef::Uninit(i, _) => peeled = i, _ => break, } } // Определяем receiver-key под `method_table`. Plan 101 surface // (receiver-linked typevar): // []T → key = "[]T", T = element type. // T → key = "T", T = obj-type whole (bare-receiver, single- // letter name — тот же эвристический критерий, что и // раньше: single-char name отличает typevar-receiver от // обычного номинального типа). // №383 (Plan p383-bounds): ЛЮБОЙ другой номинальный receiver // (`Box13`, `HashMap`, …) тоже участвует — ключ под method_table // это просто имя типа (тот же lookup, что `check_receiver_shape_match` // уже использует для номинальных receiver'ов). Раньше эта ветка была // `_ => return` — метод с конкретным non-generic receiver-типом // вообще не попадал в этот checker, из-за чего method-OWN generics // (`fn Box13 @combine[S Clone](other S)`) не проверялись никогда — // не только receiver-substitution для них не работала (это ожидаемо, // receiver сам не typevar), но и arg-based inference не запускалась // вовсе, потому что до неё дело не доходило. // №383: an Array/slice receiver (`v: []u8`) is NOT looked up under a // single key — D239 canonicalizes `[]T ≡ Vec[T]` to method_table key // "Vec" (the SAME normalization `check_instance_overload` already // applies at ~13880/~20872), which is where carrier-generic methods // like `Vec[T] mut @append[S AsSlice[T]]` actually live; a // concrete-element FACADE method (`fn []u8 @method`) lives under the // element-spelled key `"[]u8"` (~13980); the Plan 101 prefix-generic // SENTINEL `fn[T Bound] []T @method` lives under the literal string // `"[]T"` — three genuinely distinct method_table keys for one // syntactic receiver shape. The OLD code tried ONLY the sentinel // "[]T" key — so `v.append(NoSlice{..})` (append lives under "Vec") // never even reached a lookup hit, meaning its method-own generic // `S AsSlice[T]` was unreachable regardless of the arg-inference fix // below (№381/Корень А). Try candidates in the same priority the // other checker paths use: nominal "Vec" first, then the facade // spelling, then the sentinel — first HIT (method name present under // that key) wins. let candidate_keys: Vec<String> = match peeled { TypeRef::Array(inner, _) | TypeRef::FixedArray(_, inner, _) => { vec!["Vec".to_string(), format!("[]{}", render_type_ref(inner)), "[]T".to_string()] } TypeRef::Named { path, .. } if path.last().map(|s| s.len()).unwrap_or(0) == 1 => { vec!["T".to_string()] } TypeRef::Named { path, .. } => match path.last() { Some(n) => vec![n.clone()], None => return, }, _ => return, // Non-array, non-named — skip. }; let mut hit: Option<(&str, &FnDecl)> = None; for key in &candidate_keys { let Some(methods_for_recv) = self.sig.method_table.get(key.as_str()) else { continue; }; let Some(overloads) = methods_for_recv.get(method_name) else { continue; }; // Take single match (skip if multiple overloads — codegen разрулит). match overloads.as_slice() { [single] => { hit = Some((key.as_str(), single)); break; } _ => return, // ambiguous under the key that DOES have this name — bail (best-effort) } } let Some((recv_key, callee)) = hit else { return; }; if !callee.generics.iter().any(|g| !g.bounds.is_empty()) { return; } // Receiver-linked binding — ТОЛЬКО для Plan 101 prefix-generic форм // (recv_key == "[]T"/"T" sentinels): parser кладёт receiver-typevar // ПЕРВЫМ в `callee.generics` (parser/mod.rs — `fn[T]`-prefix generics // prepended перед receiver/method generics). Для "Vec"/facade/ // номинальных ключей receiver — НЕ typevar (это конкретный/carrier // тип), никакого receiver-binding нет; все generics callee — // method-own, они связываются arg-based inference'ом внутри // `check_generic_bounds_for_call` (тот же путь, что и у свободной // функции — №383, единый вход) — именно так `@append[S // AsSlice[T]]`'s `S` и `@combine[S Clone]`'s `S` теперь связываются. let recv_binding: Option<(String, TypeRef)> = match recv_key { "[]T" => match peeled { TypeRef::Array(inner, _) => { callee.generics.first().map(|g| (g.name.clone(), (**inner).clone())) } _ => None, }, "T" => callee.generics.first().map(|g| (g.name.clone(), peeled.clone())), _ => None, }; self.check_generic_bounds_for_call( callee, method_name, &[], args, recv_binding, span, scope, errors, ); } /// Plan 221.1 №88 (i) [M-structured-receiver-generic-not-enforced]: /// call-site enforcement of a STRUCTURAL receiver shape (`fn Vec[Vec[T]] /// @flatten`, `fn[T] [][]T @m`, (ii) user carriers `fn OneBox[Vec[T]] /// @first`) — `Vec[int].flatten()` / `OneBox[int].first()` (receiver /// does NOT structurally unify with the declared shape) must be an /// honest checker error, not silent garbage. Verified probe (pre-fix): /// `nova check`/`nova build` both PASS for `OneBox[int].first()` where /// `@first` is `fn OneBox[Vec[T]] @first() -> T => @v[0]`; the built /// binary's `print(b.first())` prints NOTHING at runtime (no honest /// signal anywhere in the pipeline). /// /// Reuses the SAME depth-agnostic structural unifier `build_recv_subst` /// already uses to bind a receiver typevar at mono/narrowing time /// (`const_fn_trampoline::unify_type`, Plan 153.5) — NOT the shallow /// `unify_coerce_receiver` (Plan 214.1, `#coerce`-only, R4 "без рекурсии /// вглубь": binds a whole top-level slot, never recurses into it — wrong /// tool here, a mismatch two levels deep must be caught, e.g. /// `Vec[Vec[T]]` vs a call-site `Vec[int]`). /// /// **Best-effort / single-candidate-only** (mirrors `check_method_call_bounds` /// right above): obj-type not resolvable, method not found, OR ≥2 /// overloads for this (receiver-base, method-name) pair → skip silently /// (codegen/other paths own disambiguation there — this gate never /// second-guesses an overload RESOLUTION, only a single unambiguous /// candidate's shape). Only methods with a STRUCTURED receiver /// (`receiver.receiver_ty.is_some()`) are in scope — a plain nominal /// receiver (`fn Widget @method`) needs no shape check: its dispatch is /// already exact by nominal type name via `method_table`. fn check_receiver_shape_match( &self, obj: &Expr, method_name: &str, span: Span, scope: &HashMap<String, TypeRef>, errors: &mut Vec<Diagnostic>, ) { let Some(obj_ty) = Self::infer_arg_ty(obj, scope) else { return; }; // Peel ro/mut/uninit wrappers — mirrors `build_recv_subst`'s own peel // loop; the DECLARED receiver shape never carries these. let mut peeled = &obj_ty; loop { match peeled { TypeRef::Readonly(i, _) | TypeRef::Mut(i, _) | TypeRef::Uninit(i, _) => peeled = i, _ => break, } } let TypeRef::Named { path, .. } = peeled else { return; }; let Some(base) = path.last() else { return; }; let Some(methods_for_recv) = self.sig.method_table.get(base) else { return; }; let Some(overloads) = methods_for_recv.get(method_name) else { return; }; let callee: &FnDecl = match overloads.as_slice() { [single] => single, _ => return, // ambiguous by arity — codegen/other paths resolve, not this gate }; let Some(recv) = &callee.receiver else { return; }; if !matches!(recv.kind, ReceiverKind::Instance) { return; } // No structured shape (`receiver_ty` — Plan 153.5) — plain nominal // receiver, dispatch already exact by `method_table` base key alone. let Some(decl_ty) = &recv.receiver_ty else { return; }; // Bindable typevar names: carrier-declared (`recv.generics`, covers // BOTH flat `Vec[T]` and (ii) nested-harvested `Vec[Vec[T]]`/ // user-carrier slots) UNION `fn[T]`-prefix / method-level // (`callee.generics`). A prefix-form receiver (`fn[T] []T @m`) // carries its typevar ONLY in `callee.generics`, NOT `recv.generics` // (parser/mod.rs — prefix generics prepended into `fn_generics`, never // into the Receiver's own `.generics`); omitting this union would // treat `T` as a FIXED CONCRETE name and false-positive on EVERY // `fn[T]`-prefix method (vec.nv's 7 methods) — a verified regression // class, not hypothetical. let mut generic_names: HashSet<String> = HashSet::new(); for g in &recv.generics { if let TypeRef::Named { path, generics, .. } = g { if path.len() == 1 && generics.is_empty() { generic_names.insert(path[0].clone()); } } } for g in &callee.generics { generic_names.insert(g.name.clone()); } // Plan 221.1 №88 (iii): canonicalize BOTH operands (D239, `[]T` ≡ // `Vec[T]`) before unifying — `decl_ty` is already canonical (the // parser canonicalizes every carrier slot), but the call-site's // OWN inferred type may not be: `[]u8` written as a nested generic // ARG in an ordinary type annotation (`ro v Vec[[]u8] = …`) goes // through the GENERAL type-ref parser, untouched by the carrier-slot // canonicalization, and keeps the raw `Array(Named u8)` shape. Without // re-canonicalizing here too, that cosmetic spelling difference alone // would false-positive this diagnostic even though `Vec[[]u8]` IS // `Vec[Vec[u8]]` (verified regression while implementing this check). let decl_ty_canon = crate::const_fn_trampoline::canonicalize_array_to_vec(decl_ty); let peeled_canon = crate::const_fn_trampoline::canonicalize_array_to_vec(peeled); let mut subst: HashMap<String, TypeRef> = HashMap::new(); if crate::const_fn_trampoline::unify_type(&decl_ty_canon, &peeled_canon, &generic_names, &mut subst).is_err() { errors.push(Diagnostic::new( format!( "[E_RECV_SHAPE_MISMATCH] method `{method}` requires receiver shape \ `{expected}`, got `{actual}` — the call-site receiver does not \ structurally unify with the method's declared receiver (Plan 153.5 \ structural receiver, D239 alias). Restructure the call-site value to \ match the declared shape, or double-check this is the method you meant.", method = method_name, expected = typeref_display(&decl_ty_canon), actual = typeref_display(&peeled_canon), ), span, )); } } /// №303 (221.1) [M-receiver-carrier-bound-not-enforced]: call-site /// enforcement of a RECEIVER CARRIER BOUND — `fn Holder[T Display] /// @show()`, `fn Vec[T Compare] @is_sorted()`, `fn Vec[T Clone] /// @clone()` — the bound written in the receiver's carrier brackets /// (`Receiver.carrier_bounds`, `ast/mod.rs`). Verified pre-fix probe: /// `Holder[Plain] { .. }.show()` where `Plain` does NOT implement /// `Display` type-checks CLEAN — the bound is parsed /// (`parser/mod.rs:3336`) and even fed into the METHOD BODY's own /// generic-scope (`fn_generic_scope`/the receiver-generics loop in /// `check_fn_decl`, ~5775-5808 / ~22908-22946 — so code INSIDE `@show` /// may assume `T: Display`), but nothing at the CALL SITE ever checked /// that the concrete receiver instantiation actually satisfies it — /// "Stored for future enforcement; currently informational only" /// (the field's own doc comment) was accurate until this fix. Cost: /// every conditional-method promise in std (`Vec[T Compare]`, /// `Vec[T Clone]`, `HashMap[K Clone, V Clone]`, `Set[T Clone]`, …) was /// decorative — a C++-templates model (bound only bites if the BODY /// happens to call the protocol method), not a boundary check. /// /// **Reuses, does not reinvent:** the SAME structural unifier /// (`const_fn_trampoline::unify_type` + `canonicalize_array_to_vec`) /// `check_receiver_shape_match` right above already uses to bind the /// receiver's carrier typevars (`decl_ty` `Holder[T]` vs the call-site's /// concrete `Holder[Plain]` → `subst = {T: Plain}`), and the SAME /// `check_satisfaction` engine `check_call_bounds`/ /// `check_method_call_bounds` already use for free-fn / prefix-generic /// bound enforcement (Plan 15 Ф.3 / Plan 101.2) — one satisfaction /// predicate, reused a third time, not a new diagnostic vocabulary. /// /// **Multi-overload candidate selection** (mirrors registry №295's /// `infer_call_ret_c` B08 fix in emit_c.rs, AS AN ALGORITHM — no code /// shared, different layer): when ≥2 overloads share (receiver-base, /// method-name) — e.g. two carrier-bounded siblings differing only in /// their bound — every candidate whose declared receiver shape /// structurally unifies with the call-site receiver is tried; the call /// is legitimate the moment ONE such candidate's bound is satisfied. /// An error fires only when EVERY shape-matching candidate's bound is /// unsatisfied (the first violation's diagnostics are reported — same /// permissive "single obvious cause" posture as the sibling checks in /// this file). /// /// **Best-effort**, same posture as `check_receiver_shape_match`/ /// `check_method_call_bounds`: unresolvable obj-type, non-`Named` /// receiver, or no candidate with a structured `receiver_ty` → skip /// silently (this gate only ever ADDS a bound diagnostic on top of an /// otherwise-resolved call; it never second-guesses existence/shape, /// which the other checks above already own). fn check_receiver_carrier_bounds( &self, obj: &Expr, method_name: &str, span: Span, scope: &HashMap<String, TypeRef>, errors: &mut Vec<Diagnostic>, ) { let Some(obj_ty) = Self::infer_arg_ty(obj, scope) else { return; }; // [M-receiver-carrier-bound-self-passthrough] (found verifying this // very fix against `repro_param.nv`, Plan 138.2 self-in-param): a // receiver's declared type can carry a NESTED, unresolved `Self` // typevar (`g FiltItP[Self, U]` — `Self` here is the ENCLOSING // method's OWN receiver, `MapItP[I,T,U]`), not just a top-level bare // `self`/`@` expression. This walker (unlike `TypeCheckCtx`, ~line // 7594) never seeds a `"@"` scope entry, so `scope.get("@")` is // always empty here — `current_recv_ty` (set per-fn in // `check_module`) is the substitute source. Left unresolved, // `check_satisfaction` below would look up a type literally named // `"Self"` in `method_table` — never found — and report a FALSE // bound violation (verified: this exact corpus file regressed with // the raw obj_ty, disappeared once `Self` is substituted here). // Mirrors the `subst.insert("Self", ...)` convention used // throughout this file for the same purpose. let obj_ty = match &*self.current_recv_ty.borrow() { Some(self_ty) => { let mut m: HashMap<String, TypeRef> = HashMap::new(); m.insert("Self".to_string(), self_ty.clone()); crate::const_fn_trampoline::subst_type_ref_pub(&obj_ty, &m) } None => obj_ty, }; let mut peeled = &obj_ty; loop { match peeled { TypeRef::Readonly(i, _) | TypeRef::Mut(i, _) | TypeRef::Uninit(i, _) => peeled = i, _ => break, } } let TypeRef::Named { path, .. } = peeled else { return; }; let Some(base) = path.last() else { return; }; let Some(methods_for_recv) = self.sig.method_table.get(base) else { return; }; let Some(overloads) = methods_for_recv.get(method_name) else { return; }; // Only Instance-receiver overloads that actually carry a carrier // bound are in scope — a plain nominal method (no bound) has // nothing for this check to enforce. let candidates: Vec<&FnDecl> = overloads.iter() .map(|f| *f) .filter(|f| f.receiver.as_ref().map_or(false, |r| { matches!(r.kind, ReceiverKind::Instance) && !r.carrier_bounds.is_empty() })) .collect(); if candidates.is_empty() { return; } let peeled_canon = crate::const_fn_trampoline::canonicalize_array_to_vec(peeled); let mut any_shape_matched = false; let mut satisfied_any = false; let mut first_violation: Vec<Diagnostic> = Vec::new(); for f in &candidates { let recv = f.receiver.as_ref().expect("filtered above"); // No structured shape (Plan 153.5) — cannot bind carrier // typevars via structural unify; skip this candidate (best- // effort, mirrors `check_receiver_shape_match`'s own bail). let Some(decl_ty) = &recv.receiver_ty else { continue; }; let mut generic_names: HashSet<String> = HashSet::new(); for g in &recv.generics { if let TypeRef::Named { path, generics, .. } = g { if path.len() == 1 && generics.is_empty() { generic_names.insert(path[0].clone()); } } } for g in &f.generics { generic_names.insert(g.name.clone()); } let decl_ty_canon = crate::const_fn_trampoline::canonicalize_array_to_vec(decl_ty); let mut subst: HashMap<String, TypeRef> = HashMap::new(); if crate::const_fn_trampoline::unify_type(&decl_ty_canon, &peeled_canon, &generic_names, &mut subst).is_err() { // Shape doesn't match THIS candidate — not the overload this // call site targets (`check_receiver_shape_match` owns // reporting a shape mismatch when there is only one // candidate at all). continue; } any_shape_matched = true; let mut cand_errors: Vec<Diagnostic> = Vec::new(); for cb in &recv.carrier_bounds { let Some(concrete) = subst.get(&cb.name) else { continue; }; for bound in &cb.bounds { self.check_satisfaction(concrete, bound, &cb.name, method_name, span, &mut cand_errors); } } if cand_errors.is_empty() { satisfied_any = true; break; } else if first_violation.is_empty() { first_violation = cand_errors; } } if any_shape_matched && !satisfied_any { errors.extend(first_violation); } } /// Plan 207 cmpxchg-lint волна B (D425 амендмент): `compare_exchange`/ /// `compare_exchange_weak` call-site validation on `Atomic*` receivers with a /// LITERAL (compile-time-known) `failure` `MemOrdering` argument. Non-literal /// (runtime variable) orderings are not diagnosable at compile time — skipped /// (per spec: only literal args are checked). /// /// Hard error only (`E_CAS_FAILURE_ORDER_INVALID`): `failure ∈ {Release, AcqRel}` /// is semantically invalid — the failure path of a CAS is a pure load (the /// compared value did NOT change), so it carries no release semantics (C11/C++11 /// treated this as UB/forbidden). /// /// The companion **warning** (`W_CAS_FAILURE_STRONGER` — `strength(failure) > /// strength(success)`; valid since C++17 but almost always an intent bug) lives /// in `lints.rs` (`lint_cas_failure_stronger`), NOT here: this checker's `errors` /// sink is hard-errors-only (fatal), mirroring the existing `errors`/`LintWarning` /// split used by `W_PRELUDE_SHADOW` (silent classification here, structured /// warning emitted separately by `lints::lint_prelude_shadow` — see the Plan /// 62.F.bis Ф.2 comment above `check_module`). /// /// Receiver-type gate: `Atomic*` **prefix** match (not an enumerated type-name /// list — new sized/family variants are recognized for free). Best-effort: if the /// receiver type doesn't resolve (`infer_arg_ty` is a lightweight, non-full-inference /// helper — see its `Atomic*.new(...)` ctor arm above), the check is silently /// skipped, same posture as `check_method_call_bounds`. fn check_cas_ordering( &self, e: &Expr, scope: &HashMap<String, TypeRef>, errors: &mut Vec<Diagnostic>, ) { let ExprKind::Call { func, args, .. } = &e.kind else { return; }; let ExprKind::Member { obj, name: method_name } = &func.kind else { return; }; if method_name != "compare_exchange" && method_name != "compare_exchange_weak" { return; } // `expected`+`desired` обязательны — что-то с меньшей arity не является // компилируемым CAS-вызовом вообще (другие проверки поймают отдельно). if args.len() < 2 { return; } let Some(recv_ty) = Self::infer_arg_ty(obj, scope) else { return; }; let TypeRef::Named { path, .. } = &recv_ty else { return; }; let Some(tname) = path.last() else { return; }; if !tname.starts_with("Atomic") { return; } // Plan 207 cmpxchg-rename (влито в main ПОСЛЕ первой версии этого чекера): // `compare_exchange`/`_weak` — ОДНА сигнатура с default-параметрами // (`success MemOrdering = MemOrdering.SeqCst, failure MemOrdering = // MemOrdering.SeqCst`), больше НЕТ отдельных 2-арг/4-арг overload'ов. Опущенный // `failure` (arity < 4 позиционно, или именованный `failure:` вообще не указан) // — известный литерал `SeqCst` (сам по себе всегда легален: SeqCst не входит в // {Release, AcqRel}, поэтому ветка ошибки ниже никогда не сработает для // default-значения — `failure_arg` гарантированно `Some` внутри неё). let failure_arg = Self::cas_call_arg(args, "failure", 3); let failure: &str = match failure_arg { Some(expr) => match mem_ordering_variant(expr) { Some(v) => v, None => return, // runtime-переменная — не диагностируем }, None => "SeqCst", // arg опущен — default-параметр }; if matches!(failure, "Release" | "AcqRel") { let span = failure_arg.map(|a| a.span).unwrap_or(e.span); errors.push( Diagnostic::new( format!( "[{}] `MemOrdering.{}` запрещён как failure-ordering для `{}`; \ failure-путь compare_exchange — чистый load (значение НЕ \ изменено), Release/AcqRel не имеют release-семантики на load. \ Разрешены: Relaxed, Acquire, SeqCst.", E_CAS_FAILURE_ORDER_INVALID, failure, method_name ), span, ) .with_suggestion(crate::diag::Suggestion { message: "замените на Acquire (или SeqCst для simplicity)".to_string(), span, replacement: "MemOrdering.Acquire".to_string(), applicability: crate::diag::Applicability::MaybeIncorrect, }) ); } } /// Plan 207 cmpxchg-lint: достать call-arg для именованного параметра /// `param_name` на позиции `pos`. Именованный аргумент (`CallArg::Named`) с /// совпадающим именем побеждает независимо от позиции; иначе — позиционный /// arg на `pos` (Nova не допускает позиционный аргумент ПОСЛЕ именованного, /// так что чистая позиционная последовательность всегда занимает leading- /// префикс — `args.get(pos)` безопасен, если сам не `Named`). fn cas_call_arg<'x>(args: &'x [CallArg], param_name: &str, pos: usize) -> Option<&'x Expr> { for a in args { if let CallArg::Named { name, value } = a { if name == param_name { return Some(value); } } } match args.get(pos) { Some(CallArg::Named { .. }) | None => None, Some(a) => Some(a.expr()), } } /// Plan 46 (D102): проверить argument binding на call-site. /// Резолвит callee (free fn / static-method по Path), сопоставляет /// позиционные + именованные аргументы с параметрами через /// `argbind::bind_call_args`, эмитит diagnostics. /// /// Резолв best-effort: если callee неоднозначен (overload по arity) /// или не резолвится (instance-method через Member — нужен тип obj) — /// проверка пропускается (codegen поймает на своём уровне). fn check_call_argbind( &self, e: &Expr, scope: &HashMap<String, TypeRef>, errors: &mut Vec<Diagnostic>, ) { let ExprKind::Call { func, args, trailing } = &e.kind else { return; }; // Распакуем turbofish до базового func-expr. let base: &Expr = match &func.kind { ExprKind::TurboFish { base, .. } => base, _ => func.as_ref(), }; // Резолвим callee → список параметров. // Plan 196.3 (D102/D372-позиционка, one-window fold): резолв теперь // возвращает ТАКЖЕ имя callee (не только params) — `check_keyword_only` // применяет единое, структурное D372-amend2-исключение (canonical // single-default `cap`-ctor позиционно легален) в ОДНОМ месте, вне // зависимости от того, каким из трёх путей ниже резолвился callee. // До этого исключения не было вообще: generic-static (`Vec[T].new(n)` // и т.п.) молча ИЗБЕГАЛ диагностики (Member{obj: TurboFish{..}} не // резолвится через `resolve_instance_method`, obj — не value-типа, // Попытка 1 проваливается, Попытка 2 — arity-neutral name-only — // почти всегда ambiguous среди `new`-методов разных типов → `None` // → ранний `return` до вызова `check_keyword_only`), а non-generic // static (`StringBuilder.new(128)` — `Path`, 2 сегмента, однозначный // `method_table`-lookup) резолвился и ловил D102 по факту случайной // разницы AST-формы, а не по спеке. let (callee_params, callee_name): (&[Param], &str) = match &base.kind { ExprKind::Ident(name) => { // Plan 153 fix (regression from vec `resize_with`/`fill_with`, // 2026-06-14): a LOCAL binding named `name` — in particular a // closure parameter like `f fn() -> T` — SHADOWS a module-level // free function of the same name. Without this guard, `f()` // inside such a method resolved to a free `fn f(x int)` from the // *entry* module (e.g. a `contracts` test that happens to define // `fn f`), and arg-checking the closure call against the free // fn's params raised a spurious `обязательный параметр не передан`. // The closure call is validated via the var's fn-type / codegen, // so skip the free-fn arg-check entirely when shadowed. if scope.contains_key(name) { return; } let Some(overloads) = self.sig.fn_decls.get(name) else { return; }; match overloads.as_slice() { [single] => (&single.params, name.as_str()), _ => return, // overload — пропускаем (D102: нет overload, // но bootstrap fn_decls может иметь несколько). } } ExprKind::Path(parts) if parts.len() == 2 => { // `Type.method` — static-method резолв. let Some(methods) = self.sig.method_table.get(&parts[0]) else { return; }; let Some(overloads) = methods.get(&parts[1]) else { return; }; match overloads.as_slice() { [single] => (&single.params, parts[1].as_str()), _ => return, } } // Plan 46 Ф.3 + Plan 50 follow-up: instance-method `obj.method(...)`. // Первая попытка — receiver-type inference (best-effort через // `infer_arg_ty`): если тип `obj` известен (Ident в scope, // record-литерал, литерал-примитив) — точный резолв // `method_table[type][method]`. Закрывает gap при collision // имён методов: `Box.scaled` vs `Cube.scaled` с дефолтами // больше не пропускает keyword-only диагностику. // Fallback — name-only резолв (как было в Plan 46): уникальное // имя метода через все типы. Для остальных случаев codegen // резолвит через type-info. ExprKind::Member { obj, name: method_name } => { let resolved = self.resolve_instance_method(obj, method_name, scope, args.len()); match resolved { Some(f) => (&f.params, method_name.as_str()), None => return, } } _ => return, }; // Plan 46 Ф.3: trailing-форма (D43) связывает ПОСЛЕДНРЙ // функциональный параметр. Bind'аем против params без него. // Также: если named-arg назван как trailing-bound param — это // double-bind (ловится ниже отдельно). let trailing_present = trailing.is_some(); let effective_params: &[Param] = if trailing_present && !callee_params.is_empty() { // Проверка: named-arg для trailing-bound параметра — error. let last = &callee_params[callee_params.len() - 1]; for a in args.iter() { if a.arg_name() == Some(last.name.as_str()) { errors.push(Diagnostic::new( format!( "параметр `{}` связан и trailing-формой, и именованным \ аргументом (D102)", last.name ), a.expr().span, )); return; } } &callee_params[..callee_params.len() - 1] } else { callee_params }; // Запускаем binding. Ошибка → diagnostic. // // Precedence (Plan 50): структурные ошибки argbind — арность, // неизвестное имя, двойная привязка, позиционный-после-именованного // — fail-fast в `bind_call_args` и эмитятся первыми. Правило // keyword-only (Plan 50, D102 №1) проверяется ТОЛЬКО когда // структура валидна (`Ok(bindings)`) — оно последнее в порядке // диагностик. match crate::argbind::bind_call_args(effective_params, args) { Err(err) => { let span = { let s = err.span(); if s == crate::diag::Span::dummy() { e.span } else { s } }; errors.push(Diagnostic::new(err.message(), span)); } Ok(bindings) => { // Plan 50 (D102 ревизия): параметр с дефолтом — keyword-only. // Позиционная привязка к дефолтному параметру — ошибка. // Trailing-форма исключена структурно: trailing-bound // параметр уже снят из `effective_params` выше, поэтому // в `bindings` его нет — заполнение дефолтного последнего // параметра trailing-формой не считается нарушением. // // Отдельная диагностика на КАЖДЫЙ нарушающий аргумент // (не «первый и стоп») — error recovery без каскада: // просто продолжаем цикл. self.check_keyword_only(effective_params, args, &bindings, callee_name, errors); // Plan 172.5 (D326): validate `ref` passing-mode — call-site // marker ⟺ `mut ref` param, arg addressability + mutability, // and same-call alias exclusivity (E_REF_ALIAS_OVERLAP). self.check_ref_arg_modes(effective_params, args, &bindings, scope, errors); } } } /// Plan 172.5 (D326 R4/R9): validate the `ref` passing-mode of every /// argument bound to a parameter of the resolved callee. /// /// - `mut ref` param ⟺ call-site `ref <place>` marker (R4): a missing /// marker on a `mut ref` param, or a `ref` marker on a non-`mut ref` /// param, is an error. /// - the `ref`-marked place must be addressable (R4) and mutable (writes /// land in it). /// - two `mut ref` args in the SAME call whose places overlap on a shared /// root (one path a prefix of the other, indices erased unless provably /// distinct int-literals) → `E_REF_ALIAS_OVERLAP` (R9, the sole new /// error code). This is a narrow anti-footgun, NOT a Rust/Swift exclusive /// borrow guarantee (Nova stays aliased-mut-sound under GC — D157/D246-P10). fn check_ref_arg_modes( &self, params: &[Param], args: &[CallArg], bindings: &[crate::argbind::ArgBinding], scope: &HashMap<String, TypeRef>, errors: &mut Vec<Diagnostic>, ) { use crate::argbind::ArgBinding; // Collect the places of `mut ref` args for the overlap relation. let mut mut_ref_places: Vec<(RefPlace, crate::diag::Span)> = Vec::new(); // Plan 184 (Р12): places of NON-mut value-typed args в этом же вызове — // для расширенной эксклюзивности (mut × любой параметр того же места). let mut other_value_places: Vec<(RefPlace, crate::diag::Span)> = Vec::new(); for (pi, param) in params.iter().enumerate() { let Some(binding) = bindings.get(pi) else { continue; }; // The arg expr bound to this param (single-arg bindings only; // variadic/default carry no single `ref` place). let arg_expr = match binding { ArgBinding::Positional(ai) | ArgBinding::Named(ai) => args.get(*ai).map(|a| a.expr()), _ => None, }; let Some(arg_expr) = arg_expr else { continue; }; // Plan 184 Р10: a `mut x T` param where `T` is a VALUE/primitive type // is a by-pointer in-out reference — the argument must be a mutable, // addressable lvalue so writes reach the caller. Heap / protocol / // typevar `mut` params keep their handle ABI (Р6: `ref H ≡ H`) and // impose no such constraint (mutation already reaches the shared // object; no observable copy). `consume` is a move, not in-out. if param.is_mut && !param.consume && self.param_ty_is_inout_value(¶m.ty) { if self.check_mut_inout_arg(arg_expr, scope, errors) { if let Some(place) = RefPlace::of(arg_expr) { mut_ref_places.push((place, arg_expr.span)); } } continue; } // Plan 184 (Р12): НЕ-mut value-типизированный параметр — ro-авто- // представление (копия ≤~16Б / скрытая ссылка) НАБЛЮДАЕМО при // алиасинге с mut-параметром того же места (`f(a, a)` при // `f(x Big, mut y Big)`: чтение x после записи y даёт разное в // зависимости от представления). Собираем место для проверки // пересечения с mut-местами ниже. (consume — move, не in-out.) if !param.consume && self.param_ty_is_inout_value(¶m.ty) { if let Some(place) = RefPlace::of(arg_expr) { other_value_places.push((place, arg_expr.span)); } } // Plan 184 (заход-5, п.7): мёртвый код `match param.ref_mode` // (RoRef/MutRef-арм + E_REF_MARKER_* + call-site `ref`-маркер) // удалён — парсер снял формы `ro ref`/`mut ref` в параметре // (заход-1) и call-site маркер `f(ref x)` (E_REF_CALL_MARKER_REMOVED), // поэтому `param.ref_mode` всегда `None`, а `RefArg` в CHECKER-AST не // производится (RefArg — только codegen-транспорт синтезированного // in-out, заход-4). Ось mut in-out ведёт value-путь Р10 выше. } // R9 exclusivity: pairwise prefix-overlap over the mut-ref places. for i in 0..mut_ref_places.len() { for j in (i + 1)..mut_ref_places.len() { if mut_ref_places[i].0.overlaps(&mut_ref_places[j].0) { errors.push(Diagnostic::new( "[E_REF_ALIAS_OVERLAP] два in-out `mut`-аргумента одного вызова \ ссылаются на пересекающиеся места (общий root, один путь — \ префикс другого; D326 R9). Одновременная in-out мутация \ перекрывающихся мест запрещена (анти-footgun). Разведи их на \ непересекающиеся места (напр. разные поля `x.a`/`x.b`), либо \ мутируй последовательно. Замечание: это узкая синтаксическая \ проверка, НЕ Rust/Swift-гарантия эксклюзивности." .to_string(), mut_ref_places[j].1, )); } } } // Plan 184 (Р12): расширенная эксклюзивность — mut-место × ЛЮБОЙ другой // value-параметр того же места (не только mut×mut). ro-авто-представление // делает чтение НЕ-mut параметра наблюдаемо зависящим от того, копия это // или скрытая ссылка, когда параллельный mut пишет в то же место. for (mp, _mspan) in &mut_ref_places { for (op, ospan) in &other_value_places { if mp.overlaps(op) { errors.push(Diagnostic::new( "[E_REF_ALIAS_OVERLAP] in-out `mut`-аргумент и другой \ value-аргумент того же вызова ссылаются на пересекающиеся \ места (Plan 184 Р12, расширение D326 R9). ro-авто-\ представление value-параметра (копия vs скрытая ссылка) \ наблюдаемо при параллельной mut-записи в то же место — \ результат зависел бы от выбора представления. Передай \ непересекающиеся места либо явную локальную копию \ (`ro c = a`) в неизменяемый параметр." .to_string(), *ospan, )); } } } } /// Plan 184 Р10: is `ty` a VALUE/primitive type whose `mut x T` parameter /// uses the by-pointer in-out ABI? Primitives are recognized inline; value- /// records and named tuples are looked up in `value_type_names`. Heap /// records, protocols, arrays, pointers, funcs and typevars are NOT value /// types (Р6: `ref H ≡ H` — their `mut` param keeps the handle ABI, no /// mutability constraint on the argument). Mirrors the codegen predicate /// `EmitC::param_is_inout_ptr` so checker and codegen agree. fn param_ty_is_inout_value(&self, ty: &TypeRef) -> bool { match ty { TypeRef::Named { path, .. } if path.len() == 1 => { let n = path[0].as_str(); matches!( n, "int" | "uint" | "i8" | "i16" | "i32" | "i64" | "u8" | "u16" | "u32" | "u64" | "f32" | "f64" | "bool" | "char" | "byte" | "str" ) || self.value_type_names.contains(n) } TypeRef::Tuple(..) => true, TypeRef::Readonly(inner, _) | TypeRef::Mut(inner, _) | TypeRef::Uninit(inner, _) => { self.param_ty_is_inout_value(inner) } _ => false, } } /// Plan 184 Р10: the argument bound to a value/primitive `mut x T` parameter /// (by-pointer in-out) must be a mutable, addressable lvalue — the callee /// writes through a pointer to the caller's storage. Rejected with /// `E_MUT_ARG_NOT_MUTABLE`: /// - rvalues (call results, literals, arithmetic, record literals) and /// array-index roots — no stable storage to write back into; /// - `ro`-bound locals — immutable place. /// Returns `true` if the argument is a valid mutable in-out place. fn check_mut_inout_arg( &self, arg: &Expr, _scope: &HashMap<String, TypeRef>, errors: &mut Vec<Diagnostic>, ) -> bool { match crate::ast::addr_of_chain_root(&arg.kind) { crate::ast::AddrChainRoot::Lvalue(_root) => { // Addressable lvalue (named binding / `@` / field path). The `ro` // vs `mut` binding distinction lives in `TypeCheckCtx`, not // `BoundCtx`, so binding-immutability of an lvalue is enforced by // the assignment-target checker on the write side, not here. // [M-184-mut-arg-ro-binding] — follow-up: reject `ro`-bound // lvalues at the call site too. №370 (p-diag, 2026-08-08): // the `@field`/`ident.field` axis of that follow-up is now // covered — NOT here (`BoundCtx` has no ro-binding/receiver- // mutability tracking at all, adding it would duplicate // `TypeCheckCtx`/`ConsumeCtx` state a third time), but in // `check_readonly_coerce_args` (D246-амендмент Ф.1 sibling, // same file, `ConsumeCtx`-based) — see its own doc. true } crate::ast::AddrChainRoot::IndexInChain | crate::ast::AddrChainRoot::Rvalue => { errors.push(Diagnostic::new( "[E_MUT_ARG_NOT_MUTABLE] аргумент связан с `mut`-параметром (in-out по \ значению, Plan 184 Р10) — требуется адресуемое изменяемое место \ (переменная/поле), но передано временное значение или элемент по \ индексу (нет стабильного хранилища для записи в вызывающего). Свяжи \ значение через `mut x = ...` и передай `x`." .to_string(), arg.span, )); false } } } /// Plan 172.5 (D326 R4): a `ref`-marked argument must be an addressable /// place — a named binding / `@` receiver, optionally projected through /// `.field` / `[i]`. An rvalue root (call result, literal, arithmetic) has /// no stable storage to borrow. Reuses the `addr_of` chain classifier. /// Returns `true` if addressable (no diagnostic emitted). fn check_ref_place_addressable(&self, place: &Expr, errors: &mut Vec<Diagnostic>) -> bool { match crate::ast::addr_of_chain_root(&place.kind) { crate::ast::AddrChainRoot::Lvalue(_) => true, crate::ast::AddrChainRoot::IndexInChain => { // An array-index link in the chain roots the borrow in a buffer // slot that can move (resize / GC compaction) — same hazard as // `&arr[i].field`. Conservatively banned in V1. errors.push(Diagnostic::new( "[E_REF_ARG_NOT_ADDRESSABLE] `ref` аргумент проходит через \ индекс массива (`arr[i]...`): слот может переехать (resize / \ GC-compaction), borrow был бы висячим (D326 R4). Скопируй \ элемент в локал и передай `ref` на него." .to_string(), place.span, )); false } crate::ast::AddrChainRoot::Rvalue => { errors.push(Diagnostic::new( "[E_REF_ARG_NOT_ADDRESSABLE] `ref` требует адресуемое место \ (переменная, поле, `@`-ресивер), а не rvalue (результат \ вызова, литерал, арифметика) — у него нет стабильного \ стораджа для in-out borrow (D326 R4). Присвой значение \ `mut`-локалу и передай `ref <локал>`." .to_string(), place.span, )); false } } } /// Plan 50 (D102 №1): после успешного argbind — найти позиционные /// аргументы, легшие на параметры с дефолтом, и эмитить production-grade /// диагностику на каждый (имя параметра, `note: declared here`, /// machine-applicable structured suggestion `name: <expr>`). /// /// Plan 196.3 (D102/D372-amend2 fold, one window): единственное /// структурное исключение из keyword-only — canonical single-default /// `cap`-ctor: `fn Type.new(cap int = 0) -> Self`. Спека /// (`spec/decisions/02-types.md` §D372 amend 2) делает `.new(1024)` /// позиционным ЛЕГАЛЬНЫМ наравне с `.new(cap: 1024)` — для ЛЮБОГО типа, /// принявшего конвенцию (`Vec`/`[]T`, `HashMap`, `Set`, `Queue`, /// `StringBuilder`, `WriteBuffer`, и любой будущий), generic-static или /// нет. Раньше это работало ТОЛЬКО для generic-static форм (`Vec[T] /// .new(n)`) — не потому что было спроектировано, а потому что их /// `Member{obj: TurboFish{..}}` call-shape не резолвится через /// `resolve_instance_method` (obj — не value типа) и звонок в /// `check_call_argbind` возвращался РАНО, до вызова этой функции; /// non-generic static (`StringBuilder.new(128)`, `Path`-shape, /// однозначный `method_table`-lookup) резолвился успешно и ловил D102 /// по случайности AST-формы. Проверка вынесена сюда — единственная /// точка входа для ВСЕХ трёх путей резолва `check_call_argbind`, так /// что расхождение по AST-форме больше не имеет значения. Заскоуплено /// УЗКО (имя callee буквально `new`, арность ровно 1, единственный /// параметр называется буквально `cap`) — НЕ открывает позиционку для /// произвольных default-параметров других функций/методов. fn check_keyword_only( &self, effective_params: &[Param], args: &[CallArg], bindings: &[crate::argbind::ArgBinding], callee_name: &str, errors: &mut Vec<Diagnostic>, ) { use crate::argbind::ArgBinding; use crate::diag::{Applicability, Span, Suggestion}; let is_canonical_cap_ctor = callee_name == "new" && effective_params.len() == 1 && effective_params[0].name == "cap"; if is_canonical_cap_ctor { return; } for (pi, binding) in bindings.iter().enumerate() { let ArgBinding::Positional(ai) = binding else { continue; }; let param = &effective_params[pi]; if param.default.is_none() { continue; } // Нарушение: позиционный аргумент `args[*ai]` лёг на // дефолтный параметр `param`. let arg_span = args[*ai].expr().span; // Structured suggestion — чистая ВСТАВКА `<name>: ` в начале // выражения-аргумента (span нулевой ширины). Source-независимо: // producer не читает исходник. Machine-applicable — edit // корректен и авто-применим (`nova fix` / LSP code-action). let insert_at = Span::with_file(arg_span.start, arg_span.start, arg_span.file_id); let suggestion = Suggestion { message: format!("pass `{}` by name", param.name), span: insert_at, replacement: format!("{}: ", param.name), applicability: Applicability::MachineApplicable, }; let diag = Diagnostic::new( format!( "параметр `{}` имеет значение по умолчанию — \ передаётся только по имени (D102)", param.name, ), arg_span, ) .with_note_at( format!("параметр `{}` объявлен здесь", param.name), param.span, ) .with_note( "параметры с дефолтом — keyword-only: обязательный — \ позиционно, опциональный — по имени", ) .with_suggestion(suggestion); errors.push(diag); } } /// Plan 53 / D411: refutability check для `let`(ro/mut)-pattern. /// Допустимы только /// irrefutable patterns: /// - `Ident`, `Wildcard` /// - `Tuple(pats)` — рекурсивно irrefutable /// - `Record` без type_path РЛР с type_path к record-типу (не /// sum-variant) — рекурсивно irrefutable для под-pattern'ов /// - `Binding { inner, .. }` — inner irrefutable /// /// Refutable (compile error, `[E_REFUTABLE_BINDING]`, D411): /// - `Literal`, `Variant`, `Or`, `Array` (всегда refutable) /// - `Record` с type_path к sum-variant (нужен tag-check в runtime) /// /// Production-grade diagnostic: код `[E_REFUTABLE_BINDING]` + тип /// нарушения + подсказка `if let <pat> = <expr> { ... }` / `match`. /// (`..`-partial-list rule для record-биндингов — отдельная проверка, /// `check_priv_pattern_recursive_inner` c `enforce_binding_rest`, /// `[E_RECORD_PATTERN_NEEDS_REST]`.) fn check_let_pattern_irrefutable(&self, pat: &Pattern, errors: &mut Vec<Diagnostic>) { match pat { Pattern::Ident { .. } | Pattern::Wildcard(_) => {} Pattern::Tuple(pats, _) => { for p in pats { self.check_let_pattern_irrefutable(p, errors); } } Pattern::Record { type_path, fields, span, .. } => { // Sum-variant in type_path — refutable. if let Some(path) = type_path { if let Some(last) = path.last() { if self.sum_variant_names.contains(last) { let path_str = path.join("."); errors.push( Diagnostic::new( format!( "[E_REFUTABLE_BINDING] refutable pattern in `let`: `{}` \ is a sum-variant — match is not statically guaranteed \ (D52/D411). Use `if let` or `match` instead.", path_str, ), *span, ) .with_note( "Plan 53/D411: `let` accepts only irrefutable patterns (Ident, \ Wildcard, Tuple, plain-Record). Sum-variants need a \ runtime tag-check — `let` cannot perform it.", ) .with_note( format!( "example: `if let {} {{ ... }} = <expr> {{ ... }}`", path_str, ), ), ); return; } } } // Recurse into sub-patterns of fields. for f in fields { if let Some(sub) = &f.pattern { self.check_let_pattern_irrefutable(sub, errors); } } } Pattern::Binding { inner, .. } => { self.check_let_pattern_irrefutable(inner, errors); } Pattern::Literal(_, span) => { errors.push( Diagnostic::new( "[E_REFUTABLE_BINDING] refutable pattern in `let`: literal match is not \ statically guaranteed. Use `if let` / `match`, or a plain \ `let x = ...; if x == ...`", *span, ) .with_note( "example: `if let 42 = n { ... }` or `let x = n; if x == 42 { ... }`", ), ); } Pattern::Variant { path, span, .. } => { let path_str = path.join("."); errors.push( Diagnostic::new( format!( "[E_REFUTABLE_BINDING] refutable pattern in `let`: `{}` is a \ variant-pattern — match is not statically guaranteed \ (D52/D59/D411). Use `if let` or `match` instead.", path_str, ), *span, ) .with_note( "Plan 53: variant-patterns need a runtime tag-check — `let` \ guarantees binding, not a fallible match.", ) .with_note( format!( "example: `if let {}(..) = <expr> {{ ... }}`", path_str, ), ), ); } Pattern::Or { span, .. } => { errors.push( Diagnostic::new( "[E_REFUTABLE_BINDING] refutable pattern in `let`: alternation `|` is \ not statically guaranteed to match. Use `if let` / `match`.", *span, ) .with_note( "example: `match x { A | B => ..., _ => ... }`", ), ); } Pattern::Array { span, .. } => { errors.push( Diagnostic::new( "[E_REFUTABLE_BINDING] refutable pattern in `let`: array length is not \ statically guaranteed. Use `if let` / `match`, or index/length checks \ like `xs[0]`, `xs.len`.", *span, ) .with_note( "example: `if let [a, b, c] = xs { ... } else { /* handle */ }` \ or `match xs { [a, b, c] => ..., _ => ... }`", ) .with_note( "Plan 53: array-length is checked at runtime — `let` accepts \ only statically-guaranteed patterns.", ), ); } } } /// Plan 50 follow-up: резолв `obj.method` для argbind-диагностик. /// /// Сначала best-effort receiver-type inference через `infer_arg_ty` /// — если тип `obj` известен (Ident в scope / record-литерал / /// литерал-примитив), точный резолв через `method_table[type][name]`. /// Это закрывает gap при коллизии имён методов между типами /// (`Box.scaled` vs `Cube.scaled` с дефолтами): без inference оба /// попадали в name-only поиск, тот видел >1 sig → ambiguous → skip, /// keyword-only диагностика терялась. /// /// Fallback — name-only через все типы (поведение Plan 46): подходит /// когда тип receiver'а не выводим (сложное выражение / generic). /// Уникальное имя метода → один тип → один sig → используем его. /// Рначе — пропускаем, codegen резолвит через type-info. fn resolve_instance_method( &self, obj: &Expr, method_name: &str, scope: &HashMap<String, TypeRef>, arg_count_hint: usize, ) -> Option<&FnDecl> { // [M-187-d182-turbofish-new-nameonly-collision]: a TurboFish receiver // (`Type[args].method(...)`, e.g. `D182Pair[int, str].new(5, "x")`) // is a GENERIC-STATIC call, not a value-typed instance — `infer_arg_ty` // below has no `TurboFish` arm, so Попытка 1 (receiver-type inference) // always misses for this shape and falls through to Попытка 2's // receiver-BLIND name-only scan across `self.sig.method_table`. A // receiver-own-generic method (`fn D182Pair[A, B].new(a A, b B)`) is // NOT registered in `self.sig.method_table` at all (separate // generic-method registry) — so that scan can never even see the // TRUE callee, yet still finds and returns whichever UNRELATED // concrete type's arity-compatible `.new()` happens to be the sole // "inherent" match (e.g. serde `DeError.new(kind, path = "")`, // arity 2) — a false-positive D102 keyword-only-default diagnostic // against a signature that was never actually called. This // best-effort check (see this fn's own doc + `check_call_argbind`'s // comment on the by-design "almost always ambiguous → skip" intent // for exactly this shape) is not the codegen dispatch path (that // resolves the REAL generic receiver correctly elsewhere) — bailing // out unconditionally for a TurboFish receiver just means "skip this // diagnostic", never "miss a real error". if matches!(obj.kind, ExprKind::TurboFish { .. }) { return None; } // Попытка 1: receiver-type inference. // Plan 162 Ф.3: когда тип receiver'а известен, сначала проверяем // наследуемые (inherent) методы — методы, объявленные в том же модуле // что и тип T. Inherent методы доступны без явного import модуля-метода: // при импорте типа T все его inherent методы автоматически доступны // (они слиты в merged module.items). Это позволяет вызывать foo.greet() // даже если пользователь импортировал только тип Foo, а не модуль методов. if let Some(recv_ty) = Self::infer_arg_ty(obj, scope) { if let TypeRef::Named { path, .. } = &recv_ty { if path.len() == 1 { let type_name = &path[0]; if let Some(methods) = self.sig.method_table.get(type_name) { if let Some(overloads) = methods.get(method_name) { // Plan 162 Ф.3: prefer inherent overloads over // extension overloads when receiver type is known. // Inherent = declared in same module as the type. let inherent: Vec<&&FnDecl> = overloads .iter() .filter(|f| { self.is_inherent_method(type_name, &f.name) }) .collect(); let candidates = if !inherent.is_empty() { &inherent[..] as &[&&FnDecl] } else { // All overloads (extension or unknown origin). // Wrap to match type; use slice of all overloads. // SAFETY: overloads is Vec<&FnDecl>; inherent is // empty so fall back to non-inherent branch below. &[][..] }; if let [single] = candidates { return Some(single); } // Either 0 inherent (all extension) or >1 inherent // (overloaded inherent). Fall through to non-inherent // single-overload path. if let [single] = overloads.as_slice() { return Some(single); } } } } } } // Попытка 2: name-only fallback. Уникальное имя метода через // все типы → один sig, используем. // Plan 109: фильтр по arity предотвращает ложные "expected 0, got N" // когда builtin-метод ([]T::push и т.п.) отсутствует в method_table, // но пользовательский тип случайно имеет метод с тем же именем. // Plan 162 Ф.3: в name-only поиске inherent-методы приоритетны при // неоднозначности (два типа → один inherent → один extension). let mut found_inherent: Option<&FnDecl> = None; let mut found: Option<&FnDecl> = None; let mut ambiguous_inherent = false; let mut ambiguous = false; for (type_name, methods) in &self.sig.method_table { if let Some(overloads) = methods.get(method_name) { for f in overloads { if f.params.len() != arg_count_hint { continue; } let inherent = self.is_inherent_method(type_name, &f.name); if inherent { if found_inherent.is_some() { ambiguous_inherent = true; } found_inherent = Some(f); } else { if found.is_some() { ambiguous = true; } found = Some(f); } } } } // Return inherent if unambiguous; otherwise fall back to extension if // unambiguous; otherwise return None (ambiguous → codegen resolves). if !ambiguous_inherent { if let Some(f) = found_inherent { return Some(f); } } if ambiguous { return None; } found } /// Если param's TypeRef — простой `Named{path: [T]}` где T в /// списке generics, вернуть имя T. Рначе None. fn param_generic_name(ty: &TypeRef, generics: &[GenericParam]) -> Option<String> { let TypeRef::Named { path, generics: g, .. } = ty else { return None; }; if path.len() != 1 || !g.is_empty() { return None; } if generics.iter().any(|gp| gp.name == path[0]) { Some(path[0].clone()) } else { None } } /// Минимальная inference типа argument'а — best-effort на основе /// синтаксической формы и текущего scope (let-bindings). fn infer_arg_ty(e: &Expr, scope: &HashMap<String, TypeRef>) -> Option<TypeRef> { match &e.kind { // Plan 172.1 U.1.3b step 1 (Gap A): self/@ receiver carries the enclosing // method's receiver type, injected by `f1_check_fn` under the scope key "@" // (D176, mirror of `infer_expr_type` SelfAccess arm :9225). Without this, // `self.method()` / `@.method()` calls inside a method body returned None here → // `check_instance_overload` bailed → no `resolved_callees` write → codegen // re-derived the return type (the §10 "resolution computed but thrown away" // anti-pattern; §1 "materialize the resolution"). Materializing the self-receiver // lets the checker resolve self-method calls into the channel — byte-identical // (codegen consumes only when `fn_ret_by_span` also has the callee, i.e. non-extern // self-methods where channel == legacy; extern self-methods miss the index until // U.1.3b step 2 / Gap B, so no flip here). ExprKind::SelfAccess => scope.get("@").cloned(), ExprKind::Ident(name) if name == "self" => { scope.get("self").cloned().or_else(|| scope.get("@").cloned()) } ExprKind::Ident(name) => scope.get(name).cloned(), // Plan 97.1 hardening (D142): protocol-литерал имеет тип // именованного protocol'а — это позволяет let-binding // получить корректный тип в scope (для последующего // check_protocol_method_call enforcement'а). ExprKind::ProtocolLit { proto_name, .. } => Some(TypeRef::Named { path: proto_name.clone(), generics: Vec::new(), span: e.span, }), ExprKind::RecordLit { type_name: Some(name), .. } => Some(TypeRef::Named { path: name.clone(), generics: Vec::new(), span: e.span, }), ExprKind::ArrayLit(elems) => { // []T — element type from first element. let inner = elems.iter().find_map(|el| match el { ArrayElem::Item(it) | ArrayElem::Spread(it) => Self::infer_arg_ty(it, scope), }); inner.map(|t| TypeRef::Array(Box::new(t), e.span)) } ExprKind::IntLit(_) => Some(TypeRef::Named { path: vec!["int".to_string()], generics: vec![], span: e.span }), ExprKind::FloatLit(_) => Some(TypeRef::Named { path: vec!["f64".to_string()], generics: vec![], span: e.span }), ExprKind::BoolLit(_) => Some(TypeRef::Named { path: vec!["bool".to_string()], generics: vec![], span: e.span }), ExprKind::StrLit(_) | ExprKind::InterpolatedStr { .. } => Some(TypeRef::Named { path: vec!["str".to_string()], generics: vec![], span: e.span }), ExprKind::CharLit(_) => Some(TypeRef::Named { path: vec!["char".to_string()], generics: vec![], span: e.span }), // [M-inline-cast-receiver-method-resolution] (реестр 221.1 №149): an // inline `as`-cast in RECEIVER position (`(x as u64).to_i128()`) was // invisible to this lightweight probe — no arm matched // `ExprKind::As`, so `check_instance_overload` (and every other // caller of `infer_arg_ty`) silently got `None` for the receiver // type and skipped resolution entirely. `ro v = x as u64; v.m()` // worked (the `Ident` arm above resolves through `scope`), only the // INLINE form was blind — the checker consumer treated a channel // gap as "nothing to resolve" and codegen's own name-only fallback // (`method_receivers` last-wins) silently picked a DIFFERENT // same-named overload (int128.nv's `int`/`i64 @to_i128` instead of // `u64 @to_i128`), producing either a wrong value or — for // `int @to_i128() => (@ as i64).to_i128()` — infinite self- // recursion (stack overflow, no diagnostic). The `as`-target type // IS the receiver's static type by definition (same source // `infer_expr_type`'s own `ExprKind::As(_, ty) => Some(ty.clone())` // arm already uses); this is purely additive coverage — every // caller previously got `None` here and either fell through to a // narrower fallback or a no-op, never a WRONG answer, so no // existing resolution can flip. ExprKind::As(_, ty) => Some(ty.clone()), // [M-neg-cast-receiver-blanket-dispatch] (int128-связка, план 234 часть B): // `Neg`/`Not`/`BitNot` are type-preserving unary operators — mirrors this // `TypeCheckCtx`'s own general `infer_expr_type`'s `ExprKind::Unary` arm // ("Neg/Not/BitNot: result = operand type"). Without this arm, a receiver // written `(-N as i64).mk()` — parsed as `Unary(Neg, As(N, i64))` — matched // NO arm above (fell to the blanket `_ => None` below), so // `check_instance_overload` bailed out silently and recorded NO // `resolved_callees` entry for the call. Codegen's Plan 196.7 "concrete // beats generic" channel-first dispatch (`emit_c.rs` ~43189) then had // nothing to read and fell through into the generic type-set-bounded // blanket dispatch path — even when an EXACT concrete overload (`i64 // @mk()`) exists and should win by D84 "concrete beats generic". A bare // `(N as i64).mk()` (no `Neg`) already worked via the `As` arm just above // ([M-inline-cast-receiver-method-resolution], plan 234 part B step 1). // `AddrOf`/`RawAddrOf`/`Deref` are deliberately excluded — those change // the type (pointer creation/dereference), they do not preserve it. ExprKind::Unary { op, operand } if matches!( op, crate::ast::UnOp::Neg | crate::ast::UnOp::Not | crate::ast::UnOp::BitNot ) => { Self::infer_arg_ty(operand, scope) } // Plan 207 cmpxchg-lint волна B: `Atomic*.new(...)` static ctor → `Self`. // Narrowly scoped to the `Atomic` type family (NOT a general `Type.new()` // inference — that would widen this best-effort bound-checker's blast // radius to unrelated call sites out of scope for this fix). Lets // `mut a = AtomicI64.new(0)` (the overwhelmingly common real-code shape — // no explicit `let a AtomicI64 = ...` annotation anywhere in // sync_test.nv/spec_tests/conformance) populate `scope["a"]` so the // CAS-ordering checker (`check_cas_ordering`) can see the receiver type // on the very next statement's `a.compare_exchange(...)`. // // [debug-verified] `AtomicI64.new(0)` parses as `ExprKind::Path(["AtomicI64", // "new"])`, NOT `Member{obj:Ident,name}` — the parser recognizes `AtomicI64` // as a KNOWN type name at parse time (like the primitive-static-call shape // documented at `~14047`'s `is_primitive_type_name` comment, but this applies // to any parser-known type, not only primitives) and emits `Path` instead of // `Member`. Match BOTH shapes — `Member` kept as a defensive fallback in case // a differently-resolved call site (e.g. via a type-alias) takes that path. ExprKind::Call { func, .. } => { let member_shape = if let ExprKind::Member { obj, name } = &func.kind { if name == "new" { if let ExprKind::Ident(type_name) = &obj.kind { Some(type_name.clone()) } else { None } } else { None } } else { None }; let path_shape = if let ExprKind::Path(parts) = &func.kind { if parts.len() == 2 && parts[1] == "new" { Some(parts[0].clone()) } else { None } } else { None }; if let Some(type_name) = member_shape.or(path_shape) { if type_name.starts_with("Atomic") { return Some(TypeRef::Named { path: vec![type_name], generics: Vec::new(), span: e.span, }); } } // №388 (Plan p386-bound-doors) supplement: `mut v = []u8.new()` // (no explicit annotation) parses `[]u8` as the SENTINEL // `Path(["__array", "u8"])` (parser/mod.rs's D38 array-type- // static-method carve-out), so `.new()` is `Member{obj: // Path(["__array", elem]), name: "new"}` — neither shape above // matched (`member_shape` requires an `Ident` obj; `path_shape` // requires `func` itself to be a bare 2-segment `Path`, not a // `Member` wrapping one), so `v` NEVER entered `scope` at all // and EVERY later `v.method(...)` bound-check on it silently // no-op'd — not specific to primitives: verified the identical // silent-pass for a non-primitive bad-bound argument too // (`v.append(NoMethods{..})`), only fixed by adding an // EXPLICIT `mut v []u8 = …` annotation. Recognizing this one // more shape restores the SAME `[]<elem>` type this checker // already infers for a plain `[]elem.of(...)`-style array // literal (`ArrayLit` arm above) — not a new inference rule. if let ExprKind::Member { obj, name } = &func.kind { if name == "new" { if let ExprKind::Path(parts) = &obj.kind { if parts.len() == 2 && parts[0] == "__array" { return Some(TypeRef::Array( Box::new(TypeRef::Named { path: vec![parts[1].clone()], generics: Vec::new(), span: e.span, }), e.span, )); } } } } None } _ => None, } } /// Plan 176 Ф.1 ([M-176-io-forward-bounded-generic]): does a CALLER's own /// declared bound `have` already satisfy a callee's required bound `want`, /// for a forwarded generic-typed argument (`fn f[R Read](r R) { /// read_to_end(r) }` — `have`/`want` are both `"Read"` here; a genuinely /// bounds-heterogeneous forward would have `have != want`)? Exact-name /// match is trivial. Otherwise, if `have` names a registered protocol, /// `have` is a superset of `want` when it structurally provides every /// method `want` requires (name + arity, or a `default_body` fallback — /// D183, the SAME rule `check_satisfaction_against_methods` itself uses for /// a concrete type) — reusing `protocol_specs`, which is already the /// DFS-flattened, `use`-embed-transitive method list per protocol name /// (`BoundCtx::build`), so an embedded-protocol hierarchy is covered for /// free; no NEW hierarchy notion is invented here. If either name is not a /// registered protocol (an effect, a type-set, or simply unknown), only the /// exact-name match above can succeed — no guessing at unregistered shapes. fn generic_bound_covers(&self, have: &str, want: &str) -> bool { if have == want { return true; } let Some(want_spec) = self.protocol_specs.get(want) else { return false; }; let Some(have_spec) = self.protocol_specs.get(have) else { return false; }; want_spec.iter().all(|req| { have_spec.iter().any(|m| m.name == req.name && m.params.len() == req.params.len()) || req.default_body.is_some() }) } /// Plan 15 Ф.3: проверить, что concrete-тип удовлетворяет bound'у /// (protocol-типу). При несоответствии — R5.3 diagnostic. /// /// Plan 97 Ф.2 (D142): bound может быть **анонимным** inline-protocol /// (`[T protocol { method-sig* }]`) — методы проверяются «по месту» /// без регистрации в `protocol_specs`. Закрывает Plan 15 /// `[P-15-anon-protocol-bound]`. fn check_satisfaction( &self, concrete: &TypeRef, bound: &TypeRef, type_param_name: &str, fn_name: &str, span: Span, errors: &mut Vec<Diagnostic>, ) { // Plan 97 Ф.2: inline-protocol bound — методы прямо в TypeRef. if let TypeRef::Protocol { methods, .. } = bound { self.check_satisfaction_against_methods( concrete, methods, None, // anon — нет имени type_param_name, fn_name, span, errors, ); return; } let bound_name = match bound { TypeRef::Named { path, .. } if path.len() == 1 => path[0].clone(), _ => return, // complex bounds (Hashable[K], etc.) — отдельная задача }; // Plan 15 D53 strict: bound должен быть protocol-kind. Если // имя зарегистрировано как effect-kind — это spec violation // (D72: bounds require protocols). R5.3-style diagnostic. if let Some(eff_decl) = self.effect_decls.get(&bound_name) { let _ = eff_decl; errors.push(Diagnostic::new( format!( "type `{}` is an effect, not a protocol — generic bounds \ require protocol-types (D72/D53). Hint: declare `{}` as \ `type {} protocol {{ ... }}` if structural-contract semantics \ is intended; effects are runtime-dispatched capabilities and \ can only appear in effect-rows `(...) {} -> ...`, not as \ `[T {}]` bounds.", bound_name, bound_name, bound_name, bound_name, bound_name, ), span, )); return; } // Plan 172.3 (D310): type-set bound — check concrete T ∈ member set. // MUST run BEFORE the primitive early-return below (primitives ARE the // members we validate). Membership by type IDENTITY (last path segment). if let Some(members) = self.type_sets.get(&bound_name) { let member_name = |m: &TypeRef| -> Option<String> { if let TypeRef::Named { path, .. } = m { path.last().cloned() } else { None } }; let concrete_nm: Option<String> = match concrete { TypeRef::Named { path, .. } => path.last().cloned(), _ => None, }; let is_member = concrete_nm .as_ref() .map(|cn| members.iter().any(|m| member_name(m).as_ref() == Some(cn))) .unwrap_or(false); if !is_member { // Emit ONLY when the concrete is a KNOWN concrete type (primitive or // declared) — a definitive non-member. If it is an unknown name (most // likely a passthrough type-parameter) or a complex type, SKIP // (best-effort, mirrors the protocol path) to avoid false positives. let is_primitive = |n: &str| matches!(n, "int" | "i8" | "i16" | "i32" | "i64" | "u8" | "u16" | "u32" | "u64" | "uint" | "f32" | "f64" | "bool" | "char" | "str" | "any" | "never"); let known_concrete = concrete_nm .as_ref() .map(|cn| is_primitive(cn) || self.type_defining_modules.contains_key(cn)) .unwrap_or(false); if known_concrete { let member_list = members .iter() .filter_map(member_name) .collect::<Vec<_>>() .join(", "); let cn = concrete_nm.unwrap_or_else(|| "<complex type>".to_string()); errors.push(Diagnostic::new( format!( "[E_TYPE_NOT_IN_SET] type `{}` is not a member of type-set \ `{}` (bound `[{} {}]` on `{}`). Allowed members: {{{}}}. \ Type-set bounds (D310) admit only the listed concrete types.", cn, bound_name, type_param_name, bound_name, fn_name, member_list ), span, )); } } return; // type-set bound fully handled (no protocol-method satisfaction) } let concrete_name = match concrete { TypeRef::Named { path, .. } if path.len() == 1 => path[0].clone(), // Plan 138.1 Ф.1 (D239): `[]T` ≡ `Vec[T]` — align the array // receiver with the Vec named type so bound-resolution checks // Vec's method_table (iter/next/len/...). Without this, `[]T` // passed where an `Iter`-bound type-param is expected would be // silently skipped; now it genuinely satisfies via Vec's methods. TypeRef::Array(_, _) => "Vec".to_string(), // Tuple/Func — пока пропускаем (не обрабатываем составные T). _ => return, }; // [M-property-testing-rot] (Plan 172.13 батч 3), upgraded Plan 176 Ф.1 // ([M-176-io-forward-bounded-generic]): PASSTHROUGH typevar — the // "concrete" type is a generic param of the ENCLOSING (caller) fn // (`property[G Generator[T], T]` body calling `property_with(gen, ...)`; // io-core's `read_to_string[R Read]` forwarding into // `read_to_end[R2 Read]`). Plan 196 gs carries the caller's OWN bounds // now — only skip when one of them ACTUALLY covers the callee's // required `bound_name` (exact match, or protocol-hierarchy superset via // `generic_bound_covers`); an unbounded (or incompatibly-bounded) caller // param falls through to the normal structural check below, which // correctly reports it as unsatisfied (the typevar has no `method_table` // entry, so every required method comes back "missing" — the honest // diagnostic this fix restores for the genuinely-unsatisfied case; the // pre-fix blanket by-name skip silently accepted THAT case too — // verified regression via `nova check` on an unbounded forward). if let Some(gp) = self.current_fn_gs.borrow().get(&concrete_name) { let covered = gp.bounds.iter().any(|b| { let TypeRef::Named { path: bpath, .. } = b else { return false }; bpath.len() == 1 && self.generic_bound_covers(&bpath[0], &bound_name) }); if covered { return; } } // №388 (Plan p386-bound-doors): primitives no longer bypass EVERY // bound unconditionally. `never` (Plan 76, bottom-type) and `any` // (D-any, top-type/empty-contract protocol) stay a vacuous pass // regardless of bound — that IS their entire semantics, unrelated to // primitive-ness. Every OTHER primitive auto-satisfies EXACTLY the // compiler's built-in auto-derivable protocol family // (`is_builtin_protocol`: Equal/Hash/Compare/Clone/Display/Debug/ // Serialize/Deserialize/Reflect) — the SAME authority // `auto_derive::check_field_eligibility`'s unconditional // `is_primitive_type(name) => true` already grants primitive FIELDS // for exactly this protocol family (field-eligibility for // auto-derive), reused here for a primitive used directly as a // bound's concrete type. Any OTHER bound (user protocol, `AsSlice`, // anonymous `protocol { … }`) is NOT vacuously satisfied anymore — // falls through to the SAME structural method_table lookup every // declared type goes through below (`v.append(42)` — `int` lacks // `AsSlice[u8]`'s `@ptr`/`@len` — now correctly rejected). `str`'s // previous apparent pass on `AsSlice[u8]` was NEVER decided by this // block — `#coerce str@bytes()` rewrites the call-site argument to // `[]u8` upstream of this checker; that path is untouched. if matches!(concrete_name.as_str(), "int" | "i8" | "i16" | "i32" | "i64" | "u8" | "u16" | "u32" | "u64" | "f32" | "f64" | "bool" | "char" | "str" | "any" | "never") { if matches!(concrete_name.as_str(), "any" | "never") { return; } if crate::protocols::auto_derive::is_builtin_protocol(&bound_name) { return; } // Fall through to the structural protocol_specs/method_table // check below — same path every non-primitive concrete type // takes for a non-builtin bound. } let Some(spec_methods) = self.protocol_specs.get(&bound_name) else { // Bound — не зарегистрирован ни как protocol, ни как effect. // Может быть type alias / record / unknown. Пока пропускаем — // formal check'а не делаем (best-effort permissive). return; }; // Plan 97 Ф.2: shared satisfaction-логика с anon-вариантом. self.check_satisfaction_against_methods( concrete, spec_methods.as_slice(), Some(&bound_name), type_param_name, fn_name, span, errors, ); } /// Plan 97 Ф.2 (D142): общая satisfaction-логика для named и anonymous /// protocol-bound'ов. `bound_name = Some(...)` — named (показывается /// в diagnostic); `None` — inline `[T protocol { ... }]`, рендерим /// как `protocol{...}`. fn check_satisfaction_against_methods( &self, concrete: &TypeRef, required: &[EffectMethod], bound_name: Option<&str>, type_param_name: &str, fn_name: &str, span: Span, errors: &mut Vec<Diagnostic>, ) { let concrete_name = match concrete { TypeRef::Named { path, .. } if path.len() == 1 => path[0].clone(), // Plan 138.1 Ф.1 (D239): `[]T` ≡ `Vec[T]` — see check_satisfaction. TypeRef::Array(_, _) => "Vec".to_string(), _ => return, }; // №388 (Plan p386-bound-doors): mirrors the identical fix in // `check_satisfaction` above (this fn is the shared satisfaction // primitive both `check_satisfaction` AND the anonymous-protocol / // `Vec`-element-recursion callers route through — see doc there for // the full rationale). `bound_name` is `None` for an anonymous // `protocol { … }` bound — `is_builtin_protocol` never matches // `None`, so an anon-protocol bound on a primitive correctly falls // through to the structural check (no builtin-protocol name to // vacuously grant). if matches!(concrete_name.as_str(), "int" | "i8" | "i16" | "i32" | "i64" | "u8" | "u16" | "u32" | "u64" | "f32" | "f64" | "bool" | "char" | "str" | "any" | "never") { if matches!(concrete_name.as_str(), "any" | "never") { return; } if bound_name.map_or(false, crate::protocols::auto_derive::is_builtin_protocol) { return; } } // Plan 180: a type declaring `#impl(P)` for a built-in AUTO-DERIVABLE // protocol P (Serialize/Deserialize/Equal/Hash/Clone/Compare/Display/ // Debug) satisfies the `[T P]` bound — the P-method is compiler- // SYNTHESIZED (register_synthesized_methods / inject_synthesized_methods) // and lives in the `synth_methods` overlay, NOT the base // `sig.method_table` this structural check consults. (Serde is the first // auto-derive protocol used as a generic BOUND on an `#impl`-only type; // the earlier `==`/`clone`/… protocols happened not to exercise this // path.) verify_impl_protocols already validated the `#impl` is // synthesizable, so trusting it here cannot mask a real gap. if let Some(bn) = bound_name { if crate::protocols::auto_derive::is_builtin_protocol(bn) { if let Some(protos) = self.impl_protocol_types.get(&concrete_name) { if protos.iter().any(|p| p == bn) { return; } } } } // Plan 221.1 №111: a `[]T`/`Vec[T]` container-conformance blanket // (`fn[T Bound] []T @method`, e.g. serde.nv's `Serialize`/ // `Deserialize` on `[]T`) registers under the LITERAL "[]<declared- // typevar-name>" method_table key (parser spelling), never under // "Vec" — so a DIRECT bound-check on a bare `Vec[X]`/`[]X` argument // (`json_encode(items)` with no enclosing struct field) always fell // through to the plain `method_table.get("Vec")` lookup below and // reported "type `Vec` does not satisfy" UNCONDITIONALLY, for every // element type including a plain `[]str` (verified: fails // identically for `Vec[Item]` and bare `[]str`). Search for a // registered slice-typevar method (by required-method NAME) whose own // generic parameter carries THIS SAME bound (confirms it is really a // matching blanket, not a coincidental other-typevar method sharing // the name), and if one exists for every required method, the // container's OWN satisfaction reduces to whether its ELEMENT type // satisfies the identical bound (the blanket's `[T Bound]` premise) — // recurse. `elem_of_vec_like` returns `None` for anything but a // single-level concrete-or-generic `Vec[X]`/`[]X` shape, so nested // containers (`Vec[Vec[T]]`) and non-Vec receivers are untouched. if concrete_name == "Vec" { if let Some(elem_ty) = elem_of_vec_like(concrete) { if let Some(bn) = bound_name { let blanket_for_all = required.iter().all(|req| { self.sig.method_table.iter().any(|(recv_key, methods)| { let Some(bare) = recv_key.strip_prefix("[]") else { return false }; methods.get(&req.name).map_or(false, |fns| { fns.iter().any(|f| { f.generics.iter().any(|g| { g.name == bare && g.bounds.iter().any(|b| { matches!(b, TypeRef::Named { path, .. } if path.last().map(|s| s.as_str()) == Some(bn)) }) }) }) }) }) }); if blanket_for_all { let mut elem_errors: Vec<Diagnostic> = Vec::new(); self.check_satisfaction_against_methods( &elem_ty, required, bound_name, type_param_name, fn_name, span, &mut elem_errors, ); errors.extend(elem_errors); return; } } } } let empty: HashMap<String, Vec<&FnDecl>> = HashMap::new(); let concrete_methods = self.sig.method_table.get(&concrete_name).unwrap_or(&empty); let mut missing: Vec<String> = Vec::new(); for req in required { let found = concrete_methods.get(&req.name).map(|fns| { fns.iter().any(|f| f.params.len() == req.params.len()) }).unwrap_or(false); if !found { // Plan 91.8a.2 part 2 (D183 amendment) + №384 (Plan p383-bounds, // единый вход): default body fallback. A protocol method with a // default body (`Equal.equal => @compare(other) == 0`, D145/D183) // can satisfy the bound ONLY if the body's OWN dependency calls // (here `@compare`) themselves resolve for `concrete_name` — the // OLD code accepted ANY `default_body.is_some()` unconditionally // ("Assumption: synthesis will lower default body calls correctly // или error там" — a false assumption: neither codegen NOR this // checker verified it downstream, so the call-site resolver // silently picked an ARBITRARY same-named method elsewhere in the // program — `a.equal(b)` on a type without `@compare` compiled to // `Nova_HashMap_method_equal(a, (void*)(b))`, a `void*`-cast type // confusion / UB, not a caught error). Единый вход: reuse the // EXACT SAME walker `verify_impl_protocols` already uses at // decl-site (`#impl(P)` verification) — `default_body_calls_satisfy_for` // — rather than a second, call-site-only implementation. let satisfied_via_default = req.default_body.as_ref() .map(|body| default_body_calls_satisfy_for(body, &concrete_name, self)) .unwrap_or(false); if satisfied_via_default { continue; } let sig = render_method_sig(&req.name, &req.params, &req.return_type); let prefix = if req.is_static { "." } else { "" }; missing.push(format!("{}{}", prefix, sig)); } } if missing.is_empty() { return; } // №388 (Plan p386-bound-doors) supplement: `#coerce` bridge — see // `coerce_output`'s field doc. Tried AFTER the direct structural // check fails: if `concrete_name` has a declared `#coerce` target // (`str @bytes() -> ro []u8`), the value ACTUALLY reaching a call // site is the coerced type (codegen splices the conversion in), so // bound satisfaction is decided on THAT type instead — one hop // only (no further coerce chase — `coerced_methods` is looked up // directly, no recursive coerce_output lookup), matching D429's // zero-cost/no-chaining character. `v.append("hello")` — `str` // itself lacks `@ptr`/`@len`, but `str`'s `#coerce` target `[]u8` // has both (`Vec[T] #impl(AsSlice[T])`) — satisfied. if let Some(coerced) = self.coerce_output.get(&concrete_name) { let coerced_name = match coerced.strip_modifiers() { TypeRef::Named { path, .. } if path.len() == 1 => Some(path[0].clone()), TypeRef::Array(_, _) | TypeRef::FixedArray(_, _, _) => Some("Vec".to_string()), _ => None, }; if let Some(coerced_name) = coerced_name { let coerced_methods = self.sig.method_table.get(&coerced_name).unwrap_or(&empty); let all_found = required.iter().all(|req| { coerced_methods.get(&req.name).map_or(false, |fns| { fns.iter().any(|f| f.params.len() == req.params.len()) }) }); if all_found { return; } } } let bound_display = bound_name .map(|n| n.to_string()) .unwrap_or_else(|| "<anonymous protocol>".to_string()); let mut msg = format!( "type `{}` does not satisfy `{}` bound (in call to `{}[{} {}]`).\n\n `{}` requires:\n", concrete_name, bound_display, fn_name, type_param_name, bound_display, bound_display); for req in required { let prefix = if req.is_static { "." } else { "" }; msg.push_str(&format!( " {}{}\n", prefix, render_method_sig(&req.name, &req.params, &req.return_type))); } msg.push_str(&format!("\n `{}` is missing: {}\n", concrete_name, missing.join(", "))); msg.push_str(&format!( "\n fix: добавить недостающие методы для типа `{}`. \ См. spec/decisions/02-types.md#d72 и #d142 (anonymous protocol).", concrete_name)); errors.push(Diagnostic::new(msg, span)); } } /// Plan 221.1 №111: element type of a single-level `[]X`/`Vec[X]` receiver /// (either legal D239 spelling), for the container-conformance bound-check /// recursion in `check_satisfaction_against_methods`. `None` for anything /// else (bare `Vec` with no/multiple type-args, a non-Vec Named type, tuple, /// nested container, …) — those are out of scope for this narrow carve-out. fn elem_of_vec_like(ty: &TypeRef) -> Option<TypeRef> { match ty { TypeRef::Array(inner, _) | TypeRef::FixedArray(_, inner, _) => Some((**inner).clone()), TypeRef::Named { path, generics, .. } if path.len() == 1 && path[0] == "Vec" && generics.len() == 1 => { Some(generics[0].clone()) } _ => None, } } /// Plan 15: extract simple identifier-name из Pattern. Рспользуется /// для регистрации let-bindings в scope (только Pattern::Ident; complex /// patterns — tuple/variant — пропускаются). fn pattern_simple_name(p: &Pattern) -> Option<String> { match p { Pattern::Ident { name, .. } => Some(name.clone()), _ => None, } } /// [M-closure-trailing-scalar-coercion-no-typecheck] fix: `true` для узлов, /// представляющих closure-ЛИТЕРАЛ (`|| body` / `|x| body` / `fn(...) ...`) — /// значение, которое codegen лоуэрит в указатель на функцию. НЕ включает /// вызов closure'а (`Call`) — только сам литерал-значение. fn is_closure_literal_expr(e: &Expr) -> bool { matches!( &e.kind, ExprKind::Lambda { .. } | ExprKind::ClosureLight { .. } | ExprKind::ClosureFull(_) ) } /// [M-closure-trailing-scalar-coercion-no-typecheck] fix: `true` для /// `ResolvedType`, представляющих СКАЛЯР (bool/int-family/float/`char`) — /// типов, для которых указатель-на-функцию НИКОГДА не является legal /// значением. Намеренно консервативно: НЕ включает `Str`/`Any`/`Named` /// (кроме `char`) — эти позиции вне текущего скоупа фикса (P2-дыра именно /// про скаляр, D-амендмент 02-types.md). fn resolved_type_is_scalar_like(rt: &ResolvedType) -> bool { match rt { ResolvedType::Bool | ResolvedType::Scalar { .. } | ResolvedType::Float { .. } => true, ResolvedType::Readonly(inner) => resolved_type_is_scalar_like(inner), ResolvedType::Named { name, args, .. } => name == "char" && args.is_empty(), _ => false, } } // ============================================================================ // Plan 16 (D63 forbid + D64 realtime): capability enforcement. // ============================================================================ /// Plan 16: набор "suspend"-эффектов которые нельзя использовать внутри /// `realtime { ... }` блоков (D64). Эти эффекты по семантике могут /// приостановить fiber'а в production-runtime'е. fn realtime_suspend_effect(name: &str) -> bool { matches!(name, "Net" | "Fs" | "Db" | "Time") } /// Plan 83.3 Ф.6: эффекты, запрещённые в теле `blocking { }`. Тело /// исполняется на libuv-threadpool-потоке без fiber/event-loop- /// контекста — async-I/O-эффекты (Net/Fs/Db/Time) там сломаны. /// `Blocking` сюда НЕ входит: вложенный `blocking` на threadpool- /// потоке исполняется inline (`mco_running()` == false) — безвреден. fn blocking_body_forbidden_effect(name: &str) -> bool { matches!(name, "Net" | "Fs" | "Db" | "Time") } /// Plan 16, extended by #273 (D172 amend): hardcoded whitelist of /// callee-names that **allocate** on the managed heap (and are therefore /// forbidden inside `#realtime nogc fn` — and, historically, inside the /// now-retracted `realtime nogc { ... }` block, D64). Identified by /// high-level `Type.method` shape (e.g. `[]int.new`, `StringBuilder.new`, /// `Vec[T].of`). /// /// #273: originally written (Plan 16) for the old block-form; after the /// Plan 113 retraction of that block, this same function became the /// SOLE enforcement mechanism for the `#realtime nogc fn` attribute (see /// `CapState.realtime_nogc` / `RealtimeAttr::RealtimeNogc`) — NOT dead /// code, despite the age of the surrounding comments; a live (but /// incomplete) checker-channel gate. Finding #273: the list did not /// include `.of(...)` — the variadic constructor that became the /// **canonical** way to build a `Vec[T]` from a literal (D259 amend), /// so `Vec[int].of(1, 2, 3)` inside a `#realtime nogc fn` silently /// passed `nova check`. Added below. /// /// **Not covered** by this whitelist: /// - User-defined record constructors `Foo.new()` if they allocate via /// `nova_alloc` — codegen always heap-boxes record literals, so /// effectively any record literal is "allocating". But detection /// requires bigger inference; conservatively we flag only static /// factory methods. /// - `str.from(non-str)` when it requires concatenation — for now we /// treat every `str.from` call as "allocating". /// - Transitive calls: if a user fn `f()` without a `#realtime`/ /// `#realtime nogc` annotation itself calls one of these blacklisted /// callees, and a `#realtime nogc fn` calls `f()` — not caught (no /// transitive inference, same V1 limit as `#parks` propagation, /// D172 §4). The honest alternative is a call-graph may-GC analysis /// (Plan 144.0, `nova gc-effect-analyze`, `codegen/may_gc.rs`), but /// that one targets the post-mono codegen tier (`MayGcSet` over /// `mono_fn_decls`) — a different pipeline phase than this pre-mono /// checker walk; wiring it in here is a separate task, out of scope /// for #273. fn nogc_blacklisted_call(callee_path: &[String]) -> bool { if callee_path.len() != 2 { return false; } let ty = callee_path[0].as_str(); let m = callee_path[1].as_str(); // Array constructors: `[]T.new` / `[]T.with_capacity` / `[]T.of`. if ty.starts_with("[]") && matches!(m, "new" | "with_capacity" | "of") { return true; } // Builder/buffer constructors. if matches!(ty, "StringBuilder" | "WriteBuffer" | "ReadBuffer") && matches!(m, "new" | "with_capacity" | "from") { return true; } // D91 (Plan 21): Channel.new allocates Nova_ChannelState + Sender + Receiver + buf. if ty == "Channel" && matches!(m, "new" | "with_capacity") { return true; } // Map/Set/Vec/Deque etc. `.of` -- variadic literal constructor (D259 amend): // canonical `Vec[T].of(...)` allocates just like `.new`/`.with_capacity`. if matches!(ty, "HashMap" | "Set" | "Vec" | "Deque" | "LinkedList" | "Lru" | "BloomFilter") && matches!(m, "new" | "with_capacity" | "of") { return true; } // str.from: format/conversion may allocate. if ty == "str" && m == "from" { return true; } false } /// Plan 16: registry для capability enforcement. struct CapabilityCtx<'a> { /// Plan 172.1 U.2.3.2: shared base signature registry — replaces the local /// `fn_decls`/`method_table` build (§0 single source). Read via /// `self.sig.fn_decls` / `self.sig.method_table` (base only). D84 overload /// semantics unchanged (capability check walks all overloads). sig: &'a crate::sig_registry::SigRegistry<'a>, /// Effect-type name registry (для distinguish'а effect-call vs ordinary). effect_decls: HashMap<String, &'a TypeDecl>, /// Plan 173.3 (D415 §2): type-decl registry для `#share`-предиката /// (`protocols::share_check::is_mut_alias_safe`/`is_alias_read_safe`) — capture-check на границе /// `spawn`/`parallel for` должен резолвить имя захваченного типа в его /// TypeDecl, чтобы рекурсивно определить share-ность полей. Тот же /// merge-паттерн, что `TypeCheckCtx::build` (module.items + registry-only /// builtin-модули типа sync.nv — иначе `Mutex`/`RwLock`/etc, у которых /// TypeDecl приходит ТОЛЬКО через `builtin_sig_modules()`, не резолвятся). type_decls: HashMap<String, &'a TypeDecl>, /// Plan 238 Ф.1 (D441 §1-2): pre-pass, computed once in `build` — per /// free-fn name, the set of that fn's OWN fn-typed PARAMETER names which /// are invoked somewhere inside a `spawn`/`detach`/`parallel for`/ /// `blocking` body within the fn's own definition (any nesting depth). /// Consulted at each CALL SITE (`check_transitive_closure_arg`) so a /// closure passed BY VALUE into such a parameter gets the same /// share-safety check as if it were inlined directly at the boundary — /// closing the transitive gap `race150/b_closure_bypass.nv` measures /// (closure created outside `spawn`, crosses the fiber boundary as data /// via a plain fn-parameter, invisible to the syntactic-boundary-only /// check). See `spawn_tainted_params_of_fn` for the (documented, /// conservative-UNDER-approximation) AST coverage. spawn_tainted_params: HashMap<String, HashSet<String>>, /// A-V10 (D441 §5 №167 closure): names of free fns declared /// `#thread_affine` directly (`extern` leaves — checker enforces /// `is_external`, see `E_THREAD_AFFINE_NOT_EXTERN`). Base case for the /// `E_THREAD_AFFINE_IN_FIBER` boundary check: calling one of these /// names inside a `spawn`/`detach`/`parallel for` body is a direct /// violation, zero intermediate frames. thread_affine_leaves: HashSet<String>, /// A-V10 (D441 §5 №167 closure): per free-fn name, `Some((leaf, /// first_hop))` when the fn is NOT itself `#thread_affine` but /// transitively (directly or through further named calls) reaches one, /// computed once by `thread_affine_closure` (fixed-point over the /// NAMED free-fn call graph — `own_fiber_call_names`, which is scoped /// to calls made IN THE CALLER'S OWN FIBER: it deliberately stops at /// any nested `spawn`/`detach`/`parallel for`/`blocking` sub-body, /// since those start a fresh, independent fiber decoupled from /// whichever fiber invoked the wrapper — a fn that only ever calls its /// `#thread_affine` leaf INSIDE its own `spawn` does NOT inherit the /// leaf's unsafety). `first_hop` is this fn's own direct callee that /// starts the chain to `leaf` — at least one intermediate frame for the /// diagnostic (D441 §5 №167 wording). thread_affine_transitive: HashMap<String, (String, String)>, /// Plan S1a (D441 §5 "closure-as-field" class — №117/№242§3): pre-pass, /// computed once in `build` — `(TypeName, field)` pairs whose value is /// INVOKED somewhere inside a `spawn`/`detach`/`parallel for`/`blocking` /// body within one of `TypeName`'s OWN methods (any nesting depth, /// through a `for`/`parallel for` loop driving the boundary from a /// direct field-read `@field`). Consulted at each WRITE site into such a /// field (`.push`/`.append`/… mutator call, plain assignment, record /// literal) so a closure value stored there gets the same share-safety /// check as a direct capture — closes the honest gap D441 §5 names /// verbatim ("Класс «замыкание как ПОЛЕ структуры»… НЕ проверяется"). /// See `spawn_tainted_fields_of_module` for the (documented, /// conservative-UNDER-approximation) AST coverage. spawn_tainted_fields: HashSet<(String, String)>, /// Plan S1a (D441 §5 "closure-as-field" — №117 wrapper-method shape, /// the owner's original report `bg.add(|| { log.push(..) })`): one hop /// further than `spawn_tainted_fields` — per `(TypeName, method_name)`, /// which of THAT method's OWN parameter names its body WRITES into an /// already-tainted field (`fn BackgroundTasks mut @add(f fn()->()) { /// @tasks.push(f) }` — `f` is tainted for `(BackgroundTasks, "add")`). /// Consulted at method CALL sites (`obj.method(arg)`) the same way /// `spawn_tainted_params` is consulted for free-fn calls — one /// documented hop, not a fixed-point closure (matches the existing /// honest depth limit D441 §5 already accepts for §3(а)/(в)). spawn_tainted_method_params: HashMap<(String, String), HashSet<String>>, } /// Plan 173.3 (D415 §2): adapter — `CapabilityCtx.type_decls` as a /// `share_check::ShareQuery`. struct CapShareQuery<'a>(&'a HashMap<String, &'a TypeDecl>); impl<'a> crate::protocols::share_check::ShareQuery for CapShareQuery<'a> { fn lookup_type(&self, name: &str) -> Option<&TypeDecl> { self.0.get(name).copied() } } /// Plan 173.3 (D415 §2): one lexical binding tracked for the spawn/ /// parallel-for capture-check — enough fidelity to answer "is this captured /// name a mutable, non-`#share` binding" (fn params / block-scoped `let` / /// for-loop vars; see module-level capture-scan doc for the (documented, /// deliberate) V1 coverage limit — closures/match-arm binds are not tracked). #[derive(Clone)] struct ScopeBinding { mutable: bool, ty: Option<TypeRef>, /// [M-detach-consume-escape-unchecked] (D415 §4 extension, Plan 173.3): /// `true` only for a name bound via an explicit `consume`-marked pattern /// sub-bind (`Ok(consume x)` / `Some(consume x)` — `Pattern::Ident{ /// is_consume: true }`, D157/D180) in a `match`/`if let`/`while let` arm. /// These arrive with `ty: None` (no static annotation — V1 has no /// pattern-destructure type inference), so the ordinary `ty`-based /// `type_decls`-lookup linear check in `check_capture_boundary` can never /// see them; this flag is the ONLY signal that the binding is a linear/ /// consume resource, and is authoritative regardless of `ty`. linear_pattern: bool, /// Plan 238 Ф.1 (D441 §1-2): when this binding's INIT expression is a /// closure literal (`Lambda`/`ClosureLight`/`ClosureFull` — e.g. `ro /// push = || { v.push(1) }`), the literal's OWN free-variable set /// (computed once, via `capture_scan_expr` on the init expr, at /// `Stmt::Let` registration time). `None` for anything else (fn /// PARAMETERS of a fn-type included — opaque at the definition site, /// their concrete captures are only known at each CALL site, see /// `spawn_tainted_params_of_fn`/`check_transitive_closure_arg`). /// /// Closes the №150 Ф.1 transitive gap: a closure created OUTSIDE /// `spawn`/`detach`/`parallel for`, then passed as a plain `Ident` VALUE /// into a call whose callee invokes it inside one of those boundaries, /// carried its captured environment across the fiber boundary /// completely invisibly to the original (direct-boundary-only) /// `check_capture_boundary`. Recording the literal's free vars here lets /// `check_transitive_closure_arg` re-run the SAME share-safety /// resolution against them at the call site, as if the closure body /// were inlined there. closure_free_vars: Option<Vec<String>>, } /// Plan 16: capability state передаётся через walk как mutable. /// Push/pop при входе/выходе из forbid/realtime блоков. #[derive(Default, Clone)] struct CapState { /// Stack forbidden-effects-set'ов от вложенных `forbid` блоков. /// Effect разрешён если он не в **union'е** этих set'ов. /// (Forbid внутри forbid — union, см. D63.) forbidden_stack: Vec<HashSet<String>>, /// True если мы внутри `realtime { ... }` (или `realtime nogc`). /// Suspend-effects (Net/Fs/Db/Time/Blocking) запрещены. realtime_active: bool, /// True если мы внутри `realtime nogc { ... }`. Дополнительно к /// realtime_active запрещены alloc-вызовы. realtime_nogc: bool, /// Stack handlers, установленных через `with X = ... { ... }`. /// Рспользуется для D63 forbid-handler-ban: `with X` внутри /// `forbid X` — compile error. with_handler_stack: Vec<String>, /// Plan 83.3 (D50): имена эффектов, объявленных в сигнатуре /// enclosing-функции. `blocking { }` требует наличия `Blocking` /// в этом наборе. Заполняется один раз при входе в `walk_fn_body`; /// у `test`-блоков остаётся пустым (нет сигнатуры). declared_effects: HashSet<String>, /// Plan 83.3 Ф.6 (D50): True внутри тела `blocking { }`. Тело /// исполняется на libuv-threadpool-потоке без fiber-контекста и /// без GC-регистрации — поэтому проверяется как `nogc` /// (`realtime_nogc` тоже выставляется) + бан suspend-эффектов /// Net/Fs/Db/Time (V1 leaf-контракт). Отдельный флаг, НЕ /// `realtime_active` — иначе вложенный `blocking` отвергался бы /// как «`blocking` внутри `realtime`». blocking_body_active: bool, /// Plan 173 Ф.3 п.2 (D414 §2): True в effect-root контексте (тело /// `test`-блока) — здесь эффект-требования сигнатуры не действуют /// (у теста нет сигнатуры; он — корень, как `main` для скрипта). /// `detach { }` в effect-root разрешён без объявления `Detach` /// (дефолт-handler LogAndDrop — D50/D414 §2). Для обычной `fn` /// остаётся `false` → `detach` требует `Detach` в сигнатуре. effect_root: bool, /// Plan 173.3 (D415 §2): lexical scope stack for the spawn/parallel-for /// capture-check. Frames pushed on fn-param entry / block entry / for-loop /// var entry, popped on exit. `walk_block` is the single choke point for /// ALL block-shaped bodies (if/while/for/match-arm/fn-body/spawn/etc), so /// pushing/popping there gives correct nesting with no per-call-site /// threading. See `capture_scan_*` free functions for the (separate, /// self-contained) free-variable scan run AT a spawn/parallel-for /// boundary against this stack. scopes: Vec<HashMap<String, ScopeBinding>>, /// Plan 173.3 addendum ([M-const-init-concurrency-gate], D414 §2 note): /// True while walking a MODULE-LEVEL `ro`/`const` initializer expression. /// Module-level initializers run inside `nova_consts_init()` BEFORE the /// M:N workers are armed ([M-lazy-const-init-race] eager fix); any /// concurrency construct there would lazily arm workers MID-init /// (`_auto_arm_if_needed()`) — turning the rest of consts-init /// multi-threaded and resurrecting the race. Checker bans: /// `spawn`/`supervised`/`detach`/`parallel for`/`select`/channel /// send-recv ops, and calls to fns whose signature carries the `Detach` /// effect → `E_CONST_INIT_CONCURRENCY`. Ordinary runtime calls (extern /// fns, allocation) stay LEGAL for `ro` (only `const` has the stricter /// E_CONST_EFFECT_IN_INIT purity rule). const_init: bool, /// Plan 221.1 №131 (D62 ENFORCED): `true` when walking an `export fn` /// body — mirrors the `if fd.is_export` gate `check_fn` already applies /// to `E_BANG_REQUIRES_FAIL` (№113). `check_capabilities_at`'s raw /// effect-op check (`[E_RAW_EFFECT_OP_UNDECLARED]`) fires ONLY here; /// private fn (and `main`, which is bare `fn` — not `export fn`, see /// `examples/flagship/aggregator/src/main.nv`) fall through to D28 /// auto-inference (`infer_effects`) instead — same scope split as the /// named-fn transitive class. Left `false` (default) for `Item::Test`/ /// `Item::Let`/`Item::Const` roots — never export, never hard-gated. is_export: bool, /// Plan S1a (D441 §5 "closure-as-field" — №117/№242§3): the receiver /// TYPE NAME of the enclosing method (`fd.receiver.type_name`), `None` /// for a free fn/test/module-level initializer. Lets a WRITE into /// `@field` resolve which type's `spawn_tainted_fields` entry to check /// without re-deriving it from `state.scopes` (self has no scope /// binding — `SelfAccess` is a distinct AST node, not an `Ident`). current_receiver_type: Option<String>, } impl CapState { /// Union forbidden-set'ов всех уровней стека. fn union_forbidden(&self) -> HashSet<String> { let mut out = HashSet::new(); for s in &self.forbidden_stack { out.extend(s.iter().cloned()); } out } } impl<'a> CapabilityCtx<'a> { fn build(module: &'a Module, sig: &'a crate::sig_registry::SigRegistry<'a>) -> Self { // U.2.3.2: fn_decls/method_table read from `sig` (shared base registry). let mut effect_decls: HashMap<String, &TypeDecl> = HashMap::new(); // Plan 173.3 (D415 §2): type_decls for the `#share` predicate — // module-declared types first, then registry-only builtin modules // (sync.nv's Mutex/RwLock/Semaphore/WaitGroup/Once/ReentrantMutex, // `or_insert` so a module-declared type wins), mirroring // `TypeCheckCtx::build`'s merge (§0 single source pattern). let mut type_decls: HashMap<String, &TypeDecl> = HashMap::new(); for item in &module.items { if let Item::Type(t) = item { if matches!(t.kind, TypeDeclKind::Effect(_)) { effect_decls.insert(t.name.clone(), t); } type_decls.insert(t.name.clone(), t); } } for ext_mod in crate::codegen::external_registry::builtin_sig_modules() { for item in &ext_mod.items { if let Item::Type(td) = item { type_decls.entry(td.name.clone()).or_insert(td); } } } // Plan 238 Ф.1 (D441 §1-2): compute the spawn-tainted-parameter // pre-pass over ALL free fns in this compile-unit's module BEFORE // any call-site is checked — a caller earlier in file order than // its callee must still see the callee's taint fact. let mut spawn_tainted_params: HashMap<String, HashSet<String>> = HashMap::new(); for item in &module.items { if let Item::Fn(fd) = item { let tainted = spawn_tainted_params_of_fn(fd); if !tainted.is_empty() { spawn_tainted_params.insert(fd.name.clone(), tainted); } } } // A-V10 (D441 §5 №167 closure): thread-affine leaf/transitive // pre-pass, same "computed once over module.items before any // call-site is checked" timing as `spawn_tainted_params` above. let (thread_affine_leaves, thread_affine_transitive) = thread_affine_closure(module); // Plan S1a (D441 §5 "closure-as-field" — №117/№242§3): same // once-before-any-call-site timing as the two pre-passes above. let spawn_tainted_fields = spawn_tainted_fields_of_module(module); // Plan S1a (D441 §5 "closure-as-field" — №117 wrapper-method shape): // one hop further, DEPENDS on `spawn_tainted_fields` above (computed // first, sequentially — not a fixed point, one documented hop). let spawn_tainted_method_params = spawn_tainted_method_params_of_module(module, &spawn_tainted_fields); CapabilityCtx { sig, effect_decls, type_decls, spawn_tainted_params, thread_affine_leaves, thread_affine_transitive, spawn_tainted_fields, spawn_tainted_method_params, } } fn check_module(&self, module: &Module, errors: &mut Vec<Diagnostic>) { // Plan 42 Sub-plan 42.A: file-level #forbid declarations. // Initial forbidden set из module.attrs (per-file scope). // Все functions в этом file получают эти effects forbidden. let mut file_forbidden: HashSet<String> = HashSet::new(); for attr in &module.attrs { if matches!(attr.kind, crate::ast::ModuleAttrKind::Forbid) { for e in &attr.effects { file_forbidden.insert(e.clone()); } } } for item in &module.items { match item { Item::Fn(f) => { let mut state = CapState::default(); // Plan 221.1 №131 (D62 ENFORCED): raw effect-op hard-gate // scope — see `CapState.is_export` doc. state.is_export = f.is_export; // Plan 42 Sub-plan 42.A: file-level #forbid initial frame. if !file_forbidden.is_empty() { state.forbidden_stack.push(file_forbidden.clone()); } // Plan 16 Ф.5: @realtime атрибут оборачивает body // в realtime[+nogc] контекст. match f.realtime_attr { RealtimeAttr::None => {} RealtimeAttr::Realtime => state.realtime_active = true, RealtimeAttr::RealtimeNogc => { state.realtime_active = true; state.realtime_nogc = true; } } self.walk_fn_body(f, &mut state, errors); } Item::Test(t) => { let mut state = CapState::default(); // Plan 173 Ф.3 п.2: test-блок = effect-root (нет сигнатуры) — // `detach { }` разрешён без объявления `Detach` (дефолт LogAndDrop). state.effect_root = true; if !file_forbidden.is_empty() { state.forbidden_stack.push(file_forbidden.clone()); } self.walk_block(&t.body, &mut state, errors); } // Plan 173.3 addendum ([M-const-init-concurrency-gate]): // module-level `ro` / `const` initializers run inside // nova_consts_init() BEFORE M:N workers arm — concurrency // constructs are banned there (see CapState.const_init doc). Item::Let(d) => { let mut state = CapState::default(); state.const_init = true; if !file_forbidden.is_empty() { state.forbidden_stack.push(file_forbidden.clone()); } state.scopes.push(HashMap::new()); self.walk_expr(&d.value, &mut state, errors); state.scopes.pop(); } Item::Const(c) => { let mut state = CapState::default(); state.const_init = true; if !file_forbidden.is_empty() { state.forbidden_stack.push(file_forbidden.clone()); } state.scopes.push(HashMap::new()); self.walk_expr(&c.value, &mut state, errors); state.scopes.pop(); } _ => {} } } } /// Plan 173.3 addendum ([M-const-init-concurrency-gate]): emit /// `E_CONST_INIT_CONCURRENCY` for a concurrency construct / call inside /// a module-level `ro`/`const` initializer. `what` names the construct. fn const_init_concurrency_error( &self, what: &str, span: Span, errors: &mut Vec<Diagnostic>, ) { errors.push(Diagnostic::new( format!( "[E_CONST_INIT_CONCURRENCY] {} is not allowed in a module-level \ `ro`/`const` initializer — these run inside `nova_consts_init()` \ BEFORE the M:N workers are armed; a concurrency construct here \ would lazily arm workers MID-init and make the remaining \ initializers run multi-threaded (resurrecting the \ [M-lazy-const-init-race] data race). Move the concurrent work \ into a function called from `main`/a test, or compute the value \ sequentially (ordinary runtime/extern calls remain legal in \ `ro` initializers).", what ), span, )); } fn walk_fn_body(&self, f: &FnDecl, state: &mut CapState, errors: &mut Vec<Diagnostic>) { // Plan 83.3 (D50): зафиксировать объявленные эффекты сигнатуры — // `blocking { }` в теле требует среди них `Blocking`. Имя эффекта — // последний segment Named-path (`std.io.Blocking` → `Blocking`). for ef in &f.effects { if let TypeRef::Named { path, .. } = ef { if let Some(last) = path.last() { state.declared_effects.insert(last.clone()); } } } // Plan S1a (D441 §5 "closure-as-field"): record this method's own // receiver type name (`None` for a free fn) — write-site checks // resolve `@field`'s owner from this, `state.scopes` has no entry // for `self` (`SelfAccess` is its own AST node, not an `Ident`). state.current_receiver_type = f.receiver.as_ref().map(|r| r.type_name.clone()); // Plan 173.3 (D415 §2): fn-params scope frame — outermost frame a // spawn/parallel-for capture-check can see a captured name bound in. let mut frame: HashMap<String, ScopeBinding> = HashMap::new(); for p in &f.params { frame.insert(p.name.clone(), ScopeBinding { mutable: p.is_mut, ty: Some(p.ty.clone()), linear_pattern: false, closure_free_vars: None }); } state.scopes.push(frame); match &f.body { FnBody::Expr(e) => self.walk_expr(e, state, errors), FnBody::Block(b) => self.walk_block(b, state, errors), FnBody::External => {} } state.scopes.pop(); } fn walk_block(&self, b: &Block, state: &mut CapState, errors: &mut Vec<Diagnostic>) { // Plan 173.3 (D415 §2): every block is its own lexical scope frame — // pushing/popping HERE (the single choke point for all block-shaped // bodies) gives correct nesting for the spawn/parallel-for // capture-check with no per-call-site threading elsewhere. state.scopes.push(HashMap::new()); for s in &b.stmts { self.walk_stmt(s, state, errors); } if let Some(t) = &b.trailing { self.walk_expr(t, state, errors); } state.scopes.pop(); } fn walk_stmt(&self, s: &Stmt, state: &mut CapState, errors: &mut Vec<Diagnostic>) { match s { Stmt::Expr(e) => self.walk_expr(e, state, errors), Stmt::Let(d) => { self.walk_expr(&d.value, state, errors); // Plan 173.3 (D415 §2): register this let's bound name(s) into // the CURRENT (innermost) scope frame — pushed by the // enclosing `walk_block`, guaranteed non-empty there. When the // let has no annotation, fall back to a SYNTACTIC init-expr // type sketch (ctor calls `Type.new(..)`, literals) so the // dominant unannotated forms — `mut mu = Mutex.new()` (must // NOT be flagged) and `mut acc = 0` (MUST be flagged) — // resolve without full inference (CapabilityCtx has none). // Plan 238 Ф.1 (D441 §1-2): if the init expr is a closure // literal, record its OWN free-variable set now (before it // can be shadowed) — `check_transitive_closure_arg` looks // this up later when the bound name is passed BY VALUE into // a call whose callee invokes it inside a spawn/detach/ // parallel-for/channel-send boundary (the transitive path // repro `race150/b_closure_bypass.nv` exercises: `ro push = // || { v.push(1) }` then `parallel_spawn(push, ..)`). let closure_vars: Option<Vec<String>> = match &d.value.kind { ExprKind::Lambda { .. } | ExprKind::ClosureLight { .. } | ExprKind::ClosureFull(_) => { let mut shadow = HashSet::new(); let mut free = HashSet::new(); capture_scan_expr(&d.value, &mut shadow, &mut free); Some(free.into_iter().collect()) } _ => None, }; let ty = d.ty.clone().or_else(|| { capture_syntactic_init_type(&d.value, &self.type_decls) }); let names = pattern_capture_names(&d.pattern); // №364 (D415 §4 extension, К1 звучность): a single combined // `ty` cloned onto EVERY destructured name can't classify // per-element linearity — `ro (r, w) = s.into_split()` // returning `(TcpReadHalf, TcpWriteHalf)` (BOTH `consume // value`) previously left every bound name with the whole // Tuple TypeRef, which `TypeCheckCtx::typeref_named_base`'s // Named-only unwrap can never resolve to a `type_decls` // entry — the linear check saw NOTHING to classify. When the // pattern is a top-level `Pattern::Tuple` matching the RHS's // tuple SHAPE, zip element i's own TypeRef onto bound name i // instead: either from an explicit tuple annotation, or — // unannotated — from resolving the RHS method-call's // declared return type via `self.sig.method_table` // (`resolve_tuple_call_return`, conservative: `None` on any // ambiguity, whole-`ty` fallback then applies). Computed // BEFORE `state.scopes.last_mut()` below — `resolve_tuple_ // call_return` needs an immutable `&state` (receiver-type // lookup through `state.scopes`), which can't overlap the // frame's mutable borrow. let elem_tys: Option<Vec<TypeRef>> = match &d.pattern { Pattern::Tuple(pats, _) if pats.len() == names.len() => { match &ty { Some(TypeRef::Tuple(elems, _)) if elems.len() == pats.len() => { Some(elems.clone()) } None => self .resolve_tuple_call_return(&d.value, state) .filter(|v| v.len() == pats.len()), _ => None, } } _ => None, }; if let Some(frame) = state.scopes.last_mut() { for (i, (name, pat_mut, pat_consume)) in names.into_iter().enumerate() { let elem_ty = elem_tys.as_ref().map(|v| v[i].clone()).or_else(|| ty.clone()); frame.insert(name, ScopeBinding { mutable: d.mutable || pat_mut, ty: elem_ty, // №364: `consume lst = expr` (no static // annotation, `d.consume` — the LetDecl-level // explicit-`consume` keyword, D133) is just as // authoritative a linear signal as a pattern // sub-bind's own `is_consume` (the existing // `pattern_linear_flagged` rationale above // applies verbatim: source syntax already marked // the bind `consume`). `pat_consume` alone missed // this — it's set only for `Ok(consume x)`-style // pattern sub-binds, never for the LetDecl's own // `consume` keyword on a plain `Pattern::Ident`. linear_pattern: pat_consume || d.consume, closure_free_vars: closure_vars.clone(), }); } } } Stmt::Const(_) => {} Stmt::Assign { target, value, .. } => { self.walk_expr(target, state, errors); self.walk_expr(value, state, errors); // Plan S1a (D441 §5 "closure-as-field", №117/№242§3): plain // assignment into a spawn-tainted field (`@field = v` / // `obj.field = v`) — same crossing-point machine as the // mutator-call and record-lit write sites below. if let ExprKind::Member { obj, name: field } = &target.kind { if let Some(owner) = self.resolve_field_owner_type(obj, state) { if self.spawn_tainted_fields.contains(&(owner.clone(), field.clone())) { self.check_field_sink_write(value, &owner, field, state, errors); } } } } Stmt::Return { value, .. } => { if let Some(v) = value { self.walk_expr(v, state, errors); } } Stmt::Throw { value, .. } => self.walk_expr(value, state, errors), Stmt::Break(_) | Stmt::Continue(_) => {} // D90 Plan 20 Ф.2: проверяем capability'и внутри body // defer'а. Полные constraints (no Fail/suspend/exit-control) // — Ф.3. Stmt::Defer { body, .. } => { self.walk_expr(body, state, errors); } // Plan 110 D188: walk init + body (scaffold). Stmt::ConsumeScope { init, body, .. } => { self.walk_expr(init, state, errors); for s in &body.stmts { self.walk_stmt(s, state, errors); } if let Some(t) = &body.trailing { self.walk_expr(t, state, errors); } } // Plan 33.2 Ф.8: assert_static — walk expr. Stmt::AssertStatic { expr, .. } | Stmt::Assume { expr, .. } => self.walk_expr(expr, state, errors), // Ф.4.1: apply — ghost, нет capability-эффектов. Stmt::Apply { .. } => {} // Ф.4.2: calc — ghost, нет capability-эффектов. Stmt::Calc { .. } => {} // Plan 33.9 Ф.2: reveal — ghost, нет capability-эффектов. Stmt::Reveal { .. } => {} // Plan 136: tuple destructuring assignment. Stmt::TupleAssign { lhs, rhs, .. } => { for e in lhs { self.walk_expr(e, state, errors); } for e in rhs { self.walk_expr(e, state, errors); } } } } fn walk_expr(&self, e: &Expr, state: &mut CapState, errors: &mut Vec<Diagnostic>) { // Сначала проверяем сам узел (call-bound checks), потом // погружаемся внутрь с обновлённым state'ом для блочных // конструкций (forbid/realtime/with). self.check_capabilities_at(e, state, errors); match &e.kind { ExprKind::Forbid { effects, body } => { // Push forbidden-set, walk, pop. let names: HashSet<String> = effects.iter() .filter_map(|t| match t { TypeRef::Named { path, .. } if path.len() == 1 => Some(path[0].clone()), _ => None, }) .collect(); state.forbidden_stack.push(names); self.walk_block(body, state, errors); state.forbidden_stack.pop(); } ExprKind::Realtime { nogc, body } => { let prev_active = state.realtime_active; let prev_nogc = state.realtime_nogc; state.realtime_active = true; state.realtime_nogc = state.realtime_nogc || *nogc; self.walk_block(body, state, errors); state.realtime_active = prev_active; state.realtime_nogc = prev_nogc; } ExprKind::With { bindings, body } => { // Plan 16 D63: установка handler'а для forbidden-эффекта // внутри forbid-блока — compile error. // // WithBinding.effect: TypeRef. Для названия эффекта // берём последний segment Named-path (e.g. `std.io.Net` // → "Net"). Non-Named TypeRefs (Array/Tuple/Func/etc.) — // невалидны для эффект-handler'ов, пропускаем. let pushed: Vec<String> = bindings.iter() .filter_map(|b| match &b.effect { TypeRef::Named { path, .. } if !path.is_empty() => path.last().cloned(), _ => None, }) .collect(); let forbidden = state.union_forbidden(); for n in &pushed { if forbidden.contains(n) { errors.push(Diagnostic::new( format!( "cannot install handler for `{}` inside `forbid {}` block (D63): \ forbid is impenetrable — code in body cannot escape sandbox \ via `with X = …`.", n, n ), e.span, )); } state.with_handler_stack.push(n.clone()); } // Plan 238 Ф.3 (D441 §3): a handler installed around a // fiber-containing body executes IN THE FIBER of the // failing/child operation (Ф.0 measurement) — mut-captures // there are the same race as a direct spawn-body capture. // D416§2 carve-out for `Supervisor` REVOKED (2026-07-31, // acceptance of A-V10): the serialization pin fixture // FAILED 5/10 isolated runs — the documented drive-fiber // serialization guarantee is NOT upheld by the runtime // (registry №173, M:N window with №165/№169). Until the // runtime actually serializes, a Supervisor handler's // mut-capture is the same race as any other handler's. if block_contains_fiber_boundary(body) { for b in bindings { self.check_handler_capture(&b.handler, state, errors); } } self.walk_block(body, state, errors); for _ in &pushed { state.with_handler_stack.pop(); } } ExprKind::Call { func, args, trailing } => { // Plan 173.3 addendum ([M-const-init-concurrency-gate]): // inside a module-level ro/const initializer, ban // (a) channel blocking ops by method name — `send`/`recv` // (buffered `send` PARKS when full; `recv` parks when // empty — with no workers armed that's a pre-main hang; // documented name-heuristic, matching D91 surface); // (b) calls to free fns whose signature carries the `Detach` // effect (the call would detach a fiber mid-consts-init). // Non-parking `try_send`/`try_recv` and ordinary runtime / // extern calls remain legal (`ro` init is allowed effects). if state.const_init { match &func.kind { ExprKind::Member { name, .. } if name == "send" || name == "recv" => { self.const_init_concurrency_error( &format!("channel op `.{}()`", name), e.span, errors, ); } ExprKind::Ident(fname) => { let has_detach = self .sig .free_fns(fname) .map_or(false, |fns| { fns.iter().any(|f| { f.effects.iter().any(|ef| matches!( ef, TypeRef::Named { path, .. } if path.last().map(String::as_str) == Some("Detach") )) }) }); if has_detach { self.const_init_concurrency_error( &format!( "call to `{}` (declares the `Detach` effect)", fname ), e.span, errors, ); } } _ => {} } } // Plan 238 Ф.1(а) (D441 §1-2): transitive parameter-crossing // — this call's callee may invoke ONE OF ITS OWN fn-typed // parameters inside `spawn`/`detach`/`parallel for`/ // `blocking` (`spawn_tainted_params`, precomputed pre-pass). // If the ARGUMENT at that position is a closure we can // resolve (literal, or a plain name snapshotted at its // `let`), check its captures for share-safety exactly as if // it were inlined at the boundary — closes the // `race150/b_closure_bypass.nv` gap (closure created // outside `spawn`, crosses as data via a fn-parameter). if let ExprKind::Ident(callee_name) = &func.kind { if let Some(tainted) = self.spawn_tainted_params.get(callee_name) { if let Some(fdecls) = self.sig.free_fns(callee_name) { // D84 overloads: V1 does not disambiguate — match // by arity (conservative: the common case is a // single free-fn definition per name anyway). if let Some(fd) = fdecls.iter().find(|f| f.params.len() == args.len()) { for (i, p) in fd.params.iter().enumerate() { if !tainted.contains(&p.name) { continue; } if let Some(arg) = args.get(i) { self.check_transitive_closure_arg( arg.expr(), callee_name, &p.name, state, errors, ); } } } } } } // Plan S1a (D441 §5 "closure-as-field" — №117 wrapper-method // shape, owner's original report `bg.add(|| { log.push(..) // })`): method-call twin of the free-fn check above — a // METHOD call `obj.method(arg)` parses as `Call{func: // Member{obj, name: method}, ..}`, invisible to the // `Ident(callee_name)` arm above (which only matches // free-fn-call syntax). `method`'s OWN parameter may be // WRITTEN (not called) into an already-tainted field inside // `method`'s body — `spawn_tainted_method_params` records // that one hop. // // p-s1a2 (№289): a STATIC-receiver call — `Holder.new(..)`, // no bound instance — does NOT parse as `Member{obj, name}` // at all: the grammar gives `Type.method` its own node, // `ExprKind::Path(vec![Type, method])` (see `capture_ // syntactic_init_type`'s existing `Path` arm, which already // special-cases exactly this for the SAME reason). Every // idiomatic `.new()`/`.of()` ctor call is spelled this way, // so it needed its OWN owner/method resolution branch here — // the `Member` arm alone silently never fired for it. let member_owner_method: Option<(String, &String)> = match &func.kind { ExprKind::Member { obj, name: method } => { self.resolve_field_owner_type(obj, state).map(|owner| (owner, method)) } ExprKind::Path(parts) if parts.len() == 2 => { Some((parts[0].clone(), &parts[1])) } _ => None, }; if let Some((owner, method)) = member_owner_method { if let Some(tainted) = self.spawn_tainted_method_params.get(&(owner.clone(), method.clone())) { if let Some(overloads) = self.sig.method_overloads(&owner, method) { // D84 overloads: V1 does not disambiguate — match // by arity, same conservative stance as the // free-fn arm above. if let Some(fd) = overloads.iter().find(|f| f.params.len() == args.len()) { for (i, p) in fd.params.iter().enumerate() { if !tainted.contains(&p.name) { continue; } if let Some(arg) = args.get(i) { self.check_field_write_via_method_param( arg.expr(), &owner, method, &p.name, state, errors, ); } } } } } } // Plan 238 Ф.1(б) (D441 §1-2): channel-send of a closure — // `chan.send(value)` name-heuristic (same approximation // `state.const_init` already uses above for `send`/`recv`: // no static Channel-type proof, matched by method name). // The receiving end may run in a different, concurrent // fiber by construction, so a closure value crossing here // gets the identical treatment as the direct-boundary case. if let ExprKind::Member { name, .. } = &func.kind { if name == "send" { if let Some(value_arg) = args.first() { let mut shadow = HashSet::new(); let mut free = HashSet::new(); let free_opt = match &value_arg.expr().kind { ExprKind::Lambda { .. } | ExprKind::ClosureLight { .. } | ExprKind::ClosureFull(_) => { capture_scan_expr(value_arg.expr(), &mut shadow, &mut free); Some(free) } ExprKind::Ident(vname) => state.scopes.iter().rev() .find_map(|f| f.get(vname)) .and_then(|b| b.closure_free_vars.as_ref()) .map(|v| v.iter().cloned().collect()), _ => None, }; if let Some(free) = free_opt { self.flag_boundary_captures( free, value_arg.expr().span, state, "sent into a channel (D441 §1(б) — the receiving \ end may run in a different, concurrent fiber)", "E_CONCURRENT_MUT_CAPTURE", errors, ); } } } } // Plan S1a (D441 §5 "closure-as-field", №117/№242§3): write // into a spawn-tainted field's CONTAINER via a known // mutator method name — `@tasks.push(f)` / // `@routes.insert(path, handler)` etc. Same name-heuristic // approximation the `chan.send` check above already uses // (no static container-type proof — matches project // convention, D441 §3(б) doc comment). if let ExprKind::Member { obj: recv_expr, name: method } = &func.kind { const FIELD_SINK_MUTATORS: &[&str] = &["push", "append", "add", "insert", "push_back", "push_front", "set"]; if FIELD_SINK_MUTATORS.contains(&method.as_str()) { if let ExprKind::Member { obj, name: field } = &recv_expr.kind { if let Some(owner) = self.resolve_field_owner_type(obj, state) { if self.spawn_tainted_fields.contains(&(owner.clone(), field.clone())) { if let Some(value_arg) = args.last() { self.check_field_sink_write( value_arg.expr(), &owner, field, state, errors, ); } } } } } } self.walk_expr(func, state, errors); for a in args { self.walk_expr(a.expr(), state, errors); } if let Some(t) = trailing { match t { crate::ast::Trailing::Block(b) => self.walk_block(b, state, errors), crate::ast::Trailing::LegacyBlockWithParams(tb) => { self.walk_block(&tb.body, state, errors) } crate::ast::Trailing::Fn(sb) => match &sb.body { FnBody::Expr(e) => self.walk_expr(e, state, errors), FnBody::Block(b) => self.walk_block(b, state, errors), FnBody::External => {} }, } } } ExprKind::TurboFish { base, .. } => self.walk_expr(base, state, errors), ExprKind::Binary { left, right, .. } => { self.walk_expr(left, state, errors); self.walk_expr(right, state, errors); } ExprKind::Unary { operand, .. } => self.walk_expr(operand, state, errors), ExprKind::Try(inner) | ExprKind::Bang(inner) | ExprKind::RefArg(inner) => self.walk_expr(inner, state, errors), ExprKind::Coalesce(a, b) => { self.walk_expr(a, state, errors); self.walk_expr(b, state, errors); } ExprKind::As(e, _) => self.walk_expr(e, state, errors), ExprKind::Is(e, _) => self.walk_expr(e, state, errors), ExprKind::Member { obj, .. } => self.walk_expr(obj, state, errors), ExprKind::Index { obj, index } => { self.walk_expr(obj, state, errors); self.walk_expr(index, state, errors); } ExprKind::If { cond, then, else_ } => { self.walk_expr(cond, state, errors); self.walk_block(then, state, errors); if let Some(eb) = else_ { match eb { ElseBranch::Block(b) => self.walk_block(b, state, errors), ElseBranch::If(e) => self.walk_expr(e, state, errors), } } } ExprKind::IfLet { scrutinee, pattern, guard, then, else_ } => { self.walk_expr(scrutinee, state, errors); // [M-detach-consume-escape-unchecked] (D415 §4 extension): // register the pattern's bound names — including explicit // `consume` sub-binds (`if let Ok(consume x) = … { … }`) — // into a scope frame BEFORE walking guard/then, so a nested // spawn/parallel-for/detach inside `then` that captures `x` // can resolve it via `state.scopes` (was previously // invisible — no frame was ever pushed here). let mut frame: HashMap<String, ScopeBinding> = HashMap::new(); for (name, is_mut, is_consume) in pattern_capture_names(pattern) { frame.insert(name, ScopeBinding { mutable: is_mut, ty: None, linear_pattern: is_consume, closure_free_vars: None }); } state.scopes.push(frame); if let Some(g) = guard { self.walk_expr(g, state, errors); } self.walk_block(then, state, errors); state.scopes.pop(); if let Some(eb) = else_ { match eb { ElseBranch::Block(b) => self.walk_block(b, state, errors), ElseBranch::If(e) => self.walk_expr(e, state, errors), } } } ExprKind::Match { scrutinee, arms } => { self.walk_expr(scrutinee, state, errors); for arm in arms { // [M-detach-consume-escape-unchecked] (D415 §4 extension): // same as `IfLet` above — a match-arm's own pattern binds // (e.g. `Ok(consume stream) => { detach { …stream… } }`, // the exact flagship-found gap) were entirely invisible to // `state.scopes` before this fix; register them here so // the capture-check boundary (`check_capture_boundary`, // called from `spawn`/`parallel for`/`detach` arms) can // see them. let mut frame: HashMap<String, ScopeBinding> = HashMap::new(); for (name, is_mut, is_consume) in pattern_capture_names(&arm.pattern) { frame.insert(name, ScopeBinding { mutable: is_mut, ty: None, linear_pattern: is_consume, closure_free_vars: None }); } state.scopes.push(frame); if let Some(g) = &arm.guard { self.walk_expr(g, state, errors); } match &arm.body { MatchArmBody::Expr(e) => self.walk_expr(e, state, errors), MatchArmBody::Block(b) => self.walk_block(b, state, errors), } state.scopes.pop(); } } ExprKind::Block(b) => self.walk_block(b, state, errors), ExprKind::ArrayLit(elems) => { for el in elems { match el { ArrayElem::Item(e) | ArrayElem::Spread(e) => self.walk_expr(e, state, errors), } } } ExprKind::MapLit { elems, .. } => { let pairs = crate::ast::MapElem::cloned_pairs(&elems); for (k, v) in pairs.iter() { self.walk_expr(k, state, errors); self.walk_expr(v, state, errors); } } ExprKind::TupleLit(elems) => { for e in elems { self.walk_expr(e, state, errors); } } ExprKind::RecordLit { fields, type_name, .. } => { // Plan S1a (D441 §5 "closure-as-field", №117/№242§3): a // constructor literal writing directly into a spawn-tainted // field (`BackgroundTasks { tasks: [f], .. }`) is the same // write-site class as the mutator-call/assignment checks // above — `type_name` is resolved statically here (record // literal, no scope lookup needed). // p-s1a2 (№289): a BARE record literal (no explicit // type-name prefix — `{ f }`, not `Holder { f }`) is the // overwhelmingly idiomatic `.new()`/ctor body style across // this corpus; its `type_name` is `None`, with the type // established purely by context (declared return type / // receiver). No general type inference is available here, // so fall back to `state.current_receiver_type` — the // enclosing method's OWN receiver, which for a ctor-style // method IS the constructed type in the common case. Same // fallback + rationale as the taint-scan twin // (`field_write_param_scan_expr`'s `RecordLit` arm). let owner: Option<String> = type_name.as_ref() .and_then(|p| p.last().cloned()) .or_else(|| state.current_receiver_type.clone()); let owner = owner.as_deref(); for f in fields { // p-s1a2 (№289): D52 field-punning shorthand `{ name }` // parses with `value: None` (MANDATORY spelling when the // field name matches its source — D52 §2, see the // `RecordLitField.value` doc; the explicit `{ name: name // }` spelling is itself a hard compile error). Without // this synthesized stand-in, a taint-sink ctor written // the only legal way (`fn Holder.new(f fn()->()) -> // Holder => { f }`) was invisible to this write-site // check — the whole `if let Some(v)` arm below silently // skipped every shorthand field. let shorthand_ident = f.value.is_none() .then(|| Expr::new(ExprKind::Ident(f.name.clone()), f.span)); let v: Option<&Expr> = f.value.as_ref().or(shorthand_ident.as_ref()); if let Some(v) = v { if let Some(owner) = owner { if self.spawn_tainted_fields.contains(&(owner.to_string(), f.name.clone())) { self.check_field_sink_write(v, owner, &f.name, state, errors); } } self.walk_expr(v, state, errors); } } } ExprKind::TaggedTemplate { tag, args, .. } => { self.walk_expr(tag, state, errors); for a in args { self.walk_expr(a, state, errors); } } ExprKind::InterpolatedStr { parts } => { for p in parts { if let InterpStrPart::Expr { expr: e, spec: _ } = p { self.walk_expr(e, state, errors); } } } ExprKind::Lambda { body, .. } => self.walk_expr(body, state, errors), // Plan 19, C5: CapabilityCtx обходит тело closure для // forbid/realtime проверок (D63/D64). Closure-light и // closure-full одинаково — walk by body kind. ExprKind::ClosureLight { body, .. } => match body { crate::ast::ClosureBody::Expr(e) => self.walk_expr(e, state, errors), crate::ast::ClosureBody::Block(b) => self.walk_block(b, state, errors), }, ExprKind::ClosureFull(sb) => match &sb.body { FnBody::Expr(e) => self.walk_expr(e, state, errors), FnBody::Block(b) => self.walk_block(b, state, errors), FnBody::External => {} }, ExprKind::Spawn(body) => { // Plan 173.3 addendum ([M-const-init-concurrency-gate]). if state.const_init { self.const_init_concurrency_error("`spawn`", e.span, errors); } // Plan 173.3 (D415 §2): capture-check boundary — `spawn`'s // body executes in a NEW fiber (M:N, may run on another OS // thread). Any outer `mut` binding captured by reference // (non-`#share`, non-consumed) is a data race. Scan BEFORE // walking (so the check sees only OUTER scopes, not body's // own locals — `walk_expr`/`walk_block` push body's frames). self.check_spawn_capture(body, state, errors); self.walk_expr(body, state, errors); } ExprKind::Detach(body) => { // Plan 173.3 addendum ([M-const-init-concurrency-gate]). if state.const_init { self.const_init_concurrency_error("`detach`", e.span, errors); } // Plan 173 Ф.3 п.2 (D50 / D414 §2): `detach { }` требует эффект // `Detach` в сигнатуре enclosing-`fn` (или замыкания с эффектами) — // fire-and-forget задача переживает caller'а, это наблюдаемый // capability. Разрешено без объявления когда: (a) effect-root // (test-блок — нет сигнатуры), (b) ambient `with Detach = …` // handler в scope (эффект уже погашен на месте). Тела // handler-op-литералов (`effect X { op(){ detach … } }`) сюда не // доходят — capability-walker их не обходит (эффект-полиморфны, // погашаются на use-site handler'а; HOF-эффект-полиморфизм — вне // периметра 173, см. хаб НЕ-цели). if !state.effect_root && !state.declared_effects.contains("Detach") && !state.with_handler_stack.iter().any(|h| h == "Detach") { errors.push(Diagnostic::new( "`detach { }` requires the `Detach` effect in the enclosing \ function signature (D50): a detached task outlives its caller — \ declare `Detach` (e.g. `fn f() Detach -> ()`), or install a \ `with Detach = …` handler in scope. [E_DETACH_REQUIRES_EFFECT]" .to_string(), e.span, )); } // Plan 173.3 (D415 §2 amendment, owner P1 2026-07-11): `detach { }` // is JUST as concurrent as `spawn` — an orphan fiber on the worker // pool, fire-and-forget, no join with the parent. The original // Ф.2 sweep only wired the check into `spawn`/`parallel for`; // `detach` capturing an outer `mut` (non-`#share`) by reference // is the identical data race (parent/siblings may run concurrently // with, or migrate across threads from, the orphan) and was a // silent gap. Same capture-check boundary as `spawn`. self.check_capture_boundary(body, state, errors); // A-V10 (D441 §5 №167 closure): thread-affine boundary // check — same class as `spawn`, `detach` is just as much // a fresh, possibly-different-OS-thread fiber. self.check_thread_affine_boundary(body, errors); self.walk_block(body, state, errors); } ExprKind::Blocking(body) => { // Plan 91.15 (D172): the `blocking { }` block-form was retracted // by Plan 113. The parser now rejects `blocking { ... }` outright // (parser::parse_blocking → `[D172-block-form-removed]`), so this // AST arm is unreachable in practice. The variant is retained only // to keep the AST enum stable; walk the body defensively without // requiring the removed `Blocking` effect. // // Plan 173.3 (D415 §2 amendment, owner P1 2026-07-11): investigated // and confirmed no capture-check belongs here. The LIVE replacement // (`#blocking fn`, Plan 113) is a top-level `fn`-item attribute // (parser::parse_blocking_attr, gated on a preceding `fn`), never a // closure/lambda literal — Nova `fn` items do not close over the // caller's lexical scope, so a `#blocking fn` body has no capture // surface at all (own fresh parameter scope, not nested in the // caller's `state.scopes`). This dead `ExprKind::Blocking(Block)` // arm is itself a vestige of the incomplete D172 retraction — // tracked for removal under [M-dead-exprkind-blocking-vestigial] // (docs/plans/README.md), not touched here to keep this wave // focused on the `detach` gap. self.walk_block(body, state, errors); } ExprKind::Supervised { body, cancel, deadline, on_timeout } => { // Plan 173.3 addendum ([M-const-init-concurrency-gate]). if state.const_init { self.const_init_concurrency_error("`supervised { }`", e.span, errors); } if let Some(c) = cancel { self.walk_expr(c, state, errors); } if let Some(_dl) = deadline { self.walk_expr(&_dl.expr, state, errors); } if let Some(oh) = on_timeout { self.walk_expr(oh, state, errors); } self.walk_block(body, state, errors); } ExprKind::ParallelFor { iter, body, pattern, elem_type } => { // Plan 173.3 addendum ([M-const-init-concurrency-gate]). if state.const_init { self.const_init_concurrency_error("`parallel for`", e.span, errors); } self.walk_expr(iter, state, errors); // Plan 173.3 (D415 §2): capture-check boundary — same fan-out // fiber-per-element concurrency as `spawn` (D50 desugar: // `parallel for` = `supervised { for x in iter { spawn { body } } }`). // Push the loop-var frame FIRST so the loop variable itself is // never flagged as a "captured outer mut" (it's per-iteration // local, not shared across siblings). let mut frame: HashMap<String, ScopeBinding> = HashMap::new(); for (name, is_mut, is_consume) in pattern_capture_names(pattern) { frame.insert(name, ScopeBinding { mutable: is_mut, ty: elem_type.clone(), linear_pattern: is_consume, closure_free_vars: None }); } state.scopes.push(frame); self.check_capture_boundary(body, state, errors); // A-V10 (D441 §5 №167 closure): thread-affine boundary // check — `parallel for`'s per-element body is the same // fan-out fiber-per-element concurrency as `spawn`. self.check_thread_affine_boundary(body, errors); for s in &body.stmts { self.walk_stmt(s, state, errors); } if let Some(t) = &body.trailing { self.walk_expr(t, state, errors); } state.scopes.pop(); } ExprKind::For { iter, body, pattern, elem_type, .. } => { self.walk_expr(iter, state, errors); let mut frame: HashMap<String, ScopeBinding> = HashMap::new(); for (name, is_mut, is_consume) in pattern_capture_names(pattern) { frame.insert(name, ScopeBinding { mutable: is_mut, ty: elem_type.clone(), linear_pattern: is_consume, closure_free_vars: None }); } state.scopes.push(frame); for s in &body.stmts { self.walk_stmt(s, state, errors); } if let Some(t) = &body.trailing { self.walk_expr(t, state, errors); } state.scopes.pop(); } ExprKind::While { cond, body, .. } => { self.walk_expr(cond, state, errors); self.walk_block(body, state, errors); } ExprKind::WhileLet { scrutinee, pattern, guard, body, .. } => { self.walk_expr(scrutinee, state, errors); // [M-detach-consume-escape-unchecked] (D415 §4 extension): // same fix as `IfLet`/`Match` above — `while let Ok(consume // x) = … { … }` binds `x` for the loop body; make it visible // to `state.scopes` so a nested spawn/parallel-for/detach // capturing it is checked. let mut frame: HashMap<String, ScopeBinding> = HashMap::new(); for (name, is_mut, is_consume) in pattern_capture_names(pattern) { frame.insert(name, ScopeBinding { mutable: is_mut, ty: None, linear_pattern: is_consume, closure_free_vars: None }); } state.scopes.push(frame); if let Some(g) = guard { self.walk_expr(g, state, errors); } self.walk_block(body, state, errors); state.scopes.pop(); } ExprKind::Loop { body, .. } => self.walk_block(body, state, errors), ExprKind::Select { arms } => { // Plan 173.3 addendum ([M-const-init-concurrency-gate]). if state.const_init { self.const_init_concurrency_error("`select { }`", e.span, errors); } for arm in arms { match &arm.op { SelectOp::Recv { chan, .. } => self.walk_expr(chan, state, errors), SelectOp::Send { chan, value } => { self.walk_expr(chan, state, errors); self.walk_expr(value, state, errors); } SelectOp::Default => {} } if let Some(g) = &arm.guard { self.walk_expr(g, state, errors); } self.walk_block(&arm.body, state, errors); } } ExprKind::Range { start, end, .. } => { if let Some(s) = start { self.walk_expr(s, state, errors); } if let Some(e) = end { self.walk_expr(e, state, errors); } } ExprKind::Throw(e) => self.walk_expr(e, state, errors), ExprKind::Interrupt(opt) => { if let Some(e) = opt { self.walk_expr(e, state, errors); } } // [E_COALESCE_RETURN_FALLBACK]: checker-rejected before this pass. ExprKind::CoalesceReturnFallback(opt) => { if let Some(e) = opt { self.walk_expr(e, state, errors); } } // D.1.3: квантор — только в контрактах; обходим range и body. ExprKind::Forall { range, body, .. } | ExprKind::Exists { range, body, .. } => { self.walk_expr(range, state, errors); self.walk_expr(body, state, errors); } // Литералы / ident'ы / handler-литералы — без рекурсии. ExprKind::IntLit(_) | ExprKind::FloatLit(_) | ExprKind::BoolLit(_) | ExprKind::StrLit(_) | ExprKind::CharLit(_) | ExprKind::UnitLit | ExprKind::HexBlobLit(_) | ExprKind::NullPtrLit | ExprKind::Ident(_) | ExprKind::Path(_) | ExprKind::SelfAccess | ExprKind::HandlerLit { .. } | ExprKind::ProtocolLit { .. } => {} } } /// Plan 16 Ф.2-Ф.4: проверка capability-rules на конкретном узле. /// Сейчас — только для Call'ов; forbid/realtime/with управляют /// state'ом, не вызывая check'ов на собственном узле. fn check_capabilities_at(&self, e: &Expr, state: &CapState, errors: &mut Vec<Diagnostic>) { let ExprKind::Call { func, .. } = &e.kind else { return; }; // Path-form: `Type.method`, `Effect.op` или `[]T.method`. // Для `[]T.method()` парсер строит Member{obj: Path(["__array", T]), name}. let path: Vec<String> = match &func.kind { ExprKind::Path(parts) => parts.clone(), ExprKind::Member { obj, name } => { // #273: peel off an explicit generic-application (turbofish) // wrapper on the receiver first. `Vec[int].new()` parses as // `Member{obj: TurboFish{base: Ident("Vec"), ..}, name: "new"}` // (D38) — before this fix, `TurboFish` fell straight into the // `_ => return` catch-all below, so EVERY capability check in // this function (nogc-alloc blacklist, `forbid`, effect-row) // was silently skipped for any call whose receiver carries an // explicit generic type argument: `Vec[int].*`, `HashMap[K, // V].*`, `OnceCell[int].*`, etc. — not just the `#realtime // nogc fn` gap this was found through (D172). let obj_kind = match &obj.kind { ExprKind::TurboFish { base, .. } => &base.kind, other => other, }; match obj_kind { ExprKind::Ident(n) => vec![n.clone(), name.clone()], // `[]T.method`: Path(["__array","T"]) → ["[]T", method]. ExprKind::Path(parts) if parts.len() == 2 && parts[0] == "__array" => { vec![format!("[]{}", parts[1]), name.clone()] } ExprKind::Path(parts) => { let mut v = parts.clone(); v.push(name.clone()); v } _ => return, // dynamic member-call; не resolve'им } } ExprKind::Ident(n) => vec![n.clone()], _ => return, }; // 1. Effect-op call: `Effect.op(...)` где Effect — registered effect-type. if path.len() == 2 { let head = &path[0]; if self.effect_decls.contains_key(head) { self.check_forbid_intersection(head, state, e.span, errors); if state.realtime_active && realtime_suspend_effect(head) { errors.push(Diagnostic::new( format!( "cannot use suspend-effect `{}` inside `realtime` block (D64): \ {}.{} may suspend the fiber. Hint: extract the effectful work \ out of `realtime` block, or use non-blocking alternative \ (e.g. `Channel.try_recv` instead of `Channel.recv`).", head, head, &path[1] ), e.span, )); } // Plan 83.3 Ф.6: тело `blocking { }` идёт на threadpool-потоке // без fiber/event-loop — async-I/O-эффекты там сломаны. if state.blocking_body_active && blocking_body_forbidden_effect(head) { errors.push(Diagnostic::new( format!( "cannot use suspend-effect `{}` inside `blocking {{ ... }}` body \ (Plan 83.3 V1 leaf-contract, D50 §4): {}.{} needs the \ fiber/event-loop context, which the libuv threadpool thread \ does not have. Hint: `blocking` is for genuinely-blocking C \ calls — do async I/O outside the `blocking` block.", head, head, &path[1] ), e.span, )); } self.check_raw_effect_op_declared(head, &path[1], state, e.span, errors); } } // 2. Free-fn call: lookup callee.effects. // D84: fn_decls — Vec<&FnDecl>. Без полного type-resolve в // bound-checker'е невозможно выбрать конкретную overload — // проверяем эффекты у **всех** overloads (consistent с тем что // делает method_table-ветка ниже). False-positive если разные // overloads имеют разные эффекты — в реальных API маловероятно // (overloads обычно отличаются типом аргумента, не эффектами), // но если случится — программист дисамбигуирует через cast. if path.len() == 1 { if let Some(overloads) = self.sig.fn_decls.get(&path[0]) { for callee in overloads.iter() { self.check_callee_effects(callee, &path[0], state, e.span, errors); } } } // 3. Method call: `Type.method` или `obj.method` — lookup в method_table. // (Только receiver-Path формы; instance-method через obj.method // требует type-инференции, отложен.) if path.len() == 2 { if let Some(methods) = self.sig.method_table.get(&path[0]) { if let Some(fns) = methods.get(&path[1]) { for callee in fns { self.check_callee_effects(callee, &format!("{}.{}", path[0], path[1]), state, e.span, errors); } } } } // 4. Plan 16 Ф.4: nogc alloc-fn check. // Plan 83.3 Ф.6: тело `blocking { }` тоже nogc (threadpool-поток // не GC-registered) — context-aware сообщение. if state.realtime_nogc && nogc_blacklisted_call(&path) { if state.blocking_body_active && !state.realtime_active { errors.push(Diagnostic::new( format!( "[E_BLOCKING_NOGC_ALLOC] cannot allocate inside `blocking {{ ... }}` \ body (Plan 83.3 V1 leaf-contract, D50 §4): `{}` allocates on the \ managed heap, but the body runs on a libuv threadpool thread that \ is not GC-registered. Hint: move the allocation outside the \ `blocking` block.", path.join(".") ), e.span, )); } else { errors.push(Diagnostic::new( format!( "[E_REALTIME_NOGC_ALLOC] cannot allocate inside `#realtime nogc fn` \ (D172 §nogc, historically D64): `{}` allocates on the managed \ heap. Hint: move the allocation out of the `#realtime nogc` \ function (call it from an ordinary caller and pass the result \ in), or drop `nogc` if a GC pause is acceptable here.", path.join(".") ), e.span, )); } } } /// Plan 16 Ф.2: проверка пересечения callee.effects с union forbidden-стека. fn check_callee_effects( &self, callee: &FnDecl, callee_label: &str, state: &CapState, span: Span, errors: &mut Vec<Diagnostic>, ) { // Pure — всегда OK. if callee.effects.is_empty() && state.forbidden_stack.is_empty() && !state.realtime_active { return; } let forbidden = state.union_forbidden(); for eff in &callee.effects { let TypeRef::Named { path, .. } = eff else { continue; }; if path.is_empty() { continue; } let name = &path[0]; // Forbid check. if forbidden.contains(name) { errors.push(Diagnostic::new( format!( "function `{}` requires effect `{}`, forbidden by enclosing \ `forbid {}` block (D63). Hint: pure code inside `forbid` is OK; \ to use `{}`, restructure to compute effect-free results inside \ and apply effects outside the sandbox.", callee_label, name, name, name ), span, )); } // Realtime check. if state.realtime_active && realtime_suspend_effect(name) { errors.push(Diagnostic::new( format!( "function `{}` requires suspend-effect `{}`, cannot be called \ inside `realtime` block (D64). Hint: realtime guarantees \ no fiber-suspension; effects {} block.", callee_label, name, "Net/Fs/Db/Time/Blocking suspend the fiber and are forbidden inside realtime" ), span, )); } // Plan 83.3 Ф.6: тело `blocking { }` идёт на threadpool-потоке // без fiber/event-loop-контекста — async-I/O-эффекты сломаны. if state.blocking_body_active && blocking_body_forbidden_effect(name) { errors.push(Diagnostic::new( format!( "function `{}` requires suspend-effect `{}`, cannot be called \ inside `blocking {{ ... }}` body (Plan 83.3 V1 leaf-contract, \ D50 §4): the libuv threadpool thread has no fiber/event-loop \ context for `{}`. Hint: `blocking` is for genuinely-blocking \ C calls — do async I/O outside it.", callee_label, name, name ), span, )); } } // Plan 197 (--strict-effects, experimental, D62 §Правило 1): opt-in // promotion of "undeclared transitive effect" from the (currently // unimplemented) default warning to a hard error. No-op unless the // CLI flag is set — see `crate::strict_effects::strict_effects_enabled()`. if crate::strict_effects::strict_effects_enabled() { self.check_transitive_effect_strict(callee, callee_label, state, span, errors); } } /// Plan 197 (--strict-effects): `E_UNDECLARED_TRANSITIVE_EFFECT`. Fires /// when `callee` carries a non-`Fail` effect `E` that the ENCLOSING /// function neither declares in its own signature /// (`state.declared_effects`, filled once in `walk_fn_body`) nor handles /// via an enclosing `with E = … { }` block (`state.with_handler_stack` /// — D62 "Альтернатива через with": a locally-installed handler /// discharges the obligation right there, D11 with-semantics /// unchanged). `state.effect_root` (test-block bodies — D414 §2, no /// enclosing signature to declare against) is exempt, mirroring the /// existing `Detach`-effect `effect_root` exemption above (D50). /// `Fail` is excluded — D62 §Правило 2 already makes `Fail` /// transitivity strict and UNCONDITIONAL (a separate, pre-existing /// concern, not gated by this experimental flag). fn check_transitive_effect_strict( &self, callee: &FnDecl, callee_label: &str, state: &CapState, span: Span, errors: &mut Vec<Diagnostic>, ) { if state.effect_root { return; } for eff in &callee.effects { let TypeRef::Named { path, .. } = eff else { continue; }; let Some(name) = path.last() else { continue; }; if name == "Fail" { continue; } if state.declared_effects.contains(name) { continue; } if state.with_handler_stack.iter().any(|h| h == name) { continue; } errors.push(Diagnostic::new( format!( "[E_UNDECLARED_TRANSITIVE_EFFECT] call to `{}` requires effect `{}`, \ not declared in the enclosing function's signature and not handled \ by an enclosing `with {} = …` block (--strict-effects; D62 §Правило \ 1 — without this flag this is only a warning). Hint: add `{}` to the \ enclosing fn's effect-row, or install `with {} = handler {{ … }}` \ around this call.", callee_label, name, name, name, name ), span, )); } } /// Plan 221.1 №131 (D62 ENFORCED): `[E_RAW_EFFECT_OP_UNDECLARED]`. A /// SYNTHESIS: D28 §Правило вывода п.1 makes DIRECT effect-op usage /// (`Effect.op(...)` called straight in the body — no intervening named /// fn) mandatory in `export fn` signatures UNCONDITIONALLY (not gated /// behind `--strict-effects`, unlike the transitive-via-named-fn class /// above — `E_UNDECLARED_TRANSITIVE_EFFECT` is an experimental warning /// promoted to error by that flag; this is baseline language semantics, /// same footing as `Fail`/`E_BANG_REQUIRES_FAIL`). Before this window /// `Effect.op(...)` was invisible to EVERY effect-check in the compiler /// (opus design-note 222.20-Ф.2, probes 4b-4j) — D62's "явная /// декларация обязательна" was decoration, not enforcement, for this /// call shape. /// /// **Scope** mirrors `E_BANG_REQUIRES_FAIL` (№113) exactly: fires ONLY /// when `state.is_export` (the enclosing fn is `export fn`). Private fn /// (and `main`, which is bare `fn`) fall through to D28 auto-inference /// — see `infer_effects`'s raw-effect-op walker (`types/mod.rs`, /// `push_raw_effect_ops_into_fn`), which silently adds the effect to /// `f.effects`, mirroring the existing `Fail`-on-throw auto-push. /// `state.effect_root` (test-block bodies, D414 §2 — no enclosing /// signature to declare against) is exempt, same boundary as /// `check_transitive_effect_strict` above; in practice this is already /// implied by `!state.is_export` (test-block state never sets /// `is_export`), kept explicit for readability/defensiveness. /// `with EffectName = …` in scope discharges the obligation locally /// (`state.with_handler_stack`), same D11/Правило-4 semantics as the /// named-fn class — `#default_handler` (D431) does NOT discharge it: /// D431 is a RUNTIME ambient-construction fallback, orthogonal to this /// STATIC declaration requirement (D431 text never claims otherwise — /// verified probe 3, Ф-E of the design note; NOT a second hole). fn check_raw_effect_op_declared( &self, eff_name: &str, op_name: &str, state: &CapState, span: Span, errors: &mut Vec<Diagnostic>, ) { if !state.is_export || state.effect_root { return; } if eff_name == "Fail" { return; } if state.declared_effects.contains(eff_name) { return; } if state.with_handler_stack.iter().any(|h| h == eff_name) { return; } errors.push(Diagnostic::new( format!( "[E_RAW_EFFECT_OP_UNDECLARED] raw call to effect operation `{eff}.{op}` \ requires effect `{eff}`, not declared in this `export fn`'s signature and \ not handled by an enclosing `with {eff} = …` block (D62/D28: direct \ effect-op usage is always mandatory in exported signatures — unlike a call \ to a named fn that itself declares `{eff}`, this is unconditional, not \ gated behind `--strict-effects`). Hint: add `{eff}` to this fn's effect-row, \ or install `with {eff} = handler {{ … }}` around this call.", eff = eff_name, op = op_name, ), span, )); } /// Plan 16 D63: единичная проверка effect'a против forbidden-стека. fn check_forbid_intersection( &self, eff_name: &str, state: &CapState, span: Span, errors: &mut Vec<Diagnostic>, ) { let forbidden = state.union_forbidden(); if forbidden.contains(eff_name) { errors.push(Diagnostic::new( format!( "use of effect `{}` is forbidden by enclosing `forbid {}` block (D63).", eff_name, eff_name ), span, )); } } /// Plan 173.3 (D415 §2): `spawn <body>` capture-check entry — `body` may /// be a bare `Block` expr (`spawn { ... }`) or, after the Ф.3 /// `spawn consume c [= e] { ... }` desugar, a `Block` wrapping a single /// `Stmt::ConsumeScope`. Either way the top-level `body` IS the block to /// scan (unwrap `ExprKind::Block`; anything else — e.g. a bare call /// expr `spawn foo()` — has no lexical body to scan, skip). /// /// `detach`/`blocking` bodies are already plain `&Block` (no `Expr` /// wrapper) at their call sites, so they call `check_capture_boundary` /// directly instead of going through this `spawn`-specific unwrap. fn check_spawn_capture(&self, body: &Expr, state: &CapState, errors: &mut Vec<Diagnostic>) { if let ExprKind::Block(b) = &body.kind { self.check_capture_boundary(b, state, errors); // A-V10 (D441 §5 №167 closure): same unwrap, thread-affine // boundary check alongside the capture-check. self.check_thread_affine_boundary(b, errors); } } /// Plan 173.3 (D415 §2): core capture-check — collect free variables /// referenced in `body` (names not locally shadowed within it), and for /// each one that resolves (via `state.scopes`, searched innermost-out) to /// a MUTABLE outer binding whose type is not `#share` → emit /// `E_CONCURRENT_MUT_CAPTURE`. A name that resolves to an IMMUTABLE /// (`ro`) outer binding, or to a `#share` type, or that isn't found at /// all (unknown — const/fn-name/closure-or-match-bound name; V1 does not /// track those scopes, see module docs) is never flagged — false /// negatives are the safe direction here, not false positives. /// /// Boundary = any construct whose `body` may run concurrently with the /// enclosing scope on another OS thread under M:N: `spawn`, `parallel /// for`, `detach` (2026-07-11 amendment — orphan fiber, fire-and-forget, /// never joined, identical race surface to `spawn`), and the retracted- /// but-AST-stable `blocking { }` block-form. `#blocking fn` (the live /// replacement, Plan 113) is NOT a boundary here: it is a top-level `fn` /// item with its own fresh parameter scope, not a lexical closure over /// the caller — see the `ExprKind::Blocking` arm comment for the full /// analysis of why it has no capture surface to check. /// /// The one sanctioned exception is `consume`-move: `Stmt::ConsumeScope` /// inside `body` is the explicit move-capture form (D415 §4, /// `spawn consume c [= e] { .. }` desugar) — its `init` expression's /// referenced names are EXEMPT (the move is syntactically visible right /// there), and its `binding` name is locally shadowed for the rest of /// the scan (mirrors an ordinary `let`). fn check_capture_boundary(&self, body: &Block, state: &CapState, errors: &mut Vec<Diagnostic>) { let mut shadow: HashSet<String> = HashSet::new(); let mut free: HashSet<String> = HashSet::new(); capture_scan_block(body, &mut shadow, &mut free); self.flag_boundary_captures( free, body.span, state, "captured by reference in a `spawn`/`parallel for`/`detach` body", "E_CONCURRENT_MUT_CAPTURE", errors, ); } /// A-V10 (D441 §5 №167 closure): does `body` (a `spawn`/`detach`/ /// `parallel for` body — the SAME boundary class as /// `check_capture_boundary`, called alongside it at every call site) /// directly name-call a `#thread_affine` leaf, or a fn that /// transitively reaches one (`own_fiber_call_names_of_fn` / /// `thread_affine_closure`, computed once in `build`)? No `state` /// dependency — pure name lookup against the whole-module pre-pass, no /// lexical-scope resolution needed (unlike the mut-capture checks). fn check_thread_affine_boundary(&self, body: &Block, errors: &mut Vec<Diagnostic>) { let mut called: HashSet<String> = HashSet::new(); own_fiber_call_names_block(body, &mut called); let mut names: Vec<&String> = called.iter().collect(); names.sort(); // deterministic diagnostic order (HashSet iteration) for name in names { if self.thread_affine_leaves.contains(name) { errors.push(Diagnostic::new( format!( "[E_THREAD_AFFINE_IN_FIBER] call to `{name}` — declared \ `#thread_affine` (D441 §5 №167: an M:N-unsafe leaf, \ bound to the OS thread that first calls it — \ thread-local state / a non-reentrant C-side handle) \ — directly inside a `spawn`/`detach`/`parallel for` \ body: under M:N scheduling this fiber may run on ANY \ worker thread, not the one `{name}` requires. Fix: \ call `{name}` from the main fiber BEFORE `spawn` \ (its result, if any, can then be captured `ro` into \ the fiber), or route it through a dedicated \ blocking channel served by the ONE thread `{name}` \ is affine to.", name = name, ), body.span, )); } else if let Some((leaf, first_hop)) = self.thread_affine_transitive.get(name) { // One recorded intermediate frame (`first_hop`) is enough // per D441 §5 №167 wording ("хотя бы один промежуточный // фрейм") — render it plainly when `first_hop == leaf` // (one-hop chain, `name` calls `leaf` directly) instead of // a redundant `a → b → … → b`. let chain = if first_hop == leaf { format!("`{name}` → `{leaf}`", name = name, leaf = leaf) } else { format!( "`{name}` → `{first_hop}` → … → `{leaf}`", name = name, first_hop = first_hop, leaf = leaf, ) }; errors.push(Diagnostic::new( format!( "[E_THREAD_AFFINE_IN_FIBER] call to `{name}` — \ transitively reaches the `#thread_affine` leaf \ `{leaf}` (chain: {chain}; D441 §5 №167: an \ M:N-unsafe leaf, bound to the OS thread that first \ calls it — thread-local state / a non-reentrant \ C-side handle) — inside a `spawn`/`detach`/`parallel \ for` body: under M:N scheduling this fiber may run \ on ANY worker thread, not the one `{leaf}` requires. \ Fix: call `{name}` from the main fiber BEFORE \ `spawn` (its result, if any, can then be captured \ `ro` into the fiber), or route it through a \ dedicated blocking channel served by the ONE thread \ `{leaf}` is affine to.", name = name, leaf = leaf, chain = chain, ), body.span, )); } } } /// Plan 238 Ф.1/Ф.3 (D441): shared core — resolve every name in `free` /// against `state.scopes` and emit the appropriate fiber-boundary /// diagnostic for each unsafe one. Factored out of the original /// (direct-boundary-only) `check_capture_boundary` so the SAME /// resolution/classification logic backs three crossing points: /// (1) a `spawn`/`detach`/`parallel for` body itself (`crossing_desc` /// = "captured by reference in a …body"); (2) a `with`-handler /// installed around a fiber-containing body (Ф.3, /// `check_handler_capture`); (3) a closure passed BY VALUE into a /// spawn-tainted parameter or a channel send (Ф.1 transitive path, /// `check_transitive_closure_arg`). `mut_code` lets the handler call /// site use a distinct E-code (`E_HANDLER_MUT_CAPTURE_IN_FIBER`) from /// the direct/transitive ones (`E_CONCURRENT_MUT_CAPTURE`) while /// sharing the same message body and share/linear classification. fn flag_boundary_captures( &self, free: HashSet<String>, span: Span, state: &CapState, crossing_desc: &str, mut_code: &str, errors: &mut Vec<Diagnostic>, ) { let share_q = CapShareQuery(&self.type_decls); // (name, why) — `why` is the per-field refusal explanation // ([M-173.3-share-leakage-explain]): the FIRST poison path returned // by `share_check::mut_alias_failure`, rendered as a // `Type.field.subfield` chain + reason, so that a non-share `mut` // field deep inside a type no longer strips share-ness silently. let mut flagged: Vec<(String, String)> = Vec::new(); // Plan 201 (D188-амендмент, взаимодействие с 173.1 by-value капчуром): // захват linear-значения (consume-тип: TcpStream/Transaction/…) в // тело spawn/parallel-for/detach — by-value КОПИЯ обёртки в N // файберов = N алиасов одного ресурса БЕЗ учёта владения → // double-close/интерференция. Ошибка независимо от ro/mut; канон — // `@share()`-копия per-fiber (refcount «закрывает последний») либо // явный move `spawn consume c { … }` (D415 §4 — init exempt в // capture_scan). Отдельный Vec — своя диагностика. let mut linear_flagged: Vec<(String, String)> = Vec::new(); // [M-detach-consume-escape-unchecked] (D415 §4 extension, Plan 173.3): // names bound via an explicit `consume` pattern sub-bind // (`Ok(consume stream) => { … }` in a `match`/`if let`/`while let` // arm — `ScopeBinding.linear_pattern`) never carry a `ty` (no static // annotation at a pattern-destructure site — V1 has no destructure // type inference), so the `ty`-based `type_decls` lookup above can // never classify them as linear. `linear_pattern` is set precisely // when the SOURCE syntax already marked the bind `consume` — that is // itself sufficient (and authoritative) evidence this is a linear/ // consume resource, independent of knowing its concrete type name. // Separate Vec (no type name available for the diagnostic). let mut pattern_linear_flagged: Vec<String> = Vec::new(); for name in free { let binding = state.scopes.iter().rev().find_map(|f| f.get(&name)); if let Some(b) = binding { if let Some(ty) = &b.ty { if let Some(base) = TypeCheckCtx::typeref_named_base(ty) { let is_linear = self.type_decls.get(base) .map(|td| td.consume) .unwrap_or(false); if is_linear { linear_flagged.push((name.clone(), base.to_string())); continue; } } } if b.linear_pattern { pattern_linear_flagged.push(name.clone()); continue; } if !b.mutable { // `ro` capture — deep-immutable view (D246): always safe, // no share requirement (D415 §2 "ro" arm). continue; } // `mut` binding captured by reference: safe ONLY when the // type is mut-alias-safe (audited `#share` sync type, or an // aggregate whose mutation paths bottom out in one). A bare // `mut int`/`mut []T` accumulator — THE motivating race — // is NOT (see share_check module doc). let why = match &b.ty { Some(ty) => { match crate::protocols::share_check::mut_alias_failure(&share_q, ty) { None => continue, // mut-alias-safe — not flagged. Some(f) => { if f.path.is_empty() { // The binding's own type refused. format!("`{}` is {}", f.ty, f.reason) } else { // A transitively-reached field refused — // name the full chain (leakage explain). let root = render_type_ref(ty); format!( "`{}` is poisoned at `{}` (`{}`): {}", root, f.chain(&root), f.ty, f.reason ) } } } } // Unknown type (no annotation, no syntactic ctor // inference in scope registration) — conservative: // cannot prove internal sync ⇒ flag. None => "its type is unknown at the capture site (no \ annotation, no recognizable ctor) — share-ness \ cannot be proven, conservatively flagged" .to_string(), }; flagged.push((name, why)); } } linear_flagged.sort(); // deterministic diagnostic order for (name, ty) in linear_flagged { errors.push(Diagnostic::new( format!( "[E_LINEAR_CAPTURE_IN_FIBER] `{n}` (linear consume-тип \ `{ty}`) {cross}: by-value копия обёртки в N файберов = \ N алиасов одного ресурса БЕЗ учёта владения → \ double-close/интерференция (Plan 201 / 173.1 by-value \ капчур). Возьмите `@share()`-копию per-fiber (`ro w = \ {n}.share()` перед `spawn {{ …w… }}` — refcount \ закрывает последним) либо явный move: \ `spawn consume {n} {{ … }}` / `detach consume {n} {{ … }}` \ (D415 §4).", n = name, ty = ty, cross = crossing_desc ), span, )); } pattern_linear_flagged.sort(); // deterministic diagnostic order for name in pattern_linear_flagged { errors.push(Diagnostic::new( format!( "[E_LINEAR_CAPTURE_IN_FIBER] `{n}` (явный `consume`-биндинг \ из pattern, напр. `Ok(consume {n})`) {cross} из \ объемлющего scope: by-value копия обёртки в N файберов = \ N алиасов одного ресурса БЕЗ учёта владения → \ use-after-consume/double-close (Plan 173.3 / \ [M-detach-consume-escape-unchecked], D415 §4 \ расширение). Передайте владение явным move: \ `spawn consume {n} {{ … }}` / `detach consume {n} {{ … }}` \ — тело получает `{n}` во владение, cleanup срабатывает \ при выходе из ЕГО собственного тела, а не объемлющего \ scope.", n = name, cross = crossing_desc ), span, )); } flagged.sort(); // deterministic diagnostic order (HashSet iteration) for (name, why) in flagged { errors.push(Diagnostic::new( format!( "[{code}] outer `mut` binding `{}` \ {cross}: \ {} — under M:N scheduling this alias is a data race (the \ child fiber may run concurrently with, or migrate across \ threads from, the parent/siblings). Allowed captures \ (Plan 173.3, D415 §2): move it in explicitly \ (`spawn consume {} = expr {{ .. }}` / \ `spawn consume {} {{ .. }}` / `detach consume {} {{ .. }}`), \ capture it `ro` (immutable \ view), or use an internally-synchronized `#share` type \ (`Mutex`/`Atomic*` — or a user lock-free type vouched \ with `#share`).", name, why, name, name, name, code = mut_code, cross = crossing_desc ), span, )); } } /// №364 (D415 §4 extension, К1 звучность): resolve an UNANNOTATED /// tuple-destructure `let`'s RHS method-call return type — `ro (r, w) = /// s.into_split()`, no `d.ty` — via `self.sig.method_table`, so a /// `Pattern::Tuple` `Stmt::Let` registration (see `walk_stmt`) can zip a /// per-element `TypeRef` onto each bound name instead of leaving every /// name with `ty: None` (invisible to the linear-capture classification /// in `flag_boundary_captures`). Deliberately conservative — bails to /// `None` (whole-`ty`/`None` fallback then applies at the call site) on /// ANY ambiguity: /// - RHS is not a plain `recv.method(..)` call (`ExprKind::Call` over /// `ExprKind::Member`); /// - the receiver is not a bare `Ident` already carrying a known static /// `ScopeBinding.ty` in `state.scopes` (no receiver-type inference /// here — CapabilityCtx runs BEFORE `TypeCheckCtx`'s full inference /// pass, see `run_checks`' `CapabilityCtx::build`/`TypeCheckCtx::build` /// ordering, so there is no `resolved_types` channel available yet at /// this pass); /// - `method_table` doesn't resolve the receiver type + method name to /// EXACTLY one overload (multiple overloads on the same name could /// disagree on return shape — safer to see nothing than guess wrong); /// - that overload's `return_type` isn't a `TypeRef::Tuple`. fn resolve_tuple_call_return(&self, e: &Expr, state: &CapState) -> Option<Vec<TypeRef>> { let ExprKind::Call { func, .. } = &e.kind else { return None }; let ExprKind::Member { obj, name: method } = &func.kind else { return None }; let ExprKind::Ident(recv_name) = &obj.kind else { return None }; let recv_ty = state.scopes.iter().rev() .find_map(|f| f.get(recv_name)) .and_then(|b| b.ty.as_ref()) .and_then(TypeCheckCtx::typeref_named_base)?; let overloads = self.sig.method_table.get(recv_ty)?.get(method.as_str())?; let [fd] = overloads.as_slice() else { return None }; match fd.return_type.as_ref()? { TypeRef::Tuple(elems, _) => Some(elems.clone()), _ => None, } } /// Plan 238 Ф.1 (D441 §1-2): call-site half of the transitive-parameter /// check. `arg` is the expression passed at a position `spawn_tainted_ /// params` flagged for the callee (that parameter IS invoked inside a /// `spawn`/`detach`/`parallel for`/`blocking` body somewhere in the /// callee's own definition). Resolve `arg`'s free variables — either /// directly (a closure LITERAL passed inline) or via the `let`-time /// snapshot (`ScopeBinding.closure_free_vars`, a plain `Ident` /// referencing a previously-bound closure, exactly `race150/ /// b_closure_bypass.nv`'s `parallel_spawn(push, ..)` shape) — then run /// them through the SAME share-safety resolution as a direct spawn-body /// capture would get, AT THE CALLER's scope (where the closure's own /// captures still resolve, since Nova has no closure re-entry that /// would rebind them differently). /// /// Plan S1a (D441 §5 "closure-as-field", №117/№242§3): resolve the /// static TYPE NAME a field-access receiver `obj` belongs to — `@field` /// (`SelfAccess`) resolves to the enclosing method's OWN receiver type /// (`state.current_receiver_type`); a bare `Ident` resolves through its /// `ScopeBinding.ty` IF annotated/syntactically inferred (mirrors the /// same `TypeCheckCtx::typeref_named_base` unwrap `flag_boundary_ /// captures` already uses for the mut-capture type lookup). Anything /// else (a call result, an unannotated bare-Ident receiver, `obj.field. /// field2` chains) is a documented conservative no-op — under- /// approximation, same honesty stance as `check_transitive_closure_ /// arg`'s own opaque-expression case below. fn resolve_field_owner_type(&self, obj: &Expr, state: &CapState) -> Option<String> { match &obj.kind { ExprKind::SelfAccess => state.current_receiver_type.clone(), ExprKind::Ident(name) => state.scopes.iter().rev() .find_map(|f| f.get(name)) .and_then(|b| b.ty.as_ref()) .and_then(TypeCheckCtx::typeref_named_base) .map(|s| s.to_string()) // p-s1a2 (№289): no local BINDING named `name` — it may be // the TYPE name itself, a STATIC-call receiver (`Holder.new(..)`, // `ReceiverKind::Static`, parsed with NO `self`/`@` — see // parser `fn TypeName.method(..)` receiver-qualifier dispatch). // A bound variable always wins when both would match (shadowing // a type name with a local of the same name is legal Nova and // must resolve to the VARIABLE's type, checked first above). .or_else(|| self.type_decls.contains_key(name.as_str()).then(|| name.clone())), _ => None, } } /// Plan S1a (D441 §5 "closure-as-field", №117/№242§3): the write-site /// half of the field-sink taint check — `value` is being stored into a /// field already proven `spawn_tainted_fields`-tainted (its value gets /// INVOKED inside a `spawn`/`detach`/`parallel for`/`blocking` body /// SOMEWHERE in `owner`'s own methods). Resolve `value` to a closure /// EXACTLY the way `check_transitive_closure_arg`/the `chan.send` check /// already do (literal, or a `let`/`ro`-time `closure_free_vars` /// snapshot) and run it through the SAME `flag_boundary_captures` core /// as every other crossing point — one machine, one diagnostic family. fn check_field_sink_write( &self, value: &Expr, owner: &str, field: &str, state: &CapState, errors: &mut Vec<Diagnostic>, ) { let mut shadow = HashSet::new(); let mut free = HashSet::new(); let free_opt = match &value.kind { ExprKind::Lambda { .. } | ExprKind::ClosureLight { .. } | ExprKind::ClosureFull(_) => { capture_scan_expr(value, &mut shadow, &mut free); Some(free) } ExprKind::Ident(vname) => state.scopes.iter().rev() .find_map(|f| f.get(vname)) .and_then(|b| b.closure_free_vars.as_ref()) .map(|v| v.iter().cloned().collect()), _ => None, }; if let Some(free) = free_opt { let crossing = format!( "created outside any fiber boundary, then written into `{owner}.{field}` \ — this field's value is INVOKED inside a `spawn`/`detach`/`parallel for`/\ `blocking` body elsewhere in `{owner}`'s own methods (D441 §5 \"замыкание \ как ПОЛЕ структуры\" class, Plan S1a №117/№242§3: the value carries its \ captured environment across the fiber boundary just like a direct capture \ would, only through storage — a field/container write — instead of a \ direct call)", owner = owner, field = field, ); self.flag_boundary_captures( free, value.span, state, &crossing, "E_CONCURRENT_MUT_CAPTURE", errors, ); } } /// Plan S1a (D441 §5 "closure-as-field" — №117 wrapper-method shape): /// call-site half of `spawn_tainted_method_params` — `arg` is passed /// into a method whose OWN body writes its parameter `param` into a /// tainted field (`fn BackgroundTasks mut @add(f fn()->()) { @tasks. /// push(f) }`). Resolution is IDENTICAL to `check_transitive_closure_ /// arg`'s (literal, or `let`/`ro`-time snapshot) — only the crossing /// description differs (names the two-hop wrapper shape explicitly). fn check_field_write_via_method_param( &self, arg: &Expr, owner: &str, method: &str, param: &str, state: &CapState, errors: &mut Vec<Diagnostic>, ) { let free_vars: Option<HashSet<String>> = match &arg.kind { ExprKind::Lambda { .. } | ExprKind::ClosureLight { .. } | ExprKind::ClosureFull(_) => { let mut shadow = HashSet::new(); let mut free = HashSet::new(); capture_scan_expr(arg, &mut shadow, &mut free); Some(free) } ExprKind::Ident(name) => state.scopes.iter().rev() .find_map(|f| f.get(name)) .and_then(|b| b.closure_free_vars.as_ref()) .map(|v| v.iter().cloned().collect()), _ => None, }; if let Some(free) = free_vars { let crossing = format!( "created outside any fiber boundary, then passed BY VALUE into \ `{owner}.{method}`'s parameter `{param}` — `{method}` WRITES `{param}` \ into a field of `{owner}` whose value is invoked inside a `spawn`/`detach`/\ `parallel for`/`blocking` body elsewhere in `{owner}`'s own methods (D441 §5 \ \"замыкание как ПОЛЕ структуры\" class, Plan S1a №117/№242§3 — the \ `BackgroundTasks.add(f) {{ @tasks.push(f) }}` shape: the value carries its \ captured environment across the fiber boundary two hops away — through a \ wrapper method's parameter, then through field storage)", owner = owner, method = method, param = param, ); self.flag_boundary_captures(free, arg.span, state, &crossing, "E_CONCURRENT_MUT_CAPTURE", errors); } } /// Anything else (an opaque expression we cannot trace to a literal or /// a recorded snapshot — e.g. a further fn-parameter passed through /// unchanged, a field read, a call returning a closure) is a /// documented, conservative no-op: under-approximation, not a false /// positive (Plan 238 report — honest limitation, not silently grown /// scope). fn check_transitive_closure_arg( &self, arg: &Expr, callee_name: &str, param_name: &str, state: &CapState, errors: &mut Vec<Diagnostic>, ) { let free_vars: Option<HashSet<String>> = match &arg.kind { ExprKind::Lambda { .. } | ExprKind::ClosureLight { .. } | ExprKind::ClosureFull(_) => { let mut shadow = HashSet::new(); let mut free = HashSet::new(); capture_scan_expr(arg, &mut shadow, &mut free); Some(free) } ExprKind::Ident(name) => state.scopes.iter().rev() .find_map(|f| f.get(name)) .and_then(|b| b.closure_free_vars.as_ref()) .map(|v| v.iter().cloned().collect()), _ => None, }; if let Some(free) = free_vars { // A-V10 (D441 §5 №168 closure): `{param}` is tainted either // because `{callee}` INVOKES it inside its own `spawn`/`detach`/ // `parallel for`/`blocking` body (Ф.1а, original), or because // `{callee}` INSTALLS it as a `with`-handler around such a body // (install-position extension, `collect_fiber_boundary_frees_ // expr`'s `With` arm) — one shared message covers both, since // both are the identical transitive-crossing shape from the // caller's point of view (a value created outside any fiber // boundary, carried across it by value, one function-call hop // away from the syntactic boundary). let crossing = format!( "created outside any fiber boundary, then passed BY VALUE \ into `{callee}`'s parameter `{param}` — `{callee}` invokes \ `{param}`, OR installs it as a `with`-handler around a body \ that invokes it, inside its OWN `spawn`/`detach`/`parallel \ for`/`blocking` body (D441 §1/§5 №168: transitive crossing — \ the value carries its captured environment across the fiber \ boundary just like a direct capture would, only one \ function-call hop away from the syntactic boundary)", callee = callee_name, param = param_name, ); self.flag_boundary_captures(free, arg.span, state, &crossing, "E_CONCURRENT_MUT_CAPTURE", errors); } } /// Plan 238 Ф.3 (D441 §3): check a `with`-handler's mut-captures when /// its body (the `with … { body }` it is installed around) contains a /// `spawn`/`detach`/`parallel for` — Ф.0 measured the handler executes /// IN THE FIBER of the failing/child operation (2/5 runs of 64×20 /// concurrent child failures lost updates on an unsynchronized /// `cnt = cnt + 1` inside the handler), so a mut-capture there is /// data-race-equivalent to a direct spawn-body capture. Caller /// (`ExprKind::With` in `walk_expr`) has already excluded the D416§2 /// `Supervisor.on_child_fail` carve-out (serialized on the scope's /// drive fiber — not concurrent with siblings) before calling this. fn check_handler_capture(&self, handler: &Expr, state: &CapState, errors: &mut Vec<Diagnostic>) { let crossing = "captured by reference in a `with`-handler installed \ around a body containing `spawn`/`detach`/`parallel for` \ (D441 §3 — the handler runs IN THE FIBER of the failing/\ child operation, not the installing scope's fiber; see \ Ф.0 measurement — 2/5 runs lost updates under 64×20 \ concurrent child failures)"; match &handler.kind { ExprKind::Lambda { .. } | ExprKind::ClosureLight { .. } | ExprKind::ClosureFull(_) => { let mut shadow = HashSet::new(); let mut free = HashSet::new(); capture_scan_expr(handler, &mut shadow, &mut free); self.flag_boundary_captures(free, handler.span, state, crossing, "E_HANDLER_MUT_CAPTURE_IN_FIBER", errors); } ExprKind::HandlerLit { methods, .. } => { for m in methods { let mut shadow: HashSet<String> = m.params.iter().map(|p| p.name.clone()).collect(); let mut free = HashSet::new(); match &m.body { HandlerMethodBody::Block(b) => capture_scan_block(b, &mut shadow, &mut free), HandlerMethodBody::Expr(e) => capture_scan_expr(e, &mut shadow, &mut free), } self.flag_boundary_captures(free, handler.span, state, crossing, "E_HANDLER_MUT_CAPTURE_IN_FIBER", errors); } } // A-V10 (D441 §5 №168 closure): plain `Ident` referencing a // PRECOMPUTED handler value (`ro h = |..| {..}` then // `with X = h { ...spawn... }`) — the literal is not written at // the `with`-site itself, so it was invisible to the two arms // above (Plan 238 Ф.3's original honest gap, D441 §5 point 6). // Resolve it the SAME way Ф.1's `check_transitive_closure_arg` // already resolves a closure passed BY VALUE into a // spawn-tainted parameter: the `let`/`ro`-time snapshot // (`ScopeBinding.closure_free_vars`, populated in `walk_stmt`'s // `Stmt::Let` arm for every closure-literal init, REGARDLESS of // how the bound name is later used — recorded before it can be // shadowed). A name that does NOT resolve to a recorded // snapshot (fn declared elsewhere, effect-op factory result, // etc.) stays a documented conservative no-op — under- // approximation, never a false positive, same honesty stance as // `check_transitive_closure_arg`'s own opaque-expression case. ExprKind::Ident(name) => { if let Some(free) = state.scopes.iter().rev() .find_map(|f| f.get(name)) .and_then(|b| b.closure_free_vars.as_ref()) { let free_set: HashSet<String> = free.iter().cloned().collect(); let precomputed_crossing = format!( "captured by reference in a PRECOMPUTED `with`-handler \ (`{name}`, not a literal written at the `with`-site) \ installed around a body containing `spawn`/`detach`/\ `parallel for` (D441 §5 №168 closure — the handler was \ evaluated ahead of time (\"обработчик, вычисленный \ заранее\") and resolved here via the same `let`/`ro`\ -time closure-capture snapshot Ф.1 already uses for \ spawn-tainted parameters; it runs IN THE FIBER of the \ failing/child operation exactly like a literal \ handler — see Ф.0 measurement, D441 §3)", name = name, ); self.flag_boundary_captures( free_set, handler.span, state, &precomputed_crossing, "E_HANDLER_MUT_CAPTURE_IN_FIBER", errors, ); } } // Call-shaped policy (`escalate()`/`stop()`/builtin factory) — // no user closure body to inspect, nothing to capture. _ => {} } } } /// Plan 173.3 (D415 §2): SYNTACTIC init-expr type sketch for scope /// registration when a `let` has no annotation. Handles exactly the shapes /// the capture-check needs to classify correctly without full inference: /// - `Type.new(..)` / `Type.of(..)` / any `Type.method(..)` static-ctor call /// where `Type` is a KNOWN type name → `Named{Type}` (covers /// `mut mu = Mutex.new()` — must not be false-positived); /// - int/float/str/bool/char literals → the primitive (covers /// `mut acc = 0` — must be flagged on mut capture); /// - array literal → `[]<unknown>` shape (Array of an unresolvable name — /// still classified "container ⇒ not mut-alias-safe", which is right); /// - anything else → `None` (conservative-flag direction). fn capture_syntactic_init_type( e: &Expr, type_decls: &HashMap<String, &TypeDecl>, ) -> Option<TypeRef> { let named = |n: &str| TypeRef::Named { path: vec![n.to_string()], generics: vec![], span: e.span, }; match &e.kind { ExprKind::IntLit(_) => Some(named("int")), ExprKind::FloatLit(_) => Some(named("f64")), ExprKind::StrLit(_) | ExprKind::InterpolatedStr { .. } => Some(named("str")), ExprKind::BoolLit(_) => Some(named("bool")), ExprKind::CharLit(_) => Some(named("char")), ExprKind::ArrayLit(_) => Some(TypeRef::Array( Box::new(named("__nv_unknown_elem")), e.span, )), // `mut x = TypeName { .. }` — record literal with explicit type. ExprKind::RecordLit { type_name: Some(path), .. } => { path.last().map(|n| named(n)) } ExprKind::Call { func, .. } => match &func.kind { ExprKind::Member { obj, .. } => { // `Type.new(..)` — plain, or `Type[T].new(..)` — turbofish // (generic ctor, e.g. `OnceCell[int].new()`). let base = match &obj.kind { ExprKind::TurboFish { base, .. } => &base.kind, other => other, }; if let ExprKind::Ident(tyname) = base { if type_decls.contains_key(tyname.as_str()) { return Some(named(tyname)); } } None } ExprKind::Path(parts) if parts.len() == 2 => { if type_decls.contains_key(parts[0].as_str()) { return Some(named(&parts[0])); } None } _ => None, }, _ => None, } } /// Plan 173.3 (D415 §2): bound names (+ per-name mutability + per-name /// explicit-`consume` marker) introduced by a `let`/for-loop-var/match-arm/ /// if-let/etc pattern. Non-`Ident` sub-patterns default `mutable = false, /// is_consume = false` (conservative — V1 does not track per-element `mut`/ /// `consume` in destructure patterns beyond the direct `Ident{is_mut, /// is_consume}` case; see D184 §1 "no per-element granularity V1", the same /// limitation this mirrors). Plan 173.3 [M-detach-consume-escape-unchecked] /// amendment (D415 §4 extension): the third tuple element (`is_consume`) — /// `Ok(consume tcp)`-style explicit ownership-transfer sub-binds (D157/D180) /// — feeds the `spawn`/`parallel for`/`detach` capture-check's /// `linear_pattern` classification (see `check_capture_boundary`) so a /// match-arm-bound consume value (no static type annotation — `ty` stays /// `None` in `ScopeBinding`, so the ordinary `type_decls`-lookup linear check /// can't see it) is still caught, closing the exact gap the flagship's /// `match lst.accept() { Ok(consume stream) => { detach { ...stream... } } }` /// slipped through (bare `Ok(consume stream)` bind was entirely invisible to /// `state.scopes` before this amendment — match/if-let arms pushed no scope /// frame at all). fn pattern_capture_names(pat: &Pattern) -> Vec<(String, bool, bool)> { let mut out = Vec::new(); pattern_capture_names_into(pat, &mut out); out } fn pattern_capture_names_into(pat: &Pattern, out: &mut Vec<(String, bool, bool)>) { match pat { Pattern::Ident { name, is_mut, is_consume, .. } => out.push((name.clone(), *is_mut, *is_consume)), Pattern::Variant { kind: VariantPatternKind::Tuple { patterns, .. }, .. } => { for p in patterns { pattern_capture_names_into(p, out); } } Pattern::Variant { kind: VariantPatternKind::Unit, .. } => {} Pattern::Record { fields, .. } => { for f in fields { match &f.pattern { Some(p) => pattern_capture_names_into(p, out), None => out.push((f.name.clone(), false, false)), // `{ name }` shorthand } } } Pattern::Array { elems, .. } => { for e in elems { match e { ArrayPatternElem::Item(p) => pattern_capture_names_into(p, out), ArrayPatternElem::RestBind(name) => out.push((name.clone(), false, false)), ArrayPatternElem::Rest => {} } } } Pattern::Tuple(pats, _) => { for p in pats { pattern_capture_names_into(p, out); } } Pattern::Binding { name, inner, .. } => { out.push((name.clone(), false, false)); pattern_capture_names_into(inner, out); } Pattern::Or { alternatives, .. } => { if let Some(first) = alternatives.first() { pattern_capture_names_into(first, out); } } Pattern::Wildcard(_) | Pattern::Literal(_, _) => {} } } /// Plan 173.3 (D415 §2): free-variable scan for the spawn/parallel-for /// capture-check — standalone (no `CapabilityCtx`/`CapState` dependency), /// self-contained local-shadow tracking over the SUB-tree only. Covers the /// common expression/statement shapes; an unhandled shape is a (documented, /// conservative-direction) under-approximation — it can only cause a missed /// capture, never a false E_CONCURRENT_MUT_CAPTURE. pub(crate) fn capture_scan_block(b: &Block, shadow: &mut HashSet<String>, free: &mut HashSet<String>) { for s in &b.stmts { capture_scan_stmt(s, shadow, free); } if let Some(t) = &b.trailing { capture_scan_expr(t, shadow, free); } } fn capture_scan_stmt(s: &Stmt, shadow: &mut HashSet<String>, free: &mut HashSet<String>) { match s { Stmt::Expr(e) => capture_scan_expr(e, shadow, free), Stmt::Let(d) => { capture_scan_expr(&d.value, shadow, free); for (name, _, _) in pattern_capture_names(&d.pattern) { shadow.insert(name); } } Stmt::Const(_) => {} Stmt::Assign { target, value, .. } => { capture_scan_expr(target, shadow, free); capture_scan_expr(value, shadow, free); } Stmt::Return { value, .. } => { if let Some(v) = value { capture_scan_expr(v, shadow, free); } } Stmt::Throw { value, .. } => capture_scan_expr(value, shadow, free), Stmt::Break(_) | Stmt::Continue(_) => {} Stmt::Defer { body, .. } => capture_scan_expr(body, shadow, free), // Plan 173.3 (D415 §4): the ONE sanctioned move-capture exception — // `init`'s referenced names are exempt (explicit visible move); the // `binding` shadows for the rest of the scan (like an ordinary let). Stmt::ConsumeScope { binding, body, .. } => { shadow.insert(binding.clone()); for s in &body.stmts { capture_scan_stmt(s, shadow, free); } if let Some(t) = &body.trailing { capture_scan_expr(t, shadow, free); } } Stmt::AssertStatic { expr, .. } | Stmt::Assume { expr, .. } => { capture_scan_expr(expr, shadow, free) } Stmt::Apply { .. } | Stmt::Calc { .. } | Stmt::Reveal { .. } => {} Stmt::TupleAssign { lhs, rhs, .. } => { for e in lhs { capture_scan_expr(e, shadow, free); } for e in rhs { capture_scan_expr(e, shadow, free); } } } } pub(crate) fn capture_scan_expr(e: &Expr, shadow: &mut HashSet<String>, free: &mut HashSet<String>) { match &e.kind { ExprKind::Ident(name) => { if !shadow.contains(name) { free.insert(name.clone()); } } ExprKind::Block(b) => { // A nested block introduces its own child shadow scope (names // declared inside it must not leak out as shadows for SIBLING // code after the block) — but for THIS scan we only need // "shadowed-or-not" membership, and Nova has no re-declaration // shadowing conflicts across blocks, so extending the same set // is safe (a name shadowed deeper stays shadowed; nothing here // un-shadows on block exit, which is a conservative // over-approximation of shadowing = fewer false positives). capture_scan_block(b, shadow, free); } ExprKind::Call { func, args, trailing } => { capture_scan_expr(func, shadow, free); for a in args { capture_scan_expr(a.expr(), shadow, free); } if let Some(t) = trailing { match t { crate::ast::Trailing::Block(b) => capture_scan_block(b, shadow, free), crate::ast::Trailing::LegacyBlockWithParams(tb) => { capture_scan_block(&tb.body, shadow, free) } crate::ast::Trailing::Fn(sb) => capture_scan_fn_sig_body(sb, shadow, free), } } } ExprKind::TurboFish { base, .. } => capture_scan_expr(base, shadow, free), ExprKind::Binary { left, right, .. } => { capture_scan_expr(left, shadow, free); capture_scan_expr(right, shadow, free); } ExprKind::Unary { operand, .. } => capture_scan_expr(operand, shadow, free), ExprKind::Try(inner) | ExprKind::Bang(inner) | ExprKind::RefArg(inner) => { capture_scan_expr(inner, shadow, free) } ExprKind::Coalesce(a, b) => { capture_scan_expr(a, shadow, free); capture_scan_expr(b, shadow, free); } ExprKind::As(inner, _) | ExprKind::Is(inner, _) => capture_scan_expr(inner, shadow, free), ExprKind::Member { obj, .. } => capture_scan_expr(obj, shadow, free), ExprKind::Index { obj, index } => { capture_scan_expr(obj, shadow, free); capture_scan_expr(index, shadow, free); } ExprKind::If { cond, then, else_ } => { capture_scan_expr(cond, shadow, free); capture_scan_block(then, shadow, free); capture_scan_else(else_, shadow, free); } ExprKind::IfLet { scrutinee, then, else_, guard, pattern } => { capture_scan_expr(scrutinee, shadow, free); let mut inner_shadow = shadow.clone(); for (n, _, _) in pattern_capture_names(pattern) { inner_shadow.insert(n); } if let Some(g) = guard { capture_scan_expr(g, &mut inner_shadow, free); } capture_scan_block(then, &mut inner_shadow, free); capture_scan_else(else_, shadow, free); } ExprKind::Match { scrutinee, arms } => { capture_scan_expr(scrutinee, shadow, free); for arm in arms { let mut inner_shadow = shadow.clone(); for (n, _, _) in pattern_capture_names(&arm.pattern) { inner_shadow.insert(n); } if let Some(g) = &arm.guard { capture_scan_expr(g, &mut inner_shadow, free); } match &arm.body { MatchArmBody::Expr(be) => capture_scan_expr(be, &mut inner_shadow, free), MatchArmBody::Block(bb) => capture_scan_block(bb, &mut inner_shadow, free), } } } ExprKind::ArrayLit(elems) => { for el in elems { match el { ArrayElem::Item(e) | ArrayElem::Spread(e) => capture_scan_expr(e, shadow, free), } } } ExprKind::MapLit { elems, .. } => { let pairs = crate::ast::MapElem::cloned_pairs(elems); for (k, v) in pairs.iter() { capture_scan_expr(k, shadow, free); capture_scan_expr(v, shadow, free); } } ExprKind::TupleLit(elems) => { for e in elems { capture_scan_expr(e, shadow, free); } } ExprKind::RecordLit { fields, .. } => { for f in fields { if let Some(v) = &f.value { capture_scan_expr(v, shadow, free); } } } ExprKind::InterpolatedStr { parts } => { for p in parts { if let InterpStrPart::Expr { expr, .. } = p { capture_scan_expr(expr, shadow, free); } } } ExprKind::Lambda { params, body, .. } => { let mut inner_shadow = shadow.clone(); for p in params { inner_shadow.insert(p.name.clone()); } capture_scan_expr(body, &mut inner_shadow, free); } ExprKind::ClosureLight { params, body } => { let mut inner_shadow = shadow.clone(); for p in params { inner_shadow.insert(p.name.clone()); } match body { crate::ast::ClosureBody::Expr(be) => capture_scan_expr(be, &mut inner_shadow, free), crate::ast::ClosureBody::Block(bb) => capture_scan_block(bb, &mut inner_shadow, free), } } ExprKind::ClosureFull(sb) => capture_scan_fn_sig_body(sb, shadow, free), ExprKind::For { pattern, iter, body, .. } | ExprKind::ParallelFor { pattern, iter, body, .. } => { capture_scan_expr(iter, shadow, free); let mut inner_shadow = shadow.clone(); for (n, _, _) in pattern_capture_names(pattern) { inner_shadow.insert(n); } capture_scan_block(body, &mut inner_shadow, free); } ExprKind::While { cond, body, .. } => { capture_scan_expr(cond, shadow, free); capture_scan_block(body, shadow, free); } ExprKind::WhileLet { scrutinee, body, guard, pattern, .. } => { capture_scan_expr(scrutinee, shadow, free); let mut inner_shadow = shadow.clone(); for (n, _, _) in pattern_capture_names(pattern) { inner_shadow.insert(n); } if let Some(g) = guard { capture_scan_expr(g, &mut inner_shadow, free); } capture_scan_block(body, &mut inner_shadow, free); } ExprKind::Loop { body, .. } => capture_scan_block(body, shadow, free), ExprKind::Select { arms } => { for arm in arms { match &arm.op { crate::ast::SelectOp::Recv { chan, .. } => capture_scan_expr(chan, shadow, free), crate::ast::SelectOp::Send { chan, value } => { capture_scan_expr(chan, shadow, free); capture_scan_expr(value, shadow, free); } crate::ast::SelectOp::Default => {} } let mut inner_shadow = shadow.clone(); if let Some(g) = &arm.guard { capture_scan_expr(g, &mut inner_shadow, free); } capture_scan_block(&arm.body, &mut inner_shadow, free); } } ExprKind::With { bindings, body } => { for b in bindings { capture_scan_expr(&b.handler, shadow, free); } capture_scan_block(body, shadow, free); } // Nested concurrency constructs — recurse (a nested `spawn`'s OWN // capture-check runs independently when the outer walk reaches it; // here we just need to keep collecting free-var reads for the // OUTER boundary's purposes too, since a name captured by an inner // spawn is ALSO effectively captured by the outer one if not // locally bound in between). ExprKind::Spawn(inner) => capture_scan_expr(inner, shadow, free), ExprKind::Detach(b) | ExprKind::Blocking(b) => capture_scan_block(b, shadow, free), ExprKind::Supervised { body, cancel, deadline, on_timeout } => { if let Some(c) = cancel { capture_scan_expr(c, shadow, free); } if let Some(dl) = deadline { capture_scan_expr(&dl.expr, shadow, free); } if let Some(oh) = on_timeout { capture_scan_expr(oh, shadow, free); } capture_scan_block(body, shadow, free); } ExprKind::Forbid { body, .. } | ExprKind::Realtime { body, .. } => { capture_scan_block(body, shadow, free) } ExprKind::Range { start, end, .. } => { if let Some(s) = start { capture_scan_expr(s, shadow, free); } if let Some(e2) = end { capture_scan_expr(e2, shadow, free); } } ExprKind::Interrupt(v) => { if let Some(e2) = v { capture_scan_expr(e2, shadow, free); } } // Literals / self / path / handler-lits / quantifiers / etc — no // free-variable-capturing sub-structure relevant here. _ => {} } } fn capture_scan_else(else_: &Option<ElseBranch>, shadow: &mut HashSet<String>, free: &mut HashSet<String>) { match else_ { Some(ElseBranch::Block(b)) => capture_scan_block(b, shadow, free), Some(ElseBranch::If(e)) => capture_scan_expr(e, shadow, free), None => {} } } fn capture_scan_fn_sig_body(sb: &FnSigBody, shadow: &mut HashSet<String>, free: &mut HashSet<String>) { let mut inner_shadow = shadow.clone(); for p in &sb.params { inner_shadow.insert(p.name.clone()); } match &sb.body { FnBody::Expr(e) => capture_scan_expr(e, &mut inner_shadow, free), FnBody::Block(b) => capture_scan_block(b, &mut inner_shadow, free), FnBody::External => {} } } /// Plan 238 Ф.1 (D441 §1-2): is `ty` a fn/closure-shaped type (unwrapping the /// transparent binding-modifier wrappers `ro`/`mut`/uninit/`ref`, mirroring /// `share_check::share_rec`'s own unwrap)? Used to find a fn's OWN /// fn-typed parameters for the spawn-tainted-parameter pre-pass. fn typeref_is_func_shaped(ty: &TypeRef) -> bool { match ty { TypeRef::Func { .. } => true, TypeRef::Readonly(inner, _) | TypeRef::Mut(inner, _) | TypeRef::Ref(inner, _) => { typeref_is_func_shaped(inner) } _ => false, } } /// Plan 238 Ф.1 (D441 §1-2): does `b` contain, ANYWHERE (any nesting depth), /// a `spawn`/`detach`/`parallel for`/`blocking` node? Used both by the /// spawn-tainted-parameter pre-pass and by the Ф.3 with-handler check (a /// handler installed around a body with NO fiber boundary at all runs /// synchronously in the parent — capturing `mut` there is ordinary /// sequential code, not a race; gating on this predicate is what keeps the /// (very common) sequential `with Fail[T] = |e| { mut x = .. } { ..no /// spawn.. }` idiom legal). /// /// Conservative UNDER-approximation (documented, safe direction — matches /// this module's stance elsewhere): an unhandled AST shape (e.g. a boundary /// buried inside an array/map/tuple-literal element, or behind a NESTED /// closure literal) can only cause a MISSED detection (false negative, /// i.e. a real violation slips through), never a false positive. fn block_contains_fiber_boundary(b: &Block) -> bool { b.stmts.iter().any(stmt_contains_fiber_boundary) || b.trailing.as_ref().map_or(false, |t| expr_contains_fiber_boundary(t)) } fn stmt_contains_fiber_boundary(s: &Stmt) -> bool { match s { Stmt::Expr(e) => expr_contains_fiber_boundary(e), Stmt::Let(d) => expr_contains_fiber_boundary(&d.value), Stmt::Assign { target, value, .. } => { expr_contains_fiber_boundary(target) || expr_contains_fiber_boundary(value) } Stmt::Return { value, .. } => value.as_ref().map_or(false, expr_contains_fiber_boundary), Stmt::Throw { value, .. } => expr_contains_fiber_boundary(value), Stmt::Defer { body, .. } => expr_contains_fiber_boundary(body), Stmt::ConsumeScope { init, body, .. } => { expr_contains_fiber_boundary(init) || block_contains_fiber_boundary(body) } Stmt::TupleAssign { lhs, rhs, .. } => { lhs.iter().any(expr_contains_fiber_boundary) || rhs.iter().any(expr_contains_fiber_boundary) } Stmt::Const(_) | Stmt::Break(_) | Stmt::Continue(_) | Stmt::AssertStatic { .. } | Stmt::Assume { .. } | Stmt::Apply { .. } | Stmt::Calc { .. } | Stmt::Reveal { .. } => false, } } fn expr_contains_fiber_boundary(e: &Expr) -> bool { match &e.kind { ExprKind::Spawn(_) | ExprKind::Detach(_) | ExprKind::ParallelFor { .. } | ExprKind::Blocking(_) => true, ExprKind::Block(b) => block_contains_fiber_boundary(b), ExprKind::If { cond, then, else_ } => { expr_contains_fiber_boundary(cond) || block_contains_fiber_boundary(then) || else_branch_contains_fiber_boundary(else_) } ExprKind::IfLet { scrutinee, guard, then, else_, .. } => { expr_contains_fiber_boundary(scrutinee) || guard.as_ref().map_or(false, |g| expr_contains_fiber_boundary(g)) || block_contains_fiber_boundary(then) || else_branch_contains_fiber_boundary(else_) } ExprKind::Match { scrutinee, arms } => { expr_contains_fiber_boundary(scrutinee) || arms.iter().any(|a| { a.guard.as_ref().map_or(false, expr_contains_fiber_boundary) || match &a.body { MatchArmBody::Expr(be) => expr_contains_fiber_boundary(be), MatchArmBody::Block(bb) => block_contains_fiber_boundary(bb), } }) } ExprKind::For { iter, body, .. } => { expr_contains_fiber_boundary(iter) || block_contains_fiber_boundary(body) } ExprKind::While { cond, body, .. } => { expr_contains_fiber_boundary(cond) || block_contains_fiber_boundary(body) } ExprKind::WhileLet { scrutinee, guard, body, .. } => { expr_contains_fiber_boundary(scrutinee) || guard.as_ref().map_or(false, |g| expr_contains_fiber_boundary(g)) || block_contains_fiber_boundary(body) } ExprKind::Loop { body, .. } => block_contains_fiber_boundary(body), ExprKind::Supervised { body, .. } => block_contains_fiber_boundary(body), ExprKind::With { body, .. } => block_contains_fiber_boundary(body), ExprKind::Forbid { body, .. } | ExprKind::Realtime { body, .. } => block_contains_fiber_boundary(body), ExprKind::Try(inner) | ExprKind::Bang(inner) | ExprKind::RefArg(inner) => { expr_contains_fiber_boundary(inner) } ExprKind::Coalesce(a, b) => expr_contains_fiber_boundary(a) || expr_contains_fiber_boundary(b), ExprKind::Binary { left, right, .. } => { expr_contains_fiber_boundary(left) || expr_contains_fiber_boundary(right) } ExprKind::Unary { operand, .. } => expr_contains_fiber_boundary(operand), ExprKind::Member { obj, .. } => expr_contains_fiber_boundary(obj), ExprKind::Index { obj, index } => { expr_contains_fiber_boundary(obj) || expr_contains_fiber_boundary(index) } ExprKind::Call { func, args, trailing } => { expr_contains_fiber_boundary(func) || args.iter().any(|a| expr_contains_fiber_boundary(a.expr())) || trailing.as_ref().map_or(false, |t| match t { crate::ast::Trailing::Block(b) => block_contains_fiber_boundary(b), crate::ast::Trailing::LegacyBlockWithParams(tb) => { block_contains_fiber_boundary(&tb.body) } crate::ast::Trailing::Fn(sb) => match &sb.body { FnBody::Block(b) => block_contains_fiber_boundary(b), FnBody::Expr(e) => expr_contains_fiber_boundary(e), FnBody::External => false, }, }) } _ => false, } } fn else_branch_contains_fiber_boundary(else_: &Option<ElseBranch>) -> bool { match else_ { Some(ElseBranch::Block(b)) => block_contains_fiber_boundary(b), Some(ElseBranch::If(e)) => expr_contains_fiber_boundary(e), None => false, } } /// Plan 238 Ф.1 (D441 §1-2): which of `fd`'s OWN fn-typed parameters are /// invoked inside a `spawn`/`detach`/`parallel for`/`blocking` body /// somewhere in `fd`'s definition? Free function (no `CapabilityCtx` need — /// pure AST fact, computed once per fn in the `build` pre-pass). fn spawn_tainted_params_of_fn(fd: &FnDecl) -> HashSet<String> { let func_params: HashSet<String> = fd.params.iter() .filter(|p| typeref_is_func_shaped(&p.ty)) .map(|p| p.name.clone()) .collect(); if func_params.is_empty() { return HashSet::new(); } let mut boundaries: Vec<HashSet<String>> = Vec::new(); match &fd.body { FnBody::Block(b) => collect_fiber_boundary_frees_block(b, &mut boundaries), FnBody::Expr(e) => collect_fiber_boundary_frees_expr(e, &mut boundaries), FnBody::External => {} } let mut tainted = HashSet::new(); for free in &boundaries { for p in &func_params { if free.contains(p) { tainted.insert(p.clone()); } } } tainted } /// A-V10 (D441 §5 №167 closure): whole-module pre-pass — which free fns are /// `#thread_affine` LEAVES, and which free fns TRANSITIVELY (directly or /// through further named calls, in their OWN fiber — see /// `own_fiber_call_names`) reach one? Fixed-point worklist over the named /// free-fn call graph, mirroring `spawn_tainted_params_of_fn`'s "computed /// once in `build`, keyed by fn name" shape (the closest existing /// transitive-inference precedent in this module — neither `infer_effects` /// (№131, per-fn direct-effect detection, explicitly NOT transitive across /// callers per its own doc comment) nor `#pure` (a manually-declared /// attribute, never inferred) is a literal call-graph closure; this is a /// new, small, honestly-documented fixed point built for exactly this /// D441 §5 point-4 gap). /// /// Returns `(leaves, transitive)`: `leaves` = names declared /// `#thread_affine` directly; `transitive[name] = (leaf, first_hop)` for /// every OTHER free fn whose OWN-fiber call graph reaches a leaf, with /// `first_hop` = `name`'s own direct callee that starts the chain (at /// least one intermediate frame, D441 §5 №167 wording — "цепочка подъёма /// (хотя бы один промежуточный фрейм)"). fn thread_affine_closure(module: &Module) -> (HashSet<String>, HashMap<String, (String, String)>) { let mut leaves: HashSet<String> = HashSet::new(); let mut adjacency: HashMap<String, HashSet<String>> = HashMap::new(); for item in &module.items { if let Item::Fn(fd) = item { if fd.thread_affine_attr { leaves.insert(fd.name.clone()); } adjacency.insert(fd.name.clone(), own_fiber_call_names_of_fn(fd)); } } let mut transitive: HashMap<String, (String, String)> = HashMap::new(); if leaves.is_empty() { return (leaves, transitive); } // Standard iterative dataflow: each fn is marked AT MOST once, call // graph is finite ⇒ guaranteed termination, no cycle special-casing // needed (a cycle with no path to a leaf simply never gets marked). let mut changed = true; while changed { changed = false; for (caller, callees) in &adjacency { if leaves.contains(caller) || transitive.contains_key(caller) { continue; } // Deterministic pick among multiple qualifying callees — // sort names first so the chosen `first_hop` doesn't depend on // HashSet iteration order (diagnostic-text determinism). let mut sorted_callees: Vec<&String> = callees.iter().collect(); sorted_callees.sort(); for callee in sorted_callees { if leaves.contains(callee) { transitive.insert(caller.clone(), (callee.clone(), callee.clone())); changed = true; break; } else if let Some((leaf, _)) = transitive.get(callee) { let leaf = leaf.clone(); transitive.insert(caller.clone(), (leaf, callee.clone())); changed = true; break; } } } } (leaves, transitive) } /// A-V10 (D441 §5 №167 closure): named free-fn calls made in `fd`'s OWN /// FIBER — i.e. anywhere in its body EXCEPT inside a nested `spawn`/ /// `detach`/`parallel for`/`blocking` sub-body (those start a FRESH, /// independent fiber, decoupled from whichever fiber called `fd` — a /// wrapper that only ever invokes its `#thread_affine` leaf INSIDE its own /// `spawn` does NOT inherit the leaf's unsafety, since the leaf then runs /// on a brand-new child fiber, not the caller's) and except inside a /// nested closure/handler-literal body (own scope boundary, same /// convention as `collect_raw_effect_ops_in_fn`). Deliberately the SAME /// traversal shape as `expr_contains_fiber_boundary`'s coverage, reused for /// BOTH purposes this wave needs: (1) building the free-fn call graph here, /// and (2) `check_thread_affine_boundary` scans a spawn/detach/parallel-for /// BODY with the identical function — since it also stops at any FURTHER /// nested boundary, a call two levels deep is not double-reported (the /// inner boundary gets its own independent check when `walk_expr` reaches /// it). fn own_fiber_call_names_of_fn(fd: &FnDecl) -> HashSet<String> { let mut out = HashSet::new(); match &fd.body { FnBody::Block(b) => own_fiber_call_names_block(b, &mut out), FnBody::Expr(e) => own_fiber_call_names_expr(e, &mut out), FnBody::External => {} } out } fn own_fiber_call_names_block(b: &Block, out: &mut HashSet<String>) { for s in &b.stmts { own_fiber_call_names_stmt(s, out); } if let Some(t) = &b.trailing { own_fiber_call_names_expr(t, out); } } fn own_fiber_call_names_stmt(s: &Stmt, out: &mut HashSet<String>) { match s { Stmt::Expr(e) => own_fiber_call_names_expr(e, out), Stmt::Let(d) => own_fiber_call_names_expr(&d.value, out), Stmt::Assign { target, value, .. } => { own_fiber_call_names_expr(target, out); own_fiber_call_names_expr(value, out); } Stmt::Return { value, .. } => { if let Some(v) = value { own_fiber_call_names_expr(v, out); } } Stmt::Throw { value, .. } => own_fiber_call_names_expr(value, out), Stmt::Defer { body, .. } => own_fiber_call_names_expr(body, out), Stmt::ConsumeScope { init, body, .. } => { own_fiber_call_names_expr(init, out); own_fiber_call_names_block(body, out); } Stmt::TupleAssign { lhs, rhs, .. } => { for e in lhs { own_fiber_call_names_expr(e, out); } for e in rhs { own_fiber_call_names_expr(e, out); } } Stmt::Const(_) | Stmt::Break(_) | Stmt::Continue(_) | Stmt::AssertStatic { .. } | Stmt::Assume { .. } | Stmt::Apply { .. } | Stmt::Calc { .. } | Stmt::Reveal { .. } => {} } } fn own_fiber_call_names_expr(e: &Expr, out: &mut HashSet<String>) { match &e.kind { ExprKind::Call { func, args, trailing } => { if let ExprKind::Ident(name) = &func.kind { out.insert(name.clone()); } for a in args { own_fiber_call_names_expr(a.expr(), out); } if let Some(t) = trailing { match t { crate::ast::Trailing::Block(b) => own_fiber_call_names_block(b, out), crate::ast::Trailing::LegacyBlockWithParams(tb) => { own_fiber_call_names_block(&tb.body, out) } // Trailing closure-sugar — own scope, skip (mirrors // `collect_raw_effect_ops`'s `Lambda => {}` stance). crate::ast::Trailing::Fn(_) => {} } } } // Fresh, independent fiber — deliberately NOT recursed into (see // fn-level doc comment above). `ParallelFor`'s `iter` still runs in // the CALLER's fiber (evaluated once, before fan-out), so it IS // scanned; only `body` (the per-element fiber) is skipped. ExprKind::Spawn(_) | ExprKind::Detach(_) | ExprKind::Blocking(_) => {} ExprKind::ParallelFor { iter, .. } => own_fiber_call_names_expr(iter, out), ExprKind::Block(b) => own_fiber_call_names_block(b, out), ExprKind::If { cond, then, else_ } => { own_fiber_call_names_expr(cond, out); own_fiber_call_names_block(then, out); match else_ { Some(ElseBranch::Block(b)) => own_fiber_call_names_block(b, out), Some(ElseBranch::If(e2)) => own_fiber_call_names_expr(e2, out), None => {} } } ExprKind::IfLet { scrutinee, guard, then, else_, .. } => { own_fiber_call_names_expr(scrutinee, out); if let Some(g) = guard { own_fiber_call_names_expr(g, out); } own_fiber_call_names_block(then, out); match else_ { Some(ElseBranch::Block(b)) => own_fiber_call_names_block(b, out), Some(ElseBranch::If(e2)) => own_fiber_call_names_expr(e2, out), None => {} } } ExprKind::Match { scrutinee, arms } => { own_fiber_call_names_expr(scrutinee, out); for a in arms { if let Some(g) = &a.guard { own_fiber_call_names_expr(g, out); } match &a.body { MatchArmBody::Expr(be) => own_fiber_call_names_expr(be, out), MatchArmBody::Block(bb) => own_fiber_call_names_block(bb, out), } } } ExprKind::For { iter, body, .. } => { own_fiber_call_names_expr(iter, out); own_fiber_call_names_block(body, out); } ExprKind::While { cond, body, .. } => { own_fiber_call_names_expr(cond, out); own_fiber_call_names_block(body, out); } ExprKind::WhileLet { scrutinee, guard, body, .. } => { own_fiber_call_names_expr(scrutinee, out); if let Some(g) = guard { own_fiber_call_names_expr(g, out); } own_fiber_call_names_block(body, out); } ExprKind::Loop { body, .. } => own_fiber_call_names_block(body, out), ExprKind::Supervised { body, cancel, deadline, on_timeout } => { if let Some(c) = cancel { own_fiber_call_names_expr(c, out); } if let Some(dl) = deadline { own_fiber_call_names_expr(&dl.expr, out); } if let Some(oh) = on_timeout { own_fiber_call_names_expr(oh, out); } own_fiber_call_names_block(body, out); } // Same stance as `expr_contains_fiber_boundary`'s `With` arm — only // `body` is scanned, not the handler expression (handler invocation // timing is a separate, already-covered concern — D441 §3/§5 №168). ExprKind::With { body, .. } => own_fiber_call_names_block(body, out), ExprKind::Forbid { body, .. } | ExprKind::Realtime { body, .. } => { own_fiber_call_names_block(body, out) } ExprKind::Try(inner) | ExprKind::Bang(inner) | ExprKind::RefArg(inner) => { own_fiber_call_names_expr(inner, out) } ExprKind::Coalesce(a, b) => { own_fiber_call_names_expr(a, out); own_fiber_call_names_expr(b, out); } ExprKind::Binary { left, right, .. } => { own_fiber_call_names_expr(left, out); own_fiber_call_names_expr(right, out); } ExprKind::Unary { operand, .. } => own_fiber_call_names_expr(operand, out), ExprKind::Member { obj, .. } => own_fiber_call_names_expr(obj, out), ExprKind::Index { obj, index } => { own_fiber_call_names_expr(obj, out); own_fiber_call_names_expr(index, out); } _ => {} } } /// Collect the free-variable set of EVERY `spawn`/`detach`/`parallel for`/ /// `blocking` body found anywhere within `b` (any nesting depth) into /// `out` (one `HashSet` per boundary found). Mirrors `capture_scan_block`'s /// AST coverage for the container-shaped nodes it needs to descend through /// to FIND boundaries (as opposed to `capture_scan_*`, which folds /// everything transitively into ONE set and never distinguishes "inside a /// boundary" from "outside one"). fn collect_fiber_boundary_frees_block(b: &Block, out: &mut Vec<HashSet<String>>) { for s in &b.stmts { collect_fiber_boundary_frees_stmt(s, out); } if let Some(t) = &b.trailing { collect_fiber_boundary_frees_expr(t, out); } } fn collect_fiber_boundary_frees_stmt(s: &Stmt, out: &mut Vec<HashSet<String>>) { match s { Stmt::Expr(e) => collect_fiber_boundary_frees_expr(e, out), Stmt::Let(d) => collect_fiber_boundary_frees_expr(&d.value, out), Stmt::Assign { target, value, .. } => { collect_fiber_boundary_frees_expr(target, out); collect_fiber_boundary_frees_expr(value, out); } Stmt::Return { value, .. } => { if let Some(v) = value { collect_fiber_boundary_frees_expr(v, out); } } Stmt::Throw { value, .. } => collect_fiber_boundary_frees_expr(value, out), Stmt::Defer { body, .. } => collect_fiber_boundary_frees_expr(body, out), Stmt::ConsumeScope { init, body, .. } => { collect_fiber_boundary_frees_expr(init, out); collect_fiber_boundary_frees_block(body, out); } Stmt::TupleAssign { lhs, rhs, .. } => { for e in lhs { collect_fiber_boundary_frees_expr(e, out); } for e in rhs { collect_fiber_boundary_frees_expr(e, out); } } _ => {} } } fn collect_fiber_boundary_frees_expr(e: &Expr, out: &mut Vec<HashSet<String>>) { match &e.kind { ExprKind::Spawn(inner) => { let mut shadow = HashSet::new(); let mut free = HashSet::new(); capture_scan_expr(inner, &mut shadow, &mut free); out.push(free); collect_fiber_boundary_frees_expr(inner, out); } ExprKind::Detach(b) | ExprKind::Blocking(b) => { let mut shadow = HashSet::new(); let mut free = HashSet::new(); capture_scan_block(b, &mut shadow, &mut free); out.push(free); collect_fiber_boundary_frees_block(b, out); } ExprKind::ParallelFor { iter, body, .. } => { collect_fiber_boundary_frees_expr(iter, out); let mut shadow = HashSet::new(); let mut free = HashSet::new(); capture_scan_block(body, &mut shadow, &mut free); out.push(free); collect_fiber_boundary_frees_block(body, out); } ExprKind::Block(b) => collect_fiber_boundary_frees_block(b, out), ExprKind::If { cond, then, else_ } => { collect_fiber_boundary_frees_expr(cond, out); collect_fiber_boundary_frees_block(then, out); match else_ { Some(ElseBranch::Block(b)) => collect_fiber_boundary_frees_block(b, out), Some(ElseBranch::If(e2)) => collect_fiber_boundary_frees_expr(e2, out), None => {} } } ExprKind::IfLet { scrutinee, guard, then, else_, .. } => { collect_fiber_boundary_frees_expr(scrutinee, out); if let Some(g) = guard { collect_fiber_boundary_frees_expr(g, out); } collect_fiber_boundary_frees_block(then, out); match else_ { Some(ElseBranch::Block(b)) => collect_fiber_boundary_frees_block(b, out), Some(ElseBranch::If(e2)) => collect_fiber_boundary_frees_expr(e2, out), None => {} } } ExprKind::Match { scrutinee, arms } => { collect_fiber_boundary_frees_expr(scrutinee, out); for a in arms { if let Some(g) = &a.guard { collect_fiber_boundary_frees_expr(g, out); } match &a.body { MatchArmBody::Expr(be) => collect_fiber_boundary_frees_expr(be, out), MatchArmBody::Block(bb) => collect_fiber_boundary_frees_block(bb, out), } } } ExprKind::For { iter, body, .. } => { collect_fiber_boundary_frees_expr(iter, out); collect_fiber_boundary_frees_block(body, out); } ExprKind::While { cond, body, .. } => { collect_fiber_boundary_frees_expr(cond, out); collect_fiber_boundary_frees_block(body, out); } ExprKind::WhileLet { scrutinee, guard, body, .. } => { collect_fiber_boundary_frees_expr(scrutinee, out); if let Some(g) = guard { collect_fiber_boundary_frees_expr(g, out); } collect_fiber_boundary_frees_block(body, out); } ExprKind::Loop { body, .. } => collect_fiber_boundary_frees_block(body, out), ExprKind::Supervised { body, cancel, deadline, on_timeout } => { if let Some(c) = cancel { collect_fiber_boundary_frees_expr(c, out); } if let Some(dl) = deadline { collect_fiber_boundary_frees_expr(&dl.expr, out); } if let Some(oh) = on_timeout { collect_fiber_boundary_frees_expr(oh, out); } collect_fiber_boundary_frees_block(body, out); } ExprKind::With { bindings, body } => { // A-V10 (D441 §5 №168 closure — install-position transitivity): // when `body` contains a fiber boundary (same gate the Ф.3 // with-handler check itself uses — `block_contains_fiber_ // boundary`), the HANDLER EXPRESSION is itself an install- // position crossing: a fn-typed parameter `h` used as // `with X = h { ...spawn... }` is exactly as tainted as one // literally INVOKED inside the boundary (Ф.1а's original // scan). Push the handler's own free-variable set as a // boundary set too, so `spawn_tainted_params_of_fn` (which // just tests `free.contains(param_name)`) picks up `h` the // SAME way it already picks up a spawn-tainted callee param — // no separate pre-pass needed. A bare `Ident("h")` handler's // free-var set from `capture_scan_expr` is just `{h}`, which // is exactly what's needed here. if block_contains_fiber_boundary(body) { for b in bindings { let mut shadow = HashSet::new(); let mut free = HashSet::new(); capture_scan_expr(&b.handler, &mut shadow, &mut free); out.push(free); } } for b in bindings { collect_fiber_boundary_frees_expr(&b.handler, out); } collect_fiber_boundary_frees_block(body, out); } ExprKind::Forbid { body, .. } | ExprKind::Realtime { body, .. } => { collect_fiber_boundary_frees_block(body, out) } ExprKind::Try(inner) | ExprKind::Bang(inner) | ExprKind::RefArg(inner) => { collect_fiber_boundary_frees_expr(inner, out) } ExprKind::Coalesce(a, b) => { collect_fiber_boundary_frees_expr(a, out); collect_fiber_boundary_frees_expr(b, out); } ExprKind::Binary { left, right, .. } => { collect_fiber_boundary_frees_expr(left, out); collect_fiber_boundary_frees_expr(right, out); } ExprKind::Unary { operand, .. } => collect_fiber_boundary_frees_expr(operand, out), ExprKind::Member { obj, .. } => collect_fiber_boundary_frees_expr(obj, out), ExprKind::Index { obj, index } => { collect_fiber_boundary_frees_expr(obj, out); collect_fiber_boundary_frees_expr(index, out); } ExprKind::Call { func, args, trailing } => { collect_fiber_boundary_frees_expr(func, out); for a in args { collect_fiber_boundary_frees_expr(a.expr(), out); } if let Some(t) = trailing { match t { crate::ast::Trailing::Block(b) => collect_fiber_boundary_frees_block(b, out), crate::ast::Trailing::LegacyBlockWithParams(tb) => { collect_fiber_boundary_frees_block(&tb.body, out) } crate::ast::Trailing::Fn(sb) => match &sb.body { FnBody::Block(b) => collect_fiber_boundary_frees_block(b, out), FnBody::Expr(e2) => collect_fiber_boundary_frees_expr(e2, out), FnBody::External => {} }, } } } _ => {} } } /// Plan S1a (D441 §5 "class «замыкание как ПОЛЕ структуры»" — №117/№242§3): /// pre-pass, computed once in `CapabilityCtx::build` (same timing as /// `spawn_tainted_params`/`thread_affine_closure`) — which `(TypeName, /// field)` pairs have their value INVOKED somewhere inside a `spawn`/ /// `detach`/`parallel for`/`blocking` body within one of `TypeName`'s OWN /// methods (`Item::Fn` with `receiver: Some(_)` — Nova has no separate /// `Item::Impl`, methods ARE `Item::Fn`). Two shapes recognized: /// /// - **bare fn-typed field, called directly**: `spawn { @on_error() }` — /// `@field()`/`self.field()` called while inside the boundary; /// - **container-of-fn field, driving the boundary through a loop /// variable**: `for t in @tasks { spawn { t() } }` (the `BackgroundTasks. /// drain()` shape — measured live pattern, D441 §5) or the one-construct /// collapse `parallel for t in @tasks { t() }`. The loop must iterate a /// DIRECT `@field` read (no intermediate `let`); the boundary must call /// the loop's OWN pattern variable by its bare name. /// /// Consulted at every WRITE site into such a field (assignment / `.push`- /// class mutator call / record literal) so a closure stored there gets the /// same share-safety check `flag_boundary_captures` already runs for every /// other crossing point (a/б/в) — see `check_field_sink_write`. /// /// **Documented V1 limits (honest, not silent — companion D441 §5 entry /// required before landing):** no cross-method transitivity (a field tainted /// via a boundary in method B is NOT inherited by method A calling B); no /// 2+-hop container indirection (`let ts = @tasks; for t in ts {..}` is /// invisible — the iterable must be the direct `@field` read, mirrors the /// same snapshot-only depth limit §3(а)/(в) already accept); receiver of /// unknown static type — no-op. /// p-s1a2 (№289 — S1a adversarial-pass gap): whole-module pre-pass — which /// `(TypeName, field)` pairs hold a fn/closure VALUE that gets INVOKED /// inside a `spawn`/`detach`/`parallel for`/`blocking` boundary ANYWHERE in /// the module. The original S1a version only walked `Item::Fn` with a /// `receiver`, and only recognized `@field()` (a direct `SelfAccess` /// member-call) as a "call the field" shape — that is why `spawn { (h.f)() /// }` at a bare test-level `ro h = Holder.new(..)` slipped through: no /// method, no `self`, so the self-only match never fired (parenthesizing /// `h.f` before the call is NOT a distinct AST shape — this grammar has no /// `Paren` node, so `(h.f)()` and `h.f()` parse identically; the actual gap /// was scope, not syntax). /// /// This version: /// - scans EVERY function-shaped body: instance methods, STATIC methods /// (`fn Type.new(..)` — `fd.receiver` is `Some` for these too, see the /// parser's `ReceiverKind::Static` dispatch; only truly receiver-less /// free fns get `self_ty = None`), AND `Item::Test` bodies (the exact /// scope `spawn { (h.f)() }` lives in); /// - resolves the general `obj.field()` call shape for `obj` = `self`, a /// local binding of syntactically-known type (`var_types`, seeded from /// fn params / `let` type annotations / ctor-call sketches), one level of /// nested-field access (`o.inner.f`), or a `Vec[T]`/`[]T` index /// (`v[i].f`) — not just `@field()`; /// - generalizes the old single-slot `loop_var` (for-loop pattern var /// reading `@field`) into a full `field_bindings` map (name → `(owner, /// field)`) so `let g = h.f; g()` is the SAME mechanism, not a special /// case, and works for an arbitrary resolved `obj`, not just `self`; /// - additionally recognizes the "field-read passed as a call ARGUMENT" /// crossing (`spawn { call_it(h.f) }` where `call_it`'s OWN body calls /// its parameter directly, `fn call_it(g fn()->()) => g()`) via /// `directly_called_param_positions_of_module`. fn spawn_tainted_fields_of_module(module: &Module) -> HashSet<(String, String)> { let mut type_decls: HashMap<String, &TypeDecl> = HashMap::new(); for item in &module.items { if let Item::Type(t) = item { type_decls.insert(t.name.clone(), t); } } for ext_mod in crate::codegen::external_registry::builtin_sig_modules() { for item in &ext_mod.items { if let Item::Type(td) = item { type_decls.entry(td.name.clone()).or_insert(td); } } } let called_params = directly_called_param_positions_of_module(module); let mut out: HashSet<(String, String)> = HashSet::new(); for item in &module.items { match item { Item::Fn(fd) => { let self_ty: Option<String> = fd.receiver.as_ref().map(|r| r.type_name.clone()); let mut var_types: HashMap<String, TypeRef> = HashMap::new(); for p in &fd.params { var_types.insert(p.name.clone(), p.ty.clone()); } let field_bindings: HashMap<String, (String, String)> = HashMap::new(); match &fd.body { FnBody::Block(b) => field_taint_block( b, self_ty.as_deref(), false, var_types, field_bindings, &type_decls, &called_params, &mut out, ), FnBody::Expr(e) => field_taint_expr( e, self_ty.as_deref(), false, &var_types, &field_bindings, &type_decls, &called_params, &mut out, ), FnBody::External => {} } } Item::Test(t) => field_taint_block( &t.body, None, false, HashMap::new(), HashMap::new(), &type_decls, &called_params, &mut out, ), _ => {} } } out } /// Resolve the STATIC type name of an arbitrary object expression: `self` /// (via `self_ty`), a local binding of syntactically-known type /// (`var_types`), one level+ of nested field access (`o.inner` — looks up /// `inner`'s declared field type on `o`'s owner record), or a `Vec[T]`/ /// `[]T` index (`v[i]` — element type `T`). Conservative: an unresolved /// shape (generic param, unannotated non-ctor `let`, deeper chains) returns /// `None` — a missed detection (false negative), never a false positive, /// matching this module's stance everywhere else in the D441 §5 machinery. fn resolve_owner_typeref( e: &Expr, self_ty: Option<&str>, var_types: &HashMap<String, TypeRef>, type_decls: &HashMap<String, &TypeDecl>, ) -> Option<TypeRef> { match &e.kind { ExprKind::SelfAccess => self_ty.map(|s| TypeRef::Named { path: vec![s.to_string()], generics: vec![], span: e.span, }), ExprKind::Ident(name) => var_types.get(name).cloned(), ExprKind::Member { obj, name: field } => { let owner_ref = resolve_owner_typeref(obj, self_ty, var_types, type_decls)?; let owner_name = TypeCheckCtx::typeref_named_base(&owner_ref)?; field_type_of(owner_name, field, type_decls) } ExprKind::Index { obj, .. } => { let owner_ref = resolve_owner_typeref(obj, self_ty, var_types, type_decls)?; typeref_element_type(&owner_ref).cloned() } _ => None, } } /// Name-only wrapper around `resolve_owner_typeref` — every existing /// `(TypeName, field)` taint-set entry is keyed by plain `String`. fn resolve_owner_type( e: &Expr, self_ty: Option<&str>, var_types: &HashMap<String, TypeRef>, type_decls: &HashMap<String, &TypeDecl>, ) -> Option<String> { resolve_owner_typeref(e, self_ty, var_types, type_decls) .as_ref() .and_then(TypeCheckCtx::typeref_named_base) .map(|s| s.to_string()) } /// Is `e` itself a field-READ (`obj.field`, resolved via `resolve_owner_ /// type`), or a bare name previously bound to one via `field_bindings` /// (`let g = h.f` / a `for`-loop's own pattern variable)? One recognizer /// for both the direct-call shape (`func` in a `Call`) and the /// pass-as-argument shape (`args[i]` in a `Call`) — see call sites in /// `field_taint_expr`. fn resolve_field_read( e: &Expr, self_ty: Option<&str>, var_types: &HashMap<String, TypeRef>, field_bindings: &HashMap<String, (String, String)>, type_decls: &HashMap<String, &TypeDecl>, ) -> Option<(String, String)> { match &e.kind { ExprKind::Member { obj, name: field } => { let owner = resolve_owner_type(obj, self_ty, var_types, type_decls)?; Some((owner, field.clone())) } ExprKind::Ident(name) => field_bindings.get(name).cloned(), _ => None, } } /// `Vec[T]`/`[]T` element type — the ONLY container shape resolved (V1, /// same documented depth limit as the rest of this pre-pass). fn typeref_element_type(t: &TypeRef) -> Option<&TypeRef> { match t { TypeRef::Array(inner, _) => Some(inner), TypeRef::Named { path, generics, .. } if path.last().map(|s| s.as_str()) == Some("Vec") && !generics.is_empty() => { Some(&generics[0]) } TypeRef::Readonly(inner, _) | TypeRef::Mut(inner, _) | TypeRef::Uninit(inner, _) => { typeref_element_type(inner) } _ => None, } } /// Declared type of `owner.field`, looked up in `type_decls` (`Record` / /// `NamedTuple` kinds only — the only two `TypeDeclKind`s with named, /// individually-typed fields). fn field_type_of(owner: &str, field: &str, type_decls: &HashMap<String, &TypeDecl>) -> Option<TypeRef> { let td = *type_decls.get(owner)?; match &td.kind { TypeDeclKind::Record(fields) => fields.iter().find(|f| f.name == field).map(|f| f.ty.clone()), TypeDeclKind::NamedTuple(fields) => fields.iter().find(|f| f.name == field).map(|f| f.ty.clone()), _ => None, } } /// p-s1a2 (№289 form 4/5): syntactic type-sketch for an unannotated `let` /// init expression, richer than `capture_syntactic_init_type` (which /// collapses a ctor call to a bare `Named(name)`, losing generics) — /// needed specifically to keep a `Vec[Holder]` local's ELEMENT type visible /// for a later `v[i].field` index-then-field-call chain. Recognizes /// `Type.new(..)` / `Type.of(..)` / `Type.from(..)` (the project's ctor /// naming convention) and their turbofish form `Type[T1, T2].ctor(..)`; /// anything else falls back to the (generics-losing but broader) /// `capture_syntactic_init_type`. fn syntactic_ctor_typeref(e: &Expr, type_decls: &HashMap<String, &TypeDecl>) -> Option<TypeRef> { if let ExprKind::Call { func, .. } = &e.kind { if let ExprKind::Member { obj, name: method } = &func.kind { if matches!(method.as_str(), "new" | "of" | "from") { let (base, generics) = match &obj.kind { ExprKind::TurboFish { base, type_args } => (&base.kind, type_args.clone()), other => (other, Vec::new()), }; if let ExprKind::Ident(tyname) = base { return Some(TypeRef::Named { path: vec![tyname.clone()], generics, span: e.span, }); } } } } capture_syntactic_init_type(e, type_decls) } /// p-s1a2 (№289 form 3): for every free fn (no receiver), which of its OWN /// parameter POSITIONS get referenced by name anywhere in its body — /// `fn call_it(g fn()->()) => g()` calls `g` directly, with NO nested /// `spawn` inside `call_it` itself; the boundary is at call_it's OWN call /// site (`spawn { call_it(h.f) }`), so the "called inside a boundary" /// pattern this pre-pass otherwise relies on never appears inside /// `call_it`'s body at all. /// /// Reuses the existing free-variable scanner (`capture_scan_expr`/ /// `capture_scan_block` — the project's general "names referenced, not /// locally shadowed" collector, already used for closure/spawn-capture /// analysis elsewhere in this file) rather than a bespoke walker: a fn's /// OWN params are never locally shadowed by anything inside its own body, /// so every reference to a func-shaped param name shows up as "free" here. /// This is a deliberate, bounded OVER-approximation (referenced ⊇ called — /// e.g. `fn store_it(g fn()->()) => registry.push(g)` also counts `g` as /// "referenced", even though it is stored rather than invoked): consistent /// with this suite's "escaping ⇔ dangerous" stance (№242§3) rather than a /// stray false positive, and matches the project's other name-heuristic /// approximations (`FIELD_SINK_MUTATORS`, `chan.send`). fn directly_called_param_positions_of_module(module: &Module) -> HashMap<String, HashSet<usize>> { let mut out: HashMap<String, HashSet<usize>> = HashMap::new(); for item in &module.items { if let Item::Fn(fd) = item { if fd.receiver.is_some() { continue; } let func_param_names: HashSet<&str> = fd.params.iter() .filter(|p| typeref_is_func_shaped(&p.ty)) .map(|p| p.name.as_str()) .collect(); if func_param_names.is_empty() { continue; } let mut shadow = HashSet::new(); let mut free = HashSet::new(); match &fd.body { FnBody::Block(b) => capture_scan_block(b, &mut shadow, &mut free), FnBody::Expr(e) => capture_scan_expr(e, &mut shadow, &mut free), FnBody::External => {} } let positions: HashSet<usize> = fd.params.iter().enumerate() .filter(|(_, p)| func_param_names.contains(p.name.as_str()) && free.contains(&p.name)) .map(|(i, _)| i) .collect(); if !positions.is_empty() { out.insert(fd.name.clone(), positions); } } } out } fn field_taint_block( b: &Block, self_ty: Option<&str>, in_boundary: bool, mut var_types: HashMap<String, TypeRef>, mut field_bindings: HashMap<String, (String, String)>, type_decls: &HashMap<String, &TypeDecl>, called_params: &HashMap<String, HashSet<usize>>, out: &mut HashSet<(String, String)>, ) { for s in &b.stmts { field_taint_stmt( s, self_ty, in_boundary, &var_types, &field_bindings, type_decls, called_params, out, ); // p-s1a2 (№289): `ro/mut x = obj.field` — a plain `let` reading a // field directly INTO A LOCAL, called later in a NESTED boundary // within the same block, is the SAME crossing shape as a for-loop's // own pattern variable — one `let` instead of one loop iteration. // Generalized from S1a's self-only, single-slot `loop_var` to a // full map keyed by binding name, resolved for ANY known-type // `obj` (not just `self`). Shadows any prior entry of the SAME name // for the REST of this block only — one level of tracking, matching // this pre-pass's other conservative depth limits. if let Stmt::Let(d) = s { if let Some(name) = simple_ident_pattern(&d.pattern) { if let Some(fr) = resolve_field_read(&d.value, self_ty, &var_types, &field_bindings, type_decls) { field_bindings.insert(name.to_string(), fr); } else if let Some(ty) = d.ty.clone().or_else(|| syntactic_ctor_typeref(&d.value, type_decls)) { var_types.insert(name.to_string(), ty); } } } } if let Some(t) = &b.trailing { field_taint_expr(t, self_ty, in_boundary, &var_types, &field_bindings, type_decls, called_params, out); } } fn field_taint_stmt( s: &Stmt, self_ty: Option<&str>, in_boundary: bool, var_types: &HashMap<String, TypeRef>, field_bindings: &HashMap<String, (String, String)>, type_decls: &HashMap<String, &TypeDecl>, called_params: &HashMap<String, HashSet<usize>>, out: &mut HashSet<(String, String)>, ) { match s { Stmt::Expr(e) => field_taint_expr(e, self_ty, in_boundary, var_types, field_bindings, type_decls, called_params, out), Stmt::Let(d) => field_taint_expr(&d.value, self_ty, in_boundary, var_types, field_bindings, type_decls, called_params, out), Stmt::Assign { target, value, .. } => { field_taint_expr(target, self_ty, in_boundary, var_types, field_bindings, type_decls, called_params, out); field_taint_expr(value, self_ty, in_boundary, var_types, field_bindings, type_decls, called_params, out); } Stmt::Return { value, .. } => { if let Some(v) = value { field_taint_expr(v, self_ty, in_boundary, var_types, field_bindings, type_decls, called_params, out); } } Stmt::Throw { value, .. } => field_taint_expr(value, self_ty, in_boundary, var_types, field_bindings, type_decls, called_params, out), Stmt::Defer { body, .. } => field_taint_expr(body, self_ty, in_boundary, var_types, field_bindings, type_decls, called_params, out), Stmt::ConsumeScope { init, body, .. } => { field_taint_expr(init, self_ty, in_boundary, var_types, field_bindings, type_decls, called_params, out); field_taint_block(body, self_ty, in_boundary, var_types.clone(), field_bindings.clone(), type_decls, called_params, out); } Stmt::TupleAssign { lhs, rhs, .. } => { for e in lhs { field_taint_expr(e, self_ty, in_boundary, var_types, field_bindings, type_decls, called_params, out); } for e in rhs { field_taint_expr(e, self_ty, in_boundary, var_types, field_bindings, type_decls, called_params, out); } } _ => {} } } fn field_taint_expr( e: &Expr, self_ty: Option<&str>, in_boundary: bool, var_types: &HashMap<String, TypeRef>, field_bindings: &HashMap<String, (String, String)>, type_decls: &HashMap<String, &TypeDecl>, called_params: &HashMap<String, HashSet<usize>>, out: &mut HashSet<(String, String)>, ) { match &e.kind { ExprKind::Call { func, args, trailing } => { if in_boundary { // Direct field-read call: `obj.field()` (self / local var / // nested member / `Vec`-index), OR a name previously bound // via `let g = obj.field` / a `for`-loop's own pattern var // (`field_bindings`) — ONE recognizer for both the S1a // `@field()`-only shape AND the general form (p-s1a2 №289 // forms 1/2/4/5). if let Some((owner, field)) = resolve_field_read(func, self_ty, var_types, field_bindings, type_decls) { out.insert((owner, field)); } // p-s1a2 (№289 form 3): `spawn { call_it(h.f) }` where // `call_it` directly calls its own parameter — the closure // crosses as a plain call ARGUMENT, not via storage. if let ExprKind::Ident(callee) = &func.kind { if let Some(positions) = called_params.get(callee) { for (i, a) in args.iter().enumerate() { if !positions.contains(&i) { continue; } if let Some((owner, field)) = resolve_field_read(a.expr(), self_ty, var_types, field_bindings, type_decls) { out.insert((owner, field)); } } } } } field_taint_expr(func, self_ty, in_boundary, var_types, field_bindings, type_decls, called_params, out); for a in args { field_taint_expr(a.expr(), self_ty, in_boundary, var_types, field_bindings, type_decls, called_params, out); } if let Some(t) = trailing { match t { crate::ast::Trailing::Block(b) => field_taint_block(b, self_ty, in_boundary, var_types.clone(), field_bindings.clone(), type_decls, called_params, out), crate::ast::Trailing::LegacyBlockWithParams(tb) => { field_taint_block(&tb.body, self_ty, in_boundary, var_types.clone(), field_bindings.clone(), type_decls, called_params, out) } crate::ast::Trailing::Fn(_) => {} } } } ExprKind::Spawn(inner) => field_taint_expr(inner, self_ty, true, var_types, field_bindings, type_decls, called_params, out), ExprKind::Detach(b) | ExprKind::Blocking(b) => field_taint_block(b, self_ty, true, var_types.clone(), field_bindings.clone(), type_decls, called_params, out), ExprKind::ParallelFor { pattern, iter, body, .. } => { field_taint_expr(iter, self_ty, in_boundary, var_types, field_bindings, type_decls, called_params, out); let mut fb = field_bindings.clone(); if let (Some(fr), Some(name)) = ( resolve_field_read(iter, self_ty, var_types, field_bindings, type_decls), simple_ident_pattern(pattern), ) { fb.insert(name.to_string(), fr); } // `body` IS the per-element boundary (D441 §0 `ParallelFor` // desugar doc: `supervised { for x in iter { spawn { body } } }`). field_taint_block(body, self_ty, true, var_types.clone(), fb, type_decls, called_params, out); } ExprKind::For { pattern, iter, body, .. } => { field_taint_expr(iter, self_ty, in_boundary, var_types, field_bindings, type_decls, called_params, out); let mut fb = field_bindings.clone(); if let (Some(fr), Some(name)) = ( resolve_field_read(iter, self_ty, var_types, field_bindings, type_decls), simple_ident_pattern(pattern), ) { fb.insert(name.to_string(), fr); } // NOT itself a boundary — `in_boundary` unchanged; a nested // `spawn`/`detach`/`blocking` further down flips it to true // (the `BackgroundTasks.drain()` shape: `for t in @tasks { // spawn { t() } }`). field_taint_block(body, self_ty, in_boundary, var_types.clone(), fb, type_decls, called_params, out); } ExprKind::Block(b) => field_taint_block(b, self_ty, in_boundary, var_types.clone(), field_bindings.clone(), type_decls, called_params, out), ExprKind::If { cond, then, else_ } => { field_taint_expr(cond, self_ty, in_boundary, var_types, field_bindings, type_decls, called_params, out); field_taint_block(then, self_ty, in_boundary, var_types.clone(), field_bindings.clone(), type_decls, called_params, out); match else_ { Some(ElseBranch::Block(b)) => field_taint_block(b, self_ty, in_boundary, var_types.clone(), field_bindings.clone(), type_decls, called_params, out), Some(ElseBranch::If(e2)) => field_taint_expr(e2, self_ty, in_boundary, var_types, field_bindings, type_decls, called_params, out), None => {} } } ExprKind::IfLet { scrutinee, guard, then, else_, .. } => { field_taint_expr(scrutinee, self_ty, in_boundary, var_types, field_bindings, type_decls, called_params, out); if let Some(g) = guard { field_taint_expr(g, self_ty, in_boundary, var_types, field_bindings, type_decls, called_params, out); } field_taint_block(then, self_ty, in_boundary, var_types.clone(), field_bindings.clone(), type_decls, called_params, out); match else_ { Some(ElseBranch::Block(b)) => field_taint_block(b, self_ty, in_boundary, var_types.clone(), field_bindings.clone(), type_decls, called_params, out), Some(ElseBranch::If(e2)) => field_taint_expr(e2, self_ty, in_boundary, var_types, field_bindings, type_decls, called_params, out), None => {} } } ExprKind::Match { scrutinee, arms } => { field_taint_expr(scrutinee, self_ty, in_boundary, var_types, field_bindings, type_decls, called_params, out); for a in arms { if let Some(g) = &a.guard { field_taint_expr(g, self_ty, in_boundary, var_types, field_bindings, type_decls, called_params, out); } match &a.body { MatchArmBody::Expr(be) => field_taint_expr(be, self_ty, in_boundary, var_types, field_bindings, type_decls, called_params, out), MatchArmBody::Block(bb) => field_taint_block(bb, self_ty, in_boundary, var_types.clone(), field_bindings.clone(), type_decls, called_params, out), } } } ExprKind::While { cond, body, .. } => { field_taint_expr(cond, self_ty, in_boundary, var_types, field_bindings, type_decls, called_params, out); field_taint_block(body, self_ty, in_boundary, var_types.clone(), field_bindings.clone(), type_decls, called_params, out); } ExprKind::WhileLet { scrutinee, guard, body, .. } => { field_taint_expr(scrutinee, self_ty, in_boundary, var_types, field_bindings, type_decls, called_params, out); if let Some(g) = guard { field_taint_expr(g, self_ty, in_boundary, var_types, field_bindings, type_decls, called_params, out); } field_taint_block(body, self_ty, in_boundary, var_types.clone(), field_bindings.clone(), type_decls, called_params, out); } ExprKind::Loop { body, .. } => field_taint_block(body, self_ty, in_boundary, var_types.clone(), field_bindings.clone(), type_decls, called_params, out), ExprKind::Supervised { body, cancel, deadline, on_timeout } => { if let Some(c) = cancel { field_taint_expr(c, self_ty, in_boundary, var_types, field_bindings, type_decls, called_params, out); } if let Some(dl) = deadline { field_taint_expr(&dl.expr, self_ty, in_boundary, var_types, field_bindings, type_decls, called_params, out); } if let Some(oh) = on_timeout { field_taint_expr(oh, self_ty, in_boundary, var_types, field_bindings, type_decls, called_params, out); } field_taint_block(body, self_ty, in_boundary, var_types.clone(), field_bindings.clone(), type_decls, called_params, out); } ExprKind::With { bindings, body } => { for b in bindings { field_taint_expr(&b.handler, self_ty, in_boundary, var_types, field_bindings, type_decls, called_params, out); } field_taint_block(body, self_ty, in_boundary, var_types.clone(), field_bindings.clone(), type_decls, called_params, out); } ExprKind::Forbid { body, .. } | ExprKind::Realtime { body, .. } => { field_taint_block(body, self_ty, in_boundary, var_types.clone(), field_bindings.clone(), type_decls, called_params, out) } ExprKind::Try(inner) | ExprKind::Bang(inner) | ExprKind::RefArg(inner) => { field_taint_expr(inner, self_ty, in_boundary, var_types, field_bindings, type_decls, called_params, out) } ExprKind::Coalesce(a, b) => { field_taint_expr(a, self_ty, in_boundary, var_types, field_bindings, type_decls, called_params, out); field_taint_expr(b, self_ty, in_boundary, var_types, field_bindings, type_decls, called_params, out); } ExprKind::Binary { left, right, .. } => { field_taint_expr(left, self_ty, in_boundary, var_types, field_bindings, type_decls, called_params, out); field_taint_expr(right, self_ty, in_boundary, var_types, field_bindings, type_decls, called_params, out); } ExprKind::Unary { operand, .. } => field_taint_expr(operand, self_ty, in_boundary, var_types, field_bindings, type_decls, called_params, out), ExprKind::Member { obj, .. } => field_taint_expr(obj, self_ty, in_boundary, var_types, field_bindings, type_decls, called_params, out), ExprKind::Index { obj, index } => { field_taint_expr(obj, self_ty, in_boundary, var_types, field_bindings, type_decls, called_params, out); field_taint_expr(index, self_ty, in_boundary, var_types, field_bindings, type_decls, called_params, out); } _ => {} } } /// A `for`/`parallel for`/`let` pattern that binds exactly one bare name /// (the common `for t in ..` / `ro t = ..` case) — zero-copy match on /// `Pattern::Ident` directly (narrower than `pattern_capture_names`, which /// also unpacks a single-element tuple/record destructure to one name; /// restricting to the literal bare-ident shape is the common case and /// keeps this borrowed rather than allocating). fn simple_ident_pattern(p: &Pattern) -> Option<&str> { if let Pattern::Ident { name, .. } = p { Some(name.as_str()) } else { None } } /// Plan S1a (D441 §5 "closure-as-field" — №117 wrapper-method shape, the /// owner's original report `bg.add(|| { log.push(..) })`): one hop further /// than `spawn_tainted_fields_of_module` — per `(TypeName, method_name)`, /// which of that method's OWN parameter names its body writes into an /// already-tainted field. DEPENDS on `tainted_fields` (computed first, /// sequentially — one documented hop, not a fixed-point closure). fn spawn_tainted_method_params_of_module( module: &Module, tainted_fields: &HashSet<(String, String)>, ) -> HashMap<(String, String), HashSet<String>> { let mut out: HashMap<(String, String), HashSet<String>> = HashMap::new(); for item in &module.items { if let Item::Fn(fd) = item { if let Some(recv) = &fd.receiver { let mut written: HashSet<String> = HashSet::new(); match &fd.body { FnBody::Block(b) => field_write_param_scan_block(b, &recv.type_name, tainted_fields, &mut written), FnBody::Expr(e) => field_write_param_scan_expr(e, &recv.type_name, tainted_fields, &mut written), FnBody::External => {} } // Keep only names that are actually THIS fn's own params — // `field_write_param_scan_*` collects any bare-Ident value // written to a tainted field, some of which may be a local // `let`, not a parameter (those are handled by the DIRECT // write-site check already, no need to double-report here). let param_names: HashSet<&str> = fd.params.iter().map(|p| p.name.as_str()).collect(); let tainted: HashSet<String> = written.into_iter().filter(|n| param_names.contains(n.as_str())).collect(); if !tainted.is_empty() { out.entry((recv.type_name.clone(), fd.name.clone())) .or_insert_with(HashSet::new) .extend(tainted); } } } } out } fn field_write_param_scan_block( b: &Block, self_ty: &str, tainted: &HashSet<(String, String)>, out: &mut HashSet<String>, ) { for s in &b.stmts { field_write_param_scan_stmt(s, self_ty, tainted, out); } if let Some(t) = &b.trailing { field_write_param_scan_expr(t, self_ty, tainted, out); } } fn field_write_param_scan_stmt( s: &Stmt, self_ty: &str, tainted: &HashSet<(String, String)>, out: &mut HashSet<String>, ) { match s { Stmt::Expr(e) => field_write_param_scan_expr(e, self_ty, tainted, out), Stmt::Let(d) => field_write_param_scan_expr(&d.value, self_ty, tainted, out), Stmt::Assign { target, value, .. } => { field_write_param_scan_expr(target, self_ty, tainted, out); field_write_param_scan_expr(value, self_ty, tainted, out); if let ExprKind::Member { obj, name: field } = &target.kind { if matches!(obj.kind, ExprKind::SelfAccess) && tainted.contains(&(self_ty.to_string(), field.clone())) { if let ExprKind::Ident(vname) = &value.kind { out.insert(vname.clone()); } } } } Stmt::Return { value, .. } => { if let Some(v) = value { field_write_param_scan_expr(v, self_ty, tainted, out); } } Stmt::Throw { value, .. } => field_write_param_scan_expr(value, self_ty, tainted, out), Stmt::Defer { body, .. } => field_write_param_scan_expr(body, self_ty, tainted, out), Stmt::ConsumeScope { init, body, .. } => { field_write_param_scan_expr(init, self_ty, tainted, out); field_write_param_scan_block(body, self_ty, tainted, out); } Stmt::TupleAssign { lhs, rhs, .. } => { for e in lhs { field_write_param_scan_expr(e, self_ty, tainted, out); } for e in rhs { field_write_param_scan_expr(e, self_ty, tainted, out); } } _ => {} } } fn field_write_param_scan_expr( e: &Expr, self_ty: &str, tainted: &HashSet<(String, String)>, out: &mut HashSet<String>, ) { match &e.kind { ExprKind::Call { func, args, trailing } => { const FIELD_SINK_MUTATORS: &[&str] = &["push", "append", "add", "insert", "push_back", "push_front", "set"]; if let ExprKind::Member { obj: recv_expr, name: method } = &func.kind { if FIELD_SINK_MUTATORS.contains(&method.as_str()) { if let ExprKind::Member { obj, name: field } = &recv_expr.kind { if matches!(obj.kind, ExprKind::SelfAccess) && tainted.contains(&(self_ty.to_string(), field.clone())) { if let Some(last) = args.last() { if let ExprKind::Ident(vname) = &last.expr().kind { out.insert(vname.clone()); } } } } } } field_write_param_scan_expr(func, self_ty, tainted, out); for a in args { field_write_param_scan_expr(a.expr(), self_ty, tainted, out); } if let Some(t) = trailing { match t { crate::ast::Trailing::Block(b) => field_write_param_scan_block(b, self_ty, tainted, out), crate::ast::Trailing::LegacyBlockWithParams(tb) => { field_write_param_scan_block(&tb.body, self_ty, tainted, out) } crate::ast::Trailing::Fn(_) => {} } } } ExprKind::RecordLit { fields, type_name, .. } => { // p-s1a2 (№289): a BARE record literal (no explicit type-name // prefix — `{ f }`, not `Holder { f }`) is the OVERWHELMINGLY // idiomatic `.new()`/ctor body style across this entire corpus // (`fn Holder.new(f fn()->()) -> Holder => { f }` — grep any // `*.nv` ctor). Its `type_name` field is `None`; the type is // established purely by CONTEXT (the enclosing fn's declared // return type / receiver). This pre-pass has no general type // inference, so it falls back to `self_ty` — the enclosing // method's OWN receiver type, which for a `.new()`/ctor-style // method IS the constructed type in the overwhelming common // case. Without this fallback EVERY idiomatic ctor's bare-brace // return was invisible to this write-site check (found while // closing №289 — the S1a-era fixtures never exercised this path // because their taint always wrote via `@field = f` assignment // inside a DIFFERENT method, not through a ctor's own return). let owner: Option<String> = type_name.as_ref() .and_then(|p| p.last().cloned()) .or_else(|| Some(self_ty.to_string())); let owner = owner.as_deref(); for f in fields { if let Some(v) = &f.value { if let Some(owner) = owner { if tainted.contains(&(owner.to_string(), f.name.clone())) { if let ExprKind::Ident(vname) = &v.kind { out.insert(vname.clone()); } } } field_write_param_scan_expr(v, self_ty, tainted, out); } else { // p-s1a2 (№289): shorthand `{ name }` (D52 §2, MANDATORY // spelling when the field name matches its source — the // explicit `{ name: name }` form is itself a hard // compile error) is semantically `{ name: name }`; // `value: None` in the AST must not silently skip this // write-site check (same fix + full rationale as the // runtime twin, `walk_expr`'s `ExprKind::RecordLit` arm). if let Some(owner) = owner { if tainted.contains(&(owner.to_string(), f.name.clone())) { out.insert(f.name.clone()); } } } } } ExprKind::Spawn(inner) => field_write_param_scan_expr(inner, self_ty, tainted, out), ExprKind::Detach(b) | ExprKind::Blocking(b) => field_write_param_scan_block(b, self_ty, tainted, out), ExprKind::ParallelFor { iter, body, .. } => { field_write_param_scan_expr(iter, self_ty, tainted, out); field_write_param_scan_block(body, self_ty, tainted, out); } ExprKind::For { iter, body, .. } => { field_write_param_scan_expr(iter, self_ty, tainted, out); field_write_param_scan_block(body, self_ty, tainted, out); } ExprKind::Block(b) => field_write_param_scan_block(b, self_ty, tainted, out), ExprKind::If { cond, then, else_ } => { field_write_param_scan_expr(cond, self_ty, tainted, out); field_write_param_scan_block(then, self_ty, tainted, out); match else_ { Some(ElseBranch::Block(b)) => field_write_param_scan_block(b, self_ty, tainted, out), Some(ElseBranch::If(e2)) => field_write_param_scan_expr(e2, self_ty, tainted, out), None => {} } } ExprKind::IfLet { scrutinee, guard, then, else_, .. } => { field_write_param_scan_expr(scrutinee, self_ty, tainted, out); if let Some(g) = guard { field_write_param_scan_expr(g, self_ty, tainted, out); } field_write_param_scan_block(then, self_ty, tainted, out); match else_ { Some(ElseBranch::Block(b)) => field_write_param_scan_block(b, self_ty, tainted, out), Some(ElseBranch::If(e2)) => field_write_param_scan_expr(e2, self_ty, tainted, out), None => {} } } ExprKind::Match { scrutinee, arms } => { field_write_param_scan_expr(scrutinee, self_ty, tainted, out); for a in arms { if let Some(g) = &a.guard { field_write_param_scan_expr(g, self_ty, tainted, out); } match &a.body { MatchArmBody::Expr(be) => field_write_param_scan_expr(be, self_ty, tainted, out), MatchArmBody::Block(bb) => field_write_param_scan_block(bb, self_ty, tainted, out), } } } ExprKind::While { cond, body, .. } => { field_write_param_scan_expr(cond, self_ty, tainted, out); field_write_param_scan_block(body, self_ty, tainted, out); } ExprKind::WhileLet { scrutinee, guard, body, .. } => { field_write_param_scan_expr(scrutinee, self_ty, tainted, out); if let Some(g) = guard { field_write_param_scan_expr(g, self_ty, tainted, out); } field_write_param_scan_block(body, self_ty, tainted, out); } ExprKind::Loop { body, .. } => field_write_param_scan_block(body, self_ty, tainted, out), ExprKind::Supervised { body, cancel, deadline, on_timeout } => { if let Some(c) = cancel { field_write_param_scan_expr(c, self_ty, tainted, out); } if let Some(dl) = deadline { field_write_param_scan_expr(&dl.expr, self_ty, tainted, out); } if let Some(oh) = on_timeout { field_write_param_scan_expr(oh, self_ty, tainted, out); } field_write_param_scan_block(body, self_ty, tainted, out); } ExprKind::With { bindings, body } => { for b in bindings { field_write_param_scan_expr(&b.handler, self_ty, tainted, out); } field_write_param_scan_block(body, self_ty, tainted, out); } ExprKind::Forbid { body, .. } | ExprKind::Realtime { body, .. } => { field_write_param_scan_block(body, self_ty, tainted, out) } ExprKind::Try(inner) | ExprKind::Bang(inner) | ExprKind::RefArg(inner) => { field_write_param_scan_expr(inner, self_ty, tainted, out) } ExprKind::Coalesce(a, b) => { field_write_param_scan_expr(a, self_ty, tainted, out); field_write_param_scan_expr(b, self_ty, tainted, out); } ExprKind::Binary { left, right, .. } => { field_write_param_scan_expr(left, self_ty, tainted, out); field_write_param_scan_expr(right, self_ty, tainted, out); } ExprKind::Unary { operand, .. } => field_write_param_scan_expr(operand, self_ty, tainted, out), ExprKind::Member { obj, .. } => field_write_param_scan_expr(obj, self_ty, tainted, out), ExprKind::Index { obj, index } => { field_write_param_scan_expr(obj, self_ty, tainted, out); field_write_param_scan_expr(index, self_ty, tainted, out); } _ => {} } } // ============================================================================ // Name-resolution фаза. // // Pre-collects top-level имена (fns/types/consts/variants/built-ins) + // walk fn/test bodies со scope-стеком. На `ExprKind::Ident(name)` // проверяет, что `name` в (текущий scope ∪ top-level ∪ built-ins). // Рначе — diagnostic «undefined identifier`. // // **Конкервативная стратегия**: лучше пропустить undefined чем // false-positive. Случаи, где не проверяем: // - `obj.method(args)` / `Type.method(args)` — method-имена resolve'ятся // через method_table (могут быть на любом типе). // - `obj.field` / `Record { field: val }` — поля, не идентификаторы. // - Path-сегменты `mod1::mod2::name` (intermediate — модули, не expr). // - Tagged-template tags. // - Generic-params в TypeRef (это типы, не expressions). // - Sum-variant tag в pattern (`Some(x)` — constructor name, не expr). // ============================================================================ /// Plan 19+: статическая проверка undefined идентификаторов. struct NameResCtx { /// Plan 42.15: per-group shared declarations (Rule C). Key = file_id /// peer'а. Value = declarations всех peers ЕГО module-group (folder- /// module с общим parent dir). Peers одной группы делят namespace; /// между группами — НЕ делят (imported folder-module's decls не /// протекают). group_decls: HashMap<FileId, HashSet<String>>, /// Plan 42.15: fallback для legacy/single-file (peer_files пуст) — /// flat все module.items. Рспользуется когда file_id не в group_decls. shared_decls: HashSet<String>, /// Plan 42.15: union ВСЕХ declarations (все группы + imported). НЕ /// для name-resolution enforcement (это нарушило бы Rule C) — /// используется ТОЛЬКО как эвристика в `collect_pattern_bindings` /// (отличить pattern-binding `let x` от variant-pattern `Some`). all_decls: HashSet<String>, /// Plan 42.15: per-peer imported item names — items ставшие /// видимыми в peer'е через его прямые `import` (после rename + /// selective filter). Rule C: imports НЕ shared между peers. peer_imported_names: HashMap<FileId, HashSet<String>>, /// Built-in имена, доступные в любом scope без объявления: /// primitive types, prelude variants (None/Some/Ok/Err), bool /// литералы (true/false), builtin functions (assert/print/...), /// special idents (Self). builtins: HashSet<String>, /// Per-peer import namespace (Plan 42.4 Rule C). /// Key = file_id of peer file (MAIN_FILE_ID for entry). /// Value = set of module/alias names visible in that peer. peer_module_names: HashMap<FileId, HashSet<String>>, /// Plan 170 (D307): per-file file-private leak table. Key = file_id of a /// peer F. Value = map `name → owner_path` for every `priv(file)` symbol /// declared in ANOTHER peer file of F's module-group. When F references /// such a name and it is otherwise unresolved, the resolver emits the /// specific `E_FILE_PRIV_LEAK` diagnostic (instead of generic «undefined /// identifier»). The symbol is intentionally NOT in `group_decls[F]`. file_priv_leak: HashMap<FileId, HashMap<String, String>>, } impl NameResCtx { fn build(module: &Module) -> Self { // Plan 42.15: per-group shared declarations (Rule C). // // **Module-group** = набор peer-файлов одного folder-module // (имеют общий parent dir). Внутри группы peers делят // declarations namespace (Rule C: «peers share declarations»). // МЕЖДУ группами — НЕ делят (imported folder-module's decls не // протекают в entry's namespace). // // `group_decls`: HashMap<FileId, HashSet<String>> — для каждого // peer'а (по file_id) → declarations всех peers его группы. let mut group_decls: HashMap<FileId, HashSet<String>> = HashMap::new(); // Fallback для legacy/single-file (peer_files пуст). let mut shared_decls: HashSet<String> = HashSet::new(); fn collect_decl_names(items: &[Item], out: &mut HashSet<String>) { for item in items { match item { Item::Fn(fd) => { // free-functions (без receiver) валидны как // bare-ident `foo()`. Методы — через obj.method. if fd.receiver.is_none() { out.insert(fd.name.clone()); } } Item::Type(td) => { out.insert(td.name.clone()); // Variant-имена sum-типов: `Some(x)`, `Red`, etc. if let TypeDeclKind::Sum(variants) = &td.kind { for v in variants { out.insert(v.name.clone()); } } } Item::Const(cd) => { out.insert(cd.name.clone()); } // Plan 152.4 (D199 ro-runtime side): a module-level // `ro NAME = EXPR` is a lazy-static global (genuinely // runtime RHS — call/effect/alloc; the strict const/ro // partition forces a constexpr RHS to `const`). Its binder // is a resolvable top-level name, exactly like a `const`, so // collect it for name-resolution — otherwise USES // (`tbl.get(k)`) would be flagged «undefined identifier». // Single named binder only (Ident, or single-segment unit // Variant for the UPPER_CASE form), non-ghost. Mirrors the // env.consts registration in `check_module`. Item::Let(ld) if !ld.is_ghost => { match &ld.pattern { crate::ast::Pattern::Ident { name, .. } => { out.insert(name.clone()); } crate::ast::Pattern::Variant { path, kind: crate::ast::VariantPatternKind::Unit, .. } if path.len() == 1 => { out.insert(path[0].clone()); } _ => {} } } // Plan 57: bench — top-level item но имя — string-literal, // не идентификатор; в name resolution не участвует. ghost // `let` — spec-only, не резолвится. Item::Let(_) | Item::Test(_) | Item::Bench(_) | Item::Lemma(_) => {} } } } // Plan 170 (D307): file-private leak table — per-file map of names that // are `priv(file)` in another peer file of the same module-group. let mut file_priv_leak: HashMap<FileId, HashMap<String, String>> = HashMap::new(); // Plan 170 (D307): collect a peer's `priv(file)` top-level names — these // are visible ONLY inside their own file and must NOT enter the shared // group namespace. Free fns / types (+ sum variants) / consts. fn collect_file_private_names(items: &[Item], out: &mut HashSet<String>) { for item in items { match item { Item::Fn(fd) if fd.receiver.is_none() && fd.file_private => { out.insert(fd.name.clone()); } Item::Type(td) if td.file_private => { out.insert(td.name.clone()); if let TypeDeclKind::Sum(variants) = &td.kind { for v in variants { out.insert(v.name.clone()); } } } Item::Const(cd) if cd.file_private => { out.insert(cd.name.clone()); } _ => {} } } } if module.peer_files.is_empty() { // Legacy/single-file: flat — все module.items. A lone file has no // peers, so file-private symbols are trivially visible in it. collect_decl_names(&module.items, &mut shared_decls); } else { // Группируем peers по parent dir пути. Все peers одной // папки = одна module-group, делят declarations. // // Plan 170 (D307): the SHARED group namespace excludes each peer's // `priv(file)` names. Per peer F the visible set is then // `shared_group ∪ F's own file-private names` — so a sibling never // resolves another file's private symbol. let mut groups: HashMap<(std::path::PathBuf, Vec<String>), HashSet<String>> = HashMap::new(); let mut peer_group_key: HashMap<FileId, (std::path::PathBuf, Vec<String>)> = HashMap::new(); // Per file: its OWN file-private names (re-added to its own scope). let mut own_file_private: HashMap<FileId, HashSet<String>> = HashMap::new(); // Per group: file-private name → owner file path (for leak diag). let mut group_file_private: HashMap< (std::path::PathBuf, Vec<String>), HashMap<String, (FileId, String)>, > = HashMap::new(); for pf in &module.peer_files { let dir_key = pf.path.parent() .map(|p| p.to_path_buf()) .unwrap_or_else(|| pf.path.clone()); // Plan 81 F.1: group by (dir, module_name). let group_key = (dir_key, pf.module_name.clone()); peer_group_key.insert(pf.file_id, group_key.clone()); // Collect ALL decl names, then subtract this file's privates so // the shared group set has only module-default / export names. let mut all_here: HashSet<String> = HashSet::new(); collect_decl_names(&pf.items_here, &mut all_here); let mut fp_here: HashSet<String> = HashSet::new(); collect_file_private_names(&pf.items_here, &mut fp_here); let entry = groups.entry(group_key.clone()).or_default(); for n in all_here.difference(&fp_here) { entry.insert(n.clone()); } let path_str = pf.path.to_string_lossy().to_string(); let gfp = group_file_private.entry(group_key).or_default(); for n in &fp_here { gfp.entry(n.clone()).or_insert((pf.file_id, path_str.clone())); } own_file_private.insert(pf.file_id, fp_here); } // Разворачиваем: для каждого peer'а — shared group decls ∪ его own // file-private. Также строим leak-table: file-private имена, // объявленные в ДРУГИХ файлах группы. for pf in &module.peer_files { if let Some(gk) = peer_group_key.get(&pf.file_id) { if let Some(decls) = groups.get(gk) { let mut visible = decls.clone(); if let Some(own) = own_file_private.get(&pf.file_id) { for n in own { visible.insert(n.clone()); } } group_decls.insert(pf.file_id, visible); } if let Some(gfp) = group_file_private.get(gk) { let mut leak: HashMap<String, String> = HashMap::new(); for (name, (owner_fid, owner_path)) in gfp { if *owner_fid != pf.file_id { leak.insert(name.clone(), owner_path.clone()); } } if !leak.is_empty() { file_priv_leak.insert(pf.file_id, leak); } } } } } let builtins: HashSet<String> = [ // Numeric primitives. "int", "i8", "i16", "i32", "i64", "u8", "u16", "u32", "u64", "f32", "f64", "uint", "size", // Other primitives. "bool", "str", "char", "unit", "any", // Plan 76: `never` — bottom-тип (uninhabited, 0 значений), // строчный встроенный примитив. Subtype любого `T`. Как и // остальные примитивы (`int`/`bool`/...) — НЕ объявляется в // prelude, известен компилятору напрямую. "never", // Boolean literals (parsed как Ident в bool-context кое-где). "true", "false", // Special idents. "Self", "self", // Plan 62.A: `Option`/`Result`/`Some`/`None`/`Ok`/`Err`/`Error`/ // `Ordering`/`Less`/`Equal`/`Greater` (11 names) перенесены в // std/prelude/core.nv. Type-checker теперь resolves их через // cross-file resolve (R27 auto-import). См. docs/plans/ // 62-prelude-hardcode-migration.md §62.A. // // Plan 62.C: `RuntimeError` + 6 variants (`DivByZero`, // `Overflow`, `IndexOutOfBounds`, `TypeMismatch`, `AssertFailed`, // `NoHandler`) перенесены в std/prelude/errors.nv. Аналогично // `ReadBufferError` + `UnexpectedEnd` (не были в этом HashSet'е, // но добавлены в registry через init_prelude_decls_from_items // — см. sum_schema_registry.rs::register_prelude_sum_from_decl). // Type-checker теперь resolves их через cross-file resolve. // Pre-populated `sum_schemas["RuntimeError"]` (emit_c.rs:1029-1048) // оставлен как ABI-compat fallback baseline per 62.A.bis // architecture (HardcodedBaseline остаётся, lookup precedence // DeclaredFromPrelude > HardcodedBaseline). // // `RuntimeNoneError` НЕ перенесён — bootstrap parser не // поддерживает empty-body sum syntax. Остаётся as // string-payload throw в nova_rt/effects.h. // Plan 62.B: `panic`/`exit`/`assert` (Plan 194 A4: `debug_assert` // retracted, role absorbed by `#debug assert`) перенесены в // std/prelude/runtime.nv (file-based external fn // declarations). Type-checker теперь resolves их через // cross-file resolve (R27 auto-import + R26 re-export через // std/prelude.nv facade). Codegen special-cases в emit_c.rs // (~11086-11136) остаются: // - panic/exit нужны для comma-expression обёртки // `(nv_panic(msg), (nova_int)0LL)` в expression-position // (?? coalesce, if-else branches). // - assert: D89 expression-context + Plan 11 // auto-derived cond_text (msg arg silently ignored). // См. docs/plans/62-prelude-hardcode-migration.md §62.B. // // Plan 62.B.bis (2026-05-18) closure: `print` / `println` // больше не hardcoded — formally declared в // std/prelude/runtime.nv через D69 variadic + `[]any` // (canonical D26 signature). Cross-file resolve через R27 // auto-import + R26 facade re-export находит declarations. // Codegen special-case (emit_c.rs:11270, Ф.1 reorder) fires // ДО variadic routing — preserves per-arg type info, // synthesized `[]any` array никогда не строится; per-arg // `nova_print_<type>` dispatch через infer_print_helper // (Ф.0 Plan 67 absorption — unified через infer_expr_c_type). // См. docs/plans/62.B.bis-print-println-migration.md. // Plan 32: GC introspection namespace (std.runtime.gc). // Рспользуется как `gc.heap_size()`, `gc.collect()` и т.д. // Source of truth для signatures: std/runtime/gc.nv (external fn). // Codegen dispatch: emit_c.rs:7155 special-case на name == "gc". // Builtin запись нужна потому что cross-file bare-name resolve // не работает (Plan 35 Ф.1). "gc", // Plan 57: bench DSL builtins namespace (std.bench). // `bench.opaque(v)`, `bench.iterations()`, `bench.reset_timer()`, // `bench.bytes(n)`, `bench.elements(n)`, `bench.allocs()`, // `bench.now_ns()`. Source of truth: std/bench.nv. Codegen // dispatch: emit_c.rs special-case на `name == "bench"`. "bench", // Plan 44.2 Этап 3: fiber arena introspection namespace // (std.runtime.fibers). `fibers.slot_count()`, etc. // Source of truth: std/runtime/fibers.nv. Codegen dispatch: // emit_c.rs `name == "fibers"`. "fibers", // Plan 44 Этап 0: M:N runtime control namespace // (std.runtime.runtime). `runtime.init(n)`, `runtime.shutdown()`. "runtime", // Default Fail-effect type (D65 placeholder). "Fail", // Detach effect-type для detach {} expression (D50). "Detach", // [M-canceltoken-prelude-decl] (2026-07-13, Plan 173-хвост): // `CancelToken` УБРАН из этого HashSet'а — объявлен формально в // std/prelude/concurrency.nv (`export type CancelToken(*())` + // extern "nova" методы) и re-export'ится фасадом std/prelude.nv, // образец Plan 62.D.bis (StringBuilder/WriteBuffer/ReadBuffer). // Bypass-имя без TypeDecl оставляло `CancelToken.new()` // нерезолвленным в чекере (ни Channel-1, ни Channel-2) → legacy // infer_call_ret_c угадывал чужой `.new()` по arity → CC-FAIL // класса «no member named 'cancel' in 'struct Nova_WriteBuffer'» // (std/src/concurrency/supervised_deadline_test.nv). Методы // (cancel/is_cancelled/reason/merge/cancelled_by) — по-прежнему // built-in dispatch в codegen на receiver NovaCancelToken*. // Plan 62.D.bis (2026-05-18): StringBuilder / WriteBuffer / // ReadBuffer объявлены в std/prelude/collections.nv через // `external type` (D126). **Не были** в этом HashSet'е изначально // (verified via grep на baseline) — cross-file resolve работает // через std/runtime/<name>.nv external fn декларации + теперь // через std/prelude/collections.nv type-decl (TypeDeclKind::Opaque). // `nogc_blacklisted_call` (types/mod.rs:1454) сохраняет // name-matches как capability data — не builtins source, // не conflicts. // // Plan 103.1 Ф.6: `fence` — memory fence free function // (std/runtime/sync.nv). Lowercase free fn → нужен в builtins // иначе type-checker флагает «undefined identifier» для тестов // без `import std.runtime.sync`. Dispatch: ExternalRegistry // → nova_fn_fence (free_fn_c_name ExternalRegistry-first path). "fence", // [panic-assert-intrinsic] (2026-07-11): `panic`/`assert` // (Plan 194 A4: `debug_assert` retracted) — ALWAYS-available // compiler intrinsics, resolved in ANY module including // `#no_prelude` ones, WITHOUT import or // per-file redeclaration. Root cause this closes: Plan 62.B moved // these off this hardcoded list into a real `extern "nova" fn` // declaration in std/prelude/runtime.nv, resolved purely via // cross-file resolve (R27 auto-import + R26 facade re-export). // `#no_prelude` modules (breaking the prelude→string→prelude // import cycle — std/runtime/string/core.nv and 8 other files) // never see that declaration, so the bare `Ident("panic")` callee // failed THIS pass's `is_known` check → "undefined identifier // `panic`" ([M-tls-handshake-test-panic-undefined-multifile]). // Those files worked around it with a local module-private // `extern "nova" fn panic` redeclaration (removed by this change // — no longer needed). // // Return-type inference for `panic`/`exit`/`abort`/`unreachable` // was ALREADY declaration-independent (hardcoded `never` arm, // see the `matches!(name.as_str(), "panic" | "exit" | "abort" | // "unreachable")` check in `infer_expr_type`'s `ExprKind::Call` // arm). Codegen dispatch for `panic`/`assert` was // ALSO already fully name-keyed (emit_c.rs `emit_call`, matches // `name == "panic"` / `"assert"` directly — // no `ExternalRegistry`/signature lookup involved, per the // ExternalRegistry::NAMESPACE_OVERRIDES doc: "panic()/exit()/ // assert() ... class C compiler intrinsic ... hardcoded в // emit_c.rs НАВСЕГДА"). So this `builtins` entry was the ONLY // missing piece — it just teaches the NAME-RESOLUTION pass what // codegen already knew. // // The canonical `extern "nova" fn panic/assert` // declarations in std/prelude/runtime.nv are KEPT (not removed): // they still drive `nova doc`, the D89 2-arg `assert` overload // arity, and W_PRELUDE_SHADOW future warnings for prelude-having // modules — this `builtins` entry is a permissive FALLBACK that // only matters when no declaration is reachable (`#no_prelude`). // See spec/decisions/08-runtime.md D13 amendment (2026-07-11). // Plan 194 A4: `debug_assert` retracted (role absorbed by // `#debug assert`, A2.2) — removed from this list. "panic", "assert", ] .iter() .map(|s| s.to_string()) .collect(); // Plan 42.4 Rule C: per-peer import namespace isolation. // Build a map from file_id → visible module names for that peer. // If peer_files is empty (legacy/single-file), fall back to entry. let mut peer_module_names: HashMap<FileId, HashSet<String>> = HashMap::new(); let build_import_names = |imports: &[Import], module_name: &[String]| -> HashSet<String> { let mut names: HashSet<String> = HashSet::new(); for imp in imports { if let Some(alias) = &imp.alias { names.insert(alias.clone()); } if let Some(last) = imp.path.last() { names.insert(last.clone()); } if let Some(head) = imp.path.first() { names.insert(head.clone()); } } // Own module name (last + first segment) for self-reference. if let Some(head) = module_name.first() { names.insert(head.clone()); } if let Some(last) = module_name.last() { names.insert(last.clone()); } names }; if module.peer_files.is_empty() { // Legacy/single-file: entry imports under MAIN_FILE_ID. peer_module_names.insert( MAIN_FILE_ID, build_import_names(&module.imports, &module.name), ); } else { for pf in &module.peer_files { peer_module_names.insert( pf.file_id, build_import_names(&pf.imports, &module.name), ); } } // Plan 42.15: per-peer imported item names. Resolver наполнил // `PeerFile.imported_item_names` (items притащенные прямыми // imports этого peer'а). Rule C: imports не shared между peers. let mut peer_imported_names: HashMap<FileId, HashSet<String>> = HashMap::new(); for pf in &module.peer_files { peer_imported_names.insert(pf.file_id, pf.imported_item_names.clone()); } // Plan 42.15: all_decls — union ВСЕХ declarations (эвристика для // pattern-binding detection, НЕ для enforcement). let mut all_decls: HashSet<String> = shared_decls.clone(); for gd in group_decls.values() { all_decls.extend(gd.iter().cloned()); } // Также merged module.items (imported items для эвристики). collect_decl_names(&module.items, &mut all_decls); NameResCtx { group_decls, shared_decls, all_decls, builtins, peer_module_names, peer_imported_names, file_priv_leak, } } fn check_module(&self, module: &Module, errors: &mut Vec<Diagnostic>) { for item in &module.items { let file_id = match item { Item::Fn(f) => f.span.file_id, Item::Test(t) => t.span.file_id, Item::Bench(b) => b.span.file_id, Item::Const(c) => c.span.file_id, Item::Type(t) => t.span.file_id, Item::Let(l) => l.span.file_id, Item::Lemma(ld) => ld.span.file_id, }; match item { Item::Fn(f) => self.walk_fn(f, file_id, errors), Item::Test(t) => { let mut scope: Vec<HashSet<String>> = vec![HashSet::new()]; self.walk_block(&t.body, file_id, &mut scope, errors); } // Plan 57: bench body — name-resolution как у test (один // общий scope для setup → measure → teardown, потому что // setup-bindings видны в measure и teardown). Item::Bench(b) => { let mut scope: Vec<HashSet<String>> = vec![HashSet::new()]; for s in &b.setup { self.walk_stmt(s, file_id, &mut scope, errors); } self.walk_block(&b.measure_body, file_id, &mut scope, errors); for s in &b.teardown { self.walk_stmt(s, file_id, &mut scope, errors); } } Item::Const(c) => { let mut scope: Vec<HashSet<String>> = vec![HashSet::new()]; self.walk_expr(&c.value, file_id, &mut scope, errors); } _ => {} } } } fn walk_fn(&self, f: &FnDecl, file_id: FileId, errors: &mut Vec<Diagnostic>) { // External — нет тела. if matches!(f.body, FnBody::External) { return; } let mut scope: Vec<HashSet<String>> = vec![HashSet::new()]; let mut frame: HashSet<String> = HashSet::new(); // Receiver: self/Self доступны через builtins; нет нужды добавлять. if let Some(_recv) = &f.receiver { frame.insert("self".to_string()); } for p in &f.params { frame.insert(p.name.clone()); } // Generic-params могут использоваться в expr-position? — Нет // (по spec). Но безопасно их добавить чтобы не флагать False+ // если parser/codegen где-то их так трактует. for g in &f.generics { frame.insert(g.name.clone()); } scope.push(frame); match &f.body { FnBody::Expr(e) => self.walk_expr(e, file_id, &mut scope, errors), FnBody::Block(b) => self.walk_block(b, file_id, &mut scope, errors), FnBody::External => {} } scope.pop(); } fn walk_block( &self, b: &Block, file_id: FileId, scope: &mut Vec<HashSet<String>>, errors: &mut Vec<Diagnostic>, ) { scope.push(HashSet::new()); for s in &b.stmts { self.walk_stmt(s, file_id, scope, errors); } if let Some(t) = &b.trailing { self.walk_expr(t, file_id, scope, errors); } scope.pop(); } fn walk_stmt( &self, s: &Stmt, file_id: FileId, scope: &mut Vec<HashSet<String>>, errors: &mut Vec<Diagnostic>, ) { match s { Stmt::Expr(e) => self.walk_expr(e, file_id, scope, errors), Stmt::Let(d) => { // Right-side вычисляется в текущем scope (let не // рекурсивный). Затем pattern-bindings добавляются в // текущий frame. self.walk_expr(&d.value, file_id, scope, errors); let mut bindings: HashSet<String> = HashSet::new(); self.collect_pattern_bindings(&d.pattern, &mut bindings); if let Some(top) = scope.last_mut() { for n in bindings { top.insert(n); } } } // Plan 114.4 Ф.2 / Plan 114.4.2 (D199): scope-local const — walk // RHS (may reference const fn calls) + bind name в текущий frame // так чтобы subsequent expressions могли его использовать. Stmt::Const(d) => { self.walk_expr(&d.value, file_id, scope, errors); if let Some(top) = scope.last_mut() { top.insert(d.name.clone()); } } Stmt::Assign { target, value, .. } => { self.walk_expr(target, file_id, scope, errors); self.walk_expr(value, file_id, scope, errors); } Stmt::Return { value, .. } => { if let Some(v) = value { self.walk_expr(v, file_id, scope, errors); } } Stmt::Throw { value, .. } => self.walk_expr(value, file_id, scope, errors), // D90 (Plan 20): defer/errdefer body — обычный expr в текущем // scope. Bindings внутри body локальны их собственным under-scope’ам; // на верхнем уровне defer не вводит новых имён. // Plan 173 Ф.2.B2 (D314): `defer(o ScopeOutcome)` вводит `o` в scope // тела — push frame c binding, walk, pop (зеркалит ConsumeScope). Stmt::Defer { body, outcome_binding, .. } => { if let Some(o) = outcome_binding { scope.push({ let mut frame = HashSet::new(); frame.insert(o.clone()); frame }); self.walk_expr(body, file_id, scope, errors); scope.pop(); } else { self.walk_expr(body, file_id, scope, errors); } } // Plan 110 D188: walk init + push new scope frame с binding, // walk body, pop frame. Binding visible только внутри body // (D188 §«Syntax» single-name binding). Stmt::ConsumeScope { binding, init, body, result, .. } => { self.walk_expr(init, file_id, scope, errors); scope.push({ let mut frame = HashSet::new(); frame.insert(binding.clone()); frame }); for s in &body.stmts { self.walk_stmt(s, file_id, scope, errors); } if let Some(t) = &body.trailing { self.walk_expr(t, file_id, scope, errors); } scope.pop(); // Plan 201: result-приёмник блока-выражения виден в // объемлющем scope ПОСЛЕ блока (как let-binding). if let Some(r) = result { if let Some(frame) = scope.last_mut() { frame.insert(r.name.clone()); } } } // Plan 33.2 Ф.8: assert_static — walk expr. Stmt::AssertStatic { expr, .. } | Stmt::Assume { expr, .. } => self.walk_expr(expr, file_id, scope, errors), Stmt::Break(_) | Stmt::Continue(_) => {} // Ф.4.1: apply — ghost, args walk для name-resolution. Stmt::Apply { args, .. } => { for a in args { self.walk_expr(a, file_id, scope, errors); } } // Ф.4.2: calc — ghost, шаги walk для name-resolution. Stmt::Calc { steps, .. } => { for step in steps { self.walk_expr(&step.expr, file_id, scope, errors); } } // Plan 33.9 Ф.2: reveal — ghost, name resolution в pipeline. Stmt::Reveal { .. } => {} // Plan 136: tuple destructuring assignment. Stmt::TupleAssign { lhs, rhs, .. } => { for e in lhs { self.walk_expr(e, file_id, scope, errors); } for e in rhs { self.walk_expr(e, file_id, scope, errors); } } } } fn walk_expr( &self, e: &Expr, file_id: FileId, scope: &mut Vec<HashSet<String>>, errors: &mut Vec<Diagnostic>, ) { match &e.kind { ExprKind::Ident(name) => { if !self.is_known(name, file_id, scope) { // Plan 170 (D307): if the name is file-private to ANOTHER // peer of this module-group, emit the specific leak error. if let Some(owner) = self.file_priv_leak .get(&file_id) .and_then(|m| m.get(name)) { errors.push(Diagnostic::new( format!( "[E_FILE_PRIV_LEAK] `{}` is file-private to {}; not \ visible from this file. Remove `priv(file)` from the \ declaration (to make it module-private) or move the \ symbol into this file (D307).", name, owner, ), e.span, )); } else { errors.push(Diagnostic::new( format!("undefined identifier `{}`", name), e.span, )); } } } // Path-form `Module.func` / `Type.method`: head — модуль или // type. Plan 42.15 Ф.3: head-segment check для lowercase // module-alias'ов (Rule C: peer видит только свои imports). // // Проверяем ТОЛЬКО lowercase head: Capitalized = тип/effect/ // variant (cross-file, bootstrap-консервативно пропускаем). // lowercase head должен быть: builtin namespace (gc/fibers/ // runtime) РЛР module-alias в peer's import scope. Если нет — // вероятно use чужого import'а (Rule C violation) или typo. ExprKind::Path(parts) => { if let Some(head) = parts.first() { let is_lowercase = head.chars().next() .map(|c| c.is_ascii_lowercase()) .unwrap_or(false); if is_lowercase { let in_builtins = self.builtins.contains(head); let in_peer_modules = self.peer_module_names.get(&file_id) .or_else(|| self.peer_module_names.get(&MAIN_FILE_ID)) .map_or(false, |s| s.contains(head)); // Также head может быть local binding (struct в // scope) — тогда это фактически Member-access; // парсер иногда эмитит Path. Проверяем scope. let in_scope = scope.iter().rev() .any(|frame| frame.contains(head)); if !in_builtins && !in_peer_modules && !in_scope { errors.push(Diagnostic::new( format!( "undefined module / name `{}` in path expression \ (Rule C: peer sees only its own imports)", head), e.span, )); } } } } // SelfAccess — `@field` или `@method`. Не Ident. ExprKind::SelfAccess => {} // Литералы. ExprKind::IntLit(_) | ExprKind::FloatLit(_) | ExprKind::BoolLit(_) | ExprKind::StrLit(_) | ExprKind::CharLit(_) | ExprKind::UnitLit | ExprKind::HexBlobLit(_) | ExprKind::NullPtrLit => {} ExprKind::InterpolatedStr { parts } => { for p in parts { if let InterpStrPart::Expr { expr: e, spec: _ } = p { self.walk_expr(e, file_id, scope, errors); } } } ExprKind::Call { func, args, trailing } => { // Plan 118.6 Ф.3: addr_of/addr_of_mut removed — emit // E_ADDR_OF_REMOVED before undefined-identifier fires. if let ExprKind::Ident(n) = &func.kind { if (n == "addr_of" || n == "addr_of_mut") && args.len() == 1 { errors.push(Diagnostic::new( format!( "[E_ADDR_OF_REMOVED] `{}(x)` is removed (Plan 118.6, D216 §4). \ Use `&x` instead.", n, ), func.span, )); // Still walk the argument to catch errors inside it. self.walk_expr(args[0].expr(), file_id, scope, errors); if let Some(t) = trailing { self.walk_trailing(t, file_id, scope, errors); } return; } } // Special-case: если func — bare Ident, может быть // variant-constructor (`Square(5)`) — top_level.contains. // is_known покрывает оба варианта (fn + variant). self.walk_expr(func, file_id, scope, errors); for a in args { self.walk_expr(a.expr(), file_id, scope, errors); } if let Some(t) = trailing { self.walk_trailing(t, file_id, scope, errors); } } ExprKind::TurboFish { base, .. } => self.walk_expr(base, file_id, scope, errors), ExprKind::Try(inner) | ExprKind::Bang(inner) | ExprKind::RefArg(inner) => { self.walk_expr(inner, file_id, scope, errors) } ExprKind::Coalesce(a, b) => { self.walk_expr(a, file_id, scope, errors); self.walk_expr(b, file_id, scope, errors); } ExprKind::As(e, _) | ExprKind::Is(e, _) => self.walk_expr(e, file_id, scope, errors), ExprKind::Binary { left, right, .. } => { self.walk_expr(left, file_id, scope, errors); self.walk_expr(right, file_id, scope, errors); } ExprKind::Unary { operand, .. } => self.walk_expr(operand, file_id, scope, errors), // Member-access: проверяем obj (это expr), но НЕ name (field/method). ExprKind::Member { obj, .. } => self.walk_expr(obj, file_id, scope, errors), ExprKind::Index { obj, index } => { self.walk_expr(obj, file_id, scope, errors); self.walk_expr(index, file_id, scope, errors); } ExprKind::If { cond, then, else_ } => { self.walk_expr(cond, file_id, scope, errors); self.walk_block(then, file_id, scope, errors); if let Some(eb) = else_ { match eb { ElseBranch::Block(b) => self.walk_block(b, file_id, scope, errors), ElseBranch::If(e) => self.walk_expr(e, file_id, scope, errors), } } } ExprKind::IfLet { pattern, scrutinee, guard, then, else_ } => { self.walk_expr(scrutinee, file_id, scope, errors); // Pattern-bindings — в scope для guard и then-branch. let mut bindings: HashSet<String> = HashSet::new(); self.collect_pattern_bindings(pattern, &mut bindings); scope.push(bindings); if let Some(g) = guard { self.walk_expr(g, file_id, scope, errors); } self.walk_block(then, file_id, scope, errors); scope.pop(); if let Some(eb) = else_ { match eb { ElseBranch::Block(b) => self.walk_block(b, file_id, scope, errors), ElseBranch::If(e) => self.walk_expr(e, file_id, scope, errors), } } } ExprKind::Match { scrutinee, arms } => { self.walk_expr(scrutinee, file_id, scope, errors); for arm in arms { let mut bindings: HashSet<String> = HashSet::new(); self.collect_pattern_bindings(&arm.pattern, &mut bindings); scope.push(bindings); if let Some(g) = &arm.guard { self.walk_expr(g, file_id, scope, errors); } match &arm.body { MatchArmBody::Expr(e) => self.walk_expr(e, file_id, scope, errors), MatchArmBody::Block(b) => self.walk_block(b, file_id, scope, errors), } scope.pop(); } } ExprKind::For { pattern, iter, body, .. } => { self.walk_expr(iter, file_id, scope, errors); let mut bindings: HashSet<String> = HashSet::new(); self.collect_pattern_bindings(pattern, &mut bindings); scope.push(bindings); self.walk_block(body, file_id, scope, errors); scope.pop(); } ExprKind::ParallelFor { pattern, iter, body, .. } => { self.walk_expr(iter, file_id, scope, errors); let mut bindings: HashSet<String> = HashSet::new(); self.collect_pattern_bindings(pattern, &mut bindings); scope.push(bindings); self.walk_block(body, file_id, scope, errors); scope.pop(); } ExprKind::While { cond, body, .. } => { self.walk_expr(cond, file_id, scope, errors); self.walk_block(body, file_id, scope, errors); } ExprKind::WhileLet { pattern, scrutinee, body, .. } => { self.walk_expr(scrutinee, file_id, scope, errors); let mut bindings: HashSet<String> = HashSet::new(); self.collect_pattern_bindings(pattern, &mut bindings); scope.push(bindings); self.walk_block(body, file_id, scope, errors); scope.pop(); } ExprKind::Loop { body, .. } => self.walk_block(body, file_id, scope, errors), ExprKind::Select { arms } => { for arm in arms { match &arm.op { SelectOp::Recv { binding, chan, .. } => { self.walk_expr(chan, file_id, scope, errors); let mut bindings: HashSet<String> = HashSet::new(); if let Some(b) = binding { bindings.insert(b.clone()); } scope.push(bindings); if let Some(g) = &arm.guard { self.walk_expr(g, file_id, scope, errors); } self.walk_block(&arm.body, file_id, scope, errors); scope.pop(); } SelectOp::Send { chan, value } => { self.walk_expr(chan, file_id, scope, errors); self.walk_expr(value, file_id, scope, errors); if let Some(g) = &arm.guard { self.walk_expr(g, file_id, scope, errors); } self.walk_block(&arm.body, file_id, scope, errors); } SelectOp::Default => { if let Some(g) = &arm.guard { self.walk_expr(g, file_id, scope, errors); } self.walk_block(&arm.body, file_id, scope, errors); } } } } ExprKind::Block(b) => self.walk_block(b, file_id, scope, errors), ExprKind::ArrayLit(elems) => { for el in elems { match el { ArrayElem::Item(e) | ArrayElem::Spread(e) => { self.walk_expr(e, file_id, scope, errors); } } } } ExprKind::MapLit { elems, .. } => { let pairs = crate::ast::MapElem::cloned_pairs(&elems); for (k, v) in pairs.iter() { self.walk_expr(k, file_id, scope, errors); self.walk_expr(v, file_id, scope, errors); } } ExprKind::TupleLit(elems) => { for e in elems { self.walk_expr(e, file_id, scope, errors); } } ExprKind::RecordLit { fields, .. } => { for f in fields { match &f.value { Some(v) => { // D52 §2 enforcement: redundant `{ name: name }` или // `{ field: @field }` запрещены (shorthand mandatory // когда имя поля совпадает с источником). Spec: // spec/decisions/02-types.md D52 §2. if !f.is_spread && !f.at_shorthand { use crate::ast::ExprKind as EK; let is_redundant_ident = matches!(&v.kind, EK::Ident(n) if n == &f.name); let is_redundant_self_field = matches!(&v.kind, EK::Member { obj, name } if name == &f.name && matches!(obj.kind, EK::SelfAccess)); if is_redundant_ident { errors.push(Diagnostic::new( format!( "избыточная форма поля `{name}: {name}` — \ D52 §2 требует shorthand `{name}` когда имя \ поля совпадает с источником", name = f.name), f.span, )); } else if is_redundant_self_field { errors.push(Diagnostic::new( format!( "избыточная форма поля `{name}: @{name}` — \ D52 §2 требует shorthand `@{name}` когда имя \ поля совпадает с self-полем", name = f.name), f.span, )); } } self.walk_expr(v, file_id, scope, errors); } None => { // Shorthand `{ name }` (D52 field punning): // `name` — это ident, который должен быть // в scope. if !f.is_spread && !self.is_known(&f.name, file_id, scope) { errors.push(Diagnostic::new( format!("undefined identifier `{}`", f.name), f.span, )); } } } } } // Tagged-template: tag — это специальный DSL-marker // (sql, json, html, ...). В bootstrap'е tag-функция // игнорируется (parts конкатенируются), но в production // tag — это runtime-функция/macro. Не проверяем tag как // Ident — это special-form syntax, не обычный expr-call. // Args (`${expr}` интерполяции) — обычные expressions. ExprKind::TaggedTemplate { args, .. } => { for a in args { self.walk_expr(a, file_id, scope, errors); } } // Lambda (legacy) / closure-light / closure-full — params // push'ятся как новый scope frame. ExprKind::Lambda { params, body, .. } => { let mut frame: HashSet<String> = HashSet::new(); for p in params { frame.insert(p.name.clone()); } scope.push(frame); self.walk_expr(body, file_id, scope, errors); scope.pop(); } ExprKind::ClosureLight { params, body } => { let mut frame: HashSet<String> = HashSet::new(); for p in params { if p.name != "_" { frame.insert(p.name.clone()); } } scope.push(frame); match body { crate::ast::ClosureBody::Expr(e) => self.walk_expr(e, file_id, scope, errors), crate::ast::ClosureBody::Block(b) => self.walk_block(b, file_id, scope, errors), } scope.pop(); } ExprKind::ClosureFull(sb) => { let mut frame: HashSet<String> = HashSet::new(); for p in &sb.params { frame.insert(p.name.clone()); } scope.push(frame); match &sb.body { FnBody::Expr(e) => self.walk_expr(e, file_id, scope, errors), FnBody::Block(b) => self.walk_block(b, file_id, scope, errors), FnBody::External => {} } scope.pop(); } ExprKind::With { bindings, body } => { // Effect-handler vals — обычные expressions. for b in bindings { self.walk_expr(&b.handler, file_id, scope, errors); } self.walk_block(body, file_id, scope, errors); } // Plan 97 Ф.4 (D142): protocol-литерал — name-resolution // walk идентичен handler-литералу. ExprKind::HandlerLit { methods, .. } | ExprKind::ProtocolLit { methods, .. } => { // Каждый method — op с собственным scope params. for m in methods { let mut frame: HashSet<String> = HashSet::new(); for p in &m.params { frame.insert(p.name.clone()); } scope.push(frame); match &m.body { HandlerMethodBody::Expr(e) => self.walk_expr(e, file_id, scope, errors), HandlerMethodBody::Block(b) => self.walk_block(b, file_id, scope, errors), } scope.pop(); } } ExprKind::Interrupt(opt) => { if let Some(e) = opt { self.walk_expr(e, file_id, scope, errors); } } ExprKind::Forbid { body, .. } | ExprKind::Realtime { body, .. } => { self.walk_block(body, file_id, scope, errors); } ExprKind::Range { start, end, .. } => { if let Some(s) = start { self.walk_expr(s, file_id, scope, errors); } if let Some(e) = end { self.walk_expr(e, file_id, scope, errors); } } ExprKind::Spawn(body) => self.walk_expr(body, file_id, scope, errors), ExprKind::Detach(body) | ExprKind::Blocking(body) => { self.walk_block(body, file_id, scope, errors); } ExprKind::Supervised { body, cancel, deadline, on_timeout } => { // Plan 47: `cancel:` expr — обычное выражение scope'а // (типично `Ident` токена); резолвится в текущем scope'е, // никаких новых биндингов не вводит. if let Some(c) = cancel { self.walk_expr(c, file_id, scope, errors); } if let Some(_dl) = deadline { let _dl_e = &_dl.expr; self.walk_expr(_dl_e, file_id, scope, errors); } if let Some(oh) = on_timeout { self.walk_expr(oh, file_id, scope, errors); } self.walk_block(body, file_id, scope, errors); } ExprKind::Throw(inner) => self.walk_expr(inner, file_id, scope, errors), // [E_COALESCE_RETURN_FALLBACK]: checker-rejected before this pass. ExprKind::CoalesceReturnFallback(opt) => { if let Some(inner) = opt { self.walk_expr(inner, file_id, scope, errors); } } // D.1.3: квантор — bound variable вводится в scope для body. ExprKind::Forall { var, range, body } | ExprKind::Exists { var, range, body } => { self.walk_expr(range, file_id, scope, errors); let mut frame: HashSet<String> = HashSet::new(); frame.insert(var.clone()); scope.push(frame); self.walk_expr(body, file_id, scope, errors); scope.pop(); } } } fn walk_trailing( &self, t: &crate::ast::Trailing, file_id: FileId, scope: &mut Vec<HashSet<String>>, errors: &mut Vec<Diagnostic>, ) { match t { crate::ast::Trailing::Block(b) => self.walk_block(b, file_id, scope, errors), crate::ast::Trailing::LegacyBlockWithParams(tb) => { let mut frame: HashSet<String> = HashSet::new(); for p in &tb.params { frame.insert(p.name.clone()); } scope.push(frame); self.walk_block(&tb.body, file_id, scope, errors); scope.pop(); } crate::ast::Trailing::Fn(sb) => { let mut frame: HashSet<String> = HashSet::new(); for p in &sb.params { frame.insert(p.name.clone()); } scope.push(frame); match &sb.body { FnBody::Expr(e) => self.walk_expr(e, file_id, scope, errors), FnBody::Block(b) => self.walk_block(b, file_id, scope, errors), FnBody::External => {} } scope.pop(); } } } /// Собрать все bindings из pattern (только names, без проверки /// variant-tag'ов или field-name'ов — это constructor/field /// references, не expr-bindings). fn collect_pattern_bindings(&self, p: &Pattern, out: &mut HashSet<String>) { match p { Pattern::Wildcard(_) => {} Pattern::Literal(_, _) => {} Pattern::Ident { name, .. } => { // Edge-case: Pattern::Ident { name: "Some" } — это // unit-variant Some? Нет, парсер emit'ит Variant { path: // ["Some"], kind: Unit }. Здесь — настоящий binding. // Но если имя совпадает с известным variant — считаем // это variant-pattern, не binding (D52 семантика // pattern-matching). Также Capitalized-имена в bootstrap // — это всегда type/variant (cross-file), не binding. // Plan 55 fix: all_decls contains free function names (lowercase) // which must NOT block for-loop bindings like `for inner in @`. // Only treat as variant-like if it's a builtin OR capitalized // (Nova convention: variants/types are always PascalCase, // free functions are snake_case). let is_variant_like = self.builtins.contains(name) || name.chars().next().map(|c| c.is_ascii_uppercase()).unwrap_or(false); if !is_variant_like { out.insert(name.clone()); } } Pattern::Variant { kind, .. } => { // path = variant-tag — не binding. match kind { VariantPatternKind::Unit => {} VariantPatternKind::Tuple { patterns, .. } => { for sub in patterns { self.collect_pattern_bindings(sub, out); } } } } Pattern::Record { fields, .. } => { for f in fields { match &f.pattern { Some(sub) => self.collect_pattern_bindings(sub, out), // Shorthand `{ name }` — name — это binding // (одновременно field-name и bound variable). None => { out.insert(f.name.clone()); } } } } Pattern::Array { elems, .. } => { for el in elems { match el { ArrayPatternElem::Item(sub) => self.collect_pattern_bindings(sub, out), ArrayPatternElem::Rest => {} ArrayPatternElem::RestBind(name) => { out.insert(name.clone()); } } } } Pattern::Tuple(elems, _) => { for sub in elems { self.collect_pattern_bindings(sub, out); } } Pattern::Binding { name, inner, .. } => { out.insert(name.clone()); self.collect_pattern_bindings(inner, out); } Pattern::Or { alternatives, .. } => { // По spec все alternatives имеют одинаковый набор // bindings; берём из первого. (Bootstrap-семантика — см. // ast::Pattern::Or doc.) if let Some(first) = alternatives.first() { self.collect_pattern_bindings(first, out); } } } } fn is_known(&self, name: &str, file_id: FileId, scope: &[HashSet<String>]) -> bool { // Plan 114.4.4 Ф.5 V4: t-reflection intrinsics — built-in // const fn names recognized без registration. Replaced литералом // в rewriter pass (const_fn_eval.rs). if name == "size_of" || name == "align_of" { return true; } if self.builtins.contains(name) { return true; } // Plan 42.15 Rule C: declarations module-group этого peer'а // (peers одного folder-module делят declarations namespace). // Fallback на flat shared_decls для legacy/single-file. if let Some(gd) = self.group_decls.get(&file_id) { if gd.contains(name) { return true; } } else if self.shared_decls.contains(name) { return true; } // Plan 42.15: per-peer imported item names — items притащенные // прямыми imports РМЕННО этого peer'а. Rule C: imports НЕ shared. // Fallback на MAIN_FILE_ID если file_id не найден (legacy). let imported = self.peer_imported_names.get(&file_id) .or_else(|| self.peer_imported_names.get(&MAIN_FILE_ID)); if imported.map_or(false, |s| s.contains(name)) { return true; } // Plan 42.4 Rule C: per-peer import namespace (module/alias names). let module_names = self.peer_module_names.get(&file_id) .or_else(|| self.peer_module_names.get(&MAIN_FILE_ID)); if module_names.map_or(false, |s| s.contains(name)) { return true; } for frame in scope.iter().rev() { if frame.contains(name) { return true; } } // Bootstrap-консервативность: имена начинающиеся с заглавной // буквы по convention — типы / variants / модули. Bootstrap // не имеет cross-file name resolution, поэтому ident вроде // `HashMap` (из другого .nv файла) приходит сюда не задекларированным. // Чтобы не флагать такие cross-file типы как undefined, // пропускаем Capitalized-ident'ы. Опечатки в lowercase // именах (snake_case convention для vars/fns) — настоящие // undefined и будут ловиться. if let Some(c) = name.chars().next() { if c.is_ascii_uppercase() { return true; } } false } } /// Render method signature `name(p1 T1, p2 T2) -> Ret` — для diagnostic'а. /// Plan 91.9 (D186): compare T's explicit method signature vs protocol's /// method requirement. Returns Some(reason) если mismatch, None если ok. /// /// Strict check: /// - arity (param count) must match /// - each param type must match (modulo Self ↔ T receiver-coercion) /// - return type must match (modulo Self) /// /// Self в protocol method ↔ T's own type name — допустимо. Generic params /// в protocol — допустимо в принципе (treated as wildcards), но bootstrap /// strict-match для simple cases. fn check_signature_match( t_method: &FnDecl, proto_method: &crate::ast::EffectMethod, ) -> Option<String> { if t_method.params.len() != proto_method.params.len() { return Some(format!( "arity mismatch: T's `{}` has {} param(s), protocol expects {}", t_method.name, t_method.params.len(), proto_method.params.len(), )); } for (tp, pp) in t_method.params.iter().zip(proto_method.params.iter()) { let tt = &tp.ty; let pt = &pp.ty; if !type_refs_equiv_modulo_self(tt, pt, &t_method.receiver.as_ref() .map(|r| r.type_name.as_str()) .unwrap_or("")) { return Some(format!( "param `{}`: T has `{}`, protocol expects `{}`", tp.name, render_type_ref(tt), render_type_ref(pt), )); } // [M-conformance-param-mode-check] fix (2026-07-16): see companion // check in `check_signature_match_with_subst` — param binding-mode // (`mut`/`consume`) must match the protocol declaration exactly, not // just the type (research-mut-canon пробой D: bare impl param quietly // satisfied a `mut`-declared protocol param). if tp.is_mut != pp.is_mut || tp.consume != pp.consume { let tp_prefix = if tp.consume { "consume " } else if tp.is_mut { "mut " } else { "" }; let pp_prefix = if pp.consume { "consume " } else if pp.is_mut { "mut " } else { "" }; return Some(format!( "param `{}`: T declares `{}{} {}`, protocol `#impl` requires `{}{} {}` \ (parameter binding-mode `mut`/`consume` must match the protocol \ declaration exactly, not just the type)", tp.name, tp_prefix, tp.name, render_type_ref(tt), pp_prefix, pp.name, render_type_ref(pt), )); } } let t_ret = t_method.return_type.as_ref(); let p_ret = proto_method.return_type.as_ref(); let recv_name = t_method.receiver.as_ref() .map(|r| r.type_name.as_str()).unwrap_or(""); // None ↔ Unit equivalence: both forms `fn foo()` и `fn foo() -> ()` // declare unit-returning method. Treated identically. let is_unit_or_none = |r: Option<&TypeRef>| -> bool { match r { None => true, Some(TypeRef::Unit(_)) => true, _ => false, } }; if is_unit_or_none(t_ret) && is_unit_or_none(p_ret) { return None; } match (t_ret, p_ret) { (Some(a), Some(b)) => { if !type_refs_equiv_modulo_self(a, b, recv_name) { return Some(format!( "return type: T returns `{}`, protocol expects `{}`", render_type_ref(a), render_type_ref(b), )); } } _ => return Some(format!( "return type: T returns `{}`, protocol expects `{}`", t_ret.map(render_type_ref).unwrap_or_else(|| "()".into()), p_ret.map(render_type_ref).unwrap_or_else(|| "()".into()), )), } None } /// Plan 164 Ф.1: apply a generic substitution map to a TypeRef string. /// /// `subst` maps protocol generic param names to their impl counterparts, /// e.g. [("T", "U")]. We operate on `render_type_ref` strings for simplicity: /// replace whole-word occurrences of each proto param with the impl arg. fn apply_subst_to_type_str(s: &str, subst: &[(String, String)]) -> String { let mut result = s.to_string(); for (proto_param, impl_arg) in subst { // Replace whole-word occurrences: surrounded by non-ident chars. let mut out = String::new(); let mut i = 0; let bytes = result.as_bytes(); while i < bytes.len() { let is_ident_start = |b: u8| b.is_ascii_alphabetic() || b == b'_'; if bytes[i..].starts_with(proto_param.as_bytes()) { let end = i + proto_param.len(); let before_ok = i == 0 || !is_ident_start(bytes[i - 1]); let after_ok = end >= bytes.len() || !is_ident_start(bytes[end]); if before_ok && after_ok { out.push_str(impl_arg); i = end; continue; } } out.push(bytes[i] as char); i += 1; } result = out; } result } /// Plan 164 Ф.1: normalize a rendered type string for whitespace-insensitive /// comparison. Removes spaces immediately after `,`, `[`, `(` and before `]`, /// `)` so that `render_type_ref`-produced `"(int, T)"` and the raw-source /// `"(int,T)"` compare equal. fn normalize_type_str(s: &str) -> String { // Replace all whitespace sequences with a single space, then strip spaces // adjacent to punctuation characters in type positions. let mut out = String::with_capacity(s.len()); let mut prev_was_punct = false; for ch in s.chars() { if ch.is_whitespace() { // Suppress space if the previous non-space char was a punctuation // or if we haven't started yet; we'll decide once we see the next // non-space char. if !prev_was_punct && !out.is_empty() { out.push(' '); // tentative space } // Mark: the tentative space may get removed if next char is punct. } else { let is_punct = matches!(ch, '[' | ']' | '(' | ')' | ','); if is_punct { // Remove any trailing space we just added. if out.ends_with(' ') { out.pop(); } out.push(ch); prev_was_punct = true; } else { // If the previous char was a closing punct and we have a space, // the space is valid (e.g. "Option[T]" vs method names). prev_was_punct = false; out.push(ch); } } } // Trailing spaces. while out.ends_with(' ') { out.pop(); } out } /// Plan 164 Ф.1: signature match with optional generic substitution. /// /// Like `check_signature_match` but applies `subst` to the protocol method's /// types before comparison. If `subst` is empty, falls back to the original /// behaviour (for backward compat with non-generic `#impl(Display)`). /// Plan 161 Ф.2 (D355 §4): structural inference — does `fd`'s signature /// match `proto_method`'s shape (arity + every non-generic-typed position) /// for SOME concrete binding of `generic_name`? If so, return the rendered /// concrete type bound to `generic_name` (all occurrences across params + /// return must agree on the same binding). `recv_type_name` allows the /// `Self` ↔ receiver-type equivalence used by the sibling `#impl`-checks. /// Returns `None` when `fd` doesn't conform to the protocol shape at all /// (arity mismatch, a fixed position disagrees, or the generic param would /// have to bind to two different things within one signature) — such an /// overload is silently excluded from the duplicate-impl check rather than /// mis-flagged (conservative: false negatives over false positives here). fn infer_protocol_generic_binding( fd: &FnDecl, proto_method: &crate::ast::EffectMethod, generic_name: &str, recv_type_name: &str, ) -> Option<String> { if fd.params.len() != proto_method.params.len() { return None; } let mut bindings: Vec<String> = Vec::new(); for (fp, pp) in fd.params.iter().zip(proto_method.params.iter()) { match_protocol_type_position(&fp.ty, &pp.ty, generic_name, recv_type_name, &mut bindings)?; } let is_unit_or_none = |r: Option<&TypeRef>| matches!(r, None | Some(TypeRef::Unit(_))); match (fd.return_type.as_ref(), proto_method.return_type.as_ref()) { (a, b) if is_unit_or_none(a) && is_unit_or_none(b) => {} (Some(a), Some(b)) => { match_protocol_type_position(a, b, generic_name, recv_type_name, &mut bindings)?; } _ => return None, } if bindings.is_empty() { // Protocol method never mentions its own generic param in a // params/return position (e.g. it only appears in an `effects` // clause, like `Cleanup[E]`'s `Fail[E]`) — no signal to bind from, // so this check has nothing to say about such a protocol. return None; } let first = &bindings[0]; if bindings.iter().all(|b| b == first) { Some(first.clone()) } else { None } } /// One param/return position of `infer_protocol_generic_binding`: `proto_ty` /// is the protocol's declared type at this position (may reference /// `generic_name` — the protocol's sole generic param — or `Self`); /// `concrete_ty` is the candidate implementation's type at the same /// position. Pushes the inferred binding into `bindings` when `proto_ty` /// contains exactly one whole-word occurrence of `generic_name`; requires /// an exact (mod `Self`) structural match otherwise. `None` = structural /// mismatch at this position (multiple occurrences of the generic param in /// one position, e.g. `(T, T)`, are also treated as `None` — unsupported /// unification depth for V1, same spirit as D355 §6's other V1 limits). fn match_protocol_type_position( concrete_ty: &TypeRef, proto_ty: &TypeRef, generic_name: &str, recv_type_name: &str, bindings: &mut Vec<String>, ) -> Option<()> { let proto_str = render_type_ref(proto_ty); let concrete_str = render_type_ref(concrete_ty); let occurrences = find_whole_word_occurrences(&proto_str, generic_name); if occurrences.is_empty() { if normalize_type_str(&proto_str) == normalize_type_str(&concrete_str) { return Some(()); } if proto_str == "Self" && concrete_str == recv_type_name { return Some(()); } return None; } if occurrences.len() != 1 { return None; } let (s, e) = occurrences[0]; let prefix = &proto_str[..s]; let suffix = &proto_str[e..]; if concrete_str.len() < prefix.len() + suffix.len() || !concrete_str.starts_with(prefix) || !concrete_str.ends_with(suffix) { return None; } let binding = &concrete_str[prefix.len()..concrete_str.len() - suffix.len()]; if binding.is_empty() { return None; } bindings.push(binding.to_string()); Some(()) } /// Whole-word occurrences of `word` in `haystack` (byte offset ranges), /// mirroring the tokenizer in `apply_subst_to_type_str` below. fn find_whole_word_occurrences(haystack: &str, word: &str) -> Vec<(usize, usize)> { let mut out = Vec::new(); if word.is_empty() { return out; } let bytes = haystack.as_bytes(); let is_ident = |b: u8| b.is_ascii_alphanumeric() || b == b'_'; let mut i = 0; while i < bytes.len() { if bytes[i..].starts_with(word.as_bytes()) { let end = i + word.len(); let before_ok = i == 0 || !is_ident(bytes[i - 1]); let after_ok = end >= bytes.len() || !is_ident(bytes[end]); if before_ok && after_ok { out.push((i, end)); i = end; continue; } } i += 1; } out } fn check_signature_match_with_subst( t_method: &FnDecl, proto_method: &crate::ast::EffectMethod, subst: &[(String, String)], ) -> Option<String> { if subst.is_empty() { return check_signature_match(t_method, proto_method); } if t_method.params.len() != proto_method.params.len() { return Some(format!( "arity mismatch: T's `{}` has {} param(s), protocol expects {}", t_method.name, t_method.params.len(), proto_method.params.len(), )); } let recv_name = t_method.receiver.as_ref() .map(|r| r.type_name.as_str()).unwrap_or(""); for (tp, pp) in t_method.params.iter().zip(proto_method.params.iter()) { let tt_str = render_type_ref(&tp.ty); let pt_str = apply_subst_to_type_str(&render_type_ref(&pp.ty), subst); // Plan 164 Ф.1: normalize whitespace before comparing — render produces // "( int, T )" with spaces; raw-source subst may produce "(int,T)". let tt_norm = normalize_type_str(&tt_str); let pt_norm = normalize_type_str(&pt_str); // Also accept Self ↔ recv_name equivalence. if tt_norm != pt_norm { let pt_self = pt_str == "Self" && tt_str == recv_name; let tt_self = tt_str == "Self" && pt_str == recv_name; if !pt_self && !tt_self { return Some(format!( "param `{}`: T has `{}`, protocol expects `{}`", tp.name, tt_str, pt_str, )); } } // [M-conformance-param-mode-check] fix (2026-07-16): protocol-conformance // compared param TYPES only — a param's L1 `mut`/`consume` qualifier // (`Param.is_mut`/`Param.consume`) was never cross-checked against the // protocol declaration. An impl providing `f Fmt` (bare, ro) satisfied // `#impl(Display)` even though `Display` declares `@display(mut f Fmt)` // — decl and impl silently diverge in mode (research-mut-canon пробой D). if tp.is_mut != pp.is_mut || tp.consume != pp.consume { let tp_prefix = if tp.consume { "consume " } else if tp.is_mut { "mut " } else { "" }; let pp_prefix = if pp.consume { "consume " } else if pp.is_mut { "mut " } else { "" }; return Some(format!( "param `{}`: T declares `{}{} {}`, protocol `#impl` requires `{}{} {}` \ (parameter binding-mode `mut`/`consume` must match the protocol \ declaration exactly, not just the type)", tp.name, tp_prefix, tp.name, tt_str, pp_prefix, pp.name, pt_str, )); } } let t_ret = t_method.return_type.as_ref(); let p_ret = proto_method.return_type.as_ref(); let is_unit_or_none = |r: Option<&TypeRef>| -> bool { matches!(r, None | Some(TypeRef::Unit(_))) }; if is_unit_or_none(t_ret) && is_unit_or_none(p_ret) { return None; } match (t_ret, p_ret) { (Some(a), Some(b)) => { let a_str = render_type_ref(a); let b_str = apply_subst_to_type_str(&render_type_ref(b), subst); // Plan 164 Ф.1: normalize before comparison — tuple types with spaces. let a_norm = normalize_type_str(&a_str); let b_norm = normalize_type_str(&b_str); if a_norm != b_norm { let b_self = b_str == "Self" && a_str == recv_name; let a_self = a_str == "Self" && b_str == recv_name; if !b_self && !a_self { return Some(format!( "return type: T returns `{}`, protocol expects `{}`", a_str, b_str, )); } } } _ => return Some(format!( "return type: T returns `{}`, protocol expects `{}`", t_ret.map(render_type_ref).unwrap_or_else(|| "()".into()), p_ret.as_ref().map(|b| apply_subst_to_type_str(&render_type_ref(b), subst)) .unwrap_or_else(|| "()".into()), )), } None } /// Plan 108.4 Ф.2: Check receiver-mutability match between T's implementation method /// and the protocol's method declaration. /// /// Returns Some((method_name, error_code, proto_qualifier, fix_hint)) on mismatch, None on match. /// /// Match rules (proto → impl): /// ro (receiver_mut=false, receiver_consume=false) → impl ro → OK /// mut (receiver_mut=true, receiver_consume=false) → impl mut → OK /// consume (receiver_mut=false, receiver_consume=true) → impl consume → OK /// All other combinations → mismatch → one of the 4 E_PROTO_IMPL_* errors. fn check_receiver_mut_match( t_method: &FnDecl, proto_method: &crate::ast::EffectMethod, _proto_name: &str, _type_name: &str, ) -> Option<(String, &'static str, String, String)> { // Determine proto receiver qualifier. let proto_consume = proto_method.receiver_consume; let proto_mut = proto_method.receiver_mut; // Determine impl receiver qualifier from FnDecl.receiver. let (impl_mut, impl_consume) = t_method.receiver.as_ref() .map(|r| (r.mutable, r.consume)) .unwrap_or((false, false)); // Build a string describing the protocol's declared qualifier (for messages). let proto_qual = if proto_consume { "consume".to_string() } else if proto_mut { "mut".to_string() } else { "ro".to_string() // default (no prefix = ro) }; // Match table: if proto_consume { // Protocol requires consume receiver. if impl_consume { return None; // OK } // impl is mut or ro — wrong; both cases use E_PROTO_IMPL_MUT_FOR_CONSUME // (plan: "protocol `consume @m()`, impl mut/ro" → same error code). let tname_c = t_method.receiver.as_ref().map(|r| r.type_name.as_str()).unwrap_or("T"); let impl_qual = if impl_mut { "mut " } else { "" }; let (code, fix) = ( "E_PROTO_IMPL_MUT_FOR_CONSUME", format!("Change `fn {} {}@{}(...)` to `fn {} consume @{}(...)`.", tname_c, impl_qual, t_method.name, tname_c, t_method.name), ); return Some((t_method.name.clone(), code, proto_qual, fix)); } if proto_mut { // Protocol requires mut receiver. if impl_mut { return None; // OK } // impl is consume → E_PROTO_IMPL_CONSUME_FOR_MUT // impl is ro → E_PROTO_IMPL_RO_FOR_MUT let tname = t_method.receiver.as_ref().map(|r| r.type_name.as_str()).unwrap_or("T"); let (code, fix) = if impl_consume { ("E_PROTO_IMPL_CONSUME_FOR_MUT", format!("Change `fn {} consume @{}(...)` to `fn {} mut @{}(...)`.", tname, t_method.name, tname, t_method.name)) } else { ("E_PROTO_IMPL_RO_FOR_MUT", format!("Change `fn {} @{}(...)` to `fn {} mut @{}(...)`.", tname, t_method.name, tname, t_method.name)) }; return Some((t_method.name.clone(), code, proto_qual, fix)); } // Protocol requires ro receiver (default). if !impl_mut && !impl_consume { return None; // OK — both ro } let tname = t_method.receiver.as_ref().map(|r| r.type_name.as_str()).unwrap_or("T"); let (code, fix) = if impl_mut { ("E_PROTO_IMPL_MUT_FOR_RO", format!("Change `fn {} mut @{}(...)` to `fn {} @{}(...)`.", tname, t_method.name, tname, t_method.name)) } else { // impl_consume — protocol says ro but impl is consume; treat as MUT_FOR_RO variant ("E_PROTO_IMPL_MUT_FOR_RO", format!("Change `fn {} consume @{}(...)` to `fn {} @{}(...)`.", tname, t_method.name, tname, t_method.name)) }; Some((t_method.name.clone(), code, proto_qual, fix)) } /// Two TypeRefs are equivalent if textually equal, OR one is `Self` /// and the other is `recv_name` (or vice versa). fn type_refs_equiv_modulo_self(a: &TypeRef, b: &TypeRef, recv_name: &str) -> bool { let is_self = |t: &TypeRef| matches!(t, TypeRef::Named { path, .. } if path.len() == 1 && path[0] == "Self"); let is_recv = |t: &TypeRef| matches!(t, TypeRef::Named { path, .. } if path.len() == 1 && path[0] == recv_name); if is_self(a) && (is_self(b) || is_recv(b)) { return true; } if is_self(b) && (is_self(a) || is_recv(a)) { return true; } // Plan 180: recurse into composite type-refs so `Self` is matched at ANY // nesting depth against the receiver type (`Result[Self, DeError]` ↔ // `Result[Point, DeError]` for the synthesized `Deserialize` contract — the // top-level check above only compared the bare outer names). match (a, b) { (TypeRef::Named { path: pa, generics: ga, .. }, TypeRef::Named { path: pb, generics: gb, .. }) => { pa == pb && ga.len() == gb.len() && ga.iter().zip(gb.iter()) .all(|(x, y)| type_refs_equiv_modulo_self(x, y, recv_name)) } (TypeRef::Array(ia, _), TypeRef::Array(ib, _)) => type_refs_equiv_modulo_self(ia, ib, recv_name), (TypeRef::FixedArray(na, ia, _), TypeRef::FixedArray(nb, ib, _)) => na == nb && type_refs_equiv_modulo_self(ia, ib, recv_name), (TypeRef::Tuple(ea, _), TypeRef::Tuple(eb, _)) => ea.len() == eb.len() && ea.iter().zip(eb.iter()) .all(|(x, y)| type_refs_equiv_modulo_self(x, y, recv_name)), _ => render_type_ref(a) == render_type_ref(b), } } fn render_method_sig(name: &str, params: &[Param], ret: &Option<TypeRef>) -> String { let p_strs: Vec<String> = params.iter().map(|p| { format!("{} {}", p.name, render_type_ref(&p.ty)) }).collect(); let r = ret.as_ref().map(|t| format!(" -> {}", render_type_ref(t))).unwrap_or_default(); format!("{}({}){}", name, p_strs.join(", "), r) } pub(crate) fn render_type_ref(t: &TypeRef) -> String { match t { TypeRef::Named { path, generics, .. } => { if generics.is_empty() { path.join(".") } else { let g: Vec<String> = generics.iter().map(render_type_ref).collect(); format!("{}[{}]", path.join("."), g.join(", ")) } } TypeRef::Array(inner, _) => format!("[]{}", render_type_ref(inner)), TypeRef::FixedArray(n, inner, _) => format!("[{}]{}", n, render_type_ref(inner)), TypeRef::Tuple(items, _) => { let s: Vec<String> = items.iter().map(render_type_ref).collect(); format!("({})", s.join(", ")) } TypeRef::Func { params, return_type, extern_abi, .. } => { let p: Vec<String> = params.iter().map(render_type_ref).collect(); let r = return_type.as_ref().map(|t| format!(" -> {}", render_type_ref(t))).unwrap_or_default(); // D353: surface the ABI-tag so diagnostics distinguish Nova-ABI // `*fn` from C-ABI `*extern "C" fn`. let abi = match extern_abi.as_deref() { Some("C") => "extern \"C\" ", _ => "", }; format!("{}fn({}){}", abi, p.join(", "), r) } // Plan 97 Ф.2 (D142): анонимный protocol-тип — пишется через // render_method_sig, чтобы R5.3 diagnostic'и видели полную // сигнатуру inline-protocol bound'а. TypeRef::Protocol { methods, .. } => { let sigs: Vec<String> = methods .iter() .map(|m| { let prefix = if m.is_static { "." } else { "" }; let full = render_method_sig(&m.name, &m.params, &m.return_type); format!("{}{}", prefix, full) }) .collect(); format!("protocol {{ {} }}", sigs.join("; ")) } TypeRef::Unit(_) => "()".to_string(), // D176 (Plan 108): readonly T — display as "readonly T" TypeRef::Readonly(inner, _) => format!("ro {}", render_type_ref(inner)), // Plan 118 D216 §1 / Plan 118.5 V2 (2026-06-04): typed pointer // `*T` family — split into three AST wrapper arms after V2 amend. // - Pointer(T, _) → `* T` (canonical readonly) // - Mut(inner, _) → `mut <inner>` (right-binding wrapper) // - Uninit(inner, _) → `uninit <inner>` (right-binding wrapper); // `unsafe <inner>` instead when inner is `Func` (§10a rename, // Plan 174.5 — D216 §10 legacy fn-pointer shape kept `unsafe`). TypeRef::Pointer(inner, _) => format!("* {}", render_type_ref(inner)), TypeRef::Mut(inner, _) => format!("mut {}", render_type_ref(inner)), TypeRef::Uninit(inner, _) => { let kw = if matches!(inner.as_ref(), TypeRef::Func { .. }) { "unsafe" } else { "uninit" }; format!("{} {}", kw, render_type_ref(inner)) } TypeRef::Ref(inner, _) => format!("ref {}", render_type_ref(inner)), } } /// D28 effect inference для private fn. /// /// Walk модуль mutably: для каждой private (`!is_export`) fn, /// если её тело использует `throw`, и в effect-row нет ни одного /// `Fail`/`Fail[E]`/`Fail[any]` — добавляем `Fail` (placeholder). /// /// Это упрощённая реализация D28 для bootstrap'а: /// - Полная version выводила бы конкретный E из type-of(throw expr). /// Bootstrap не имеет точного типизатора, поэтому выводит просто /// `Fail` (placeholder, по D65 — inference placeholder). /// - Для public fn ничего не делаем (D62: явная декларация обязательна). /// - Транзитивная inference (callee имеет Fail → caller тоже) не /// реализована; программист должен явно импортировать. /// /// Эффекты типа Db/Net/Time/etc. **не** добавляются автоматически — /// они resource-capability и должны быть видны в сигнатуре, программист /// объявляет явно. Только Fail имеет особый placeholder-режим. pub fn infer_effects(module: &mut Module) { // Plan 221.1 №131 (D28 §Правило вывода п.1 — companion to the // Fail-on-throw push below): known effect-type names in this CU, needed // to recognize the `Effect.op(...)` raw-call shape (same discrimination // `CapabilityCtx.effect_decls` / `check_handler_op_declarations` use). // Scans `module.items` + peer-files (co-equal folder-module files, // D-folder-module convention) — module.items alone would miss effects // declared in a sibling file of the same folder-module. let mut known_effects: HashSet<String> = HashSet::new(); for it in &module.items { if let Item::Type(td) = it { if matches!(td.kind, TypeDeclKind::Effect(_)) { known_effects.insert(td.name.clone()); } } } for pf in &module.peer_files { for it in &pf.items_here { if let Item::Type(td) = it { if matches!(td.kind, TypeDeclKind::Effect(_)) { known_effects.insert(td.name.clone()); } } } } for item in &mut module.items { if let Item::Fn(f) = item { if f.is_export { continue; } if has_throw_in_fn(f) && !has_fail_effect(&f.effects) { let span = f.span; f.effects.push(TypeRef::Named { path: vec!["Fail".to_string()], generics: vec![], span, }); } // Plan 221.1 №131 (D62 ENFORCED, D28 §Правило вывода п.1): a // DIRECT raw effect-op call (`Effect.op(...)`) in a private fn's // OWN body is a direct effect — the compiler auto-adds it to // the signature, same footing as the `Fail`-on-throw push // above ("этот эффект добавляется" — mandatory inference, not // a mere warning). Mirrors the EXPORTED hard-error gate // (`check_raw_effect_op_declared`, `CapState.is_export`) — // this is the private-fn side of the same D62 scope split. if !known_effects.is_empty() { let mut used: HashSet<String> = HashSet::new(); collect_raw_effect_ops_in_fn(f, &known_effects, &mut used); if !used.is_empty() { let already: HashSet<String> = f.effects.iter() .filter_map(|e| match e { TypeRef::Named { path, .. } => path.last().cloned(), _ => None, }) .collect(); let mut new_names: Vec<String> = used.into_iter() .filter(|n| !already.contains(n)) .collect(); // Deterministic order — HashSet iteration order is not stable, // and effect-row order is visible (`--show-effects`, error text). new_names.sort(); let span = f.span; for name in new_names { f.effects.push(TypeRef::Named { path: vec![name], generics: vec![], span }); } } } } } } /// Plan 221.1 №131: собирает имена эффектов, чьи операции вызваны ПРЯМО /// (`Effect.op(...)`) в теле `f` — сама `f`, БЕЗ вложенных `Lambda`/ /// `HandlerLit`/`ProtocolLit` тел (у них своя scope-граница эффектов, тот /// же принцип что `has_throw_in_expr`'s `Lambda => false`, см. её /// комментарий). `known` — множество имён деклараций `type X effect {…}` /// в этом CU (эффект НЕ в этом множестве — не эффект, не считается, даже /// если совпадает по имени с локальной переменной/типом). fn collect_raw_effect_ops_in_fn(f: &FnDecl, known: &HashSet<String>, out: &mut HashSet<String>) { match &f.body { FnBody::Expr(e) => collect_raw_effect_ops_expr(e, known, out), FnBody::Block(b) => collect_raw_effect_ops_block(b, known, out), FnBody::External => {} } } /// Path-голова 2-сегментного call-вида `Effect.op(...)` — та же форма, /// что распознаёт `check_capabilities_at`'s "1. Effect-op call" (см. её /// построение `path` из `ExprKind::Path`/`Member{obj: Ident, ..}`). fn raw_effect_op_head(func: &Expr) -> Option<String> { match &func.kind { ExprKind::Path(parts) if parts.len() == 2 => Some(parts[0].clone()), ExprKind::Member { obj, .. } => match &obj.kind { ExprKind::Ident(n) => Some(n.clone()), _ => None, }, _ => None, } } fn collect_raw_effect_ops_block(b: &Block, known: &HashSet<String>, out: &mut HashSet<String>) { for s in &b.stmts { collect_raw_effect_ops_stmt(s, known, out); } if let Some(t) = &b.trailing { collect_raw_effect_ops_expr(t, known, out); } } fn collect_raw_effect_ops_stmt(s: &Stmt, known: &HashSet<String>, out: &mut HashSet<String>) { match s { Stmt::Expr(e) => collect_raw_effect_ops_expr(e, known, out), Stmt::Let(decl) => collect_raw_effect_ops_expr(&decl.value, known, out), Stmt::Const(_) => {} Stmt::Assign { target, value, .. } => { collect_raw_effect_ops_expr(target, known, out); collect_raw_effect_ops_expr(value, known, out); } Stmt::Return { value, .. } => { if let Some(v) = value { collect_raw_effect_ops_expr(v, known, out); } } Stmt::Throw { value, .. } => collect_raw_effect_ops_expr(value, known, out), Stmt::Break(_) | Stmt::Continue(_) => {} // D90: defer/errdefer body — как и has_throw, отдельный scope с // ограничениями; сырые опы там всё равно принадлежат ЭТОЙ fn // (defer не заводит своей сигнатуры) — сканируем. Stmt::Defer { body, .. } => collect_raw_effect_ops_expr(body, known, out), Stmt::ConsumeScope { init, body, .. } => { collect_raw_effect_ops_expr(init, known, out); collect_raw_effect_ops_block(body, known, out); } Stmt::AssertStatic { expr, .. } | Stmt::Assume { expr, .. } => collect_raw_effect_ops_expr(expr, known, out), Stmt::Apply { args, .. } => { for a in args { collect_raw_effect_ops_expr(a, known, out); } } Stmt::Calc { steps, .. } => { for step in steps { collect_raw_effect_ops_expr(&step.expr, known, out); } } Stmt::Reveal { .. } => {} Stmt::TupleAssign { lhs, rhs, .. } => { for e in lhs { collect_raw_effect_ops_expr(e, known, out); } for e in rhs { collect_raw_effect_ops_expr(e, known, out); } } } } fn collect_raw_effect_ops_expr(e: &Expr, known: &HashSet<String>, out: &mut HashSet<String>) { match &e.kind { ExprKind::Call { func, args, .. } => { if let Some(head) = raw_effect_op_head(func) { if known.contains(&head) { out.insert(head); } } collect_raw_effect_ops_expr(func, known, out); for a in args { collect_raw_effect_ops_expr(a.expr(), known, out); } } ExprKind::Throw(inner) | ExprKind::Try(inner) | ExprKind::Bang(inner) | ExprKind::Unary { operand: inner, .. } => collect_raw_effect_ops_expr(inner, known, out), ExprKind::Binary { left, right, .. } => { collect_raw_effect_ops_expr(left, known, out); collect_raw_effect_ops_expr(right, known, out); } ExprKind::Member { obj, .. } => collect_raw_effect_ops_expr(obj, known, out), ExprKind::Index { obj, index } => { collect_raw_effect_ops_expr(obj, known, out); collect_raw_effect_ops_expr(index, known, out); } ExprKind::If { cond, then, else_, .. } => { collect_raw_effect_ops_expr(cond, known, out); collect_raw_effect_ops_block(then, known, out); match else_ { Some(ElseBranch::Block(b)) => collect_raw_effect_ops_block(b, known, out), Some(ElseBranch::If(e2)) => collect_raw_effect_ops_expr(e2, known, out), None => {} } } ExprKind::IfLet { scrutinee, then, else_, .. } => { collect_raw_effect_ops_expr(scrutinee, known, out); collect_raw_effect_ops_block(then, known, out); match else_ { Some(ElseBranch::Block(b)) => collect_raw_effect_ops_block(b, known, out), Some(ElseBranch::If(e2)) => collect_raw_effect_ops_expr(e2, known, out), None => {} } } ExprKind::Match { scrutinee, arms } => { collect_raw_effect_ops_expr(scrutinee, known, out); for arm in arms { match &arm.body { MatchArmBody::Expr(e2) => collect_raw_effect_ops_expr(e2, known, out), MatchArmBody::Block(b) => collect_raw_effect_ops_block(b, known, out), } if let Some(g) = &arm.guard { collect_raw_effect_ops_expr(g, known, out); } } } ExprKind::While { cond, body, .. } => { collect_raw_effect_ops_expr(cond, known, out); collect_raw_effect_ops_block(body, known, out); } ExprKind::WhileLet { scrutinee, body, .. } => { collect_raw_effect_ops_expr(scrutinee, known, out); collect_raw_effect_ops_block(body, known, out); } ExprKind::For { iter, body, .. } => { collect_raw_effect_ops_expr(iter, known, out); collect_raw_effect_ops_block(body, known, out); } ExprKind::Loop { body, .. } => collect_raw_effect_ops_block(body, known, out), ExprKind::Select { arms } => { for a in arms { match &a.op { SelectOp::Recv { chan, .. } => collect_raw_effect_ops_expr(chan, known, out), SelectOp::Send { chan, value } => { collect_raw_effect_ops_expr(chan, known, out); collect_raw_effect_ops_expr(value, known, out); } SelectOp::Default => {} } if let Some(g) = &a.guard { collect_raw_effect_ops_expr(g, known, out); } collect_raw_effect_ops_block(&a.body, known, out); } } ExprKind::Block(b) => collect_raw_effect_ops_block(b, known, out), // Own effect-row scope — same reasoning as `has_throw_in_expr`'s // `Lambda => false` and handler-op-decl's HandlerLit skip above: // a raw op used inside a nested closure/handler-literal/protocol- // literal body is that construct's OWN obligation (its coercion // target / the `with`-establishment context), not this fn's. ExprKind::Lambda { .. } | ExprKind::HandlerLit { .. } | ExprKind::ProtocolLit { .. } => {} ExprKind::Range { start, end, .. } => { if let Some(s) = start { collect_raw_effect_ops_expr(s, known, out); } if let Some(e2) = end { collect_raw_effect_ops_expr(e2, known, out); } } ExprKind::TupleLit(elems) => { for e2 in elems { collect_raw_effect_ops_expr(e2, known, out); } } ExprKind::ArrayLit(elems) => { for el in elems { match el { ArrayElem::Item(e2) => collect_raw_effect_ops_expr(e2, known, out), ArrayElem::Spread(e2) => collect_raw_effect_ops_expr(e2, known, out), } } } ExprKind::RecordLit { fields, .. } => { for fld in fields { if let Some(v) = &fld.value { collect_raw_effect_ops_expr(v, known, out); } } } // `with X = …` LOCALLY discharges `X` (D11/Правило 4) — но other // effects used inside `body` still belong to this fn; scan both // the handler-construction expr and the body, same as // `has_throw_in_expr`'s `With` arm. We don't special-case the // discharged name here: it's harmless if `X` also appears raw // elsewhere OUTSIDE this with-block in the same fn (still a real // direct use needing declaration); a raw op INSIDE the with-block // for the SAME `X` it installs would double-count into `used` but // that's conservative-safe for private auto-inference (adds `X` // to the signature even though technically discharged locally — // over-declaring in private is harmless, D28 §"Случайное // расширение"). ExprKind::With { bindings, body } => { for bd in bindings { collect_raw_effect_ops_expr(&bd.handler, known, out); } collect_raw_effect_ops_block(body, known, out); } ExprKind::Spawn(inner) => collect_raw_effect_ops_expr(inner, known, out), ExprKind::Supervised { body, cancel, deadline, on_timeout } => { collect_raw_effect_ops_block(body, known, out); if let Some(c) = cancel { collect_raw_effect_ops_expr(c, known, out); } if let Some(dl) = deadline { collect_raw_effect_ops_expr(&dl.expr, known, out); } if let Some(oh) = on_timeout { collect_raw_effect_ops_expr(oh, known, out); } } ExprKind::ParallelFor { iter, body, .. } => { collect_raw_effect_ops_expr(iter, known, out); collect_raw_effect_ops_block(body, known, out); } ExprKind::TurboFish { base, .. } => collect_raw_effect_ops_expr(base, known, out), _ => {} } } /// Есть ли хотя бы один `Fail`/`Fail[...]` в effect-row. pub(crate) fn has_fail_effect(effects: &[TypeRef]) -> bool { effects.iter().any(|e| { matches!(e, TypeRef::Named { path, .. } if path.len() == 1 && path[0] == "Fail") }) } /// D432-амендмент 2026-08-04 (№315 fix): имена эффектов, присутствующих в /// `cleanup_row` (effect-row объявленного `@cleanup`), но отсутствующих в /// `current_fn_effects` (собственные, явно объявленные эффекты объемлющей /// функции — того места, откуда компилятор синтезирует вызов /// `X.@cleanup(outcome)`). Сравнение ТОЛЬКО по имени (`path.last()`) — /// тот же прецедент, что `has_fail_effect`: несовпадение generic-аргумента /// `Fail[E]` этой проверке не важно (D65 — любой `Fail[..]` в сигнатуре /// удовлетворяет требование «Fail объявлен»). fn missing_cleanup_effect_names(cleanup_row: &[TypeRef], current_fn_effects: &[TypeRef]) -> Vec<String> { let declared: HashSet<&str> = current_fn_effects.iter() .filter_map(|e| match e { TypeRef::Named { path, .. } => path.last().map(|s| s.as_str()), _ => None, }) .collect(); let mut missing: Vec<String> = Vec::new(); for eff in cleanup_row { if let TypeRef::Named { path, .. } = eff { if let Some(nm) = path.last() { if !declared.contains(nm.as_str()) && !missing.iter().any(|m| m == nm) { missing.push(nm.clone()); } } } } missing } /// D432-амендмент 2026-08-04 (№315 fix): диагностика /// `E_D432_CLEANUP_EFFECT_NOT_DECLARED` — авто-вставленный компилятором /// вызов `<ty>.@cleanup(outcome)` (на непотреблённом `consume`-биндинге — /// bare leftover-at-exit ИЛИ block-форма `consume X = e { body }`) несёт /// эффект(ы), которые объемлющая функция не объявляет. Текст по образцу /// D158-defer-fail-not-in-sig (тот же класс: эффект компилятор-вставленного /// вызова протекает мимо sig-объявленных эффектов), но общий — любой прямой /// эффект, не только `Fail`. Диагностика указывает на САМ биндинг (D432 §1 /// п.3 «с указанием на биндинг, который её породил»), не на место throw'а — /// throw-сайта в тексте программы нет, он синтезирован. fn d432_cleanup_effect_diag(binding_name: &str, ty: &str, missing: &[String], span: Span) -> Diagnostic { let missing_fmt = missing.iter().map(|m| format!("`{}`", m)).collect::<Vec<_>>().join(", "); Diagnostic::new( format!( "[E_D432_CLEANUP_EFFECT_NOT_DECLARED] variable `{name}` (type `{ty}`) is left \ un-consumed here; the compiler auto-inserts `{ty}.@cleanup(outcome)` on this \ binding (D432 hybrid-C auto-cleanup), and that call carries effect(s) {missing_fmt} \ which the enclosing function does not declare. D432 §1 amendment (2026-08-04): a \ cleanup call's effects are DIRECT effects of the enclosing function (the call is \ physically generated in its body), not transitive — add {missing_fmt} to this \ function's signature, or explicitly consume `{name}` (e.g. call its close/finalize \ method) before scope-exit to avoid auto-cleanup entirely.", name = binding_name, ty = ty, missing_fmt = missing_fmt, ), span, ) } /// Содержит ли тело fn выражение `throw` (рекурсивно). fn has_throw_in_fn(f: &FnDecl) -> bool { match &f.body { FnBody::Expr(e) => has_throw_in_expr(e), FnBody::Block(b) => has_throw_in_block(b), // D82: external fn — тела нет; throw'ы декларируются через // Fail[E] effect-аннотацию в сигнатуре, не в теле. FnBody::External => false, } } fn has_throw_in_block(b: &Block) -> bool { for s in &b.stmts { if has_throw_in_stmt(s) { return true; } } if let Some(t) = &b.trailing { if has_throw_in_expr(t) { return true; } } false } fn has_throw_in_stmt(s: &Stmt) -> bool { match s { Stmt::Expr(e) => has_throw_in_expr(e), Stmt::Let(decl) => has_throw_in_expr(&decl.value), Stmt::Const(_) => false, Stmt::Assign { target, value, .. } => has_throw_in_expr(target) || has_throw_in_expr(value), Stmt::Return { value, .. } => value.as_ref().map_or(false, has_throw_in_expr), Stmt::Throw { value, .. } => { // Statement-level throw: явный сигнал, что Fail нужен. let _ = value; true } Stmt::Break(_) | Stmt::Continue(_) => false, // D90: defer/errdefer body **запрещают** throw внутри (Ф.3 // body-constraint). Throw в body — compile error. Поэтому // body не считается throw-носителем — он отдельный scope с // ограничением. Если в body throw обнаружен — Ф.3 даст // отдельную compile error раньше этой проверки. Stmt::Defer { .. } => false, // Plan 110 D188: consume scope-block body может содержать throw — // D188 R3 cancel-shield масаит throw к caller'у после cleanup; // для has_throw analysis считаем body как throw-носитель. Stmt::ConsumeScope { init, body, .. } => { if has_throw_in_expr(init) { return true; } for s in &body.stmts { if has_throw_in_stmt(s) { return true; } } body.trailing.as_ref().map_or(false, |t| has_throw_in_expr(t)) } // Plan 33.2 Ф.8: assert_static — bool expr, no throw inside. Stmt::AssertStatic { expr, .. } | Stmt::Assume { expr, .. } => has_throw_in_expr(expr), // Ф.4.1: apply — ghost, args могут содержать throw (теоретически нет, но проверяем). Stmt::Apply { args, .. } => args.iter().any(has_throw_in_expr), // Ф.4.2: calc — ghost, шаги могут содержать throw. Stmt::Calc { steps, .. } => steps.iter().any(|s| has_throw_in_expr(&s.expr)), // Plan 33.9 Ф.2: reveal — ghost, no throw inside. Stmt::Reveal { .. } => false, // Plan 136: tuple destructuring assignment — check all lhs + rhs. Stmt::TupleAssign { lhs, rhs, .. } => { lhs.iter().any(has_throw_in_expr) || rhs.iter().any(has_throw_in_expr) } } } fn has_throw_in_expr(e: &Expr) -> bool { match &e.kind { ExprKind::Throw(_) => true, ExprKind::Try(inner) => has_throw_in_expr(inner), // Plan 19, C7 (D85): `!!` тоже может бросить (`Err`/`None`). // №428 fix (2026-08-07): was `has_throw_in_expr(inner)` — ONLY // recursed into `inner`, never counting the `!!` operator ITSELF as // a throw source. `inner` (e.g. `risky(x)` in `risky(x)!!`) is // normally an ordinary non-throwing expr (a résult-returning call), // so this arm returned `false` for the textbook `expr!!` idiom — // `has_throw_in_fn` (the D28 auto-inference gate `infer_effects` // uses to silently add `Fail` to a private fn) MISSED every private // fn whose ONLY Fail-source was a bare `!!` (no separate `throw` // statement). D85 says `!!` is ALWAYS throw-style — same // unconditional footing as `ExprKind::Throw(_) => true` two arms // above; `?` (`Try`, kept recursion-only just below) stays // DIFFERENT on purpose — it is dual (return-only vs throw-style // depending on context), so it must not be hardcoded `true` here. ExprKind::Bang(inner) => { let _ = inner; true } ExprKind::Binary { left, right, .. } => has_throw_in_expr(left) || has_throw_in_expr(right), ExprKind::Unary { operand, .. } => has_throw_in_expr(operand), ExprKind::Call { func, args, .. } => has_throw_in_expr(func) || args.iter().any(|a| has_throw_in_expr(a.expr())), ExprKind::Member { obj, .. } => has_throw_in_expr(obj), ExprKind::Index { obj, index } => has_throw_in_expr(obj) || has_throw_in_expr(index), ExprKind::If { cond, then, else_, .. } => { if has_throw_in_expr(cond) || has_throw_in_block(then) { return true; } match else_ { Some(ElseBranch::Block(b)) => has_throw_in_block(b), Some(ElseBranch::If(e)) => has_throw_in_expr(e), None => false, } } ExprKind::IfLet { scrutinee, then, else_, .. } => { if has_throw_in_expr(scrutinee) || has_throw_in_block(then) { return true; } match else_ { Some(ElseBranch::Block(b)) => has_throw_in_block(b), Some(ElseBranch::If(e)) => has_throw_in_expr(e), None => false, } } ExprKind::Match { scrutinee, arms } => { if has_throw_in_expr(scrutinee) { return true; } arms.iter().any(|arm| match &arm.body { MatchArmBody::Expr(e) => has_throw_in_expr(e), MatchArmBody::Block(b) => has_throw_in_block(b), }) } ExprKind::While { cond, body, .. } => has_throw_in_expr(cond) || has_throw_in_block(body), ExprKind::WhileLet { scrutinee, body, .. } => has_throw_in_expr(scrutinee) || has_throw_in_block(body), ExprKind::For { iter, body, .. } => has_throw_in_expr(iter) || has_throw_in_block(body), ExprKind::Loop { body, .. } => has_throw_in_block(body), ExprKind::Select { arms } => arms.iter().any(|a| { (match &a.op { SelectOp::Recv { chan, .. } => has_throw_in_expr(chan), SelectOp::Send { chan, value } => has_throw_in_expr(chan) || has_throw_in_expr(value), SelectOp::Default => false, }) || a.guard.as_ref().map_or(false, has_throw_in_expr) || has_throw_in_block(&a.body) }), ExprKind::Block(b) => has_throw_in_block(b), ExprKind::Lambda { .. } => false, // Lambda has its own scope; throw inside lambda — её эффекты, не текущей fn. ExprKind::Range { start, end, .. } => start.as_deref().map_or(false, has_throw_in_expr) || end.as_deref().map_or(false, has_throw_in_expr), ExprKind::TupleLit(elems) => elems.iter().any(has_throw_in_expr), ExprKind::ArrayLit(elems) => elems.iter().any(|el| match el { ArrayElem::Item(e) => has_throw_in_expr(e), ArrayElem::Spread(e) => has_throw_in_expr(e), }), ExprKind::RecordLit { fields, .. } => fields.iter().any(|f| f.value.as_ref().map_or(false, has_throw_in_expr)), ExprKind::With { bindings, body } => { if bindings.iter().any(|b| has_throw_in_expr(&b.handler)) { return true; } has_throw_in_block(body) } ExprKind::Spawn(e) => has_throw_in_expr(e), ExprKind::Supervised { body, cancel, deadline, on_timeout } => { has_throw_in_block(body) || cancel.as_ref().map_or(false, |c| has_throw_in_expr(c)) || deadline.as_ref().map_or(false, |dl| has_throw_in_expr(&dl.expr)) || on_timeout.as_ref().map_or(false, |oh| has_throw_in_expr(oh)) } ExprKind::ParallelFor { iter, body, .. } => has_throw_in_expr(iter) || has_throw_in_block(body), ExprKind::TurboFish { base, .. } => has_throw_in_expr(base), _ => false, } } // Plan 172.1 U.5.4: `ty_of_ref` (TypeRef → lossy `Ty`) was deleted — its sole consumer // (the `never` propagation check) now uses `ResolvedType::from_type_ref` (which mirrors // `ty_of_ref`'s `never`/collapse rules losslessly). `Ty` enum deleted with it. /// D84: structural equality для TypeRef (игнорирует Span'ы). /// /// Рспользуется для detection дублированных signatures свободных /// функций — "точное совпадение" arity + arg-types запрещено как /// ambiguous overload без возможности резолва. /// /// Не использует PartialEq/Eq derive потому что TypeRef содержит /// Span'ы (позиции в исходнике), которые отличаются у разных /// определений того же типа. fn typeref_equal(a: &TypeRef, b: &TypeRef) -> bool { match (a, b) { ( TypeRef::Named { path: pa, generics: ga, .. }, TypeRef::Named { path: pb, generics: gb, .. }, ) => { pa == pb && ga.len() == gb.len() && ga.iter().zip(gb.iter()).all(|(x, y)| typeref_equal(x, y)) } (TypeRef::Array(ia, _), TypeRef::Array(ib, _)) => typeref_equal(ia, ib), (TypeRef::FixedArray(na, ia, _), TypeRef::FixedArray(nb, ib, _)) => { na == nb && typeref_equal(ia, ib) } (TypeRef::Tuple(ea, _), TypeRef::Tuple(eb, _)) => { ea.len() == eb.len() && ea.iter().zip(eb.iter()).all(|(x, y)| typeref_equal(x, y)) } ( TypeRef::Func { params: pa, return_type: ra, effects: ea, .. }, TypeRef::Func { params: pb, return_type: rb, effects: eb, .. }, ) => { pa.len() == pb.len() && pa.iter().zip(pb.iter()).all(|(x, y)| typeref_equal(x, y)) && match (ra.as_deref(), rb.as_deref()) { (Some(x), Some(y)) => typeref_equal(x, y), (None, None) => true, _ => false, } && ea.len() == eb.len() && ea.iter().zip(eb.iter()).all(|(x, y)| typeref_equal(x, y)) } (TypeRef::Unit(_), TypeRef::Unit(_)) => true, // D176 (Plan 108): readonly T == readonly T if inner equal. (TypeRef::Readonly(ia, _), TypeRef::Readonly(ib, _)) => typeref_equal(ia, ib), // Plan 118.1.6: V2 modifier wrappers — Pointer/Mut/Unsafe must compare // structurally with their inner types. Без этих арм две одинаковые // подписи с *T / mut T / unsafe T силкомо не считаются равными, // ломая duplicate-signature detection и breaking coercion checks // (E_UNSAFE_FN_PTR_COERCION ниже полагается на структурное сравнение // outer Unsafe wrapper). (TypeRef::Pointer(ia, _), TypeRef::Pointer(ib, _)) => typeref_equal(ia, ib), (TypeRef::Mut(ia, _), TypeRef::Mut(ib, _)) => typeref_equal(ia, ib), (TypeRef::Uninit(ia, _), TypeRef::Uninit(ib, _)) => typeref_equal(ia, ib), _ => false, } } /// [M-generic-arg-type-mismatch-silent] — extract `(base-name, type-arguments)` /// from ANY generic type: a user `Stack[T]` / `Map[K,V]` / `Box[T]` as well as /// the builtin `Vec[T]` and its `[]T` array sugar. Deliberately NOT Vec-specific: /// the silent element-reinterpretation bug applies to every generic type — a /// `Stack[int]` passed where `Stack[u32]` is expected is the same /// pointer-reinterpretation footgun as `Vec[int]`→`Vec[u32]`. /// /// The `[]T` array form normalizes to base `"Vec"` (D239: `[]T ≡ Vec[T]`) so the /// two spellings of the one builtin compare equal. Peels `ro`/`mut` binding /// wrappers. Returns `None` for non-generic types (bare names, primitives, /// tuples, …) — the caller then skips the check. fn generic_args(tr: &TypeRef) -> Option<(String, Vec<&TypeRef>)> { match tr { TypeRef::Readonly(inner, _) | TypeRef::Mut(inner, _) => generic_args(inner), TypeRef::Array(inner, _) => Some(("Vec".to_string(), vec![inner.as_ref()])), TypeRef::FixedArray(_, inner, _) => Some(("Vec".to_string(), vec![inner.as_ref()])), TypeRef::Named { path, generics, .. } if !generics.is_empty() => { Some((path.last()?.clone(), generics.iter().collect())) } _ => None, } } /// Plan 172.2: substitute generic type-params (`subst` keys) throughout a /// `TypeRef`. A bare `Named{T}` (single segment, no args) whose name is in /// `subst` is replaced by its concrete binding; everything else recurses /// structurally. Used to instantiate a generic method's param types from the /// receiver's concrete type-args (`Vec[T] @push(value T)` + receiver `Vec[u32]` /// → param `u32`) so the method-arg narrowing check has a CONCRETE expected type /// (a bare `T` would resolve to `ResolvedType::Any` and skip the check). fn subst_typeref(t: &TypeRef, subst: &HashMap<String, TypeRef>) -> TypeRef { match t { TypeRef::Named { path, generics, span } => { if path.len() == 1 && generics.is_empty() { if let Some(rep) = subst.get(&path[0]) { return rep.clone(); } } TypeRef::Named { path: path.clone(), generics: generics.iter().map(|g| subst_typeref(g, subst)).collect(), span: *span, } } TypeRef::Array(inner, s) => TypeRef::Array(Box::new(subst_typeref(inner, subst)), *s), TypeRef::FixedArray(n, inner, s) => TypeRef::FixedArray(*n, Box::new(subst_typeref(inner, subst)), *s), TypeRef::Tuple(elems, s) => TypeRef::Tuple(elems.iter().map(|e| subst_typeref(e, subst)).collect(), *s), TypeRef::Readonly(inner, s) => TypeRef::Readonly(Box::new(subst_typeref(inner, subst)), *s), TypeRef::Mut(inner, s) => TypeRef::Mut(Box::new(subst_typeref(inner, subst)), *s), TypeRef::Pointer(inner, s) => TypeRef::Pointer(Box::new(subst_typeref(inner, subst)), *s), TypeRef::Uninit(inner, s) => TypeRef::Uninit(Box::new(subst_typeref(inner, subst)), *s), // Plan 184: `ref T` — подставляем цель (`f[T]() -> ref T` при T=…). TypeRef::Ref(inner, s) => TypeRef::Ref(Box::new(subst_typeref(inner, subst)), *s), // Func/Protocol/Unit — params/methods rarely reference the receiver's // type-params in a way the narrowing check needs; clone as-is. _ => t.clone(), } } /// Plan 172.2: map a generic method's RECEIVER type-params to the concrete /// type-args of the call-site receiver. `recv` is the method's declared receiver /// (`Vec[T]` → `generics = [Named{T}]`); `recv_ty` is the inferred call-site type /// (`Vec[u32]` → `[u32]`; `[]u32` → element `u32`, the slice alias). Returns /// `{T: u32}`. Empty when arity mismatches / names aren't bare params (