/
nv-lang
/
nova
Обзор
Документация
Войти
/
nv-lang
/
nova
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
main
compiler-codegen/src/parser/mod.rs
13 521 строка
651 KB
Evgeniy Golovin
gate: clear four of the eight failures the authoritative run reported
10 авг 2026, 13:17
10 авг 2026, 13:17
42e8a07
Код
Авторство
О чём код?
//! Recursive-descent parser для Nova. //! //! Один большой модуль: `Parser` — состояние с указателем на токены, //! методы для каждого нетерминала. Никаких внешних парсер-комбинаторов: //! минимум зависимостей в bootstrap'е. use crate::ast::*; use crate::diag::{Diagnostic, Span}; use crate::lexer::{Token, TokenKind}; /// Plan 164 Ф.1 (D268 amend): helper — extract the bare protocol name from /// an impl-spec string that may carry generic args, e.g.: /// "Next" → "Next" /// "Next[T]" → "Next" /// "Next[(int,T)]" → "Next" /// Used wherever impl_protocols entries are looked up in self.types (keyed by /// bare name) or compared against protocol_method_registry keys. pub fn impl_spec_base_name(spec: &str) -> &str { match spec.find('[') { Some(idx) => &spec[..idx], None => spec, } } /// Plan 164 Ф.1: extract the raw bracket suffix from an impl-spec string, /// e.g. "Next[U]" → "[U]", "Next" → "". Used by the checker to build the /// generic substitution map (proto generic → impl generic). pub fn impl_spec_args_text(spec: &str) -> &str { match spec.find('[') { Some(idx) => &spec[idx..], None => "", } } /// Plan 33.1 (D24): contract-related атрибуты, собранные перед `fn`. /// /// Передаются из `parse_item` в `parse_fn`. По умолчанию — все поля /// в `Default`/`None`/`Unknown` (backward-compat для функций без /// контрактов и атрибутов). #[derive(Debug, Clone, Default)] pub(crate) struct ContractAttrs { pub verify_mode: VerifyMode, pub verify_timeout_ms: Option<u32>, pub purity: Purity, pub is_trusted: bool, /// Plan 33.9 Ф.1: `#opaque` attribute. pub is_opaque: bool, /// Plan 33.9 Ф.3: `#fuel(n)` attribute. pub fuel: Option<u32>, /// Plan 33.7 Ф.4: `#nooverflow` — emit overflow VCs for BitVec arithmetic. pub no_overflow: bool, /// Plan 103.6 / Plan 113: sync interaction class from #realtime/#parks/#wakes. pub sync_class: Option<crate::ast::SyncClass>, /// Plan 118.1.7 (D2 amend): `unsafe fn` keyword modifier. Set by parse_item /// when `unsafe` keyword is seen before `fn` (not via parse_contract_attrs — /// `#unsafe fn` now emits E_UNSAFE_ATTR_DEPRECATED hard error). Body /// implicitly in unsafe context; callers must `unsafe { }` wrap. /// Checker enforcement E_UNSAFE_CALL_REQUIRES_WRAP — Ф.3.3-3.5 followup. pub unsafe_attr: bool, /// Plan 114.4.4 Ф.1 (D199 V3): `#fn_eval_max_depth(N)` — per-fn override /// const fn evaluator recursion depth (default 256). For deep recursion /// (e.g. fact(500) с big-int arith). 0 < N <= 65535. pub fn_eval_max_depth: Option<u32>, /// Plan 124.6 (D225): `#test_access(TypeX, TypeY, ...)` — fn body /// gets priv-field access to listed types (escape hatch для unit /// tests). Default empty Vec = no extra access. pub test_access_for: Vec<String>, } impl ContractAttrs { pub(crate) fn is_empty(&self) -> bool { matches!(self.verify_mode, VerifyMode::Default) && self.verify_timeout_ms.is_none() && matches!(self.purity, Purity::Unknown) && !self.is_trusted && !self.is_opaque && self.fuel.is_none() && !self.no_overflow && self.sync_class.is_none() && !self.unsafe_attr && self.fn_eval_max_depth.is_none() && self.test_access_for.is_empty() } } pub struct Parser { tokens: Vec<Token>, pos: usize, /// Когда true — `Ident { ... }` не парсится как record-литерал /// (используется в head-позициях `if`/`while`/`match`-scrutinee /// и `for`-итераторах, чтобы `{` следующего блока не съедался). no_struct_lit: bool, /// Когда true — `expr(args) { ... }` не парсится как call-with- /// trailing-block. Используется в head-позиции `match`-scrutinee, /// чтобы `match foo() { Some(i) => ... }` не рассматривался как /// `foo()` с trailing-block'ом. no_trailing_block: bool, /// Оригинальный текст для обратной выборки (используется в /// `.n.m`-positional-tuple-access, где Float-токен нужно /// расщепить обратно в две части). src: String, /// **Plan 118.5 / D216 V2 §V2.6 (2026-06-04):** parser-emitted lint /// warnings collected during parsing. Drained by the driver via /// `into_warnings()` and merged with post-parse lint_module() output. /// (Plan 138.5: the legacy pointer-syntax grace-period warnings were /// dropped — prefix pointer modifiers are now hard errors.) pub warnings: Vec<crate::diag::Diagnostic>, /// **Plan 138.5 (2026-06-11):** set true by the `*` (Star) arm of /// `parse_type` immediately before it parses its pointee, then captured /// (and cleared) at the very start of the recursive `parse_type` call. /// When a `ro`/`mut`/`unsafe` modifier arm runs with this captured flag /// set, the modifier is the POSTFIX pointee modifier of an enclosing `*` /// (`*mut T`, `*ro *mut T`, …) → allowed even if it wraps a pointer. /// When the flag is clear, the modifier is a PREFIX modifier in plain /// type position (`mut * T`) → forbidden if it wraps a pointer /// (`E_POINTER_PREFIX_MODIFIER`). pointee_ctx: bool, /// [M-serde-slice-generic-method-parse] (2026-07-07): set true by /// `parse_fn`'s slice-receiver arm (`[]T`, `[][]T`, …) immediately before /// parsing the element type, then captured (and cleared) at the very /// start of the recursive `parse_type` call — same single-consumption /// pattern as `pointee_ctx`, but propagated through the `[]`-nesting /// (Array/FixedArray arms re-arm it before their own recursive /// `parse_type()` call) so it survives down to the innermost element /// identifier at any slice depth. When set, the `Ident` arm's /// dotted-path continuation (`path.push` loop) is skipped: in receiver /// position a `.` immediately after the element type starts the /// static-method-name suffix (`[]T.deserialize[D ...]`, D42 static /// receiver), never a qualified sub-path of the element type itself. /// Without this, `parse_type` greedily folded `T.deserialize` into one /// dotted path and then tried to parse the method's OWN generic-decl /// list (`[D Deserializer]`) as generic type-ARGS (no bounds allowed), /// failing on the bound identifier. receiver_elem_ctx: bool, /// Plan 153.5 (D263) / [M-153.5-flatten-nested-receiver]: the structured /// carrier slot TypeRefs collected by the most recent CARRIER-mode /// `parse_generic_decl_params_inner(true)` call. For `Vec[Vec[T]]` this is /// `[Named{Vec,[Named T]}]`; for the flat `Vec[T]` it is `[Named T]`. Read /// immediately afterwards in `parse_fn` to build the structured receiver /// type (`Receiver.receiver_ty`); never persists across fn declarations. last_carrier_slot_types: Vec<TypeRef>, } /// **Plan 138.5 / D216 V2/V3 simplification (2026-06-11):** build the /// `E_POINTER_PREFIX_MODIFIER` diagnostic emitted when a `ro`/`mut`/`unsafe` /// token appears in type position immediately before `*` (e.g. `mut * T`, /// `ro * T`, `unsafe * T`). Pointer modifiers live on the pointee (postfix /// `*mut T` / `*ro T` / `*unsafe T`) or on the binding (`mut x *T`); prefix /// forms are not allowed. Extends the `E_INVALID_POINTER_MODIFIER` family /// (D216 §1). `modifier` is the offending leading keyword spelling. fn pointer_prefix_modifier_error(modifier: &str, span: Span) -> Diagnostic { Diagnostic::new( format!( "[E_POINTER_PREFIX_MODIFIER] `{m} *` is not allowed — pointer \ modifiers go on the pointee (after `*`: `*mut T` / `*ro T` / \ `*unsafe T`) or on the binding (`mut x *T`); a modifier before \ `*` is forbidden (Plan 138.5 / D216 §1). \ Migrate: `{m} * T` → `*{m} T` (modifier describes the pointee), \ and pointer reassignability is expressed by the binding \ (`let p *T` = fixed, `mut p *T` = rebindable, D36). Nullable \ pointers use `Option[*T]` only (NPO).", m = modifier, ), span, ) } /// **§10a rename (Plan 174.5, 2026-07-11):** build the /// `E_UNSAFE_TYPE_MODIFIER_RENAMED` diagnostic emitted when the `unsafe` /// keyword appears as a possibly-uninit type-modifier (`unsafe T` value- /// wrapper or `*unsafe T` pointer, T not `Func`). The type-modifier was /// renamed to `uninit` to decouple «possibly-uninit pointee/value» from /// «unsafe operation» — `unsafe { }` blocks and `unsafe fn` / /// `external unsafe fn` declaration attributes are UNCHANGED, as is the /// legacy fn-pointer composition `*unsafe fn(...)` / `*extern "C" unsafe /// fn(...)` (D216 §10 — encodes call-requires-unsafe, not possibly-uninit /// data, so it keeps the `unsafe` spelling). fn unsafe_type_modifier_renamed_error(span: Span) -> Diagnostic { Diagnostic::new( "[E_UNSAFE_TYPE_MODIFIER_RENAMED] the possibly-uninit type-modifier \ `unsafe` is renamed to `uninit` (§10a rename, Plan 174.5 / D216 \ amend, 2026-07-11) — use `uninit T` (value-wrapper) instead of \ `unsafe T`, and `*uninit T` (pointer to possibly-uninit T) instead \ of `*unsafe T`. This does NOT affect `unsafe { ... }` blocks or \ `unsafe fn` / `external unsafe fn` declaration attributes — those \ keep the `unsafe` spelling. The fn-pointer-type composition \ `*unsafe fn(...)` / `*extern \"C\" unsafe fn(...)` (D216 §10) is \ ALSO unchanged (it encodes «call requires unsafe», not \ possibly-uninit data).".to_string(), span, ) } /// **Plan 147 Ф.2 (D246, 3-axis L3):** `*ro T` is redundant — a bare `*T` /// IS the ro-pointee canon (`*T ≡ *ro T` universally). Emit a hard error with /// a fix-it to `*T`. Distinct from `*mut T` (the single mut-pointee opt-in, /// still valid) and from the value-level `ro T` content-view (L2, valid before /// a value/record type, not in pointee position). fn redundant_pointer_ro_error(span: Span) -> Diagnostic { Diagnostic::new( "[E_REDUNDANT_POINTER_RO] `*ro T` is redundant — a bare `*T` is \ already a readonly pointer (the L3 pointee default is `ro` per \ Plan 147 / D246: `*T ≡ *ro T` universally). \ Fix: write `*T` (drop the `ro`). Writable pointee is the single \ opt-in `*mut T`; pointer reassignability is a binding concern \ (`let p *T` fixed vs `mut p *T` rebindable, D36), not a type modifier." .to_string(), span, ) } /// **[M-redundant-param-ro-diagnostic] (Plan 172.13 batch 4, D246 amendment):** /// an explicit `ro` in PARAMETER position is redundant — parameters are the /// ro-view by default (D176 amend / D246 P-rows). Mirror of /// `E_REDUNDANT_POINTER_RO` (same redundancy principle, L1/L2 axis instead of /// L3). Covers BOTH spellings: the prefix form `(ro x T)` and the /// type-modifier form `x ro T`. The V3-amend combination `ro x mut T` /// (ro binding + explicit mut content-view) stays legal — only the bare /// redundant `ro` is rejected. fn redundant_param_ro_error(span: Span) -> Diagnostic { Diagnostic::new( "[E_REDUNDANT_PARAM_RO] explicit `ro` on a parameter is redundant — \ parameters are readonly by default (D176 / D246: a bare `x T` param \ IS the ro view). Fix: drop the `ro` (`x T`). Mutable access is the \ single opt-in `mut x T`; ownership transfer is `consume x T`." .to_string(), span, ) } /// **[M-redundant-param-ro-diagnostic] (Plan 172.13 batch 4, D246 amendment):** /// an explicit `mut` in RETURN position is redundant/meaningless — a returned /// value is OWNED by the caller (its mutability is decided by the caller's /// binding: `mut x = f()` vs `ro x = f()`), so `-> mut T` promises nothing. /// Same redundancy class as `E_REDUNDANT_POINTER_RO`/`E_REDUNDANT_PARAM_RO`. /// Distinct from `-> ro T` (oracle row D — a MEANINGFUL readonly-view return) /// and from `-> *mut T` (L3 pointee capability on a returned pointer — legal). fn redundant_return_mut_error(span: Span) -> Diagnostic { Diagnostic::new( "[E_REDUNDANT_RETURN_MUT] explicit `mut` on a return type is \ redundant — the returned value is owned by the caller, whose binding \ decides mutability (`mut x = f()`). Fix: drop the `mut` (`-> T`). \ (A returned POINTER's pointee capability is spelled on the pointer \ itself: `-> *mut T` — that form is unaffected.)" .to_string(), span, ) } /// №301 (221.1, owner decision 2026-08-03): the postfix return form /// `-> T consume` is RETRACTED. Canon is the prefix form `-> consume T`, /// matching `-> ro T` / `-> mut T` — the modifier ALWAYS precedes what it /// describes (a binding name, a receiver, a return type, a type body). /// There is no postfix modifier position left anywhere in the grammar. fn postfix_return_consume_retracted_error(span: Span) -> Diagnostic { Diagnostic::new( "[E_RETURN_CONSUME_POSTFIX_RETRACTED] postfix `-> T consume` is \ retracted (№301, 2026-08-03 owner decision) — the canon is the \ prefix form `-> consume T`, symmetric with `-> ro T` / `-> mut T`. \ The modifier always stands before what it describes. Fix: move \ `consume` before the type (`-> consume T`)." .to_string(), span, ) } /// **Plan 150 / D248:** comparison operators cannot be chained (`a < b < c`, /// `0 <= i < n`, `a == b == c`). Nova does NOT support Python-style chaining; /// the canonical range form is `a OP1 b && b OP2 c`. Hard error with a fix-it /// (matches Rust). This rejects the vacuous-truth footgun where `0 <= i < n` /// parses as `(0 <= i) < n` = `bool < n` (always true for `n > 1`), silently /// neutralizing `requires 0 <= i < @len` bounds contracts. Equality chains /// (`a == b == c`) are rejected for the same reason. The detection is /// paren-aware: `(a < b) < c` parses the inner `<` inside the parentheses, so /// the outer loop only sees ONE operator and is NOT flagged — explicit /// parenthesization is an intentional `bool`-comparison and stays legal. fn chained_comparison_error(span: Span) -> Diagnostic { Diagnostic::new( "[E_CMP_CHAIN_UNSUPPORTED] comparison operators cannot be chained \ (e.g. `a < b < c` or `0 <= i < n`). Nova does not support Python-style \ chained comparison: `a < b < c` would otherwise parse as `(a < b) < c`, \ comparing a `bool` against the third operand — a silent vacuous-truth \ bug. Fix: split into `a OP1 b && b OP2 c` (e.g. `0 <= i && i < n`). \ If you really mean to compare a comparison result, parenthesize it \ explicitly (`(a < b) == c`)." .to_string(), span, ) } impl Parser { pub fn new(tokens: Vec<Token>) -> Self { Self::with_src(tokens, String::new()) } pub fn with_src(tokens: Vec<Token>, src: String) -> Self { Self { tokens, pos: 0, no_struct_lit: false, no_trailing_block: false, src, warnings: Vec::new(), pointee_ctx: false, receiver_elem_ctx: false, last_carrier_slot_types: Vec::new(), } } /// **Plan 118.5 / D216 V2 §V2.6 (2026-06-04):** consume the parser /// and return collected warnings. Driver merges these with post-parse /// lint_module() warnings. pub fn into_warnings(self) -> Vec<crate::diag::Diagnostic> { self.warnings } fn src_substring(&self, span: Span) -> String { if self.src.is_empty() { // Парсер был создан без src — fallback к синтезу из Float return String::new(); } self.src .get(span.start..span.end) .map(|s| s.to_string()) .unwrap_or_default() } /// Disable struct-literal parsing внутри `f` (ambiguity guard для /// `if x { }` / `while x { }` / etc). Сейчас current parser использует /// более точный `with_no_struct_or_trailing` (см. ниже); этот helper /// сохранён как minimal API для случаев когда нужен только struct-lit /// guard без trailing-block. #[allow(dead_code)] fn with_no_struct_lit<R>( &mut self, f: impl FnOnce(&mut Self) -> Result<R, Diagnostic>, ) -> Result<R, Diagnostic> { let saved = self.no_struct_lit; self.no_struct_lit = true; let result = f(self); self.no_struct_lit = saved; result } /// Аналогично, но также блокирует trailing-block-attachment к call'у. /// Используется в match-scrutinee позиции. fn with_no_struct_or_trailing<R>( &mut self, f: impl FnOnce(&mut Self) -> Result<R, Diagnostic>, ) -> Result<R, Diagnostic> { let saved_struct = self.no_struct_lit; let saved_trailing = self.no_trailing_block; self.no_struct_lit = true; self.no_trailing_block = true; let result = f(self); self.no_struct_lit = saved_struct; self.no_trailing_block = saved_trailing; result } /// Точка входа: парсит модуль (файл целиком). pub fn parse_module(&mut self) -> Result<Module, Diagnostic> { self.skip_newlines(); let start = self.peek().span; // Plan 45 Ф.2 / D104: `//!` Inner doc-comments — собираются как // module-level. Спецификация: они валидны только в начале файла // (после `module X` и imports, до первой декларации), но для // robustness'а парсер собирает Inner-токены отовсюду на module- // уровне (вне function-bodies), сливая в `module_doc`. let mut module_doc: Option<crate::ast::DocBlock> = self.consume_doc_block_of_kind(crate::lexer::DocCommentKind::Inner); // Plan 42.16 Ф.2: module-level атрибуты (`#forbid` / `#cfg` / // `#doc`) идут **ПЕРЕД** `module` declaration — консистентно с // item-level атрибутами (`#cfg`/`#realtime`/`#pure` перед `fn`). // `#cfg` семантически — гейт «существует ли файл», логично // читать до `module`. AI-first: условия файла видны первыми. let module_attrs = self.parse_module_attrs()?; // Plan 45 Ф.22.1 / D105: module-level doc-attrs (`#stable`/etc.) // — могут идти между classic module attrs (`#cfg`/`#forbid`) и // `module` declaration. Используем тот же parse_doc_attrs() что // и для items. let module_doc_attrs = self.parse_doc_attrs()?; // module keyword.path // // D174 (Plan 107): inline clause-syntax `module X no_prelude` / // `module X partial_prelude(...)` / `module X allow_prelude_shadow` // УДАЛЕНЫ — hard compile error с migration hint (D174). // Новые формы: `#no_prelude` / `#prelude(...)` / `#allow(shadow)` // идут ПЕРЕД `module` declaration (parse_module_attrs выше). // clause_attrs всегда пустой (inline-формы возвращают hard error). let clause_attrs: Vec<ModuleAttr> = Vec::new(); let module_name = if self.eat(&TokenKind::KwModule).is_some() { let path = self.parse_dotted_path()?; // D174: inline clause loop — all known identifiers return hard error. // Unknown identifiers → break (not a clause, end of module line). loop { let clause_start = self.peek().span; let clause_name = if let TokenKind::Ident(n) = &self.peek().kind { n.clone() } else { break; }; match clause_name.as_str() { "no_prelude" => { return Err(Diagnostic::new( "inline `no_prelude` clause removed (D174, Plan 107): \ move to `#no_prelude` before `module` declaration\n \ change: module <path> no_prelude\n \ to: #no_prelude\n \ · module <path>", clause_start)); } "partial_prelude" => { self.bump(); // consume partial_prelude ident so span is accurate // skip optional (...) to avoid cascade errors if matches!(self.peek().kind, TokenKind::LParen) { self.bump(); // ( let mut depth = 1usize; while depth > 0 { match self.peek().kind { TokenKind::LParen => { depth += 1; self.bump(); } TokenKind::RParen => { depth -= 1; self.bump(); } TokenKind::Newline | TokenKind::Eof => break, _ => { self.bump(); } } } } return Err(Diagnostic::new( "inline `partial_prelude(...)` clause removed (D174, Plan 107): \ move to `#prelude(...)` before `module` declaration\n \ change: module <path> partial_prelude(core, runtime)\n \ to: #prelude(core, runtime)\n \ · module <path>", clause_start)); } "allow_prelude_shadow" => { return Err(Diagnostic::new( "inline `allow_prelude_shadow` clause removed (D174, Plan 107): \ move to `#allow(shadow)` before `module` declaration\n \ change: module <path> allow_prelude_shadow\n \ to: #allow(shadow)\n \ · module <path>", clause_start)); } _ => break, } } self.expect_newline_or_eof()?; path } else { Vec::new() }; let mut imports = Vec::new(); let mut items = Vec::new(); loop { self.skip_newlines(); // Plan 45 Ф.2 / D104: на каждой итерации loop'а проверяем, // не появился ли Inner doc-comment (`//!`) — допустимо // между imports и first item; merge'им в `module_doc`. if let Some(extra_inner) = self .consume_doc_block_of_kind(crate::lexer::DocCommentKind::Inner) { module_doc = Some(match module_doc.take() { None => extra_inner, Some(prev) => crate::ast::DocBlock { kind: crate::lexer::DocCommentKind::Inner, content: format!("{}\n\n{}", prev.content, extra_inner.content), span: prev.span.merge(extra_inner.span), }, }); self.skip_newlines(); } if matches!(self.peek().kind, TokenKind::Eof) { break; } // Plan 42.17 Ф.5: `#forbid` / `#doc` — module-level атрибуты, // валидны ТОЛЬКО перед `module` declaration. После — чёткая // ошибка (раньше падало в parse_item с misleading «expected // fn/type/...»). `#cfg` — исключение: легитимен как item-level // атрибут перед fn/type/const, его здесь не трогаем. if matches!(self.peek().kind, TokenKind::Hash) { let next_kind = self.tokens.get(self.pos + 1).map(|t| &t.kind); let is_forbid = matches!(next_kind, Some(TokenKind::KwForbid)); // `#doc` неоднозначен: D101 `#doc "string"` (module-attr) vs // D105 `#doc(summary=...)` / `#doc(inline)` (item-attr). // Disambig по третьему токену: string → module-attr; // `(` → item-attr (передаём в parse_item). let is_doc_module_attr = if let Some(TokenKind::Ident(n)) = next_kind { if n == "doc" { let third = self.tokens.get(self.pos + 2).map(|t| &t.kind); matches!(third, Some(TokenKind::Str(_))) } else { false } } else { false }; if is_forbid || is_doc_module_attr { let attr = if is_forbid { "#forbid" } else { "#doc" }; return Err(Diagnostic::new( format!( "`{attr}` is a module-level attribute — it must \ precede the `module` declaration, not follow it"), self.peek().span)); } } // Plan 45 Ф.24.11: `#doc_inline` / `#doc_no_inline` before import/re-export. // Collect doc-attrs that apply to an immediately-following import statement. let import_doc_attrs = if matches!(self.peek().kind, TokenKind::Hash) { let next_kind = self.tokens.get(self.pos + 1).map(|t| &t.kind); let is_import_doc_attr = matches!(next_kind, Some(TokenKind::Ident(n)) if n == "doc_inline" || n == "no_inline" || n == "doc_no_inline"); let is_doc_paren = if let Some(TokenKind::Ident(n)) = next_kind { if n == "doc" { let third = self.tokens.get(self.pos + 2).map(|t| &t.kind); matches!(third, Some(TokenKind::LParen)) } else { false } } else { false }; if is_import_doc_attr || is_doc_paren { let attrs = self.parse_doc_attrs()?; self.skip_newlines(); attrs } else { Vec::new() } } else { Vec::new() }; // Plan 239 (D443): `use` — контекстный import-synonym (был // `KwUse`). Lookahead-1: `use` считается import-head только // если следующий токен похож на начало пути (`Ident` или `.`/ // `..` relative-anchor) — тот же приём, что и у `bench` // (peek-1 disambiguation), чтобы не съедать `use` в позиции, // где он в будущем мог бы значить что-то ещё на top-level. let is_use_import_head = |p: &Self| -> bool { matches!(p.peek().kind, TokenKind::Ident(ref s) if s == "use") && matches!(p.peek_at(1).kind, TokenKind::Ident(_) | TokenKind::Dot | TokenKind::DotDot) }; if matches!(self.peek().kind, TokenKind::KwImport) || is_use_import_head(self) { imports.push(self.parse_import_with_attrs(import_doc_attrs)?); continue; } // Plan 35 sub-plan 35.A (R26): `export import X` re-export // (D29). Lookahead-1 чтобы не съесть `export` для других items // (export fn, export type, etc.). Только `export import|use` — // дополнительный путь для парсинга import'а. if matches!(self.peek().kind, TokenKind::KwExport) && self.pos + 1 < self.tokens.len() && (matches!(self.tokens[self.pos + 1].kind, TokenKind::KwImport) || (matches!(self.tokens[self.pos + 1].kind, TokenKind::Ident(ref s) if s == "use") && self.pos + 2 < self.tokens.len() && matches!( self.tokens[self.pos + 2].kind, TokenKind::Ident(_) | TokenKind::Dot | TokenKind::DotDot ))) { imports.push(self.parse_import_with_attrs(import_doc_attrs)?); continue; } // If doc_attrs were collected but no import follows, they belong to parse_item. // parse_item will re-read them from source — but we've consumed them here. // This is fine: if #doc_inline is before a non-import, it's an error in parse_doc_attrs // (unknown attr). Drop them silently (parse_item will re-encounter the next token). // Plan 42.14 Ф.2: parse_item возвращает Option — None если // item gated `#cfg(...)` predicate'ом который inactive для // current target/features → item дропается на parse-этапе. if let Some(item) = self.parse_item()? { items.push(item); } } let span = start.merge(self.peek().span); // Plan 42 Sub-plan 42.4: Module.peer_files оставляем пустым на // parser уровне — parser не знает path к исходнику. Caller'ы // (imports.rs::resolve_imports_inline / test_runner / cmd_check) // заполняют `peer_files` после parse. // // D174 (Plan 107): clause_attrs always empty (inline forms hard-error). // Kept for structural compat. module_attrs already has #no_prelude etc. let mut all_attrs = module_attrs; all_attrs.extend(clause_attrs); Ok(Module { name: module_name, imports, items, attrs: all_attrs, doc_attrs: module_doc_attrs, span, peer_files: Vec::new(), doc: module_doc, rebind_shadows: std::collections::HashMap::new(), consume_reuse_spans: std::collections::HashSet::new(), }) } // ─── helpers ───────────────────────────────────────────────────────── fn peek(&self) -> &Token { &self.tokens[self.pos] } fn peek_at(&self, offset: usize) -> &Token { if self.pos + offset >= self.tokens.len() { self.tokens.last().unwrap() } else { &self.tokens[self.pos + offset] } } fn bump(&mut self) -> Token { let t = self.tokens[self.pos].clone(); if self.pos + 1 < self.tokens.len() { self.pos += 1; } t } fn eat(&mut self, kind: &TokenKind) -> Option<Token> { if std::mem::discriminant(&self.peek().kind) == std::mem::discriminant(kind) { Some(self.bump()) } else { None } } fn expect(&mut self, kind: &TokenKind) -> Result<Token, Diagnostic> { if std::mem::discriminant(&self.peek().kind) == std::mem::discriminant(kind) { Ok(self.bump()) } else { let span = self.peek().span; let actual = self.peek().kind.name(); Err(Diagnostic::new( format!("expected {}, got {}", kind.name(), actual), span, )) } } fn skip_newlines(&mut self) { while matches!(self.peek().kind, TokenKind::Newline | TokenKind::Semicolon) { self.bump(); } } /// Consumes only real `Newline` tokens (not `;`). Used where a `;` /// must be caught as an error rather than silently swallowed — e.g. /// between `match` arms (D452, Plan 264: `;` there is retracted). fn skip_plain_newlines(&mut self) { while matches!(self.peek().kind, TokenKind::Newline) { self.bump(); } } /// D452 (Plan 264) — statements are a *sequence* ("then"): separated /// by a real newline when multi-line, or by `;` when several sit on /// one line. Call right after a statement has been parsed (and any of /// its own trailing tokens consumed) to verify a separator actually /// stood between it and whatever follows. Two statements glued by /// nothing but whitespace on the same line is a retracted form: it /// used to parse silently (`{ a = 1 b = 2 }`) even though `syntax.md` /// already required `;` there — see plan 264 §2. fn expect_stmt_separator(&mut self) -> Result<(), Diagnostic> { match self.peek().kind { TokenKind::Newline | TokenKind::Semicolon => { self.skip_newlines(); Ok(()) } TokenKind::RBrace | TokenKind::Eof => Ok(()), _ => { let next_span = self.peek().span; let insert_span = Span { start: next_span.start, end: next_span.start, file_id: next_span.file_id }; Err(Diagnostic::new( "[E_STMT_SEP_MISSING] two statements on the same line must be \ separated by `;` (D452, Plan 264) — nothing but whitespace \ follows the previous statement here; this reads as one \ statement running into the next. Add `;` before this one, \ or put it on its own line." .to_string(), next_span, ) .with_suggestion(crate::diag::Suggestion { message: "insert `;` before this statement".to_string(), span: insert_span, replacement: "; ".to_string(), applicability: crate::diag::Applicability::MachineApplicable, })) } } } /// D452 (Plan 264) — separator between `match` arms depends on /// whether the next arm sits on the same line or a new one: arms are /// *alternatives* ("or"), a newline alone separates them when /// multi-line, `,` is required (and only meaningful) when several /// arms share one line. `;` between arms is always rejected (it /// promises a sequence where the arms are mutually exclusive [INV-PROPERTY] /// [INV-PROPERTY]: нарушение невыразимо — парсер отвергает форму /// диагностикой E_MATCH_ARM_SEMICOLON), and so /// is `,` immediately before a newline/`}` — that is the retracted /// "comma in multiline arms, including trailing" form. Call right /// after an arm has been fully parsed; on `Ok`, the loop is /// positioned to either see `}` or parse the next arm's pattern. fn expect_match_arm_separator(&mut self) -> Result<(), Diagnostic> { match self.peek().kind { TokenKind::Comma => { let comma_span = self.peek().span; self.bump(); if matches!(self.peek().kind, TokenKind::Newline | TokenKind::RBrace) { return Err(Diagnostic::new( "[E_MATCH_ARM_COMMA_MULTILINE] `,` right before a newline \ (or the closing `}`) after a match arm is not allowed \ (D452, Plan 264) — multi-line arms are separated by the \ newline alone, a trailing `,` included; `,` is only for \ several arms written on ONE line." .to_string(), comma_span, ) .with_suggestion(crate::diag::Suggestion { message: "remove the `,`".to_string(), span: comma_span, replacement: String::new(), applicability: crate::diag::Applicability::MachineApplicable, })); } Ok(()) } TokenKind::Semicolon => { let semi_span = self.peek().span; Err(Diagnostic::new( "[E_MATCH_ARM_SEMICOLON] `;` between match arms is not \ allowed (D452, Plan 264) — exactly one arm runs, \ never a sequence, and `;` promises a sequence. Use \ `,` for several arms on one line, or put each arm on its \ own line (no separator needed then)." .to_string(), semi_span, ) .with_suggestion(crate::diag::Suggestion { message: "use `,` instead of `;`".to_string(), span: semi_span, replacement: ",".to_string(), applicability: crate::diag::Applicability::MachineApplicable, })) } TokenKind::Newline => { self.skip_plain_newlines(); Ok(()) } TokenKind::RBrace => Ok(()), _ => { let next_span = self.peek().span; let insert_span = Span { start: next_span.start, end: next_span.start, file_id: next_span.file_id }; Err(Diagnostic::new( "[E_MATCH_ARM_SEP_MISSING] match arms on the same line must \ be separated by `,` (D452, Plan 264) — this arm directly \ follows the previous one with only whitespace between \ them, which reads as one arm running into the next. Add \ `,` here, or put this arm on its own line (no separator \ needed then)." .to_string(), next_span, ) .with_suggestion(crate::diag::Suggestion { message: "insert `,` before this arm".to_string(), span: insert_span, replacement: ", ".to_string(), applicability: crate::diag::Applicability::MachineApplicable, })) } } } /// Plan 45 Ф.2 / D104: консумит подряд идущие `DocComment`-токены /// заданного `kind`'а (пропуская newline/semicolon между ними) и /// склеивает их content в один `DocBlock`. Возвращает `None`, если /// ни одного doc-токена заданного kind'а на текущей позиции нет. /// /// Несколько подряд идущих doc-блоков того же kind'а (после /// blank-line) объединяются: лексер уже сливает строки в один токен, /// но parser может встретить два таких токена, разделённых newline'ом. /// Этот метод объединяет их через `\n\n` (markdown paragraph break). fn consume_doc_block_of_kind( &mut self, kind: crate::lexer::DocCommentKind, ) -> Option<crate::ast::DocBlock> { // Сохраним стартовую позицию — чтобы можно было откатиться, // если ничего не нашли. let start_pos = self.pos; // Пропускаем ведущие newline (они между предыдущим item'ом и // следующим doc-блоком). while matches!( self.peek().kind, TokenKind::Newline | TokenKind::Semicolon ) { self.bump(); } let mut accumulated: Option<crate::ast::DocBlock> = None; loop { match &self.peek().kind { TokenKind::DocComment { kind: tok_kind, content, } if *tok_kind == kind => { let content = content.clone(); let span = self.peek().span; self.bump(); accumulated = Some(match accumulated { None => crate::ast::DocBlock { kind, content, span, }, Some(prev) => crate::ast::DocBlock { kind, content: format!("{}\n\n{}", prev.content, content), span: prev.span.merge(span), }, }); // Между doc-блоками допускаем blank-line. while matches!( self.peek().kind, TokenKind::Newline | TokenKind::Semicolon ) { self.bump(); } } _ => break, } } if accumulated.is_none() { // Ничего не нашли — откатимся, чтобы caller'ские // `skip_newlines` отработали как раньше. self.pos = start_pos; } accumulated } /// Plan 45 Ф.2: helper для случаев, когда doc-token попался в /// неожиданной позиции (например, внутри тела функции). Тихо /// съедает их, чтобы не валить парсинг. Lint в Ф.3 даст warning /// «orphan doc-comment». /// /// **Reserved**: текущий parser обрабатывает orphan doc-comments /// inline через `match self.peek().kind { DocComment => ... }` /// без отдельного helper-prologue. Helper сохранён как мечтаемая /// точка консолидации этой логики (Plan 45 Ф.3 lint integration). #[allow(dead_code)] fn skip_stray_doc_comments(&mut self) { while matches!(self.peek().kind, TokenKind::DocComment { .. }) { self.bump(); } } fn at_newline(&self) -> bool { matches!( self.peek().kind, TokenKind::Newline | TokenKind::Semicolon | TokenKind::Eof ) } fn expect_newline_or_eof(&mut self) -> Result<(), Diagnostic> { match self.peek().kind { TokenKind::Newline | TokenKind::Semicolon => { self.bump(); Ok(()) } TokenKind::Eof => Ok(()), _ => { let span = self.peek().span; Err(Diagnostic::new( format!( "expected newline or end of input, got {}", self.peek().kind.name() ), span, )) } } } fn parse_ident(&mut self) -> Result<(String, Span), Diagnostic> { let span = self.peek().span; match &self.peek().kind { TokenKind::Ident(s) => { let name = s.clone(); self.bump(); Ok((name, span)) } other => Err(Diagnostic::new( format!("expected identifier, got {}", other.name()), span, )), } } fn parse_dotted_path(&mut self) -> Result<Vec<String>, Diagnostic> { let mut parts = Vec::new(); let (first, _) = self.parse_ident()?; parts.push(first); // Plan 35 sub-plan 35.A (R26): stop on `.{` — selective items follow. while matches!(self.peek().kind, TokenKind::Dot) && self.pos + 1 < self.tokens.len() && !matches!(self.tokens[self.pos + 1].kind, TokenKind::LBrace) { self.bump(); // . let (next, _) = self.parse_ident()?; parts.push(next); } Ok(parts) } /// Plan 84: распознаёт относительный префикс импорта. /// `./` (= `Dot Slash`) — директория импортирующего файла (`up == 0`); /// `../`×n (= `DotDot Slash` ×n) — n уровней вверх. Без префикса — /// `ImportAnchor::Package` (абсолютный путь от корня пакета). fn parse_import_anchor(&mut self) -> Result<crate::ast::ImportAnchor, Diagnostic> { use crate::ast::ImportAnchor; let peek2_slash = |p: &Self| -> bool { p.pos + 1 < p.tokens.len() && matches!(p.tokens[p.pos + 1].kind, TokenKind::Slash) }; // `./` — директория текущего файла. if matches!(self.peek().kind, TokenKind::Dot) && peek2_slash(self) { self.bump(); // . self.bump(); // / if matches!(self.peek().kind, TokenKind::DotDot) && peek2_slash(self) { return Err(Diagnostic::new( "после `./` не может идти `../` — пишите `../` напрямую", self.peek().span, )); } return Ok(ImportAnchor::Relative { up: 0 }); } // `../`+ — n уровней вверх. if matches!(self.peek().kind, TokenKind::DotDot) && peek2_slash(self) { let mut up: u32 = 0; while matches!(self.peek().kind, TokenKind::DotDot) && peek2_slash(self) { self.bump(); // .. self.bump(); // / up += 1; } return Ok(ImportAnchor::Relative { up }); } Ok(ImportAnchor::Package) } // ─── module-level attributes ───────────────────────────────────────── /// Plan 42.16 Ф.2: парсит module-level атрибуты ПЕРЕД `module` /// declaration. `#forbid X, Y` / `#cfg(<expr>)` / `#doc "..."`. /// /// **Decision 2026-05-13:** `#requires` отвергнуто — implicit /// effects в function signatures противоречат Nova AI-first /// explicit principle (D62). `#forbid` оставлен как security boundary. fn parse_module_attrs(&mut self) -> Result<Vec<ModuleAttr>, Diagnostic> { let mut module_attrs = Vec::new(); loop { self.skip_newlines(); if !matches!(self.peek().kind, TokenKind::Hash) { break; } let next_kind = self.tokens.get(self.pos + 1).map(|t| t.kind.clone()); // `forbid` is keyword (KwForbid) in Nova; `cfg` и `doc` — обычные idents. let is_forbid = matches!(next_kind, Some(TokenKind::KwForbid)); let is_cfg = matches!(&next_kind, Some(TokenKind::Ident(name)) if name == "cfg"); let is_doc = matches!(&next_kind, Some(TokenKind::Ident(name)) if name == "doc"); let is_must_verify_module = matches!(&next_kind, Some(TokenKind::Ident(name)) if name == "must_verify_module"); let is_proof_budget = matches!(&next_kind, Some(TokenKind::Ident(name)) if name == "proof_budget"); let is_no_prelude_attr = matches!(&next_kind, Some(TokenKind::Ident(name)) if name == "no_prelude"); let is_prelude_attr = matches!(&next_kind, Some(TokenKind::Ident(name)) if name == "prelude"); let is_allow_attr = matches!(&next_kind, Some(TokenKind::Ident(name)) if name == "allow"); if !is_forbid && !is_cfg && !is_doc && !is_must_verify_module && !is_proof_budget && !is_no_prelude_attr && !is_prelude_attr && !is_allow_attr { break; // not a module-level attribute } let attr_start = self.peek().span; self.bump(); // # if is_must_verify_module { // Plan 33.3 Ф.13: `#must_verify_module` — все функции MustVerify. self.bump(); // must_verify_module (ident) self.expect_newline_or_eof()?; let attr_end = self.tokens[self.pos.saturating_sub(1)].span; module_attrs.push(ModuleAttr { kind: ModuleAttrKind::MustVerifyModule, effects: Vec::new(), span: attr_start.merge(attr_end), }); continue; } if is_proof_budget { // Ф.3.4 (Plan 33.6): `#proof_budget(timeout_ms=N, vc_count_max=M)`. self.bump(); // proof_budget let mut timeout_ms: Option<u32> = None; let mut vc_count_max: Option<u32> = None; if matches!(self.peek().kind, TokenKind::LParen) { self.bump(); // ( loop { if matches!(self.peek().kind, TokenKind::RParen) { break; } let (key, key_span) = self.parse_ident()?; if !matches!(self.peek().kind, TokenKind::Eq) { return Err(Diagnostic::new( "`#proof_budget` key must be followed by `=`", key_span)); } self.bump(); // = let val_span = self.peek().span; let val = if let TokenKind::Int(n) = self.peek().kind { let v = n as u32; self.bump(); v } else { return Err(Diagnostic::new( "`#proof_budget` value must be integer literal", val_span)); }; match key.as_str() { "timeout_ms" => timeout_ms = Some(val), "vc_count_max" => vc_count_max = Some(val), _ => return Err(Diagnostic::new( format!("unknown `#proof_budget` key `{}`; \ expected `timeout_ms` or `vc_count_max`", key), key_span)), } if matches!(self.peek().kind, TokenKind::Comma) { self.bump(); } } if !matches!(self.peek().kind, TokenKind::RParen) { return Err(Diagnostic::new("expected `)` to close `#proof_budget(...)`", self.peek().span)); } self.bump(); // ) } self.expect_newline_or_eof()?; let attr_end = self.tokens[self.pos.saturating_sub(1)].span; module_attrs.push(ModuleAttr { kind: ModuleAttrKind::ProofBudget { timeout_ms, vc_count_max }, effects: Vec::new(), span: attr_start.merge(attr_end), }); continue; } // D174: #no_prelude — полный opt-out из prelude if is_no_prelude_attr { self.bump(); // no_prelude (ident) self.expect_newline_or_eof()?; let attr_end = self.tokens[self.pos.saturating_sub(1)].span; module_attrs.push(ModuleAttr { kind: ModuleAttrKind::NoPrelude, effects: Vec::new(), span: attr_start.merge(attr_end), }); continue; } // D174: #prelude(names…) — selective opt-in, ≥1 name if is_prelude_attr { self.bump(); // prelude (ident) if !matches!(self.peek().kind, TokenKind::LParen) { return Err(Diagnostic::new( "expected `(` after `#prelude` \ (e.g. `#prelude(core, runtime)` or use `#no_prelude` for empty)", self.peek().span)); } self.bump(); // ( // Empty `#prelude()` → explicit compile error (D174: use #no_prelude instead) if matches!(self.peek().kind, TokenKind::RParen) { return Err(Diagnostic::new( "`#prelude()` with empty list is not allowed; \ use `#no_prelude` to disable all prelude auto-imports (D174)", self.peek().span)); } let mut names: Vec<String> = Vec::new(); loop { if matches!(self.peek().kind, TokenKind::RParen) { break; } let (n, _) = self.parse_ident()?; names.push(n); if matches!(self.peek().kind, TokenKind::Comma) { self.bump(); // , } else { break; } } if !matches!(self.peek().kind, TokenKind::RParen) { return Err(Diagnostic::new( "expected `)` closing `#prelude(...)` name list", self.peek().span)); } self.bump(); // ) self.expect_newline_or_eof()?; let attr_end = self.tokens[self.pos.saturating_sub(1)].span; module_attrs.push(ModuleAttr { kind: ModuleAttrKind::PartialPrelude(names), // reuse existing variant effects: Vec::new(), span: attr_start.merge(attr_end), }); continue; } // D174: #allow(shadow) — suppress W_PRELUDE_SHADOW if is_allow_attr { self.bump(); // allow (ident) if !matches!(self.peek().kind, TokenKind::LParen) { return Err(Diagnostic::new( "expected `(` after `#allow` (e.g. `#allow(shadow)`)", self.peek().span)); } self.bump(); // ( let (allow_name, allow_span) = self.parse_ident()?; let allow_kind = match allow_name.as_str() { "shadow" => ModuleAttrKind::AllowPreludeShadow, "view_extend_detach" => ModuleAttrKind::AllowViewExtendDetach, _ => return Err(Diagnostic::new( format!("`#allow({})` is not a recognized suppressor; \ valid values: `shadow` (W_PRELUDE_SHADOW, D174), \ `view_extend_detach` (W_VIEW_EXTEND_DETACH, D141 amend)", allow_name), allow_span)), }; if !matches!(self.peek().kind, TokenKind::RParen) { return Err(Diagnostic::new( "expected `)` closing `#allow(...)`", self.peek().span)); } self.bump(); // ) self.expect_newline_or_eof()?; let attr_end = self.tokens[self.pos.saturating_sub(1)].span; module_attrs.push(ModuleAttr { kind: allow_kind, effects: Vec::new(), span: attr_start.merge(attr_end), }); continue; } if is_doc { // Plan 42.11: `#doc "..."` — module-level documentation line. self.bump(); // doc (ident) let doc_span = self.peek().span; let text = if let TokenKind::Str(s) = &self.peek().kind { let v = s.clone(); self.bump(); v } else { return Err(Diagnostic::new( "expected string literal after `#doc`", doc_span)); }; self.expect_newline_or_eof()?; let attr_end = self.tokens[self.pos.saturating_sub(1)].span; module_attrs.push(ModuleAttr { kind: ModuleAttrKind::Doc(text), effects: Vec::new(), span: attr_start.merge(attr_end), }); continue; } if is_forbid { self.bump(); // forbid let mut effects: Vec<String> = Vec::new(); loop { let (name, _) = self.parse_ident()?; effects.push(name); if matches!(self.peek().kind, TokenKind::Comma) { self.bump(); } else { break; } } self.expect_newline_or_eof()?; let attr_end = self.tokens[self.pos.saturating_sub(1)].span; module_attrs.push(ModuleAttr { kind: ModuleAttrKind::Forbid, effects, span: attr_start.merge(attr_end), }); } else { // Plan 42.12 + 42.14 + 42.16: `#cfg(<expr>)` — // feature/target_os + операторы `|| && !`. self.bump(); // cfg (ident) if !matches!(self.peek().kind, TokenKind::LParen) { return Err(Diagnostic::new("expected `(` after `#cfg`", self.peek().span)); } self.bump(); // ( let pred = self.parse_cfg_predicate()?; if !matches!(self.peek().kind, TokenKind::RParen) { return Err(Diagnostic::new( "expected `)` closing #cfg predicate", self.peek().span)); } self.bump(); // ) self.expect_newline_or_eof()?; let attr_end = self.tokens[self.pos.saturating_sub(1)].span; module_attrs.push(ModuleAttr { kind: ModuleAttrKind::Cfg(pred), effects: Vec::new(), span: attr_start.merge(attr_end), }); } } Ok(module_attrs) } // ─── #cfg predicate ────────────────────────────────────────────────── /// Plan 42.16 Ф.1: `#cfg` predicate с операторами `|| && !` /// (Go/C-style вместо функц-формы `any/all/not` из Plan 42.14). /// /// Grammar (precedence: `!` > `&&` > `||`, скобки override): /// ```text /// cfg_expr := cfg_or /// cfg_or := cfg_and ('||' cfg_and)* /// cfg_and := cfg_not ('&&' cfg_not)* /// cfg_not := '!' cfg_not | cfg_atom /// cfg_atom := '(' cfg_expr ')' | key '=' string /// ``` /// /// AST: `a || b || c` → `Any([a,b,c])`, `a && b` → `All`, `!a` → `Not` /// (имена вариантов internal — пользователь видит только операторы). /// Caller уже consumed opening `(` после `#cfg`; этот метод парсит /// один cfg_expr (не consumes closing `)` верхнего уровня). fn parse_cfg_predicate(&mut self) -> Result<CfgPredicate, Diagnostic> { self.parse_cfg_or() } /// `cfg_or := cfg_and ('||' cfg_and)*` — самый низкий приоритет. fn parse_cfg_or(&mut self) -> Result<CfgPredicate, Diagnostic> { let first = self.parse_cfg_and()?; let mut alts = vec![first]; while matches!(self.peek().kind, TokenKind::PipePipe) { self.bump(); // || alts.push(self.parse_cfg_and()?); } if alts.len() == 1 { Ok(alts.into_iter().next().unwrap()) } else { Ok(CfgPredicate::Any(alts)) } } /// `cfg_and := cfg_not ('&&' cfg_not)*` fn parse_cfg_and(&mut self) -> Result<CfgPredicate, Diagnostic> { let first = self.parse_cfg_not()?; let mut parts = vec![first]; while matches!(self.peek().kind, TokenKind::AmpAmp) { self.bump(); // && parts.push(self.parse_cfg_not()?); } if parts.len() == 1 { Ok(parts.into_iter().next().unwrap()) } else { Ok(CfgPredicate::All(parts)) } } /// `cfg_not := '!' cfg_not | cfg_atom` fn parse_cfg_not(&mut self) -> Result<CfgPredicate, Diagnostic> { if matches!(self.peek().kind, TokenKind::Bang) { self.bump(); // ! let inner = self.parse_cfg_not()?; Ok(CfgPredicate::Not(Box::new(inner))) } else { self.parse_cfg_atom() } } /// `cfg_atom := '(' cfg_expr ')' | key '=' string` fn parse_cfg_atom(&mut self) -> Result<CfgPredicate, Diagnostic> { // Скобочная группа. if matches!(self.peek().kind, TokenKind::LParen) { self.bump(); // ( let inner = self.parse_cfg_or()?; if !matches!(self.peek().kind, TokenKind::RParen) { return Err(Diagnostic::new( "expected `)` closing #cfg group", self.peek().span)); } self.bump(); // ) return Ok(inner); } // Атом: key '=' string. let start = self.peek().span; let (key, _) = self.parse_ident()?; if !matches!(self.peek().kind, TokenKind::Eq) { return Err(Diagnostic::new( format!("expected `=` after `{}` in #cfg predicate", key), self.peek().span)); } self.bump(); // = let value_span = self.peek().span; let value = if let TokenKind::Str(s) = &self.peek().kind { let v = s.clone(); self.bump(); v } else { return Err(Diagnostic::new( "expected string literal in #cfg predicate", value_span)); }; match key.as_str() { "feature" => Ok(CfgPredicate::Feature(value)), "target_os" => Ok(CfgPredicate::TargetOs(value)), other => Err(Diagnostic::new( format!("unknown #cfg key `{}` — expected `feature` or `target_os` \ (composition via `||` `&&` `!`)", other), start)), } } // ─── imports ───────────────────────────────────────────────────────── fn parse_import_with_attrs(&mut self, doc_attrs: Vec<crate::ast::DocAttr>) -> Result<Import, Diagnostic> { self.parse_import_inner(doc_attrs) } /// Public no-attr entry point — callers use /// `parse_import_with_attrs(Vec::new())` instead. Сохранён для /// symmetry с D-rule attr-aware parsing semantics. #[allow(dead_code)] fn parse_import(&mut self) -> Result<Import, Diagnostic> { self.parse_import_inner(Vec::new()) } fn parse_import_inner(&mut self, doc_attrs: Vec<crate::ast::DocAttr>) -> Result<Import, Diagnostic> { // Plan 35 sub-plan 35.A: support `import X.Y.{A, B as C}` selective // и `export import X.{A}` re-export. // Detect leading `export` keyword. Парсер уже потребил `KwExport` // в parse_item только перед fn/type/const/let — не перед import. // Здесь мы стартуем с `import` или `export import` (или `use`). let start = self.peek().span; let is_export = if matches!(self.peek().kind, TokenKind::KwExport) { self.bump(); true } else { false }; // Принимаем как `import`, так и `use` — оба парсятся идентично: // `use` будет использоваться для embedding (D39), но в bootstrap // мы не различаем. self.bump(); // Plan 84: относительный префикс `./` / `../` перед путём. let anchor = self.parse_import_anchor()?; let path = self.parse_dotted_path()?; // Optional `.{Item1, Item2 as Alias, ...}` — selective items. // Префикс — `.` (= Dot), затем `{`. parse_dotted_path остановился на // `.` перед `{`, надо его съесть. let items = if matches!(self.peek().kind, TokenKind::Dot) && self.pos + 1 < self.tokens.len() && matches!(self.tokens[self.pos + 1].kind, TokenKind::LBrace) { self.bump(); // . self.bump(); // { let mut items = Vec::new(); loop { if matches!(self.peek().kind, TokenKind::RBrace) { break; } let item_start = self.peek().span; let (name, _) = self.parse_ident()?; let alias = if matches!(self.peek().kind, TokenKind::KwAs) { self.bump(); let (a, _) = self.parse_ident()?; Some(a) } else { None }; let item_end = self.tokens[self.pos.saturating_sub(1)].span; items.push(ImportItem { name, alias, span: item_start.merge(item_end), }); if matches!(self.peek().kind, TokenKind::Comma) { self.bump(); } else { break; } } if !matches!(self.peek().kind, TokenKind::RBrace) { let span = self.peek().span; return Err(Diagnostic::new( "expected `}` to close selective-import list", span, )); } self.bump(); // } if items.is_empty() { let span = start; return Err(Diagnostic::new( "selective-import list must contain at least one item", span, )); } Some(items) } else { None }; let alias = if matches!(self.peek().kind, TokenKind::KwAs) { self.bump(); let (name, _) = self.parse_ident()?; Some(name) } else { None }; // Mutual exclusivity: нельзя одновременно selective items и alias. if items.is_some() && alias.is_some() { return Err(Diagnostic::new( "cannot combine `import X.{A, B}` with `as` alias — use \ alias per-item: `import X.{A as Aliased, B}`", start, )); } self.expect_newline_or_eof()?; let span = start.merge(self.tokens[self.pos.saturating_sub(1)].span); Ok(Import { path, items, alias, is_export, span, doc_attrs, anchor }) } // ─── top-level items ───────────────────────────────────────────────── fn parse_item(&mut self) -> Result<Option<Item>, Diagnostic> { // Plan 45 Ф.2 / D104: outer doc-comment (`///`) перед декларацией // — собираем; передаём дальше через `pending_doc`. Если за doc'ом // следует `let` / `test` (которые doc-поля не имеют) — doc // отбрасывается (lint warning в Ф.3). let pending_doc = self.consume_doc_block_of_kind(crate::lexer::DocCommentKind::Outer); // Plan 42.14 Ф.2: item-level `#cfg(...)`. Парсится ПЕРЕД // export/external/realtime/contract attrs. Если predicate inactive // для current target/features — item пропускается (return None), // но всё равно полностью парсится (для корректного advance токенов). let item_cfg: Option<CfgPredicate> = if matches!(self.peek().kind, TokenKind::Hash) && matches!( self.tokens.get(self.pos + 1).map(|t| &t.kind), Some(TokenKind::Ident(name)) if name == "cfg" ) { self.bump(); // # self.bump(); // cfg if !matches!(self.peek().kind, TokenKind::LParen) { return Err(Diagnostic::new("expected `(` after `#cfg`", self.peek().span)); } self.bump(); // ( let pred = self.parse_cfg_predicate()?; if !matches!(self.peek().kind, TokenKind::RParen) { return Err(Diagnostic::new( "expected `)` closing #cfg predicate", self.peek().span)); } self.bump(); // ) self.skip_newlines(); Some(pred) } else { None }; // Plan 45 Ф.3 / D105: doc-атрибуты `#deprecated(...)`, `#since(...)`, // `#stable[(...)]`, `#unstable(...)`, `#experimental(...)`, // `#hide_doc`, `#doc_alias(...)`, `#doc(...)`. Парсятся ПЕРЕД // export/external/realtime/contract attrs, ПОСЛЕ `#cfg`. // Несколько подряд — собираются в Vec; передаются в parse_fn / // parse_type_decl / parse_const_decl через pending_doc_attrs. // // Plan 103.6 / Plan 113: sync-class attrs (#realtime/#parks/#wakes) also // appear BEFORE `export external fn`. They may interleave with doc // attrs (e.g. `#realtime\n#stable(since="0.1")\nexport…`), so // we alternate the two parsers until neither makes progress. let mut pending_doc_attrs = self.parse_doc_attrs()?; let mut pre_sync_class: Option<crate::ast::SyncClass> = self.parse_sync_class_attr()?; loop { let pos_before = self.pos; let more_doc = self.parse_doc_attrs()?; if !more_doc.is_empty() { pending_doc_attrs.extend(more_doc); } if let Some(cls) = self.parse_sync_class_attr()? { pre_sync_class = Some(cls); } if self.pos == pos_before { break; // nothing consumed — done } } // Plan 113: parse_sync_class_attr consumes `#realtime` but not the optional // `nogc` modifier (which is an Ident, not a keyword). When the two-line form // `#realtime nogc\nfn foo()` is used, `nogc` is left in the stream after the // sync-class loop. Consume it here so it doesn't confuse subsequent parsers. let pre_sync_nogc = if matches!(pre_sync_class, Some(crate::ast::SyncClass::Realtime)) { if let TokenKind::Ident(ref n) = self.peek().kind { if n == "nogc" { self.bump(); self.skip_newlines(); true } else { false } } else { false } } else { false }; // Plan 52 Ф.1: `#from_fields` — маркер на декларации типа. // Помечает str-keyed map-тип для D55 map-coercion (`{field: v}`). // Парсится ПЕРЕД `export` (консистентно с `#cfg`) и только перед // `type`. Контекстный разбор после `#` (не keyword). let (type_attrs, impl_protocols, zero_on_move_attr, pub_to_attr, serde_attrs, no_copy_attr) = self.parse_type_attrs()?; // Plan 110.7.3.a: pre-parse #cancel_safe здесь чтобы canonical // form `#cancel_safe\nexternal fn ...` работала (attribute может // стоять перед `external`). Дополнительный pass позже подберёт // attribute если он был между `external` и `fn`. let pre_cancel_safe = self.parse_cancel_safe_attr(); // Plan 214 (D429): pre-parse `#coerce` here too — mirrors // `pre_cancel_safe` exactly. Authors write `#coerce` on its own line // BEFORE `export` (matching the `#realtime`/doc-attr convention seen // throughout std, e.g. `#realtime\nexport extern "nova" fn …`); without // this pre-parse, `#coerce` sitting before `export` would strand the // parser at the `#` token (neither `parse_type_attrs` nor the // subsequent `is_export = eat(KwExport)` consume an unrecognized // leading `#`-attribute), silently losing `is_export` AND misfiring // "`#coerce` is only valid before `fn`" once the post-export loop // finally consumes it with `export` now stuck in between. let pre_coerce = self.parse_coerce_attr(); // A-V10 (D441 §5 №167 closure): pre-parse `#thread_affine` here too — // mirrors `pre_cancel_safe`/`pre_coerce` exactly (attribute may be // written on its own line before `export`/`extern`). let pre_thread_affine = self.parse_thread_affine_attr(); // Канон владельца (2026-07-31): ВСЕ атрибуты — ПЕРЕД `export`. // Contract-attrs (#verify/#unverified/#verify_timeout/#pure/#trusted) // раньше парсились только ПОСЛЕ export — отсюда аномалия // `export #unverified` (единственная случайно-принятая форма, // найденная перебором окном 236). Теперь канонический порядок // `#unverified ⏎ export fn` — парсится здесь; пост-export форма — // честная ошибка (см. ниже). let pre_contract_attrs = self.parse_contract_attrs()?; // Plan 170 (D307): `priv(file)` top-level visibility modifier — file-private. // Parsed BEFORE `export` (mutually exclusive). Forms: // `priv(file)` → file_private = true (visible only in this file) // `priv` (bare) → error: bare top-level priv not supported (this plan) // `priv(<other>)` → error: E_PRIV_QUALIFIER (unknown qualifier) // `file` is NOT a keyword → matched as `Ident("file")` inside `priv(...)`. let mut file_private = false; if matches!(self.peek().kind, TokenKind::KwPriv) { let priv_sp = self.peek().span; self.bump(); // consume `priv` if matches!(self.peek().kind, TokenKind::LParen) { self.bump(); // consume `(` if matches!(self.peek().kind, TokenKind::Ident(ref s) if s == "file") { self.bump(); // consume `file` self.expect(&TokenKind::RParen)?; file_private = true; } else { let bad_sp = self.peek().span; return Err(Diagnostic::new( "[E_PRIV_QUALIFIER] unknown qualifier inside top-level `priv(…)`. \ The only valid top-level form is `priv(file)` (file-private). \ Omit the modifier for module-private visibility (D307, \ spec/decisions/02-types.md).".to_string(), bad_sp, )); } } else { return Err(Diagnostic::new( "[E_PRIV_QUALIFIER] bare `priv` on a top-level item is not supported; \ use `priv(file)` for file-private visibility, or omit the modifier \ for module-private (default). See D307 (spec/decisions/02-types.md).".to_string(), priv_sp, )); } } let is_export = self.eat(&TokenKind::KwExport).is_some(); // Plan 170 (D307): `priv(file)` and `export` are mutually exclusive. // Catch BOTH orders: `priv(file) export …` (file_private already set) // AND `export priv(file) …` (priv token still ahead after eating export). if file_private && is_export { let sp = self.peek().span; return Err(Diagnostic::new( "[E_PRIV_QUALIFIER] `priv(file)` and `export` are mutually exclusive — \ a symbol cannot be both file-private and exported. Pick one (D307).".to_string(), sp, )); } if is_export && matches!(self.peek().kind, TokenKind::KwPriv) { let sp = self.peek().span; return Err(Diagnostic::new( "[E_PRIV_QUALIFIER] `export` and `priv(file)` are mutually exclusive — \ a symbol cannot be both exported and file-private. Pick one (D307).".to_string(), sp, )); } // Plan 91.12 Ф.-1 (D282): `extern "nova" fn` / `extern "C" fn` — canonical FFI syntax. // `external fn` keyword retracted (E_EXTERNAL_FN_RETRACTED): use `extern "nova" fn`. // `external type X` retracted (E_EXTERNAL_TYPE_RETRACTED); types/mod.rs enforces this. let (is_external, extern_abi) = if self.eat(&TokenKind::KwExternal).is_some() { // `external type X` — let parse proceed; types/mod.rs emits E_EXTERNAL_TYPE_RETRACTED. // `external fn` — hard error here. let after_span = self.peek().span; let is_fn_next = matches!(self.peek().kind, TokenKind::KwFn) || (matches!(self.peek().kind, TokenKind::KwUnsafe) && matches!(self.peek_at(1).kind, TokenKind::KwFn)); if is_fn_next { return Err(Diagnostic::new( "[E_EXTERNAL_FN_RETRACTED] `external fn` syntax was removed (D282, Plan 91.12). \ Use `extern \"nova\" fn` for runtime-backed functions, \ or `extern \"C\" fn` for literal C symbol names.", after_span, )); } (true, None::<String>) } else if self.eat(&TokenKind::KwExtern).is_some() { let abi_span = self.peek().span; let abi = match &self.peek().kind.clone() { TokenKind::Str(s) => { let s = s.clone(); match s.as_str() { "nova" | "C" => { self.bump(); s } other => { return Err(Diagnostic::new( format!("unknown ABI `\"{}\"`; expected `\"nova\"` or `\"C\"`", other), abi_span, )); } } } _ => { return Err(Diagnostic::new( "expected ABI string `\"nova\"` or `\"C\"` after `extern`", abi_span, )); } }; (true, Some(abi)) } else { (false, None) }; // Plan 118.1.7 (D2 amend): `external unsafe fn` — `unsafe` keyword // directly before `fn` as part of the fn type. Consumed here after // `external` so `external unsafe fn foo()` sets unsafe_kw = true. let mut unsafe_kw = false; if is_external { // Allow `unsafe` keyword between `external`/`extern "ABI"` and `fn`. if matches!(self.peek().kind, TokenKind::KwUnsafe) { unsafe_kw = true; self.bump(); // unsafe } // Только `fn` либо `type` допустимы после `external`/`extern "ABI"` (+ optional `unsafe`). if !matches!(self.peek().kind, TokenKind::KwFn | TokenKind::KwType) { let span = self.peek().span; return Err(Diagnostic::new( format!( "`external`/`extern` is only valid before `fn` or `type`, got {}", self.peek().kind.name() ), span, )); } } // Plan 16 (D64 §3697) + Plan 33.1 (D-attr-syntax): // `#realtime` / `#realtime nogc` префикс перед `fn`. Атрибуты — через `#` // (а не `@`, чтобы не конфликтовать с receiver-prefix `@field`). // Парсим в RealtimeAttr enum, передаём в parse_fn. // Plan 113 (D172): `#blocking` — fn-level threadpool offload attr. // Both `#realtime` and `#blocking` may appear before `fn` in either order. // Plan 110.7.3.a: parse #cancel_safe / #realtime / #blocking в любом // порядке. Iterate пока хотя бы один pattern matches. Pre-seed // cancel_safe_attr с pre_cancel_safe (consumed выше до eat KwExternal). let mut realtime_attr = self.parse_realtime_attr()?; let mut blocking_attr = self.parse_blocking_attr(); let mut cancel_safe_attr = pre_cancel_safe || self.parse_cancel_safe_attr(); // Plan 214 (D429): `#coerce` — declares an implicit zero-cost conversion. // Parsed alongside the other leading `#`-attributes (any order, same loop). let mut coerce_attr = pre_coerce || self.parse_coerce_attr(); // A-V10 (D441 §5 №167 closure): `#thread_affine` — parsed alongside // the other leading `#`-attributes (any order, same loop), same // contextual-Ident path as `#cancel_safe`/`#coerce`. let mut thread_affine_attr = pre_thread_affine || self.parse_thread_affine_attr(); loop { let mut progressed = false; if matches!(realtime_attr, RealtimeAttr::None) { let r = self.parse_realtime_attr()?; if !matches!(r, RealtimeAttr::None) { realtime_attr = r; progressed = true; } } if !blocking_attr && self.parse_blocking_attr() { blocking_attr = true; progressed = true; } if !cancel_safe_attr && self.parse_cancel_safe_attr() { cancel_safe_attr = true; progressed = true; } if !coerce_attr && self.parse_coerce_attr() { coerce_attr = true; progressed = true; } if !thread_affine_attr && self.parse_thread_affine_attr() { thread_affine_attr = true; progressed = true; } if !progressed { break; } } if !matches!(realtime_attr, RealtimeAttr::None) && !matches!(self.peek().kind, TokenKind::KwFn) && !matches!(self.peek().kind, TokenKind::Hash) { let span = self.peek().span; return Err(Diagnostic::new( "`#realtime` is only valid before `fn`", span, )); } if blocking_attr && !matches!(self.peek().kind, TokenKind::KwFn) && !matches!(self.peek().kind, TokenKind::Hash) { let span = self.peek().span; return Err(Diagnostic::new( "`#blocking` is only valid before `fn`", span, )); } if coerce_attr && !matches!(self.peek().kind, TokenKind::KwFn) && !matches!(self.peek().kind, TokenKind::Hash) { let span = self.peek().span; return Err(Diagnostic::new( "`#coerce` is only valid before `fn`", span, )); } if thread_affine_attr && !matches!(self.peek().kind, TokenKind::KwFn) && !matches!(self.peek().kind, TokenKind::Hash) { let span = self.peek().span; return Err(Diagnostic::new( "`#thread_affine` is only valid before `fn`", span, )); } // A-V10 (D441 §5 №167 closure): `#thread_affine` marks a leaf that // is unsafe to call off its ORIGINAL OS thread (thread-affine/ // non-reentrant C-side state) — only meaningful on the FFI boundary // itself, an `extern` fn (no Nova-level body to mark instead). if thread_affine_attr && !is_external { let span = self.peek().span; return Err(Diagnostic::new( "[E_THREAD_AFFINE_NOT_EXTERN] `#thread_affine` is only valid on \ `extern` fn declarations (D441 §5 №167: it marks an M:N-unsafe \ leaf — a C-side call bound to its calling OS thread — and only \ an FFI boundary can make that promise).", span, )); } // Plan 33.1 (D24): `#verify` / `#unverified` / `#verify_timeout(ms)` / // `#pure` — contract-related атрибуты перед `fn`. Парсятся // отдельно от `#realtime`, могут идти в любом порядке. // Не keyword'ы в лексере (контекстный разбор после `#`). let post_contract_attrs = self.parse_contract_attrs()?; if !post_contract_attrs.is_empty() && is_export { // Канон владельца (2026-07-31): атрибуты перед export. Аномальная // форма `export #unverified ⏎ fn` (bigdecimal-прецедент) закрыта. return Err(Diagnostic::new( "[E_ATTR_AFTER_EXPORT] contract attributes (`#verify` / \ `#unverified` / `#verify_timeout` / `#pure` / `#trusted`) must \ precede `export`, on their own line:\n #unverified\n export fn ...\n\ (all item attributes go BEFORE `export` — same as `#stable`)." .to_string(), self.peek().span, )); } let mut contract_attrs = if pre_contract_attrs.is_empty() { post_contract_attrs } else if post_contract_attrs.is_empty() { pre_contract_attrs } else { return Err(Diagnostic::new( "[E_ATTR_AFTER_EXPORT] contract attributes given both before and \ after `export`/`extern` — put them all BEFORE, on their own lines." .to_string(), self.peek().span, )); }; // Plan 103.6 / Plan 113: merge pre-export sync-class attr (if any) into contract_attrs. // Pre-export attrs take precedence over any (invalid) post-export ones. // Plan 113: `#realtime` on any fn (external or not) also sets realtime_attr // so the body restriction is enforced even for non-external fns. if pre_sync_class.is_some() { contract_attrs.sync_class = pre_sync_class; } // Plan 113: if #realtime was consumed by parse_sync_class_attr before export/external, // promote to realtime_attr so fn body gets the realtime restriction enforced. // pre_sync_nogc is true when `nogc` modifier was also consumed (see above). let realtime_attr = if matches!(pre_sync_class, Some(crate::ast::SyncClass::Realtime)) && matches!(realtime_attr, RealtimeAttr::None) { if pre_sync_nogc { RealtimeAttr::RealtimeNogc } else { RealtimeAttr::Realtime } } else { realtime_attr }; // Plan 124.6 (D225): `#test_access(T)` may appear before `test "..." { }`. // If contract_attrs only has test_access_for set (everything else default), // also allow `test` as the next token. Other contract attrs still require fn/external. let contract_attrs_only_test_access = !contract_attrs.test_access_for.is_empty() && { let mut tmp = contract_attrs.clone(); tmp.test_access_for = Vec::new(); tmp.is_empty() }; if !contract_attrs.is_empty() && !matches!(self.peek().kind, TokenKind::KwFn | TokenKind::KwExtern | TokenKind::KwUnsafe) && !(contract_attrs_only_test_access && matches!(self.peek().kind, TokenKind::KwTest)) { let span = self.peek().span; return Err(Diagnostic::new( "contract attributes (`#verify` / `#unverified` / `#verify_timeout` / `#pure` / `#trusted`) are only valid before `fn` or `extern \"nova\" fn`", span, )); } // `#trusted external fn` form removed — use `#trusted extern "nova" fn` instead. // `extern "nova"` is already consumed above (before contract_attrs), so `is_external` // is already correct here. No special case needed. // Plan 118.1.7 (D2 amend): `unsafe fn` keyword syntax — `unsafe` keyword // directly before `fn` (non-external path). Consumed here so plain // `unsafe fn foo()` sets unsafe_kw = true. if matches!(self.peek().kind, TokenKind::KwUnsafe) { unsafe_kw = true; self.bump(); // unsafe } // Propagate unsafe keyword into contract_attrs so parse_fn receives it. if unsafe_kw { contract_attrs.unsafe_attr = true; } // Plan 52 Ф.1: `#from_fields` валиден только перед `type`-декларацией. // Plan 124.8 [M-124.8-zero-on-move]: `#zero_on_move` — также только // перед `type`. // Plan 124.6 (D225): `#pub_to(...)` — также только перед `type`. // Plan 248 (mech, wave 1): `#no_copy` — также только перед `type`. if (!type_attrs.is_empty() || zero_on_move_attr || !pub_to_attr.is_empty() || !serde_attrs.is_empty() || no_copy_attr) && !matches!(self.peek().kind, TokenKind::KwType) { let span = self.peek().span; return Err(Diagnostic::new( "`#from_fields` / `#from_pairs` / `#zero_on_move` / `#pub_to` / `#serde` / `#no_copy` are only valid before `type`", span, )); } // Plan 91.9 (D186): `#impl(...)` валиден перед `type`-декларацией. // Plan 154.1 (D268): `#impl(...)` ТАКЖЕ валиден перед `fn`-декларацией // метода (opt-in conformance). Перед остальными item'ами — ошибка. if !impl_protocols.is_empty() && !matches!(self.peek().kind, TokenKind::KwType | TokenKind::KwFn) { let span = self.peek().span; return Err(Diagnostic::new( "`#impl(...)` is only valid before `type` or `fn`", span, )); } // Plan 170 (D307): `priv(file)` applies only to `fn` / `type` / `const`. // Reject it before any other item kind (test/bench/lemma/let/ro). if file_private && !matches!(self.peek().kind, TokenKind::KwFn | TokenKind::KwType | TokenKind::KwConst) { let sp = self.peek().span; return Err(Diagnostic::new( format!( "[E_PRIV_QUALIFIER] `priv(file)` is only valid before `fn`, `type`, or \ `const`, got `{}` (D307). `test`/`bench`/`lemma`/`let`/`ro` items have \ no file-private visibility.", self.peek().kind.name() ), sp, )); } let parsed = match self.peek().kind { TokenKind::KwFn => Item::Fn(self.parse_fn(is_export, is_external, extern_abi, realtime_attr, blocking_attr, thread_affine_attr, cancel_safe_attr, coerce_attr, impl_protocols, contract_attrs, pending_doc.clone(), pending_doc_attrs.clone(), file_private)?), TokenKind::KwType => Item::Type(self.parse_type_decl(is_export, is_external, type_attrs, impl_protocols, zero_on_move_attr, pub_to_attr, serde_attrs, no_copy_attr, pending_doc.clone(), pending_doc_attrs.clone(), file_private)?), TokenKind::KwLet => { if let Some(d) = &pending_doc { // Plan 45 Ф.3: orphan `///` warning — doc-comment'ы // не имеют семантики для `let`/`test`/`lemma` items. eprintln!( "warning: doc-comment (`///`) before `let` is ignored \ — `let` declarations are not documented (Plan 45 Ф.3). \ span: {:?}", d.span ); } Item::Let(self.parse_let_decl()?) } // Plan 114 (D184): `ro X = expr` — module-level immutable binding. // Заменяет `let X = expr` host для non-constexpr lazy-init. TokenKind::KwRo => { // Plan 157 (D200 amend): `ro Type.NAME [Type] = expr` — // associated ro-value. Lookahead ONLY (no tokens consumed // yet beyond the already-eaten `ro`... no, `ro` itself is // NOT yet consumed here): `KwRo Ident '.' Ident` — the // qualified out-of-body form, symmetric with `const // Type.NAME`'s own dotted-name detection in // `parse_const_decl`. A bare `ro NAME = expr` never has a // `.` right after the first ident (that would make it a // destructuring/variant pattern with no `const`-equivalent, // already routed generically by `parse_ro_mut_binding` / // `check_ro_module_partition`), so this lookahead cannot // misfire on the existing bare form. let is_assoc_ro = matches!(self.peek_at(1).kind, TokenKind::Ident(_)) && matches!(self.peek_at(2).kind, TokenKind::Dot) && matches!(self.peek_at(3).kind, TokenKind::Ident(_)); if is_assoc_ro { // №157 + LSP-находка владельца 2026-07-31: ассоциированная // ro-константа — публичная API-поверхность; doc-comment // легален и уходит в nova doc/hover (симметрия // parse_const_decl). Warning остаётся только для bare-формы. Item::Const(self.parse_assoc_ro_decl(is_export, pending_doc.clone(), pending_doc_attrs.clone(), file_private)?) } else { if let Some(d) = &pending_doc { eprintln!( "warning: doc-comment (`///`) before bare module-level \ `ro` is ignored (Plan 45 Ф.3; associated `ro Type.NAME` \ IS documented since Plan 157). span: {:?}", d.span ); } Item::Let(self.parse_ro_mut_binding(false)?) } } // Plan 114 (D184): `mut X = expr` запрещён на module-level // (module-level mutable global — anti-pattern). TokenKind::KwMut => { return Err(Diagnostic::new( "[E_MUT_AT_MODULE_LEVEL] `mut X = expr` is not allowed at \ module level — module-level mutable globals are an \ anti-pattern. Use `ro X = …` for runtime constants, \ `const X = …` for compile-time, or wrap mutable state in \ a record type (Plan 114 D184).".to_string(), self.peek().span, )); } // Plan 114 (D184): `consume X = expr` запрещён на module-level // (consume-obligation требует scope-exit; module-level scope // никогда не выходит — ill-formed). TokenKind::KwConsume => { return Err(Diagnostic::new( "[E_CONSUME_AT_MODULE_LEVEL] `consume X = expr` is not \ allowed at module level — consume-obligation requires \ scope-exit, but module-level scope never exits. Move \ the binding into a function (Plan 114 D184).".to_string(), self.peek().span, )); } TokenKind::KwConst => Item::Const(self.parse_const_decl(is_export, pending_doc.clone(), pending_doc_attrs.clone(), file_private)?), TokenKind::KwTest if !is_export => { if let Some(d) = &pending_doc { eprintln!( "warning: doc-comment (`///`) before `test` is ignored \ — `test` declarations are not documented (Plan 45 Ф.3). \ span: {:?}", d.span ); } // Plan 124.6 (D225): optional `#test_access(TypeA, ...)` before test block. // Two paths: // 1. `#test_access` was already consumed by parse_contract_attrs() above // (the validation gate now allows this when next token is `test`) — // use contract_attrs.test_access_for directly. // 2. `#test_access` was NOT consumed (e.g. not recognised — shouldn't // happen now) — fall back to parse_test_access_attr() for safety. let test_access = if !contract_attrs.test_access_for.is_empty() { contract_attrs.test_access_for.clone() } else { self.parse_test_access_attr()? }; Item::Test(self.parse_test_decl(test_access)?) } // Plan 57: контекстный `bench` ident. Распознаём как bench-decl // только если за ним идёт string-literal: `bench "name" { ... }`. // Иначе — обычный ident-expr (`bench.opaque(v)`, etc.) который // тут не valid top-level item, ошибка ниже даст «expected fn/type/...». TokenKind::Ident(ref s) if s == "bench" && !is_export && matches!(self.peek_at(1).kind, TokenKind::Str(_)) => { if let Some(d) = &pending_doc { eprintln!( "warning: doc-comment (`///`) before `bench` is ignored \ — `bench` declarations are not documented (Plan 57). \ span: {:?}", d.span ); } Item::Bench(self.parse_bench_decl()?) } TokenKind::KwLemma if !is_export && !is_external => { if let Some(d) = &pending_doc { eprintln!( "warning: doc-comment (`///`) before `lemma` is ignored \ — `lemma` declarations are not documented (Plan 45 Ф.3). \ span: {:?}", d.span ); } Item::Lemma(self.parse_lemma_decl()?) } _ => { let span = self.peek().span; return Err(Diagnostic::new( format!( "expected fn / type / let / const / test, got {}", self.peek().kind.name() ), span, )); } }; // Plan 42.14 Ф.2: item-level `#cfg` — eval predicate. Если inactive // для current target/features, item полностью parsed но дропается // (return None). Eval использует те же env-источники что imports.rs // (NOVA_TARGET_OS / NOVA_FEATURES) — consistency между фазами. if let Some(pred) = item_cfg { let target = crate::imports::current_target_os(); let features = crate::imports::enabled_features(); if !crate::imports::eval_cfg_predicate(&pred, target, &features) { return Ok(None); } } Ok(Some(parsed)) } /// Plan 16 (D64 §3697) + Plan 33.1 (D-attr-syntax): /// parse `#realtime` или `#realtime nogc` атрибут перед fn-declaration. /// Возвращает RealtimeAttr::None если префикса нет. /// /// `realtime` — keyword (TokenKind::KwRealtime), `nogc` — обычный /// identifier (не keyword в lexer'е). Префикс `#`, не `@` /// (разделение от receiver-prefix). fn parse_realtime_attr(&mut self) -> Result<RealtimeAttr, Diagnostic> { if !matches!(self.peek().kind, TokenKind::Hash) { return Ok(RealtimeAttr::None); } // Look ahead: должно быть `#` затем `realtime` keyword. if !matches!(self.peek_at(1).kind, TokenKind::KwRealtime) { return Ok(RealtimeAttr::None); } self.bump(); // # self.bump(); // realtime // Optional `nogc` modifier (Ident, не keyword). let nogc = if let TokenKind::Ident(ref n) = self.peek().kind { if n == "nogc" { self.bump(); true } else { false } } else { false }; // Skip newline после атрибута, чтобы `fn` шёл на следующей строке. self.skip_newlines(); Ok(if nogc { RealtimeAttr::RealtimeNogc } else { RealtimeAttr::Realtime }) } /// Plan 113 (D172): parse `#blocking` attribute перед fn-declaration. /// Returns true if `#blocking` was present, false otherwise. fn parse_blocking_attr(&mut self) -> bool { if !matches!(self.peek().kind, TokenKind::Hash) { return false; } if !matches!(self.peek_at(1).kind, TokenKind::KwBlocking) { return false; } self.bump(); // # self.bump(); // blocking self.skip_newlines(); true } /// Plan 110.7.3.a (D188 §FFI): parse `#cancel_safe` attribute перед /// `external fn` (or any fn). Attests на cancel-safety при invocation /// из ConsumeScope cleanup body. `cancel_safe` — обычный identifier /// (не keyword в lexer'е), парсится контекстно после `#`. /// Returns true if attribute present. fn parse_cancel_safe_attr(&mut self) -> bool { if !matches!(self.peek().kind, TokenKind::Hash) { return false; } match &self.peek_at(1).kind { TokenKind::Ident(n) if n == "cancel_safe" => { self.bump(); // # self.bump(); // cancel_safe self.skip_newlines(); true } _ => false, } } /// Plan 214 (D429): parse `#coerce` attribute перед fn-declaration. /// `coerce` — обычный identifier (не keyword в lexer'е), парсится /// контекстно после `#`, как `cancel_safe`/`blocking` выше. Returns /// true if the attribute was present. fn parse_coerce_attr(&mut self) -> bool { if !matches!(self.peek().kind, TokenKind::Hash) { return false; } match &self.peek_at(1).kind { TokenKind::Ident(n) if n == "coerce" => { self.bump(); // # self.bump(); // coerce self.skip_newlines(); true } _ => false, } } /// A-V10 (D441 §5 №167 closure): parse `#thread_affine` attribute перед /// `extern fn`-declaration. `thread_affine` — обычный identifier (не /// keyword в lexer'е), парсится контекстно после `#`, тем же путём, что /// `cancel_safe`/`coerce` выше (мирроринг `#blocking`'s file-attribute /// slot, но без нового lexer-keyword — не нужен для одного контекстного /// имени). Returns true if the attribute was present. fn parse_thread_affine_attr(&mut self) -> bool { if !matches!(self.peek().kind, TokenKind::Hash) { return false; } match &self.peek_at(1).kind { TokenKind::Ident(n) if n == "thread_affine" => { self.bump(); // # self.bump(); // thread_affine self.skip_newlines(); true } _ => false, } } /// Plan 33.1 (D24): contract-related атрибуты перед fn-declaration. /// /// Поддерживаемые: /// - `#verify` — SMT обязан доказать (D24 §50). /// - `#unverified` — отказ от SMT, всегда runtime fallback в debug, /// стирается в release (D24 §53). /// - `#verify_timeout(N)` — локальный override SMT-timeout в ms. /// - `#pure` — assertion что функция чистая (использование в контрактах /// composition через 33.2). /// /// Контекстный разбор: keyword'ов в лексере нет, парсер ищет /// `#` + Ident в position перед `fn`. Префикс `#` (не `@`) — /// разделение от receiver-prefix. /// Plan 45 Ф.3 / D105: парсит ноль или больше doc-атрибутов. /// Распознаваемые: `#deprecated`, `#since`, `#stable`, `#unstable`, /// `#experimental`, `#hide_doc`, `#doc_alias`, `#doc`. Каждый /// разделён newline'ами. Останавливается на первом `#name`, который /// не в whitelist'е (передаёт ход следующему parser'у — /// `#realtime` / `#verify` / etc.). fn parse_doc_attrs(&mut self) -> Result<Vec<crate::ast::DocAttr>, Diagnostic> { use crate::ast::DocAttr; let mut out: Vec<DocAttr> = Vec::new(); loop { if !matches!(self.peek().kind, TokenKind::Hash) { break; } let name = match &self.peek_at(1).kind { TokenKind::Ident(n) => n.clone(), _ => break, }; let attr = match name.as_str() { "deprecated" => { self.bump(); // # self.bump(); // deprecated let (since, note, until) = if matches!(self.peek().kind, TokenKind::LParen) { self.parse_deprecated_args()? } else { (None, None, None) }; DocAttr::Deprecated { since, note, until } } "since" => { self.bump(); // # self.bump(); // since let v = self.parse_doc_attr_single_string("since")?; DocAttr::Since(v) } "stable" => { self.bump(); // # self.bump(); // stable let since = if matches!(self.peek().kind, TokenKind::LParen) { Some(self.parse_doc_attr_kv_string("since")?) } else { None }; DocAttr::Stable { since } } "unstable" => { self.bump(); self.bump(); let feature = if matches!(self.peek().kind, TokenKind::LParen) { Some(self.parse_doc_attr_kv_string("feature")?) } else { None }; DocAttr::Unstable { feature } } "experimental" => { self.bump(); self.bump(); let note = if matches!(self.peek().kind, TokenKind::LParen) { Some(self.parse_doc_attr_kv_string("note")?) } else { None }; DocAttr::Experimental { note } } "hide_doc" => { self.bump(); self.bump(); DocAttr::HideDoc } "doc_alias" => { self.bump(); self.bump(); let aliases = self.parse_doc_attr_string_list()?; DocAttr::DocAlias(aliases) } "doc" => { self.bump(); self.bump(); self.parse_doc_attr_doc_variant()? } "default_handler" => { self.bump(); // # self.bump(); // default_handler // Plan 175.2 Ф.2-v4 (П7, D431): `(EffectName)` is now // OPTIONAL — bare `#default_handler` infers the effect // from the decorated fn's `-> Effect[X]` return type // (checker-side, `check_default_handlers`). Explicit // `(EffectName)` form still parses unchanged. let eff_name = if matches!(self.peek().kind, TokenKind::LParen) { self.bump(); // ( let name = match &self.peek().kind { TokenKind::Ident(n) => n.clone(), _ => return Err(Diagnostic::new( "expected an effect type name inside `#default_handler(...)`", self.peek().span, )), }; self.bump(); // Ident self.expect(&TokenKind::RParen)?; Some(name) } else { None }; DocAttr::DefaultHandler(eff_name) } _ => break, // не наш — другому parser'у }; out.push(attr); self.skip_newlines(); } Ok(out) } /// `#deprecated(since = "X", note = "Y", until = "Z"?)` — парсит /// 1-3 named-args. Все опциональны. fn parse_deprecated_args( &mut self, ) -> Result<(Option<String>, Option<String>, Option<String>), Diagnostic> { let mut since = None; let mut note = None; let mut until = None; if !matches!(self.peek().kind, TokenKind::LParen) { return Err(Diagnostic::new("expected `(` after `#deprecated`", self.peek().span)); } self.bump(); // ( loop { if matches!(self.peek().kind, TokenKind::RParen) { break; } let key = match &self.peek().kind { TokenKind::Ident(n) => n.clone(), _ => return Err(Diagnostic::new( "expected `since` / `note` / `until` key", self.peek().span, )), }; self.bump(); // key // `key = "value"` (D96 named args). if !matches!(self.peek().kind, TokenKind::Eq) { return Err(Diagnostic::new( "expected `=` after attribute key", self.peek().span, )); } self.bump(); // = let val = match self.peek().kind.clone() { TokenKind::Str(s) => { self.bump(); s } _ => return Err(Diagnostic::new( "expected string literal", self.peek().span, )), }; match key.as_str() { "since" => since = Some(val), "note" => note = Some(val), "until" => until = Some(val), _ => return Err(Diagnostic::new( format!("unknown `#deprecated` key `{}`; expected since/note/until", key), self.peek().span, )), } if matches!(self.peek().kind, TokenKind::Comma) { self.bump(); } } if !matches!(self.peek().kind, TokenKind::RParen) { return Err(Diagnostic::new("expected `)` closing `#deprecated`", self.peek().span)); } self.bump(); // ) Ok((since, note, until)) } /// `#name("value")` или `#name(key = "value")` — поддерживает обе /// формы для single-string attrs (e.g. `#since("0.1.0")` или /// `#since(version = "0.1.0")`). fn parse_doc_attr_single_string(&mut self, attr_name: &str) -> Result<String, Diagnostic> { if !matches!(self.peek().kind, TokenKind::LParen) { return Err(Diagnostic::new( format!("expected `(\"...\")` after `#{}`", attr_name), self.peek().span, )); } self.bump(); // ( let val = match self.peek().kind.clone() { TokenKind::Str(s) => { self.bump(); s } TokenKind::Ident(_) => { // key=value форма self.bump(); // ident (key — ignored — single key allowed) if !matches!(self.peek().kind, TokenKind::Eq) { return Err(Diagnostic::new( format!("expected `=` or string literal in `#{}`", attr_name), self.peek().span, )); } self.bump(); // = match self.peek().kind.clone() { TokenKind::Str(s) => { self.bump(); s } _ => return Err(Diagnostic::new( format!("expected string literal in `#{}`", attr_name), self.peek().span, )), } } _ => return Err(Diagnostic::new( format!("expected string literal in `#{}`", attr_name), self.peek().span, )), }; if !matches!(self.peek().kind, TokenKind::RParen) { return Err(Diagnostic::new( format!("expected `)` closing `#{}`", attr_name), self.peek().span, )); } self.bump(); // ) Ok(val) } /// `#name(key = "value")` — парсит ровно одну named-pair. fn parse_doc_attr_kv_string(&mut self, expected_key: &str) -> Result<String, Diagnostic> { if !matches!(self.peek().kind, TokenKind::LParen) { return Err(Diagnostic::new("expected `(`", self.peek().span)); } self.bump(); // ( let key = match &self.peek().kind { TokenKind::Ident(n) => n.clone(), _ => return Err(Diagnostic::new( format!("expected `{}` key", expected_key), self.peek().span, )), }; if key != expected_key { return Err(Diagnostic::new( format!("expected `{}`, got `{}`", expected_key, key), self.peek().span, )); } self.bump(); // key if !matches!(self.peek().kind, TokenKind::Eq) { return Err(Diagnostic::new("expected `=`", self.peek().span)); } self.bump(); // = let val = match self.peek().kind.clone() { TokenKind::Str(s) => { self.bump(); s } _ => return Err(Diagnostic::new("expected string literal", self.peek().span)), }; if !matches!(self.peek().kind, TokenKind::RParen) { return Err(Diagnostic::new("expected `)`", self.peek().span)); } self.bump(); // ) Ok(val) } /// `#doc_alias("a", "b", ...)` — list of string literals. fn parse_doc_attr_string_list(&mut self) -> Result<Vec<String>, Diagnostic> { let mut out = Vec::new(); if !matches!(self.peek().kind, TokenKind::LParen) { return Err(Diagnostic::new("expected `(` after `#doc_alias`", self.peek().span)); } self.bump(); // ( loop { if matches!(self.peek().kind, TokenKind::RParen) { break; } match self.peek().kind.clone() { TokenKind::Str(s) => { self.bump(); out.push(s); } _ => return Err(Diagnostic::new( "expected string literal", self.peek().span, )), } if matches!(self.peek().kind, TokenKind::Comma) { self.bump(); } } self.bump(); // ) if out.is_empty() { return Err(Diagnostic::new( "`#doc_alias` requires at least one alias", self.peek().span, )); } Ok(out) } /// `#doc(inline)` / `#doc(no_inline)` / `#doc(summary = "...")` / /// `#doc(section = "...")` / `#doc(test_handlers = "...")`. fn parse_doc_attr_doc_variant(&mut self) -> Result<crate::ast::DocAttr, Diagnostic> { use crate::ast::DocAttr; if !matches!(self.peek().kind, TokenKind::LParen) { return Err(Diagnostic::new( "expected `(<variant>)` after `#doc`", self.peek().span, )); } self.bump(); // ( let key = match &self.peek().kind { TokenKind::Ident(n) => n.clone(), _ => return Err(Diagnostic::new( "expected variant name after `#doc(`", self.peek().span, )), }; self.bump(); // key let attr = match key.as_str() { "inline" => DocAttr::DocInline, "no_inline" => DocAttr::DocNoInline, "summary" | "section" | "test_handlers" => { if !matches!(self.peek().kind, TokenKind::Eq) { return Err(Diagnostic::new( format!("expected `=` after `#doc({}`", key), self.peek().span, )); } self.bump(); // = let val = match self.peek().kind.clone() { TokenKind::Str(s) => { self.bump(); s } _ => return Err(Diagnostic::new( "expected string literal", self.peek().span, )), }; match key.as_str() { "summary" => DocAttr::DocSummary(val), "section" => DocAttr::DocSection(val), "test_handlers" => DocAttr::DocTestHandlers(val), _ => unreachable!(), } } _ => return Err(Diagnostic::new( format!("unknown `#doc(...)` variant `{}`; expected inline/no_inline/summary/section/test_handlers", key), self.peek().span, )), }; if !matches!(self.peek().kind, TokenKind::RParen) { return Err(Diagnostic::new("expected `)` closing `#doc`", self.peek().span)); } self.bump(); // ) Ok(attr) } /// Plan 103.6 / Plan 113: Parse optional sync-class attribute (`#realtime` / /// `#parks` / `#wakes`) that appears BEFORE `export external fn`. /// /// `#realtime` is `KwRealtime` (keyword); `#parks` / `#wakes` are idents. /// These attrs occupy the same syntactic position as doc-attrs (before /// `export`) and must be parsed BEFORE `is_export` is consumed. Multiple /// sync-class attrs on one fn are rejected (last-one-wins is confusing); /// skip_newlines() is called after each to allow `#parks\nexport...`. fn parse_sync_class_attr(&mut self) -> Result<Option<crate::ast::SyncClass>, Diagnostic> { use crate::ast::SyncClass; let mut result: Option<SyncClass> = None; loop { if !matches!(self.peek().kind, TokenKind::Hash) { break; } // `#realtime` uses KwRealtime; `#parks`/`#wakes` use Ident. let cls = match &self.peek_at(1).kind { TokenKind::KwRealtime => SyncClass::Realtime, TokenKind::Ident(n) => match n.as_str() { "parks" => SyncClass::Parks, "wakes" => SyncClass::Wakes, _ => break, }, _ => break, }; self.bump(); // # self.bump(); // realtime / parks / wakes result = Some(cls); self.skip_newlines(); } Ok(result) } fn parse_contract_attrs(&mut self) -> Result<ContractAttrs, Diagnostic> { let mut attrs = ContractAttrs::default(); loop { if !matches!(self.peek().kind, TokenKind::Hash) { break; } // Look ahead: `#` затем Ident с одним из contract-keyword'ов, // или KwUnsafe (Plan 118 Ф.3.2 — `#unsafe` keyword-attribute). // KwUnsafe handled inline ниже; others via Ident branch. if matches!(self.peek_at(1).kind, TokenKind::KwUnsafe) { // Plan 118.1.7 (D2 amend): `#unsafe` attribute on fn declarations // is DEPRECATED — hard error E_UNSAFE_ATTR_DEPRECATED. // Use `unsafe fn` keyword syntax instead (Plan 118.1.7 Ф.1.3). let span = self.peek().span; return Err(Diagnostic::new( "[E_UNSAFE_ATTR_DEPRECATED] `#unsafe fn` attribute syntax is removed. \ Use `unsafe fn` keyword syntax instead: \ `unsafe fn foo()` / `external unsafe fn foo()` \ (D2 amend, Plan 118.1.7)", span, )); } let next_name = match &self.peek_at(1).kind { TokenKind::Ident(n) => n.clone(), _ => break, // не идентификатор после `#` — выходим }; match next_name.as_str() { "verify" => { if !matches!(attrs.verify_mode, VerifyMode::Default) { let span = self.peek().span; return Err(Diagnostic::new( "duplicate or conflicting verify mode attribute", span, )); } self.bump(); // # self.bump(); // verify attrs.verify_mode = VerifyMode::MustVerify; } "unverified" => { if !matches!(attrs.verify_mode, VerifyMode::Default) { let span = self.peek().span; return Err(Diagnostic::new( "duplicate or conflicting verify mode attribute", span, )); } self.bump(); // # self.bump(); // unverified attrs.verify_mode = VerifyMode::Unverified; } "verify_timeout" => { if attrs.verify_timeout_ms.is_some() { let span = self.peek().span; return Err(Diagnostic::new( "duplicate `#verify_timeout` attribute", span, )); } self.bump(); // # self.bump(); // verify_timeout self.expect(&TokenKind::LParen)?; let ms = match self.peek().kind { TokenKind::Int(n) if n > 0 => { let v = n as u32; self.bump(); v } _ => { let span = self.peek().span; return Err(Diagnostic::new( "`#verify_timeout(N)` expects positive integer milliseconds", span, )); } }; self.expect(&TokenKind::RParen)?; attrs.verify_timeout_ms = Some(ms); } "pure" => { if matches!(attrs.purity, Purity::Pure) { let span = self.peek().span; return Err(Diagnostic::new( "duplicate `#pure` attribute", span, )); } self.bump(); // # self.bump(); // pure attrs.purity = Purity::Pure; } "trusted" => { // Plan 33.3 Ф.13: #trusted — только для external fn. // Enforcement (must be external) в type-checker или pipeline. self.bump(); // # self.bump(); // trusted attrs.is_trusted = true; } "opaque" => { // Plan 33.9 Ф.1: #opaque — body не раскрывается в SMT. // Validation (#opaque + #pure required, #opaque + #verify // conflict) — в pipeline / verify_module. self.bump(); // # self.bump(); // opaque attrs.is_opaque = true; } "fuel" => { // Plan 33.9 Ф.3: #fuel(n) — unfolding depth для opaque. self.bump(); // # self.bump(); // fuel if !matches!(self.peek().kind, TokenKind::LParen) { let span = self.peek().span; return Err(Diagnostic::new( "#fuel требует аргумент: `#fuel(N)`", span, )); } self.bump(); // ( let n_token = self.peek().clone(); let n = if let TokenKind::Int(n) = n_token.kind { self.bump(); if !(0..=100).contains(&n) { return Err(Diagnostic::new( "#fuel(N) требует 0 <= N <= 100", n_token.span, )); } n as u32 } else { return Err(Diagnostic::new( "#fuel требует int literal: `#fuel(2)`", n_token.span, )); }; if !matches!(self.peek().kind, TokenKind::RParen) { let span = self.peek().span; return Err(Diagnostic::new( "ожидался `)` после #fuel(N)", span, )); } self.bump(); // ) attrs.fuel = Some(n); } "nooverflow" => { // Plan 33.7 Ф.4: #nooverflow — emit overflow VCs for // each BitVec arithmetic op in the fn body. self.bump(); // # self.bump(); // nooverflow attrs.no_overflow = true; } "fn_eval_max_depth" => { // Plan 114.4.4 Ф.1 (D199 V3): `#fn_eval_max_depth(N)` // — per-fn override evaluator recursion depth. // Default 256. Range 1..=65535. self.bump(); // # self.bump(); // fn_eval_max_depth if !matches!(self.peek().kind, TokenKind::LParen) { let span = self.peek().span; return Err(Diagnostic::new( "#fn_eval_max_depth требует аргумент: `#fn_eval_max_depth(N)`", span, )); } self.bump(); // ( let n_token = self.peek().clone(); let n = if let TokenKind::Int(n) = n_token.kind { self.bump(); if !(1..=65535).contains(&n) { return Err(Diagnostic::new( "#fn_eval_max_depth(N) требует 1 <= N <= 65535", n_token.span, )); } n as u32 } else { return Err(Diagnostic::new( "#fn_eval_max_depth требует int literal: `#fn_eval_max_depth(512)`", n_token.span, )); }; if !matches!(self.peek().kind, TokenKind::RParen) { let span = self.peek().span; return Err(Diagnostic::new( "ожидался `)` после #fn_eval_max_depth(N)", span, )); } self.bump(); // ) attrs.fn_eval_max_depth = Some(n); } "test_access" => { // Plan 124.6 (D225): `#test_access(TypeX[, TypeY...])` // — fn body получает priv-field access к указанным types. // Escape hatch для unit tests + sibling helper fns. self.bump(); // # self.bump(); // test_access if !matches!(self.peek().kind, TokenKind::LParen) { let span = self.peek().span; return Err(Diagnostic::new( "#test_access требует list: `#test_access(TypeX, TypeY, ...)`", span, )); } self.bump(); // ( let mut names = Vec::new(); loop { match self.peek().kind.clone() { TokenKind::Ident(n) => { names.push(n); self.bump(); } TokenKind::RParen => break, _ => { let sp = self.peek().span; return Err(Diagnostic::new( "ожидался Type-identifier или `)` в #test_access(...)", sp, )); } } if matches!(self.peek().kind, TokenKind::Comma) { self.bump(); self.skip_newlines(); } else { break; } } self.expect(&TokenKind::RParen)?; if names.is_empty() { let sp = self.peek().span; return Err(Diagnostic::new( "#test_access требует хотя бы один Type: `#test_access(TypeX, ...)`", sp, )); } attrs.test_access_for.extend(names); } _ => break, // unknown #-name — не contract-attr, выходим } self.skip_newlines(); } Ok(attrs) } /// Plan 52 Ф.1 (D108): атрибуты-маркеры перед `type`-декларацией. /// Plan 91.9 (D186): `#impl(P1 + P2 + ...)` — protocol opt-in список. /// /// Поддерживаемые: /// - `#from_fields` — помечает str-keyed map-тип. /// - `#from_pairs` — target для `[k:v]` desugar. /// - `#impl(Name1 + Name2 + ...)` — opt-in protocol implementation /// list. Verification: каждый Name должен быть protocol-типом /// и type должен предоставить все методы. Gates bare-call synthesis. /// - `#pub_to(TypeA, TypeB, ...)` — Plan 124.6 (D225): selective /// friend visibility; listed types get private-field read access. /// /// Returns `(attrs, impl_protocols, zero_on_move, pub_to, serde_attrs, no_copy)`. fn parse_type_attrs(&mut self) -> Result<(Vec<crate::ast::TypeAttr>, Vec<String>, bool, Vec<String>, Vec<crate::ast::SerdeArg>, bool), Diagnostic> { let mut attrs = Vec::new(); let mut impl_protocols: Vec<String> = Vec::new(); // Plan 124.8 [M-124.8-zero-on-move] (2026-06-03): `#zero_on_move` // attribute opts a type into memset-zero-on-consume codegen. let mut zero_on_move: bool = false; // Plan 124.6 (D225): `#pub_to(TypeA, TypeB, ...)` — selective friend visibility. let mut pub_to: Vec<String> = Vec::new(); // Plan 180 Ф.6 (D382): `#serde(tag=/content=/untagged)` — serde tagging mode. let mut serde_attrs: Vec<crate::ast::SerdeArg> = Vec::new(); // Plan 248 (mech, wave 1, p248-mech): `#no_copy` — type-level affine // marker (D133 таблица, «искомое»: значение нельзя связать вторым // именем, но забыть — можно; в отличие от `consume` не требует // расхода на каждом exit-пути). Bare marker, no args, по образцу // `zero_on_move`. let mut no_copy: bool = false; loop { if !matches!(self.peek().kind, TokenKind::Hash) { break; } let next_name = match &self.peek_at(1).kind { TokenKind::Ident(n) => n.clone(), _ => break, }; match next_name.as_str() { "from_fields" => { if attrs.contains(&crate::ast::TypeAttr::FromFields) { let span = self.peek().span; return Err(Diagnostic::new( "duplicate `#from_fields` attribute", span, )); } self.bump(); // # self.bump(); // from_fields attrs.push(crate::ast::TypeAttr::FromFields); } "from_pairs" => { if attrs.contains(&crate::ast::TypeAttr::FromPairs) { let span = self.peek().span; return Err(Diagnostic::new( "duplicate `#from_pairs` attribute", span, )); } self.bump(); // # self.bump(); // from_pairs attrs.push(crate::ast::TypeAttr::FromPairs); } "share" => { // Plan 173.3 (D415): `#share` — audited data-race-freedom // vouch. Bare marker, no args. if attrs.contains(&crate::ast::TypeAttr::Share) { let span = self.peek().span; return Err(Diagnostic::new( "duplicate `#share` attribute", span, )); } self.bump(); // # self.bump(); // share attrs.push(crate::ast::TypeAttr::Share); } "impl" => { // Plan 91.9 (D186): #impl(P1 + P2 + ...) opt-in list. // Plan 164 Ф.1: protocol names may carry generic args, // e.g. #impl(Next[T]) or #impl(Next[(int,T)]). // We parse the bare Ident, then capture any optional // bracketed argument list `[…]` (with nesting) as the // raw source text, and store the full spec like "Next[U]" // in impl_protocols. Consumers that need a bare name // (e.g. self.types.get) use impl_spec_base_name() to // strip the bracket suffix. self.bump(); // # self.bump(); // impl self.expect(&TokenKind::LParen)?; let mut names: Vec<String> = Vec::new(); loop { let (n, _sp) = self.parse_ident()?; // Capture optional generic args `[T]` / `[T, U]` / `[(int,T)]` // as raw source text and append to the name. let full_spec = if matches!(self.peek().kind, TokenKind::LBracket) { let bracket_start = self.peek().span.start; let mut depth = 1usize; self.bump(); // consume `[` while depth > 0 { match self.peek().kind { TokenKind::LBracket => { depth += 1; self.bump(); } TokenKind::RBracket => { depth -= 1; self.bump(); } TokenKind::Eof => { let span = self.peek().span; return Err(Diagnostic::new( "unterminated `[` in #impl protocol argument", span, )); } _ => { self.bump(); } } } // After the loop, pos points just past the closing `]`. // The closing `]` was the last bumped token; peek is now // the token AFTER it. We need the span of the token just // consumed (peek_at(-1) isn't available), so we use the // already-advanced src slice: the bracket group is // src[bracket_start .. end_of_last_consumed_token.end]. // Use tokens[pos-1].span.end as the end. let bracket_end = if self.pos > 0 { self.tokens[self.pos - 1].span.end } else { bracket_start }; let bracket_text = self.src .get(bracket_start..bracket_end) .unwrap_or("") .to_string(); format!("{}{}", n, bracket_text) } else { n.clone() }; // Duplicate check uses the bare name so #impl(Next[T]+Next[U]) // is caught as duplicate "Next". if names.iter().any(|x| impl_spec_base_name(x) == n.as_str()) { let span = self.peek().span; return Err(Diagnostic::new( format!("duplicate protocol `{}` в #impl list", n), span, )); } names.push(full_spec); if self.eat(&TokenKind::Plus).is_some() { continue; } break; } self.expect(&TokenKind::RParen)?; if names.is_empty() { let span = self.peek().span; return Err(Diagnostic::new( "#impl(...) requires at least one protocol name", span, )); } if !impl_protocols.is_empty() { let span = self.peek().span; return Err(Diagnostic::new( "duplicate `#impl` attribute — use `#impl(A + B + ...)` \ with all protocols в одном annotation", span, )); } impl_protocols = names; } "zero_on_move" => { // Plan 124.8 [M-124.8-zero-on-move] (2026-06-03): // opt-in security attribute — на consume value такого // type'а codegen emits memset(source, 0) для затирания // исходной ячейки. Применимо к records (heap + value), // named tuples, newtypes. if zero_on_move { let span = self.peek().span; return Err(Diagnostic::new( "duplicate `#zero_on_move` attribute", span, )); } self.bump(); // # self.bump(); // zero_on_move zero_on_move = true; } "no_copy" => { // Plan 248 (mech, wave 1): `#no_copy type X { ... }` — // affine marker. Bare marker, no args, duplicate-check // — same shape as `#zero_on_move` above. if no_copy { let span = self.peek().span; return Err(Diagnostic::new( "duplicate `#no_copy` attribute", span, )); } self.bump(); // # self.bump(); // no_copy no_copy = true; } "pub_to" => { // Plan 124.6 (D225): `#pub_to(TypeA, TypeB, ...)` — selective // friend visibility. The listed types get private-field read // access to this type (as if they were in the same module). if !pub_to.is_empty() { let span = self.peek().span; return Err(Diagnostic::new( "duplicate `#pub_to` attribute — use a single \ `#pub_to(TypeA, TypeB, ...)` with all friend types", span, )); } self.bump(); // # self.bump(); // pub_to if !matches!(self.peek().kind, TokenKind::LParen) { let span = self.peek().span; return Err(Diagnostic::new( "#pub_to requires a list: `#pub_to(TypeX, TypeY, ...)`", span, )); } self.bump(); // ( let mut names: Vec<String> = Vec::new(); loop { match self.peek().kind.clone() { TokenKind::Ident(n) => { names.push(n); self.bump(); } TokenKind::RParen => break, _ => { let sp = self.peek().span; return Err(Diagnostic::new( "expected type identifier or `)` in #pub_to(...)", sp, )); } } if matches!(self.peek().kind, TokenKind::Comma) { self.bump(); self.skip_newlines(); } else { break; } } self.expect(&TokenKind::RParen)?; if names.is_empty() { let sp = self.peek().span; return Err(Diagnostic::new( "#pub_to requires at least one type: `#pub_to(TypeX, ...)`", sp, )); } pub_to = names; } "serde" => { // Plan 180 Ф.6 (D382): `#serde(tag="t")` / `#serde(tag="t", // content="c")` / `#serde(untagged)` — sum tagging mode. self.parse_serde_attr(&mut serde_attrs)?; } _ => break, } self.skip_newlines(); } Ok((attrs, impl_protocols, zero_on_move, pub_to, serde_attrs, no_copy)) } /// Plan 180 Ф.6 (D382): parse ONE `#serde(...)` annotation, appending its /// arguments to `out`. Positioned at `#` (peek) with `serde` at peek_at(1). /// Grammar: `#serde ( arg (`,` arg)* )`, `arg := ident [ `=` StrLit ]`. /// Recognized keys (V1 — enum tagging, D382): `tag="s"`, `content="s"`, /// `untagged`. Any other key → `E_SERDE_BAD_ATTRIBUTE` (field-customization /// attributes rename/skip/… are a separate followup /// [M-180-serde-field-attributes]; the AST/grammar are general so adding /// them later is trivial). fn parse_serde_attr(&mut self, out: &mut Vec<crate::ast::SerdeArg>) -> Result<(), Diagnostic> { use crate::ast::SerdeArg; self.bump(); // # self.bump(); // serde if !matches!(self.peek().kind, TokenKind::LParen) { return Err(Diagnostic::new( "[E_SERDE_BAD_ATTRIBUTE] `#serde` requires arguments, e.g. \ `#serde(tag=\"type\")` or `#serde(untagged)`", self.peek().span, )); } self.bump(); // ( loop { self.skip_newlines(); if matches!(self.peek().kind, TokenKind::RParen) { break; } let (key, key_span) = match &self.peek().kind { TokenKind::Ident(n) => (n.clone(), self.peek().span), // Plan 180.1 Ф.1.6: `alias` is ALSO a reserved keyword // (`type X alias OtherType`, D52) — the lexer emits `KwAlias`, // not a plain `Ident`, so it needs an explicit alternative here. TokenKind::KwAlias => ("alias".to_string(), self.peek().span), _ => return Err(Diagnostic::new( "[E_SERDE_BAD_ATTRIBUTE] expected a serde attribute name inside `#serde(...)`", self.peek().span, )), }; self.bump(); // key match key.as_str() { "untagged" => out.push(SerdeArg::Untagged), "skip" => out.push(SerdeArg::Skip), "flatten" => out.push(SerdeArg::Flatten), "deny_unknown_fields" => out.push(SerdeArg::DenyUnknownFields), "allow_unknown" => out.push(SerdeArg::AllowUnknown), "default" => { // Plan 180.1 Ф.1.5: bare `default` OR `default = "fn_name"`. if matches!(self.peek().kind, TokenKind::Eq) { self.bump(); // = let val = match &self.peek().kind { TokenKind::Str(s) => { let s = s.clone(); self.bump(); s } _ => return Err(Diagnostic::new( "[E_SERDE_BAD_ATTRIBUTE] `default = ...` requires a string \ value naming a zero-arg function: `#serde(default = \"fn_name\")`", self.peek().span, )), }; out.push(SerdeArg::Default(Some(val))); } else { out.push(SerdeArg::Default(None)); } } "tag" | "content" | "rename" | "skip_serializing_if" | "alias" => { self.expect(&TokenKind::Eq)?; let val = match &self.peek().kind { TokenKind::Str(s) => { let s = s.clone(); self.bump(); s } _ => return Err(Diagnostic::new( format!("[E_SERDE_BAD_ATTRIBUTE] `{}` requires a string value: \ `#serde({}=\"...\")`", key, key), self.peek().span, )), }; match key.as_str() { "tag" => out.push(SerdeArg::Tag(val)), "content" => out.push(SerdeArg::Content(val)), "rename" => out.push(SerdeArg::Rename(val)), "skip_serializing_if" => out.push(SerdeArg::SkipSerializingIf(val)), "alias" => out.push(SerdeArg::Alias(val)), _ => unreachable!(), } } "rename_all" => { self.expect(&TokenKind::Eq)?; let (val, val_span) = match &self.peek().kind { TokenKind::Str(s) => { let s = s.clone(); let sp = self.peek().span; self.bump(); (s, sp) } _ => return Err(Diagnostic::new( "[E_SERDE_BAD_ATTRIBUTE] `rename_all` requires a string value: \ `#serde(rename_all=\"camelCase\")`", self.peek().span, )), }; match crate::ast::RenameConvention::parse(&val) { Some(conv) => out.push(SerdeArg::RenameAll(conv)), None => return Err(Diagnostic::new( format!("[E_SERDE_BAD_ATTRIBUTE] unknown `rename_all` convention \ `{}`. Supported: `camelCase`, `snake_case`, `kebab-case`, \ `SCREAMING_SNAKE_CASE`, `PascalCase`.", val), val_span, )), } } other => return Err(Diagnostic::new( format!("[E_SERDE_BAD_ATTRIBUTE] unknown serde attribute `{}`. Supported: \ `tag`, `content`, `untagged` (enum tagging, D382); `rename`, \ `rename_all`, `skip`, `skip_serializing_if`, `default`, `alias`, \ `flatten`, `deny_unknown_fields`, `allow_unknown` (field/wire \ customization, 180.1 Ф.1/Ф.7).", other), key_span, )), } self.skip_newlines(); if matches!(self.peek().kind, TokenKind::Comma) { self.bump(); } else { break; } } self.expect(&TokenKind::RParen)?; Ok(()) } /// Plan 33.1 (D24): парсит блок `requires <expr>` / `ensures <expr>` /// + Plan 33.2 (D24): `reads <expr>{, <expr>}*` / `modifies <expr>{, <expr>}*` /// после сигнатуры функции, перед телом (`=>` / `{`). /// /// Контекстный разбор: `requires` / `ensures` / `reads` / `modifies` — /// обычные Ident'ы в лексере, парсер распознаёт их в позиции после /// return-type до `=>`/`{`. Один clause на строке; разделение по newline. fn parse_contracts(&mut self) -> Result<(Vec<Contract>, Vec<FrameTarget>, Vec<FrameTarget>, Option<Expr>), Diagnostic> { let mut contracts = Vec::new(); let mut reads = Vec::new(); let mut modifies = Vec::new(); let mut decreases: Option<Expr> = None; loop { // Пропустить newlines между контрактами. self.skip_newlines(); // Plan 194 Ф.1 (D-блок TBD): опц. `#debug` перед клаузой — // dev-only (см. `eat_debug_contract_attr`). let debug_only = self.eat_debug_contract_attr(); match &self.peek().kind { TokenKind::Ident(n) if n == "requires" => { let start = self.peek().span; self.bump(); let expr = self.parse_expr()?; let (message, message_expr) = self.parse_opt_contract_message()?; let span = start.merge(expr.span); contracts.push(Contract { kind: ContractKind::Requires, expr, span, message, message_expr, debug_only }); } TokenKind::Ident(n) if n == "ensures" => { let start = self.peek().span; self.bump(); let expr = self.parse_expr()?; let (message, message_expr) = self.parse_opt_contract_message()?; let span = start.merge(expr.span); contracts.push(Contract { kind: ContractKind::Ensures, expr, span, message, message_expr, debug_only }); } TokenKind::Ident(n) if n == "ensures_fail" => { self.reject_debug_attr_on_non_debug_clause(debug_only, "ensures_fail")?; let start = self.peek().span; self.bump(); let expr = self.parse_expr()?; let (message, message_expr) = self.parse_opt_contract_message()?; let span = start.merge(expr.span); contracts.push(Contract { kind: ContractKind::EnsuresFail, expr, span, message, message_expr, debug_only: false }); } TokenKind::Ident(n) if n == "reads" => { self.reject_debug_attr_on_non_debug_clause(debug_only, "reads")?; self.bump(); self.parse_frame_target_list(&mut reads)?; } TokenKind::Ident(n) if n == "modifies" => { self.reject_debug_attr_on_non_debug_clause(debug_only, "modifies")?; self.bump(); self.parse_frame_target_list(&mut modifies)?; } TokenKind::Ident(n) if n == "decreases" => { self.reject_debug_attr_on_non_debug_clause(debug_only, "decreases")?; if decreases.is_some() { let sp = self.peek().span; return Err(Diagnostic::new( "duplicate `decreases` clause", sp)); } self.bump(); let expr = self.parse_expr()?; decreases = Some(expr); } _ => { if debug_only { let sp = self.peek().span; return Err(Diagnostic::new( "[E_DEBUG_ATTR_TARGET] `#debug` здесь допустим только перед \ `requires`/`ensures` (клауза контракта); получено что-то другое. \ См. Plan 194 (#debug — единый dev-only префикс).", sp, )); } break; } } } Ok((contracts, reads, modifies, decreases)) } /// Plan 194 Ф.1 (D-блок TBD): опц. `#debug`-префикс перед контракт-клаузой /// (`requires`/`ensures`/`invariant`) — единый маркер dev-only (Plan 194 /// решение владельца 2026-07-14: замена зоопарка `debug_requires` и т.п.). /// По образцу `#cfg`/`#stable`: `#` + contextual-ident `debug`, оба — /// обычные токены в лексере (не keyword), поэтому lookahead на 2 токена. /// Возвращает `true` и consumes `#debug`, если он присутствует; `false` /// иначе (no-op, позиция не сдвигается). fn eat_debug_contract_attr(&mut self) -> bool { if matches!(self.peek().kind, TokenKind::Hash) && matches!(&self.peek_at(1).kind, TokenKind::Ident(n) if n == "debug") { self.bump(); // # self.bump(); // debug true } else { false } } /// Plan 194 Ф.1: `#debug` валиден только перед requires/ensures/invariant /// (контракт-клаузами, зеркало §2 плана) — на `reads`/`modifies`/ /// `decreases`/`ensures_fail` и т.п. — внятная ошибка, а не silent-ignore. fn reject_debug_attr_on_non_debug_clause( &self, debug_only: bool, clause_name: &str, ) -> Result<(), Diagnostic> { if debug_only { let sp = self.peek().span; Err(Diagnostic::new( format!( "[E_DEBUG_ATTR_TARGET] `#debug` не поддерживается перед `{clause_name}` — \ только перед `requires`/`ensures`/`invariant` (клаузы) или перед \ `assert(...)` (statement). См. Plan 194 (#debug dev-only prefix)." ), sp, )) } else { Ok(()) } } /// Plan 140.1 Ф.1 (D24 amend): после contract-выражения опционально /// разбирает `, "<message>"` (string-literal). Возвращает `Some(msg)`, /// если запятая присутствует И за ней следует string-literal; `None`, /// если запятой нет (контракт без сообщения). /// /// Грамматика: `requires <expr> ("," <string-lit>)?`. Запятая в этой /// позиции зарезервирована только под сообщение — не-строковый-литерал /// после неё → `E_CONTRACT_MESSAGE_NOT_STRING`. Пользователь НЕ /// включает локацию в сообщение (она авто-проставляется на codegen). /// /// Plan 140.3 ([M-140.1-message-interpolation]): сообщение может быть /// **interp-строкой** `"... ${e} ..."`. Возвращает пару `(static, expr)`: /// плоский литерал → `(Some(msg), None)` (zero-cost baked C-строка); /// строка с `${...}` → `(Some(raw), Some(InterpolatedStr))` — dual-populate: /// `static` = сырой литерал-fallback (для сайтов без interp-поддержки, напр. /// type invariants), `expr` = интерполированная форма (requires вычисляет её /// на сайте нарушения, lazy, с захватом runtime-значений). Запятая без строки /// → `E_CONTRACT_MESSAGE_NOT_STRING`. Без запятой → `(None, None)`. fn parse_opt_contract_message(&mut self) -> Result<(Option<String>, Option<Expr>), Diagnostic> { if !matches!(self.peek().kind, TokenKind::Comma) { return Ok((None, None)); } self.bump(); // consume `,` let tok = self.peek().clone(); match tok.kind { TokenKind::Str(s) => { self.bump(); // Только строки с `${` идут через desugar (interp); плоский // литерал — прежний zero-cost путь (поведение не меняется). if s.contains("${") { let raw = s.clone(); let e = self.desugar_string_interpolation(s, tok.span)?; match e.kind { // desugar мог решить, что интерполяции по факту нет. ExprKind::StrLit(m) => Ok((Some(m), None)), // Dual-populate: `message` = сырой литерал (fallback для // codegen-сайтов, пока не поддерживающих interp — type // invariants; сохраняет прежнее поведение, без silent-drop), // `message_expr` = интерполированная форма (requires/ensures // codegen предпочитает её, вычисляя значения на сайте провала). _ => Ok((Some(raw), Some(e))), } } else { Ok((Some(s), None)) } } _ => { let span = self.peek().span; let actual = self.peek().kind.name(); Err(Diagnostic::new( format!( "[E_CONTRACT_MESSAGE_NOT_STRING] contract message must be a \ string literal, optionally interpolated (e.g. \ `requires x > 0, \"x must be positive\"` or \ `requires x > 0, \"got ${{x}}\"`), got {actual}" ), span, )) } } } /// Plan 33.2: парсит `reads <expr>{, <expr>}*` или `modifies <expr>{, <expr>}*`. /// Each target — l-value: `name` | `name.field` | `arr[i]` | `arr[*]`. fn parse_frame_target_list(&mut self, out: &mut Vec<FrameTarget>) -> Result<(), Diagnostic> { loop { let target = self.parse_frame_target()?; out.push(target); if !matches!(self.peek().kind, TokenKind::Comma) { break; } self.bump(); // , self.skip_newlines(); } Ok(()) } fn parse_frame_target(&mut self) -> Result<FrameTarget, Diagnostic> { let start = self.peek().span; // Parse base — Ident. let expr = self.parse_postfix()?; // Check shape: Ident → Whole; Member → Field; Index → ArrayElem. match &expr.kind { ExprKind::Member { obj, name } => Ok(FrameTarget::Field { receiver: (**obj).clone(), field: name.clone(), span: start.merge(expr.span), }), ExprKind::Index { obj, index } => { // `arr[*]` — special case: index = Ident("*") syntactic // pattern. Парсер видит `*` как BinOp, не Ident. На данный // момент detect через unary multiplication of nothing — // парсер сейчас не примет. Оставим как future TODO. Ok(FrameTarget::ArrayElem { array: (**obj).clone(), index: (**index).clone(), span: start.merge(expr.span), }) } _ => Ok(FrameTarget::Whole(expr)), } } // ─── fn ────────────────────────────────────────────────────────────── fn parse_fn(&mut self, is_export: bool, is_external: bool, extern_abi: Option<String>, realtime_attr: RealtimeAttr, blocking_attr: bool, thread_affine_attr: bool, cancel_safe_attr: bool, coerce_attr: bool, impl_protocols: Vec<String>, contract_attrs: ContractAttrs, doc: Option<crate::ast::DocBlock>, doc_attrs: Vec<crate::ast::DocAttr>, file_private: bool) -> Result<FnDecl, Diagnostic> { let start = self.peek().span; self.expect(&TokenKind::KwFn)?; // Plan 101.1 / D145: `fn[T] ReceiverType @method` префикс — generic // declaration для receiver'ов без carrier-brackets (`[]T`, bare T, // tuple `(T, U)`). Detection: `[` сразу после `fn` + identifier (НЕ `]`, // т.к. `[]T` — slice-receiver case ниже). // // Example: `fn[T] []T @push(a T) { ... }` — T декларирован prefix'ом, // потом используется в `[]T` receiver и `(a T)` param. let mut prefix_generics: Vec<GenericParam> = Vec::new(); if matches!(self.peek().kind, TokenKind::LBracket) && !matches!(self.peek_at(1).kind, TokenKind::RBracket) { prefix_generics = self.parse_generic_decl_params()?; } // Special case: receiver `[]T` (slice-receiver, vec.nv style D38). // `fn []T @method(...)` — парсим как receiver type_name="[]T", где T — // generic param. Первый idеntifier синтезируется из bracket'ов // как "[]<elem>" (один логический name). let (first_ident, first_span); // Plan 15 (D72): generic-параметры в форме declaration (с optional // bound) до момента disambiguation между receiver и free fn. let mut generics_first_decl: Vec<GenericParam> = Vec::new(); // Plan 153.5 (D263) / [M-153.5-flatten-nested-receiver]: the FULL // structured receiver type (`Array(Array(Named T))` for `[][]T`, // `Named{Vec,[Named{Vec,[Named T]}]}` for `Vec[Vec[T]]`) so the // monomorphizer can bind a receiver typevar at ANY nesting depth. let mut receiver_structured_ty: Option<TypeRef> = None; if matches!(self.peek().kind, TokenKind::LBracket) && matches!(self.peek_at(1).kind, TokenKind::RBracket) { let lb = self.bump().span; self.bump(); // ] // Парсим element-type. Для `[]T` это `T` (Named); для НЕСКОЛЬКИХ // уровней (`[][]T`, `[][][]T`, …) `parse_type` рекурсивно строит // `Array(Array(...Named T))` — внутренние `[]` уже потреблены этим // вызовом. Plan 153.5: сохраняем ПОЛНУЮ структуру + считаем глубину. // [M-serde-slice-generic-method-parse]: element type is in // receiver position — a following `.method[...]` (static D42 // receiver, e.g. `[]T.deserialize[D Deserializer](...)`) must NOT // be folded into the element type as a dotted qualified path. self.receiver_elem_ctx = true; let elem_ty = self.parse_type()?; let elem_span = elem_ty.span(); // Full structured receiver type = outer Array wrapping whatever // `parse_type` produced (which already nests every inner `[]`). let full_span = lb.merge(elem_span); let full_ty = TypeRef::Array(Box::new(elem_ty.clone()), full_span); // Сохраняем generic-параметр как fn_generics, а type_name = "[]T" // (или "[][]T", … по глубине). Bootstrap-codegen видит receiver_type // и ищет методы на "[]"-типе. Plan 153.5: depth-aware — count every // `Array` level, descend to the innermost `Named` for the typevar name. let (depth, elem_name) = Self::slice_receiver_depth_and_inner(&full_ty); first_ident = format!("{}{}", "[]".repeat(depth), elem_name); first_span = full_span; receiver_structured_ty = Some(full_ty); } else { // Сначала парсим первый идентификатор. Это либо имя fn, либо // имя receiver-типа. let (id, sp) = self.parse_ident()?; first_ident = id; first_span = sp; // Если за ним `[`, `<`, `mut`, `@` или `.` — это receiver. if matches!(self.peek().kind, TokenKind::LBracket) { // Plan 15 (D72): generic params могут быть либо declaration // (free fn — `fn name[T Hashable]`) либо instantiation // (receiver — `fn TypeName[T] @method`). Парсим как // declaration-form (с optional bound), потом disambiguation. // Plan 153.5 (D263): carrier-position — a slot may be a NESTED // type (`Vec[Vec[T]]`). Detection (`Ident[`) only fires for // genuine receiver carriers; free-fn `[T Bound = D]` slots never // match, so this is safe to enable before the receiver/free-fn // disambiguation below. generics_first_decl = self.parse_generic_decl_params_inner(true)?; // Plan 153.5: build the structured carrier receiver type // `Named{path:[first_ident], generics:[slot types]}` from the // carrier slots (captured before any method-extra-generics parse // below overwrites `last_carrier_slot_types`). Only meaningful if // this turns out to be a receiver — harmless if it's a free fn // (then `receiver_structured_ty` is simply ignored). let carrier_slots = std::mem::take(&mut self.last_carrier_slot_types); if !carrier_slots.is_empty() { let slots_span = carrier_slots.last().map(|t| t.span()).unwrap_or(sp); receiver_structured_ty = Some(TypeRef::Named { path: vec![first_ident.clone()], generics: carrier_slots, span: sp.merge(slots_span), }); } } } let receiver: Option<Receiver>; let name: String; let mut fn_generics: Vec<GenericParam> = Vec::new(); // Plan 73 (D131): optional `mut` / `consume` receiver-квалификатор // перед `@`/`.`. Взаимоисключающие — `consume` забирает значение // целиком, `mut` мутирует на месте. let mut receiver_mut = false; let mut receiver_consume = false; if matches!(self.peek().kind, TokenKind::KwMut | TokenKind::KwConsume) && matches!( self.peek_at(1).kind, TokenKind::At | TokenKind::Dot | TokenKind::KwMut | TokenKind::KwConsume ) { if matches!(self.peek().kind, TokenKind::KwMut) { receiver_mut = true; } else { receiver_consume = true; } self.bump(); // mut / consume // Второй квалификатор подряд → конфликт (D131). if matches!(self.peek().kind, TokenKind::KwMut | TokenKind::KwConsume) { return Err(Diagnostic::new( "receiver не может иметь два квалификатора подряд: `mut` и \ `consume` взаимоисключающие (D131) — `consume` забирает \ значение целиком, `mut` мутирует его на месте; оставьте один" .to_string(), self.peek().span, )); } } if matches!(self.peek().kind, TokenKind::At | TokenKind::Dot) { // `Type [mut|consume] @method` (instance) / `... .method` (static). let kind = if matches!(self.peek().kind, TokenKind::At) { self.bump(); ReceiverKind::Instance } else { self.bump(); ReceiverKind::Static }; // Receiver: bounds in carrier position (e.g. `Vec[T Printable]`) // are extracted into carrier_bounds for future enforcement. let (recv_generics, carrier_bounds) = Self::generic_params_to_type_refs(generics_first_decl)?; receiver = Some(Receiver { type_name: first_ident.clone(), generics: recv_generics, carrier_bounds, // Plan 153.5 (D263): structured receiver type for depth-agnostic // monomorphizer typevar binding (`[][]T`, `Vec[Vec[T]]`, …). receiver_ty: receiver_structured_ty.take(), kind, mutable: receiver_mut, consume: receiver_consume, span: first_span, }); let (n, _) = self.parse_ident()?; name = n; } else { // Свободная функция: `fn name[T](...)`. В этом случае // `generics_first_decl` — это generics функции (с optional // bounds), а `first_ident` — имя. receiver = None; name = first_ident; fn_generics.extend(generics_first_decl); } // Если у метода есть свои generics (D42 model B): `fn Repo[T] @bulk_load[K](...)` if receiver.is_some() && matches!(self.peek().kind, TokenKind::LBracket) { let method_generics_decl = self.parse_generic_decl_params()?; fn_generics.extend(method_generics_decl); } // Plan 101.1 / D145: `fn[T]` prefix-generics добавляются в общий // fn_generics список **перед** receiver-generics и method-generics // — они декларируют typevars для receiver-position (`[]T`, bare T, // tuple) где carrier-brackets не работают. // // Conflict detection (E_DUPLICATE_GENERIC_DECL) — выполняется в // type-checker (Plan 101.1 Ф.2): если prefix и receiver-carrier // декларируют одно имя — error. if !prefix_generics.is_empty() { // Prepend: prefix generics declared first lexically. let mut combined = prefix_generics; combined.append(&mut fn_generics); fn_generics = combined; } // (params) self.expect(&TokenKind::LParen)?; let mut params = Vec::new(); while !matches!(self.peek().kind, TokenKind::RParen) { params.push(self.parse_param()?); if !matches!(self.peek().kind, TokenKind::RParen) { self.expect(&TokenKind::Comma)?; self.skip_newlines(); } } self.expect(&TokenKind::RParen)?; // Plan 14 Ф.6 (D69): variadic-параметр обязан быть последним. // Если variadic не на последней позиции — compile error. for (i, p) in params.iter().enumerate() { if p.is_variadic && i != params.len() - 1 { return Err(Diagnostic::new( format!("variadic-параметр `{}` должен быть последним в списке (D69)", p.name), p.span, )); } } // Plan 46 (D102): параметры с дефолтом идут строго ПОСЛЕ // параметров без дефолта. `fn f(x int = 0, y int)` — error. let mut seen_default = false; for p in ¶ms { if p.default.is_some() { seen_default = true; } else if seen_default && !p.is_variadic { return Err(Diagnostic::new( format!( "параметр `{}` без значения по умолчанию не может идти после \ параметра с дефолтом (D102)", p.name ), p.span, )); } } // Effects: до `->` или до тела let effects = self.parse_effects_until_arrow_or_body()?; // Plan 77 (D132): `-> @` — fluent-return (метод возвращает receiver). let mut returns_receiver = false; // Plan 114.4.2 (D199): `-> const T` — comptime-evaluable return. // Marks fn as `const fn`. All-or-nothing verified в parse_fn после. let mut return_is_const = false; let return_type = if self.eat(&TokenKind::Arrow).is_some() { if matches!(self.peek().kind, TokenKind::KwConst) { self.bump(); // const return_is_const = true; } if matches!(self.peek().kind, TokenKind::At) { let at_span = self.peek().span; if return_is_const { return Err(Diagnostic::new( "[E_CONST_FN_FLUENT_RETURN] `-> const @` not allowed (D199 + D132): \ fluent-return подразумевает runtime receiver, не comptime literal." .to_string(), at_span, )); } self.bump(); // `@` returns_receiver = true; // `-> @` допустим только для instance-метода (есть `@`-receiver). let ok = matches!( &receiver, Some(Receiver { kind: ReceiverKind::Instance, .. }) ); if !ok { return Err(Diagnostic::new( "`-> @` (fluent-return, возврат receiver'а) допустим \ только для instance-метода с `@`-receiver'ом (D132)" .to_string(), at_span, )); } // Plan 100.1 (D8 / D133): `fn T consume @m() -> @` — parse error. // consume = record self-destructs, `-> @` = вернуть тот же объект. // Противоречие: consume уничтожает receiver, но fluent-return // хочет его вернуть. D132 + D133 несовместимы на consume-receiver. if matches!(&receiver, Some(Receiver { consume: true, .. })) { return Err(Diagnostic::new( "[E_CONSUME_RECEIVER_RETURNS_AT] `consume` receiver и `-> @` \ (fluent-return) несовместимы (D8 / D133 / D326 R6): \ consume-метод уничтожает record, fluent-return требует его \ сохранить. Убери `consume` или `-> @`." .to_string(), at_span, )); } // Тип результата — receiver-тип; представляем как `Self`, // переиспользуя всю Self-инфраструктуру type-checker/codegen. Some(TypeRef::Named { path: vec!["Self".to_string()], generics: vec![], span: at_span, }) } else { // №301 (221.1, owner canon 2026-08-03): `-> consume T` — // PREFIX consume-typed return, symmetric with `-> ro T` / // `-> mut T` above. Eat the modifier the same way the // Plan 103.9 (D174) postfix form used to: the information // (that the return is consume-typed) is already carried by // the type name itself — consume-ness is a property of the // type decl, not stored separately on the return type. self.eat(&TokenKind::KwConsume); let rt = self.parse_type()?; // [M-redundant-param-ro-diagnostic] (Plan 172.13 batch 4, // D246 amendment): `-> mut T` — redundant `mut` in return // position (returned value is caller-owned; the caller's // binding decides mutability). Top-level only: `-> *mut T` // is Pointer(Mut(..)) — a meaningful L3 capability, passes. if rt.is_mut() { return Err(redundant_return_mut_error(rt.span())); } // №301: postfix `-> T consume` is RETRACTED — canon moved to // the prefix form eaten above. A trailing `consume` here is // the old spelling; point at the new one instead of silently // accepting it. if matches!(self.peek().kind, TokenKind::KwConsume) { let consume_span = self.peek().span; return Err(postfix_return_consume_retracted_error(consume_span)); } Some(rt) } } else { None }; // Plan 114.4.2 V1 (D199) / Plan 114.4.3 Ф.3 V2 (D199 amend): // const fn classification + restrictions. // // V2 (mixed-args): убираем all-or-nothing reject. Allow: // - Все params const + const return = fully-const fn (V1 surface, // evaluator-inlined + dropped из codegen). // - Mixed (≥1 const param, не fully-const) = monomorphizable fn: // const params trigger constexpr-arg requirement на call-site, // body — runtime, return — runtime/const valid. // - Только const return + runtime params — невалидно: return cannot // reference runtime params если promised constexpr. // - is_external + const param/return → reject (no Nova body). // - effect-list — fully-const reject; mixed может (runtime body // allowed). let any_const_param = params.iter().any(|p| p.is_const); let all_const_params = !params.is_empty() && params.iter().all(|p| p.is_const); let is_fully_const_fn = (all_const_params || params.is_empty()) && return_is_const; let has_const_surface = any_const_param || return_is_const; if has_const_surface { // Runtime params + const return = potential E_CONST_FN_RUNTIME_REF_IN_CONST_RETURN // (body verification by check_const_fn_decl); parser-level — // только if нет ни одного const param: явная ошибка, return cannot // produce constexpr из runtime params. if return_is_const && !any_const_param && !params.is_empty() { return Err(Diagnostic::new( "[E_CONST_FN_PARTIAL_CONSTNESS] `-> const T` return с все-runtime \ params (D199 V2). const-return requires at least один const param \ или fully no params. Make params `const` или замени `-> const T` \ на `-> T`." .to_string(), start, )); } // External + const fn — reject (no Nova body to evaluate). if is_external { return Err(Diagnostic::new( "[E_CONST_FN_EXTERNAL] `external fn` не может быть `const fn` \ (D199): comptime evaluation требует Nova body." .to_string(), start, )); } // Plan 114.4.3 Ф.4 V2: generic const fn allowed — T-independent // body only. T reflection (sizeof[T], T.field) — V3 follow-up. // Fully-const fn — strict: no effect-list (comptime-only). // Mixed fn — runtime body может effects (V2 allows). if is_fully_const_fn && !effects.is_empty() { return Err(Diagnostic::new( "[E_CONST_FN_EFFECT_IN_SIGNATURE] fully-const fn (D199) не \ может иметь effect-list — comptime evaluation runtime-effects \ бессмысленна. Убери effect-list или сделай mixed (хотя бы один \ runtime param)." .to_string(), effects[0].span(), )); } } // Plan 33.1+33.2 (D24): contracts + reads/modifies после сигнатуры, // до тела. `requires <expr>` / `ensures <expr>` / `reads ...` / // `modifies ...` на отдельных строках. let (contracts, reads, modifies, decreases) = self.parse_contracts()?; // external fn не может иметь контрактов (кроме #trusted external fn — Plan 33.3 Ф.13). if is_external && !contract_attrs.is_trusted && (!contracts.is_empty() || !reads.is_empty() || !modifies.is_empty() || decreases.is_some()) { let span = contracts.first().map(|c| c.span) .or_else(|| reads.first().map(|f| f.span())) .or_else(|| modifies.first().map(|f| f.span())) .unwrap_or(start); return Err(Diagnostic::new( format!( "external function `{}` cannot have contracts in Plan 33.1 (use `#trusted` in Plan 33.3 when available)", name ), span, )); } // Plan 91.10 (D163 RETRACTED 2026-05-30): `needs <Cap>` clause удалён. // Plan 91.15 Ф.5: needs_caps field removed from AST — hard-error on // `needs <Cap>` syntax retained so existing .nv files get a clear message. if is_external { let saved_pos = self.pos; self.skip_newlines(); if matches!(&self.peek().kind, TokenKind::Ident(n) if n == "needs") { let needs_span = self.peek().span; return Err(Diagnostic::new( format!( "`needs <Cap>` clause is retracted (Plan 91.10, D163 retract). \ Capability tracking via отдельный syntax merged into effect system. \ If capability gating needed для `external fn {}`, declare an \ effect (`type Fs effect {{ ... }}`) и используйте standard effect \ syntax между params и `->`. См. docs/plans/91.10-d163-retract-capability-syntax.md.", name ), needs_span, )); } self.pos = saved_pos; } // Тело: `=> expr` или `{ block }`. Для `external fn` — тело // отсутствует (D82); следующий токен должен быть Newline/Eof. let (body, end_span) = if is_external { // Body должен отсутствовать. match self.peek().kind { TokenKind::FatArrow | TokenKind::LBrace => { let span = self.peek().span; return Err(Diagnostic::new( format!( "external function `{}` cannot have a body", name ), span, )); } _ => {} } let last_span = self.tokens[self.pos.saturating_sub(1)].span; (FnBody::External, last_span) } else { let b = self.parse_fn_body()?; let s = match &b { FnBody::Expr(e) => e.span, FnBody::Block(bk) => bk.span, FnBody::External => unreachable!(), }; (b, s) }; // Plan 51 Ф.2: `=>`-тело — record-литерал ⇒ тип ровно один раз. Self::check_record_lit_type_once(&return_type, receiver.as_ref(), &body)?; Ok(FnDecl { doc, doc_attrs, is_export, is_external, extern_abi, name, receiver, generics: fn_generics, params, effects, return_type, return_is_const, returns_receiver, body, span: start.merge(end_span), realtime_attr, blocking_attr, // A-V10 (D441 §5 №167 closure): `#thread_affine` ведущий атрибут // перед `extern fn`. thread_affine_attr, cancel_safe_attr, // Plan 214 (D429): `#coerce` ведущий атрибут перед `fn`. coerce_attr, // Plan 154.1 (D268): `#impl(P)` ведущий атрибут на методе. impl_protocols, // Plan 33.1 (D24): contracts + verify attributes. // Backward-compat: пустой Vec для функций без контрактов; // Default verify_mode / Unknown purity для функций без атрибутов. contracts, // Plan 33.2 (D24): reads/modifies frame conditions + // decreases termination measure. reads, modifies, decreases, verify_mode: contract_attrs.verify_mode, verify_timeout_ms: contract_attrs.verify_timeout_ms, purity: contract_attrs.purity, is_trusted: contract_attrs.is_trusted, is_opaque: contract_attrs.is_opaque, fuel: contract_attrs.fuel, no_overflow: contract_attrs.no_overflow, sync_class: contract_attrs.sync_class, // Plan 118.1.7 (D2 amend): `unsafe fn` keyword syntax. unsafe_attr // is set via contract_attrs.unsafe_attr by parse_item when `unsafe` // keyword is consumed before `fn`. Type-checker enforcement // E_UNSAFE_CALL_REQUIRES_WRAP — Ф.3.3-3.5 followup. unsafe_attr: contract_attrs.unsafe_attr, fn_eval_max_depth: contract_attrs.fn_eval_max_depth, test_access_for: contract_attrs.test_access_for.clone(), // Plan 126.2 Ф.1: user-written fns никогда не compiler-generated. compiler_generated: false, // Plan 170 (D307): `priv(file) fn` — file-private visibility. file_private, }) } fn parse_param(&mut self) -> Result<Param, Diagnostic> { // Plan 238 Ф.3 (D446 §4/§5 амендмент): `#fiber_safe name Type` — // explicit annotation, parsed FIRST (before `...`/`ro`/`const`/ // `consume`/`mut`), same contextual-after-`#` convention as // `parse_cancel_safe_attr`/`parse_coerce_attr` above. Accepted on // ANY param syntactically (not just a function-typed one, not just // an extern fn's param) — the narrower "only meaningful on a // function-typed param of a no-body fn" rule is a checker-channel // concern (`fiber_safety.rs`), not a parser one; mirrors how // `#cancel_safe` itself is parsed unconditionally on any fn and // only CONSULTED where it matters. let fiber_safe_attr = self.parse_fiber_safe_attr(); // Plan 14 Ф.6 (D69): `...` префикс перед именем — variadic param. // Только последний param в списке может быть variadic; check // выполняется в parse_fn после сбора всех params'ов. let is_variadic = self.eat(&TokenKind::DotDotDot).is_some(); // D178 (Plan 91 Ф.2.6): `readonly name Type` — параметр с readonly-типом. // Plan 114 (D184) Ф.1.5: `readonly` retracted; используется `ro`. if matches!(self.peek().kind, TokenKind::KwReadonly) { return Err(Diagnostic::new( "[E_KW_REMOVED_READONLY] `readonly` keyword renamed to `ro` \ in Plan 114 (D184). Use `ro name Type` instead of \ `readonly name Type`. Error code E_READONLY_* preserved as \ stable API. Run scripts/tools/plan114_rewrite.py to migrate.".to_string(), self.peek().span, )); } let has_readonly_prefix = self.eat(&TokenKind::KwRo).is_some(); // Plan 114.4.2 (D199): `const name Type` — comptime-only параметр. // All-or-nothing: проверяется в parse_fn после сбора всех params. // Конфликты с другими modifier'ами проверяем сейчас. let is_const_param = if matches!(self.peek().kind, TokenKind::KwConst) { self.bump(); if has_readonly_prefix { return Err(Diagnostic::new( "[E_CONST_PARAM_MOD_CONFLICT] параметр не может быть одновременно \ `ro` и `const` (D199): `const` уже подразумевает immutable \ comptime value. Убери `ro`.".to_string(), self.peek().span, )); } if matches!(self.peek().kind, TokenKind::KwMut) { return Err(Diagnostic::new( "[E_CONST_PARAM_MOD_CONFLICT] параметр не может быть одновременно \ `const` и `mut` (D199): `const` — comptime value, не имеет \ runtime storage. Используй runtime fn если нужна mutation.".to_string(), self.peek().span, )); } if matches!(self.peek().kind, TokenKind::KwConsume) { return Err(Diagnostic::new( "[E_CONST_PARAM_MOD_CONFLICT] параметр не может быть одновременно \ `const` и `consume` (D199): `const` — comptime literal, \ не имеет ownership. Убери `consume`.".to_string(), self.peek().span, )); } true } else { false }; // Plan 73 (D131): `consume name Type` — consuming параметр. После // передачи аргумента в такой параметр переменная-источник // логически инвалидируется (use-after-consume → compile error). // `consume` идёт перед именем (как leading `mut`). let is_consume = if matches!(self.peek().kind, TokenKind::KwConsume) { if is_const_param { return Err(Diagnostic::new( "[E_CONST_PARAM_MOD_CONFLICT] параметр не может быть одновременно \ `const` и `consume` (D199).".to_string(), self.peek().span, )); } self.bump(); // Plan 100.1 (D131 / D133): `consume mut name Type` — parse error. // `consume` = ownership transfer (D131); `mut` = mutable borrow. // Совмещение противоречит семантике: consume забирает ownership, // mut-borrow оставляет его у caller'а. D131 запрещает комбинацию. if matches!(self.peek().kind, TokenKind::KwMut) { return Err(Diagnostic::new( "параметр не может быть одновременно `consume` и `mut` (D131): \ `consume` = ownership transfer, `mut` = mutable borrow — \ взаимоисключающие квалификаторы. Убери `mut` если нужен \ ownership transfer, или замени `consume` на `mut`." .to_string(), self.peek().span, )); } true } else { false }; // Plan 72 P1-A (D6): `mut name type` prefix form. // Plan 108.1 (D176 amend): `mut` now carries semantic weight — // params без `mut` = readonly (default). Mut-method/index-assign // на параметре без `mut` → E_PARAM_NOT_MUT. // // Сочетания с `consume`/`readonly` запрещены parser-level — // E_PARAM_MOD_CONFLICT. let mut is_mut = false; // [M-canon-mut-param-position]: set below only for the bare postfix // `name mut Type` legacy synonym (never for the canonical prefix form // parsed in this very branch, nor for the sanctioned R2-split // `ro name mut Type`) — see the postfix branch after `parse_ident`. let mut mut_type_pos_legacy = false; if matches!(self.peek().kind, TokenKind::KwMut) { if is_const_param { return Err(Diagnostic::new( "[E_CONST_PARAM_MOD_CONFLICT] параметр не может быть одновременно \ `const` и `mut` (D199).".to_string(), self.peek().span, )); } if is_consume { return Err(Diagnostic::new( "[E_PARAM_MOD_CONFLICT] параметр не может быть одновременно \ `consume` и `mut`: `consume` подразумевает ownership transfer \ (D131) — receiver уже владеет и может мутировать. Убери `mut`." .to_string(), self.peek().span, )); } if has_readonly_prefix { return Err(Diagnostic::new( "[E_PARAM_MOD_CONFLICT] параметр не может быть одновременно \ `readonly` и `mut` (D176): взаимоисключающие квалификаторы." .to_string(), self.peek().span, )); } self.bump(); // consume leading `mut` is_mut = true; // Plan 108.1: after consuming `mut`, check for following // `consume`/`readonly` which would create conflict. if matches!(self.peek().kind, TokenKind::KwConsume) { return Err(Diagnostic::new( "[E_PARAM_MOD_CONFLICT] параметр не может быть одновременно \ `mut` и `consume`: `consume` подразумевает ownership transfer \ (D131) — receiver уже владеет и может мутировать. Убери `mut`." .to_string(), self.peek().span, )); } if matches!(self.peek().kind, TokenKind::KwReadonly) { return Err(Diagnostic::new( "[E_KW_REMOVED_READONLY] `readonly` renamed to `ro` (Plan 114 D184). \ Also: cannot combine `mut` and `ro` — взаимоисключающие квалификаторы.".to_string(), self.peek().span, )); } if matches!(self.peek().kind, TokenKind::KwRo) { return Err(Diagnostic::new( "[E_PARAM_MOD_CONFLICT] параметр не может быть одновременно \ `mut` и `ro` (D176 / D184): взаимоисключающие квалификаторы.".to_string(), self.peek().span, )); } } // Plan 184 (D326-ревизия Р3/Р10): `ref` ИСЧЕЗАЕТ из сигнатур параметров. // Формы `mut ref name T` / `ro ref name T` / `ref name T` — удалены. // Синтаксис параметра — тройная ось режима {ro, mut, consume} БЕЗ `ref`: // `f(x T)` (ro, представление по размеру), `f(mut x T)` (in-out всегда), // `f(consume x T)` (владение). `ref` остаётся только как тип приёмника // (`@` = `ref Self`), `-> @` и локал-алиасы (Р1). if matches!(self.peek().kind, TokenKind::KwRef) { let ref_span = self.peek().span; let hint = if is_mut { "пишите `mut x T` — параметр `mut` теперь in-out ссылка всегда" } else if has_readonly_prefix { "пишите `x T` — представление ro-параметра выбирает компилятор по размеру" } else { "пишите `mut x T` (in-out) либо `x T` (ro)" }; return Err(Diagnostic::new( format!( "[E_REF_PARAM_FORM_REMOVED] формы `mut ref`/`ro ref`/`ref` в \ параметре удалены (D326-ревизия, Plan 184 Р3/Р10): `ref` больше \ не пишется в сигнатуре. {}.", hint ), ref_span, ) .with_suggestion(crate::diag::Suggestion { message: hint.to_string(), span: ref_span, replacement: String::new(), applicability: crate::diag::Applicability::MachineApplicable, })); } let (name, name_span) = self.parse_ident()?; // D6: mut-маркер после имени — `name mut type` (legacy form). // Plan 108.1: тоже принимаем, помечаем is_mut. // // [M-canon-mut-param-position] (2026-07-17): canon mut-параметров — // ПРЕФИКСНАЯ форма `mut name Type`. Голая постфиксная `name mut Type` // (без предшествующего `ro`, без предшествующего prefix `mut`) — полный // поведенческий синоним, зафиксирован под запрет lint'ом // `W_PARAM_TYPE_POS_MUT` (lints.rs) для НЕ-slice типов; `mut_type_pos_legacy` // отмечает ИМЕННО этот случай. Санкционированный D246 R2-split // `ro name mut Type` (has_readonly_prefix уже true здесь) — НЕ отмечается. let bare_before_postfix_mut = !has_readonly_prefix && !is_mut; if matches!(self.peek().kind, TokenKind::KwMut) { if is_consume { return Err(Diagnostic::new( "[E_PARAM_MOD_CONFLICT] параметр не может быть одновременно \ `consume` и `mut` (D131 + D176)." .to_string(), self.peek().span, )); } // **Plan 118.5 V3 amend (binding-context relaxation, 2026-06-05):** // `ro x mut T` — orthogonal binding modifiers. `ro` = no-rebind // semantic at binding level, `mut` = mut-method access at binding. // NOT mutually exclusive per user-confirmed V3 amend. Closes // [M-118.5-V3-binding-context-relaxation]. // // (Pre-name `ro` keeps wrapping type as Readonly(T) for content- // readonly compatibility c существующими callers; binding-mut // flag is set additionally — downstream type-checker reads both.) self.bump(); is_mut = true; mut_type_pos_legacy = bare_before_postfix_mut; } let ty = { let inner = self.parse_type()?; // [M-redundant-param-ro-diagnostic] (Plan 172.13 batch 4, D246 // amendment): explicit `ro` in param position is redundant — // params are ro by default. Both spellings rejected here: // prefix form `(ro x T)` → has_readonly_prefix // type form `x ro T` → inner.is_readonly() (top-level) // The V3-amend combo `ro x mut T` (ro binding + explicit mut // content-view) is NOT redundant — gated on !is_mut. `const`/ // `consume` prefixes conflict with `ro` earlier (parse errors // above), so only the plain-ro cases reach this check. if !is_mut { if has_readonly_prefix { return Err(redundant_param_ro_error(name_span)); } if inner.is_readonly() { return Err(redundant_param_ro_error(inner.span())); } } if has_readonly_prefix && !inner.is_readonly() { let sp = inner.span(); TypeRef::Readonly(Box::new(inner), sp) } else { inner } }; // D69 constraint: тип variadic-param обязан быть `[]T` (TypeRef::Array). if is_variadic && !matches!(ty, TypeRef::Array(..)) { return Err(Diagnostic::new( format!("variadic-параметр `{}` должен иметь тип `[]T` (массив)", name), ty.span(), )); } // Plan 46 (D102): опциональное `= expr` — значение по умолчанию. // Variadic-параметр не может иметь дефолт (его дефолт — пустой // пакет). Правило «default после required» проверяется в parse_fn // после сбора всех params (нужен весь список). let default = if matches!(self.peek().kind, TokenKind::Eq) { if is_variadic { return Err(Diagnostic::new( format!("variadic-параметр `{}` не может иметь значение по умолчанию (D102)", name), self.peek().span, )); } self.bump(); // = // Default-выражение без struct-literal ambiguity (как в других // expr-position внутри сигнатуры). Some(self.with_no_struct_or_trailing(|p| p.parse_expr())?) } else { None }; let span_end = default.as_ref().map(|e| e.span).unwrap_or_else(|| ty.span()); Ok(Param { name, ty: ty.clone(), span: name_span.merge(span_end), is_variadic, default, consume: is_consume, is_mut, is_const: is_const_param, mut_type_pos_legacy, fiber_safe_attr, }) } /// Plan 238 Ф.3 (D446 §4/§5 амендмент): parse `#fiber_safe` attribute /// перед a parameter. `fiber_safe` — обычный identifier (не keyword в /// lexer'е), парсится контекстно после `#`, тем же путём, что /// `cancel_safe`/`coerce`/`thread_affine` выше. Returns true if the /// attribute was present. fn parse_fiber_safe_attr(&mut self) -> bool { if !matches!(self.peek().kind, TokenKind::Hash) { return false; } match &self.peek_at(1).kind { TokenKind::Ident(n) if n == "fiber_safe" => { self.bump(); // # self.bump(); // fiber_safe self.skip_newlines(); true } _ => false, } } /// Plan 184 (D326-ревизия Р4): call-site маркер `ref <place>` УДАЛЁН. /// Вызов везде `f(x)` — без `ref x`. Обоснование владельца: кучевые объекты /// и так мутируются без маркера; маркер на стековых давал ложное чувство /// «нет `ref` = нет мутации». `f(ref x)` → парс-ошибка. fn parse_call_arg_value(&mut self) -> Result<Expr, Diagnostic> { if matches!(self.peek().kind, TokenKind::KwRef) { let ref_span = self.peek().span; return Err(Diagnostic::new( "[E_REF_CALL_MARKER_REMOVED] маркер вызова `ref` удалён \ (D326-ревизия, Plan 184 Р4): пишите `f(x)` без `ref`." .to_string(), ref_span, ) .with_suggestion(crate::diag::Suggestion { message: "уберите маркер `ref` — пишите `f(x)`".to_string(), span: ref_span, replacement: String::new(), applicability: crate::diag::Applicability::MachineApplicable, })); } self.parse_expr() } /// Парсит список эффектов между `)` и (`->` | `{` | `=>`). /// Эффект — TypeRef (обычно Named, но может быть с generics: Fail[E]). fn parse_effects_until_arrow_or_body(&mut self) -> Result<Vec<TypeRef>, Diagnostic> { let mut effects = Vec::new(); loop { match self.peek().kind { TokenKind::Arrow | TokenKind::FatArrow | TokenKind::LBrace => break, TokenKind::Ident(_) => { effects.push(self.parse_type()?); } _ => break, } } Ok(effects) } fn parse_fn_body(&mut self) -> Result<FnBody, Diagnostic> { match self.peek().kind { TokenKind::FatArrow => { self.bump(); self.skip_newlines(); let expr = self.parse_expr()?; Ok(FnBody::Expr(expr)) } TokenKind::LBrace => Ok(FnBody::Block(self.parse_block()?)), _ => { let span = self.peek().span; Err(Diagnostic::new( format!( "expected `=>` or `{{` for function body, got {}", self.peek().kind.name() ), span, )) } } } /// Plan 51 Ф.2: когда `=>`-тело функции/замыкания — record-литерал, /// тип берётся из return-аннотации; писать его И в литерале нельзя /// (тип объявляется ровно один раз). `-> Self` резолвится к типу /// receiver'а (`-> Self => Counter{}` в методе `Counter` — тоже /// избыточно). `path ≠ return` (sum-coercion, `-> Shape => Circle{}`) /// — не трогаем. Используется и `parse_fn`, и `parse_closure_full`. fn check_record_lit_type_once( return_type: &Option<TypeRef>, receiver: Option<&crate::ast::Receiver>, body: &FnBody, ) -> Result<(), Diagnostic> { let FnBody::Expr(e) = body else { return Ok(()); }; let ExprKind::RecordLit { type_name: Some(lit_path), .. } = &e.kind else { return Ok(()); }; let resolve = |p: &Vec<String>| -> Vec<String> { if p.len() == 1 && p[0] == "Self" { if let Some(r) = receiver { return vec![r.type_name.clone()]; } } p.clone() }; match return_type { None => Err(Diagnostic::new( "a function whose `=>` body is a record literal must declare \ its return type — write `fn ... -> T => { ... }`", e.span)), Some(TypeRef::Named { path: ret_path, .. }) => { if resolve(lit_path) == resolve(ret_path) { Err(Diagnostic::new( format!( "redundant type prefix on record literal — the return \ type `-> {}` already declares it; write `=> {{ ... }}`", ret_path.join(".")), e.span)) } else { Ok(()) } } Some(_) => Ok(()), } } // ─── type declarations ─────────────────────────────────────────────── fn parse_type_decl(&mut self, is_export: bool, is_external: bool, attrs: Vec<crate::ast::TypeAttr>, impl_protocols: Vec<String>, zero_on_move: bool, pub_to: Vec<String>, serde_attrs: Vec<crate::ast::SerdeArg>, no_copy: bool, doc: Option<crate::ast::DocBlock>, doc_attrs: Vec<crate::ast::DocAttr>, file_private: bool) -> Result<TypeDecl, Diagnostic> { let start = self.peek().span; self.expect(&TokenKind::KwType)?; let (name, name_span) = self.parse_ident()?; // Plan 15 (D72): generics в форме `[T]` или `[T Hashable]`. // Bound — protocol-тип, проверяется в type-checker'е на use-site. let generics: Vec<GenericParam> = if matches!(self.peek().kind, TokenKind::LBracket) { self.parse_generic_decl_params()? } else { Vec::new() }; // Plan 100.1 (D133 / D1): `type X consume { ... }` — type-level // must-be-consumed marker. После имени и generics, перед body. // Plan 100.5 (D163): `external type X consume` — allowed for FFI opaque // consume-types (File, Mutex, Socket). The `consume` marker declares that // caller must consume instances (via consume-method wrapper). No body // needed — consume enforcement is field-independent for opaque types // (the entire opaque value is the resource). // // Plan 124 (D220): `type X priv { ... }` — type-level default visibility // flip; fields default = priv для этого type'а, explicit `pub` modifier // на field override priv default. Markers `consume` и `priv` могут идти // в любом порядке (взаимно независимые). // // Plan 124.8 (D226 NEW): `type X value { ... }` — stack-allocated record // (value type, copy semantics на pass). `value` — contextual keyword // (Ident("value") в этой позиции; backward compat для variables/fields // named `value`). Composable с consume/priv в любом порядке. // // Plan 148 Ф.1 (D241): canonical type-modifier order — `consume value priv`, // отсортировано по Rust-like ownership > representation > visibility. // «One canonical syntax» Nova запрещает order-independence: // out-of-canon порядок → `E_MODIFIER_ORDER` с машинно-применимым // fix-it «переставь в канон». // // Каждому модификатору присвоен canonical rank: // `consume` → 0 (must-consume обязательство — ownership первично) // `value` → 1 (аллокация/представление — representation вторично) // `priv` → 2 (дефолт видимости полей в `{…}` — вплотную к `{`) // Правило обобщается на любые будущие type-модификаторы: новый // модификатор получает rank по своему scope и автоматически попадает // в проверку монотонности (никаких произвольных синонимичных порядков). let mut consume_marker = false; let mut field_default_visibility = crate::ast::FieldDefaultVisibility::Public; let mut allocation = crate::ast::AllocKind::Heap; // (rank, lexeme, token-span) для каждого встреченного модификатора, // в порядке появления в исходнике. let mut seen_mods: Vec<(u8, &'static str, Span)> = Vec::new(); loop { if !consume_marker && matches!(self.peek().kind, TokenKind::KwConsume) { let sp = self.peek().span; self.bump(); consume_marker = true; seen_mods.push((0, "consume", sp)); continue; } if matches!(field_default_visibility, crate::ast::FieldDefaultVisibility::Public) && matches!(self.peek().kind, TokenKind::KwPriv) { let sp = self.peek().span; self.bump(); // Plan 160 (D281) new design: // `priv` (no qualifier) → Module (module-private) // `priv(type)` → Private (type-private only) // `priv(module)` → error (removed; use bare `priv`) // `priv(<other>)` → error (unknown qualifier) if matches!(self.peek().kind, TokenKind::LParen) { self.bump(); // consume `(` if matches!(self.peek().kind, TokenKind::KwType) { self.bump(); // consume `type` self.expect(&TokenKind::RParen)?; field_default_visibility = crate::ast::FieldDefaultVisibility::Private; seen_mods.push((2, "priv(type)", sp)); } else if matches!(self.peek().kind, TokenKind::KwModule) { let bad_sp = self.peek().span; return Err(crate::diag::Diagnostic::new( "[E_PRIV_QUALIFIER] `priv(module)` is no longer valid \ (Plan 160 / D281 new design). Use bare `priv` for \ module-private fields, or `priv(type)` for type-private.", bad_sp, )); } else { let bad_sp = self.peek().span; return Err(crate::diag::Diagnostic::new( "[E_PRIV_QUALIFIER] unknown qualifier inside `priv(…)`. \ Valid forms: `priv` (module-private) or `priv(type)` \ (type-private). See D281 (spec/decisions/02-types.md).", bad_sp, )); } } else { // bare `priv` → module-private field_default_visibility = crate::ast::FieldDefaultVisibility::Module; seen_mods.push((2, "priv", sp)); } continue; } // Plan 124.8: `value` — contextual keyword (Ident match, // не KwValue, для backward compat). if matches!(allocation, crate::ast::AllocKind::Heap) && matches!(self.peek().kind, TokenKind::Ident(ref s) if s == "value") { let sp = self.peek().span; self.bump(); allocation = crate::ast::AllocKind::Value; seen_mods.push((1, "value", sp)); continue; } break; } // D241 enforcement: ranks должны строго возрастать в порядке появления. // Любая инверсия (`priv value`, `priv consume`, `consume value`, …) → // E_MODIFIER_ORDER. Fix-it переписывает весь modifier-регион в канон // (модификаторы, отсортированные по rank, через пробел). if seen_mods.len() >= 2 { let is_canonical = seen_mods .windows(2) .all(|w| w[0].0 < w[1].0); if !is_canonical { let region = seen_mods .first() .unwrap() .2 .merge(seen_mods.last().unwrap().2); let mut canonical: Vec<(u8, &'static str)> = seen_mods.iter().map(|(r, l, _)| (*r, *l)).collect(); canonical.sort_by_key(|(r, _)| *r); let canon_str = canonical .iter() .map(|(_, l)| *l) .collect::<Vec<_>>() .join(" "); let got_str = seen_mods .iter() .map(|(_, l, _)| *l) .collect::<Vec<_>>() .join(" "); return Err(Diagnostic::new( format!( "[E_MODIFIER_ORDER] type-declaration modifiers `{}` are \ out of canonical order — Nova has one canonical syntax \ (no order-independence). Canonical order is ownership → \ representation → visibility: type-level ownership \ (`consume`) → type-level representation (`value`) → \ field-default visibility (`priv`). Reorder to `{}`. \ See D241 (spec/decisions/03-syntax.md).", got_str, canon_str, ), region, ) .with_suggestion(crate::diag::Suggestion { message: format!("reorder modifiers to canonical `{}`", canon_str), span: region, replacement: canon_str, applicability: crate::diag::Applicability::MachineApplicable, })); } } // Plan 62.D.bis (D126): `external type X [Generics]` — opaque type, // реализация в runtime. Body отсутствует — никакого `{ ... }`, `|`, // `effect`, `protocol`, `alias TYPE`, newtype `TYPE`. Если parser // встречает что-то похожее на body — это compile error. if is_external { // Plan 91.12 V2 followup [M-91.12-parser-body-detect-heuristic] // (closed 2026-06-01): зафиксировать наличие newline ДО skip, // чтобы distinguish: // 1. `external type X\n\nfn dummy()` — `fn` это next decl, не body. // 2. `external type X\n{...}` — `{` это body (на любом расстоянии). // 3. `external type X Foo` — `Foo` (на той же линии) это newtype body. // Старая логика skip_newlines() → check is_body_start ловила (1) // как ложный newtype body (fn after `external type X` treat'ился // как body, не next decl). Новая: только same-line check (no newline) // ловит newtype body case, иначе newlines = end of decl. let saw_newline_before_body = matches!( self.peek().kind, TokenKind::Newline | TokenKind::Semicolon ); // Always check brace/pipe/effect/protocol/alias на любом расстоянии — // эти однозначные body markers даже при newline нарушают opaque-type // контракт (`external type X\n{ ... }` — попытка body, error). self.skip_newlines(); match self.peek().kind { TokenKind::LBrace | TokenKind::Pipe | TokenKind::KwEffect | TokenKind::KwProtocol | TokenKind::KwAlias => { let span = self.peek().span; return Err(Diagnostic::new( format!( "external type `{}` cannot have a body (got `{}`); \ external types are opaque — implementation lives in runtime \ (`nova_rt/<name>.h`/.c). See D126 (spec/decisions/03-syntax.md).", name, self.peek().kind.name() ), span, )); } _ => {} } // Ident/LBracket/KwFn/Amp body detect — ONLY на той же линии // (no newline separator). Если был newline до пика — это next // module-level declaration (fn / type / const / etc), не body. let is_body_start = !saw_newline_before_body && match &self.peek().kind { TokenKind::Ident(s) if !s.is_empty() && s.chars().next().map(|c| c.is_alphabetic() || c == '_').unwrap_or(false) => true, TokenKind::LBracket => true, // []byte etc. TokenKind::KwFn => true, // fn-type TokenKind::Amp => true, // &Type _ => false, }; if is_body_start { let span = self.peek().span; return Err(Diagnostic::new( format!( "external type `{}` cannot have a newtype-style body \ (got token `{}`); external types are opaque — \ implementation lives in runtime. See D126.", name, self.peek().kind.name() ), span, )); } // OK — newline или EOF. Создаём Opaque декларацию. // Plan 100.5 (D163): `external type X consume` — preserve consume_marker. // Opaque consume-types (File, Mutex, Socket) carry must-consume obligation // via LinearityRegistry; consume_marker = true → type tracked as consume. let last_span = self.tokens[self.pos.saturating_sub(1)].span; self.expect_newline_or_eof().ok(); return Ok(TypeDecl { doc, doc_attrs, is_export, name, generics, kind: TypeDeclKind::Opaque, impl_protocols: impl_protocols.clone(), span: start.merge(last_span), assoc_consts: Vec::new(), attrs, invariants: Vec::new(), axioms: Vec::new(), consume: consume_marker, field_default_visibility, allocation, zero_on_move, no_copy, pub_to: pub_to.clone(), file_private, serde_attrs: serde_attrs.clone(), }); } // Silence unused warning when is_external is false; name_span used только в Opaque branch. let _ = name_span; // Тело типа может идти на следующей строке для multi-line sum'ов // и эффектов. Skip newlines перед body. self.skip_newlines(); // Тело: `effect { ... }` | `protocol { ... }` | `alias TYPE` | // `{ fields }` | `| variant | variant` | `TYPE` (newtype) | // начинается с `|` для sum. // // Plan 15 D53 strict: protocol/effect — отдельные kind'ы // несмотря на синтаксическое сходство. По D62 тело идентично // (parse_effect_methods переиспользуется), но семантика // разная: // - effect: capability с runtime vtable + handler-dispatch. // - protocol: compile-time структурный контракт; usage как // bound (D72) и тип-значение. // Codegen эмитит vtable только для Effect-kind. BoundCtx // (D72 enforcement) регистрирует только Protocol-kind. // Plan 33.3 Ф.9: axioms собираются внутри effect-блока (после // методов и pure_view-объявлений). Для protocol — всегда пусто. // Plan 72 P1-B: empty-sum syntax — `type X` without body. // After skip_newlines(), if the next token starts a new declaration // (keyword like `fn`, `type`, `const`, `export`, `import`, `test`, // `module`) or is a newline/EOF (meaning the declaration is done), // treat this as an empty sum type: 0 variants, uninhabited. // Also accept `type X { }` (empty braces) as empty sum for symmetry. // Use case: `type RuntimeNoneError` (marker / uninhabited). // Note: bottom-тип `never` — встроенный примитив (Plan 76), не // объявляется через `type` — empty-sum здесь к нему не относится. { // [D52-амендмент, ОКНО-5] `type Handler fn(A) -> B` — newtype над // fn-типом. Голый `KwFn` здесь неоднозначен: настоящий следующий // top-level `fn name(...)` ВСЕГДА несёт identifier сразу после // `fn` (top-level fn без имени не существует), тогда как // fn-TYPE-body начинается `fn(` — сразу `(`, без имени // (`parse_fn_type_signature` требует `fn` `(` подряд). Поэтому // `KwFn` считаем концом (пустой sum, `fn` — следующая декларация) // ТОЛЬКО когда следующий токен ПОСЛЕ `fn` НЕ `(` — иначе это // fn-type body, продолжаем в общий `_ => parse_type()` arm ниже. let kwfn_is_next_decl = matches!(self.peek().kind, TokenKind::KwFn) && !matches!(self.peek_at(1).kind, TokenKind::LParen); let is_body_end = kwfn_is_next_decl || matches!( self.peek().kind, TokenKind::Newline | TokenKind::Eof | TokenKind::KwType | TokenKind::KwConst | TokenKind::KwExport | TokenKind::KwImport | TokenKind::KwTest | TokenKind::KwModule // Plan 100.4.1 fix: outer doc-comment `///` начинает new // top-level item (attached to next decl). After bodyless // type, doc-comment следует к next type/fn — terminate // empty-sum body. Inner `//!` тоже attaches к enclosing. | TokenKind::DocComment { .. } ); if is_body_end { let last_span = self.tokens[self.pos.saturating_sub(1)].span; let kind = TypeDeclKind::Sum(Vec::new()); // Skip trailing newline if present self.expect_newline_or_eof().ok(); // Invariants and axioms not applicable for empty sum return Ok(TypeDecl { doc, doc_attrs, is_export, name, generics, kind, span: start.merge(last_span), assoc_consts: Vec::new(), attrs, invariants: Vec::new(), axioms: Vec::new(), consume: false, field_default_visibility: crate::ast::FieldDefaultVisibility::Public, allocation: crate::ast::AllocKind::Heap, impl_protocols: impl_protocols.clone(), zero_on_move, no_copy, pub_to: pub_to.clone(), file_private, serde_attrs: serde_attrs.clone(), }); } } let mut effect_axioms: Vec<EffectAxiom> = Vec::new(); // Plan 114.4.1 (D200): assoc consts собираются здесь; populates только // для Record-types в Ф.1 (Sum-type + Generic — Ф.2/Ф.3 followup). let mut assoc_consts: Vec<AssocConst> = Vec::new(); let kind = match self.peek().kind { TokenKind::KwEffect => { self.bump(); self.expect(&TokenKind::LBrace)?; // Effect bodies use bare-ident syntax (no `@` receiver prefix). let methods = self.parse_effect_methods(false)?; effect_axioms = self.parse_effect_axioms()?; self.expect(&TokenKind::RBrace)?; TypeDeclKind::Effect(methods) } TokenKind::KwProtocol => { self.bump(); self.expect(&TokenKind::LBrace)?; // Plan 101.4 (D145 Ред. 5): protocol composition. Парсим // `use TypeName` items (могут быть несколько, перед/после // методов; для V1 — в любом месте тела). Остальное — // обычные method-signatures. let (methods, embeds) = self.parse_protocol_body()?; // Plan 33.3 Ф.9 (refactor): protocol также может содержать // axioms. Verification impl-handler'а отложена на V2 // (#verify_impl / #trusted_impl) — в V1 axioms в protocol // трактуются как trusted-by-default (любая impl верна, что // декларирует axiom). Symmetry с #trusted handler'ами. effect_axioms = self.parse_effect_axioms()?; self.expect(&TokenKind::RBrace)?; TypeDeclKind::Protocol { methods, embeds } } TokenKind::KwAlias => { self.bump(); let ty = self.parse_type()?; TypeDeclKind::Alias(ty) } // D406: `type Name enum A | B` — sum-type с явным enum маркером. // `enum` — контекстный ident (не lexer-keyword) в позиции после `type Name`. // Inline: первый вариант без `|`. Многострочный: `|` обязателен у каждого. TokenKind::Ident(ref s) if s == "enum" => { self.bump(); // consume `enum` let variants = self.parse_sum_variants_after_enum()?; TypeDeclKind::Sum(variants) } // Plan 172.3 (D310): `type Name set A | B | C` — type-set bound. // `set` — КОНТЕКСТНЫЙ kind-токен (не lexer-keyword), распознаётся только // в этой позиции (после `type Name`), как `value` ниже. Диспетч по первому // токену после имени (D52); ноль конфликта с sum (`type X | A` — Pipe сразу, // без `set`). Члены — TypeRef через `|`. TokenKind::Ident(ref s) if s == "set" => { self.bump(); // consume `set` let members = self.parse_type_set_members()?; TypeDeclKind::TypeSet(members) } TokenKind::LBrace => { self.bump(); // Plan 124 (D220): pass type-level default visibility to field parser. let (fields, acs) = self.parse_record_fields_with_default(field_default_visibility)?; assoc_consts.extend(acs); self.expect(&TokenKind::RBrace)?; TypeDeclKind::Record(fields) } TokenKind::Pipe => { let variants = self.parse_sum_variants()?; TypeDeclKind::Sum(variants) } // Plan 120 (D215): named tuple `type Point(x f64, y f64)` vs // positional tuple `type Point(f64, f64)` — disambiguate here. // Lookahead: after `(`, if IDENT followed by type-starting token // → named tuple. Otherwise delegate to parse_type() as before. // // Plan 124.7 (D225): `type Vec3 priv (x f64, y f64, z f64)` — // type-level priv default flip для tuple form (extends D220 // record form). field_default_visibility pass'ится в field parser. TokenKind::LParen if self.is_named_tuple_decl() => { self.bump(); // consume `(` let fields = self.parse_named_tuple_fields_with_default(field_default_visibility)?; self.expect(&TokenKind::RParen)?; TypeDeclKind::NamedTuple(fields) } // type Name OtherType — newtype (includes positional tuple `type X(T, U)`) _ => { let ty = self.parse_type()?; TypeDeclKind::Newtype(ty) } }; // Sum-варианты сами съедают newlines в конце; для других форм // ожидаем разделитель. if !matches!(kind, TypeDeclKind::Sum(_)) { self.expect_newline_or_eof().ok(); } // Plan 33.2 Ф.7 (D24): `invariant <expr>` clauses на record-типах. // Парсятся после type-body, могут быть несколько. Для не-record // типов — error (sum/protocol/alias/newtype invariants — будущее). let mut invariants: Vec<Contract> = Vec::new(); loop { self.skip_newlines(); // Plan 194 Ф.1: опц. `#debug` перед type-`invariant` (dev-only). let debug_only = self.eat_debug_contract_attr(); match &self.peek().kind { TokenKind::Ident(n) if n == "invariant" => { if !matches!(kind, TypeDeclKind::Record(_)) { let sp = self.peek().span; return Err(Diagnostic::new( "`invariant` clauses are only supported on record types in Plan 33.2 \ (sum/protocol/alias invariants — future)", sp, )); } let cstart = self.peek().span; self.bump(); let expr = self.parse_expr()?; let (message, message_expr) = self.parse_opt_contract_message()?; let cspan = cstart.merge(expr.span); invariants.push(Contract { kind: ContractKind::Ensures, // invariants are 'ensures'-like expr, span: cspan, message, message_expr, debug_only, }); } _ => { if debug_only { let sp = self.peek().span; return Err(Diagnostic::new( "[E_DEBUG_ATTR_TARGET] `#debug` здесь допустим только перед \ `invariant` (клауза типа). См. Plan 194.", sp, )); } break; } } } let span = start.merge(self.tokens[self.pos.saturating_sub(1)].span); Ok(TypeDecl { doc, doc_attrs, is_export, name, generics, kind, span, assoc_consts, attrs, invariants, axioms: effect_axioms, consume: consume_marker, field_default_visibility, allocation, impl_protocols, zero_on_move, no_copy, pub_to, file_private, serde_attrs, }) } /// Plan 120 (D215): lookahead to detect named tuple field pattern. /// Returns true if tokens[pos] = IDENT and tokens[pos+1] = type-start. /// This is called when we're positioned AT `(` in type decl. /// tokens[pos] = `(`, tokens[pos+1] = first token inside parens. /// /// Plan 124.8 (D215 amend): skip newlines в lookahead — поддержка /// multi-line форм `type X(\n name T,\n ...\n)`. fn is_named_tuple_decl(&self) -> bool { // tokens[pos] = `(` (current). Skip newlines after `(` to find first // non-trivia token. let mut i = 1; while matches!(self.peek_at(i).kind, TokenKind::Newline | TokenKind::Semicolon) { i += 1; } let first = &self.peek_at(i).kind; // Plan 124.4 (retracted в Plan 124.8 D222 amend): `priv` / `pub` на // tuple field больше не разрешены, но если parser видит их — // распознаём как «named tuple intent» чтобы emit clean error // `E_TUPLE_NO_PRIV` в parse_named_tuple_fields_with_default. // Plan 124.8 (D215 amend): same для `mut`/`ro` per-field modifiers — // emit `E_TUPLE_NO_PER_FIELD_MOD` clean error. if matches!(first, TokenKind::KwPriv | TokenKind::KwPub | TokenKind::KwMut) { return true; } // KwRo — может быть как per-field modifier (ban) или как type modifier // в positional tuple `type X(ro []u8)`. Distinguish: KwRo + Ident-then-type // → per-field modifier (named tuple intent); KwRo + type → positional. if matches!(first, TokenKind::KwRo) { // Lookahead: ro IDENT TYPE → named tuple intent (emit error); // ro TYPE → positional (let parse_type handle). let mut k = i + 1; while matches!(self.peek_at(k).kind, TokenKind::Newline | TokenKind::Semicolon) { k += 1; } if matches!(self.peek_at(k).kind, TokenKind::Ident(_)) { let mut m = k + 1; while matches!(self.peek_at(m).kind, TokenKind::Newline | TokenKind::Semicolon) { m += 1; } if matches!(self.peek_at(m).kind, TokenKind::Ident(_) | TokenKind::LBracket | TokenKind::KwFn | TokenKind::KwRo ) { return true; } } } // Skip newlines between IDENT and type token. let mut j = i + 1; while matches!(self.peek_at(j).kind, TokenKind::Newline | TokenKind::Semicolon) { j += 1; } let second = &self.peek_at(j).kind; // Named field: IDENT followed by a type-starting token (not `,` not `)`) matches!(first, TokenKind::Ident(_)) && matches!(second, TokenKind::Ident(_) | TokenKind::LBracket | TokenKind::KwFn | TokenKind::KwRo ) } /// Plan 120 (D215): parse `name1 T1, name2 T2, ...` inside `(...)`. /// Backward-compat shim — calls parse_named_tuple_fields_with_default(Public). #[allow(dead_code)] fn parse_named_tuple_fields(&mut self) -> Result<Vec<NamedTupleField>, Diagnostic> { self.parse_named_tuple_fields_with_default(crate::ast::FieldDefaultVisibility::Public) } /// Plan 120 (D215) — base parser for named tuple fields. /// /// Plan 124.8 (D215 amend + D222 amend + D225 retract): /// - Multi-line + trailing comma support (skip_newlines after `(`, /// between fields, after comma). /// - `priv`/`pub` modifiers → `E_TUPLE_NO_PRIV` (tuples всегда public). /// - `mut`/`ro` per-field modifiers → `E_TUPLE_NO_PER_FIELD_MOD` /// (mutability — binding-level only, как Rust). /// /// `default_vis` parameter — type-level FieldDefaultVisibility. After /// D225 retract, priv/priv(type) на tuples — error (tuples all-public). /// Called after consuming `(`. Stops before `)`. fn parse_named_tuple_fields_with_default(&mut self, default_vis: crate::ast::FieldDefaultVisibility) -> Result<Vec<NamedTupleField>, Diagnostic> { let mut fields: Vec<NamedTupleField> = Vec::new(); // Plan 124.8: post-D225 retract, type-level priv/priv(type) not allowed для tuples. // If a caller passes non-Public, that's a bug in caller (type-level priv flip // для tuples retracted). Defensive check — emit error if reached. if !matches!(default_vis, crate::ast::FieldDefaultVisibility::Public) { let sp = self.peek().span; return Err(Diagnostic::new( "[E_TUPLE_NO_PRIV] type-level `priv` / `priv(type)` flip для named tuples retracted \ в Plan 124.8 (D225 superseded). Tuples всегда all-public. Use \ `type X value priv { ... }` для stack-allocated record с priv (D226).", sp, )); } loop { self.skip_newlines(); if matches!(self.peek().kind, TokenKind::RParen) { break; } let field_start = self.peek().span; // Plan 124.8 (D222 amend): ban priv/pub modifiers на tuple field. if matches!(self.peek().kind, TokenKind::KwPriv | TokenKind::KwPub) { let sp = self.peek().span; let modifier = if matches!(self.peek().kind, TokenKind::KwPriv) { "priv" } else { "pub" }; return Err(Diagnostic::new( format!( "[E_TUPLE_NO_PRIV] `{}` modifier не разрешён на tuple field \ (Plan 124.8 / D222 amend / D225 retract). Tuples всегда \ all-public по design (pure data carriers, как Rust tuples). \ Если нужна encapsulation на стеке — используй \ `type X value {{ {} f T }}` (D226).", modifier, modifier ), sp, )); } // Plan 124.8 (D215 amend): ban per-field mut/ro modifiers. // Mutability — binding-level only (Rust-style: `mut p` → все // поля mut; `ro p` → all frozen). if matches!(self.peek().kind, TokenKind::KwMut | TokenKind::KwRo) { let sp = self.peek().span; let modifier = if matches!(self.peek().kind, TokenKind::KwMut) { "mut" } else { "ro" }; return Err(Diagnostic::new( format!( "[E_TUPLE_NO_PER_FIELD_MOD] `{}` per-field modifier не разрешён \ на tuple field (Plan 124.8 / D215 amend). Tuples используют \ binding-level mutability (Rust-style): `mut p = Vec3(...)` \ позволяет мутировать все поля; `ro p = ...` блокирует все. \ Если нужен per-field control — используй \ `type X value {{ {} f T }}` record form (D226).", modifier, modifier ), sp, )); } // Plan 124.8: priv_field always false для tuples (no priv allowed). let priv_field = false; // Expect IDENT (field name) if !matches!(self.peek().kind, TokenKind::Ident(_)) { let sp = self.peek().span; return Err(Diagnostic::new( format!( "[E_TUPLE_MIXED_FIELDS] tuple fields must be all named (`name type`) \ or all positional (bare `type`); expected field name (identifier), \ got `{}`", self.peek().kind.name() ), sp, )); } // After the IDENT, must be a type-start; otherwise this is a positional // field smuggled in after named fields (mixed). if !matches!(self.peek_at(1).kind, TokenKind::Ident(_) | TokenKind::LBracket | TokenKind::KwFn | TokenKind::KwRo ) { let sp = self.peek().span; return Err(Diagnostic::new( format!( "[E_TUPLE_MIXED_FIELDS] tuple fields must be all named (`name type`) \ or all positional (bare `type`); field `{}` lacks a type annotation \ (looks like a bare positional type mixed with named fields)", if let TokenKind::Ident(n) = &self.peek().kind { n } else { "?" } ), sp, )); } let (name, _) = self.parse_ident()?; let ty = self.parse_type()?; // D215 amend: optional `= expr` default value for named tuple field. // Parsed like function param defaults (D102 Plan 46). let default = if matches!(self.peek().kind, TokenKind::Eq) { self.bump(); // consume `=` Some(self.with_no_struct_or_trailing(|p| p.parse_expr())?) } else { None }; let span_end = default.as_ref().map(|e| e.span).unwrap_or_else(|| ty.span()); let span = field_start.merge(span_end); fields.push(NamedTupleField { name, ty, span, priv_field, priv_module_field: false, visible_to: Vec::new(), default }); // Plan 124.8 (D215 amend): allow trailing comma + multi-line. // After parsing field — expect either Comma or RParen. // If Comma: skip + skip_newlines → loop top will handle next // field or RParen (trailing comma case). // If RParen: break (end of fields). if self.eat(&TokenKind::Comma).is_none() { break; } self.skip_newlines(); } // Plan 124.8 (D215 amend): skip newlines перед closing `)` для // multi-line форм без trailing comma: // type Vec3( // x f64, // y f64, // z f64 ← no trailing comma → newline → `)` // ) self.skip_newlines(); if fields.is_empty() { let sp = self.peek().span; return Err(Diagnostic::new( "[E_NAMED_TUPLE_EMPTY] named tuple must have at least one field; \ use `type X` (unit/empty-sum) for parameterless types", sp, )); } Ok(fields) } /// Plan 114.4.1 (D200): расширено возвращать tuple /// `(Vec<RecordField>, Vec<AssocConst>)`. Поля типа `const NAME T = expr` /// внутри `type X { ... }` collected отдельно как associated constants — /// НЕ в instance layout, accessible через namespace `Type.NAME`. fn parse_record_fields(&mut self) -> Result<(Vec<RecordField>, Vec<AssocConst>), Diagnostic> { // Backward-compat shim: existing call sites still call без default_priv; // default = Public (D47 unchanged). self.parse_record_fields_with_default(crate::ast::FieldDefaultVisibility::Public) } /// Plan 124 (D220) / Plan 160 (D281): parse record fields с type-level default visibility. /// `default_vis` приходит из `type X priv { ... }` / `type X priv(module) { ... }` syntax /// (parse_type_decl after type-level marker parsing); Public = fields default = public /// (D47 unchanged). Field-level explicit `priv`/`pub` override default. fn parse_record_fields_with_default(&mut self, default_vis: crate::ast::FieldDefaultVisibility) -> Result<(Vec<RecordField>, Vec<AssocConst>), Diagnostic> { let mut fields = Vec::new(); let mut assoc_consts = Vec::new(); self.skip_newlines(); while !matches!(self.peek().kind, TokenKind::RBrace) { // [M-assoc-const-out-of-body-syntax] (D200 AMEND, окно №66): in-body // `const NAME T = expr` внутри `type X { ... }` РЕТРАКТИРОВАНА — // «одна дверь», каноническая форма теперь ВНЕ тела: // `const Type.NAME <Тип> = <значение>` (module-scope, симметрично // `fn Type.new`/`fn Type @method`). Тело типа = ТОЛЬКО instance-layout. if matches!(self.peek().kind, TokenKind::KwConst) { let sp = self.peek().span; return Err(Diagnostic::new( "[E_CONST_IN_BODY_RETRACTED] in-body `const NAME = value` внутри \ тела типа retракти́рована (D200 AMEND, Plan 114.4 окно №66): \ associated const теперь объявляется ВНЕ тела, через квалификатор \ `Type.NAME` — симметрично `fn Type.new`/`fn Type @method`. \ Move `const NAME <Тип> = <значение>` наружу типа как \ `const TypeName.NAME <Тип> = <значение>`." .to_string(), sp, )); } // `export const NAME T = expr` внутри тела — та же ретракция. if matches!(self.peek().kind, TokenKind::KwExport) && matches!(self.peek_at(1).kind, TokenKind::KwConst) { let sp = self.peek().span; return Err(Diagnostic::new( "[E_CONST_IN_BODY_RETRACTED] in-body `export const NAME = value` \ внутри тела типа retракти́рована (D200 AMEND, Plan 114.4 окно №66): \ move наружу как `export const TypeName.NAME <Тип> = <значение>`." .to_string(), sp, )); } // Plan 124.6 (D225): `#visible_to(OtherType[, ...])` field-level // attribute — explicit friend declaration. Methods of listed // types get priv access. Parsed BEFORE priv/pub modifier. let mut visible_to: Vec<String> = Vec::new(); // Plan 180 Ф.6 (D382): `#serde(...)` field-level attributes. let mut field_serde_attrs: Vec<crate::ast::SerdeArg> = Vec::new(); while matches!(self.peek().kind, TokenKind::Hash) { if let TokenKind::Ident(n) = &self.peek_at(1).kind { if n == "serde" { self.parse_serde_attr(&mut field_serde_attrs)?; self.skip_newlines(); continue; } if n == "visible_to" { self.bump(); // # self.bump(); // visible_to if !matches!(self.peek().kind, TokenKind::LParen) { let sp = self.peek().span; return Err(Diagnostic::new( "#visible_to требует list: `#visible_to(TypeX, TypeY, ...)`", sp, )); } self.bump(); // ( loop { match self.peek().kind.clone() { TokenKind::Ident(t) => { visible_to.push(t); self.bump(); } TokenKind::RParen => break, _ => { let sp = self.peek().span; return Err(Diagnostic::new( "ожидался Type-identifier или `)` в #visible_to(...)", sp, )); } } if matches!(self.peek().kind, TokenKind::Comma) { self.bump(); self.skip_newlines(); } else { break; } } self.expect(&TokenKind::RParen)?; if visible_to.is_empty() { let sp = self.peek().span; return Err(Diagnostic::new( "#visible_to требует хотя бы один Type", sp, )); } self.skip_newlines(); continue; } } break; } // Plan 124 (D220): per-field visibility modifier. `priv` или `pub` // (mutually exclusive); idёт ДО mutability modifiers (ro/mut/consume) // — symmetric с module-level `export` keyword position. // // Effective priv_field resolution: // - Explicit `pub` → priv_field = false (overrides type-level default) // - Explicit `priv` → priv_field = true (overrides type-level default) // - Neither → priv_field = default_priv (type-level inherit; D47 default = public) // // Conflict detection: both orders `priv pub` и `pub priv` — explicit // E_PRIV_PUB_CONFLICT error. // D281: field-level `priv` = module-private (same as type-level bare `priv`). // field-level `priv(type)` = type-private (only own methods). // field-level `pub` = public (overrides type-level default). let mut explicit_priv = false; let mut explicit_priv_type = false; // priv(type) = type-private let mut explicit_pub = false; if self.eat(&TokenKind::KwPriv).is_some() { if matches!(self.peek().kind, TokenKind::LParen) { self.bump(); // consume `(` if matches!(self.peek().kind, TokenKind::KwType) { self.bump(); // consume `type` self.expect(&TokenKind::RParen)?; explicit_priv_type = true; } else if matches!(self.peek().kind, TokenKind::KwModule) { let bad_sp = self.peek().span; return Err(crate::diag::Diagnostic::new( "[E_PRIV_QUALIFIER] `priv(module)` is not a valid field modifier. \ Use bare `priv` for module-private, or `priv(type)` for type-private.", bad_sp, )); } else { let bad_sp = self.peek().span; return Err(crate::diag::Diagnostic::new( "[E_PRIV_QUALIFIER] unknown qualifier inside `priv(…)`. \ Valid forms: `priv` (module-private) or `priv(type)` (type-private).", bad_sp, )); } } else { explicit_priv = true; } // detect `priv pub` order — consume `pub` so conflict check fires if matches!(self.peek().kind, TokenKind::KwPub) { self.bump(); explicit_pub = true; } } else { explicit_pub = self.eat(&TokenKind::KwPub).is_some(); // detect `pub priv` order — consume `priv` so conflict check fires if explicit_pub && matches!(self.peek().kind, TokenKind::KwPriv) { self.bump(); explicit_priv = true; } } if (explicit_priv || explicit_priv_type) && explicit_pub { return Err(Diagnostic::new( "[E_PRIV_PUB_CONFLICT] field cannot have both `priv` and `pub` \ modifiers — these are mutually exclusive (Plan 124 / D220).".to_string(), self.peek().span, )); } // Suppress potential mut-warning. let _ = (&mut explicit_priv, &mut explicit_priv_type, &mut explicit_pub); // D281: resolve effective field privacy. // explicit `priv` → module-private (priv_field=true, priv_module_field=true) // explicit `priv(type)` → type-private (priv_field=true, priv_module_field=false) // explicit `pub` → public (priv_field=false, priv_module_field=false) // neither → inherit from type-level FieldDefaultVisibility let (field_priv, field_priv_module) = if explicit_priv { (true, true) } else if explicit_priv_type { (true, false) } else if explicit_pub { (false, false) } else { let is_priv = matches!( default_vis, crate::ast::FieldDefaultVisibility::Private | crate::ast::FieldDefaultVisibility::Module ); let is_module = matches!(default_vis, crate::ast::FieldDefaultVisibility::Module); (is_priv, is_module) }; let mut readonly = false; let mut mutable = false; // Plan 114 (D184) Ф.1.5: `readonly` retracted; `ro` — canonical // short form для read-only field modifier. if matches!(self.peek().kind, TokenKind::KwReadonly) { return Err(Diagnostic::new( "[E_KW_REMOVED_READONLY] `readonly` field modifier \ renamed to `ro` in Plan 114 (D184). Use `ro NAME TYPE` \ instead of `readonly NAME TYPE`. Error code \ E_READONLY_FIELD preserved as stable API. Run \ scripts/tools/plan114_rewrite.py to migrate.".to_string(), self.peek().span, )); } // Plan 114.4.1 (D200) modifier-conflicts: `mut const` / `ro const` // / `consume const` — error before consuming modifier (ambiguous // intent: assoc const vs instance field). if matches!(self.peek().kind, TokenKind::KwRo | TokenKind::KwMut | TokenKind::KwConsume) && matches!(self.peek_at(1).kind, TokenKind::KwConst) { let kw = match self.peek().kind { TokenKind::KwRo => "ro", TokenKind::KwMut => "mut", TokenKind::KwConsume => "consume", _ => unreachable!(), }; let code = match self.peek().kind { TokenKind::KwRo => "E_CONST_RO_REDUNDANT", TokenKind::KwMut => "E_CONST_MUT_CONFLICT", TokenKind::KwConsume => "E_CONST_CONSUME_CONFLICT", _ => unreachable!(), }; return Err(Diagnostic::new( format!("[{code}] cannot combine `{kw}` with `const` field — \ in-body assoc const retракти́рована целиком (D200 AMEND, \ окно №66): move `const NAME <Тип> = <значение>` наружу \ типа как `const TypeName.NAME <Тип> = <значение>` — там \ `{kw}`-конфликта не возникает (модификаторы применимы \ только к instance-полям)."), self.peek().span, )); } if self.eat(&TokenKind::KwRo).is_some() { readonly = true; } else if self.eat(&TokenKind::KwMut).is_some() { mutable = true; } // Plan 100.1 (D133 / D4): `consume field T` — field-level marker для // consume-typed полей. Mutually exclusive с `mut` (consume-fields // меняются только через explicit replace pattern; см. D5.1). let field_consume = self.eat(&TokenKind::KwConsume).is_some(); if field_consume && mutable { return Err(Diagnostic::new( "field cannot be both `mut` and `consume`: consume-fields use \ explicit replace pattern (see D5.1, Plan 100.1). Remove `mut` if \ consume semantics intended, or remove `consume` for shared mutation." .to_string(), self.peek().span, )); } // D39 / Plan 11 Ф.9: `use name Type` (named embed) или // `use _ Type` (anonymous embed). Plan 239 (D443): `use` // контекстный (был `KwUse`) — embed распознаётся по 2-токенному // lookahead: `use` + Ident (alias/`_`) + НЕ-terminator (значит // дальше идёт Type). Без этого — обычное поле с именем `use` // (`use <Type>`: один ident перед разделителем полей). let is_embed = matches!(self.peek().kind, TokenKind::Ident(ref s) if s == "use") && matches!(self.peek_at(1).kind, TokenKind::Ident(_)) && !matches!( self.peek_at(2).kind, TokenKind::Newline | TokenKind::Semicolon | TokenKind::Comma | TokenKind::RBrace | TokenKind::Eof ); if is_embed { self.bump(); // consume `use` } let (name, name_span, anonymous) = if is_embed { // После `use` ожидаем ident (alias name) или `_` для anonymous. let (n, sp) = self.parse_ident()?; if n == "_" { // Anonymous: имя пока пустое, заполним после parse_type // синтетическим `__embed_<TypeName>`. (String::new(), sp, true) } else { (n, sp, false) } } else { let (n, sp) = self.parse_ident()?; (n, sp, false) }; let ty = self.parse_type()?; // Synthesize anonymous embed name на основе типа (для уникальности // в record-схеме и доступа). По convention: `__embed_<TypeName>`. let final_name = if anonymous { let type_name = match &ty { TypeRef::Named { path, .. } => path.join("_"), _ => "Anon".to_string(), }; format!("__embed_{}", type_name) } else { name }; fields.push(RecordField { name: final_name, ty: ty.clone(), readonly, mutable, is_embed, embed_anonymous: anonymous, span: name_span.merge(ty.span()), consume: field_consume, priv_field: field_priv, priv_module_field: field_priv_module, visible_to, serde_attrs: field_serde_attrs, }); // Separator: comma (inline or multi-line) OR newline (multi-line // only). Per D49 + D215 spec: on a SINGLE LINE, a comma is // required between fields; a bare newline is accepted only when // it actually appears. Without either, `{ x int y int }` would // silently parse — now we reject it. if self.eat(&TokenKind::Comma).is_some() { self.skip_newlines(); } else if matches!( self.peek().kind, TokenKind::Newline | TokenKind::Semicolon | TokenKind::RBrace ) { self.skip_newlines(); } else { let sp = self.peek().span; return Err(Diagnostic::new( "[E_RECORD_FIELD_MISSING_SEPARATOR] record fields on the same line must be \ separated by a comma; add `,` after this field, or move the next field to \ a new line", sp, )); } } Ok((fields, assoc_consts)) } /// Plan 172.3 (D310): parse `Member1 | Member2 | …` after the `set` kind-token. /// Члены — TypeRef'ы (конкретные типы по идентичности). Минимум один член. /// /// Inline: `set i8 | i16 | i32` — первый член без `|` /// Многострочный: `set\n | i8 | i16` — `|` обязателен у каждого члена /// включая первый (D310, аналогично D406 `enum`). fn parse_type_set_members(&mut self) -> Result<Vec<TypeRef>, Diagnostic> { let multiline = matches!(self.peek().kind, TokenKind::Newline | TokenKind::Semicolon); self.skip_newlines(); let mut members = Vec::new(); if multiline { // Многострочный: | обязателен у каждого члена включая первый. while matches!(self.peek().kind, TokenKind::Pipe) { self.bump(); // | self.skip_newlines(); members.push(self.parse_type()?); self.skip_newlines(); } if members.is_empty() { let sp = self.peek().span; return Err(Diagnostic::new("expected `|` before first type-set member in multiline form", sp)); } } else { // Inline: первый член без `|`. members.push(self.parse_type()?); while matches!(self.peek().kind, TokenKind::Pipe) { self.bump(); // | self.skip_newlines(); members.push(self.parse_type()?); } } Ok(members) } /// D406: parse sum variants after `enum` kind-token. /// Inline: `enum A | B(T)` — первый вариант без `|` /// Многострочный: `enum\n | A\n | B(T)` — `|` обязателен у каждого включая первый. fn parse_sum_variants_after_enum(&mut self) -> Result<Vec<SumVariant>, Diagnostic> { let multiline = matches!(self.peek().kind, TokenKind::Newline | TokenKind::Semicolon); self.skip_newlines(); if multiline { // Многострочный: делегируем в parse_sum_variants (уже требует | у каждого). let variants = self.parse_sum_variants_list()?; if variants.is_empty() { let sp = self.peek().span; return Err(Diagnostic::new("expected `|` before first variant in multiline enum form", sp)); } Ok(variants) } else { // Inline: первый вариант без `|`. let mut variants = Vec::new(); variants.push(self.parse_one_sum_variant()?); self.skip_newlines(); while matches!(self.peek().kind, TokenKind::Pipe) { self.bump(); // | self.skip_newlines(); variants.push(self.parse_one_sum_variant()?); self.skip_newlines(); } Ok(variants) } } /// Старый синтаксис `type X | A | B` — только `| варианты` (Pipe-led). fn parse_sum_variants(&mut self) -> Result<Vec<SumVariant>, Diagnostic> { self.skip_newlines(); self.parse_sum_variants_list() } /// Pipe-led список вариантов: `| A | B(T) | C { x int }`. /// Используется и в старом синтаксисе и в D406 многострочном. fn parse_sum_variants_list(&mut self) -> Result<Vec<SumVariant>, Diagnostic> { let mut variants = Vec::new(); while matches!(self.peek().kind, TokenKind::Pipe) { self.bump(); // | self.skip_newlines(); variants.push(self.parse_one_sum_variant()?); self.skip_newlines(); } Ok(variants) } /// Парсит один вариант sum-type: `Name` / `Name(T)` / `Name { fields }` / `Name = N`. fn parse_one_sum_variant(&mut self) -> Result<SumVariant, Diagnostic> { // Plan 180 Ф.6 (D382): leading `#serde(...)` variant-level attributes. let mut variant_serde_attrs: Vec<crate::ast::SerdeArg> = Vec::new(); while matches!(self.peek().kind, TokenKind::Hash) { if matches!(&self.peek_at(1).kind, TokenKind::Ident(n) if n == "serde") { self.parse_serde_attr(&mut variant_serde_attrs)?; self.skip_newlines(); } else { break; } } let (name, name_span) = self.parse_ident()?; let kind = match self.peek().kind { TokenKind::LParen => { self.bump(); let mut tys = Vec::new(); while !matches!(self.peek().kind, TokenKind::RParen) { tys.push(self.parse_type()?); if self.eat(&TokenKind::Comma).is_none() { break; } self.skip_newlines(); } self.expect(&TokenKind::RParen)?; SumVariantKind::Tuple(tys) } TokenKind::LBrace => { self.bump(); // Plan 114.4.1 Ф.2 followup: per-variant assoc const // НЕ поддерживается V1 ([M-114.4.1-per-variant-const]). let (fields, _variant_acs) = self.parse_record_fields()?; self.expect(&TokenKind::RBrace)?; SumVariantKind::Record(fields) } _ => SumVariantKind::Unit, }; // Discriminant `= N` / `= -N` (№296: spec/decisions/02-types.md // "Sum-варианты с числовыми discriminants" гласит пример со знаком // `type Sign enum Negative = -1 | Zero = 0 | Positive = 1`; лексер // токенизирует `-1` как `Minus` + `Int(1)` — отдельного // отрицательного int-литерала нет (см. `parse_unary`), значит // discriminant обязан явно съесть optional leading `-`). let discriminant = if self.eat(&TokenKind::Eq).is_some() { let negative = self.eat(&TokenKind::Minus).is_some(); if let TokenKind::Int(n) = self.peek().kind { self.bump(); Some(if negative { -n } else { n }) } else { return Err(Diagnostic::new( "expected integer discriminant", self.peek().span, )); } } else { None }; let end = self.tokens[self.pos.saturating_sub(1)].span; Ok(SumVariant { name, kind, discriminant, span: name_span.merge(end), serde_attrs: variant_serde_attrs, }) } /// Plan 108.4 Ф.1 (D175 amend): parse method signatures from effect or /// protocol body. /// /// `is_protocol = false` (effect mode): bare-ident syntax `name(...)` — /// no receiver, no `@` prefix, no mut/consume qualifier. Backwards-compat. /// /// `is_protocol = true` (protocol mode): methods MUST declare their receiver /// kind explicitly via leading `@`, `mut @`, `consume @` or `.` (static). /// Bare-ident without preceding `@` / `.` → E_PROTO_METHOD_NEEDS_AT error. fn parse_effect_methods(&mut self, is_protocol: bool) -> Result<Vec<EffectMethod>, Diagnostic> { let mut methods = Vec::new(); self.skip_newlines(); while !matches!(self.peek().kind, TokenKind::RBrace) { // Plan 33.3 Ф.9: `axiom <name>(...) => <formula>` обрабатывается // отдельной функцией. Останавливаемся как только видим `axiom`. if let TokenKind::Ident(n) = &self.peek().kind { if n == "axiom" { break; } } // Plan 101.4: `use` keyword внутри method-loop означает попытку // объявить embed после метода (в protocol-теле) или невалидно // в effect/handler контексте — в обоих случаях указываем на // позиционное ограничение для protocol composition. // Plan 239 (D443): `use` контекстный (был `KwUse`) — та же // disambiguation что и у leading embed items ниже: `use` + // Ident (тип) → misplaced embed; `use(` → обычный bare-ident // метод по имени `use` (легитимно в effect-режиме). if matches!(self.peek().kind, TokenKind::Ident(ref s) if s == "use") && matches!(self.peek_at(1).kind, TokenKind::Ident(_)) { let sp = self.peek().span; return Err(Diagnostic::new( "[E_PROTOCOL_EMBED_AFTER_METHOD] `use TypeName` items must \ appear at the start of a protocol body, before any method \ signatures. In effect bodies `use` is not allowed at all.", sp, )); } // Plan 214 (D429 R15): `#coerce` on a protocol/effect method REQUIREMENT // is rejected outright — an attribute on a requirement would hand a pair // to EVERY implementor (mass R3/R11 conflicts). `#coerce` is only legal on // a concrete `fn`/extension-method declaration (parsed in `parse_fn`, // which threads its OWN `coerce_attr` bool onto `FnDecl` — protocol/effect // requirements never become a `FnDecl`, so this is the only checkpoint). if matches!(self.peek().kind, TokenKind::Hash) { if let TokenKind::Ident(n) = &self.peek_at(1).kind { if n == "coerce" { let sp = self.peek().span.merge(self.peek_at(1).span); return Err(Diagnostic::new( "[E_COERCE_ON_PROTOCOL] `#coerce` is not valid on a \ protocol/effect method REQUIREMENT (D429 R15) — it would \ implicitly hand an (I,O) pair to every implementing type, \ guaranteeing mass duplicate-pair conflicts. Declare `#coerce` \ on a concrete `fn`/extension-method instead (the type that \ should coerce, not the protocol it conforms to).", sp, )); } } } // Plan 33.3 Ф.9 (refactor): `#pure` атрибут перед operation. // Раньше использовался keyword `pure_view` — заменили на `#pure` // для consistency с другими `#`-атрибутами Nova. // Разрешён в обоих effect и protocol (axiom + pure_view как // declarative spec; в protocol verification impl'а — V2 через // #verify_impl/#trusted_impl). let op_kind = if matches!(self.peek().kind, TokenKind::Hash) { if let TokenKind::Ident(n) = &self.peek_at(1).kind { if n == "pure" { self.bump(); // # self.bump(); // pure EffectOpKind::PureView } else { EffectOpKind::Operation } } else { EffectOpKind::Operation } } else { EffectOpKind::Operation }; // Plan 108.4 Ф.1 (D175 amend): receiver-qualifier dispatch. // // Protocol mode: leading token determines receiver kind. // `.` → static method (no receiver) // `@` → instance method, read-only receiver // `mut @` → instance method, mutable receiver // `consume @` → instance method, consuming receiver // bare-ident → [E_PROTO_METHOD_NEEDS_AT] error // // Effect mode (is_protocol = false): bare-ident parsing only — // no `@` prefix, no mut/consume qualifier. Backwards-compat. let is_static: bool; let receiver_mut: bool; let receiver_consume: bool; if is_protocol { // Static: leading `.` if matches!(self.peek().kind, TokenKind::Dot) { self.bump(); // consume `.` is_static = true; receiver_mut = false; receiver_consume = false; } else { is_static = false; // Optional `mut` / `consume` receiver qualifier. let mod_span = self.peek().span; let has_mut = self.eat(&TokenKind::KwMut).is_some(); let has_consume = if !has_mut { self.eat(&TokenKind::KwConsume).is_some() } else { false }; // Conflict: `mut consume @` or `consume mut @` — impossible // with single eat, but guard `mut consume` ordering. if has_mut && matches!(self.peek().kind, TokenKind::KwConsume) { let sp = self.peek().span; return Err(Diagnostic::new( "[E_PROTO_METHOD_MOD_CONFLICT] protocol method receiver cannot \ have both `mut` and `consume` qualifiers — they are mutually \ exclusive (D131): `consume` transfers ownership, `mut` mutates \ in place. Use one: `mut @method(...)` or `consume @method(...)`.", mod_span.merge(sp), )); } if has_consume && matches!(self.peek().kind, TokenKind::KwMut) { let sp = self.peek().span; return Err(Diagnostic::new( "[E_PROTO_METHOD_MOD_CONFLICT] protocol method receiver cannot \ have both `consume` and `mut` qualifiers — they are mutually \ exclusive (D131): `consume` transfers ownership, `mut` mutates \ in place. Use one: `mut @method(...)` or `consume @method(...)`.", mod_span.merge(sp), )); } // Expect `@` for instance protocol methods. if self.eat(&TokenKind::At).is_none() { let sp = self.peek().span; return Err(Diagnostic::new( format!( "[E_PROTO_METHOD_NEEDS_AT] protocol method declaration must \ start with `@` (instance) or `.` (static). \ Write `@{}(...)` for a read-only receiver, \ `mut @{}(...)` for a mutable receiver, or \ `consume @{}(...)` for a consuming receiver.", match &self.peek().kind { TokenKind::Ident(n) => n.clone(), _ => "method_name".to_string(), }, match &self.peek().kind { TokenKind::Ident(n) => n.clone(), _ => "method_name".to_string(), }, match &self.peek().kind { TokenKind::Ident(n) => n.clone(), _ => "method_name".to_string(), }, ), sp, )); } receiver_mut = has_mut; receiver_consume = has_consume; } } else { // Effect mode: Plan 97 (Q-static-method-protocol resolved): // leading `.` в effect-теле помечает метод **статическим**. // Bare-имя остаётся instance (backwards-compat). // Для effect-методов leading `.` тоже парсится, но семантически // некорректен — type-checker отвергнёт (followup); парсер // принимает универсально. is_static = self.eat(&TokenKind::Dot).is_some(); receiver_mut = false; receiver_consume = false; } let (name, name_span) = self.parse_ident()?; // Plan 15 (D72): generics — declaration form с optional bounds. let generics: Vec<GenericParam> = if matches!(self.peek().kind, TokenKind::LBracket) { self.parse_generic_decl_params()? } else { Vec::new() }; self.expect(&TokenKind::LParen)?; let mut params = Vec::new(); while !matches!(self.peek().kind, TokenKind::RParen) { params.push(self.parse_param()?); if self.eat(&TokenKind::Comma).is_none() { break; } self.skip_newlines(); } self.expect(&TokenKind::RParen)?; let effects = self.parse_effects_until_arrow_or_body()?; let return_type = if self.eat(&TokenKind::Arrow).is_some() { Some(self.parse_type()?) } else { None }; // Plan 33.5 Ф.5.1: контракты метода эффекта — requires/ensures. // Используются для Liskov-верификации handler'ов (Ф.5.2). let mut contracts: Vec<Contract> = Vec::new(); self.skip_newlines(); loop { let cstart = self.peek().span; // Plan 194 Ф.1: опц. `#debug` перед клаузой (dev-only). let debug_only = self.eat_debug_contract_attr(); match self.peek().kind.clone() { TokenKind::Ident(ref n) if n == "requires" => { self.bump(); let expr = self.parse_expr()?; let (message, message_expr) = self.parse_opt_contract_message()?; let span = cstart.merge(expr.span); contracts.push(Contract { kind: ContractKind::Requires, expr, span, message, message_expr, debug_only }); self.skip_newlines(); } TokenKind::Ident(ref n) if n == "ensures" => { self.bump(); let expr = self.parse_expr()?; let (message, message_expr) = self.parse_opt_contract_message()?; let span = cstart.merge(expr.span); contracts.push(Contract { kind: ContractKind::Ensures, expr, span, message, message_expr, debug_only }); self.skip_newlines(); } _ => { if debug_only { let sp = self.peek().span; return Err(Diagnostic::new( "[E_DEBUG_ATTR_TARGET] `#debug` здесь допустим только перед \ `requires`/`ensures`. См. Plan 194.", sp, )); } break; } } } // Plan 91.8a (D183): default body — optional `=> expr` или `{ ... }` // после return type/contracts. Если присутствует — метод имеет // default impl; type-implementer может override. Если отсутствует — // метод abstract (implementer ОБЯЗАН реализовать). self.skip_newlines(); let default_body: Option<Block> = if matches!(self.peek().kind, TokenKind::FatArrow) { self.bump(); // => let expr = self.parse_expr()?; let span = expr.span; Some(Block { stmts: vec![], trailing: Some(Box::new(expr)), span, is_unsafe: false }) } else if matches!(self.peek().kind, TokenKind::LBrace) { Some(self.parse_block()?) } else { None }; let end = self.tokens[self.pos.saturating_sub(1)].span; // Plan 33.3 Ф.9: `#pure` op обязан иметь return type // (что-то наблюдать). Без `-> R` объявление бессмысленно. if op_kind == EffectOpKind::PureView && return_type.is_none() { return Err(Diagnostic::new( "`#pure` operation must declare a return type \ (`-> <Type>`); without it it observes nothing", name_span.merge(end), )); } methods.push(EffectMethod { name, generics, params, effects, return_type, span: name_span.merge(end), kind: op_kind, contracts, is_static, receiver_mut, receiver_consume, default_body, }); self.skip_newlines(); } Ok(methods) } /// Plan 101.4 (D145 Ред. 5): protocol composition. Парсит тело /// `protocol { ... }`. Сначала собирает leading `use TypeName` items /// (embed'ы другого protocol'а), затем стандартные method-signatures /// через `parse_effect_methods`. `use`-items должны идти В НАЧАЛЕ /// тела (перед методами и axiom'ами) — упрощает грамматику и читаемость. /// Если `use` встречается ПОСЛЕ метода — diagnostic. /// Distinguishing `use TypeName` от метода с именем `use`: после /// `use` обязан идти Ident-тип, а не `(`. Если `use(` — это бы был /// метод (нестандартно, и `use` зарезервирован под embed-кейс). fn parse_protocol_body( &mut self, ) -> Result<(Vec<EffectMethod>, Vec<TypeRef>), Diagnostic> { let mut embeds = Vec::new(); self.skip_newlines(); // Leading `use TypeName` items. Plan 239 (D443): `use` — контекстный // identifier (был `KwUse`), не hard keyword. После `use` обязан // идти Ident (имя типа). Distinguishing от top-level `use` импорта — // здесь мы строго внутри `protocol { ... }`, поэтому семантика // однозначна: embed-protocol. // Поддерживается comma-separated: `use Reader, Writer` (D145 spec), // и линия-на-use: `use Reader\n use Writer` (более читаемо). loop { if matches!(self.peek().kind, TokenKind::Ident(ref s) if s == "use") && matches!(self.peek_at(1).kind, TokenKind::Ident(_)) { self.bump(); // consume `use` embeds.push(self.parse_type()?); // Comma-list continuation: `use A, B, C`. while self.eat(&TokenKind::Comma).is_some() { self.skip_newlines(); embeds.push(self.parse_type()?); } self.skip_newlines(); continue; } break; } // Plan 108.4 Ф.1: protocol methods require `@` / `mut @` / `consume @` // / `.` receiver syntax — pass is_protocol = true. let methods = self.parse_effect_methods(true)?; // Forbid `use` after methods — for clarity. Plan 239 (D443): `use` // контекстный — та же Ident+Ident lookahead disambiguation. self.skip_newlines(); if matches!(self.peek().kind, TokenKind::Ident(ref s) if s == "use") && matches!(self.peek_at(1).kind, TokenKind::Ident(_)) { let sp = self.peek().span; return Err(Diagnostic::new( "[E_PROTOCOL_EMBED_AFTER_METHOD] `use TypeName` items must \ appear at the start of a protocol body, before any method \ signatures", sp, )); } Ok((methods, embeds)) } /// Plan 33.3 Ф.9: `axiom <name>(binders) => <formula>` внутри effect-блока. /// /// `binders` — список идентификаторов без типов (V1; типы выводятся /// из usage). `formula` — обычное Nova-выражение типа `bool`. /// Семантика: глобальное assert'ение, видимое во всех контрактах /// где импортирован эффект. fn parse_effect_axioms(&mut self) -> Result<Vec<EffectAxiom>, Diagnostic> { let mut axioms = Vec::new(); self.skip_newlines(); loop { let is_axiom = matches!( &self.peek().kind, TokenKind::Ident(n) if n == "axiom" ); if !is_axiom { break; } let start = self.peek().span; self.bump(); // consume `axiom` let (ax_name, _) = self.parse_ident()?; // Plan 33.3 (refactor): generic params `axiom name[T](id T) => ...` let generics: Vec<GenericParam> = if matches!(self.peek().kind, TokenKind::LBracket) { self.parse_generic_decl_params()? } else { Vec::new() }; self.expect(&TokenKind::LParen)?; // Typed binders: `axiom name(id int, x str) => ...` // Untyped: `axiom name(id, x) => ...` (type inferred) // Generic refs: `axiom name[T](id T) => ...` → Generic("T") let generic_names: std::collections::HashSet<String> = generics.iter().map(|g| g.name.clone()).collect(); let mut binders: Vec<crate::ast::BinderDef> = Vec::new(); while !matches!(self.peek().kind, TokenKind::RParen) { let (b, b_span) = self.parse_ident()?; // Если следующий токен — не запятая и не ')' — это тип. let kind = if !matches!(self.peek().kind, TokenKind::Comma | TokenKind::RParen) { let ty = self.parse_type()?; // Проверяем: тип = единственный Named{path:[T]} где T generic? if let crate::ast::TypeRef::Named { path, generics: g, .. } = &ty { if g.is_empty() && path.len() == 1 && generic_names.contains(&path[0]) { crate::ast::BinderType::Generic(path[0].clone()) } else { crate::ast::BinderType::Typed(ty) } } else { crate::ast::BinderType::Typed(ty) } } else { crate::ast::BinderType::Untyped }; binders.push(crate::ast::BinderDef { name: b, kind, span: b_span }); if self.eat(&TokenKind::Comma).is_none() { break; } self.skip_newlines(); } self.expect(&TokenKind::RParen)?; self.expect(&TokenKind::FatArrow)?; let formula = self.parse_expr()?; let span = start.merge(formula.span); axioms.push(EffectAxiom { name: ax_name, generics, binders, formula, span, }); self.skip_newlines(); } Ok(axioms) } // ─── let / const / test ────────────────────────────────────────────── fn parse_let_decl(&mut self) -> Result<LetDecl, Diagnostic> { // Plan 114 (D184) Ф.1.5: `let` keyword retracted. Lexer всё ещё // узнаёт лексему — здесь parser отвергает с понятным сообщением // вместо generic 'unknown identifier'. Migrate через // scripts/tools/plan114_rewrite.py (R1-R10). let span = self.peek().span; return Err(Diagnostic::new( "[E_KW_REMOVED_LET] `let` keyword removed in Plan 114 (D184). \ Use `ro X = expr` for immutable binding, `mut X = expr` for \ mutable, `consume X = expr` for owned. For pattern-bind in \ condition use `if Some(x) = e` (drop `let`) or `if ro X = e` \ for identifier-pattern. Run scripts/tools/plan114_rewrite.py to \ migrate.".to_string(), span, )); } #[allow(dead_code)] fn parse_let_decl_legacy(&mut self) -> Result<LetDecl, Diagnostic> { let start = self.peek().span; // Plan 33.3 (D24): `ghost let` / `ghost var` — spec-only binding. // Прификс `ghost` перед `let`/`var`. Контекстный keyword. let is_ghost = if let TokenKind::Ident(n) = &self.peek().kind { if n == "ghost" && matches!(self.peek_at(1).kind, TokenKind::KwLet) { self.bump(); true } else { false } } else { false }; self.expect(&TokenKind::KwLet)?; let mutable = self.eat(&TokenKind::KwMut).is_some(); // Plan 108.3 (D36 amend): запрет group-mut на pattern. // `let mut (a, b) = ...` — `mut` относится к pattern целиком, // что неоднозначно (a mutable? b mutable? оба?). Правильная // форма — per-name: `let (mut a, b) = ...`. if mutable && matches!(self.peek().kind, TokenKind::LParen | TokenKind::LBrace | TokenKind::LBracket) { return Err(Diagnostic::new( "[E_PATTERN_GROUP_MUT] `let mut` не может применяться к pattern \ (tuple/record/array). Используй per-name `mut` внутри pattern: \ `let (mut a, b) = ...` (D36 amend Plan 108.3)." .to_string(), self.peek().span, )); } let pattern = self.parse_pattern()?; let ty = if !matches!(self.peek().kind, TokenKind::Eq) { Some(self.parse_type()?) } else { None }; self.expect(&TokenKind::Eq)?; // Allow newline after `=` so that `let x =\n if ... else ...` works (D49 #5). self.skip_newlines(); let value = self.parse_expr()?; let span = start.merge(value.span); self.expect_newline_or_eof().ok(); // Plan 51 Ф.2: `let x T = T { ... }` — тип объявлен дважды (в // аннотации и в литерале). Тип пишется ровно один раз: каноничные // формы — `let x T = { ... }` либо `let x = T { ... }`. if let Some(TypeRef::Named { path: ann_path, .. }) = &ty { if let ExprKind::RecordLit { type_name: Some(lit_path), .. } = &value.kind { if ann_path == lit_path { return Err(Diagnostic::new( format!( "redundant type prefix on record literal — the `let` \ annotation already declares `{}`; write `= {{ ... }}`", ann_path.join(".")), value.span)); } } } Ok(LetDecl { mutable, pattern, ty, value, span, is_ghost, consume: false, }) } /// Plan 100.1 (D133 / D9): `consume tx = expr` — explicit ownership binding. /// Plan 110 (D188): `consume tx = expr { body }` — scope-block с /// автоматическим вызовом `Cleanup.cleanup` при выходе. /// /// Парсится из `parse_stmt_or_expr` при lookahead KwConsume + Ident/KwMut. /// Lookahead `{` после init expr (с disabled trailing-block) решает между /// scope-block (Stmt::ConsumeScope) и raw form (Stmt::Let). fn parse_consume_decl_or_scope(&mut self) -> Result<Stmt, Diagnostic> { let start = self.peek().span; self.expect(&TokenKind::KwConsume)?; // `consume mut tx` — не валидно: consume = full transfer, // mut-доступ через receiver's `mut` qualifier (D7 100.1). if matches!(self.peek().kind, TokenKind::KwMut) { return Err(Diagnostic::new( "`consume mut tx` is not valid: consume = ownership transfer; \ mut-methods accessed via receiver's `mut` qualifier (D7 100.1)." .to_string(), self.peek().span, )); } // Plan 201 (D188-амендмент 2026-07-13): `consume X { body }` — // re-consume СУЩЕСТВУЮЩЕГО owned-биндинга, statement-позиция // (значение блока отброшено). Интерцепт ДО parse_pattern: иначе // `X {` съедается как record-destructure pattern. Дизамбиг: // строчный IDENT + `{` на ТОЙ ЖЕ строке БЕЗ `=`; `Type { … }` // (заглавная) остаётся record-pattern'ом raw-формы D180. // // Plan 174 (D188-амендмент): `consume A, B, C { body }` — // multi-var re-consume, ЧИСТЫЙ САХАР над вложением. Дизамбиг: // строчный IDENT + `,` на ТОЙ ЖЕ строке — тот же класс формы, // список идентов вместо одного. if let TokenKind::Ident(n) = &self.peek().kind { let lower_ident = n != "_" && !n.chars().next().map(|c| c.is_ascii_uppercase()).unwrap_or(false); if lower_ident && matches!(self.peek_at(1).kind, TokenKind::LBrace) { let (name, name_span) = self.parse_ident()?; let body = self.parse_block()?; let span = start.merge(body.span); // D452 (Plan 264): separator between this statement and the // next is now enforced centrally by the block loop's // `expect_stmt_separator` — no need to (weakly) pre-consume // it here (it used to be `.ok()`, i.e. never enforced // anything, and consuming it early made the block loop see // "no separator" even on legitimately separated code). return Ok(Stmt::ConsumeScope { binding: name.clone(), type_annot: None, init: Expr::new(ExprKind::Ident(name), name_span), body, re_consume: true, result: None, span, }); } if lower_ident && matches!(self.peek_at(1).kind, TokenKind::Comma) { return self.parse_multi_reconsume_scope(start); } } let pattern = self.parse_pattern()?; // Optional type annotation between pattern and `=`. let ty = if !matches!(self.peek().kind, TokenKind::Eq | TokenKind::Newline) { Some(self.parse_type()?) } else { None }; self.expect(&TokenKind::Eq)?; self.skip_newlines(); // Plan 201: `consume s = consume X { body }` — блок-выражение, // result-приёмник с явным consume-keyword. if matches!(self.peek().kind, TokenKind::KwConsume) { let result_name = match &pattern { Pattern::Ident { name, is_mut: false, .. } => name.clone(), _ => { return Err(Diagnostic::new( "Plan 201 (D188): `consume s = consume X { body }` требует \ простого identifier-биндинга слева (без destructure/mut)." .to_string(), self.peek().span, )); } }; return self.parse_reconsume_block_expr_stmt( start, ConsumeScopeResult { name: result_name, mutable: false, declared_consume: true, span: start, }, ); } // Parse init expression с disabled trailing-block чтобы не путать // `init() { body }` с trailing-block call syntax. Struct literals // разрешены (no_struct_lit НЕ устанавливаем — `Config { ... }` // как init остаётся валидным). let saved_trailing = self.no_trailing_block; self.no_trailing_block = true; let value_result = self.parse_expr(); self.no_trailing_block = saved_trailing; let value = value_result?; // Plan 110 D188: lookahead `{` после init expr — scope-block form. // Newline между init и `{` разрешён. let saved_pos = self.pos; self.skip_newlines(); if matches!(self.peek().kind, TokenKind::LBrace) { // Scope-block form требует single-ident immutable binding. let binding = match &pattern { Pattern::Ident { name, is_mut: false, .. } => name.clone(), Pattern::Ident { is_mut: true, .. } => { return Err(Diagnostic::new( "Plan 110 D188: `consume mut X = expr { body }` is not valid; \ scope-block requires immutable single-ident binding." .to_string(), self.peek().span, )); } _ => { return Err(Diagnostic::new( "Plan 110 D188: `consume X = expr { body }` requires single \ identifier binding (destructure not allowed in scope-block; \ use raw `consume X = expr` for linear ownership transfer)." .to_string(), self.peek().span, )); } }; let body = self.parse_block()?; let span = start.merge(body.span); // D452 (Plan 264): see note above — separator enforced by the // caller's `expect_stmt_separator`, not pre-consumed here. return Ok(Stmt::ConsumeScope { binding, type_annot: ty, init: value, body, re_consume: false, result: None, span, }); } // Не scope-block — rewind newlines + raw form (D180). self.pos = saved_pos; let span = start.merge(value.span); Ok(Stmt::Let(LetDecl { mutable: false, pattern, ty, value, span, is_ghost: false, consume: true, })) } /// Plan 174 (D188-амендмент): `consume A, B, C { body }` — multi-var /// re-consume форма, ЧИСТЫЙ САХАР на парс-этапе над вложением: /// `consume A, B, C { body }` ≡ /// `consume A { consume B { consume C { body } } }`. /// Cleanup срабатывает в LIFO-порядке (C, потом B, потом A) — /// естественное следствие вложения, а не отдельная механика. Все /// правила single-формы (owned-требование E_CONSUME_BLOCK_NOT_OWNED, /// Cleanup[E]-требование E_D188_NOT_CLEANUP, guard/дренаж /// E_CONSUME_BLOCK_MOVE_OUT, tail/return-вынос дизармит СВОЙ cleanup) /// действуют на каждый идент независимо — checker/codegen не знают /// про multi-форму, видят только вложенные `Stmt::ConsumeScope`. /// /// Caller уже увидел `IDENT ,` на текущей позиции (после `consume`). /// Список идентов — ТОЛЬКО re-consume форма (без `=`); смешение с /// binding-формой (`consume A, B = expr { … }`) — парс-ошибка. fn parse_multi_reconsume_scope(&mut self, start: Span) -> Result<Stmt, Diagnostic> { let mut idents: Vec<(String, Span)> = Vec::new(); loop { let (name, name_span) = self.parse_ident()?; idents.push((name, name_span)); if self.eat(&TokenKind::Comma).is_some() { continue; } break; } if matches!(self.peek().kind, TokenKind::Eq) { return Err(Diagnostic::new( "[E_CONSUME_MULTIVAR_BINDING_MIX] `consume A, B = expr { … }` \ не валиден: multi-var re-consume форма \ (`consume A, B, C { body }`, Plan 174 D188-амендмент) — \ ЧИСТЫЙ САХАР над вложенным re-consume СУЩЕСТВУЮЩИХ owned-\ биндингов и не поддерживает `=`-инициализацию; binding-\ форма (`consume X = expr { body }`) остаётся одно-идентной." .to_string(), self.peek().span, )); } if !matches!(self.peek().kind, TokenKind::LBrace) { return Err(Diagnostic::new( "Plan 174 (D188): `consume A, B, C` (multi-var re-consume) \ требует блок `{ body }` на той же строке." .to_string(), self.peek().span, )); } let body = self.parse_block()?; let outer_span = start.merge(body.span); // D452 (Plan 264): separator enforced by the caller's // `expect_stmt_separator`, not pre-consumed here. // Desugar (LIFO): последний идент — самый внутренний слой, его // body — РЕАЛЬНОЕ тело пользователя (unchanged, включая свой // trailing/return для собственного tail-дизарма). Каждый следующий // слой снаружи оборачивает предыдущий в единственный statement. let mut inner_body = body; for (name, name_span) in idents.into_iter().rev() { let scope_span = name_span.merge(inner_body.span); let scope_stmt = Stmt::ConsumeScope { binding: name.clone(), type_annot: None, init: Expr::new(ExprKind::Ident(name), name_span), body: inner_body, re_consume: true, result: None, span: scope_span, }; inner_body = Block { stmts: vec![scope_stmt], trailing: None, span: scope_span, is_unsafe: false, }; } // После цикла `inner_body` — Block с ровно ОДНИМ stmt: самый // внешний (первый по списку) `Stmt::ConsumeScope`. Разворачиваем // его наружу — caller (parse_stmt_or_expr) ожидает Stmt, не Block. match inner_body.stmts.into_iter().next() { Some(mut outer @ Stmt::ConsumeScope { .. }) => { if let Stmt::ConsumeScope { span, .. } = &mut outer { *span = outer_span; } Ok(outer) } // Недостижимо: idents гарантированно непусто (parse_ident // выше минимум раз успешно распарсил один идент до `,`/`{`). _ => unreachable!("parse_multi_reconsume_scope: empty ident list"), } } /// Plan 201 (D188-амендмент 2026-07-13): распарсить rvalue-форму /// `consume X { body }` (re-consume block как ВЫРАЖЕНИЕ) в позиции /// значения биндинга: `ro s = consume X { …; X }` / /// `mut s = consume X { … }` / `consume s = consume X { … }`. /// Caller уже распарсил `<kw> s =` и стоит на `consume`. /// Let-биндинг РАСТВОРЯЕТСЯ в `Stmt::ConsumeScope { result: Some(…) }` /// (result-имя объявляется в объемлющем scope после блока). fn parse_reconsume_block_expr_stmt( &mut self, start: Span, result: ConsumeScopeResult, ) -> Result<Stmt, Diagnostic> { self.expect(&TokenKind::KwConsume)?; let (name, name_span) = match self.peek().kind.clone() { TokenKind::Ident(n) if n != "_" && !n.chars().next().map(|c| c.is_ascii_uppercase()).unwrap_or(false) => { self.parse_ident()? } _ => { return Err(Diagnostic::new( "Plan 201 (D188): в rvalue-позиции после `=` ожидается \ re-consume блок `consume X { body }` — `X` = существующий \ owned-биндинг (строчный идентификатор)." .to_string(), self.peek().span, )); } }; if !matches!(self.peek().kind, TokenKind::LBrace) { return Err(Diagnostic::new( "Plan 201 (D188): `consume X` в rvalue-позиции требует блок \ `{ body }` на той же строке (re-consume block-выражение)." .to_string(), self.peek().span, )); } let body = self.parse_block()?; let span = start.merge(body.span); // D452 (Plan 264): separator enforced by the caller's // `expect_stmt_separator`, not pre-consumed here. Ok(Stmt::ConsumeScope { binding: name.clone(), type_annot: None, init: Expr::new(ExprKind::Ident(name), name_span), body, re_consume: true, result: Some(result), span, }) } /// Plan 114 (D184) helper: pattern is structural (constructor / /// destructure form) — `Some(x)`, `(a, b)`, `{ name, age }`, /// `None` (unit variant), `Cons(h, ..)`. Не identifier-only. fn is_structural_pattern(pat: &Pattern) -> bool { matches!(pat, Pattern::Variant { .. } | Pattern::Record { .. } | Pattern::Array { .. } | Pattern::Tuple(_, _)) } /// Plan 114 (D184) helper: pattern — bare identifier (`x`). fn is_ident_pattern(pat: &Pattern) -> bool { matches!(pat, Pattern::Ident { .. }) } /// Plan 114 (D184): `ro X = expr` / `mut X = expr` binding-statement. /// Симметричен `consume X = expr` (Plan 73.1); leading keyword'ом /// определяет mutability, без `let`-prefix'а. `let` retracted. /// /// `is_mut = false` → KwRo; `is_mut = true` → KwMut. /// /// Destructure-pattern (`(a, b)` / `{ name, age }`): leading keyword /// distributes на все имена (`mut (a, b)` → оба mutable). Per-element /// granularity (`(ro a, mut b)`) НЕ вводится (D184 «Out of scope V1»). fn parse_ro_mut_binding(&mut self, is_mut: bool) -> Result<LetDecl, Diagnostic> { let start = self.peek().span; // Plan 33.3 (D24) carry-over: `ghost ro X = …` / `ghost mut X = …` // — spec-only binding. Контекстный keyword `ghost` перед ro/mut. let is_ghost = if let TokenKind::Ident(n) = &self.peek().kind { if n == "ghost" && matches!(self.peek_at(1).kind, TokenKind::KwRo | TokenKind::KwMut) { self.bump(); true } else { false } } else { false }; if is_mut { self.expect(&TokenKind::KwMut)?; } else { self.expect(&TokenKind::KwRo)?; } let pattern = self.parse_pattern()?; // Plan 124.8 (D33/D176 amend) — binding propagation rules: // // `ro`/`mut` binding propagates по default на тип справа. Explicit // повторение модификатора — redundant error. // // | Декларация | Парсится | Семантика | // |---|---|---| // | `ro x T` | ✅ default | binding ro → type ro | // | `mut x T` | ✅ default | binding mut → type mut | // | `ro x ro T` | ❌ E_REDUNDANT_TYPE_MODIFIER | то же что `ro x T` | // | `mut x mut T` | ❌ E_REDUNDANT_TYPE_MODIFIER | то же что `mut x T` | // | `ro x mut T` | ✅ NEW | binding ro, content mut | // | `mut x ro T` | ✅ existing | binding mut, content ro | // // Implementation: // - `ro` после name: redundant if !is_mut; otherwise let parse_type // handle (TypeRef::Readonly wrapper). // - `mut` после name: redundant if is_mut; otherwise consume + ignore // (mut T ≡ T в Nova type system; default mutability). if matches!(self.peek().kind, TokenKind::KwRo) && !is_mut { return Err(Diagnostic::new( "[E_REDUNDANT_TYPE_MODIFIER] `ro` после `ro` binding — \ redundant. `ro` binding автоматически распространяет \ readonly на тип (D33/D176 amend, Plan 124.8). Напиши \ `ro x T` вместо `ro x ro T`.".to_string(), self.peek().span, )); } if matches!(self.peek().kind, TokenKind::KwMut) && is_mut { return Err(Diagnostic::new( "[E_REDUNDANT_TYPE_MODIFIER] `mut` после `mut` binding — \ redundant. `mut` binding автоматически распространяет \ mutability на тип (D33/D176 amend, Plan 124.8). Напиши \ `mut x T` вместо `mut x mut T`.".to_string(), self.peek().span, )); } // **Plan 147 Ф.2 (D246, R2-split):** `ro x mut T` form — binding ro // (L1, reassign ❌), type explicit `mut T` (L2 content-view ✅). The // `mut` is NO LONGER dropped: it must be preserved as a `TypeRef::Mut` // wrapper so the checker can honour the R2-split (content-writable // through a ro binding — `ro r mut Point` → `r.x = v` ✅ / `r = X` ❌). // We do NOT bump here; the `mut` is parsed by `parse_type()`'s KwMut // arm below, mirroring how `mut x ro T` preserves `TypeRef::Readonly`. // (Previously, Plan 124.8 consumed+ignored `mut` under the old // «mut T ≡ T» model; that erased the L2 axis and is reversed here.) let ty = if !matches!(self.peek().kind, TokenKind::Eq) { Some(self.parse_type()?) } else { None }; // Plan 114 (D184) §«binding statements»: bare `ro x` / `mut x` // без init'а — parse error. if !matches!(self.peek().kind, TokenKind::Eq) { return Err(Diagnostic::new( "[E_BINDING_REQUIRES_INIT] `ro` / `mut` binding requires \ initialization. Write `ro x = expr` or `mut x = expr` \ (Plan 114 D184).".to_string(), self.peek().span, )); } self.expect(&TokenKind::Eq)?; self.skip_newlines(); let value = self.parse_expr()?; let span = start.merge(value.span); // D452 (Plan 264): separator enforced by the caller's // `expect_stmt_separator`, not pre-consumed here. // Plan 51 Ф.2: redundant `T = T { … }` (carry-over check). if let Some(TypeRef::Named { path: ann_path, .. }) = &ty { if let ExprKind::RecordLit { type_name: Some(lit_path), .. } = &value.kind { if ann_path == lit_path { let kw = if is_mut { "mut" } else { "ro" }; return Err(Diagnostic::new( format!( "redundant type prefix on record literal — the `{}` \ annotation already declares `{}`; write `= {{ ... }}`", kw, ann_path.join(".")), value.span)); } } } Ok(LetDecl { mutable: is_mut, pattern, ty, value, span, is_ghost, consume: false, }) } fn parse_const_decl(&mut self, is_export: bool, doc: Option<crate::ast::DocBlock>, doc_attrs: Vec<crate::ast::DocAttr>, file_private: bool) -> Result<ConstDecl, Diagnostic> { let start = self.peek().span; self.expect(&TokenKind::KwConst)?; let (mut name, _) = self.parse_ident()?; // [M-assoc-const-out-of-body-syntax] (D200 AMEND, окно №66): каноническая // форма associated const — ВНЕ тела типа, `const Type.NAME <Тип> = <значение>` // (симметрично `fn Type.new` / `fn Type @method` — всё привязанное к типу // объявляется через квалификатор `Type.` снаружи тела). Первый ident может // оказаться типом — если за ним `.`, второй ident — фактическое имя const'а; // qualified name хранится как "Type.NAME" и разбирается downstream'ом // (`imports::attach_out_of_body_assoc_consts`) в `TypeDecl.assoc_consts` // — тот же const-table путь, что и (retракти́рованная) in-body форма. // T-dependent `Box[int].SIZE` — синтаксис на потом ([M-assoc-const-out-of-body-syntax] // followup, не в этом окне): `[` сразу после первого ident НЕ обрабатывается здесь. if matches!(self.peek().kind, TokenKind::Dot) { self.bump(); // . let (const_name, _) = self.parse_ident()?; name = format!("{}.{}", name, const_name); } let ty = if !matches!(self.peek().kind, TokenKind::Eq) { Some(self.parse_type()?) } else { None }; self.expect(&TokenKind::Eq)?; self.skip_newlines(); let value = self.parse_expr()?; let value_span = value.span; // D452 (Plan 264): when this is a scope-local `const` (statement // position), separator is enforced by the caller's // `expect_stmt_separator`; at module level the item loop handles // its own newline/EOF tolerance — either way, not pre-consumed here. Ok(ConstDecl { doc, doc_attrs, is_export, name, ty, value, span: start.merge(value_span), file_private, is_lazy_ro: false, }) } /// Plan 157 (D200 amend): `ro Type.NAME [Type] = expr` — associated /// **ro**-value on a type (out-of-body, qualifier `Type.`), mirroring /// `parse_const_decl`'s out-of-body `const Type.NAME` handling but for /// the `ro` keyword. Produces a `ConstDecl` with `is_lazy_ro: true` so it /// flows through the EXACT SAME `Item::Const` → `imports::attach_out_of_ /// body_assoc_consts` → `TypeDecl.assoc_consts` pipeline as `const /// Type.NAME` (namespace-only access, `E_CONST_INSTANCE_ACCESS`, /// cross-module `export` — all reused unchanged); the ONE semantic /// difference (constexpr-eligibility) is enforced downstream by the /// `is_lazy_ro` flag, not here. Caller (`parse_item`'s `KwRo` arm) has /// already confirmed the `Ident '.' Ident` lookahead before calling this. fn parse_assoc_ro_decl(&mut self, is_export: bool, doc: Option<crate::ast::DocBlock>, doc_attrs: Vec<crate::ast::DocAttr>, file_private: bool) -> Result<ConstDecl, Diagnostic> { let start = self.peek().span; self.expect(&TokenKind::KwRo)?; let (type_name, _) = self.parse_ident()?; self.expect(&TokenKind::Dot)?; let (ro_name, _) = self.parse_ident()?; let name = format!("{}.{}", type_name, ro_name); let ty = if !matches!(self.peek().kind, TokenKind::Eq) { Some(self.parse_type()?) } else { None }; if !matches!(self.peek().kind, TokenKind::Eq) { return Err(Diagnostic::new( "[E_BINDING_REQUIRES_INIT] `ro Type.NAME` requires initialization \ — write `ro Type.NAME <Тип> = expr` (Plan 157, D200 amend).".to_string(), self.peek().span, )); } self.expect(&TokenKind::Eq)?; self.skip_newlines(); let value = self.parse_expr()?; let value_span = value.span; self.expect_newline_or_eof().ok(); Ok(ConstDecl { doc, doc_attrs, is_export, name, ty, value, span: start.merge(value_span), file_private, is_lazy_ro: true, }) } /// Plan 124.6 (D225): parse optional `#test_access(TypeA, TypeB, ...)` attribute /// that may appear immediately before a `test "name" { ... }` block. /// Returns the list of type names (empty if attribute is absent). fn parse_test_access_attr(&mut self) -> Result<Vec<String>, Diagnostic> { // Look for `# test_access` (Hash followed by Ident("test_access")). if !matches!(self.peek().kind, TokenKind::Hash) { return Ok(Vec::new()); } match &self.peek_at(1).kind { TokenKind::Ident(n) if n == "test_access" => {} _ => return Ok(Vec::new()), } self.bump(); // # self.bump(); // test_access if !matches!(self.peek().kind, TokenKind::LParen) { let span = self.peek().span; return Err(Diagnostic::new( "#test_access requires a list: `#test_access(TypeX, TypeY, ...)`", span, )); } self.bump(); // ( let mut names: Vec<String> = Vec::new(); loop { match self.peek().kind.clone() { TokenKind::Ident(n) => { names.push(n); self.bump(); } TokenKind::RParen => break, _ => { let sp = self.peek().span; return Err(Diagnostic::new( "expected type identifier or `)` in #test_access(...)", sp, )); } } if matches!(self.peek().kind, TokenKind::Comma) { self.bump(); self.skip_newlines(); } else { break; } } self.expect(&TokenKind::RParen)?; if names.is_empty() { let sp = self.peek().span; return Err(Diagnostic::new( "#test_access requires at least one type: `#test_access(TypeX, ...)`", sp, )); } self.skip_newlines(); Ok(names) } fn parse_test_decl(&mut self, test_access: Vec<String>) -> Result<TestDecl, Diagnostic> { let start = self.peek().span; self.expect(&TokenKind::KwTest)?; let name = match &self.peek().kind { TokenKind::Str(s) => { let n = s.clone(); self.bump(); n } _ => { return Err(Diagnostic::new( "expected test name as string literal", self.peek().span, )) } }; // Plan 173 Ф.6 (D348): контекстное KW `panics` + строка-паттерн — // инверсия PASS/FAIL (PASS ⇔ тело запаниковало сообщением ⊇ паттерн). // Как `raw`/`bench` — обычный идентификатор вне этой позиции. let panics = if matches!(&self.peek().kind, TokenKind::Ident(s) if s == "panics") { self.bump(); match &self.peek().kind { TokenKind::Str(s) => { let pat = s.clone(); self.bump(); Some(pat) } _ => { return Err(Diagnostic::new( "expected panic-message pattern as string literal after \ `panics` (D348: `test \"имя\" panics \"паттерн\" { … }`; \ пустая строка = любая паника)", self.peek().span, )) } } } else { None }; let body = self.parse_block()?; let body_span = body.span; Ok(TestDecl { name, body, span: start.merge(body_span), test_access, panics, }) } /// Plan 57: `bench "name" { setup_stmts; measure { measured_body } teardown_stmts }` /// /// Внутри body парсим обычные statements; при встрече ключевого /// слова `measure` — переключаемся, парсим один measure-блок, затем /// продолжаем как teardown. /// /// Ровно один `measure { ... }` блок в body. Иначе диагностика. fn parse_bench_decl(&mut self) -> Result<BenchDecl, Diagnostic> { let start = self.peek().span; // Caller already verified `bench` ident + string-literal lookahead; // here consume ident, then string. match &self.peek().kind { TokenKind::Ident(s) if s == "bench" => { self.bump(); } _ => return Err(Diagnostic::new( "expected `bench` keyword", self.peek().span, )), } let name = match &self.peek().kind { TokenKind::Str(s) => { let n = s.clone(); self.bump(); n } _ => { return Err(Diagnostic::new( "expected bench name as string literal", self.peek().span, )) } }; // Plan 57.B.3: optional parameter sweep — `(IDENT in [v1, v2, ...])` // ДО opening brace. let params = if matches!(self.peek().kind, TokenKind::LParen) { let lp_span = self.peek().span; self.bump(); // ( let var_name = match self.peek().kind.clone() { TokenKind::Ident(n) => { self.bump(); n } _ => return Err(Diagnostic::new( "expected parameter name after `(` in bench-sweep", self.peek().span)), }; self.expect(&TokenKind::KwIn)?; self.expect(&TokenKind::LBracket)?; let mut values = Vec::new(); loop { if matches!(self.peek().kind, TokenKind::RBracket) { break; } match self.peek().kind.clone() { TokenKind::Int(n) => { values.push(n); self.bump(); } _ => return Err(Diagnostic::new( "bench-sweep values must be integer literals", self.peek().span)), } if matches!(self.peek().kind, TokenKind::Comma) { self.bump(); } } let rb_span = self.expect(&TokenKind::RBracket)?.span; let rp_span = self.expect(&TokenKind::RParen)?.span; if values.is_empty() { return Err(Diagnostic::new( "bench-sweep values list cannot be empty", lp_span.merge(rp_span))); } Some(BenchParams { var_name, values, span: lp_span.merge(rb_span), }) } else { None }; let brace_open = self.expect(&TokenKind::LBrace)?.span; self.skip_newlines(); // Plan 57.B.5: lookahead — если первый token внутри body — `group`-ident // followed by string-literal, parsing pattern переключается в group mode. let is_group_mode = match &self.peek().kind { TokenKind::Ident(s) if s == "group" && matches!(self.peek_at(1).kind, TokenKind::Str(_)) => true, _ => false, }; if is_group_mode { let mut groups: Vec<BenchGroup> = Vec::new(); while !matches!(self.peek().kind, TokenKind::RBrace) { let g_start = self.peek().span; // expect `group "name" { ... }` match &self.peek().kind { TokenKind::Ident(s) if s == "group" => { self.bump(); } _ => return Err(Diagnostic::new( "expected `group` keyword in bench body (group-mode)", self.peek().span)), } let group_name = match &self.peek().kind { TokenKind::Str(s) => { let n = s.clone(); self.bump(); n } _ => return Err(Diagnostic::new( "expected group name as string literal", self.peek().span)), }; self.expect(&TokenKind::LBrace)?; self.skip_newlines(); let mut cases: Vec<BenchCase> = Vec::new(); while !matches!(self.peek().kind, TokenKind::RBrace) { let c_start = self.peek().span; match &self.peek().kind { TokenKind::Ident(s) if s == "case" => { self.bump(); } _ => return Err(Diagnostic::new( "expected `case` keyword inside `group { ... }`", self.peek().span)), } let case_name = match &self.peek().kind { TokenKind::Str(s) => { let n = s.clone(); self.bump(); n } _ => return Err(Diagnostic::new( "expected case name as string literal", self.peek().span)), }; // Case body = same as bench body (setup; measure { ... }; teardown). let cb_open = self.expect(&TokenKind::LBrace)?.span; self.skip_newlines(); let mut c_setup: Vec<Stmt> = Vec::new(); let mut c_measure: Option<Block> = None; let mut c_teardown: Vec<Stmt> = Vec::new(); while !matches!(self.peek().kind, TokenKind::RBrace) { let is_meas = match &self.peek().kind { TokenKind::Ident(s) if s == "measure" && matches!(self.peek_at(1).kind, TokenKind::LBrace) => true, _ => false, }; if is_meas { if c_measure.is_some() { return Err(Diagnostic::new( "case must contain exactly one `measure { ... }` block", self.peek().span)); } self.bump(); c_measure = Some(self.parse_block()?); self.expect_stmt_separator()?; continue; } let so = self.parse_stmt_or_expr()?; let s = match so { StmtOrExpr::Stmt(s) => s, StmtOrExpr::Expr(e) => Stmt::Expr(e), }; if c_measure.is_none() { c_setup.push(s) } else { c_teardown.push(s) } self.expect_stmt_separator()?; } let cb_close = self.expect(&TokenKind::RBrace)?.span; let c_measure = c_measure.ok_or_else(|| Diagnostic::new( "case body must contain `measure { ... }` block", cb_open.merge(cb_close)))?; cases.push(BenchCase { name: case_name, setup: c_setup, measure_body: c_measure, teardown: c_teardown, span: c_start.merge(cb_close), }); self.skip_newlines(); } let g_close = self.expect(&TokenKind::RBrace)?.span; if cases.is_empty() { return Err(Diagnostic::new( "group must contain at least one `case`", g_start.merge(g_close))); } groups.push(BenchGroup { name: group_name, cases, span: g_start.merge(g_close), }); self.skip_newlines(); } let brace_close = self.expect(&TokenKind::RBrace)?.span; if groups.is_empty() { return Err(Diagnostic::new( "group-mode bench requires at least one `group`", brace_open.merge(brace_close))); } return Ok(BenchDecl { name, setup: Vec::new(), // Placeholder — never used когда groups непустой. measure_body: Block { stmts: Vec::new(), trailing: None, span: brace_open.merge(brace_close), is_unsafe: false }, teardown: Vec::new(), params, groups, span: start.merge(brace_close), }); } // Plain / parameterized mode (existing logic). let mut setup: Vec<Stmt> = Vec::new(); let mut measure_body: Option<Block> = None; let mut teardown: Vec<Stmt> = Vec::new(); while !matches!(self.peek().kind, TokenKind::RBrace) { let is_measure_block = match &self.peek().kind { TokenKind::Ident(s) if s == "measure" && matches!(self.peek_at(1).kind, TokenKind::LBrace) => true, _ => false, }; if is_measure_block { if measure_body.is_some() { return Err(Diagnostic::new( "bench body must contain exactly one `measure { ... }` block", self.peek().span, )); } self.bump(); let mb = self.parse_block()?; measure_body = Some(mb); self.expect_stmt_separator()?; continue; } let so = self.parse_stmt_or_expr()?; let s = match so { StmtOrExpr::Stmt(s) => s, StmtOrExpr::Expr(e) => Stmt::Expr(e), }; if measure_body.is_none() { setup.push(s); } else { teardown.push(s); } self.expect_stmt_separator()?; } let brace_close = self.expect(&TokenKind::RBrace)?.span; let measure_body = measure_body.ok_or_else(|| { Diagnostic::new( "bench body must contain `measure { ... }` block", brace_open.merge(brace_close), ) })?; Ok(BenchDecl { name, setup, measure_body, teardown, params, groups: Vec::new(), span: start.merge(brace_close), }) } // ─── lemma ─────────────────────────────────────────────────────────── /// Plan 33.5 Ф.4.1: `lemma name(params) requires P ensures Q { body }`. /// /// Синтаксис — упрощённый fn без effects/return_type/decreases/modifies. /// Контракты: только `requires` и `ensures` (body доказывает ensures при requires). fn parse_lemma_decl(&mut self) -> Result<LemmaDecl, Diagnostic> { let start = self.expect(&TokenKind::KwLemma)?.span; let name = match self.peek().kind.clone() { TokenKind::Ident(n) => { self.bump(); n } _ => return Err(Diagnostic::new( "expected lemma name", self.peek().span, )), }; // Generics: `[T]` form (optional). let generics = if matches!(self.peek().kind, TokenKind::LBracket) { self.parse_generic_decl_params()? } else { Vec::new() }; // Params. self.expect(&TokenKind::LParen)?; let mut params = Vec::new(); while !matches!(self.peek().kind, TokenKind::RParen) { params.push(self.parse_param()?); if !matches!(self.peek().kind, TokenKind::RParen) { self.expect(&TokenKind::Comma)?; } } self.expect(&TokenKind::RParen)?; // Contracts: requires / ensures (same parsing as in parse_fn). let mut contracts = Vec::new(); loop { self.skip_newlines(); let cstart = self.peek().span; // Plan 194 Ф.1: опц. `#debug` перед клаузой (dev-only). let debug_only = self.eat_debug_contract_attr(); match self.peek().kind.clone() { TokenKind::Ident(ref n) if n == "requires" => { self.bump(); let expr = self.parse_expr()?; let (message, message_expr) = self.parse_opt_contract_message()?; let span = cstart.merge(expr.span); contracts.push(Contract { kind: ContractKind::Requires, expr, span, message, message_expr, debug_only }); } TokenKind::Ident(ref n) if n == "ensures" => { self.bump(); let expr = self.parse_expr()?; let (message, message_expr) = self.parse_opt_contract_message()?; let span = cstart.merge(expr.span); contracts.push(Contract { kind: ContractKind::Ensures, expr, span, message, message_expr, debug_only }); } _ => { if debug_only { let sp = self.peek().span; return Err(Diagnostic::new( "[E_DEBUG_ATTR_TARGET] `#debug` здесь допустим только перед \ `requires`/`ensures`. См. Plan 194.", sp, )); } break; } } } // Body: `=> expr` или `{ ... }` block (как у fn). let (body, end_span) = if matches!(self.peek().kind, TokenKind::FatArrow) { self.bump(); // consume `=>` let expr = self.parse_expr()?; let sp = expr.span; (FnBody::Expr(expr), sp) } else { let b = self.parse_block()?; let sp = b.span; (FnBody::Block(b), sp) }; Ok(LemmaDecl { name, generics, params, contracts, body, span: start.merge(end_span), }) } /// Plan 33.5 Ф.4.2: `calc { expr; == expr; == expr; }`. /// /// Синтаксис: /// calc { /// expr1 ; /// == expr2 ; // или <=, <, >=, > /// == expr3 ; /// } /// /// Первый шаг — просто expr (без отношения). Остальные начинаются с rel. fn parse_calc_stmt(&mut self, start: Span) -> Result<Stmt, Diagnostic> { self.expect(&TokenKind::LBrace)?; let mut steps: Vec<CalcStep> = Vec::new(); loop { self.skip_newlines(); if matches!(self.peek().kind, TokenKind::RBrace) { break; } // Первый шаг — без отношения; последующие начинаются с rel-оператора. let rel = if steps.is_empty() { None } else { // Ожидаем rel-оператор: ==, <=, <, >=, > let rel = match &self.peek().kind { TokenKind::EqEq => { self.bump(); CalcRel::Eq } TokenKind::Le => { self.bump(); CalcRel::Le } TokenKind::Lt => { self.bump(); CalcRel::Lt } TokenKind::Ge => { self.bump(); CalcRel::Ge } TokenKind::Gt => { self.bump(); CalcRel::Gt } _ => return Err(Diagnostic::new( "expected relation operator (==, <=, <, >=, >) in `calc` step", self.peek().span, )), }; Some(rel) }; let expr = self.parse_expr()?; let step_span = expr.span; // Опциональная точка с запятой после выражения. self.skip_newlines(); if matches!(self.peek().kind, TokenKind::Semicolon) { self.bump(); } steps.push(CalcStep { rel, expr, span: step_span }); } let end = self.expect(&TokenKind::RBrace)?.span; if steps.is_empty() { return Err(Diagnostic::new("empty `calc` block", start)); } Ok(Stmt::Calc { steps, span: start.merge(end) }) } // ─── types ─────────────────────────────────────────────────────────── fn parse_type(&mut self) -> Result<TypeRef, Diagnostic> { let start = self.peek().span; // **Plan 138.5 (2026-06-11):** capture-and-clear the pointee context. // Only an immediately-enclosing `*` (Star arm) sets `pointee_ctx` // just before this call; we read it once and reset, so any FURTHER // recursion (a modifier arm parsing its own inner) defaults to the // non-pointee (prefix) context — `*mut mut * T` still errors on the // inner doubled prefix. let is_pointee = self.pointee_ctx; self.pointee_ctx = false; // [M-serde-slice-generic-method-parse]: single-consumption capture, // same pattern as `is_pointee` above — see `receiver_elem_ctx` doc. let no_dotted_path = self.receiver_elem_ctx; self.receiver_elem_ctx = false; match self.peek().kind { // Plan 184 (D326-ревизия, 2026-07-07): `ref` — ОГРАНИЧЕННЫЙ тип // (аналог C++ `T&`), а НЕ «режим передачи». Парсер принимает `ref T` // в любой типовой позиции → `TypeRef::Ref`; ЗАПРЕТ позиций (поля, // коллекции, суммы, Option, тип-аргументы дженериков → Р1 // E_REF_TYPE_POSITION) вводит ЧЕКЕР, т.к. Р6-нормализация (`ref H ≡ // H`) требует знания heap/value, доступного только после резолва. // Формы `ref`/`ro ref`/`mut ref` в ПАРАМЕТРЕ сняты заходом-1 // (E_REF_PARAM_FORM_REMOVED в parse_param) — здесь не воскрешаем. TokenKind::KwRef => { let ref_span = self.bump().span; let inner = self.parse_type()?; let full = ref_span.merge(inner.span()); return Ok(TypeRef::Ref(Box::new(inner), full)); } // Plan 114 (D184) Ф.1.5: `readonly T` renamed to `ro T`. TokenKind::KwReadonly => { return Err(Diagnostic::new( "[E_KW_REMOVED_READONLY] `readonly` type-modifier \ renamed to `ro` in Plan 114 (D184). Use `ro TYPE` \ instead of `readonly TYPE` in param/return/field/binding \ positions. Error codes E_READONLY_* preserved as stable \ API. Run scripts/tools/plan114_rewrite.py to migrate.".to_string(), start, )); } // D176 (Plan 108): compile-time immutability modifier // (Plan 114 keyword: `ro`). TokenKind::KwRo => { // **Plan 147 Ф.2 (D246, 3-axis):** `*ro T` is a HARD ERROR. // Under the three-axis model `*T ≡ *ro T` UNIVERSALLY — a bare // `*T` is already the ro-pointee canon (L3 default = ro), so an // explicit `*ro T` postfix is redundant. The `is_pointee` flag // (set by the enclosing `*` Star arm just before this // `parse_type()` call) tells us we are in the pointee position; // a leading `ro` there is `*ro T`. Emit E_REDUNDANT_POINTER_RO // with a fix-it to the canonical `*T`. (Choice (a): consumers // few, std still forming; no silent rewrite — author must edit.) if is_pointee { let span = self.peek().span; return Err(redundant_pointer_ro_error(span)); } self.bump(); let inner = self.parse_type()?; let span = start.merge(inner.span()); // **Plan 138.5 / D216 V2/V3 simplification (2026-06-11):** // FORBID prefix pointer modifier `ro *` — pointer modifiers // go on the pointee (postfix `*ro T`) or the binding // (`mut x *T`), never before `*`. With the `is_pointee` guard // above already rejecting the postfix `*ro` form, any `ro` // reaching here is a value-level L2 content-view modifier // (`ro Point`) or a forbidden prefix-pointer (`ro * T`). if inner.is_pointer_or_wraps_pointer() { return Err(pointer_prefix_modifier_error("ro", span)); } // **Plan 138.5 §V3.2 flip / §V3.3-§V3.4 retire (2026-06-11):** // value-type modifier ORDER (`ro unsafe T` vs `unsafe ro T`) // is now free — safety and mutability axes commute on a // value-T (`Unsafe(Readonly(T))` ≡ `Readonly(Unsafe(T))`). // The old §V3.2 E_MODIFIER_ORDER + §V3.4 E_REDUNDANT_TYPE_MODIFIER // checks were scaffolding around prefix-pointer propagation; // with prefix forbidden above there is nothing to propagate, // so they are retired. ro+mut value-T conflict (§V3.1) is // still enforced downstream in types/mod.rs. return Ok(TypeRef::Readonly(Box::new(inner), span)); } // **Plan 118.5 Ф.2.1 / D216 V2 §V2.1 (2026-06-04):** universal // right-binding rule — `mut T` type-level modifier. Parses // recursively (same template as KwRo arm above). Distinct от // binding-level `mut x T` (Plan 108) — here it's the **type** // that carries mutability marker. Canonical form для wrapped // pointer: `mut * T` ≡ Mut(Pointer(T)). TokenKind::KwMut => { self.bump(); let inner = self.parse_type()?; let span = start.merge(inner.span()); // **Plan 138.5 (2026-06-11):** FORBID prefix pointer modifier // `mut *` — see KwRo arm. NOTE: binding-level `mut x *T` // (pre-name `mut` consumed by parse_param) is unaffected — // that `mut` never reaches this type-position arm. `is_pointee` // allows the postfix pointee form `*mut *ro T`. if !is_pointee && inner.is_pointer_or_wraps_pointer() { return Err(pointer_prefix_modifier_error("mut", span)); } // **Plan 138.5 §V3.2 flip / §V3.3-§V3.4 retire (2026-06-11):** // value-T modifier order/redundancy checks retired (see KwRo). return Ok(TypeRef::Mut(Box::new(inner), span)); } // **Plan 118.5 Ф.2.2 / D216 V2 §V2.2-§V2.3 (2026-06-04); §10a // rename (Plan 174.5, 2026-07-11):** the possibly-uninit // type-modifier was RENAMED `unsafe` → `uninit` (see the // `KwUninit` arm below) to decouple «possibly-uninit // pointee/value» from «unsafe operation». `unsafe` in type // position is now legal ONLY for the UNRENAMED legacy fn-pointer // composition (D216 §10 «unsafe fn pointer» — encodes // call-requires-unsafe, not possibly-uninit data): postfix // `*unsafe fn(...)` and bare `unsafe fn(...)`. Any other inner // type (data, non-`Func`) is a hard error pointing at `uninit`. TokenKind::KwUnsafe => { self.bump(); let inner = self.parse_type()?; let span = start.merge(inner.span()); // Prefix `unsafe *` (= `Uninit(Pointer(..))`) stays forbidden // regardless of payload — mirrors the KwUninit arm below (and // the pre-rename behaviour for this token). if !is_pointee && inner.is_pointer_or_wraps_pointer() { return Err(pointer_prefix_modifier_error("unsafe", span)); } // §10a: `unsafe` survives in type position ONLY wrapping a // `Func` payload (the D216 §10 fn-pointer-type shape). Any // other inner — migrate to `uninit`. if !matches!(inner, TypeRef::Func { .. }) { return Err(unsafe_type_modifier_renamed_error(span)); } return Ok(TypeRef::Uninit(Box::new(inner), span)); } // **§10a rename (Plan 174.5, 2026-07-11):** `uninit T` — the // possibly-uninit type-level modifier (was `unsafe T`). Parses // recursively (same template as KwRo/KwMut arms above). Marks // T's safety contracts off (init/layout/aliasing/identity) — // MaybeUninit-style first-class wrapper. Two orthogonal axes: // `uninit * T` → Uninit(Pointer(T)) — possibly-null ptr к valid T // `* uninit T` → Pointer(Uninit(T)) — valid ptr к possibly-uninit T // Read enforcement E_UNSAFE_T_READ_REQUIRES_WRAP — Ф.4 work. TokenKind::KwUninit => { self.bump(); let inner = self.parse_type()?; let span = start.merge(inner.span()); // **Plan 138.5 (2026-06-11), carried over §10a:** FORBID // prefix `uninit *` (= `Uninit(Pointer(..))`). This RETIRES // the `Uninit(Pointer)` construction path entirely — the // outer-uninit-pointer form is gone. Rationale (138.5 §1): // (1) std usage = 0; (2) it collided with `Option[*T]` as a // second "nullable mut ptr" spelling; (3) `Option[uninit * T]` // lost NPO → 16B footgun. Nullable is now ONLY `Option[*T]` // (NPO); FFI nullable-uninit is `Option[*uninit T]`. // KEPT: postfix `*uninit T` (= `Pointer(Uninit(T))`, valid // ptr to possibly-uninit T) and the `uninit T` value-wrapper // (§V2.3) — both bottom out at a non-pointer base, so they do // NOT trip is_pointer_or_wraps_pointer(). `is_pointee` allows // the postfix pointee form `*uninit *mut T`. if !is_pointee && inner.is_pointer_or_wraps_pointer() { return Err(pointer_prefix_modifier_error("uninit", span)); } // **Plan 138.5 §V3.4 retire (2026-06-11):** uninit-uninit // redundancy check retired (was prefix-chain scaffolding). return Ok(TypeRef::Uninit(Box::new(inner), span)); } // **Plan 138.5 / D216 V2/V3 simplification (2026-06-11):** the // `safe` type-modifier (propagation-stopper, §V3.4) is RETIRED. // It only ever made sense as a stopper for outer-`unsafe` // propagation inside `unsafe * safe T`; with prefix `unsafe *` // now forbidden there is no propagation to stop, and standalone // `safe T` ≡ `T` was already a no-op. Emit a hard error with a // migration hint. The `safe` token is still produced by the // lexer so the diagnostic is precise (vs "expected type"). TokenKind::KwSafe => { let span = self.peek().span; return Err(Diagnostic::new( "[E_SAFE_RETIRED] the `safe` type-modifier is retired \ (Plan 138.5 / D216 V2/V3 simplification). It was a \ propagation-stopper for the now-forbidden `unsafe *` \ prefix pointer form; with prefix pointer modifiers \ removed there is nothing to stop. Standalone `safe T` \ was always equivalent to `T` — just drop it. Pointer \ pointee-safety is expressed by `*uninit T` (postfix) and \ nullability by `Option[*T]`." .to_string(), span, )); } // Plan 118 D216 §1-3; §10a rename (Plan 174.5, 2026-07-11): // typed pointer family `*T` / `*ro T` / `*mut T` / `*uninit T`. // Modifier `Ro` is default (omitted ≡ ro). // Chain order: `*mut *ro T` = mut pointer на ro pointer на T // (recursive PointerType production, left-to-right). TokenKind::Star => { self.bump(); // eat * // Plan 118 (D216 §1): Rust-import error — `*const T`. Emit // explicit E_INVALID_POINTER_MODIFIER с hint к canonical // Nova syntax (`*ro T` или just `*T`). if matches!(self.peek().kind, TokenKind::KwConst) { let span = self.peek().span; return Err(Diagnostic::new( "[E_INVALID_POINTER_MODIFIER] `*const T` is not valid \ Nova syntax — use `*ro T` (canonical readonly) or \ just `*T` (default readonly per D216 §1). Nova \ pointer modifiers: `ro` / `mut` / `uninit`. \ `const` is a keyword для const declarations, не \ pointer modifier.".to_string(), span, )); } // **Plan 138.5 / D216 V2/V3 final model (2026-06-11); §10a // rename (Plan 174.5, 2026-07-11):** Star is a pure pointer // constructor — NO inline modifier consumption. The // recursive parse_type() call below picks up the (postfix) // pointee modifier via its own right-binding arms, producing // the CANONICAL forms: // `*T` → Pointer(T) (default ≡ ro target) // `*ro T` → Pointer(Readonly(T)) (ro target) // `*mut T` → Pointer(Mut(T)) (writable target) // `*uninit T` → Pointer(Uninit(T)) (ptr to possibly-uninit T) // Plus the UNRENAMED legacy fn-pointer shape `*unsafe fn(...)` // (D216 §10 — call-requires-unsafe, not this rename's scope). // // The pointee modifier (`ro`/`mut`/`uninit`, or legacy // `unsafe` before `fn`) is parsed by the KwRo/KwMut/KwUninit/ // KwUnsafe arms as the recursive `inner`; since that `inner` // bottoms out at a non-pointer base it does NOT trip the // prefix-pointer guard there. The PREFIX forms // (`ro * T` / `mut * T` / `uninit * T`) are now hard errors // (`E_POINTER_PREFIX_MODIFIER`) — pointer reassignability is // a binding concern (`let`/`mut` before the name, D36), not a // type wrapper, and nullability is `Option[*T]` (NPO) only. // // Mark the immediately-following type as the POSTFIX pointee: // a leading `ro`/`mut`/`uninit` here is the pointee modifier // (allowed even if the pointee is itself a pointer, e.g. // `*mut *ro T`), NOT a forbidden prefix. self.pointee_ctx = true; let inner = self.parse_type()?; let span = start.merge(inner.span()); return Ok(TypeRef::Pointer(Box::new(inner), span)); } TokenKind::LBracket => { self.bump(); // []T или [N]T if let TokenKind::Int(n) = self.peek().kind { self.bump(); self.expect(&TokenKind::RBracket)?; // [M-serde-slice-generic-method-parse]: re-arm for the // recursive inner call so the no-dotted-path restriction // survives further `[]`/`[N]` nesting (`[][]T.method`). self.receiver_elem_ctx = no_dotted_path; let inner = self.parse_type()?; let span = start.merge(inner.span()); Ok(TypeRef::FixedArray(n as usize, Box::new(inner), span)) } else { self.expect(&TokenKind::RBracket)?; self.receiver_elem_ctx = no_dotted_path; let inner = self.parse_type()?; let span = start.merge(inner.span()); Ok(TypeRef::Array(Box::new(inner), span)) } } TokenKind::LParen => { self.bump(); if matches!(self.peek().kind, TokenKind::RParen) { let end = self.bump().span; return Ok(TypeRef::Unit(start.merge(end))); } let mut tys = vec![self.parse_type()?]; while self.eat(&TokenKind::Comma).is_some() { tys.push(self.parse_type()?); } let end = self.expect(&TokenKind::RParen)?.span; if tys.len() == 1 { Ok(tys.into_iter().next().unwrap()) } else { Ok(TypeRef::Tuple(tys, start.merge(end))) } } TokenKind::KwFn => { // fn(A, B) E1 E2 -> R — Nova-ABI fn-type (extern_abi = None). self.parse_fn_type_signature(start, None) } // **Plan 174.6 M1 / D353 (2026-07-04):** `extern "C" fn(...)` / // `extern "C" unsafe fn(...)` — C-ABI fn-указательный тип-тег, // синтаксически параллельный объявлению `extern "C" fn` (D282). // Обычно под указателем (`*extern "C" fn(...)`, canonical), но arm // здесь ловит тег в любой type-position. Только ABI `"C"` валиден на // fn-указательном типе (Nova-ABI пишется без тега — просто `fn`); // `extern "nova" fn`-ТИП бессмыслен (= `fn`), отвергается. // `*extern "C" unsafe fn` = `Pointer(Uninit(Func{extern_abi:C}))` — // тег на Func, `unsafe` — постфиксный pointee-модификатор (D216 §10), // зеркалит `*unsafe fn` = `Pointer(Uninit(Func))`. §10a rename // (Plan 174.5, 2026-07-11) НЕ трогает эту композицию — здесь // `unsafe` остаётся `unsafe` (call-requires-unsafe, не // possibly-uninit data); AST-вариант переименован в `Uninit` // (внутреннее имя), но keyword на этом сайте — по-прежнему `unsafe`. TokenKind::KwExtern => { self.bump(); // extern let abi_span = self.peek().span; match &self.peek().kind.clone() { TokenKind::Str(s) if s == "C" => { self.bump(); } TokenKind::Str(other) => { return Err(Diagnostic::new( format!( "[E_FFI_NON_C_ABI_TYPE] fn-указательный ABI-тег `extern \"{}\"` \ не поддержан — на fn-указательном ТИПЕ допустим только \ `extern \"C\" fn(...)` (C-ABI callback, D353). Nova-ABI \ fn-указатель пишется без тега: `*fn(...)`.", other ), abi_span, )); } _ => { return Err(Diagnostic::new( "expected ABI string `\"C\"` after `extern` in fn-pointer type \ (`*extern \"C\" fn(...)`, D353)".to_string(), abi_span, )); } } // Optional `unsafe` between `extern "C"` and `fn` (D216 §10 // composition: `*extern "C" unsafe fn`). §10a rename (Plan // 174.5) does NOT touch this — `unsafe` here stays `unsafe`. // Wrap the Func in a `Uninit` value-wrapper, mirroring the // `*unsafe fn` shape (same AST node the KwUnsafe arm above // produces for the legacy fn-pointer form). let unsafe_kw = self.eat(&TokenKind::KwUnsafe).is_some(); if !matches!(self.peek().kind, TokenKind::KwFn) { let span = self.peek().span; return Err(Diagnostic::new( format!( "expected `fn` after `extern \"C\"`{} in fn-pointer type, got {}", if unsafe_kw { " unsafe" } else { "" }, self.peek().kind.name() ), span, )); } let func = self.parse_fn_type_signature(start, Some("C".to_string()))?; if unsafe_kw { let span = func.span(); Ok(TypeRef::Uninit(Box::new(func), span)) } else { Ok(func) } } TokenKind::Ident(_) => { let mut path = vec![self.parse_ident()?.0]; // [M-serde-slice-generic-method-parse]: in slice-receiver // element position (`[]T`, `[][]T`, …), a following `.` is // NEVER a qualified sub-path continuation of the element // type — it starts the receiver's static-method-name suffix // (`[]T.deserialize[D ...]`, D42 static receiver). Skip the // dotted-path loop so the `.` and what follows are left for // `parse_fn` to consume as the method name. while !no_dotted_path && matches!(self.peek().kind, TokenKind::Dot) && matches!(self.peek_at(1).kind, TokenKind::Ident(_)) { self.bump(); path.push(self.parse_ident()?.0); } let generics = if matches!(self.peek().kind, TokenKind::LBracket) { self.parse_type_args()? } else { Vec::new() }; let end = self.tokens[self.pos.saturating_sub(1)].span; Ok(TypeRef::Named { path, generics, span: start.merge(end), }) } // Plan 97 Ф.2 (D53 §628 / D142): анонимный protocol-тип в // позиции типа — `protocol { method-sig* }`. Body парсится // тем же `parse_effect_methods` с is_protocol = true, что // enforce'ит `@` / `mut @` / `consume @` / `.` syntax. TokenKind::KwProtocol => { self.bump(); self.expect(&TokenKind::LBrace)?; // Plan 108.4 Ф.1: anonymous protocol types also require `@` // receiver prefix — pass is_protocol = true. let methods = self.parse_effect_methods(true)?; let end = self.expect(&TokenKind::RBrace)?.span; Ok(TypeRef::Protocol { methods, span: start.merge(end), }) } _ => Err(Diagnostic::new( format!("expected type, got {}", self.peek().kind.name()), start, )), } } /// **Plan 174.6 M1 / D353:** parse the `fn(A, B) E1 E2 -> R` signature of /// a fn-pointer TYPE, assuming the `fn` keyword is the current token. /// `start` = span of the leading `fn`/`extern` token; `extern_abi` carries /// the D353 ABI-tag (`None` = Nova-ABI `*fn`, `Some("C")` = C-ABI /// `*extern "C" fn`). Shared by the `KwFn` and `KwExtern` arms of /// `parse_type`. fn parse_fn_type_signature( &mut self, start: crate::diag::Span, extern_abi: Option<String>, ) -> Result<TypeRef, Diagnostic> { self.expect(&TokenKind::KwFn)?; self.expect(&TokenKind::LParen)?; let mut params = Vec::new(); while !matches!(self.peek().kind, TokenKind::RParen) { params.push(self.parse_type()?); if self.eat(&TokenKind::Comma).is_none() { break; } } self.expect(&TokenKind::RParen)?; let effects = self.parse_effects_until_arrow_or_body()?; let return_type = if self.eat(&TokenKind::Arrow).is_some() { Some(Box::new(self.parse_type()?)) } else { None }; let end = self.tokens[self.pos.saturating_sub(1)].span; Ok(TypeRef::Func { params, effects, return_type, extern_abi, span: start.merge(end), }) } fn parse_type_args(&mut self) -> Result<Vec<TypeRef>, Diagnostic> { self.expect(&TokenKind::LBracket)?; let mut args = Vec::new(); while !matches!(self.peek().kind, TokenKind::RBracket) { args.push(self.parse_type()?); // Plan 103.9 (D174): `T consume` — consume-typed generic arg. // e.g. `Option[MutexGuard consume]`. Eat the suffix qualifier. self.eat(&TokenKind::KwConsume); if self.eat(&TokenKind::Comma).is_none() { break; } } self.expect(&TokenKind::RBracket)?; Ok(args) } /// Plan 15 (D72): parse generic-DECLARATION params `[name [bound], ...]`. /// /// Используется для declaration-сайтов: free fn `[T Hashable]`, /// type decl `type HashMap[K Hashable, V]`, method-extra-generics /// `fn Repo[T] @bulk[K Ord]`, effect-method generics. /// /// Каждый параметр — простой identifier (имя), за которым может /// идти optional bound (любой тип, обычно protocol). Bound парсится /// если следующий после имени токен НЕ `,`/`]` (т.е. что-то ещё). /// /// Forward-references проверяются ниже type-checker'ом /// (текущий список параметров доступен только слева направо). fn parse_generic_decl_params(&mut self) -> Result<Vec<GenericParam>, Diagnostic> { self.parse_generic_decl_params_inner(false) } /// Plan 153.5 (D263) / [M-153.5-flatten-nested-receiver]: variant that, in /// CARRIER/receiver position (`in_carrier_position = true`), permits a /// NESTED type slot (`Vec[Vec[T]]` → slot `Vec[T]`) — not just a bare /// typevar name. Detection: a carrier slot whose first ident is IMMEDIATELY /// followed by `[` is a nested generic type (`Ident[...]`); the WHOLE slot /// is then parsed with `parse_type` and EVERY free typevar appearing inside /// it is collected as a `GenericParam` (recursively, any depth). Bare-ident /// + bound (`T Printable`) and bare typevar (`T`) slots keep the legacy /// path unchanged, so free-fn `[T Bound = D]` parsing is untouched (the /// non-carrier mode never takes the nested branch). fn parse_generic_decl_params_inner( &mut self, in_carrier_position: bool, ) -> Result<Vec<GenericParam>, Diagnostic> { self.expect(&TokenKind::LBracket)?; let mut params = Vec::new(); // Plan 153.5: structured carrier slot TypeRefs, parallel to `params`' // logical slots (one per `,`-separated slot). Populated only in carrier // mode; read back via `last_carrier_slot_types`. let mut slot_types: Vec<TypeRef> = Vec::new(); self.skip_newlines(); while !matches!(self.peek().kind, TokenKind::RBracket) { // Plan 153.5: carrier-position NESTED type slot. A slot of the form // `Ident[` (ident immediately followed by `[`) is a nested generic // type (`Vec[T]`, `Vec[Vec[T]]`, …), NOT a bare typevar-with-bound. // Parse the whole slot as a type and harvest its free typevars. // Plan 221.1 №88 (iii) / D239: a carrier slot spelled with `[]`-sugar // (`OneBox[[]T]`, `Vec[[]u8]`) — the slot itself starts with `[` // rather than `Ident[`. Same free-typevar harvest as the nested // `Ident[` branch below, then D239-canonicalize (`[]T` ≡ `Vec[T]`) // so the stored slot always matches the shape `unify_type`/call-site // concrete receiver types use (Vec instances always infer as // `Named{Vec,...}`, never `TypeRef::Array` — see `f3_check_member_ctx` // requiring `TypeRef::Named`). Without canonicalization the stored // `Array(...)` slot would never structurally unify against a real // call-site `Vec[...]` receiver, silently breaking dispatch. if in_carrier_position && matches!(self.peek().kind, TokenKind::LBracket) { let slot_ty_raw = self.parse_type()?; let slot_ty = Self::canonicalize_slice_alias(slot_ty_raw); let mut tvars: Vec<(String, Span)> = Vec::new(); Self::collect_free_typevars(&slot_ty, &mut tvars); for (tv, tv_span) in tvars { if !params.iter().any(|p: &GenericParam| p.name == tv) { params.push(GenericParam { name: tv, bounds: Vec::new(), default: None, consume_bound: false, span: tv_span, }); } } slot_types.push(slot_ty); if self.eat(&TokenKind::Comma).is_none() { break; } self.skip_newlines(); continue; } if in_carrier_position && matches!(self.peek().kind, TokenKind::Ident(_)) && matches!(self.peek_at(1).kind, TokenKind::LBracket) { let slot_ty_raw = self.parse_type()?; // Plan 221.1 №88 (iii) / D239: canonicalize any `[]`-sugar // appearing NESTED inside this slot too (`Vec[[]T]`), same // rationale as the sibling branch above. let slot_ty = Self::canonicalize_slice_alias(slot_ty_raw); let mut tvars: Vec<(String, Span)> = Vec::new(); Self::collect_free_typevars(&slot_ty, &mut tvars); for (tv, tv_span) in tvars { // Dedup: a typevar may textually appear once per nested slot; // the same name across slots collapses to one GenericParam. if !params.iter().any(|p: &GenericParam| p.name == tv) { params.push(GenericParam { name: tv, bounds: Vec::new(), default: None, consume_bound: false, span: tv_span, }); } } slot_types.push(slot_ty); if self.eat(&TokenKind::Comma).is_none() { break; } self.skip_newlines(); continue; } let (name, name_span) = self.parse_ident()?; // Plan 153.5: record the structured slot type for carrier mode. A // bare-ident slot (`T`, optionally with a bound) is `Named{[name]}` // — the bound is orthogonal to the receiver shape. if in_carrier_position { slot_types.push(TypeRef::Named { path: vec![name.clone()], generics: Vec::new(), span: name_span, }); } // Plan 100.2 (D156) + №300 (221.1, owner form 2026-08-03): // `[T consume]` — consume-bound marker, optionally followed // DIRECTLY by a protocol-bound chain: `[T consume Hash + Equal]`. // Modifier always comes first, then the `+`-chain of what the // type IMPLEMENTS — linearity itself never joins that chain // (it isn't implemented, it's declared), so there is no `+` // between `consume` and the first bound. let mut consume_bound = false; if matches!(self.peek().kind, TokenKind::KwConsume) { consume_bound = true; self.bump(); } // Bound(s): если следующий токен — не `,`, `]`, `=`, парсим // первый bound. Plan 101.3 (D145 Ред. 5): далее цепочка // `+ Type` для multi-bound `[T A + B + C]` — conjunction // (T satisfies каждый bound). Семантически equivalent // `protocol { use A use B use C }`. №300: то же самое // после `consume` — `[T consume Hash + Equal]`. let mut bounds: Vec<TypeRef> = Vec::new(); if !matches!( self.peek().kind, TokenKind::Comma | TokenKind::RBracket | TokenKind::Eq ) { bounds.push(self.parse_type()?); while matches!(self.peek().kind, TokenKind::Plus) { self.bump(); // consume `+` self.skip_newlines(); bounds.push(self.parse_type()?); } } // Plan 19, C10 (D88): default-значение generic'а через `=`. // Грамматика: `name [bound (+ bound)*] [= default]`. let default = if self.eat(&TokenKind::Eq).is_some() { Some(self.parse_type()?) } else { None }; let end_span = default .as_ref() .map(|t| t.span()) .or_else(|| bounds.last().map(|t| t.span())) .unwrap_or(name_span); params.push(GenericParam { name, bounds, default, consume_bound, span: name_span.merge(end_span), }); if self.eat(&TokenKind::Comma).is_none() { break; } self.skip_newlines(); } // D88 constraint: параметры с default'ом должны идти после // обязательных. Проверяем после сборки. let mut seen_default = false; for p in ¶ms { if p.default.is_some() { seen_default = true; } else if seen_default { return Err(Diagnostic::new( format!( "generic-параметр без default'а `{}` следует после параметра с default — \ параметры с default'ом должны идти последними (D88)", p.name ), p.span, )); } } self.expect(&TokenKind::RBracket)?; // Plan 153.5: publish the structured carrier slots for `parse_fn` to // build `Receiver.receiver_ty`. Non-carrier mode leaves it empty. self.last_carrier_slot_types = if in_carrier_position { slot_types } else { Vec::new() }; Ok(params) } /// Plan 153.5 (D263) / [M-153.5-flatten-nested-receiver]: a name is a free /// typevar (in carrier/receiver position) iff it is a short all-uppercase /// single-segment identifier (`T`, `U`, `K`, `V`, `E`, `TT`, …). Matches the /// existing convention used by the type-checker (`types/mod.rs`: carrier /// typevar detection `len <= 2 && all ascii-uppercase`). fn ident_is_typevar(name: &str) -> bool { !name.is_empty() && name.len() <= 2 && name.chars().all(|c| c.is_ascii_uppercase()) } /// Plan 153.5 (D263): recursively collect every FREE typevar appearing in a /// parsed type, in first-seen order (no dups), as `(name, span)`. Walks /// Named (recursing into its generics), Array, FixedArray, Tuple, Func /// (params + effects + return), Option/Result (which are Named), and the /// transparent wrappers Readonly/Mut/Unsafe/Pointer. Used to harvest the /// typevars a nested carrier slot (`Vec[Vec[T]]`) introduces, at any depth. fn collect_free_typevars(ty: &TypeRef, out: &mut Vec<(String, Span)>) { match ty { TypeRef::Named { path, generics, span } => { if path.len() == 1 && generics.is_empty() && Self::ident_is_typevar(&path[0]) { if !out.iter().any(|(n, _)| n == &path[0]) { out.push((path[0].clone(), *span)); } } for g in generics { Self::collect_free_typevars(g, out); } } TypeRef::Array(inner, _) => Self::collect_free_typevars(inner, out), TypeRef::FixedArray(_, inner, _) => Self::collect_free_typevars(inner, out), TypeRef::Tuple(items, _) => { for it in items { Self::collect_free_typevars(it, out); } } TypeRef::Func { params, effects, return_type, .. } => { for p in params { Self::collect_free_typevars(p, out); } for e in effects { Self::collect_free_typevars(e, out); } if let Some(rt) = return_type { Self::collect_free_typevars(rt, out); } } TypeRef::Readonly(inner, _) | TypeRef::Mut(inner, _) | TypeRef::Uninit(inner, _) | TypeRef::Pointer(inner, _) | TypeRef::Ref(inner, _) => Self::collect_free_typevars(inner, out), TypeRef::Protocol { .. } | TypeRef::Unit(_) => {} } } /// Plan 221.1 №88 (iii) / [D239](../../spec/decisions/02-types.md#d239--t--синтаксический-псевдоним-vect): /// `[]T` is a syntactic alias for `Vec[T]` — canonicalize a CARRIER SLOT /// so `OneBox[[]T]`/`Vec[[]u8]` store the SAME `receiver_ty` shape as /// `OneBox[Vec[T]]`/`Vec[Vec[u8]]`. Thin wrapper over the SHARED /// canonicalizer (`const_fn_trampoline::canonicalize_array_to_vec`) — the /// checker's call-site shape enforcement (Plan 221.1 №88 (i), /// `check_receiver_shape_match`) canonicalizes the CALL-SITE'S inferred /// type with the exact same function before unifying, so both sides MUST /// use one implementation or the two would silently drift. fn canonicalize_slice_alias(ty: TypeRef) -> TypeRef { crate::const_fn_trampoline::canonicalize_array_to_vec(&ty) } /// Plan 153.5 (D263): for a slice-spelled receiver type `Array(Array(... /// Named))` (`[]T`, `[][]T`, `[][][]T`, …) return `(depth, innermost_name)` /// where `depth` counts the `Array` levels and `innermost_name` is the name /// of the innermost `Named` (the element/typevar). Falls back to depth 1 + /// "T" if the innermost is not a single Named (e.g. a tuple element), which /// preserves the legacy `"[]T"` receiver string for the unusual shape. fn slice_receiver_depth_and_inner(ty: &TypeRef) -> (usize, String) { let mut depth = 0usize; let mut cur = ty; loop { match cur { TypeRef::Array(inner, _) => { depth += 1; cur = inner; } TypeRef::Named { path, .. } if path.len() == 1 => { return (depth.max(1), path[0].clone()); } _ => return (depth.max(1), "T".to_string()), } } } /// Plan 15 (D72): convert `Vec<GenericParam>` → `Vec<TypeRef>` для /// receiver / instantiation context. Все params обязаны быть простыми /// именами без bound (bound допустим только в declaration). /// Convert receiver carrier params to `(type_refs, carrier_bounds)`. /// Bounds in carrier position (e.g. `Vec[T Printable]`) are allowed and /// stored in `carrier_bounds` for future enforcement. Defaults remain /// an error (not meaningful in receiver context). fn generic_params_to_type_refs( params: Vec<GenericParam>, ) -> Result<(Vec<TypeRef>, Vec<GenericParam>), Diagnostic> { let mut out = Vec::with_capacity(params.len()); let mut carrier_bounds: Vec<GenericParam> = Vec::new(); for p in params { if let Some(d) = &p.default { return Err(Diagnostic::new( "generic default не разрешён в receiver/instantiation context — \ defaults допустимы только в declaration `[T = Default]` (D88)".to_string(), d.span(), )); } if !p.bounds.is_empty() || p.consume_bound { // Bound in carrier: `fn Vec[T Printable] @method()`. // Store for informational / future enforcement; don't error. carrier_bounds.push(p.clone()); } out.push(TypeRef::Named { path: vec![p.name], generics: Vec::new(), span: p.span, }); } Ok((out, carrier_bounds)) } /// D38 turbofish disambiguation в expression-position. Caller — на токене /// `[`. Speculative-parse: пробуем разобрать `[T1, T2, ...]` как type-args; /// если получилось до `]`, проверяем next token: /// - `(` → call — turbofish (`func[T](args)`) /// - `.` IDENT `(` → method call — turbofish (`Type[T].method(...)`) /// - `?` → try — turbofish (`func[T]?`) /// - иначе → не turbofish, rollback (это Index). /// Если parse_type fails внутри — rollback (Index). Возвращает Some((args, /// end_span_of_RBracket)) при успехе и оставляет позицию **сразу за `]`**; /// возвращает None и оставляет позицию **на `[`** при rollback. /// /// `base_is_type_like`: true когда base-выражение перед `[` может быть /// именем ТИПА (bare `Ident` / `Path`), а не value-выражением. Нужно для /// разрешения неоднозначности `expr[i].method(...)`: статический /// turbofish-вызов `Type[T].method(...)` всегда имеет type-name base, тогда /// как `@buf[i].compare(...)` / `arr[i].m(...)` — это INDEX за которым идёт /// method-call (base — value, не тип). Без этого `@buf[i]` (i как одно- /// именный type-арг) мис-парсился turbofish'ем `@buf::<i>`, теряя индекс. fn try_parse_turbofish_args(&mut self, base_is_type_like: bool) -> Option<(Vec<TypeRef>, Span)> { debug_assert!(matches!(self.peek().kind, TokenKind::LBracket)); let saved_pos = self.pos; // Bump `[` self.bump(); // Empty `[]` нелегально для turbofish — rollback. if matches!(self.peek().kind, TokenKind::RBracket) { self.pos = saved_pos; return None; } let mut args: Vec<TypeRef> = Vec::new(); loop { // Speculative parse_type. Если ошибка — rollback. let before = self.pos; let ty = match self.parse_type() { Ok(t) => t, Err(_) => { self.pos = saved_pos; let _ = before; return None; } }; args.push(ty); if self.eat(&TokenKind::Comma).is_none() { break; } } // Должны быть на `]`. let end_span = match self.peek().kind { TokenKind::RBracket => self.peek().span, _ => { self.pos = saved_pos; return None; } }; // Bump `]`. self.bump(); // Post-`]` continuation check. let is_turbofish = match &self.peek().kind { TokenKind::LParen => true, TokenKind::Question => true, TokenKind::Dot => { // `.` IDENT `(` — method call. Голый `.field` — не turbofish. // Однако `Type[T].method(...)` (static call на generic-типе) — // единственная легальная форма этого continuation, и её base // ВСЕГДА имя типа (`Deque`/`HashMap[..]`/…). `@buf[i].cmp(...)` // и `arr[i].m(...)` имеют value-base — это INDEX + method-call, // НЕ turbofish. Требуем type-like base, иначе одно-именный // type-арг (`[i]`) ложно триггерит turbofish и теряет индекс. base_is_type_like && matches!(self.peek_at(1).kind, TokenKind::Ident(_)) && matches!(self.peek_at(2).kind, TokenKind::LParen) } _ => false, }; if !is_turbofish { self.pos = saved_pos; return None; } Some((args, end_span)) } // ─── expressions ───────────────────────────────────────────────────── /// D48 (2026-07-02): split сырого текста tagged template на текстовые /// сегменты (`parts`, с развёрнутыми escape'ами) и интерполяции /// (`args` = (исходник выражения, byte-offset в сыром тексте) — offset /// нужен для абсолютных span'ов sub-парса). Грамматика D48: /// escape-seq = '\\' ('`' | '\\' | '$' | 'n' | 't'); `\$` — литеральный /// `$` (интерполяцию не открывает); скобки внутри `${…}` считаются по /// глубине (вложенные `{}` в выражении допустимы). /// Инвариант: parts.len() == args.len() + 1. fn split_tagged_template( raw: &str, tok_span: crate::diag::Span, ) -> Result<(Vec<String>, Vec<(String, usize)>), Diagnostic> { let bytes = raw.as_bytes(); let mut parts: Vec<String> = vec![String::new()]; let mut args: Vec<(String, usize)> = Vec::new(); let mut i = 0usize; while i < bytes.len() { let b = bytes[i]; if b == b'\\' && i + 1 < bytes.len() { let cur = parts.last_mut().expect("parts non-empty"); match bytes[i + 1] { b'`' => cur.push('`'), b'\\' => cur.push('\\'), b'$' => cur.push('$'), b'n' => cur.push('\n'), b't' => cur.push('\t'), // Неизвестный escape — сохраняем как есть (permissive, // открытый набор в грамматике D48). other => { cur.push('\\'); cur.push(other as char); } } i += 2; continue; } if b == b'$' && i + 1 < bytes.len() && bytes[i + 1] == b'{' { let src_start = i + 2; let mut depth = 1usize; let mut j = src_start; while j < bytes.len() { match bytes[j] { b'{' => depth += 1, b'}' => { depth -= 1; if depth == 0 { break; } } _ => {} } j += 1; } if depth != 0 { return Err(Diagnostic::new( "[E_TAGGED_TEMPLATE_ARG] незакрытая интерполяция `${` \ в tagged template" .to_string(), tok_span, )); } let inner = raw[src_start..j].trim(); if inner.is_empty() { return Err(Diagnostic::new( "[E_TAGGED_TEMPLATE_ARG] пустая интерполяция `${}` \ в tagged template" .to_string(), tok_span, )); } args.push((raw[src_start..j].to_string(), src_start)); parts.push(String::new()); i = j + 1; continue; } // Обычный символ (UTF-8 — целиком). let ch = raw[i..].chars().next().expect("valid utf8"); parts.last_mut().expect("parts non-empty").push(ch); i += ch.len_utf8(); } Ok((parts, args)) } pub fn parse_expr(&mut self) -> Result<Expr, Diagnostic> { self.parse_implication() } /// Plan 33.1 (D24): `==>` (impl) и `<==>` (iff) — приоритет ниже `||`, /// правоассоциативные. Используются в контрактах. Семантика: /// - `A ==> B` ≡ `!A || B`. /// - `A <==> B` ≡ `A == B` (только для bool). fn parse_implication(&mut self) -> Result<Expr, Diagnostic> { let left = self.parse_or()?; // Right-associative: if we see ==> or <==>, recurse for right side. let op = match self.peek().kind { TokenKind::Implies => BinOp::Implies, TokenKind::Iff => BinOp::Iff, _ => return Ok(left), }; self.bump(); self.skip_newlines(); let right = self.parse_implication()?; let span = left.span.merge(right.span); Ok(Expr::new( ExprKind::Binary { op, left: Box::new(left), right: Box::new(right), }, span, )) } fn parse_or(&mut self) -> Result<Expr, Diagnostic> { let mut left = self.parse_and()?; loop { // D49 newline-tolerance: `or` keyword after newline continues the // expression (`a\nor b`). We do NOT extend this to `||` because // `||` is also the no-arg closure syntax (`|| body`). Allowing a // newline before `||` would mis-parse: // let x = 42 // || closure_body ← new closure, not binary-OR continuation // Use `or` for multi-line logical-OR instead. let saved_pos = self.pos; if matches!(self.peek().kind, TokenKind::Newline) { self.skip_newlines(); // Only `or` keyword is safe to continue after a newline. // `||` after a newline is a new closure expression, not binary OR. if !matches!(self.peek().kind, TokenKind::KwOr) { self.pos = saved_pos; break; } } if !matches!(self.peek().kind, TokenKind::PipePipe | TokenKind::KwOr) { self.pos = saved_pos; break; } self.bump(); self.skip_newlines(); let right = self.parse_and()?; let span = left.span.merge(right.span); left = Expr::new( ExprKind::Binary { op: BinOp::Or, left: Box::new(left), right: Box::new(right), }, span, ); } Ok(left) } fn parse_and(&mut self) -> Result<Expr, Diagnostic> { let mut left = self.parse_eq()?; loop { let saved_pos = self.pos; if matches!(self.peek().kind, TokenKind::Newline) { self.skip_newlines(); } if !matches!(self.peek().kind, TokenKind::AmpAmp | TokenKind::KwAnd) { self.pos = saved_pos; break; } self.bump(); self.skip_newlines(); let right = self.parse_eq()?; let span = left.span.merge(right.span); left = Expr::new( ExprKind::Binary { op: BinOp::And, left: Box::new(left), right: Box::new(right), }, span, ); } Ok(left) } fn parse_eq(&mut self) -> Result<Expr, Diagnostic> { let mut left = self.parse_cmp()?; let mut seen_equality = false; loop { let op = match self.peek().kind { TokenKind::EqEq => BinOp::Eq, TokenKind::BangEq => BinOp::Neq, _ => break, }; // Plan 150 / D248: a SECOND equality operator at this level means a // chained comparison (`a == b == c`) — hard error, same as // relational chains. Paren-aware (see parse_cmp). if seen_equality { return Err(chained_comparison_error(left.span.merge(self.peek().span))); } seen_equality = true; self.bump(); self.skip_newlines(); let right = self.parse_cmp()?; let span = left.span.merge(right.span); left = Expr::new( ExprKind::Binary { op, left: Box::new(left), right: Box::new(right), }, span, ); } Ok(left) } fn parse_cmp(&mut self) -> Result<Expr, Diagnostic> { let mut left = self.parse_bit_or()?; let mut seen_relational = false; loop { let op = match self.peek().kind { TokenKind::Lt => BinOp::Lt, TokenKind::Le => BinOp::Le, TokenKind::Gt => BinOp::Gt, TokenKind::Ge => BinOp::Ge, _ => break, }; // Plan 150 / D248: a SECOND relational operator at this level // means a chained comparison (`a < b < c`, `0 <= i < n`) — hard // error. Paren-aware: `(a < b) < c` consumes the inner `<` inside // the parentheses, so the outer loop sees only one operator here. if seen_relational { return Err(chained_comparison_error(left.span.merge(self.peek().span))); } seen_relational = true; self.bump(); self.skip_newlines(); let right = self.parse_bit_or()?; let span = left.span.merge(right.span); left = Expr::new( ExprKind::Binary { op, left: Box::new(left), right: Box::new(right), }, span, ); } Ok(left) } /// Bitwise-or `|` (level 7 в spec). Не путаем с `||`. fn parse_bit_or(&mut self) -> Result<Expr, Diagnostic> { let mut left = self.parse_bit_xor()?; loop { // D49 newline-tolerance: leading `|` after newline продолжает expression. // Аналогично `||`/`&&` (rule 6). Ценен для multi-line bitwise expr'ов // в hash/codec алгоритмах (base64 / md5 / sha / fnv-style packs). let saved_pos = self.pos; if matches!(self.peek().kind, TokenKind::Newline) { self.skip_newlines(); } if !matches!(self.peek().kind, TokenKind::Pipe) { self.pos = saved_pos; break; } // Plan 114.4.4 V4.6 M2 [M-114.4.4-closure-light-after-const-stmt-parser]: // disambiguation — после newline pattern `|ident<,ident>*|` это ClosureLight // start (новое expression), не binary OR continuation. Lookahead: // peek = `|`, peek_at(1) = Ident OR `_`, then `,` Ident sequence, // ends с `|`. // Если pattern detected — pos restore'им и breakаем (binary OR done). if self.pos != saved_pos && self.looks_like_closure_light_params() { self.pos = saved_pos; break; } self.bump(); self.skip_newlines(); let right = self.parse_bit_xor()?; let span = left.span.merge(right.span); left = Expr::new( ExprKind::Binary { op: BinOp::BitOr, left: Box::new(left), right: Box::new(right), }, span, ); } Ok(left) } /// Plan 114.4.4 V4.6 M2 lookahead: looking at `|`, check whether form is /// `|ident_list|` (ClosureLight params). Returns true if YES — caller can /// stop parsing binary OR continuation. Pos NOT mutated (lookahead only). fn looks_like_closure_light_params(&self) -> bool { // Expected at peek: `|`. Then ident(or `_`), optionally repeated с `,`, // ending с `|`. if !matches!(self.peek().kind, TokenKind::Pipe) { return false; } let mut i: usize = 1; // Empty `||` is parsed as separate PipePipe token; we handle only // non-empty closure params `|x|`, `|x, y|`, `|_|`. loop { // Skip newlines between tokens. while matches!(self.peek_at(i).kind, TokenKind::Newline) { i += 1; } match &self.peek_at(i).kind { TokenKind::Ident(_) => { i += 1; } _ => return false, } while matches!(self.peek_at(i).kind, TokenKind::Newline) { i += 1; } match &self.peek_at(i).kind { TokenKind::Comma => { i += 1; continue; } TokenKind::Pipe => return true, _ => return false, } } } /// Bitwise-xor `^` (level 8). fn parse_bit_xor(&mut self) -> Result<Expr, Diagnostic> { let mut left = self.parse_bit_and()?; loop { // D49 newline-tolerance: leading `^` after newline. let saved_pos = self.pos; if matches!(self.peek().kind, TokenKind::Newline) { self.skip_newlines(); } if !matches!(self.peek().kind, TokenKind::Caret) { self.pos = saved_pos; break; } self.bump(); self.skip_newlines(); let right = self.parse_bit_and()?; let span = left.span.merge(right.span); left = Expr::new( ExprKind::Binary { op: BinOp::BitXor, left: Box::new(left), right: Box::new(right), }, span, ); } Ok(left) } /// Bitwise-and `&` (level 9). Не путаем с `&&`. fn parse_bit_and(&mut self) -> Result<Expr, Diagnostic> { let mut left = self.parse_shift()?; loop { // D49 newline-tolerance: leading `&` after newline. let saved_pos = self.pos; if matches!(self.peek().kind, TokenKind::Newline) { self.skip_newlines(); } if !matches!(self.peek().kind, TokenKind::Amp) { self.pos = saved_pos; break; } self.bump(); self.skip_newlines(); let right = self.parse_shift()?; let span = left.span.merge(right.span); left = Expr::new( ExprKind::Binary { op: BinOp::BitAnd, left: Box::new(left), right: Box::new(right), }, span, ); } Ok(left) } /// Shift `<<` / `>>` (level 10). fn parse_shift(&mut self) -> Result<Expr, Diagnostic> { let mut left = self.parse_range()?; loop { // D49 newline-tolerance: leading `<<`/`>>` after newline. let saved_pos = self.pos; if matches!(self.peek().kind, TokenKind::Newline) { self.skip_newlines(); } let op = match self.peek().kind { TokenKind::Shl => BinOp::Shl, TokenKind::Shr => BinOp::Shr, _ => { self.pos = saved_pos; break; } }; self.bump(); self.skip_newlines(); let right = self.parse_range()?; let span = left.span.merge(right.span); left = Expr::new( ExprKind::Binary { op, left: Box::new(left), right: Box::new(right), }, span, ); } Ok(left) } /// Plan 96 Ф.2 — поддержка 5 форм Range: /// `a..b` / `a..=b` (closed-form, D58) /// `a..` (open-end, Plan 96 D144) /// `..b` / `..=b` (open-start, D144) /// `..` (full, D144) /// Open-ended формы допустимы синтаксически везде; type-checker /// ограничивает их slice-context'ом (`arr[range]`). fn parse_range(&mut self) -> Result<Expr, Diagnostic> { // Открывающее `..` / `..=` без left-operand — open-start форма. let prefix_start_span = self.peek().span; let prefix_dotdot = matches!(self.peek().kind, TokenKind::DotDot | TokenKind::DotDotEq); if prefix_dotdot { let inclusive = matches!(self.peek().kind, TokenKind::DotDotEq); self.bump(); // После `..` / `..=` — либо expression (`..b`), либо конец (`..` // следующий токен — `]`/`)`/`,`/EOF). let end_opt = if self.range_end_follows() { Some(Box::new(self.parse_add()?)) } else { None }; let span = match &end_opt { Some(e) => prefix_start_span.merge(e.span), None => prefix_start_span, }; return Ok(Expr::new( ExprKind::Range { start: None, end: end_opt, inclusive }, span, )); } let left = self.parse_add()?; let inclusive = match self.peek().kind { TokenKind::DotDot => false, TokenKind::DotDotEq => true, _ => return Ok(left), }; self.bump(); // После `expr ..` / `expr ..=` — либо expression (`a..b`), либо // конец (`a..` без правой границы). let right_opt = if self.range_end_follows() { Some(Box::new(self.parse_add()?)) } else { None }; let span = match &right_opt { Some(r) => left.span.merge(r.span), None => left.span, }; Ok(Expr::new( ExprKind::Range { start: Some(Box::new(left)), end: right_opt, inclusive, }, span, )) } /// Plan 96 Ф.2 — есть ли expression после `..` / `..=`? /// Возвращает `false` если следующий токен — конец range-expression /// в контексте: `]`/`)`/`,`/`{`/`;`/EOF/`=>`/etc. fn range_end_follows(&self) -> bool { !matches!( self.peek().kind, TokenKind::RBracket | TokenKind::RParen | TokenKind::Comma | TokenKind::LBrace | TokenKind::RBrace | TokenKind::Semicolon | TokenKind::Eof | TokenKind::FatArrow | TokenKind::Newline ) } fn parse_add(&mut self) -> Result<Expr, Diagnostic> { let mut left = self.parse_mul()?; loop { let op = match self.peek().kind { TokenKind::Plus => BinOp::Add, TokenKind::Minus => BinOp::Sub, _ => break, }; self.bump(); self.skip_newlines(); let right = self.parse_mul()?; let span = left.span.merge(right.span); left = Expr::new( ExprKind::Binary { op, left: Box::new(left), right: Box::new(right), }, span, ); } Ok(left) } fn parse_mul(&mut self) -> Result<Expr, Diagnostic> { let mut left = self.parse_unary()?; loop { let op = match self.peek().kind { TokenKind::Star => BinOp::Mul, TokenKind::Slash => BinOp::Div, TokenKind::Percent => BinOp::Mod, _ => break, }; self.bump(); self.skip_newlines(); let right = self.parse_unary()?; let span = left.span.merge(right.span); left = Expr::new( ExprKind::Binary { op, left: Box::new(left), right: Box::new(right), }, span, ); } Ok(left) } fn parse_unary(&mut self) -> Result<Expr, Diagnostic> { let start = self.peek().span; match self.peek().kind { TokenKind::Minus => { self.bump(); let operand = self.parse_unary()?; let span = start.merge(operand.span); Ok(Expr::new( ExprKind::Unary { op: UnOp::Neg, operand: Box::new(operand), }, span, )) } TokenKind::Bang | TokenKind::KwNot => { self.bump(); let operand = self.parse_unary()?; let span = start.merge(operand.span); Ok(Expr::new( ExprKind::Unary { op: UnOp::Not, operand: Box::new(operand), }, span, )) } // Plan 234 Ф.2 (D46-амендмент): `~x` — побитовое дополнение. // Тот же унарный приоритет, что `!`/унарный `-` (D46 §2: // "Приоритет — унарный, как `!` и унарный `-`"). TokenKind::Tilde => { self.bump(); let operand = self.parse_unary()?; let span = start.merge(operand.span); Ok(Expr::new( ExprKind::Unary { op: UnOp::BitNot, operand: Box::new(operand), }, span, )) } // Plan 118.7 D216 §4 amend: `raw &x` — сырой стек-адрес без // escape analysis / auto-promote. Требует `unsafe {}` (type- // checker enforces E_UNSAFE_REQUIRED). Те же lvalue-ограничения // что и у `&x` (E_AMP_LITERAL, E_ARRAY_INDEX_PTR_BANNED, etc.). // `raw` — контекстное ключевое слово (не зарезервировано в lexer, // аналог `bench`/`measure`): распознаётся только перед `&`. TokenKind::Ident(ref s) if s == "raw" && matches!( self.tokens[self.pos + 1].kind, TokenKind::Amp ) => { self.bump(); // eat `raw` let amp_span = self.peek().span; if !matches!(self.peek().kind, TokenKind::Amp) { return Err(Diagnostic::new( "`raw` must be followed by `&` (e.g. `raw &x`). \ `raw` is not a standalone expression.".to_string(), amp_span, )); } self.bump(); // eat `&` let raw_operand = self.parse_unary()?; // [M-ptr-raw-access-contract-and-unaligned] (D141/D54 амендмент): // `raw &expr as *T` должен ассоциироваться как `(raw &expr) as *T`, // а не `raw &(expr as *T)`. Operand parse (`self.parse_unary()`) // прогоняется через тот же postfix-loop (`parse_postfix`), который // и «as»/«is» обрабатывает — без переассоциации голый trailing // `as` был бы проглочен внутрь operand'а, и lvalue-проверка ниже // видела бы Cast-rvalue вместо реального адресуемого места. Снимаем // ОДИН уровень `As`, адресуем настоящий operand, затем возвращаем // cast поверх результата. let (operand, recast_ty) = match raw_operand.kind { ExprKind::As(inner, ty) => (*inner, Some(ty)), other => (Expr::new(other, raw_operand.span), None), }; // Те же lvalue-проверки что у AddrOf. if matches!(operand.kind, ExprKind::RecordLit { .. }) { return Err(Diagnostic::new( "[E_AMP_RECORD_LITERAL] `raw &Record { ... }` без named \ binding запрещён — use `ro acc = Record { ... }; \ unsafe { raw &acc }`.".to_string(), start, )); } if matches!(operand.kind, ExprKind::Index { .. }) { return Err(Diagnostic::new( "[E_ARRAY_INDEX_PTR_BANNED] `raw &arr[i]` forbidden \ (D216 §15) — array buffer может relocate.".to_string(), start, )); } if matches!( operand.kind, ExprKind::IntLit(_) | ExprKind::FloatLit(_) | ExprKind::BoolLit(_) | ExprKind::CharLit(_) | ExprKind::StrLit(_) ) { return Err(Diagnostic::new( "[E_AMP_LITERAL] `raw &<literal>` forbidden (D216 §15) — \ literals не addressable. Bind в named local first.".to_string(), start, )); } match crate::ast::addr_of_chain_root(&operand.kind) { crate::ast::AddrChainRoot::Lvalue(_) => {} crate::ast::AddrChainRoot::IndexInChain => { return Err(Diagnostic::new( "[E_ARRAY_INDEX_PTR_BANNED] `raw &(...arr[i]...)` forbidden \ (D216 §15) — field path through array index → dangling.".to_string(), start, )); } crate::ast::AddrChainRoot::Rvalue => { return Err(Diagnostic::new( "[E_ADDR_OF_NON_LVALUE] `raw &value` requires an lvalue \ (named binding, field access, or self).".to_string(), start, )); } } let span = start.merge(operand.span); let addr_expr = Expr::new( ExprKind::Unary { op: UnOp::RawAddrOf, operand: Box::new(operand), }, span, ); Ok(match recast_ty { Some(ty) => { let cast_span = span.merge(ty.span()); Expr::new(ExprKind::As(Box::new(addr_expr), ty), cast_span) } None => addr_expr, }) } // Plan 118 D216 §4: `&value` pointer creation (prefix operator, // expr-position only — type-position `&Type` doesn't exist). // Type-checker (Ф.2.3) enforces unsafe context требование + // (Ф.2.4) escape analysis с auto-promote. TokenKind::Amp => { self.bump(); let raw_operand = self.parse_unary()?; // [M-ptr-raw-access-contract-and-unaligned] (D141/D54 амендмент): // `&expr as *T` должен ассоциироваться как `(&expr) as *T`, а не // `&(expr as *T)` — та же re-ассоциация что у `raw &` выше (см. // комментарий там); operand-parse (`self.parse_unary()`) идёт // через тот же postfix-loop, который поглощает trailing `as`. let (operand, recast_ty) = match raw_operand.kind { ExprKind::As(inner, ty) => (*inner, Some(ty)), other => (Expr::new(other, raw_operand.span), None), }; // Plan 118 (D216 §4 amend, user design decision Session 2): // `&Record { ... }` без named binding запрещён — // E_AMP_RECORD_LITERAL. Records в Nova уже heap-allocated // (D32) → `*Record` это double-pointer (Nova_Record**). // Anonymous-local-from-temporary auto-promote слишком // implicit для prod-grade кода. User должен ввести named // local + `&named_local` для clarity: // // ❌ ro p = &Acc { name: "Piter" } // ✓ ro acc = Acc { name: "Piter" }; ro p = &acc // // Aналогично NEG-T2.11 (`&42` literal → E_AMP_LITERAL), // unified design — `&` требует named lvalue. if matches!(operand.kind, ExprKind::RecordLit { .. }) { return Err(Diagnostic::new( "[E_AMP_RECORD_LITERAL] `&Record { ... }` без named \ binding запрещён (Plan 118 D216 §4 amend). Records \ в Nova уже heap-allocated (D32) — `*Record` это \ double-pointer (Nova_Record**) used для FFI \ out-params. Required pattern: \ `ro acc = Record { ... }; ro p = &acc` — \ explicit named local makes storage semantics clear \ для reader.".to_string(), start, )); } // Plan 118 D216 §15: `&arr[i]` forbidden — array buffer // может resize / GC compaction → pointer dangling. Для FFI // buffer access use slice fat-pointer pattern (Plan 118.2) // или `.as_ptr_unsafe()` method (deferred Q-block). if matches!(operand.kind, ExprKind::Index { .. }) { return Err(Diagnostic::new( "[E_ARRAY_INDEX_PTR_BANNED] `&arr[i]` forbidden \ (Plan 118 D216 §15) — array buffer может resize \ (`.push`) или relocate via GC compaction; pointer \ становится dangling. Для FFI buffer access use \ slice fat-pointer pattern (Plan 118.2 — *[T] /\ *ro [T] / *mut [T]) которое carries (ptr, len) pair \ с bounds-tracking.".to_string(), start, )); } // Plan 118 D216 §15: `&<literal>` forbidden — literals // не addressable (no stable storage). User должен bind в // named local + take address of it. if matches!( operand.kind, ExprKind::IntLit(_) | ExprKind::FloatLit(_) | ExprKind::BoolLit(_) | ExprKind::CharLit(_) | ExprKind::StrLit(_) ) { return Err(Diagnostic::new( "[E_AMP_LITERAL] `&<literal>` forbidden \ (Plan 118 D216 §15) — literals (числа, строки, \ bools, chars) не addressable; они не имеют stable \ storage. Bind в named local: \ `ro x = 42; ro p = &x` — explicit local has \ well-defined storage (stack или heap via escape \ analysis auto-promote per D216 §4).".to_string(), start, )); } // Plan 118.1 [M-118.1-addr-of-chains]: `&value` must root in a // named lvalue — the SAME chain-root rule as `addr_of(...)` // (which desugars to this very UnOp::AddrOf). Reject a field path // rooted in a call result / arithmetic (dangling temporary) or // passing through an array index (D216 §15). The top-level // RecordLit / Index / literal cases are already handled above // with their dedicated codes; this catches the chained forms // (`&make().f`, `&arr[i].f`, `&produce()`). match crate::ast::addr_of_chain_root(&operand.kind) { crate::ast::AddrChainRoot::Lvalue(_) => {} crate::ast::AddrChainRoot::IndexInChain => { return Err(Diagnostic::new( "[E_ARRAY_INDEX_PTR_BANNED] `&(...arr[i]...)` forbidden \ (Plan 118 D216 §15) — a field path through an array \ index has an unstable base (buffer resize via `.push` \ / GC compaction) → dangling pointer. Bind the element \ to a named local first.".to_string(), start, )); } crate::ast::AddrChainRoot::Rvalue => { return Err(Diagnostic::new( "[E_ADDR_OF_NON_LVALUE] `&value` requires an lvalue \ (named binding, field access, or self) — rvalue \ expressions (call results, arithmetic, etc.) are not \ addressable. Bind in a named local first.".to_string(), start, )); } } let span = start.merge(operand.span); let addr_expr = Expr::new( ExprKind::Unary { op: UnOp::AddrOf, operand: Box::new(operand), }, span, ); Ok(match recast_ty { Some(ty) => { let cast_span = span.merge(ty.span()); Expr::new(ExprKind::As(Box::new(addr_expr), ty), cast_span) } None => addr_expr, }) } // Plan 118 D216 §5: `*p` explicit deref (prefix in expression // position). Type-position `*T` parsed в parse_type (Ф.1.2). // Type-checker (Ф.4) enforces unsafe context + one-level deref. TokenKind::Star => { self.bump(); let operand = self.parse_unary()?; let span = start.merge(operand.span); Ok(Expr::new( ExprKind::Unary { op: UnOp::Deref, operand: Box::new(operand), }, span, )) } _ => self.parse_postfix(), } } fn parse_postfix(&mut self) -> Result<Expr, Diagnostic> { let mut expr = self.parse_primary()?; loop { match self.peek().kind { TokenKind::Dot => { self.bump(); // .field или .0 (positional). Float-токен `0.0` после // `.` появляется когда лексер увидел `<expr>.0.0` и // съел `.0.0` как `Dot Float(0.0)` — расщепляем // `n.m` обратно в два positional-доступа. let (name, name_span) = match self.peek().kind.clone() { TokenKind::Int(n) => { let sp = self.peek().span; self.bump(); (format!("{}", n), sp) } TokenKind::Float(f) => { // `n.m` — два positional access'а подряд. let sp = self.peek().span; let raw = self.tokens[self.pos].span; let text = self.src_substring(raw); let (first, second) = if !text.is_empty() { let parts: Vec<&str> = text.splitn(2, '.').collect(); if parts.len() != 2 { return Err(Diagnostic::new( "malformed positional access", sp, )); } (parts[0].to_string(), parts[1].to_string()) } else { // Fallback: восстанавливаем по значению // (только для целых частей вроде 0.0, 1.2). let s = format!("{}", f); let parts: Vec<&str> = s.splitn(2, '.').collect(); if parts.len() != 2 { return Err(Diagnostic::new( "malformed positional access", sp, )); } (parts[0].to_string(), parts[1].to_string()) }; self.bump(); // Применяем первый Member, потом второй. let mid = Expr::new( ExprKind::Member { obj: Box::new(expr), name: first, }, sp, ); expr = Expr::new( ExprKind::Member { obj: Box::new(mid), name: second, }, sp, ); continue; } TokenKind::Ident(_) => self.parse_ident()?, TokenKind::At => { // `obj.@method` — bound method value (Plan 11 Ф.4). // `Type.@method` — unbound method value. // Префикс "@" в имени маркирует method-value seman- // тику для codegen (bound vs unbound — по obj kind). let at_span = self.peek().span; self.bump(); // consume @ if !matches!(self.peek().kind, TokenKind::Ident(_)) { return Err(Diagnostic::new( "expected method name after `.@`", self.peek().span, )); } let (mname, mname_span) = self.parse_ident()?; (format!("@{}", mname), at_span.merge(mname_span)) } _ => { return Err(Diagnostic::new( "expected field name or index after `.`", self.peek().span, )); } }; let span = expr.span.merge(name_span); expr = Expr::new( ExprKind::Member { obj: Box::new(expr), name, }, span, ); } TokenKind::LBracket => { // D38 turbofish: `Type[T1, T2].method(...)` или `func[T](args)`. // Disambiguation: speculative parse `[...]` as type-args; если // успешно (все элементы — типы) И post-`]` token — `(`, `.IDENT(` // или `?`, это turbofish; иначе rollback к Index. // // Rationale: index-доступ всегда single-arg expression, // turbofish — N type-args + обязательный postfix-continuation // (call / method-call / try). Multi-arg внутри `[...]` → // однозначно turbofish (Index не имеет comma). // `base_is_type_like`: имя типа (bare `Ident` / `Path`) может // быть receiver статического turbofish-вызова // `Type[T].method(...)`. Value-base (`@buf`, `arr[i]`, // `f().x`, …) НЕ может — там `[...]` это Index. Гейтит только // `.IDENT(` continuation внутри try_parse_turbofish_args; // `[T](args)` / `[T]?` остаются доступны любому base // (`req.body.parse[T]()` и т.п.). let base_is_type_like = matches!( &expr.kind, ExprKind::Ident(_) | ExprKind::Path(_) ); if let Some((type_args, end_span)) = self.try_parse_turbofish_args(base_is_type_like) { expr = Expr::new( ExprKind::TurboFish { base: Box::new(expr.clone()), type_args, }, expr.span.merge(end_span), ); } else { self.bump(); let index = self.parse_expr()?; let end = self.expect(&TokenKind::RBracket)?.span; expr = Expr::new( ExprKind::Index { obj: Box::new(expr.clone()), index: Box::new(index), }, expr.span.merge(end), ); } } TokenKind::LParen => { self.bump(); let mut args: Vec<CallArg> = Vec::new(); self.skip_newlines(); while !matches!(self.peek().kind, TokenKind::RParen) { // Plan 14 Ф.6 (D69): `...expr` в call-args — spread. // Mirroring parse_array_lit pattern: check DotDotDot // BEFORE parse_expr (parse_expr не понимает `...` // как prefix-operator). if self.eat(&TokenKind::DotDotDot).is_some() { let inner = self.parse_expr()?; args.push(CallArg::Spread(inner)); } else if matches!(self.peek().kind, TokenKind::Ident(_)) && matches!( self.tokens.get(self.pos + 1).map(|t| &t.kind), Some(TokenKind::Colon) ) { // Plan 46 (D102): `name: expr` — именованный // аргумент. Внутри `(...)` вызова `ident ':'` // всегда named-arg (коллизии с record-литералом // нет — record это `Имя { ... }`). let (arg_name, _) = self.parse_ident()?; self.bump(); // : self.skip_newlines(); let value = self.parse_call_arg_value()?; args.push(CallArg::Named { name: arg_name, value }); } else { args.push(CallArg::Item(self.parse_call_arg_value()?)); } if self.eat(&TokenKind::Comma).is_some() { self.skip_newlines(); } else { break; } } let end = self.expect(&TokenKind::RParen)?.span; // Trailing-конструкция после `)`. Plan 19 D43-rev: // - `{` → trailing-block (без params), либо legacy // `{ x => body }` (с params, до миграции). // - `fn` → trailing-fn `f(args) fn(p) body`. // Skip when inside match-scrutinee context // (see no_trailing_block flag) — `match f(x) { ... }` // should be parsed as `match` over `f(x)`, not // `f(x){...}` call-with-block. let trailing = if !self.no_trailing_block { match self.peek().kind { TokenKind::LBrace => { // Plan 19, C13: trailing-block только // без params (D43-rev). LegacyBlockWithParams // удалён из parser-path (сам enum-вариант // оставлен для совместимости с тестами, // но parser его больше не создаёт). let tb = self.parse_trailing_block()?; Some(crate::ast::Trailing::Block(Box::new(tb.body))) } // Plan 19, C4: trailing-fn `fn(p) body`. // Парсим как closure-full без имени: // переиспользуем parse_closure_full и // распаковываем результат в FnSigBody. TokenKind::KwFn => { let fn_start = self.peek().span; let cf_expr = self.parse_closure_full(fn_start)?; let ExprKind::ClosureFull(sb) = cf_expr.kind else { unreachable!( "parse_closure_full must produce ExprKind::ClosureFull" ); }; Some(crate::ast::Trailing::Fn(sb)) } _ => None, } } else { None }; let span = expr.span.merge(end); expr = Expr::new( ExprKind::Call { func: Box::new(expr), args, trailing, }, span, ); } TokenKind::Backtick(_) => { // D48 (2026-07-02): tagged template `tag`text${e}text`` — // ДЕСУГАРИТСЯ в обычный вызов `tag([parts...], [args...])` // прямо в парсере (§3 «одно окно»: tag-функция резолвится // как любая fn — чекер/codegen видят ординарный Call, без // спец-путей). parts.len() == args.len() + 1 (инвариант D48). let tok = self.bump(); let tok_span = tok.span; let TokenKind::Backtick(tpl) = tok.kind else { unreachable!() }; // Сырой текст начинается после открывающего бэктика. let raw_base = tok_span.start + 1; let file_id = tok_span.file_id; let (parts, arg_srcs) = Self::split_tagged_template(&tpl, tok_span)?; let mut arg_exprs: Vec<Expr> = Vec::with_capacity(arg_srcs.len()); for (src_text, rel_off) in &arg_srcs { let abs_off = raw_base + rel_off; let mut toks = crate::lexer::lex_with_file_id(src_text, file_id)?; // Сдвиг span'ов токенов на абсолютную позицию // интерполяции — диагностики указывают в файл. for t in &mut toks { t.span.start += abs_off; t.span.end += abs_off; } let mut sub = Parser::with_src(toks, src_text.clone()); let e = sub.parse_expr()?; if !matches!(sub.peek().kind, TokenKind::Eof | TokenKind::Newline) { return Err(Diagnostic::new( "[E_TAGGED_TEMPLATE_ARG] интерполяция `${…}` в tagged \ template должна содержать РОВНО одно выражение" .to_string(), e.span, )); } arg_exprs.push(e); } let span = expr.span.merge(tok_span); let parts_arr = Expr::new( ExprKind::ArrayLit( parts .into_iter() .map(|p| { crate::ast::ArrayElem::Item(Expr::new( ExprKind::StrLit(p), tok_span, )) }) .collect(), ), tok_span, ); let args_arr = Expr::new( ExprKind::ArrayLit( arg_exprs .into_iter() .map(crate::ast::ArrayElem::Item) .collect(), ), tok_span, ); expr = Expr::new( ExprKind::Call { func: Box::new(expr), args: vec![ crate::ast::CallArg::Item(parts_arr), crate::ast::CallArg::Item(args_arr), ], trailing: None, }, span, ); } TokenKind::Question => { self.bump(); let span = expr.span; expr = Expr::new(ExprKind::Try(Box::new(expr)), span); } // Plan 19, C7 (D85): `expr!!` postfix-throw оператор. // Лексер не объединяет `!!` в один токен (было бы // конфликтно с prefix `!!cond` = `!(!cond)`); вместо // этого парсер ловит два Bang подряд в postfix-position. // В postfix `expr` уже распарсен как operand, поэтому // `expr!!` однозначно postfix-throw. // // На Some(v)/Ok(v) разворачивает; на None/Err(e) // бросает через Fail[E]. TokenKind::Bang if matches!(self.peek_at(1).kind, TokenKind::Bang) => { self.bump(); // first '!' self.bump(); // second '!' let span = expr.span; expr = Expr::new(ExprKind::Bang(Box::new(expr)), span); } TokenKind::Question2 => { self.bump(); // [E_COALESCE_RETURN_FALLBACK] (D86 AMEND 2026-07-23, // ретракция формы `X ?? return R`): парсер ПРИНИМАЕТ // форму в AST (rustc-style parse-then-diagnose) — сам // парсер типов не знает, чем является `X` и что // возвращает объемлющая fn, поэтому подсказка (`?` / // `.ok()?` / `.map_err(..)?` / `.ok_or(..)?`) строится // контекстно в чекере (см. `check_coalesce_return_ // fallback` в `types/mod.rs`), который ВСЕГДА отвергает // эту форму диагностикой `E_COALESCE_RETURN_FALLBACK`. // `return` принимается ТОЛЬКО здесь — непосредственно // справа от `??` — нигде больше в expression-position // `return` не валиден (см. `ExprKind:: // CoalesceReturnFallback` doc-комментарий). if matches!(self.peek().kind, TokenKind::KwReturn) { let ret_start = self.bump().span; // `return` let value = if self.at_newline() || matches!(self.peek().kind, TokenKind::RBrace) { None } else { Some(Box::new(self.parse_unary()?)) }; let end = value.as_ref().map(|v| v.span).unwrap_or(ret_start); let ret_span = ret_start.merge(end); let right = Expr::new(ExprKind::CoalesceReturnFallback(value), ret_span); let span = expr.span.merge(right.span); expr = Expr::new( ExprKind::Coalesce(Box::new(expr), Box::new(right)), span, ); continue; } let right = self.parse_unary()?; let span = expr.span.merge(right.span); expr = Expr::new( ExprKind::Coalesce(Box::new(expr), Box::new(right)), span, ); } TokenKind::KwAs => { self.bump(); let ty = self.parse_type()?; let span = expr.span.merge(ty.span()); expr = Expr::new(ExprKind::As(Box::new(expr), ty), span); } TokenKind::KwIs => { self.bump(); let ty = self.parse_type()?; let span = expr.span.merge(ty.span()); expr = Expr::new(ExprKind::Is(Box::new(expr), ty), span); } // Plan 91 followup (2026-05-30): multi-line method chain // continuation. После expression-statement newline, если // следующий значимый token — `.` (member access), продолжаем // chain. Не: implicit statement-end + orphan `.` parse error. // // Example: // @buf.push((cp >> 6) as u8) // .push((cp & 0x3F) as u8) // <-- continuation // // Conservative scope: только `.` после newline. Не охватывает // `?.` / `!.` / wrapping call args / etc. (separate followup). // Spec: см. docs/plans/91-stdlib-mvp-for-0.1.md секцию // «multi-line chain continuation» + D-block для expression // line-continuation. TokenKind::Newline | TokenKind::Semicolon => { let mut look_pos = self.pos + 1; while look_pos < self.tokens.len() && matches!( self.tokens[look_pos].kind, TokenKind::Newline | TokenKind::Semicolon ) { look_pos += 1; } if look_pos < self.tokens.len() && matches!(self.tokens[look_pos].kind, TokenKind::Dot) { // Multi-line chain — skip whitespace, продолжаем loop. self.pos = look_pos; continue; } break; } _ => break, } } Ok(expr) } fn parse_primary(&mut self) -> Result<Expr, Diagnostic> { let start = self.peek().span; match self.peek().kind.clone() { TokenKind::Int(n) => { self.bump(); Ok(Expr::new(ExprKind::IntLit(n), start)) } TokenKind::Float(n) => { self.bump(); Ok(Expr::new(ExprKind::FloatLit(n), start)) } TokenKind::Str(s) => { self.bump(); self.desugar_string_interpolation(s, start) } // D412 (Plan 186): hex-blob literal `x"48 69"` → compile-time // `[]u8`. Bytes already decoded by the lexer. TokenKind::HexBlob(bytes) => { self.bump(); Ok(Expr::new(ExprKind::HexBlobLit(bytes), start)) } TokenKind::Char(cp) => { self.bump(); Ok(Expr::new(ExprKind::CharLit(cp), start)) } TokenKind::KwTrue => { self.bump(); Ok(Expr::new(ExprKind::BoolLit(true), start)) } TokenKind::KwFalse => { self.bump(); Ok(Expr::new(ExprKind::BoolLit(false), start)) } TokenKind::Backtick(s) => { self.bump(); // bare backtick без tag-функции = строка. Ok(Expr::new(ExprKind::StrLit(s), start)) } TokenKind::At => { self.bump(); // Plan 110.7.3.a (D03 строка 1460-1461): `@.field` невалидно. // Канонический доступ к полю self — `@field` без точки. // `(@).field` через postfix-парсер also rejected here (sole // entry point для @). if matches!(self.peek().kind, TokenKind::Dot) { let span = self.peek().span; return Err(Diagnostic::new( "[E_SELF_DOT_INVALID] `@.field` is invalid syntax — use `@field` for self-field access (D03 §D35/D37)", span, )); } // @ или @field if matches!(self.peek().kind, TokenKind::Ident(_)) { let (name, name_span) = self.parse_ident()?; let self_expr = Expr::new(ExprKind::SelfAccess, start); Ok(Expr::new( ExprKind::Member { obj: Box::new(self_expr), name, }, start.merge(name_span), )) } else { Ok(Expr::new(ExprKind::SelfAccess, start)) } } TokenKind::Ident(ref kw) if kw == "forall" || kw == "exists" => { let is_forall = kw == "forall"; self.parse_quantifier(is_forall) } // Plan 115 D214 / Plan 118 Ф.5.7 A23 (D214 amend 2026-06-02): // `null ptr` literal RETRACTED. После Ф.5 NPO codegen (A19/A21), // `Option[*T]` + `Option[ptr]` provide null-safety через // type-system (sizeof=8 single-pointer layout, NULL=None // convention). `null ptr` literal становится redundant и // ambiguous (Some(null ptr) == None под NPO — confusing). // // Migration: `null ptr` → `(0 as ptr)` mechanical replacement, // semantically equivalent (NULL = (void*)0 в C ABI). // Recommended: use `Option[ptr]` с `None` для new code. // // `null <other_prim>` — by-product от Plan 115 V1 diagnostic; // retained as `E_NULL_PTR_RETRACTED_USE_OPTION`. TokenKind::Ident(ref n) if n == "null" && matches!(&self.peek_at(1).kind, TokenKind::Ident(ty) // Plan 134: `ptr` retained here so `null ptr` still gets // the dedicated retraction hint (steer to Option/None) // rather than a bare "undefined identifier"; the message // body migrates `ptr` → `*()`. if ty == "ptr" || ty == "int" || ty == "i8" || ty == "i16" || ty == "i32" || ty == "i64" || ty == "u8" || ty == "u16" || ty == "u32" || ty == "u64" || ty == "uint" || ty == "f32" || ty == "f64" || ty == "bool" || ty == "char" || ty == "str") => { let null_span = self.bump().span; let TokenKind::Ident(ty_name) = self.peek().kind.clone() else { unreachable!("guarded by matches! above"); }; let ty_span = self.bump().span; let full_span = null_span.merge(ty_span); // Plan 118 Ф.5.7 (A23): hard retraction. Err(Diagnostic::new( format!( "[E_NULL_PTR_RETRACTED_USE_OPTION] `null {ty}` literal \ retracted (Plan 118 D214 amend 2026-06-02). После \ NPO codegen (Plan 118 Ф.5 A19/A21), `Option[*T]` и \ `Option[*()]` provide null-safety через type-system \ (single-pointer layout NULL=None convention). \ Migration (Plan 134 — `ptr` builtin removed, opaque \ pointer = `*()` = `void*`): \ (1) для existing pointer code: replace `null *()` → \ `(0 as *())` (mechanical, NULL=(void*)0); \ (2) для new code: use `Option[*()]` с `None` (type-safe).", ty = ty_name ), full_span, )) } // Plan 97 Ф.3 (D142): hint при `handler IDENT {` — // legacy literal-форма, снята clean-break'ом. Подсказка // должна указывать на новый `effect IDENT { ... }`. TokenKind::Ident(ref kw) if kw == "handler" && matches!(self.peek_at(1).kind, TokenKind::Ident(_)) && matches!(self.peek_at(2).kind, TokenKind::LBrace) => { let span = self.peek().span; return Err(Diagnostic::new( "`handler` keyword removed (Plan 97 / D142) — use `effect X { ... }` \ instead. Migration: replace `handler EffectName { ops }` with \ `effect EffectName { ops }`. Builtin type `Handler[E, IRT]` also \ renamed to `Effect[E, IRT]`.", span, )); } TokenKind::Ident(_) => { // Простой идентификатор. Dot-цепочки `.field`/`.method` // обрабатываются в parse_postfix как Member access — это // позволяет `p.x` работать когда `p` — обычная переменная, // а не qualifier пути. // // Type/Module qualifier (PascalCase + dot) превращается в // Path только когда первый токен — заглавная буква И за ним // `.IDENT`, чтобы поддержать `Type.method` / `Module.fn`. // Дальше member-доступ всё равно работает через Dot → // Member на postfix-стадии. let (first, first_span) = self.parse_ident()?; let starts_uppercase = first .chars() .next() .map(|c| c.is_ascii_uppercase()) .unwrap_or(false); let mut path = vec![first.clone()]; // Plan 08 Ф.2: primitive type-names — `int`, `f64`, `bool`, // `char`, `byte`, `str`, `u8`-`u64`, `i8`-`i64`, `f32`/`f64` — // могут быть subject'ом static-method'а (`int.try_from`, // `str.from`, `f64.try_from`). Lowercase, поэтому не path // через PascalCase-rule. Делаем явное исключение. // Plan 70.5 Ф.4: `uint` добавлен (alias u64) — `uint.MAX` работает. let is_primitive_type = matches!(first.as_str(), "int" | "i8" | "i16" | "i32" | "i64" | "u8" | "u16" | "u32" | "u64" | "uint" | "f32" | "f64" | "bool" | "char" | "str" // Plan 76: `never` — bottom-тип, строчный встроенный примитив. | "never" // Plan 134: `ptr` builtin REMOVED — opaque pointer is now // `*()` (pointer-to-unit = void*). `ptr` no longer a // primitive type; use в type position → E_TYPE_UNKNOWN // (types/mod.rs walk_typeref guard). ); if (starts_uppercase || is_primitive_type) && matches!(self.peek().kind, TokenKind::Dot) && matches!(self.peek_at(1).kind, TokenKind::Ident(_)) { // Один шаг path: primitive.method (стесняемся продолжать, // primitives не имеют sub-namespace'ов). if is_primitive_type && !starts_uppercase { self.bump(); // dot path.push(self.parse_ident()?.0); } } if starts_uppercase { while matches!(self.peek().kind, TokenKind::Dot) && matches!(self.peek_at(1).kind, TokenKind::Ident(_)) { // Заглядываем: если следующий ident — тоже PascalCase, // продолжаем path; иначе оставляем для Member access. let TokenKind::Ident(next_name) = &self.peek_at(1).kind else { break; }; let next_upper = next_name .chars() .next() .map(|c| c.is_ascii_uppercase()) .unwrap_or(false); // Продолжаем только если оба — PascalCase (Module.SubModule). // Type.method (метод с lowercase) останавливаем здесь, // чтобы получить Path[Type] и Member через postfix — // но это сломало бы static-method вызовы. Вместо этого: // съедаем dot всегда после PascalCase, пока следующее — // identifier. Проблема — для Type.method.x сначала // соберём Path[Type, method], потом .x как Member. let _ = next_upper; self.bump(); path.push(self.parse_ident()?.0); } } // Если за path идёт `{`, и **это валидно как record-литерал**: if matches!(self.peek().kind, TokenKind::LBrace) { // Plan 52 Ф.1: keyword в field-position → actionable error // с HELP-подсказкой, до того как `{` уйдёт в блок-ветку. if let Some(diag) = self.record_lit_keyword_field_error() { return Err(diag); } if self.looks_like_record_lit() { return self.parse_record_lit_after_path(path, first_span); } } if path.len() == 1 { Ok(Expr::new( ExprKind::Ident(path.into_iter().next().unwrap()), first_span.merge(self.tokens[self.pos.saturating_sub(1)].span), )) } else { Ok(Expr::new( ExprKind::Path(path), first_span.merge(self.tokens[self.pos.saturating_sub(1)].span), )) } } TokenKind::LBrace => { // Запись или блок? В Nova record-литерал без типа — тоже // валиден (D55 coercion). Различаем: { name : ... } -> record, // { name, name: ... } -> record (D52 punning), иначе блок. // Plan 52 Ф.1: keyword в field-position → actionable error // с HELP-подсказкой, до того как `{` уйдёт в блок-ветку. if let Some(diag) = self.record_lit_keyword_field_error() { return Err(diag); } if self.looks_like_record_lit() { self.parse_record_lit_after_path(Vec::new(), start) } else { let block = self.parse_block()?; let span = block.span; Ok(Expr::new(ExprKind::Block(block), span)) } } // Plan 118 D216 §8 (D2 amend): `unsafe { ... }` block. Syntactic // sugar над built-in `unsafe_handler` effect handler (D2 v2): // unsafe { expr } ≡ with unsafe_handler { perform UnsafeOps.<op>(expr) } // // V1 Ф.3 scaffold: parsed как regular Block expression (no // runtime overhead — block emits identical C code). Type-checker // enforcement (Ф.3.5 E_UNSAFE_REQUIRED / E_UNSAFE_CALL_REQUIRES_WRAP) // — followup phase. Effect propagation: D216 §8 «no propagation // up» — unsafe encapsulates per fn (canonical Rust pattern). TokenKind::KwUnsafe => { self.bump(); // eat 'unsafe' keyword; LBrace остаётся для parse_block let mut block = self.parse_block()?; // Plan 118 (D216 §8, Ф.3.3 enforcement foundation): mark // block as unsafe-context. Type-checker (Ф.3.5 follow-on) // uses is_unsafe flag для E_UNSAFE_REQUIRED gating pointer // ops (&, *, *T deref, p.field на pointer). block.is_unsafe = true; let span = start.merge(block.span); Ok(Expr::new(ExprKind::Block(block), span)) } TokenKind::LBracket => self.parse_array_lit(), TokenKind::LParen => { self.bump(); self.skip_newlines(); if matches!(self.peek().kind, TokenKind::RParen) { // Plan 19 C13: zero-arg lambda `() => ...` удалена // (Plan 19 D22-rev). Используется `||` для no-arg // closure-light. Здесь `()` — unit-литерал. let end = self.bump().span; return Ok(Expr::new(ExprKind::UnitLit, start.merge(end))); } // Plan 19, C13: legacy `(params) => ...` lambda // полностью удалена. Если в коде встречается // — выдаём понятную ошибку с подсказкой использовать // `|x|` (closure-light) или `fn(x)` (closure-full). // try_parse_lambda больше не вызывается; tuple/group // парсится прямо. let first = self.parse_expr()?; if self.eat(&TokenKind::Comma).is_some() { let mut elems = vec![first]; self.skip_newlines(); while !matches!(self.peek().kind, TokenKind::RParen) { elems.push(self.parse_expr()?); if self.eat(&TokenKind::Comma).is_none() { break; } self.skip_newlines(); } let end = self.expect(&TokenKind::RParen)?.span; // Plan 19 C13: detect legacy lambda // `(p1, p2) => body` и выдать понятную ошибку. if matches!( self.peek().kind, TokenKind::FatArrow | TokenKind::Arrow ) { return Err(Diagnostic::new( "legacy lambda `(params) => body` removed in Plan 19 D22-rev — \ use closure-light `|x, y| body` or closure-full `fn(x T) -> R body`".to_string(), self.peek().span, )); } Ok(Expr::new(ExprKind::TupleLit(elems), start.merge(end))) } else { // D49: ignore newlines before closing `)` in grouped expr self.skip_newlines(); self.expect(&TokenKind::RParen)?; if matches!( self.peek().kind, TokenKind::FatArrow | TokenKind::Arrow ) { return Err(Diagnostic::new( "legacy lambda `(x) => body` removed in Plan 19 D22-rev — \ use closure-light `|x| body` or closure-full `fn(x T) -> R body`".to_string(), self.peek().span, )); } Ok(first) } } TokenKind::KwIf => self.parse_if(), TokenKind::KwMatch => self.parse_match(), TokenKind::KwFor => self.parse_for(), TokenKind::KwWhile => self.parse_while(), TokenKind::KwLoop => self.parse_loop(), TokenKind::KwWith => self.parse_with(), TokenKind::KwInterrupt => self.parse_interrupt_expr(), TokenKind::KwSpawn => self.parse_spawn(), TokenKind::KwSupervised => self.parse_supervised(), TokenKind::KwParallel => self.parse_parallel_for(), TokenKind::KwDetach => self.parse_detach(), TokenKind::KwBlocking => self.parse_blocking(), TokenKind::KwThrow => { // D25/D65: `throw expr` as expression (type never). // Stmt-level throw уже обрабатывается parse_stmt_or_expr; // expression-level — здесь, для match-arm body, ternary, // тd. Codegen эмитирует как Nova_Fail_fail(msg) + // zero-of-target-type dummy. let start_span = self.bump().span; let value = self.parse_expr()?; let span = start_span.merge(value.span); Ok(Expr::new(ExprKind::Throw(Box::new(value)), span)) } // Plan 97 Ф.3 (D142): литерал handler'а через keyword `effect` // (тот же, что в declaration — disambig по позиции). В // expression-position `effect IDENT { ... }` всегда literal, // т.к. declaration `type X effect {...}` начинается с `type`. TokenKind::KwEffect => self.parse_handler_lit(), // Plan 97 Ф.4 (D142): protocol-литерал в expression-position // — `protocol IDENT { method-impl* }`. Disambig от // type-position `protocol { sig* }` (Ф.2, без IDENT) — для // expr-position обязательно имя именованного протокола. // Anonymous protocol-литерал в expr — не разрешён (нет // контракта для structural check'а). TokenKind::KwProtocol => self.parse_protocol_lit(), TokenKind::KwForbid => self.parse_forbid(), TokenKind::KwRealtime => self.parse_realtime(), TokenKind::KwSelect => self.parse_select(), // Plan 19, C2: closure-light `|x| body` / `||` / `|_|`. // В expression-position (parse_primary) `|` всегда означает // начало closure-light. В infix-position он остаётся // bitwise OR (см. parse_bit_or в parser-цепочке). TokenKind::Pipe => self.parse_closure_light_with_params(start), // `||` — closure-light без параметров. Disambiguation от // logical-OR работает по позиции: в expression-position // (start) `||` всегда no-arg closure; в infix-position — // logical-OR (см. parse_logical_or). TokenKind::PipePipe => self.parse_closure_light_no_params(start), // Plan 19, C3: closure-full `fn(x int) Effects -> R body` — // анонимная типизированная fn в expression-position. В // отличие от item-level `fn name(...)`, имени нет — // следующий токен после `fn` обязан быть `(`. // // Type-expression `fn(int) -> bool` парсится отдельно // через parse_type, не сюда; в expression-position // type-expr не появляется. TokenKind::KwFn => self.parse_closure_full(start), other => Err(Diagnostic::new( format!("unexpected {} in expression", other.name()), start, )), } } /// Plan 19, C2: парсит closure-light с параметрами. /// /// Ожидает текущим токеном `|`. После `|` идут идентификаторы /// (имена параметров, разделённые запятой), затем закрывающий `|`, /// затем тело — bare expression или block. /// /// Wildcard `_` разрешён как имя параметра (D59 расширение). /// Типы параметров **запрещены** — closure-light всегда untyped. /// Если нужны типы — программист использует closure-full /// (`fn(x int) ...`, см. parse_closure_full). fn parse_closure_light_with_params(&mut self, start: Span) -> Result<Expr, Diagnostic> { // Съедаем открывающий `|` let open = self.expect(&TokenKind::Pipe)?.span; self.skip_newlines(); let mut params = Vec::new(); // Пустой `|...|` без параметров — некорректно, программист // должен писать `||`. Однако `|_|` валиден (один wildcard). if !matches!(self.peek().kind, TokenKind::Pipe) { loop { let p = self.parse_closure_light_param()?; params.push(p); self.skip_newlines(); if self.eat(&TokenKind::Comma).is_none() { break; } self.skip_newlines(); } } // Закрывающий `|`. Если параметров нет — это была ошибка // программиста (`||` ловится отдельной веткой), здесь сразу // expect — даст понятную диагностику. let _close = self.expect(&TokenKind::Pipe).map_err(|d| { // Делаем сообщение чётче: подсказываем форму записи. let mut d = d; d.message = format!( "{} (in closure-light parameter list — `|x|`, `|x, y|`, `|_|`, или `||` для no-arg)", d.message ); d })?; let _ = open; self.parse_closure_light_body(start, params) } /// Plan 19, C2: парсит closure-light без параметров (`|| body`). /// /// Текущий токен — `||` (двойной pipe). Тело — bare expression /// или block, как у обычной closure-light. fn parse_closure_light_no_params(&mut self, start: Span) -> Result<Expr, Diagnostic> { // Съедаем `||` целиком. let _ = self.expect(&TokenKind::PipePipe)?; self.parse_closure_light_body(start, Vec::new()) } /// Парсит один параметр closure-light: имя или wildcard `_`. /// Типы запрещены — если программист написал `|x int|`, парсер /// даёт явную ошибку с подсказкой переключиться на closure-full. fn parse_closure_light_param(&mut self) -> Result<crate::ast::ClosureLightParam, Diagnostic> { let tok = self.peek().clone(); let (name, span) = match &tok.kind { // В лексере Nova `_` парсится как `Ident("_")` (lexer/mod.rs:417); // wildcard и обычный identifier различаются по строковому значению. TokenKind::Ident(s) => { self.bump(); (s.clone(), tok.span) } _ => { return Err(Diagnostic::new( format!( "expected closure-light parameter name (identifier or `_`), got {}", tok.kind.name() ), tok.span, )) } }; // Параметр не должен иметь тип (это было бы closure-full). // Если за именем идёт type-ish токен — даём понятную ошибку. if self.is_closure_light_type_after_name() { return Err(Diagnostic::new( "closure-light parameters are untyped — use `fn(x T)` syntax for typed closures (closure-full)".to_string(), self.peek().span, )); } Ok(crate::ast::ClosureLightParam { name, span }) } /// Эвристика «после имени параметра идёт тип»: первый токен, /// который выглядит как начало type-expression. Используется /// для генерации хорошей ошибки в parse_closure_light_param. /// /// Знаем что после имени допустимы только: `,` (следующий param) /// или `|` (закрытие списка). Всё остальное — type-like. fn is_closure_light_type_after_name(&self) -> bool { !matches!( self.peek().kind, TokenKind::Comma | TokenKind::Pipe ) } /// Общая часть: после съеденного `|...|` парсит тело closure'а. /// Тело — `Block` (если следующий токен `{`) или bare `Expr`. fn parse_closure_light_body( &mut self, start: Span, params: Vec<crate::ast::ClosureLightParam>, ) -> Result<Expr, Diagnostic> { // closure-light не использует `=>` — это часть «освобождения // `=>` от роли лямбда-стрелки» (D22-rev). Если программист // написал `|x| => expr`, даём явную ошибку. if matches!(self.peek().kind, TokenKind::FatArrow) { let span = self.peek().span; return Err(Diagnostic::new( "closure-light body starts immediately after `|...|`, no `=>` is used (D22-rev). Drop the `=>` or use a named fn / `fn(...)` if you need `=>`".to_string(), span, )); } // Block-форма: `|x| { stmts; expr }`. // В отличие от parse_primary, здесь НЕ применяем // record-литерал-эвристику — `|x| { name: ... }` это // block-body с record-литералом внутри был бы крайне редким // паттерном; для consistency block-форма всегда побеждает. if matches!(self.peek().kind, TokenKind::LBrace) { let block = self.parse_block()?; let span = start.merge(block.span); return Ok(Expr::new( ExprKind::ClosureLight { params, body: crate::ast::ClosureBody::Block(block), }, span, )); } // Expression-форма: `|x| expr`. Тело — одно выражение. // Как у старой Lambda, парсим через parse_expr (полный // pratt-парсер). let body = self.parse_expr()?; let span = start.merge(body.span); Ok(Expr::new( ExprKind::ClosureLight { params, body: crate::ast::ClosureBody::Expr(Box::new(body)), }, span, )) } /// Plan 19, C3: парсит closure-full — анонимную типизированную fn. /// /// Грамматика идентична named fn без имени: /// ```text /// closure-full = 'fn' '(' params ')' [ effects ] [ '->' type ] body /// body = '=>' expression | block /// ``` /// /// Где `params` — обычные `Param` с типами (как у named fn). /// **Generics на closure-full в bootstrap не поддерживаются** — /// rank-2 polymorphism это открытый вопрос (см. Q-rank2 / D61). /// Если после `fn` идёт `[`, парсер даст понятную ошибку. /// /// Тело — `=> expr` или `{ block }`, переиспользует parse_fn_body. fn parse_closure_full(&mut self, start: Span) -> Result<Expr, Diagnostic> { // Съедаем `fn`. let _ = self.expect(&TokenKind::KwFn)?; // Запрещаем generics: `fn[T](x T) -> T => x` не поддерживается // в bootstrap'е. Если потребуется — отдельный D-decision. if matches!(self.peek().kind, TokenKind::LBracket) { return Err(Diagnostic::new( "generics on closure-full are not supported in bootstrap (rank-2 polymorphism, see Q-rank2). Use a named fn or workaround through erasure".to_string(), self.peek().span, )); } // (params) — переиспользуем существующий parse_param. self.expect(&TokenKind::LParen)?; self.skip_newlines(); let mut params = Vec::new(); while !matches!(self.peek().kind, TokenKind::RParen) { params.push(self.parse_param()?); self.skip_newlines(); if !matches!(self.peek().kind, TokenKind::RParen) { self.expect(&TokenKind::Comma)?; self.skip_newlines(); } } self.expect(&TokenKind::RParen)?; // Variadic check (D69): variadic-параметр обязан быть последним. for (i, p) in params.iter().enumerate() { if p.is_variadic && i != params.len() - 1 { return Err(Diagnostic::new( format!("variadic-параметр `{}` должен быть последним в списке (D69)", p.name), p.span, )); } } // Effects между `)` и (`->` | body). let effects = self.parse_effects_until_arrow_or_body()?; let return_type = if self.eat(&TokenKind::Arrow).is_some() { Some(self.parse_type()?) } else { None }; // Тело — `=> expr` или `{ block }`. Переиспользуем parse_fn_body. let body = self.parse_fn_body()?; let body_span = match &body { FnBody::Expr(e) => e.span, FnBody::Block(b) => b.span, FnBody::External => unreachable!( "closure-full cannot be `external` — only named fns can; \ parse_fn_body would have returned External only for top-level \ external fn parsing path" ), }; let span = start.merge(body_span); // Plan 51 Ф.2: `=>`-тело замыкания — record-литерал ⇒ тип ровно // один раз (как для named fn; receiver у замыкания нет). Self::check_record_lit_type_once(&return_type, None, &body)?; Ok(Expr::new( ExprKind::ClosureFull(Box::new(crate::ast::FnSigBody { params, effects, return_type, body, span, })), span, )) } /// D.1.3: парсит `forall x in lo..hi : P(x)` или `exists x in lo..hi : P(x)`. /// /// Вызывается из parse_primary когда текущий токен — Ident("forall") /// или Ident("exists"). Оба являются контекстными ключевыми словами /// (не TokenKind), поэтому диспатч через проверку содержимого Ident. fn parse_quantifier(&mut self, is_forall: bool) -> Result<Expr, Diagnostic> { let start = self.bump().span; // consume "forall" / "exists" let (var_name, _var_span) = self.parse_ident()?; // bound variable self.expect(&TokenKind::KwIn)?; // Диапазон — lo..hi. Отключаем trailing/struct чтобы `:` не // поглощалось как named-argument или record-поле. let range = self.with_no_struct_or_trailing(|p| p.parse_expr())?; self.expect(&TokenKind::Colon)?; let body = self.parse_expr()?; let span = start.merge(body.span); if is_forall { Ok(Expr::new(ExprKind::Forall { var: var_name, range: Box::new(range), body: Box::new(body), }, span)) } else { Ok(Expr::new(ExprKind::Exists { var: var_name, range: Box::new(range), body: Box::new(body), }, span)) } } /// Эвристика: `{` перед нами — это начало record-литерала? /// Смотрим первый «значимый» токен внутри: `Ident :` или `...` или `}`. /// Plan 52 Ф.1 (D108): диагностика «keyword как имя поля в `{...}`». /// /// `{type: 1}` — `type` это keyword, не валидное имя поля (D83). Без /// этой проверки `looks_like_record_lit` вернул бы `false` (keyword ≠ /// `Ident`), `{` распарсился бы как блок, и ошибка парсера была бы /// непонятной. Возвращает `Some(Diagnostic)` если после `{` (через /// newlines) стоит keyword в field-position (`kw :` / `kw ,` / `kw }`), /// с HELP-подсказкой использовать map-литерал `["kw": value]`. fn record_lit_keyword_field_error(&self) -> Option<Diagnostic> { if self.no_struct_lit { return None; } let mut i = self.pos + 1; // после `{` while i < self.tokens.len() && matches!(self.tokens[i].kind, TokenKind::Newline | TokenKind::Semicolon) { i += 1; } let tok = self.tokens.get(i)?; let kw_text = match &tok.kind { TokenKind::KwModule => "module", TokenKind::KwImport => "import", TokenKind::KwExport => "export", TokenKind::KwExternal => "external", TokenKind::KwFn => "fn", TokenKind::KwType => "type", TokenKind::KwProtocol => "protocol", TokenKind::KwEffect => "effect", TokenKind::KwAlias => "alias", TokenKind::KwLet => "let", TokenKind::KwConst => "const", TokenKind::KwMut => "mut", TokenKind::KwRo => "ro", TokenKind::KwReadonly => "readonly", TokenKind::KwIf => "if", TokenKind::KwElse => "else", TokenKind::KwMatch => "match", TokenKind::KwFor => "for", TokenKind::KwWhile => "while", TokenKind::KwLoop => "loop", TokenKind::KwIn => "in", TokenKind::KwReturn => "return", TokenKind::KwBreak => "break", TokenKind::KwContinue => "continue", TokenKind::KwTest => "test", TokenKind::KwWith => "with", TokenKind::KwThrow => "throw", TokenKind::KwAs => "as", TokenKind::KwIs => "is", TokenKind::KwSpawn => "spawn", TokenKind::KwSupervised => "supervised", TokenKind::KwParallel => "parallel", TokenKind::KwDetach => "detach", TokenKind::KwBlocking => "blocking", TokenKind::KwInterrupt => "interrupt", TokenKind::KwForbid => "forbid", TokenKind::KwRealtime => "realtime", TokenKind::KwDefer => "defer", TokenKind::KwErrDefer => "errdefer", TokenKind::KwOkDefer => "okdefer", TokenKind::KwSelect => "select", _ => return None, // не keyword — обычный путь }; // Keyword в field-position только если за ним `:` / `,` / `}`. let next = self.tokens.get(i + 1); if matches!( next.map(|t| &t.kind), Some(TokenKind::Colon | TokenKind::Comma | TokenKind::RBrace) ) { Some(Diagnostic::new( format!( "keyword `{kw_text}` cannot be used as a field name in a \ record/map-coercion literal — use map-literal syntax \ instead: [\"{kw_text}\": value]" ), tok.span, )) } else { None } } fn looks_like_record_lit(&self) -> bool { if self.no_struct_lit { return false; } // Skip newlines внутри. let mut i = self.pos + 1; while i < self.tokens.len() && matches!(self.tokens[i].kind, TokenKind::Newline | TokenKind::Semicolon) { i += 1; } if i >= self.tokens.len() { return false; } // Plan 52.2 Ф.1: пустой `{}` — это **пустой блок**, не пустой // anonymous record (D55 §5 ревизия). Без этого parser'у удавалось // создать `RecordLit { type_name: None, fields: [] }` который // codegen не может обработать (нет struct-name для inference). // Empty block — это valid `nova_unit` value, что и ожидается в // позициях типа `_ => {}` (match-arm body), `if cond { } else ...`. if matches!(self.tokens[i].kind, TokenKind::RBrace) { return false; } if matches!(self.tokens[i].kind, TokenKind::DotDotDot) { return true; } // `@`-shorthand в record-литерале: `@field` punning. Но `@method(...)` // — это method call (statement). Различаем по двум следующим // токенам: `@ Ident (Comma|RBrace|Colon)` — punning; иначе — call. if matches!(self.tokens[i].kind, TokenKind::At) { let after_at = self.tokens.get(i + 1); let after_ident = self.tokens.get(i + 2); if matches!(after_at.map(|t| &t.kind), Some(TokenKind::Ident(_))) && matches!(after_ident.map(|t| &t.kind), Some(TokenKind::Comma | TokenKind::RBrace | TokenKind::Colon)) { return true; } // Bare `@` (без ident) — self-value; в record-lit это // нонсенс, но в expression-блоке валидно. Не record-lit. return false; } if matches!(self.tokens[i].kind, TokenKind::Ident(_)) { // smart: `Ident :` → record. `Ident ,` → punning. `Ident }` → punning. let next = self.tokens.get(i + 1); return matches!( next.map(|t| &t.kind), Some(TokenKind::Colon | TokenKind::Comma | TokenKind::RBrace) ); } false } fn parse_record_lit_after_path( &mut self, path: Vec<String>, start: Span, ) -> Result<Expr, Diagnostic> { self.expect(&TokenKind::LBrace)?; let mut fields = Vec::new(); self.skip_newlines(); while !matches!(self.peek().kind, TokenKind::RBrace) { if self.eat(&TokenKind::DotDotDot).is_some() { let v = self.parse_expr()?; fields.push(RecordLitField { name: String::new(), value: Some(v.clone()), is_spread: true, at_shorthand: false, span: v.span, }); } else { // field-shorthand `name` или `@name`, или `name: expr` if matches!(self.peek().kind, TokenKind::At) { let at_span = self.bump().span; let (name, name_span) = self.parse_ident()?; let value = Expr::new( ExprKind::Member { obj: Box::new(Expr::new(ExprKind::SelfAccess, at_span)), name: name.clone(), }, at_span.merge(name_span), ); fields.push(RecordLitField { name, value: Some(value), is_spread: false, at_shorthand: true, span: at_span.merge(name_span), }); } else { let (name, name_span) = self.parse_ident()?; if self.eat(&TokenKind::Colon).is_some() { let v = self.parse_expr()?; let span = name_span.merge(v.span); fields.push(RecordLitField { name, value: Some(v), is_spread: false, at_shorthand: false, span, }); } else { // shorthand fields.push(RecordLitField { name, value: None, is_spread: false, at_shorthand: false, span: name_span, }); } } } if self.eat(&TokenKind::Comma).is_some() { self.skip_newlines(); } else { self.skip_newlines(); } } let end = self.expect(&TokenKind::RBrace)?.span; Ok(Expr::new( ExprKind::RecordLit { type_name: if path.is_empty() { None } else { Some(path) }, fields, // Plan 52 Ф.10: заполняется type-checker'ом (MapLitAnnotator) // если запись стоит в позиции #from_fields-типа. inferred_map_v: None, // D450 (реестр 221.1 №503): заполняется той же точкой, // что и `inferred_map_v` — см. `MapLitAnnotator`. inferred_target_type: None, }, start.merge(end), )) } /// Парсит `[...]` — array-литерал (D27/D38) ИЛИ map-литерал (D108). /// /// Парсинг **локальный, без type-directed** (D108): /// 1. `[]` пустой → `ArrayLit(vec![])` — array-или-map, разрешается на /// type-check по ожидаемому типу. /// 2. Иначе парсим первое выражение; если первый элемент — `...spread`, /// это всегда array. Следующий токен после первого expr: /// - `:` → map-литерал, дальше пары `expr : expr`; /// - `,` / `]` → array-литерал. /// 3. Смешение форм (`[a, b: c]`) → actionable error. fn parse_array_lit(&mut self) -> Result<Expr, Diagnostic> { let start = self.expect(&TokenKind::LBracket)?.span; self.skip_newlines(); // Пустой `[]` — array-или-map, разрешается на type-check. // D38 array-type-static-method: `[]T.method(...)` — empty литерал // immediately followed by Ident `.` означает array-type prefix, не // empty literal. Превращаем в Path(["__array", "<T>"]) — codegen // эмитит как `nova_array_new_<T>` для `.new()` / `.with_capacity()`. if matches!(self.peek().kind, TokenKind::RBracket) { let end = self.expect(&TokenKind::RBracket)?.span; if let TokenKind::Ident(_) = &self.peek().kind { if matches!(self.peek_at(1).kind, TokenKind::Dot) { let (elem_type_name, ty_span) = self.parse_ident()?; return Ok(Expr::new( ExprKind::Path(vec!["__array".to_string(), elem_type_name]), start.merge(ty_span), )); } // [221.1 №510] `[]T[Args].method(...)` — `T` carries its own // turbofish type-args (`[]Ent[str, int].new()`). The bare- // Ident branch above only looks ONE token ahead (`Ident` // then `Dot`); with turbofish args the next token is `[`, // so that branch never fired. The already-consumed `]` // then fell through to the plain "empty array literal" // return below, and `Ent[str, int].new()` was left to be // parsed as an unrelated SIBLING statement/expression — // silently wrong code, not a parse error (see the bug // record for the `Nova_EmbeddedDir_static_new()` symptom). // // Fix: desugar to `Vec[T[Args]].method(...)` — reuses the // already-working explicit `Vec[...]` static-dispatch // machinery (D27 `[]T ≡ Vec[T]`), the same trick the // `[](T1, T2)` tuple-type amendment right below already // uses. Speculative + rollback: if `T[Args]` doesn't parse // as a type, or isn't followed by a genuine `.method(` // continuation, fall through to the hard error a few // lines down — per D38 this sugar is ALWAYS followed by // `.method(...)`, so a bare `[]Ident` with no recognized // continuation is never a legal program; there is nothing // legitimate left to silently drop. if matches!(self.peek_at(1).kind, TokenKind::LBracket) { let saved_pos = self.pos; if let Ok(ty) = self.parse_type() { if matches!(self.peek().kind, TokenKind::Dot) && matches!(self.peek_at(1).kind, TokenKind::Ident(_)) && matches!(self.peek_at(2).kind, TokenKind::LParen) { let ty_span = ty.span(); return Ok(Expr::new( ExprKind::TurboFish { base: Box::new(Expr::new( ExprKind::Ident("Vec".to_string()), start, )), type_args: vec![ty], }, start.merge(ty_span), )); } } self.pos = saved_pos; } // [221.1 №510] Insurance against the whole bug CLASS, not // just the turbofish carrier: statements are always // Newline/Semicolon-separated (`parse_stmt_or_expr`), so an // `Ident` sitting immediately after `]` with NO separator // token in between can only be an attempt at this `[]T...` // sugar — there is no other legal Nova construct it could // start. Silently falling through to the empty-array- // literal return below would reopen exactly the class this // record closes: the `]` gets read as a COMPLETE // expression and the Ident starts an unrelated sibling // statement, changing the program's meaning without a // diagnostic. Refuse instead of guessing. let bad_span = self.peek().span; let ident_preview = if let TokenKind::Ident(n) = &self.peek().kind { n.clone() } else { String::new() }; return Err(Diagnostic::new( format!( "`[]{ident_preview}...` is not a recognized array-type \ static-method call — expected `[]T.method(...)` or \ `[]T[Args].method(...)` (D38); an identifier \ immediately after `[]` can never start a separate \ statement here" ), bad_span, )); } // [221.1 №24 / D-amend D27] `[](T1, T2, ...)` — a parenthesized // TYPE form (tuple-type shape, D27 `[]T ≡ Vec[T]` alias) // immediately after an empty `[]` in EXPRESSION position, // followed by a genuine type-usage continuation (`.`/`{`) — // desugars to `Vec[<type>]` (the identical `TurboFish` shape a // spelled-out `Vec[(T1, T2)]` already produces — reuses 100% // of that existing static-dispatch machinery, no new codegen/ // typecheck path). Grammatically free: calling an empty // literal (`[](...)` as a CALL) was always a type error before // this amendment, so repurposing the shape loses no legal // program (spec/decisions/03-syntax.md D27). // Speculative + rollback (mirrors `try_parse_turbofish_args`): // if the parenthesized form doesn't parse as a type, or isn't // followed by a real type-usage continuation, fall through // unchanged to the empty-literal-then-call parse — preserves // the original diagnostic on a genuinely malformed call. if matches!(self.peek().kind, TokenKind::LParen) { let saved_pos = self.pos; if let Ok(ty) = self.parse_type() { if matches!(self.peek().kind, TokenKind::Dot | TokenKind::LBrace) { let ty_span = ty.span(); return Ok(Expr::new( ExprKind::TurboFish { base: Box::new(Expr::new( ExprKind::Ident("Vec".to_string()), start, )), type_args: vec![ty], }, start.merge(ty_span), )); } } self.pos = saved_pos; } return Ok(Expr::new(ExprKind::ArrayLit(Vec::new()), start.merge(end))); } // Первый элемент: `...spread`. Spread может быть **либо** в array // (`[...arr1, x, ...arr2]`), **либо** в map (`[...defaults, k:v]`). // Plan 55 followup (D108 spread): lookahead на second элемент чтобы // определить mode: // - `...spread` потом `, expr :` → map literal с map-spread. // - `...spread` потом `, expr ,` / `, expr ]` / `, ...spread` → array. // Edge cases: одиночный `[...spread]` — рассматривается как array // (legacy default, более частый use case). if self.eat(&TokenKind::DotDotDot).is_some() { let v = self.parse_expr()?; // Lookahead: после `, <expr> :` → map mode. if self.peek().kind == TokenKind::Comma { // Snapshot saved-position для potential rollback. Используем // peek_at для non-destructive lookahead. // Skip newlines после comma логически — но peek_at не делает // skip; используем sliding scan через peek_at. let mut i = 1usize; // skip comma at offset 0 // Skip newlines tokens (TokenKind::Newline). while matches!(self.peek_at(i).kind, TokenKind::Newline) { i += 1; } // Если следующий не-newline токен — `...` → array (still). // Иначе парсим expression и проверяем `:`. Чтобы не парсить // дважды — scan на наличие `:` до `,`/`]` на верхнем уровне. // Bootstrap: используем простой scan с depth counter. // Inline scan на верхнем уровне `[...]` — ищем `:` до `,`/`]`. // Depth counter для nested brackets/parens/braces. let mut depth = 0i32; let mut is_map = false; let mut j = i; loop { let t = &self.peek_at(j).kind; match t { TokenKind::LParen | TokenKind::LBracket | TokenKind::LBrace => depth += 1, TokenKind::RParen | TokenKind::RBrace => { if depth == 0 { break; } depth -= 1; } TokenKind::RBracket => { if depth == 0 { break; } depth -= 1; } TokenKind::Comma if depth == 0 => break, TokenKind::Colon if depth == 0 => { is_map = true; break; } TokenKind::Eof => break, _ => {} } j += 1; if j > i + 256 { break; } // safety — limit lookahead. } if is_map { // Map mode: convert уже распарсенный spread в MapElem. return self.parse_map_lit_rest(start, vec![MapElem::Spread(v)]); } } return self.parse_array_lit_rest(start, vec![ArrayElem::Spread(v)]); } let first = self.parse_expr()?; if matches!(self.peek().kind, TokenKind::Colon) { self.bump(); // : self.skip_newlines(); let first_val = self.parse_expr()?; return self.parse_map_lit_rest(start, vec![MapElem::Pair(first, first_val)]); } // Array-литерал: первый элемент уже распарсен. self.parse_array_lit_rest(start, vec![ArrayElem::Item(first)]) } /// Продолжает парсинг array-литерала после уже распарсенного первого /// элемента. Обрабатывает `,`-разделители, `...spread`, trailing comma. /// Если внутри встречается `expr :` — actionable error (смешение форм). fn parse_array_lit_rest( &mut self, start: Span, mut elems: Vec<ArrayElem>, ) -> Result<Expr, Diagnostic> { // После первого элемента: либо `,` (ещё элементы), либо `]`. loop { self.skip_newlines(); if matches!(self.peek().kind, TokenKind::RBracket) { break; } if self.eat(&TokenKind::Comma).is_none() { // Нет `,` и нет `]` — синтаксическая ошибка. Частый случай: // `[a b]` (забыли запятую). let span = self.peek().span; return Err(Diagnostic::new( format!( "expected `,` or `]` in array literal, got {}", self.peek().kind.name() ), span, )); } self.skip_newlines(); if matches!(self.peek().kind, TokenKind::RBracket) { break; // trailing comma } if self.eat(&TokenKind::DotDotDot).is_some() { let v = self.parse_expr()?; elems.push(ArrayElem::Spread(v)); } else { let v = self.parse_expr()?; // Смешение форм: `[a, b: c]` — после array-элемента видим `:`. if matches!(self.peek().kind, TokenKind::Colon) { return Err(Diagnostic::new( "cannot mix array and map syntax in `[...]` — either all \ elements are `k: v` pairs (map literal) or none are (array \ literal)", v.span, )); } elems.push(ArrayElem::Item(v)); } } let end = self.expect(&TokenKind::RBracket)?.span; Ok(Expr::new(ExprKind::ArrayLit(elems), start.merge(end))) } /// Продолжает парсинг map-литерала после уже распарсенной первой пары /// `k: v`. Обрабатывает `,`-разделители, trailing comma. Если внутри /// встречается элемент без `:` — actionable error (смешение форм). fn parse_map_lit_rest( &mut self, start: Span, mut elems: Vec<MapElem>, ) -> Result<Expr, Diagnostic> { loop { self.skip_newlines(); if matches!(self.peek().kind, TokenKind::RBracket) { break; } if self.eat(&TokenKind::Comma).is_none() { let span = self.peek().span; return Err(Diagnostic::new( format!( "expected `,` or `]` in map literal, got {}", self.peek().kind.name() ), span, )); } self.skip_newlines(); if matches!(self.peek().kind, TokenKind::RBracket) { break; // trailing comma } // Plan 55 followup (D108-spread): `...m` в map-литерале — // spread другой map. Должен быть совместимого типа. if self.eat(&TokenKind::DotDotDot).is_some() { let v = self.parse_expr()?; elems.push(MapElem::Spread(v)); continue; } let k = self.parse_expr()?; // Смешение форм: `[k: v, x]` — после map-пары элемент без `:`. if !matches!(self.peek().kind, TokenKind::Colon) { return Err(Diagnostic::new( "cannot mix map and array syntax in `[...]` — every entry of a \ map literal must be `key: value` (или `...map-spread`)", k.span, )); } self.bump(); // : self.skip_newlines(); let v = self.parse_expr()?; elems.push(MapElem::Pair(k, v)); } let end = self.expect(&TokenKind::RBracket)?.span; // Plan 52 Ф.7: inferred_key/value заполняются type-checker'ом // через MapLitCtx::annotate_module — для генерации turbofish // `HashMap[K,V].with_capacity(n)` в десугаринге. Парсер не имеет // type-info, оставляет None. Ok(Expr::new( ExprKind::MapLit { elems, inferred_key: None, inferred_value: None, inferred_target_type: None, }, start.merge(end), )) } // Plan 19, C13: try_parse_lambda удалена. Старая `(params) =>` // grammar отменена в Plan 19 D22-rev. Closure-light `|x| body` // и closure-full `fn(x T) -> R body` — единственные формы // безымянной функции. fn parse_if(&mut self) -> Result<Expr, Diagnostic> { let start = self.expect(&TokenKind::KwIf)?.span; // `if let pattern = expr { ... }` — Rust-форма RETRACTED (D184, // Plan 114; парсер-энфорс — реестр 221.1 №95, // [M-if-let-retraction-not-enforced]). Канон — unified pattern // grammar БЕЗ `let`: `if Pat = expr { ... }` (D34, 03-syntax.md:1540). if matches!(self.peek().kind, TokenKind::KwLet) { let sp = self.peek().span; return Err(Diagnostic::new( "[E_IF_LET_RETRACTED] `if let` retracted (Plan 114/D184) — \ write the pattern directly: `if Some(x) = expr { ... }`." .to_string(), sp, )); } // Plan 114 (D184): `if ro IDENT = e` / `if mut IDENT = e` — // identifier-pattern с explicit keyword (footgun protection). if matches!(self.peek().kind, TokenKind::KwRo | TokenKind::KwMut) { let _is_mut = matches!(self.peek().kind, TokenKind::KwMut); self.bump(); let pattern = self.parse_pattern()?; self.expect(&TokenKind::Eq)?; // Plan 106: parse scrutinee stopping before `&&` (parse_eq level). let scrutinee = self.with_no_struct_or_trailing(|p| p.parse_eq())?; let guard = if matches!(self.peek().kind, TokenKind::AmpAmp) { self.bump(); Some(Box::new(self.with_no_struct_or_trailing(|p| p.parse_expr())?)) } else { None }; let then = self.parse_block()?; let else_ = self.parse_optional_else()?; let end = then.span; return Ok(Expr::new( ExprKind::IfLet { pattern, scrutinee: Box::new(scrutinee), guard, then, else_, }, start.merge(end), )); } // Plan 114 (D184): `if consume Pat = e` запрещён. if matches!(self.peek().kind, TokenKind::KwConsume) { return Err(Diagnostic::new( "[E_CONSUME_IN_CONDITION] `consume` binding не разрешён \ в if/while condition — consume требует scope-exit \ tracking, который complicated в condition-position. \ Use match arm or extract в statement (Plan 114 D184).".to_string(), self.peek().span, )); } // Plan 114 (D184): speculative pattern-binding для constructor / // destructure patterns (`if Some(x) = e`, `if (a, b) = pair`, // `if { name, age } = user`). Если pattern парсится + следующий // токен `=` — это IfLet, иначе восстанавливаем pos и парсим bool. let save = self.pos; if let Ok(pattern) = self.parse_pattern() { if matches!(self.peek().kind, TokenKind::Eq) { // Allow if it's a structural pattern (constructor / tuple / // record / sum-variant). Plain identifier pattern в этой // позиции отвергаем — footgun protection (визуально // assignment-в-condition). if Self::is_structural_pattern(&pattern) { self.expect(&TokenKind::Eq)?; // Plan 106: parse scrutinee stopping before `&&` (parse_eq level). let scrutinee = self.with_no_struct_or_trailing(|p| p.parse_eq())?; let guard = if matches!(self.peek().kind, TokenKind::AmpAmp) { self.bump(); Some(Box::new(self.with_no_struct_or_trailing(|p| p.parse_expr())?)) } else { None }; let then = self.parse_block()?; let else_ = self.parse_optional_else()?; let end = then.span; return Ok(Expr::new( ExprKind::IfLet { pattern, scrutinee: Box::new(scrutinee), guard, then, else_, }, start.merge(end), )); } // Identifier-pattern без keyword'а — explicit error. if Self::is_ident_pattern(&pattern) { return Err(Diagnostic::new( "[E_AMBIGUOUS_IDENT_PATTERN] bare identifier pattern \ в if/while condition требует explicit `ro` или `mut` \ keyword (footgun protection: bare `if x = compute()` \ визуально неотличимо от assignment). \ Write `if ro IDENT = …` for immutable or \ `if mut IDENT = …` for mutable (Plan 114 D184).".to_string(), self.peek().span, )); } } // Pattern parsed but no `=` — restore and parse as bool expr. self.pos = save; } else { // Pattern parse failed — restore and try bool expr. self.pos = save; } let cond = self.with_no_struct_or_trailing(|p| p.parse_expr())?; let then = self.parse_block()?; let else_ = self.parse_optional_else()?; let end = then.span; Ok(Expr::new( ExprKind::If { cond: Box::new(cond), then, else_, }, start.merge(end), )) } fn parse_optional_else(&mut self) -> Result<Option<ElseBranch>, Diagnostic> { // newlines не должны мешать `else`, но обычно `}` else на той же строке let saved = self.pos; self.skip_newlines(); if !matches!(self.peek().kind, TokenKind::KwElse) { self.pos = saved; return Ok(None); } self.bump(); if matches!(self.peek().kind, TokenKind::KwIf) { let inner = self.parse_if()?; Ok(Some(ElseBranch::If(Box::new(inner)))) } else { let block = self.parse_block()?; Ok(Some(ElseBranch::Block(block))) } } fn parse_match(&mut self) -> Result<Expr, Diagnostic> { let start = self.expect(&TokenKind::KwMatch)?.span; let scrutinee = self.with_no_struct_or_trailing(|p| p.parse_expr())?; self.expect(&TokenKind::LBrace)?; let mut arms = Vec::new(); self.skip_newlines(); while !matches!(self.peek().kind, TokenKind::RBrace) { // Pattern alternation: `pat1 | pat2 | pat3 =>`. Собираем // в Pattern::Or если есть хотя бы один `|` после первого // pattern'а (но до `=>` / `if`-guard'а). let first = self.parse_pattern()?; let pattern = if matches!(self.peek().kind, TokenKind::Pipe) { let mut alts = vec![first]; let start_span = alts[0].span(); while matches!(self.peek().kind, TokenKind::Pipe) { self.bump(); self.skip_newlines(); alts.push(self.parse_pattern()?); } let end_span = alts.last().map(|p| p.span()).unwrap_or(start_span); Pattern::Or { alternatives: alts, span: start_span.merge(end_span), } } else { first }; let guard = if matches!(self.peek().kind, TokenKind::KwIf) { self.bump(); Some(self.parse_expr()?) } else { None }; self.expect(&TokenKind::FatArrow)?; self.skip_newlines(); // body: либо `{ block }` (D19 исключение), либо expr. // Special case: `return X` / `break` / `continue` — control-flow // statements в arm body. Идиоматично для early-exit: // match opt { Some(v) => v, None => return defaults } // Эмитим как Block с одним stmt'ом, тип unit (! фактически). let body = if matches!(self.peek().kind, TokenKind::KwReturn | TokenKind::KwBreak | TokenKind::KwContinue) { let stmt_or_expr = self.parse_stmt_or_expr()?; let stmts = match stmt_or_expr { StmtOrExpr::Stmt(s) => vec![s], StmtOrExpr::Expr(e) => vec![Stmt::Expr(e)], }; let last_span = stmts.last().map(|s| match s { Stmt::Return { span, .. } => *span, Stmt::Break(s) | Stmt::Continue(s) => *s, Stmt::Expr(e) => e.span, _ => pattern.span(), }).unwrap_or_else(|| pattern.span()); MatchArmBody::Block(Block { stmts, trailing: None, span: pattern.span().merge(last_span), is_unsafe: false }) } else if matches!(self.peek().kind, TokenKind::LBrace) { let saved = self.pos; if self.looks_like_record_lit() { // record-литерал — выражение self.pos = saved; MatchArmBody::Expr(self.parse_expr()?) } else { MatchArmBody::Block(self.parse_block()?) } } else { MatchArmBody::Expr(self.parse_expr()?) }; let span = pattern.span().merge(match &body { MatchArmBody::Expr(e) => e.span, MatchArmBody::Block(b) => b.span, }); arms.push(MatchArm { pattern, guard, body, span, }); // D452 (Plan 264): separator between arms depends on same-line // vs multi-line — see `expect_match_arm_separator`. self.expect_match_arm_separator()?; } let end = self.expect(&TokenKind::RBrace)?.span; Ok(Expr::new( ExprKind::Match { scrutinee: Box::new(scrutinee), arms, }, start.merge(end), )) } fn parse_for(&mut self) -> Result<Expr, Diagnostic> { let start = self.expect(&TokenKind::KwFor)?.span; // Plan 100.2 (D156): `for consume x in iter` — consume-iteration mode. // Mutually exclusive with `mut` (view and consume are different modes). // Must be checked BEFORE `mut` since consume is a distinct mode. let iter_consume = self.eat(&TokenKind::KwConsume).is_some(); // Plan 87 → Plan 108.3: `for mut x [TYPE] in` — `mut` помечает // loop-переменную мутабельной (D32/D33). После Plan 108.3 // `mut` semantic: enforce'ится type-checker'ом через `local_mut`. // `parse_pattern` ТОЖЕ принимает `mut` для per-name в pattern // (`for (mut a, b) in pairs`); здесь съедаем leading mut на // for-уровне для backward compat и инжектим в pattern. let loop_var_mut = if !iter_consume { self.eat(&TokenKind::KwMut).is_some() } else { false }; let mut pattern = self.parse_pattern()?; // Plan 108.3: leading `for mut x` распространяется на pattern's Ident. if loop_var_mut { if let Pattern::Ident { is_mut, .. } = &mut pattern { *is_mut = true; } } // Plan 87: явный тип элемента — `for x TYPE in iter`. Если после // loop-pattern сразу не `in` — это аннотация типа по правилу // «name type» (как `let x int`, `fn(x int)`, `[T Bound]`). let elem_type = if matches!(self.peek().kind, TokenKind::KwIn) { None } else { Some(self.parse_type()?) }; self.expect(&TokenKind::KwIn)?; let iter = self.with_no_struct_or_trailing(|p| p.parse_expr())?; // Plan 33.2 Ф.6 + 33.3 Ф.9.3/9.5/9.8: loop invariants/decreases. let (invs, decr) = self.parse_loop_clauses()?; let mut body = self.parse_block()?; Self::inject_loop_invariants(invs.clone(), &mut body); Self::inject_loop_decreases(decr.clone(), &mut body); let end = body.span; let loop_expr = Expr::new( ExprKind::For { pattern, iter: Box::new(iter), body, elem_type, invariants: invs.clone(), decreases: decr.map(Box::new), iter_consume, }, start.merge(end), ); Ok(Self::wrap_loop_with_preentry_check(loop_expr, &invs)) } /// `parallel for x in iter { body }` — D14 fan-out (D50 supervised + spawn). fn parse_parallel_for(&mut self) -> Result<Expr, Diagnostic> { let start = self.expect(&TokenKind::KwParallel)?.span; self.expect(&TokenKind::KwFor)?; // Plan 87: `parallel for mut x [TYPE] in` — консистентно с `for`. let _loop_var_mut = self.eat(&TokenKind::KwMut).is_some(); let pattern = self.parse_pattern()?; // Plan 87: явный тип элемента — `parallel for x TYPE in iter` // (консистентно с обычным `for`). let elem_type = if matches!(self.peek().kind, TokenKind::KwIn) { None } else { Some(self.parse_type()?) }; self.expect(&TokenKind::KwIn)?; let iter = self.with_no_struct_or_trailing(|p| p.parse_expr())?; let body = self.parse_block()?; let end = body.span; Ok(Expr::new( ExprKind::ParallelFor { pattern, iter: Box::new(iter), body, elem_type, }, start.merge(end), )) } fn parse_while(&mut self) -> Result<Expr, Diagnostic> { let start = self.expect(&TokenKind::KwWhile)?.span; // `while let pattern = expr` — Rust-форма RETRACTED (D184, Plan 114; // парсер-энфорс — реестр 221.1 №95, // [M-if-let-retraction-not-enforced]). Канон: `while Pat = expr { ... }` // (D34, 03-syntax.md:1540). if matches!(self.peek().kind, TokenKind::KwLet) { let sp = self.peek().span; return Err(Diagnostic::new( "[E_IF_LET_RETRACTED] `while let` retracted (Plan 114/D184) — \ write the pattern directly: `while Some(x) = expr { ... }`." .to_string(), sp, )); } // Plan 114 (D184): `while ro IDENT = e` / `while mut IDENT = e`. if matches!(self.peek().kind, TokenKind::KwRo | TokenKind::KwMut) { self.bump(); let pattern = self.parse_pattern()?; self.expect(&TokenKind::Eq)?; // Plan 106: parse scrutinee stopping before `&&` (parse_eq level). let scrutinee = self.with_no_struct_or_trailing(|p| p.parse_eq())?; let guard = if matches!(self.peek().kind, TokenKind::AmpAmp) { self.bump(); Some(Box::new(self.with_no_struct_or_trailing(|p| p.parse_expr())?)) } else { None }; let body = self.parse_block()?; let end = body.span; return Ok(Expr::new( ExprKind::WhileLet { pattern, scrutinee: Box::new(scrutinee), guard, body, invariants: vec![], decreases: None, }, start.merge(end), )); } if matches!(self.peek().kind, TokenKind::KwConsume) { return Err(Diagnostic::new( "[E_CONSUME_IN_CONDITION] `consume` binding не разрешён \ в while condition (Plan 114 D184).".to_string(), self.peek().span, )); } // Plan 114 (D184): speculative pattern-binding для constructor/destructure. let save = self.pos; if let Ok(pattern) = self.parse_pattern() { if matches!(self.peek().kind, TokenKind::Eq) { if Self::is_structural_pattern(&pattern) { self.expect(&TokenKind::Eq)?; // Plan 106: parse scrutinee stopping before `&&` (parse_eq level). let scrutinee = self.with_no_struct_or_trailing(|p| p.parse_eq())?; let guard = if matches!(self.peek().kind, TokenKind::AmpAmp) { self.bump(); Some(Box::new(self.with_no_struct_or_trailing(|p| p.parse_expr())?)) } else { None }; let body = self.parse_block()?; let end = body.span; return Ok(Expr::new( ExprKind::WhileLet { pattern, scrutinee: Box::new(scrutinee), guard, body, invariants: vec![], decreases: None, }, start.merge(end), )); } if Self::is_ident_pattern(&pattern) { return Err(Diagnostic::new( "[E_AMBIGUOUS_IDENT_PATTERN] bare identifier pattern в \ while condition требует `ro` или `mut` (Plan 114 D184).".to_string(), self.peek().span, )); } } self.pos = save; } else { self.pos = save; } let cond = self.with_no_struct_or_trailing(|p| p.parse_expr())?; // Plan 33.2 Ф.6 + 33.3 Ф.9.3/9.5/9.8: loop invariants/decreases. let (invs, decr) = self.parse_loop_clauses()?; let mut body = self.parse_block()?; Self::inject_loop_invariants(invs.clone(), &mut body); Self::inject_loop_decreases(decr.clone(), &mut body); let end = body.span; let loop_expr = Expr::new( ExprKind::While { cond: Box::new(cond), body, invariants: invs.clone(), decreases: decr.map(Box::new), }, start.merge(end), ); Ok(Self::wrap_loop_with_preentry_check(loop_expr, &invs)) } fn parse_loop(&mut self) -> Result<Expr, Diagnostic> { let start = self.expect(&TokenKind::KwLoop)?.span; // Plan 33.2 Ф.6 + 33.3 Ф.9.3/9.5/9.8: loop invariants/decreases. let (invs, decr) = self.parse_loop_clauses()?; let mut body = self.parse_block()?; Self::inject_loop_invariants(invs.clone(), &mut body); Self::inject_loop_decreases(decr.clone(), &mut body); let end = body.span; let loop_expr = Expr::new(ExprKind::Loop { body, invariants: invs.clone(), decreases: decr.map(Box::new) }, start.merge(end)); Ok(Self::wrap_loop_with_preentry_check(loop_expr, &invs)) } /// Plan 33.2 Ф.6 + 33.3 Ф.9.3 (D24): парсит loop-attached clauses. /// - `invariant <expr>` (multiple lines). /// - `decreases <expr>` (single, optional). /// /// Возвращает Vec<Expr> с invariants. `decreases` пока проглатывается /// (runtime check для loop-decreases — отдельная задача). /// /// **Plan 33.3 Ф.9.3**: invariants теперь возвращаются caller'у вместо /// игнорирования. Caller (parse_while/for/loop) inject'ит их в body /// как `assert_static`-statements: pre-loop, post-iteration. Это даёт /// runtime-check в debug-сборке. SMT verify (полноценный havoc-based) /// ждёт Z3 backend. fn parse_loop_clauses(&mut self) -> Result<(Vec<Expr>, Option<Expr>), Diagnostic> { let mut invariants = Vec::new(); let mut decreases: Option<Expr> = None; loop { self.skip_newlines(); match &self.peek().kind { TokenKind::Ident(n) if n == "invariant" => { self.bump(); let e = self.with_no_struct_or_trailing(|p| p.parse_expr())?; invariants.push(e); } TokenKind::Ident(n) if n == "decreases" => { if decreases.is_some() { let sp = self.peek().span; return Err(Diagnostic::new( "duplicate `decreases` clause on loop", sp)); } self.bump(); let e = self.with_no_struct_or_trailing(|p| p.parse_expr())?; decreases = Some(e); } _ => break, } } // Skip trailing newlines чтобы caller'у parse_block видеть `{`. self.skip_newlines(); Ok((invariants, decreases)) } /// Plan 33.3 Ф.9.8: inject loop `decreases` runtime check. /// До body эмитим `let _nova_decr_old = <decreases_expr>` (snapshot). /// После body эмитим `assert_static <decreases_expr> < _nova_decr_old` /// (проверка decrement). fn inject_loop_decreases(decreases: Option<Expr>, body: &mut Block) { let Some(d) = decreases else { return }; let span = d.span; // Synthesize: let _nova_decr_old = <d> let snapshot_let = Stmt::Let(LetDecl { mutable: false, pattern: Pattern::Ident { name: "_nova_decr_old".into(), span, is_mut: false, is_consume: false }, ty: None, value: d.clone(), span, is_ghost: false, consume: false, }); // Synthesize: assert_static (<d>) < _nova_decr_old let check_expr = Expr::new( ExprKind::Binary { op: BinOp::Lt, left: Box::new(d.clone()), right: Box::new(Expr::new(ExprKind::Ident("_nova_decr_old".into()), span)), }, span, ); let check_stmt = Stmt::AssertStatic { expr: check_expr, span }; // Snapshot — в начало body, check — в конец. body.stmts.insert(0, snapshot_let); body.stmts.push(check_stmt); } /// Inject invariant'ы в body цикла как assert_static-stmt'ы. /// Per-iteration: prepend invariants в начало body — check срабатывает /// перед каждой итерацией (после первой). /// /// Pre-entry check делается отдельно через `wrap_loop_with_preentry_check` /// (Plan 33.3 Ф.9.5) — wrap'ит loop-expr в Block с pre-entry asserts /// перед loop'ом. fn inject_loop_invariants(invariants: Vec<Expr>, body: &mut Block) { for inv in invariants.into_iter().rev() { let span = inv.span; body.stmts.insert(0, Stmt::AssertStatic { expr: inv, span }); } } /// Plan 33.3 Ф.9.5: wrap loop-expression в Block с pre-entry assert_static /// для каждого invariant. Это catches violation **до** первой итерации /// (когда invariant ложен от старта или loop никогда не выполняется). fn wrap_loop_with_preentry_check(loop_expr: Expr, invariants: &[Expr]) -> Expr { if invariants.is_empty() { return loop_expr; } let span = loop_expr.span; let stmts: Vec<Stmt> = invariants.iter() .map(|inv| Stmt::AssertStatic { expr: inv.clone(), span: inv.span }) .collect(); // Trailing: loop сам — final expr block'а (loop возвращает unit). let block = Block { stmts, trailing: Some(Box::new(loop_expr)), span, is_unsafe: false }; Expr::new(ExprKind::Block(block), span) } fn parse_with(&mut self) -> Result<Expr, Diagnostic> { let start = self.expect(&TokenKind::KwWith)?.span; let mut bindings = Vec::new(); loop { // Plan 33.3 Ф.9.6: optional `#verify_handler` или `#trusted_handler` // перед effect-name. Применяется к ЭТОЙ конкретной binding'е. let verification = self.parse_handler_verification_attr()?; let effect = self.parse_type()?; self.expect(&TokenKind::Eq)?; // handler — выражение. handler-литерал (`EffName { op() => ... }`) // приходит как `Path` + record-like, мы обрабатываем как expr // (эвристика record_lit может ошибиться; для надёжности парсим // handler-литерал отдельно если за typeref идёт `{` с // методами, не полями). let handler = self.parse_expr_or_handler_lit()?; let span = effect.span().merge(handler.span); bindings.push(WithBinding { effect, handler, span, verification, }); if self.eat(&TokenKind::Comma).is_none() { break; } self.skip_newlines(); } let body = self.parse_block()?; let end = body.span; Ok(Expr::new( ExprKind::With { bindings, body }, start.merge(end), )) } /// Plan 33.3 Ф.9.6: парсит `#verify` или `#trusted` (без аргументов) /// перед with-binding'ом. Без атрибута — Unverified. /// Дублирующиеся / противоречащие атрибуты → error. /// /// Refactor: раньше `#verify_handler` / `#trusted_handler` — /// упростили до `#verify` / `#trusted` (контекст определяет /// смысл — внутри with-binding это про handler). fn parse_handler_verification_attr(&mut self) -> Result<HandlerVerification, Diagnostic> { if !matches!(self.peek().kind, TokenKind::Hash) { return Ok(HandlerVerification::Unverified); } let name = match &self.peek_at(1).kind { TokenKind::Ident(n) => n.clone(), _ => return Ok(HandlerVerification::Unverified), }; let result = match name.as_str() { "verify" => HandlerVerification::Verify, "trusted" => HandlerVerification::Trusted, _ => return Ok(HandlerVerification::Unverified), }; self.bump(); // # self.bump(); // ident self.skip_newlines(); // Disallow stacking `#verify #trusted` (контрадикция). if matches!(self.peek().kind, TokenKind::Hash) { if let TokenKind::Ident(next) = &self.peek_at(1).kind { if next == "verify" || next == "trusted" { let span = self.peek().span; return Err(Diagnostic::new( "duplicate or conflicting handler verification attribute \ (`#verify` and `#trusted` are mutually exclusive)", span, )); } } } Ok(result) } /// Парсит handler-value в позиции `with EFFECT = <handler> { body }`. /// /// До D61 здесь жила эвристика, распознающая bare-form handler-литерал /// без keyword'а (`EffName { op(...) => ... }` угадывался по «после `{` /// первый токен — `Ident (`»). D61 сделало keyword-префикс (`handler`, /// затем `effect` — Plan 97 Ф.3) ОБЯЗАТЕЛЬНЫМ для handler-литерала /// (см. spec/decisions/04-effects.md «Слово `handler` — keyword (D61)»): /// inline handler-литерал теперь всегда парсится через `parse_handler_lit` /// (keyword `effect`, диспетчеризуется в основном expr-парсере по /// `TokenKind::KwEffect`), а НЕ через эту функцию. /// /// Эвристика пережила D61 как dead code и ложно триггерилась на bare- /// identifier handler-value, чьё with-тело начиналось с вызова вида /// `Ident(...)` — например `with Db = primary { b() }`: `primary` /// парсился как path, `{` — как начало литерала, `b(` внутри — как /// «похоже на сигнатуру handler-метода», и парсер уходил разбирать /// `{ b() }` как тело handler-литерала (падая на отсутствующем `=>`/`{` /// после `b()`, встретив закрывающую `}` самого with-тела). Найдено /// Plan 197 audit на `examples/real_world/orm_decorators.nv:145` /// (nested `with` внутри тела handler-метода — closure-аргумент /// вызова другого handler-метода). Regression-guard: /// spec_tests/conformance/d11_with_value_body_nested_in_handler_method.nv. /// /// Fix: удалить эвристику целиком — handler-value в `with`-биндинге /// это ВСЕГДА обычное выражение (bare identifier, вызов, `effect Name /// {...}` литерал и т.д.), парсим `parse_expr()` напрямую. fn parse_expr_or_handler_lit(&mut self) -> Result<Expr, Diagnostic> { // Handler — это выражение в позиции `with E = <expr> { body }`. // Чтобы `(e) => interrupt Some(e)` не "сожрало" следующий `{`-block // как trailing-block, парсим в режиме no_trailing_block. С этим // флагом `interrupt Some(e) { body }` остановится на `interrupt Some(e)`, // а `{ body }` достанется внешнему with-парсеру. let saved_trailing = self.no_trailing_block; self.no_trailing_block = true; let result = self.parse_expr(); self.no_trailing_block = saved_trailing; result } fn parse_interrupt_expr(&mut self) -> Result<Expr, Diagnostic> { let start = self.expect(&TokenKind::KwInterrupt)?.span; let value = if self.at_newline() || matches!(self.peek().kind, TokenKind::RBrace) { None } else { Some(Box::new(self.parse_expr()?)) }; let end = match &value { Some(e) => e.span, None => start, }; Ok(Expr::new( ExprKind::Interrupt(value), start.merge(end), )) } /// Plan 97 Ф.4 (D142): `protocol ProtoName { method-impl* }` — value- /// литерал, реализующий контракт named-protocol'а (один-в-один по /// сигнатуре, structural check в type-checker'е). Парсер /// переиспользует body-парсер handler-методов (parse_handler_methods). /// Disambig от type-position `protocol { sig* }` (Ф.2): expr-position /// требует IDENT после keyword'а. fn parse_protocol_lit(&mut self) -> Result<Expr, Diagnostic> { let start = self.expect(&TokenKind::KwProtocol)?.span; if !matches!(self.peek().kind, TokenKind::Ident(_)) { return Err(Diagnostic::new( "expected named protocol after `protocol` in expression-position \ (anonymous protocol-literal in expression is not allowed; \ D142 requires a named protocol contract for structural check). \ Use `protocol ProtoName { method-impl* }`.", self.peek().span, )); } let mut path = vec![self.parse_ident()?.0]; while matches!(self.peek().kind, TokenKind::Dot) && matches!(self.peek_at(1).kind, TokenKind::Ident(_)) { self.bump(); path.push(self.parse_ident()?.0); } // Опциональные generic-параметры (Iter[T], FromIter[T], etc.) if matches!(self.peek().kind, TokenKind::LBracket) { let _ = self.parse_type_args()?; } self.expect(&TokenKind::LBrace)?; self.skip_newlines(); let methods = self.parse_handler_methods()?; let end = self.expect(&TokenKind::RBrace)?.span; Ok(Expr::new( ExprKind::ProtocolLit { proto_name: path, methods, }, start.merge(end), )) } /// `effect EffectName { ops }` — keyword-форма handler-литерала /// (D61, переименована Plan 97 Ф.3 / D142: `handler` → `effect`). /// Реиспользует существующую логику парсинга handler-method'ов. /// Внутреннее имя AST-узла `HandlerLit` сохранено (рефакторинг /// имён — отдельный noise; семантика та же). fn parse_handler_lit(&mut self) -> Result<Expr, Diagnostic> { let start = self.expect(&TokenKind::KwEffect)?.span; // Имя эффекта — dotted path, опционально с generic-параметрами. let mut path = vec![self.parse_ident()?.0]; while matches!(self.peek().kind, TokenKind::Dot) && matches!(self.peek_at(1).kind, TokenKind::Ident(_)) { self.bump(); path.push(self.parse_ident()?.0); } // Опциональные generic-параметры эффекта (Fail[Error], etc.) // — парсим, но в bootstrap не используем (хранится в name path). if matches!(self.peek().kind, TokenKind::LBracket) { // Пропускаем generic-аргументы целиком let _ = self.parse_type_args()?; } self.expect(&TokenKind::LBrace)?; self.skip_newlines(); let methods = self.parse_handler_methods()?; let end = self.expect(&TokenKind::RBrace)?.span; Ok(Expr::new( ExprKind::HandlerLit { effect_name: path, methods, }, start.merge(end), )) } /// Извлечённая логика парсинга handler-method'ов. /// Используется в parse_handler_lit (`effect Name { ops }`) и /// parse_protocol_lit (`protocol Name { method-impl* }`). fn parse_handler_methods(&mut self) -> Result<Vec<HandlerMethod>, Diagnostic> { let mut methods = Vec::new(); while !matches!(self.peek().kind, TokenKind::RBrace) { let (mname, mspan) = self.parse_ident()?; self.expect(&TokenKind::LParen)?; let mut params = Vec::new(); while !matches!(self.peek().kind, TokenKind::RParen) { let (pname, pspan) = self.parse_ident()?; let pty = if !matches!( self.peek().kind, TokenKind::Comma | TokenKind::RParen ) { let attempt = self.pos; match self.parse_type() { Ok(t) => Some(t), Err(_) => { self.pos = attempt; None } } } else { None }; params.push(HandlerMethodParam { name: pname, ty: pty, span: pspan, }); if self.eat(&TokenKind::Comma).is_none() { break; } } self.expect(&TokenKind::RParen)?; // Plan 175.2 Ф.2-v4 (П4): optional `-> Type` after the param // list — parsed for BOTH callers (`effect X {...}` handler- // literals AND `protocol P {...}` method-impls), stored as // `Option<TypeRef>`. Mandatory-ness (E_INCOMPLETE_HANDLER_OP_DECL) // is a CHECKER rule (`check_handler_op_declarations`) scoped ONLY // to `HandlerLit` — `ProtocolLit` method-impls stay optional // here (unchanged, out of scope for this D-амендмент). let ret_ty = if self.eat(&TokenKind::Arrow).is_some() { Some(self.parse_type()?) } else { None }; let body = match self.peek().kind { TokenKind::FatArrow => { self.bump(); self.skip_newlines(); HandlerMethodBody::Expr(self.parse_expr()?) } TokenKind::LBrace => HandlerMethodBody::Block(self.parse_block()?), _ => { return Err(Diagnostic::new( "expected `=>` or `{` for handler-method body", self.peek().span, )); } }; let end = match &body { HandlerMethodBody::Expr(e) => e.span, HandlerMethodBody::Block(b) => b.span, }; methods.push(HandlerMethod { name: mname, params, ret_ty, body, span: mspan.merge(end), }); self.skip_newlines(); } Ok(methods) } fn parse_spawn(&mut self) -> Result<Expr, Diagnostic> { let start = self.expect(&TokenKind::KwSpawn)?.span; // Plan 173.3 Ф.3 (D415 §4): `spawn consume c [= expr] { body }` — // EXPLICIT move-capture into the child fiber's OWNING scope (cleanup // fires when the CHILD body exits, not the lexical block). Mirror of // `consume c = expr { body }` (D188 ConsumeScope) — desugars to // `Spawn(Block[ConsumeScope])` by reusing THAT machinery verbatim: // the ConsumeScope's own body IS the spawn body, so its existing // cleanup-on-body-exit codegen already fires at child-fiber exit. // No new runtime plumbing needed (D415 §4 design note). // // `spawn consume c { body }` (no `= expr`) — `c` already bound in the // OUTER (parent) scope; re-consuming it here transfers ownership into // the child exactly like any other outer-scope reference already // gets captured into the spawned closure (D415 §4 "already-bound" // form) — the desugar's ConsumeScope `init` is simply `Ident(c)`. if matches!(self.peek().kind, TokenKind::KwConsume) { self.bump(); // consume let (name, name_span) = self.parse_ident()?; // №379: `spawn consume a, b, ... { body }` — multi-var mirror of // this single-var form (D415-amendment). Comma after the first // ident routes to the shared multi-var desugar helper. if matches!(self.peek().kind, TokenKind::Comma) { let (wrapped, cs_span) = self.parse_spawn_detach_consume_multivar( start, name, name_span, "spawn", )?; let body_expr = Expr::new(ExprKind::Block(wrapped), cs_span); return Ok(Expr::new(ExprKind::Spawn(Box::new(body_expr)), cs_span)); } let init = if matches!(self.peek().kind, TokenKind::Eq) { self.bump(); // = self.skip_newlines(); let saved_trailing = self.no_trailing_block; self.no_trailing_block = true; let e = self.parse_expr(); self.no_trailing_block = saved_trailing; e? } else { Expr::new(ExprKind::Ident(name.clone()), name_span) }; self.skip_newlines(); if !matches!(self.peek().kind, TokenKind::LBrace) { return Err(Diagnostic::new( "`spawn consume c [= expr]` requires a block body `{ ... }` \ (D415 §4) — the child fiber owns `c`; its cleanup fires \ when the child body exits, not the lexical block." .to_string(), self.peek().span, )); } let user_body = self.parse_block()?; let cs_span = start.merge(user_body.span); let wrapped = Block { stmts: vec![Stmt::ConsumeScope { binding: name, type_annot: None, init, body: user_body, // D415 §4 already-bound form — НЕ Plan-201 re-consume: // у spawn-формы свои правила (move-out-запрет не вводился). re_consume: false, result: None, span: cs_span, }], trailing: None, span: cs_span, is_unsafe: false, }; let body_expr = Expr::new(ExprKind::Block(wrapped), cs_span); return Ok(Expr::new(ExprKind::Spawn(Box::new(body_expr)), cs_span)); } let body = self.parse_expr()?; let span = start.merge(body.span); Ok(Expr::new(ExprKind::Spawn(Box::new(body)), span)) } /// `supervised { body }` / `supervised(cancel: tok) { body }` — /// structured-concurrency scope (D50 / D75 revised, Plan 47). /// /// После `supervised` опционально идёт `( cancel : expr )` — единственный /// допустимый именованный аргумент keyword-конструкции (V1). Прочие имена /// или позиционная форма → diagnostic. fn parse_supervised(&mut self) -> Result<Expr, Diagnostic> { let start = self.expect(&TokenKind::KwSupervised)?.span; // Именованные аргументы `cancel:` / `deadline:` / `timeout:` в любом // порядке, через запятую (Plan 174 / D349 — расширение план-47 формы). // Каждый — не более одного раза; `deadline:` и `timeout:` взаимно // исключающи (обе задают срок; выбери одну форму). let mut cancel: Option<Box<Expr>> = None; let mut deadline: Option<crate::ast::SupervisedDeadline> = None; // D449: `on_timeout:` — handler called instead of raising // `Fail[TimeoutError]` when THIS scope's own deadline/timeout // expires. `on_timeout_span` remembers where it was written so the // post-loop "requires deadline:/timeout:" check below can point the // diagnostic at the argument itself, not at `supervised`. let mut on_timeout: Option<Box<Expr>> = None; let mut on_timeout_span: Option<Span> = None; if matches!(self.peek().kind, TokenKind::LParen) { self.bump(); // ( self.skip_newlines(); loop { let arg_span = self.peek().span; let name = match &self.peek().kind { TokenKind::Ident(n) if n == "cancel" || n == "deadline" || n == "timeout" || n == "on_timeout" => { n.clone() } other => { return Err(Diagnostic::new( format!( "`supervised` accepts only named arguments \ `cancel:`, `deadline:`, `timeout:`, `on_timeout:` \ (got {}); use `supervised(cancel: tok) {{ ... }}`, \ `supervised(deadline: mono) {{ ... }}`, \ `supervised(timeout: dur) {{ ... }}` or \ `supervised(timeout: dur, on_timeout: |e| ...) {{ ... }}`", other.name() ), self.peek().span, )); } }; self.bump(); // name self.expect(&TokenKind::Colon)?; self.skip_newlines(); let expr = self.parse_expr()?; match name.as_str() { "cancel" => { if cancel.is_some() { return Err(Diagnostic::new( "duplicate `cancel:` argument in `supervised`".to_string(), arg_span, )); } cancel = Some(Box::new(expr)); } kind @ ("deadline" | "timeout") => { if deadline.is_some() { return Err(Diagnostic::new( "`supervised` accepts at most one of `deadline:` / \ `timeout:` (both set a scope deadline — pick one form; \ `timeout: d` == `deadline: Monotonic.now() + d`)" .to_string(), arg_span, )); } deadline = Some(crate::ast::SupervisedDeadline { expr: Box::new(expr), relative: kind == "timeout", span: arg_span, }); } "on_timeout" => { if on_timeout.is_some() { return Err(Diagnostic::new( "duplicate `on_timeout:` argument in `supervised`".to_string(), arg_span, )); } on_timeout = Some(Box::new(expr)); on_timeout_span = Some(arg_span); } _ => unreachable!(), } self.skip_newlines(); if self.eat(&TokenKind::Comma).is_some() { self.skip_newlines(); // Разрешаем trailing-comma перед `)`. if matches!(self.peek().kind, TokenKind::RParen) { break; } continue; } break; } self.skip_newlines(); self.expect(&TokenKind::RParen)?; } // D449: `on_timeout:` without `deadline:`/`timeout:` can NEVER fire // (no deadline is ever armed) — silent dead code, forbidden by the // language's never-silent-garbage discipline (D317, same principle // D408's zero/negative-deadline rule cites). Reject at parse time, // same place the `cancel`/`deadline` mutual-exclusion check lives. if let (Some(_), None) = (&on_timeout, &deadline) { return Err(Diagnostic::new( "[E_SUPERVISED_ON_TIMEOUT_NO_DEADLINE] `on_timeout:` requires \ `deadline:` or `timeout:` — without a scope deadline this \ handler could never fire (D449); add `timeout: d` (or \ `deadline: mono`) alongside `on_timeout:`" .to_string(), on_timeout_span.expect("on_timeout is Some ⇒ its span was recorded"), )); } let block = self.parse_block()?; let end = block.span; Ok(Expr::new( ExprKind::Supervised { body: block, cancel, deadline, on_timeout }, start.merge(end), )) } /// `detach { body }` — fire-and-forget, global supervisor (D50). /// /// [M-detach-consume-escape-unchecked] (D415 §4 extension, Plan 173.3): /// `detach consume c [= expr] { body }` — mirror of `spawn consume` /// (see `parse_spawn` above for the full desugar rationale). `detach` is /// JUST as concurrent a boundary as `spawn` (orphan fiber, may run on /// another OS thread after the enclosing scope has already exited) — a /// bare `detach { … stream … }` capturing a `consume`-typed outer /// binding (e.g. `TcpStream`) by reference is a use-after-consume / /// use-after-free once the owning scope exits before the orphan fiber /// runs. This explicit-move form is the escape valve the type-checker's /// `E_LINEAR_CAPTURE_IN_FIBER` (types/mod.rs `check_capture_boundary`) /// now requires. Desugars to `Detach(Block[ConsumeScope])` — reuses the /// SAME `Stmt::ConsumeScope` machinery `spawn consume` uses (D188), just /// without the extra `Expr::Block` wrapper `Spawn` needs (`Detach` /// already takes a bare `Block`, not a boxed `Expr`). fn parse_detach(&mut self) -> Result<Expr, Diagnostic> { let start = self.expect(&TokenKind::KwDetach)?.span; if matches!(self.peek().kind, TokenKind::KwConsume) { self.bump(); // consume let (name, name_span) = self.parse_ident()?; // №379: `detach consume a, b, ... { body }` — multi-var mirror, // symmetric with `spawn consume a, b, ... { body }` // (D415-amendment). Comma after the first ident routes to the // shared multi-var desugar helper. if matches!(self.peek().kind, TokenKind::Comma) { let (wrapped, cs_span) = self.parse_spawn_detach_consume_multivar( start, name, name_span, "detach", )?; return Ok(Expr::new(ExprKind::Detach(wrapped), cs_span)); } let init = if matches!(self.peek().kind, TokenKind::Eq) { self.bump(); // = self.skip_newlines(); let saved_trailing = self.no_trailing_block; self.no_trailing_block = true; let e = self.parse_expr(); self.no_trailing_block = saved_trailing; e? } else { Expr::new(ExprKind::Ident(name.clone()), name_span) }; self.skip_newlines(); if !matches!(self.peek().kind, TokenKind::LBrace) { return Err(Diagnostic::new( "`detach consume c [= expr]` requires a block body `{ ... }` \ (D415 §4) — the orphan fiber owns `c`; its cleanup fires \ when the fiber's own body exits, not the lexical block." .to_string(), self.peek().span, )); } let user_body = self.parse_block()?; let cs_span = start.merge(user_body.span); let wrapped = Block { stmts: vec![Stmt::ConsumeScope { binding: name, type_annot: None, init, body: user_body, // D415 §4 already-bound form — mirrors `spawn consume`: // не Plan-201 re-consume, у detach-формы свои правила. re_consume: false, result: None, span: cs_span, }], trailing: None, span: cs_span, is_unsafe: false, }; return Ok(Expr::new(ExprKind::Detach(wrapped), cs_span)); } let block = self.parse_block()?; let end = block.span; Ok(Expr::new(ExprKind::Detach(block), start.merge(end))) } /// №379 (D415-amendment): `spawn consume a, b, ... { body }` / /// `detach consume a, b, ... { body }` — multi-var mirror of the /// single-var `spawn consume c { body }` form (D415 §4). NOT the D188- /// multivar `consume A, B, C { body }` sugar (`parse_multi_reconsume_ /// scope`) — that gives a RO view inside (`E_CONSUME_BLOCK_MOVE_OUT` /// blocks passing a listed binding into a consume-param), because it /// re_consume's the SAME still-owning lexical scope. Here every listed /// binding transfers REAL OWNERSHIP into the child fiber, same as the /// single-var spawn/detach form: each nested layer uses /// `re_consume: false` (D415 §4 "already-bound" form, not the Plan-201 /// re-consume view — no move-out restriction is introduced). /// /// Desugar is nested `Stmt::ConsumeScope`, innermost layer = the real /// user body — same shape `parse_multi_reconsume_scope` uses for the /// non-spawn D188-multivar sugar. The checker's spawn/detach free- /// variable capture scan (`capture_scan_stmt`'s `Stmt::ConsumeScope` /// arm, types/mod.rs) walks the WHOLE closure body for owned/linear /// references regardless of nesting depth and exempts every `init` /// Ident along the chain — so all listed bindings are captured (moved) /// into the child at the ONE `spawn`/`detach` statement point, before /// the child body starts running. Nesting is parse-time sugar only; it /// does not introduce a sequential-move-at-runtime story. /// /// Cleanup order: LIFO — the last-listed binding's `ConsumeScope` is /// innermost (closest to the user body), so its cleanup fires FIRST on /// the way back out, mirroring D188-multivar's documented order. See /// spec/decisions/06-concurrency.md D415 §4 amendment. /// /// Caller has already consumed `spawn`/`detach consume` and the FIRST /// ident (`first_name`/`first_span`); this is called on seeing a `,` /// after it. The list is re-consume-only (no `=` init) — mixing with /// the binding form (`spawn consume a, b = expr { … }`) is a parse /// error, mirroring D188-multivar's own mix-ban. fn parse_spawn_detach_consume_multivar( &mut self, start: Span, first_name: String, first_span: Span, keyword: &'static str, ) -> Result<(Block, Span), Diagnostic> { let mut idents: Vec<(String, Span)> = vec![(first_name, first_span)]; loop { if self.eat(&TokenKind::Comma).is_some() { let (name, name_span) = self.parse_ident()?; idents.push((name, name_span)); continue; } break; } if matches!(self.peek().kind, TokenKind::Eq) { return Err(Diagnostic::new( format!( "[E_SPAWN_CONSUME_MULTIVAR_BINDING_MIX] `{kw} consume a, b = expr {{ … }}` \ is not valid (№379): multi-var `{kw} consume` moves EXISTING owned \ bindings into the child fiber and does not support `=`-initialization; \ the binding form (`{kw} consume c = expr {{ body }}`) stays single-ident.", kw = keyword ), self.peek().span, )); } self.skip_newlines(); if !matches!(self.peek().kind, TokenKind::LBrace) { return Err(Diagnostic::new( format!( "`{kw} consume a, b, ... {{ ... }}` requires a block body `{{ ... }}` \ (№379, D415-amendment) — the child fiber owns every listed binding; \ cleanup fires when the child body exits, not the lexical block.", kw = keyword ), self.peek().span, )); } let user_body = self.parse_block()?; let cs_span = start.merge(user_body.span); let mut inner_body = user_body; for (name, name_span) in idents.into_iter().rev() { let scope_span = name_span.merge(inner_body.span); let scope_stmt = Stmt::ConsumeScope { binding: name.clone(), type_annot: None, init: Expr::new(ExprKind::Ident(name), name_span), body: inner_body, re_consume: false, result: None, span: scope_span, }; inner_body = Block { stmts: vec![scope_stmt], trailing: None, span: scope_span, is_unsafe: false, }; } // `inner_body` now: a Block with exactly ONE stmt, the OUTERMOST // (first-listed) `Stmt::ConsumeScope`, spanning only its own // name+inner-body merge. Force both the wrapping Block's span AND // the outer ConsumeScope's own span to the full `cs_span` (mirrors // `parse_multi_reconsume_scope`'s outer-span fixup) for accurate // diagnostic positions on the whole construct. inner_body.span = cs_span; if let Some(Stmt::ConsumeScope { span, .. }) = inner_body.stmts.first_mut() { *span = cs_span; } Ok((inner_body, cs_span)) } /// Plan 113 (D172): `blocking { }` block-form is removed. /// Emit a parse error directing users to extract body into `#blocking fn`. fn parse_blocking(&mut self) -> Result<Expr, Diagnostic> { let span = self.expect(&TokenKind::KwBlocking)?.span; // Consume the block so the parser can continue after the error. let _ = self.parse_block(); Err(Diagnostic::new( "[D172-block-form-removed] `blocking { }` block-form has been removed (Plan 113). \ Extract the body into a `#blocking fn` and call it instead. \ See spec/decisions/06-concurrency.md §D172." .to_string(), span, )) } /// `forbid X1, X2, ... { body }` — capability sandbox (D63). fn parse_forbid(&mut self) -> Result<Expr, Diagnostic> { let start = self.expect(&TokenKind::KwForbid)?.span; // Список эффектов через запятую до открывающего `{`. let mut effects = Vec::new(); loop { effects.push(self.parse_type()?); if self.eat(&TokenKind::Comma).is_none() { break; } self.skip_newlines(); } let body = self.parse_block()?; let end = body.span; Ok(Expr::new( ExprKind::Forbid { effects, body }, start.merge(end), )) } /// `select { arm* }` --- D94 multiplexed channel operation. fn parse_select(&mut self) -> Result<Expr, Diagnostic> { let start = self.expect(&TokenKind::KwSelect)?.span; self.expect(&TokenKind::LBrace)?; let mut arms: Vec<SelectArm> = Vec::new(); self.skip_newlines(); while !matches!(self.peek().kind, TokenKind::RBrace) { let arm_start = self.peek().span; let op = self.parse_select_op()?; let guard = if matches!(self.peek().kind, TokenKind::KwIf) { self.bump(); Some(self.parse_expr()?) } else { None }; self.expect(&TokenKind::FatArrow)?; self.skip_newlines(); let body = self.parse_block()?; let arm_span = arm_start.merge(body.span); arms.push(SelectArm { op, guard, body, span: arm_span }); self.eat(&TokenKind::Comma); self.skip_newlines(); } let end = self.expect(&TokenKind::RBrace)?.span; Ok(Expr::new(ExprKind::Select { arms }, start.merge(end))) } fn parse_select_op(&mut self) -> Result<SelectOp, Diagnostic> { // `Some(ident) = expr` --- recv arm with binding if matches!(self.peek().kind, TokenKind::Ident(ref s) if s == "Some") { let saved = self.pos; self.bump(); if matches!(self.peek().kind, TokenKind::LParen) { self.bump(); if let TokenKind::Ident(binding_s) = self.peek().kind.clone() { self.bump(); if matches!(self.peek().kind, TokenKind::RParen) { self.bump(); if matches!(self.peek().kind, TokenKind::Eq) { self.bump(); let chan = self.parse_expr()?; return Ok(SelectOp::Recv { binding: Some(binding_s), none_arm: false, chan: Box::new(chan) }); } } } } self.pos = saved; } // Plan 173 Ф.3 п.3 (D94): `None = expr` recv arm — fires only on // closed+empty channel (distinguishes closed from value). if matches!(self.peek().kind, TokenKind::Ident(ref s) if s == "None") { let saved = self.pos; self.bump(); if matches!(self.peek().kind, TokenKind::Eq) { self.bump(); let chan = self.parse_expr()?; return Ok(SelectOp::Recv { binding: None, none_arm: true, chan: Box::new(chan) }); } self.pos = saved; } // `_ = expr` recv arm or `_` default arm if matches!(self.peek().kind, TokenKind::Ident(ref s) if s == "_") { let saved = self.pos; self.bump(); if matches!(self.peek().kind, TokenKind::Eq) { self.bump(); let chan = self.parse_expr()?; return Ok(SelectOp::Recv { binding: None, none_arm: false, chan: Box::new(chan) }); } self.pos = saved; self.bump(); return Ok(SelectOp::Default); } // Send arm: `chan.send(value)` let chan = self.parse_primary()?; self.expect(&TokenKind::Dot)?; let method_span = self.peek().span; match &self.peek().kind { TokenKind::Ident(s) if s == "send" => { self.bump(); } _ => return Err(Diagnostic::new( "select send arm: expected `.send(value)` after channel expression".to_string(), method_span, )), } self.expect(&TokenKind::LParen)?; let value = self.parse_expr()?; self.expect(&TokenKind::RParen)?; Ok(SelectOp::Send { chan: Box::new(chan), value: Box::new(value) }) } /// Plan 113 (D172): `realtime { }` block-form is removed. /// Emit a parse error directing users to extract body into `#realtime fn`. fn parse_realtime(&mut self) -> Result<Expr, Diagnostic> { let span = self.expect(&TokenKind::KwRealtime)?.span; // Consume optional `nogc` modifier and the block so parser can continue. if let TokenKind::Ident(name) = &self.peek().kind { if name == "nogc" { self.bump(); } } let _ = self.parse_block(); Err(Diagnostic::new( "[D172-block-form-removed] `realtime { }` block-form has been removed (Plan 113). \ Extract the body into a `#realtime fn` (callee guarantee) and call it instead. \ See spec/decisions/06-concurrency.md §D172." .to_string(), span, )) } // ─── block & stmts ─────────────────────────────────────────────────── fn parse_block(&mut self) -> Result<Block, Diagnostic> { let start = self.expect(&TokenKind::LBrace)?.span; let mut stmts = Vec::new(); let mut trailing: Option<Box<Expr>> = None; self.skip_newlines(); while !matches!(self.peek().kind, TokenKind::RBrace) { // Пытаемся определить statement vs expression. let stmt_or_expr = self.parse_stmt_or_expr()?; match stmt_or_expr { StmtOrExpr::Stmt(s) => { stmts.push(s); self.expect_stmt_separator()?; } StmtOrExpr::Expr(e) => { self.expect_stmt_separator()?; if matches!(self.peek().kind, TokenKind::RBrace) { trailing = Some(Box::new(e)); } else { stmts.push(Stmt::Expr(e)); } } } } let end = self.expect(&TokenKind::RBrace)?.span; Ok(Block { stmts, trailing, span: start.merge(end), is_unsafe: false }) } fn parse_stmt_or_expr(&mut self) -> Result<StmtOrExpr, Diagnostic> { let start = self.peek().span; match self.peek().kind { TokenKind::KwLet => { let l = self.parse_let_decl()?; Ok(StmtOrExpr::Stmt(Stmt::Let(l))) } // Plan 114 (D184): `ro X = expr` scope-binding (immutable). // Plan 201 (D188-амендмент): `ro s = consume X { body }` — // consume-блок-выражение; интерцепт ДО parse_ro_mut_binding // (KwConsume не парсится как expression). TokenKind::KwRo if matches!(self.peek_at(1).kind, TokenKind::Ident(_)) && matches!(self.peek_at(2).kind, TokenKind::Eq) && matches!(self.peek_at(3).kind, TokenKind::KwConsume) => { self.bump(); // ro let (rname, rspan) = self.parse_ident()?; self.expect(&TokenKind::Eq)?; let stmt = self.parse_reconsume_block_expr_stmt( start, ConsumeScopeResult { name: rname, mutable: false, declared_consume: false, span: rspan, }, )?; Ok(StmtOrExpr::Stmt(stmt)) } TokenKind::KwRo => { let l = self.parse_ro_mut_binding(false)?; Ok(StmtOrExpr::Stmt(Stmt::Let(l))) } // Plan 114 (D184): `mut X = expr` scope-binding (mutable). // Disambiguation: leading `mut` в stmt-position — binding. // `mut` внутри patterns / params / receivers — обрабатывается // в parse_pattern / parse_param / parse_method_decl. // Plan 201: `mut s = consume X { body }` — см. KwRo-интерцепт. TokenKind::KwMut if matches!(self.peek_at(1).kind, TokenKind::Ident(_)) && matches!(self.peek_at(2).kind, TokenKind::Eq) && matches!(self.peek_at(3).kind, TokenKind::KwConsume) => { self.bump(); // mut let (rname, rspan) = self.parse_ident()?; self.expect(&TokenKind::Eq)?; let stmt = self.parse_reconsume_block_expr_stmt( start, ConsumeScopeResult { name: rname, mutable: true, declared_consume: false, span: rspan, }, )?; Ok(StmtOrExpr::Stmt(stmt)) } TokenKind::KwMut => { let l = self.parse_ro_mut_binding(true)?; Ok(StmtOrExpr::Stmt(Stmt::Let(l))) } // Plan 114.4 Ф.2: scope-local `const N = expr` inside fn body / // block. Constexpr-only (checker enforces); inline literal в // codegen. Reuses parse_const_decl (без is_export / doc). TokenKind::KwConst => { // Plan 170 (D307): scope-local const — file-private inapplicable. let c = self.parse_const_decl(false, None, Vec::new(), false)?; Ok(StmtOrExpr::Stmt(Stmt::Const(c))) } // Plan 33.3 (D24): `ghost let` — контекстный keyword `ghost`. TokenKind::Ident(ref n) if n == "ghost" && matches!(self.peek_at(1).kind, TokenKind::KwLet) => { let l = self.parse_let_decl()?; Ok(StmtOrExpr::Stmt(Stmt::Let(l))) } // Plan 114 (D184): `ghost ro X` / `ghost mut X` — spec-only binding. TokenKind::Ident(ref n) if n == "ghost" && matches!(self.peek_at(1).kind, TokenKind::KwRo) => { let l = self.parse_ro_mut_binding(false)?; Ok(StmtOrExpr::Stmt(Stmt::Let(l))) } TokenKind::Ident(ref n) if n == "ghost" && matches!(self.peek_at(1).kind, TokenKind::KwMut) => { let l = self.parse_ro_mut_binding(true)?; Ok(StmtOrExpr::Stmt(Stmt::Let(l))) } TokenKind::KwReturn => { self.bump(); let value = if self.at_newline() || matches!(self.peek().kind, TokenKind::RBrace) { None } else { Some(self.parse_expr()?) }; let end = match &value { Some(e) => e.span, None => start, }; Ok(StmtOrExpr::Stmt(Stmt::Return { value, span: start.merge(end), })) } TokenKind::KwBreak => { self.bump(); Ok(StmtOrExpr::Stmt(Stmt::Break(start))) } TokenKind::KwContinue => { self.bump(); Ok(StmtOrExpr::Stmt(Stmt::Continue(start))) } TokenKind::KwThrow => { self.bump(); let value = self.parse_expr()?; let span = start.merge(value.span); Ok(StmtOrExpr::Stmt(Stmt::Throw { value, span })) } // D90: `defer body` — scope-level cleanup. body — expression // (включая block-expression `{ ... }`). // Plan 173 Ф.2 (D314): расширено `defer(o ScopeOutcome) { body }` — // outcome-несущая форма (замена ретрактнутых errdefer/okdefer/defer|r|). TokenKind::KwDefer => { self.bump(); // Plan 110.5.7 (D189): `defer |result_binding|` form removed. // Hard cutover — emit D189-removed-defer-result error. if matches!(self.peek().kind, TokenKind::Pipe) { let span = self.peek().span; return Err(Diagnostic::new( "[D189-removed-defer-result] `defer |result| { ... }` reason-aware \ form retracted by Plan 110 D189. Migrate к `consume X = init() { body }` \ scope-block с `match outcome` в `cleanup` method, OR `with ResourceTrace = \ handler { body }` (D185) для observability-only logging pattern.".to_string(), span, )); } // Plan 173 Ф.2 (D314): `defer(o ScopeOutcome) { … }`. Bounded lookahead — // `( IDENT IDENT` (два идента подряд после `(`) НЕДОСТИЖИМ в валидном // выражении, поэтому однозначно сигнализирует intent defer(o); всё // остальное (`defer (expr)`, `defer { }`, `defer foo()`) идёт на parse_expr. if matches!(self.peek().kind, TokenKind::LParen) && matches!(self.peek_at(1).kind, TokenKind::Ident(_)) && matches!(self.peek_at(2).kind, TokenKind::Ident(_)) { let binding = if let TokenKind::Ident(n) = &self.peek_at(1).kind { n.clone() } else { unreachable!() }; let ty_name = if let TokenKind::Ident(n) = &self.peek_at(2).kind { n.clone() } else { unreachable!() }; let ty_span = self.peek_at(2).span; if ty_name != "ScopeOutcome" { return Err(Diagnostic::new(format!( "[E_DEFER_OUTCOME_TYPE] `defer(o {ty})`: outcome-биндинг требует тип \ `ScopeOutcome` (единственный тип исхода, D314), получен `{ty}`.", ty = ty_name), ty_span)); } self.bump(); // ( self.bump(); // binding IDENT self.bump(); // `ScopeOutcome` IDENT if !matches!(self.peek().kind, TokenKind::RParen) { return Err(Diagnostic::new( "[E_DEFER_OUTCOME_ARITY] `defer(o ScopeOutcome) { … }` принимает РОВНО \ один outcome-биндинг (не список); для нескольких значений — вложенные \ `defer` (D314).".to_string(), self.peek().span)); } self.bump(); // ) let body = self.parse_expr()?; let span = start.merge(body.span); return Ok(StmtOrExpr::Stmt(Stmt::Defer { body, outcome_binding: Some(binding), span })); } let body = self.parse_expr()?; let span = start.merge(body.span); Ok(StmtOrExpr::Stmt(Stmt::Defer { body, outcome_binding: None, span })) } // Plan 110.5.7 (D189): `errdefer` retracted. Hard cutover. TokenKind::KwErrDefer => { let span = self.peek().span; return Err(Diagnostic::new( "[D189-removed-errdefer] `errdefer { body }` retracted by Plan 110 D189. \ Migrate к `consume X = init() { body }` scope-block с `match outcome \ { Failure(_) => ... }` в `cleanup` method, OR use \ `mut done = false; defer { if !done { ... } }; ...; done = true` для \ bare cleanup-state pattern.".to_string(), span, )); } // Plan 110.5.7 (D189): `okdefer` retracted. Hard cutover. TokenKind::KwOkDefer => { let span = self.peek().span; return Err(Diagnostic::new( "[D189-removed-okdefer] `okdefer { body }` retracted by Plan 110 D189. \ Migrate к `consume X = init() { body }` scope-block с `match outcome \ { Success => ... }` в `cleanup` method.".to_string(), span, )); } // Plan 33.2 Ф.8 (D24): `assert_static <bool>` — intermediate // proof obligation. Контекстный keyword (Ident в лексере). TokenKind::Ident(ref n) if n == "assert_static" => { self.bump(); let expr = self.parse_expr()?; let span = start.merge(expr.span); Ok(StmtOrExpr::Stmt(Stmt::AssertStatic { expr, span })) } // Plan 33.3 (D24): `assume <bool>` — escape hatch. TokenKind::Ident(ref n) if n == "assume" => { self.bump(); let expr = self.parse_expr()?; let span = start.merge(expr.span); Ok(StmtOrExpr::Stmt(Stmt::Assume { expr, span })) } // Plan 33.5 Ф.4.1: `apply lemma_name(args)` — активация lemma. // Ф.13.1 (Plan 33.6): `apply lemma_name` (без скобок) — auto-inference из scope. // Контекстуальный keyword: `apply` не резервируем глобально. TokenKind::Ident(ref n) if n == "apply" => { self.bump(); let name = match self.peek().kind.clone() { TokenKind::Ident(n) => { self.bump(); n } _ => return Err(Diagnostic::new( "expected lemma name after `apply`", self.peek().span, )), }; // Ф.13.1: args скобки опциональны. `apply lemma` без `(...)` → // empty args → verify-side auto-inference. let mut args = Vec::new(); let end = if matches!(self.peek().kind, TokenKind::LParen) { self.bump(); // ( while !matches!(self.peek().kind, TokenKind::RParen) { args.push(self.parse_expr()?); if !matches!(self.peek().kind, TokenKind::RParen) { self.expect(&TokenKind::Comma)?; } } self.expect(&TokenKind::RParen)?.span } else { // Без скобок — auto-mode, span до имени. self.tokens[self.pos.saturating_sub(1)].span }; let span = start.merge(end); Ok(StmtOrExpr::Stmt(Stmt::Apply { lemma: name, args, span })) } // Plan 33.5 Ф.4.2: `calc { ... }` — структурированное доказательство. // Контекстуальный keyword (не резервируем `calc` глобально). TokenKind::Ident(ref n) if n == "calc" => { self.bump(); // consume `calc` Ok(StmtOrExpr::Stmt(self.parse_calc_stmt(start)?)) } // Plan 33.9 Ф.2: `reveal name` — раскрыть opaque fn body в SMT // scope текущей fn body. Контекстуальный keyword (не резервируем // `reveal` глобально). V1: parser/AST only — verify integration в V2. TokenKind::Ident(ref n) if n == "reveal" => { self.bump(); // consume `reveal` let name = match self.peek().kind.clone() { TokenKind::Ident(n) => { self.bump(); n } _ => return Err(Diagnostic::new( "expected fn name after `reveal`", self.peek().span, )), }; let end = self.tokens[self.pos.saturating_sub(1)].span; let span = start.merge(end); Ok(StmtOrExpr::Stmt(Stmt::Reveal { name, span })) } // Plan 194 Ф.1 (D-блок TBD, 09-tooling.md / D81-амендмент): // `#debug assert(<expr>)` — statement-form, зеркало `assert`, // единый dev-only префикс (см. `eat_debug_contract_attr` для // клауз requires/ensures/invariant). `#`/`debug` — обычные // токены (не keyword'ы), lookahead на `debug` ident по образцу // `#cfg`/`#stable` (item-level attrs, parse_item). // Expr.debug_only=true записывается в AST; codegen erasure- // поведение по `--contracts`-режиму — `mode_erases_debug` (Ф.2, // Plan 194 A2.2). Plan 194 A4: legacy `debug_assert(...)` // intrinsic РЕТРАКТИРОВАН — `#debug assert(...)` единственная // dev-only форма. TokenKind::Hash if matches!(&self.peek_at(1).kind, TokenKind::Ident(n) if n == "debug") => { self.bump(); // # self.bump(); // debug let is_assert_call = matches!(&self.peek().kind, TokenKind::Ident(n) if n == "assert") && matches!(self.peek_at(1).kind, TokenKind::LParen); if !is_assert_call { let sp = self.peek().span; return Err(Diagnostic::new( "[E_DEBUG_ATTR_TARGET] `#debug` в теле функции допустим только перед \ вызовом `assert(...)` (dev-only statement, Plan 194); для \ `requires`/`ensures`/`invariant` клауз пишите `#debug` перед \ соответствующей клаузой в сигнатуре/типе, не в теле.", sp, )); } let mut expr = self.parse_expr()?; expr.debug_only = true; Ok(StmtOrExpr::Expr(expr)) } // Plan 100.1 (D133 / D9): `consume tx = expr` binding form. // Disambig vs `consume` в receiver-pos (parse_fn handles before // stmt-context). Disambig vs `consume` method-arg keyword // (always inside parens, handled by parse_call_args). // Lookahead: `consume <ident>` / `consume mut` / `consume (…)` // (positional destructure, №378 M-73.1-destructure) / // `consume {…}` (record/named-tuple destructure, same followup) // — не путать с `consume` как часть expression (method call на // receiver). `(`/`{` can't start a valid stand-alone expression // right after bare `consume` (no such expr form exists), so // widening the gate introduces no ambiguity with the `_` arm. TokenKind::KwConsume if matches!( self.peek_at(1).kind, TokenKind::Ident(_) | TokenKind::KwMut | TokenKind::LParen | TokenKind::LBrace ) => { // Plan 110 D188: returns either Stmt::Let (raw form, D180) // или Stmt::ConsumeScope (block form, D188). let stmt = self.parse_consume_decl_or_scope()?; Ok(StmtOrExpr::Stmt(stmt)) } _ => { let expr = self.parse_expr()?; // Assignment? let op = match self.peek().kind { TokenKind::Eq => Some(AssignOp::Assign), TokenKind::PlusEq => Some(AssignOp::Add), TokenKind::MinusEq => Some(AssignOp::Sub), TokenKind::StarEq => Some(AssignOp::Mul), TokenKind::SlashEq => Some(AssignOp::Div), // Plan 234 Ф.2а (D46-амендмент §C): compound bitwise-присваивания. TokenKind::AmpEq => Some(AssignOp::BitAnd), TokenKind::PipeEq => Some(AssignOp::BitOr), TokenKind::CaretEq => Some(AssignOp::BitXor), TokenKind::ShlEq => Some(AssignOp::Shl), TokenKind::ShrEq => Some(AssignOp::Shr), _ => None, }; if let Some(op) = op { self.bump(); self.skip_newlines(); let value = self.parse_expr()?; let span = expr.span.merge(value.span); // Plan 136 (D236): detect tuple destructuring assignment // `(a, b) = (expr1, expr2)` — only for plain `=`. if matches!(op, AssignOp::Assign) { if let ExprKind::TupleLit(ref lhs_exprs) = expr.kind { if let ExprKind::TupleLit(ref rhs_exprs) = value.kind { return Ok(StmtOrExpr::Stmt(Stmt::TupleAssign { lhs: lhs_exprs.clone(), rhs: rhs_exprs.clone(), span, })); } else { return Err(Diagnostic::new( "[E_TUPLE_ASSIGN_RHS] tuple destructuring assignment \ rhs must be a tuple literal: (a, b) = (expr1, expr2)" .to_string(), value.span, )); } } } return Ok(StmtOrExpr::Stmt(Stmt::Assign { target: expr, op, value, span, })); } Ok(StmtOrExpr::Expr(expr)) } } } fn parse_trailing_block(&mut self) -> Result<TrailingBlock, Diagnostic> { let start = self.peek().span; // Plan 19, C13: `{ params => body }` отменён. Trailing-block // только без params (DSL-форма по D43-rev). Trailing с // параметрами — отдельная конструкция `f(args) fn(p) body`. self.expect(&TokenKind::LBrace)?; let mut stmts = Vec::new(); let mut trailing: Option<Box<Expr>> = None; self.skip_newlines(); while !matches!(self.peek().kind, TokenKind::RBrace) { match self.parse_stmt_or_expr()? { StmtOrExpr::Stmt(s) => { stmts.push(s); self.expect_stmt_separator()?; } StmtOrExpr::Expr(e) => { self.expect_stmt_separator()?; if matches!(self.peek().kind, TokenKind::RBrace) { trailing = Some(Box::new(e)); } else { stmts.push(Stmt::Expr(e)); } } } } let end = self.expect(&TokenKind::RBrace)?.span; Ok(TrailingBlock { params: Vec::new(), body: Block { stmts, trailing, span: start.merge(end), is_unsafe: false, }, span: start.merge(end), }) } // ─── patterns ──────────────────────────────────────────────────────── fn parse_pattern(&mut self) -> Result<Pattern, Diagnostic> { let start = self.peek().span; // Plan 108.3 (D36 amend): `mut name` in pattern position — // per-name mutability marker для destructure (tuple/record) и // loop-var. `let (mut a, b) = pair` → a mutable, b immutable. // Запрет group-mut `let mut (a, b)` обрабатывается parse_let'ом // на верхнем уровне (E_PATTERN_GROUP_MUT). let pat_is_mut = self.eat(&TokenKind::KwMut).is_some(); // Plan 73.2 (D157/D180-амендмент, consume-волна А, 2026-07-19): // `consume name` sub-pattern — ownership-transfer биндинг для // must-consume пейлоада внутри single-arg tuple-variant // (`Ok(consume tcp)`, `Some(consume f)`). Взаимоисключающе с `mut` // (D131 «consume и mut на одном receiver/binding» — parse error // в обе стороны написания). if pat_is_mut && matches!(self.peek().kind, TokenKind::KwConsume) { return Err(Diagnostic::new( "[E_PATTERN_CONSUME_MUT_CONFLICT] `mut` и `consume` на одном \ pattern-биндинге взаимоисключающие (D131/D157-амендмент) — \ `consume` уже несёт права мутации (D180).".to_string(), start, )); } let pat_is_consume = if pat_is_mut { false } else { self.eat(&TokenKind::KwConsume).is_some() }; if pat_is_consume && matches!(self.peek().kind, TokenKind::KwMut) { return Err(Diagnostic::new( "[E_PATTERN_CONSUME_MUT_CONFLICT] `consume` и `mut` на одном \ pattern-биндинге взаимоисключающие (D131/D157-амендмент) — \ `consume` уже несёт права мутации (D180).".to_string(), start, )); } match self.peek().kind.clone() { TokenKind::Ident(s) if s == "_" => { self.bump(); if pat_is_mut { return Err(Diagnostic::new( "[E_PATTERN_GROUP_MUT] `mut _` бессмысленно (wildcard не bind'ит).", start, )); } if pat_is_consume { return Err(Diagnostic::new( "[E_PATTERN_GROUP_MUT] `consume _` бессмысленно (wildcard не bind'ит).", start, )); } Ok(Pattern::Wildcard(start)) } TokenKind::Int(n) => { self.bump(); Ok(Pattern::Literal(Literal::Int(n), start)) } TokenKind::Float(f) => { self.bump(); Ok(Pattern::Literal(Literal::Float(f), start)) } TokenKind::Str(s) => { self.bump(); Ok(Pattern::Literal(Literal::Str(s), start)) } TokenKind::Char(cp) => { self.bump(); Ok(Pattern::Literal(Literal::Char(cp), start)) } TokenKind::KwTrue => { self.bump(); Ok(Pattern::Literal(Literal::Bool(true), start)) } TokenKind::KwFalse => { self.bump(); Ok(Pattern::Literal(Literal::Bool(false), start)) } TokenKind::Minus => { // отрицательный числовой литерал self.bump(); match self.peek().kind { TokenKind::Int(n) => { self.bump(); Ok(Pattern::Literal(Literal::Int(-n), start)) } TokenKind::Float(f) => { self.bump(); Ok(Pattern::Literal(Literal::Float(-f), start)) } _ => Err(Diagnostic::new( "expected number literal after `-` in pattern", start, )), } } TokenKind::LBracket => self.parse_array_pattern(), TokenKind::LParen => { self.bump(); if matches!(self.peek().kind, TokenKind::RParen) { let end = self.bump().span; return Ok(Pattern::Literal(Literal::Unit, start.merge(end))); } let mut pats = vec![self.parse_pattern()?]; while self.eat(&TokenKind::Comma).is_some() { pats.push(self.parse_pattern()?); } let end = self.expect(&TokenKind::RParen)?.span; if pats.len() == 1 { Ok(pats.into_iter().next().unwrap()) } else { Ok(Pattern::Tuple(pats, start.merge(end))) } } TokenKind::LBrace => { // record pattern без типа self.parse_record_pattern(None, start) } TokenKind::Ident(_) => { let mut path = vec![self.parse_ident()?.0]; while matches!(self.peek().kind, TokenKind::Dot) && matches!(self.peek_at(1).kind, TokenKind::Ident(_)) { self.bump(); path.push(self.parse_ident()?.0); } // Variant с args? if matches!(self.peek().kind, TokenKind::LParen) { self.bump(); let mut patterns = Vec::new(); let mut rest = false; while !matches!(self.peek().kind, TokenKind::RParen) { if self.eat(&TokenKind::DotDot).is_some() { rest = true; // .., больше элементов не разрешено break; } patterns.push(self.parse_pattern()?); if self.eat(&TokenKind::Comma).is_none() { break; } self.skip_newlines(); } let end = self.expect(&TokenKind::RParen)?.span; return Ok(Pattern::Variant { path, kind: VariantPatternKind::Tuple { patterns, rest }, span: start.merge(end), }); } // Record pattern с типом? if matches!(self.peek().kind, TokenKind::LBrace) { return self.parse_record_pattern(Some(path), start); } if path.len() == 1 { let name = path.into_iter().next().unwrap(); // Заглавная буква — variant unit; иначе binding if name .chars() .next() .map(|c| c.is_ascii_uppercase()) .unwrap_or(false) { Ok(Pattern::Variant { path: vec![name], kind: VariantPatternKind::Unit, span: start, }) } else { Ok(Pattern::Ident { name, span: start, is_mut: pat_is_mut, is_consume: pat_is_consume }) } } else { Ok(Pattern::Variant { path, kind: VariantPatternKind::Unit, span: start, }) } } other => Err(Diagnostic::new( format!("expected pattern, got {}", other.name()), start, )), } } fn parse_array_pattern(&mut self) -> Result<Pattern, Diagnostic> { let start = self.expect(&TokenKind::LBracket)?.span; let mut elems: Vec<ArrayPatternElem> = Vec::new(); self.skip_newlines(); while !matches!(self.peek().kind, TokenKind::RBracket) { if self.eat(&TokenKind::DotDot).is_some() { // ..rest или просто .. if let TokenKind::Ident(_) = self.peek().kind { let (name, _) = self.parse_ident()?; elems.push(ArrayPatternElem::RestBind(name)); } else { elems.push(ArrayPatternElem::Rest); } } else { elems.push(ArrayPatternElem::Item(self.parse_pattern()?)); } if self.eat(&TokenKind::Comma).is_none() { break; } self.skip_newlines(); } let end = self.expect(&TokenKind::RBracket)?.span; Ok(Pattern::Array { elems, span: start.merge(end), }) } fn parse_record_pattern( &mut self, type_path: Option<Vec<String>>, start: Span, ) -> Result<Pattern, Diagnostic> { self.expect(&TokenKind::LBrace)?; let mut fields = Vec::new(); let mut rest = false; self.skip_newlines(); while !matches!(self.peek().kind, TokenKind::RBrace) { if self.eat(&TokenKind::DotDot).is_some() { rest = true; break; } let (name, name_span) = self.parse_ident()?; let pattern = if self.eat(&TokenKind::Colon).is_some() { Some(self.parse_pattern()?) } else { None // shorthand }; fields.push(RecordPatternField { name, pattern, span: name_span, }); if self.eat(&TokenKind::Comma).is_none() { break; } self.skip_newlines(); } let end = self.expect(&TokenKind::RBrace)?.span; Ok(Pattern::Record { type_path, fields, rest, span: start.merge(end), }) } /// D44 string interpolation (Plan 17 Ф.4): `"... ${expr} ..."` → /// `ExprKind::InterpolatedStr { parts }`. /// /// Вход — сырая строка после lex'а; literal `\${` уже преобразован /// lexer'ом в sentinel `\x01$` (SOH+$), чтобы парсер мог отличить /// настоящий `${` от escape'нутого. Sentinel удаляется при сборке /// literal-частей. /// /// Если интерполяций нет — возвращаем обычный `StrLit`. /// Иначе codegen сам построит StringBuilder-цепочку (одна /// аллокация с pre-size estimate, без O(N²) от `+`). fn desugar_string_interpolation( &mut self, raw: String, span: Span, ) -> Result<Expr, Diagnostic> { let bytes = raw.as_bytes(); let mut parts: Vec<InterpPart> = Vec::new(); let mut cur_lit = String::new(); let mut i = 0; while i < bytes.len() { let b = bytes[i]; // \$ sentinel: the lexer emits SOH (U+0001) + `$` for an escaped `${`. // Treat 0x01 as the sentinel ONLY when the next byte is `$`. A bare // U+0001 is a legitimate Control codepoint (e.g. from a `\u{1}` escape // — common in UAX #29 grapheme test data); it must pass through to the // normal codepoint handler below, not be mistaken for the sentinel // (the old code pushed the *next* byte as a char and skipped 2, which // desynced the cursor inside a following multibyte char → parser panic). // The sentinel collides with content only for the astronomically rare // literal `\u{1}` directly followed by a literal `$`; resolving that // fully needs a non-content lexer marker (out of scope). if b == 0x01 && i + 1 < bytes.len() && bytes[i + 1] == b'$' { cur_lit.push('$'); i += 2; continue; } if b == b'$' && i + 1 < bytes.len() && bytes[i + 1] == b'{' { // ${expr} — flush литерала и парсим выражение. if !cur_lit.is_empty() { parts.push(InterpPart::Lit(std::mem::take(&mut cur_lit))); } // Plan 102 (D258-амендмент, 196-консолидация): найти парную // `}` через ТОТ ЖЕ string/brace-aware хелпер, что использует // лексер (`crate::lexer::scan_interpolation_body`) — не // независимый наивный `{`/`}`-счётчик (старый баг: наивный // счётчик слепо считал КАЖДУЮ `{`/`}`, включая те, что // попадают внутрь вложенной строки как обычный символ — // напр. литерал с фигурной скобкой в аргументе, `${f("a}b")}`, // преждевременно "закрыл" бы интерполяцию на этой `}`). // На практике этот путь недостижим для well-formed токенов: // лексер (`lex_string`) уже гарантирует парность `${`/`}` // ДО того, как включит диапазон в `TokenKind::Str` (иначе // сам вернул бы диагностику "unterminated interpolation // (started here)" с точным span'ом на `${`) — проверка ниже // остаётся defensive (не unreachable!()), т.к. `raw` в // принципе мог бы прийти не только из `lex_string`. let brace_pos = i + 1; let expr_start = i + 2; let close_pos = match crate::lexer::scan_interpolation_body(bytes, brace_pos) { Some(j) => j, None => { return Err(Diagnostic::new( "unterminated ${...} interpolation in string literal", span, )); } }; let j = close_pos; let expr_src = &raw[expr_start..j]; if expr_src.trim().is_empty() { return Err(Diagnostic::new( "empty `${}` interpolation in string literal", span, )); } // Sub-lex и sub-parse выражение из ${...}. let mut tokens = crate::lexer::lex(expr_src).map_err(|e| { Diagnostic::new( format!("invalid expression in `${{...}}`: {}", e.message), span, ) })?; // Владелец 2026-07-21 (найдено при str-concat-lint // канонизации, [M-str-interp-wrong-file-id]): `lex(expr_src)` // — независимый sub-lex сырой substring'а `${...}`, каждый // токен получает span с `file_id = MAIN_FILE_ID` (0) по // умолчанию (`Span::new`, backward-compat конструктор) — БЕЗ // контекста, в каком реальном файле лежит объемлющий // string-литерал. Для интерполяции внутри entry-файла (его // file_id обычно 0) это случайно совпадало; но для ЛЮБОГО // не-entry файла (типичный случай — module-private `const`, // резолвящийся в codegen по `(file_id, name)` через // `private_const_c_names`, emit_c.rs) lookup мисс'ил на // неверном file_id=0 и молча падал на bare unmangled name → // C `error: use of undeclared identifier` (репро: `"${FORMAT_TAG}"`, // std/src/_experimental/crypto/insecure_demo_kdf.nv, module-private // const в НЕ-entry файле). Фикс: перед sub-parse // проставляем токенам РЕАЛЬНЫЙ `file_id` объемлющего string- // литерала (`span.file_id`) — byte-offset'ы (`start`/`end`) // остаются relative к `expr_src` substring'у, как и раньше // (только `file_id`, единственное поле, использующееся как // HashMap-ключ в const-резолве, был неверен). for t in &mut tokens { t.span.file_id = span.file_id; } let mut sub = Parser::with_src(tokens, expr_src.to_string()); let inner = sub.parse_expr().map_err(|e| { Diagnostic::new( format!("invalid expression in `${{...}}`: {}", e.message), span, ) })?; // **Plan 91.14 Ф.2 (D229) + Plan 152.7-B (D258):** after expr, // peek for a `:` token. If present, the REMAINDER of the raw // `${...}` body (everything after the colon) is the format spec. // // We must parse the spec from the RAW substring, not via the // Nova sub-lexer: a Rust-style spec like `<5`, `*^10`, `.2`, // `#x`, `08X` is NOT a valid Nova token sequence (`<`/`^`/`#` // etc. would mis-lex or error). The sub-parser already stopped // at the colon after consuming the full expression, so the // colon token's byte span gives us the precise split point in // `expr_src`. let spec = if matches!(sub.peek().kind, TokenKind::Colon) { let colon_end = sub.peek().span.end; // `expr_src` is the raw `${...}` body; slice off the spec // text after the colon and parse it directly. let spec_raw = expr_src.get(colon_end..).unwrap_or(""); crate::ast::format_spec::parse_rich_format_spec(spec_raw) .map_err(|msg| Diagnostic::new(msg, span))? } else { // No `:` — but the sub-parser must have consumed the whole // expression. Any leftover token is a genuine syntax error. if !matches!(sub.peek().kind, TokenKind::Eof) { return Err(Diagnostic::new( format!( "[E_FORMAT_SPEC_TRAILING] unexpected tokens after \ expression in `${{...}}`: expected end of \ expression or a `:` format spec, found `{}`. \ Valid syntax — `${{expr}}`, `${{expr:?}}`, or \ `${{expr:SPEC}}` (Plan 152.7-B).", sub.peek().kind.name(), ), span, )); } crate::ast::FormatSpec::None }; parts.push(InterpPart::Expr(inner, spec)); i = j + 1; continue; } // Обычный байт — берём целиком codepoint. let ch_len = parser_utf8_char_len(b); let end = (i + ch_len).min(bytes.len()); cur_lit.push_str(&raw[i..end]); i = end; } if !cur_lit.is_empty() { parts.push(InterpPart::Lit(cur_lit)); } // Если только Lit-части (или ничего) — обычный StrLit // (без interpolation). if parts.iter().all(|p| matches!(p, InterpPart::Lit(_))) { let s: String = parts .into_iter() .map(|p| match p { InterpPart::Lit(s) => s, _ => unreachable!(), }) .collect(); return Ok(Expr::new(ExprKind::StrLit(s), span)); } // Конвертим InterpPart → InterpStrPart (для AST). let ast_parts: Vec<InterpStrPart> = parts .into_iter() .map(|p| match p { InterpPart::Lit(s) => InterpStrPart::Lit(s), InterpPart::Expr(e, spec) => InterpStrPart::Expr { expr: Box::new(e), spec, }, }) .collect(); Ok(Expr::new( ExprKind::InterpolatedStr { parts: ast_parts }, span, )) } } enum InterpPart { Lit(String), /// Plan 91.14 (D229) + Plan 152.7-B (D258): expr with optional format spec. /// Specs: FormatSpec::None / FormatSpec::Debug / FormatSpec::Spec(..). Expr(Expr, crate::ast::FormatSpec), } // Plan 152.7-B (D258): the format spec is parsed directly from the raw `${...}` // substring via `crate::ast::format_spec::parse_rich_format_spec` (a Rust-style // spec is not a valid Nova token sequence, so it cannot be sub-lexed). The old // token-driven `parse_format_spec` helper (Plan 91.14, `:?`-only) was retired. fn parser_utf8_char_len(first_byte: u8) -> usize { match first_byte { b if b < 0x80 => 1, b if b < 0xC0 => 1, b if b < 0xE0 => 2, b if b < 0xF0 => 3, _ => 4, } } enum StmtOrExpr { Stmt(Stmt), Expr(Expr), } /// Удобная обёртка — лексирует и парсит исходник. /// `file_id = MAIN_FILE_ID` (backward compat). pub fn parse(src: &str) -> Result<Module, Diagnostic> { parse_with_file_id(src, crate::diag::MAIN_FILE_ID) } /// Plan 42 Sub-plan 42.4 шаг 2: parse с explicit FileId. /// Все Span'ы AST получат указанный file_id (через token spans от lexer). pub fn parse_with_file_id(src: &str, file_id: crate::diag::FileId) -> Result<Module, Diagnostic> { let tokens = crate::lexer::lex_with_file_id(src, file_id)?; // Plan 45 Ф.2: doc-comment токены остаются в стриме; парсер // консумит их через `consume_doc_block_of_kind` (Outer — перед // item'ами; Inner — на уровне модуля). Interim shim из Ф.1 // удалён. let mut p = Parser::with_src(tokens, src.to_string()); p.parse_module() } /// **Plan 118.5 / D216 V2 §V2.6 (2026-06-04):** parse and return both module /// and parser-emitted warnings (Plan 118.5 deprecation warnings, etc.). /// /// Use this instead of `parse()` when the driver wants to surface parser /// warnings to the user. Existing callers of `parse()` continue to work /// (warnings silently dropped). pub fn parse_collecting_warnings( src: &str, ) -> Result<(Module, Vec<Diagnostic>), Diagnostic> { let tokens = crate::lexer::lex(src)?; let mut p = Parser::with_src(tokens, src.to_string()); let module = p.parse_module()?; Ok((module, p.into_warnings())) } /// Plan 45 Ф.24.6: parse a Nova type expression string → TypeRef AST. /// Returns Err if the input is not a valid type expression. pub fn parse_type_str(ty: &str) -> Result<crate::ast::TypeRef, crate::diag::Diagnostic> { let tokens = crate::lexer::lex(ty)?; let mut p = Parser::with_src(tokens, ty.to_string()); p.parse_type() } #[cfg(test)] mod doc_attach_tests { //! Plan 45 Ф.2: проверяем, что парсер прикрепляет doc-блоки к //! item'ам и к модулю. Тесты идут поверх существующего parser-pipeline //! (lex → parser); doc-token'ы попадают в стрим из lexer'а Ф.1. use super::*; use crate::ast::{Item, TypeDeclKind}; use crate::lexer::DocCommentKind; fn parse_or_panic(src: &str) -> crate::ast::Module { super::parse(src).unwrap_or_else(|e| panic!("parse failed: {:?}", e)) } #[test] fn outer_doc_attaches_to_fn() { let src = "\ module m /// Returns the absolute value of `x`. fn abs(x int) -> int => x "; let m = parse_or_panic(src); let fn_decl = m.items.iter().find_map(|it| match it { Item::Fn(f) => Some(f), _ => None, }).expect("fn must exist"); let doc = fn_decl.doc.as_ref().expect("doc must be attached"); assert_eq!(doc.kind, DocCommentKind::Outer); assert_eq!(doc.content, "Returns the absolute value of `x`."); } #[test] fn outer_doc_multi_line_attaches() { let src = "\ module m /// Summary line. /// /// Long description spanning /// multiple lines. fn foo() -> int => 1 "; let m = parse_or_panic(src); let fn_decl = m.items.iter().find_map(|it| match it { Item::Fn(f) => Some(f), _ => None, }).expect("fn must exist"); let doc = fn_decl.doc.as_ref().expect("doc must be attached"); assert!(doc.content.starts_with("Summary line.")); assert!(doc.content.contains("Long description")); } #[test] fn outer_doc_attaches_to_type() { let src = "\ module m /// A point in 2D space. type Point { x int; y int } "; let m = parse_or_panic(src); let ty = m.items.iter().find_map(|it| match it { Item::Type(t) => Some(t), _ => None, }).expect("type must exist"); let doc = ty.doc.as_ref().expect("doc must be attached"); assert_eq!(doc.content, "A point in 2D space."); assert!(matches!(ty.kind, TypeDeclKind::Record(_))); } #[test] fn outer_doc_attaches_to_const() { let src = "\ module m /// Maximum buffer size in bytes. const MAX_BUF int = 4096 "; let m = parse_or_panic(src); let c = m.items.iter().find_map(|it| match it { Item::Const(c) => Some(c), _ => None, }).expect("const must exist"); let doc = c.doc.as_ref().expect("doc must be attached"); assert_eq!(doc.content, "Maximum buffer size in bytes."); } #[test] fn inner_doc_attaches_to_module() { let src = "\ //! This module provides examples for testing. module m fn foo() -> int => 1 "; let m = parse_or_panic(src); let doc = m.doc.as_ref().expect("module doc must be attached"); assert_eq!(doc.kind, DocCommentKind::Inner); assert_eq!(doc.content, "This module provides examples for testing."); } #[test] fn outer_doc_with_attrs_in_between() { // `///` then `#realtime` then `fn` — doc must attach to fn, not lost. let src = "\ module m /// Realtime-safe abs. #realtime fn abs(x int) -> int => x "; let m = parse_or_panic(src); let fn_decl = m.items.iter().find_map(|it| match it { Item::Fn(f) => Some(f), _ => None, }).expect("fn must exist"); let doc = fn_decl.doc.as_ref().expect("doc must be attached"); assert_eq!(doc.content, "Realtime-safe abs."); } #[test] fn no_doc_means_none() { let src = "\ module m fn no_doc_fn() -> int => 1 "; let m = parse_or_panic(src); let fn_decl = m.items.iter().find_map(|it| match it { Item::Fn(f) => Some(f), _ => None, }).expect("fn must exist"); assert!(fn_decl.doc.is_none()); assert!(m.doc.is_none()); } } #[cfg(test)] mod tests { use super::*; fn parse_or_panic(src: &str) -> Module { match parse(src) { Ok(m) => m, Err(e) => panic!("parse error: {} (span {})", e.message, e.span), } } #[test] fn empty_module() { let m = parse_or_panic(""); assert!(m.items.is_empty()); } #[test] fn module_decl() { let m = parse_or_panic("module a.b.c\n"); assert_eq!(m.name, vec!["a", "b", "c"]); } #[test] fn fn_simple() { let m = parse_or_panic("fn double(x int) -> int => x * 2\n"); assert_eq!(m.items.len(), 1); let Item::Fn(f) = &m.items[0] else { panic!() }; assert_eq!(f.name, "double"); assert_eq!(f.params.len(), 1); assert!(matches!(f.body, FnBody::Expr(_))); } #[test] fn fn_block_body() { let m = parse_or_panic( r#" fn area(r f64) -> f64 { ro pi = 3.14 pi * r * r } "#, ); let Item::Fn(f) = &m.items[0] else { panic!() }; assert!(matches!(f.body, FnBody::Block(_))); } #[test] fn fn_with_method_receiver() { let m = parse_or_panic("fn Point @magnitude() -> f64 => 0.0\n"); let Item::Fn(f) = &m.items[0] else { panic!() }; let r = f.receiver.as_ref().unwrap(); assert_eq!(r.type_name, "Point"); assert!(matches!(r.kind, ReceiverKind::Instance)); assert_eq!(f.name, "magnitude"); } #[test] fn fn_static_method() { // Plan 51 / D55 (record-literal type unification): `-> Account => Account { }` // более не допускается — тип объявлен дважды. Тест проверяет только // signature parsing (receiver kind), поэтому body — block-form (`{ }`), // которая всегда валидна и не зависит от record-literal правил. let m = parse_or_panic("fn Account.new(owner str) -> Account { }\n"); let Item::Fn(f) = &m.items[0] else { panic!() }; let r = f.receiver.as_ref().unwrap(); assert_eq!(r.type_name, "Account"); assert!(matches!(r.kind, ReceiverKind::Static)); } #[test] fn type_record() { let m = parse_or_panic( r#" type User { ro id u64 name str } "#, ); let Item::Type(t) = &m.items[0] else { panic!() }; let TypeDeclKind::Record(fields) = &t.kind else { panic!() }; assert_eq!(fields.len(), 2); assert!(fields[0].readonly); } #[test] fn type_sum() { let m = parse_or_panic("type Color | Red | Green | Blue\n"); let Item::Type(t) = &m.items[0] else { panic!() }; let TypeDeclKind::Sum(variants) = &t.kind else { panic!() }; assert_eq!(variants.len(), 3); } #[test] fn type_effect() { let m = parse_or_panic( r#" type Db effect { query(q Sql) -> int exec(q Sql) -> int } "#, ); let Item::Type(t) = &m.items[0] else { panic!() }; let TypeDeclKind::Effect(methods) = &t.kind else { panic!() }; assert_eq!(methods.len(), 2); } // Plan 62.D.bis (D126, 2026-05-18): `external type X [Generics]` — // opaque type, реализация в runtime. Parser tests cover positive + // negative (body rejection) + generic-params scenarios. Whitelist // enforcement (only std.runtime.* / std.prelude.*) — отдельный // type-checker test, не parser. #[test] fn external_type_simple() { let m = parse_or_panic("external type StringBuilder\n"); let Item::Type(t) = &m.items[0] else { panic!("expected Type item") }; assert_eq!(t.name, "StringBuilder"); assert!(t.generics.is_empty(), "non-generic opaque"); assert!(!t.is_export, "no `export` modifier"); assert!(matches!(t.kind, TypeDeclKind::Opaque), "kind must be Opaque"); } #[test] fn external_type_export() { let m = parse_or_panic("export external type WriteBuffer\n"); let Item::Type(t) = &m.items[0] else { panic!() }; assert_eq!(t.name, "WriteBuffer"); assert!(t.is_export, "`export` modifier present"); assert!(matches!(t.kind, TypeDeclKind::Opaque)); } #[test] fn external_type_generic() { // Future Channel[T] use-case: parser должен принять generic params. let m = parse_or_panic("external type Channel[T]\n"); let Item::Type(t) = &m.items[0] else { panic!() }; assert_eq!(t.name, "Channel"); assert_eq!(t.generics.len(), 1); assert_eq!(t.generics[0].name, "T"); assert!(matches!(t.kind, TypeDeclKind::Opaque)); } #[test] fn external_type_generic_multi() { let m = parse_or_panic("external type Region[T, C]\n"); let Item::Type(t) = &m.items[0] else { panic!() }; assert_eq!(t.generics.len(), 2); assert_eq!(t.generics[0].name, "T"); assert_eq!(t.generics[1].name, "C"); assert!(matches!(t.kind, TypeDeclKind::Opaque)); } #[test] fn external_type_body_rejected_brace() { let r = parse("external type Foo { x int }\n"); assert!(r.is_err(), "external type with record body must error"); let err = r.unwrap_err().message; assert!(err.contains("cannot have a body"), "msg: {err}"); } #[test] fn external_type_body_rejected_sum() { let r = parse("external type Foo | A | B\n"); assert!(r.is_err(), "external type with sum body must error"); let err = r.unwrap_err().message; assert!(err.contains("cannot have a body"), "msg: {err}"); } #[test] fn external_type_body_rejected_effect() { let r = parse("external type Foo effect { op() -> int }\n"); assert!(r.is_err(), "external type with effect body must error"); let err = r.unwrap_err().message; assert!(err.contains("cannot have a body"), "msg: {err}"); } #[test] fn external_type_body_rejected_protocol() { let r = parse("external type Foo protocol { op() -> int }\n"); assert!(r.is_err(), "external type with protocol body must error"); let err = r.unwrap_err().message; assert!(err.contains("cannot have a body"), "msg: {err}"); } #[test] fn external_type_body_rejected_alias() { let r = parse("external type Foo alias int\n"); assert!(r.is_err(), "external type with alias body must error"); let err = r.unwrap_err().message; assert!(err.contains("cannot have a body"), "msg: {err}"); } #[test] fn external_type_body_rejected_newtype() { let r = parse("external type Foo SomeOther\n"); assert!(r.is_err(), "external type with newtype-style body must error"); let err = r.unwrap_err().message; assert!(err.contains("cannot have a") && err.contains("body"), "msg: {err}"); } #[test] fn external_only_before_fn_or_type() { // Регрессионный — `external let` остаётся ошибкой с обновлённым сообщением. let r = parse("external let x = 1\n"); assert!(r.is_err()); let err = r.unwrap_err().message; assert!(err.contains("`fn` or `type`"), "msg: {err}"); } #[test] fn match_with_arms() { let m = parse_or_panic( r#" fn f(x int) -> str => match x { 0 => "zero" _ => "other" } "#, ); assert_eq!(m.items.len(), 1); } #[test] fn array_lit_with_spread() { let m = parse_or_panic("fn t() -> int => [0, ...arr, 4]\n"); let Item::Fn(f) = &m.items[0] else { panic!() }; let FnBody::Expr(e) = &f.body else { panic!() }; let ExprKind::ArrayLit(elems) = &e.kind else { panic!() }; assert_eq!(elems.len(), 3); assert!(matches!(elems[1], ArrayElem::Spread(_))); } #[test] fn record_lit_with_spread() { let m = parse_or_panic( r#" fn make() -> User => { ...other, name: "bob" } "#, ); let Item::Fn(f) = &m.items[0] else { panic!() }; let FnBody::Expr(e) = &f.body else { panic!() }; let ExprKind::RecordLit { fields, .. } = &e.kind else { panic!() }; assert_eq!(fields.len(), 2); assert!(fields[0].is_spread); assert_eq!(fields[1].name, "name"); } #[test] fn try_operator() { let m = parse_or_panic( r#" fn read() Throws -> int => parse(s)? "#, ); assert_eq!(m.items.len(), 1); } #[test] fn if_let_pattern() { let m = parse_or_panic( r#" fn f(opt int) -> int { if let Some(x) = opt { x } else { 0 } } "#, ); assert_eq!(m.items.len(), 1); } #[test] fn for_in_range() { let m = parse_or_panic( r#" fn loop_test() -> int { mut s = 0 for i in 0..10 { s += i } s } "#, ); assert_eq!(m.items.len(), 1); } #[test] fn handler_lit_in_with() { let m = parse_or_panic( r#" fn run() { with Db = effect Db { query(q) => [] exec(q) => 0 } { work() } } "#, ); assert_eq!(m.items.len(), 1); } #[test] fn test_decl() { let m = parse_or_panic( r#" test "addition works" { assert 1 + 1 == 2 } "#, ); assert_eq!(m.items.len(), 1); let Item::Test(t) = &m.items[0] else { panic!() }; assert_eq!(t.name, "addition works"); } #[test] fn array_pattern() { let m = parse_or_panic( r#" fn first(xs []int) -> int => match xs { [] => 0 [x] => x [head, ..] => head } "#, ); assert_eq!(m.items.len(), 1); } // ─── Plan 19, C2: closure-light parsing ──────────────────────── /// Helper: ищет верхнеуровневое closure-light выражение в первом /// `let`-биндинге модуля. Используется в тестах ниже. fn first_let_closure_light(m: &Module) -> (&Vec<crate::ast::ClosureLightParam>, &crate::ast::ClosureBody) { let Item::Let(l) = &m.items[0] else { panic!("expected first item to be `let`, got {:?}", m.items[0]); }; let ExprKind::ClosureLight { params, body } = &l.value.kind else { panic!( "expected ClosureLight in let value, got {:?}", l.value.kind ); }; (params, body) } #[test] fn closure_light_one_param_expr() { let m = parse_or_panic("ro inc = |x| x + 1\n"); let (params, body) = first_let_closure_light(&m); assert_eq!(params.len(), 1); assert_eq!(params[0].name, "x"); assert!(matches!(body, crate::ast::ClosureBody::Expr(_))); } #[test] fn closure_light_no_params() { let m = parse_or_panic("ro zero = || 0\n"); let (params, body) = first_let_closure_light(&m); assert!(params.is_empty()); assert!(matches!(body, crate::ast::ClosureBody::Expr(_))); } #[test] fn closure_light_wildcard_param() { let m = parse_or_panic("ro any = |_| 42\n"); let (params, _body) = first_let_closure_light(&m); assert_eq!(params.len(), 1); assert_eq!(params[0].name, "_"); } #[test] fn closure_light_multi_params() { let m = parse_or_panic("ro add = |a, b| a + b\n"); let (params, _body) = first_let_closure_light(&m); assert_eq!(params.len(), 2); assert_eq!(params[0].name, "a"); assert_eq!(params[1].name, "b"); } #[test] fn closure_light_block_body() { let m = parse_or_panic( r#" ro f = |x| { ro y = x * 2 y + 1 } "#, ); let (params, body) = first_let_closure_light(&m); assert_eq!(params.len(), 1); assert_eq!(params[0].name, "x"); assert!(matches!(body, crate::ast::ClosureBody::Block(_))); } #[test] fn closure_light_no_params_block_body() { let m = parse_or_panic( r#" ro g = || { ro x = 10 x * x } "#, ); let (params, body) = first_let_closure_light(&m); assert!(params.is_empty()); assert!(matches!(body, crate::ast::ClosureBody::Block(_))); } #[test] fn closure_light_in_call_arg() { // Closure-light внутри args вызова — частый use-case (HOF). let m = parse_or_panic("ro r = list.filter(|x| x > 0)\n"); let Item::Let(l) = &m.items[0] else { panic!() }; // r = ExprKind::Call { ... args: [Closure...] } let ExprKind::Call { args, .. } = &l.value.kind else { panic!("expected Call, got {:?}", l.value.kind); }; assert_eq!(args.len(), 1); let CallArg::Item(arg_expr) = &args[0] else { panic!() }; assert!(matches!(arg_expr.kind, ExprKind::ClosureLight { .. })); } #[test] fn closure_light_typed_param_rejected() { // |x int| — невалидно, типы только в closure-full. let result = parse("ro bad = |x int| x + 1\n"); assert!(result.is_err(), "typed param must be rejected in closure-light"); let err = result.unwrap_err(); assert!( err.message.contains("untyped") || err.message.contains("fn(x T)"), "error message should hint at closure-full, got: {}", err.message ); } #[test] fn closure_light_arrow_in_body_rejected() { // |x| => expr — невалидно (D22-rev: closure-light не использует =>). let result = parse("ro bad = |x| => x + 1\n"); assert!(result.is_err(), "`|x| => expr` must be rejected"); let err = result.unwrap_err(); assert!( err.message.contains("=>") || err.message.contains("D22-rev") || err.message.contains("closure-light body"), "error message should explain `=>` is not used, got: {}", err.message ); } #[test] fn closure_light_does_not_break_binary_or() { // `|` в infix-position — binary OR, не closure. // 5 | 2 — bitwise OR, должно дать значение 7 (но мы парсим, не вычисляем). let m = parse_or_panic("ro r = 5 | 2\n"); let Item::Let(l) = &m.items[0] else { panic!() }; // Должен быть Binary, не ClosureLight. assert!( matches!(l.value.kind, ExprKind::Binary { .. }), "expected Binary OR, got {:?}", l.value.kind ); } #[test] fn closure_light_does_not_break_logical_or() { // `||` в infix-position — logical OR, не no-arg closure. let m = parse_or_panic("ro r = true || false\n"); let Item::Let(l) = &m.items[0] else { panic!() }; assert!( matches!(l.value.kind, ExprKind::Binary { .. }), "expected logical OR (Binary), got {:?}", l.value.kind ); } // ─── Plan 19, C3: closure-full parsing ───────────────────────── /// Helper: достаёт closure-full из первого let'а. fn first_let_closure_full(m: &Module) -> &crate::ast::FnSigBody { let Item::Let(l) = &m.items[0] else { panic!("expected first item to be `let`, got {:?}", m.items[0]); }; let ExprKind::ClosureFull(sb) = &l.value.kind else { panic!( "expected ClosureFull in let value, got {:?}", l.value.kind ); }; sb } #[test] fn closure_full_typed_expr_body() { let m = parse_or_panic("ro f = fn(x int) -> int => x * 2\n"); let sb = first_let_closure_full(&m); assert_eq!(sb.params.len(), 1); assert_eq!(sb.params[0].name, "x"); assert!(sb.return_type.is_some()); assert!(matches!(sb.body, FnBody::Expr(_))); } #[test] fn closure_full_typed_block_body() { let m = parse_or_panic( r#" ro f = fn(x int, y int) -> int { ro z = x + y z * 2 } "#, ); let sb = first_let_closure_full(&m); assert_eq!(sb.params.len(), 2); assert!(matches!(sb.body, FnBody::Block(_))); } #[test] fn closure_full_with_effects() { let m = parse_or_panic( "ro mid = fn(req int) Db Log -> int => req + 1\n", ); let sb = first_let_closure_full(&m); assert_eq!(sb.effects.len(), 2); assert!(sb.return_type.is_some()); } #[test] fn closure_full_no_params() { let m = parse_or_panic("ro pure = fn() -> int => 42\n"); let sb = first_let_closure_full(&m); assert!(sb.params.is_empty()); assert!(sb.return_type.is_some()); } #[test] fn closure_full_no_return_type() { let m = parse_or_panic("ro logger = fn(s str) Log { ro x = s }\n"); let sb = first_let_closure_full(&m); assert_eq!(sb.params.len(), 1); assert!(sb.return_type.is_none()); assert_eq!(sb.effects.len(), 1); } #[test] fn closure_full_generics_rejected() { let result = parse("ro f = fn[T](x T) -> T => x\n"); assert!(result.is_err(), "generics on closure-full must be rejected in bootstrap"); let err = result.unwrap_err(); assert!( err.message.contains("generics") || err.message.contains("rank-2"), "error should mention generics/rank-2, got: {}", err.message ); } #[test] fn closure_full_in_call_arg() { let m = parse_or_panic( "ro r = list.map(fn(x int) -> int => x * 2)\n", ); let Item::Let(l) = &m.items[0] else { panic!() }; let ExprKind::Call { args, .. } = &l.value.kind else { panic!("expected Call, got {:?}", l.value.kind); }; let CallArg::Item(arg_expr) = &args[0] else { panic!() }; assert!(matches!(arg_expr.kind, ExprKind::ClosureFull(_))); } #[test] fn closure_full_does_not_break_named_fn() { // top-level `fn foo()` всё ещё parses нормально — это item, не expr. let m = parse_or_panic("fn foo(x int) -> int => x + 1\n"); assert_eq!(m.items.len(), 1); let Item::Fn(f) = &m.items[0] else { panic!() }; assert_eq!(f.name, "foo"); } // ─── Plan 19, C10 (D88): default generic params ─────────────── #[test] fn generic_default_simple() { let m = parse_or_panic( "fn run[T = int](a T) -> T => a\n" ); let Item::Fn(f) = &m.items[0] else { panic!() }; assert_eq!(f.generics.len(), 1); assert_eq!(f.generics[0].name, "T"); assert!(f.generics[0].default.is_some()); } #[test] fn generic_default_with_bound() { let m = parse_or_panic( "fn run[T Numeric = f64](a T) -> T => a\n" ); let Item::Fn(f) = &m.items[0] else { panic!() }; assert_eq!(f.generics.len(), 1); assert!(!f.generics[0].bounds.is_empty()); assert!(f.generics[0].default.is_some()); } #[test] fn generic_default_must_be_after_required() { // [T = int, U] — error: required after default. let result = parse("fn bad[T = int, U](x T, y U) -> T => x\n"); assert!(result.is_err(), "params with default must precede defaults"); let err = result.unwrap_err(); assert!( err.message.contains("default"), "error should mention default ordering, got: {}", err.message ); } #[test] fn generic_default_on_type() { let m = parse_or_panic( "type Complex[T = f64] { re T, im T }\n" ); let Item::Type(t) = &m.items[0] else { panic!() }; assert_eq!(t.generics.len(), 1); assert!(t.generics[0].default.is_some()); } // ─── Plan 19, C9 (D87) → Plan 97 Ф.3 (D142): Effect[E, IRT] ──── #[test] fn effect_two_param_generic() { // `Effect[Logger, int]` — двухпараметрический generic // (E + IRT). Plan 97 Ф.3 (D142): renamed from `Handler`. let m = parse_or_panic( "fn make_h() -> Effect[Logger, int] => some_handler\n" ); let Item::Fn(f) = &m.items[0] else { panic!() }; let Some(TypeRef::Named { path, generics, .. }) = &f.return_type else { panic!("expected named return type, got {:?}", f.return_type); }; assert_eq!(path, &vec!["Effect".to_string()]); assert_eq!(generics.len(), 2); } #[test] fn effect_single_param_default_irt() { // `Effect[E]` ≡ `Effect[E, never]` через D88 default. // Plan 97 Ф.3 (D142): renamed from `Handler`. let m = parse_or_panic( "fn make_h() -> Effect[Logger] => some_handler\n" ); let Item::Fn(f) = &m.items[0] else { panic!() }; let Some(TypeRef::Named { path, generics, .. }) = &f.return_type else { panic!() }; assert_eq!(path, &vec!["Effect".to_string()]); assert_eq!(generics.len(), 1); } // ─── Plan 19, C4: trailing parsing ───────────────────────────── #[test] fn trailing_block_no_params() { // `f() { body }` — DSL-форма (D43-rev). let m = parse_or_panic( r#" fn dummy(x fn() -> int) -> int => x() ro r = dummy() { 42 } "#, ); // dummy — Item 0, let — Item 1. let Item::Let(l) = &m.items[1] else { panic!("expected let at items[1], got {:?}", m.items[1]); }; let ExprKind::Call { trailing, .. } = &l.value.kind else { panic!() }; let t = trailing.as_ref().unwrap(); assert!(matches!(t, crate::ast::Trailing::Block(_))); } #[test] fn trailing_block_legacy_form_rejected() { // Plan 19 C13: `f() { x => body }` legacy форма удалена // (заменена на `f() fn(x int) -> ... body`). Парсер // её больше не принимает: внутри `{ ... }` `x =>` парсится // как match-arm в block, что даёт parse-error дальше. // (Точная форма ошибки зависит от того, как парсер // интерпретирует `x =>` в начале block-statement'а — нам // важно лишь что результат **не** Trailing::LegacyBlockWithParams.) let result = parse( r#" fn dummy(x fn(int) -> int) -> int => x(0) let r = dummy() { x => x + 1 } "#, ); // Либо parse fail, либо parse OK но trailing — Block (без params) // с инородным `x => x + 1` внутри. В обоих случаях нет // LegacyBlockWithParams. if let Ok(m) = result { if let Item::Let(l) = &m.items[1] { if let ExprKind::Call { trailing, .. } = &l.value.kind { if let Some(t) = trailing { assert!( !matches!(t, crate::ast::Trailing::LegacyBlockWithParams(_)), "Plan 19 C13 should not produce LegacyBlockWithParams" ); } } } } } #[test] fn trailing_fn_typed_expr_body() { // `f() fn(x) => x > 0` — D43-rev trailing-fn. let m = parse_or_panic( r#" fn dummy(x fn(int) -> bool) -> bool => x(1) ro r = dummy() fn(x int) -> bool => x > 0 "#, ); let Item::Let(l) = &m.items[1] else { panic!() }; let ExprKind::Call { trailing, .. } = &l.value.kind else { panic!() }; let t = trailing.as_ref().unwrap(); let crate::ast::Trailing::Fn(sb) = t else { panic!("expected Trailing::Fn, got {:?}", t); }; assert_eq!(sb.params.len(), 1); assert!(matches!(sb.body, FnBody::Expr(_))); } #[test] fn trailing_fn_block_body() { let m = parse_or_panic( r#" fn dummy(x fn(int, int) -> int) -> int => x(1, 2) ro r = dummy() fn(a int, b int) -> int { ro s = a + b s * 2 } "#, ); let Item::Let(l) = &m.items[1] else { panic!() }; let ExprKind::Call { trailing, .. } = &l.value.kind else { panic!() }; let t = trailing.as_ref().unwrap(); let crate::ast::Trailing::Fn(sb) = t else { panic!() }; assert_eq!(sb.params.len(), 2); assert!(matches!(sb.body, FnBody::Block(_))); } #[test] fn trailing_fn_with_effects() { let m = parse_or_panic( r#" fn dummy(x fn(int) Db -> int) Db -> int => x(1) ro r = dummy() fn(n int) Db -> int => n "#, ); let Item::Let(l) = &m.items[1] else { panic!() }; let ExprKind::Call { trailing, .. } = &l.value.kind else { panic!() }; let t = trailing.as_ref().unwrap(); let crate::ast::Trailing::Fn(sb) = t else { panic!() }; assert_eq!(sb.effects.len(), 1); } }