From 77e6e3a7c637bf8cd9410151588244ee4e8c0fb9 Mon Sep 17 00:00:00 2001 From: darmie Date: Sun, 27 Sep 2026 19:55:39 +0000 Subject: [PATCH] Lowering: a hook the program defines for a library extern waits until something calls it Only declarations with a body count as a name declared twice, so a program's body for a library's extern hook is deferred like any other own function and the extern stands for it until a built body calls it. A program whose operator dunders are used only on typed instances no longer builds the dynamic arithmetic the arithmetic hook reaches. git-bug: ff74c2356bb2a69e5033ddc17a5e4a7662cc2efba6f9e633aea1cbd96301bdcf --- crates/compiler/src/lowering.rs | 6 +++- crates/zyntax_python/tests/reach_lowering.rs | 34 ++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/crates/compiler/src/lowering.rs b/crates/compiler/src/lowering.rs index 154c8c1d..bc77f14a 100644 --- a/crates/compiler/src/lowering.rs +++ b/crates/compiler/src/lowering.rs @@ -2855,7 +2855,7 @@ impl LoweringContext { /// the first of those appears. Kept are the entry points and what /// is entered some other way or does work at lowering: externs and /// bodiless declarations, fiber and async bodies, generics, - /// functions with effects or handlers, and a name declared twice, + /// functions with effects or handlers, and a name given two bodies, /// whose meaning depends on the order its declarations are built. /// /// `ZYNTAX_DISABLE_REACH_LOWERING=1` builds every own function up @@ -2868,10 +2868,14 @@ impl LoweringContext { if !self.config.closed || std::env::var_os("ZYNTAX_DISABLE_REACH_LOWERING").is_some() { return deferred; } + // Only bodies count: a body beside a bodiless declaration of its + // name, as a hook the program defines for a library's extern, has + // one meaning, and the extern stands for it until it is built. let mut seen = std::collections::HashSet::new(); let mut twice = std::collections::HashSet::new(); for decl in &program.declarations { if let TypedDeclaration::Function(f) = &decl.node + && f.body.is_some() && !seen.insert(f.name) { twice.insert(f.name); diff --git a/crates/zyntax_python/tests/reach_lowering.rs b/crates/zyntax_python/tests/reach_lowering.rs index b7480ae2..c7e87771 100644 --- a/crates/zyntax_python/tests/reach_lowering.rs +++ b/crates/zyntax_python/tests/reach_lowering.rs @@ -90,3 +90,37 @@ fn a_closed_program_builds_only_what_its_body_reaches() { let open = compiled(UNREACHED, false); assert!(open.iter().any(|n| n == "unused"), "{open:?}"); } + +const TYPED_OPERATOR: &str = r#" +class Vec3: + def __init__(self, x: float, y: float, z: float): + self.x = x + self.y = y + self.z = z + + def __add__(self, other): + return Vec3(self.x + other.x, self.y + other.y, self.z + other.z) + +def main() -> int: + a = Vec3(1.0, 2.0, 3.0) + acc = Vec3(0.0, 0.0, 0.0) + for i in range(10): + acc = acc + a + return int(acc.x + acc.y + acc.z) + +print(main()) +"#; + +/// A class's operator is reached only through its typed instances, so +/// the hook that dispatches a dynamic `a + b` to it is never called and +/// neither it nor the dynamic arithmetic it serves is built. +#[test] +fn an_operator_used_on_typed_instances_builds_no_dynamic_arithmetic() { + let built = compiled(TYPED_OPERATOR, true); + for name in ["zb_hook_instance_arith", "zb_any_arith"] { + assert!( + !built.iter().any(|n| n == name), + "{name} is built for a program that never boxes an instance" + ); + } +}