/
nv-lang
/
nova
Обзор
Документация
Войти
/
nv-lang
/
nova
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
main
compiler-codegen/src/codegen/operator_dispatch.rs
415 строк
21 KB
Evgeniy Golovin
fix(codegen): №274 — binop/ueq operator stubs now mirror auto-by-ref ABI
02 авг 2026, 07:26
02 авг 2026, 07:26
53464e1
Код
Авторство
О чём код?
//! Plan opunify (owner decision 2026-08-01, BRIEF_opunify.md): ONE table + //! ONE dispatch path for every binary/unary operator-overload, replacing the //! per-operator copy-paste that plans 65/85.4/175/234/int128 grew over time //! (three same-class regressions in two days: `Set[int] | Set[int]` with no //! mono body, `<<`/`>>` not dispatching at all, `@neg`/`@not` DCE'd away). //! Was `bitwise_ops.rs` (Plan 234, git mv — history preserved); this module //! now owns the D46 selector table (`03-syntax.md`) for ALL binops, not just //! the bitwise family, and the shared resolver both the bitwise fast-path //! AND the arithmetic fast-path call through. `emit_c.rs` keeps only thin //! call-sites (arch-ratchet, `scripts/guards/arch-ratchet.sh`), same pattern //! as `codegen/mono_method_registry.rs` (№129) and `codegen/assoc_ro.rs` //! (№157). use crate::ast::{BinOp, UnOp, FnDecl, TypeRef}; use super::emit_c::MethodSig; /// Shape of the second operand for a binary operator-overload dispatch (D46, /// `03-syntax.md`). Drives which flat (non-generic-receiver) resolution /// strategy `resolve_binop_dispatch` uses. #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub(crate) enum OperandShape { /// RHS type == `Self` (the receiver) — the checker already guarantees /// this holds, so the flat/non-generic-receiver path does not need a /// runtime overload lookup: it can emit `<Recv>_method_<name>(l, r)` /// directly. `+ * / % & | ^` (Plan 65/234's original "flat fast-path"). Homogeneous, /// RHS type may legitimately differ from `Self` (`Timestamp - Duration`, /// `record << int`) — the flat path MUST look up `method_overloads` for /// an overload whose sole parameter matches `rty` EXACTLY (D124: this is /// what prevents e.g. silent `Timestamp - Monotonic` cross-clock /// arithmetic), erroring with `NoMatchingOverload` otherwise. `- << >>`. Heterogeneous, } /// One row of the D46 operator-dispatch table: operator -> {selector name, /// operand shape}. SINGLE source of truth, consumed by: /// - `resolve_binop_dispatch` (this file) — the shared dispatch algorithm. /// - `emit_c.rs` call-sites — thin, table-driven instead of per-op copies. /// - `lints.rs` `collect_used_names` — DCE seeds iterate this table instead /// of a hand-maintained selector list (closes the "forgot to seed" class /// of bug: the reachability closure never sees these as syntactic calls). pub(crate) struct BinOpEntry { pub op: BinOp, /// D46 selector name (`03-syntax.md`) — e.g. `Add` -> `"plus"`. Names /// are NOT invented here; each one already existed per-operator before /// this unification (see the file history / git-blame on the pre-mv /// `bitwise_ops.rs` and the arithmetic arms this table replaces in /// `emit_c.rs`). pub method_name: &'static str, pub shape: OperandShape, } pub(crate) const BINOP_TABLE: &[BinOpEntry] = &[ BinOpEntry { op: BinOp::Add, method_name: "plus", shape: OperandShape::Homogeneous }, BinOpEntry { op: BinOp::Mul, method_name: "times", shape: OperandShape::Homogeneous }, BinOpEntry { op: BinOp::Div, method_name: "div", shape: OperandShape::Homogeneous }, BinOpEntry { op: BinOp::Mod, method_name: "rem", shape: OperandShape::Homogeneous }, BinOpEntry { op: BinOp::BitAnd, method_name: "bitand", shape: OperandShape::Homogeneous }, BinOpEntry { op: BinOp::BitOr, method_name: "bitor", shape: OperandShape::Homogeneous }, BinOpEntry { op: BinOp::BitXor, method_name: "bitxor", shape: OperandShape::Homogeneous }, BinOpEntry { op: BinOp::Sub, method_name: "minus", shape: OperandShape::Heterogeneous }, BinOpEntry { op: BinOp::Shl, method_name: "shl", shape: OperandShape::Heterogeneous }, BinOpEntry { op: BinOp::Shr, method_name: "shr", shape: OperandShape::Heterogeneous }, ]; /// One row of the D46 UNARY operator-dispatch table (`- ~`). `!` is NOT a /// member (D46-AMEND 2026-08-02, `spec/decisions/03-syntax.md`, owner /// decision, window p-op-channel): `@not` RETRACTED — `!` narrowed to /// STRICTLY `bool`, no longer a user-overloadable operator (mirrors `&&`/`||` /// never being overloadable, Rule 2/5a). The checker now hard-errors /// (`[E_UNARY_OPERAND_TYPE]`, `types/mod.rs`) on `!x` for any definitively- /// known non-`bool` operand instead of falling through to a `@not` dispatch /// attempt — so `UnOp::Not` deliberately has NO row here (and therefore no /// DCE seed via `lints.rs::collect_used_names`, which iterates this table). pub(crate) struct UnOpEntry { pub op: UnOp, pub method_name: &'static str, } pub(crate) const UNOP_TABLE: &[UnOpEntry] = &[ UnOpEntry { op: UnOp::Neg, method_name: "neg" }, UnOpEntry { op: UnOp::BitNot, method_name: "bitnot" }, ]; /// D46 selector name for a binary operator-overload, `None` for operators /// with no user-overload form (`==`/comparisons — `emit_c.rs` still handles /// those with its own `@equal`/`@compare` protocol-dispatch chain — and /// non-overloadable sugar like `==>`). pub(crate) fn binop_method_name(op: BinOp) -> Option<&'static str> { BINOP_TABLE.iter().find(|e| e.op == op).map(|e| e.method_name) } /// D46 selector name for a unary operator-overload. pub(crate) fn unop_method_name(op: UnOp) -> Option<&'static str> { UNOP_TABLE.iter().find(|e| e.op == op).map(|e| e.method_name) } /// Outcome of resolving a table-driven binary operator dispatch for a heap /// `Nova_T*` (record/sum VALUE, single pointer) receiver. pub(crate) enum BinOpResolution { /// Concrete non-generic overload found — its already-mangled `c_name`. Concrete(String), /// Generic-mono receiver (`Set[T]`-class, `____` in the receiver /// spelling): caller must `register_mono_method_instance(&fn_decl, /// type_subst, &mono_name, &recv_short)` before emitting a call to /// `mono_name`. Shared by ALL table entries — this is what closes /// [M-arith-binop-generic-receiver-no-mono-register]: `+ * / %` did not /// have this arm at all before unification (only `- & | ^ << >>` did). GenericMono { fn_decl: FnDecl, type_subst: Vec<(String, String)>, mono_name: String, }, /// A registered overload set exists on this receiver but none takes /// `rty` (Heterogeneous shape only) — hard compile error (D124). NoMatchingOverload(String), /// Nothing registered for this receiver/method — leave to fall-through /// untouched (would become invalid C, caught later). NotFound, } /// ONE resolver for every `BINOP_TABLE` entry on a `Nova_T*` (record/sum /// value, single pointer) receiver — replaces the separate per-operator /// flat+generic-mono arms `emit_c.rs` used to carry for Bit*/Shl-Shr/@minus, /// and ADDS the previously-missing generic-mono arm for `+ * / %` (closes /// [M-arith-binop-generic-receiver-no-mono-register]). /// /// Caller contract (mirrors the pre-unification `resolve_shift_dispatch`): /// - `recv_full`: `lty` (or the matched sum-type side) minus the trailing /// `*`, `Nova_` prefix INTACT (e.g. `Nova_Set____nova_int`, `Nova_Timestamp`). /// - `recv_short`: `recv_full` minus the `Nova_` prefix (`Set____nova_int`, /// `Timestamp`) — the key `method_overloads`/`self_method_decls` use. /// - `overloads`: `self.method_overloads.get(&(recv_short, op_method))` /// (only consulted for `Heterogeneous` shape — see `OperandShape`). /// - `mono_fn_decl`: `self.self_method_decls.get(&(base_type, op_method))` /// when `recv_short` carries a `____` mono marker, `None` otherwise. /// /// `channel_callee`: the checker's already-resolved callee span for THIS /// binary-expr node (`resolved_callees.get(&expr.id)`, p-op-w1b spike, /// `docs/plans/wip/196-op-channel-progress.md` рекомендация 1) — populated /// ONLY for a Heterogeneous op (`- << >>`) on a CONCRETE (non-generic, /// non-mono) `Nova_T*` receiver (types/mod.rs Binary-arm). When present AND /// it matches one of `overloads` by `fn_span`, it is consulted FIRST — skips /// the `rty`-string re-derivation below entirely (strangler-fig: absent for /// every other case — Homogeneous ops, GenericMono receivers, comparisons, /// or any producer gap — falls straight through to the unchanged scan). pub(crate) fn resolve_binop_dispatch( op: BinOp, rty: &str, recv_full: &str, recv_short: &str, overloads: Option<&[MethodSig]>, mono_fn_decl: Option<FnDecl>, channel_callee: Option<crate::diag::Span>, ) -> BinOpResolution { let Some(entry) = BINOP_TABLE.iter().find(|e| e.op == op) else { return BinOpResolution::NotFound; }; let op_method = entry.method_name; if let Some(chosen_span) = channel_callee { if let Some(sig) = overloads.unwrap_or(&[]).iter().find(|s| s.fn_span == Some(chosen_span)) { return BinOpResolution::Concrete(sig.c_name.clone()); } } if entry.shape == OperandShape::Heterogeneous { if let Some(sigs) = overloads { let matching = sigs.iter().find(|s| { s.is_instance && s.param_c_types.len() == 1 && s.param_c_types[0] == rty }); if let Some(sig) = matching { return BinOpResolution::Concrete(sig.c_name.clone()); } return BinOpResolution::NoMatchingOverload(format!( "binop `{:?}`: no @{} overload on {} taking {} \ (D46 03-syntax.md / D124: exact-overload cross-type rule). \ Available overloads: {}", op, op_method, recv_short, rty, sigs.iter() .filter(|s| s.is_instance && s.param_c_types.len() == 1) .map(|s| s.param_c_types[0].clone()) .collect::<Vec<_>>() .join(", "), )); } // No registered overload SET at all — either a generic-mono // receiver or truly nothing; falls through to the shared // generic-mono arm below, same as `Homogeneous`. } // Shared generic-mono branch (D46: `Set[T]`-class receivers where // `method_overloads` has no concrete mono'd entry — the erased key is // the base type, not the mono'd one). if let Some(idx) = recv_short.find("____") { let Some(fn_decl) = mono_fn_decl else { return BinOpResolution::NotFound; }; let mono_args = &recv_short[idx + 4..]; let mono_parts: Vec<String> = mono_args.split("__").map(|s| s.to_string()).collect(); let recv_generics: Vec<String> = fn_decl.receiver.as_ref() .map(|r| r.generics.iter().filter_map(|tr| { if let TypeRef::Named { path, .. } = tr { path.first().cloned() } else { None } }).collect::<Vec<_>>()) .unwrap_or_default(); if recv_generics.len() == mono_parts.len() { let type_subst: Vec<(String, String)> = recv_generics.into_iter().zip(mono_parts).collect(); let mono_name = format!("{}_method_{}", recv_full, op_method); return BinOpResolution::GenericMono { fn_decl, type_subst, mono_name }; } return BinOpResolution::NotFound; } // No `____` marker — non-generic (flat) receiver. match entry.shape { // Homogeneous flat receiver: the checker already guarantees the // overload exists (it type-checked `Self op Self`), so no runtime // `method_overloads` lookup is needed — emit the direct call. This // is the SAME blind emission `+ * / % & | ^` always did pre- // unification (byte-identical for every currently-passing flat // case); now routed through this ONE resolver instead of being // duplicated per operator at the call-site. OperandShape::Homogeneous => BinOpResolution::Concrete(format!("{}_method_{}", recv_full, op_method)), // Heterogeneous flat receiver with NO registered overload set at // all (the `overloads` param was `None`) — nothing to dispatch to; // leave to fall-through (would become invalid C, caught later). // Matches the pre-unification `@minus`/shift behavior exactly. OperandShape::Heterogeneous => BinOpResolution::NotFound, } } /// План 234 Ф.2 — integer-promotion таблица эмиссии унарного `~` на /// ПРИМИТИВНОМ C-типе (D46-амендмент, решение владельца 2026-07-27: эмитить /// надёжный низкий уровень, как обход и делал). C продвигает узкий операнд /// в `int` перед `~`; БЕЗЗНАКОВЫЕ узкие (`u8`->`nova_byte`, `u16`-> /// `uint16_t`) получают НУЛЕВОЕ продвижение, так что голый `~` оставил бы /// лишние единичные биты выше исходной ширины в expression-контексте — /// эмитим `x ^ MASK` нужной ширины вместо (маскирует продвинутые лишние /// биты, восстанавливая корректное узкое значение). ЗНАКОВЫЕ узкие /// (`i8`/`i16`) получают ЗНАКОВОЕ продвижение — голый `~x` УЖЕ корректен в /// диапазоне типа (XOR-маска была бы НЕВЕРНА без каста). Широкие типы /// (ранг >= `int`: `i32`/`i64`/`u32`/`u64`/`int`/`uint`) продвижения не /// имеют вовсе — голый `~` корректен. `operand_c` — уже эмитированный C- /// текст операнда; `operand_c_ty` — его C-тип, как даёт `infer_expr_c_type`. pub(crate) fn bitnot_primitive_emit(operand_c: &str, operand_c_ty: &str) -> String { let mask = match operand_c_ty { "nova_byte" => Some("0xFFU"), "uint16_t" => Some("0xFFFFU"), _ => None, }; match mask { Some(m) => format!("({} ^ {})", operand_c, m), None => format!("(~{})", operand_c), } } /// №274 [M-binop-stub-ignores-auto-byref-abi]: late-emitted BY-VALUE operator /// wrapper for a value-record `==`/`+ - * / %`/`< <= > >=` desugar /// (`nova_vr_ueq_*`/`nova_vr_binop_*`, emit_c.rs). The wrapper's OWN two /// params (`a`,`b`) stay by-value — call-sites remain rvalue-safe /// (`Timestamp.now() + d`) because `&a` inside the wrapper body is always a /// legal lvalue (a local parameter). The REAL method's second-parameter ABI /// may be `T*` when Plan 172.14 auto-by-ref applies (value-struct >16Б) — /// `is_byref` (caller: `param_c_types` pointer-suffix OR `method_byref_flag`, /// the latter needed because `param_c_types` predates the auto-by-ref pass /// and never carries the `*` for it) decides whether the wrapper forwards /// `&b` or `b` to the real method. Returns `(prototype, definition)`. pub(crate) fn emit_vr_wrapper( storage: &str, ret_c: &str, wrap_name: &str, recv_c: &str, arg_c: &str, real_c_name: &str, is_byref: bool, ) -> (String, String) { let arg_expr = if is_byref { "&b" } else { "b" }; let proto = format!( "{s}{ret} {w}({recv} a, {arg} b);\n", s = storage, ret = ret_c, w = wrap_name, recv = recv_c, arg = arg_c, ); let def = format!( "{s}{ret} {w}({recv} a, {arg} b) {{ return {c}(&a, {argexpr}); }}\n", s = storage, ret = ret_c, w = wrap_name, recv = recv_c, arg = arg_c, c = real_c_name, argexpr = arg_expr, ); (proto, def) } #[cfg(test)] mod tests { use super::*; #[test] fn binop_table_covers_arithmetic_bitwise_and_shift() { assert_eq!(binop_method_name(BinOp::Add), Some("plus")); assert_eq!(binop_method_name(BinOp::Sub), Some("minus")); assert_eq!(binop_method_name(BinOp::Mul), Some("times")); assert_eq!(binop_method_name(BinOp::Div), Some("div")); assert_eq!(binop_method_name(BinOp::Mod), Some("rem")); assert_eq!(binop_method_name(BinOp::BitAnd), Some("bitand")); assert_eq!(binop_method_name(BinOp::BitOr), Some("bitor")); assert_eq!(binop_method_name(BinOp::BitXor), Some("bitxor")); assert_eq!(binop_method_name(BinOp::Shl), Some("shl")); assert_eq!(binop_method_name(BinOp::Shr), Some("shr")); assert_eq!(binop_method_name(BinOp::Eq), None); } #[test] fn resolve_homogeneous_flat_receiver_emits_direct_call() { // No `____` marker, no overloads passed in (checker already // guaranteed the flat receiver has the overload) — Homogeneous ops // (`+ * / % & | ^`) resolve to a direct call, mirroring the // pre-unification blind emission exactly. match resolve_binop_dispatch(BinOp::Add, "Nova_Duration", "Nova_Duration", "Duration", None, None, None) { BinOpResolution::Concrete(c) => assert_eq!(c, "Nova_Duration_method_plus"), _ => panic!("expected Concrete"), } match resolve_binop_dispatch(BinOp::BitOr, "Nova_SetX", "Nova_SetX", "SetX", None, None, None) { BinOpResolution::Concrete(c) => assert_eq!(c, "Nova_SetX_method_bitor"), _ => panic!("expected Concrete"), } } #[test] fn resolve_heterogeneous_flat_receiver_no_overloads_is_not_found() { // Mirrors the pre-unification `@minus`/shift behavior: no // registered overload SET at all (not even a mismatched one) and no // `____` marker => NotFound, left for the caller's fall-through. match resolve_binop_dispatch(BinOp::Sub, "nova_int", "Nova_Timestamp", "Timestamp", None, None, None) { BinOpResolution::NotFound => {} _ => panic!("expected NotFound"), } } #[test] fn resolve_heterogeneous_channel_callee_wins_over_rty_mismatch() { // p-op-w1b spike: the checker-resolved `channel_callee` (fn_span) is // consulted FIRST — it picks the matching overload by SPAN identity // even when `rty` (the C-type string the legacy scan below would // match on) would otherwise miss. Proves the channel path is live // and takes priority over the fallback scan, not just a no-op. let hit_span = crate::diag::Span::new(10, 20); let other_span = crate::diag::Span::new(30, 40); let sigs = vec![ MethodSig { is_instance: true, param_c_types: vec!["Nova_Duration*".to_string()], c_name: "Nova_Timestamp_method_minus_duration".to_string(), fn_span: Some(other_span), ..Default::default() }, MethodSig { is_instance: true, param_c_types: vec!["Nova_Monotonic*".to_string()], c_name: "Nova_Timestamp_method_minus_monotonic".to_string(), fn_span: Some(hit_span), ..Default::default() }, ]; // `rty` deliberately matches NEITHER overload's `param_c_types` — a // legacy-only scan would return `NoMatchingOverload`; the channel // hit must short-circuit before that scan ever runs. match resolve_binop_dispatch( BinOp::Sub, "nova_int", "Nova_Timestamp", "Timestamp", Some(&sigs), None, Some(hit_span), ) { BinOpResolution::Concrete(c) => assert_eq!(c, "Nova_Timestamp_method_minus_monotonic"), _ => panic!("expected Concrete via channel, got a different resolution"), } } #[test] fn unop_table_covers_neg_bitnot_not_not() { assert_eq!(unop_method_name(UnOp::Neg), Some("neg")); assert_eq!(unop_method_name(UnOp::BitNot), Some("bitnot")); // D46-AMEND 2026-08-02: `@not` retracted — `!` no longer dispatches. assert_eq!(unop_method_name(UnOp::Not), None); } #[test] fn bitnot_narrow_unsigned_uses_mask() { assert_eq!(bitnot_primitive_emit("x", "nova_byte"), "(x ^ 0xFFU)"); assert_eq!(bitnot_primitive_emit("x", "uint16_t"), "(x ^ 0xFFFFU)"); } #[test] fn vr_wrapper_byref_forwards_address_of_b() { let (proto, def) = emit_vr_wrapper( "static ", "nova_bool", "nova_vr_ueq_BigRat", "NovaValue_BigRat", "NovaValue_BigRat", "Nova_BigRat_method_equal", true, ); assert_eq!(proto, "static nova_bool nova_vr_ueq_BigRat(NovaValue_BigRat a, NovaValue_BigRat b);\n"); assert_eq!( def, "static nova_bool nova_vr_ueq_BigRat(NovaValue_BigRat a, NovaValue_BigRat b) { return Nova_BigRat_method_equal(&a, &b); }\n" ); } #[test] fn vr_wrapper_by_value_forwards_b_unchanged() { let (_, def) = emit_vr_wrapper( "static ", "NovaValue_Duration", "nova_vr_binop_Nova_Duration_method_plus", "NovaValue_Duration", "nova_int", "Nova_Duration_method_plus", false, ); assert!(def.ends_with("{ return Nova_Duration_method_plus(&a, b); }\n")); } #[test] fn bitnot_signed_narrow_and_wide_use_bare_tilde() { assert_eq!(bitnot_primitive_emit("x", "int8_t"), "(~x)"); assert_eq!(bitnot_primitive_emit("x", "int16_t"), "(~x)"); assert_eq!(bitnot_primitive_emit("x", "uint32_t"), "(~x)"); assert_eq!(bitnot_primitive_emit("x", "nova_int"), "(~x)"); assert_eq!(bitnot_primitive_emit("x", "nova_uint"), "(~x)"); } }