Three production sites use std transcendentals, and hypot diverges more than the exp case the rule cites #63

Closed
opened 2026-09-09 14:53:07 +00:00 by logaritmisk · 0 comments
Owner

CLAUDE.md's invariant:

Transcendentals go through libm, not std. IEEE 754 pins the basic operations and sqrt but says nothing about exp/log/erf, and std delegates to the system math library — measured, f64::exp and libm::exp disagree on 9.7% of inputs by one ULP. Since inference is an iterative fixed point, one ULP can change an iteration count.

Three production sites violate it. Each was confirmed to sit outside its file's #[cfg(test)] boundaries — margin.rs has test blocks at 61 and 96, and cavity_log_evidence at 80-95 falls between them.

site call measured std-vs-libm divergence on this host
src/factor/margin.rs:84 cavity.sigma().hypot(sigma) 12.136% of 1e6 inputs differ, max 1 ULP
src/factor/margin.rs:92 f64::MIN_POSITIVE.ln() 3.437% of 1e6 inputs, max 1 ULP
src/factor/trunc.rs:98 f64::MIN_POSITIVE.ln() same

hypot is the material one

It is on the production path for every scored event, and its divergence rate of 12.1% is higher than the 9.7% the rule cites for exp as its own justification. libm::hypot exists and is a drop-in.

Worth being clear that the hypot choice is correct and should stay — the comment above it explains exactly why, and it was measured: hypot(1e200, 1.0) = 1e200 where naive sqrt(a*a + b*b) gives inf, and hypot(1e-200, 1e-200) = 1.414e-200 where naive gives 0. This issue is only about which hypot.

The two ln calls

f64::MIN_POSITIVE.ln() happens to agree bit-for-bit with libm::log(f64::MIN_POSITIVE) on this host — 0xc086232bdd7abcd2 both ways — so there is no measured effect here. But that is precisely the platform dependence the rule exists to remove: it is a constant, evaluated at runtime through the system libm, and nothing guarantees the same bits on a different target. Both are trivially libm::log(f64::MIN_POSITIVE), or better, a const computed once.

Everything else is clean

src/lib.rs and src/quadrature.rs production code route through libm correctly; the .exp()/.ln() hits there are all inside #[cfg(test)], which the rule explicitly permits. src/predict.rs:512 (hypot) and src/acquisition.rs:228 (ln) are also test-only.

Worth adding

A lint or a test that greps for \.(exp|ln|log|powf|hypot|erf|erfc)\( outside #[cfg(test)] would make this self-enforcing. The rule is stated clearly in CLAUDE.md and was still violated in three places, which suggests prose is not sufficient to hold it.

Found by a floating-point audit, 2026-09-09. Sites confirmed by reading; divergence rates from the audit's measurement.

CLAUDE.md's invariant: > **Transcendentals go through `libm`, not `std`.** IEEE 754 pins the basic operations and `sqrt` but says nothing about `exp`/`log`/`erf`, and `std` delegates to the *system* math library — measured, `f64::exp` and `libm::exp` disagree on 9.7% of inputs by one ULP. Since inference is an iterative fixed point, one ULP can change an iteration count. Three production sites violate it. Each was confirmed to sit outside its file's `#[cfg(test)]` boundaries — `margin.rs` has test blocks at 61 and 96, and `cavity_log_evidence` at 80-95 falls between them. | site | call | measured std-vs-libm divergence on this host | |---|---|---| | `src/factor/margin.rs:84` | `cavity.sigma().hypot(sigma)` | **12.136%** of 1e6 inputs differ, max 1 ULP | | `src/factor/margin.rs:92` | `f64::MIN_POSITIVE.ln()` | 3.437% of 1e6 inputs, max 1 ULP | | `src/factor/trunc.rs:98` | `f64::MIN_POSITIVE.ln()` | same | ## `hypot` is the material one It is on the production path for **every scored event**, and its divergence rate of 12.1% is *higher* than the 9.7% the rule cites for `exp` as its own justification. `libm::hypot` exists and is a drop-in. Worth being clear that the `hypot` **choice** is correct and should stay — the comment above it explains exactly why, and it was measured: `hypot(1e200, 1.0) = 1e200` where naive `sqrt(a*a + b*b)` gives `inf`, and `hypot(1e-200, 1e-200) = 1.414e-200` where naive gives `0`. This issue is only about which `hypot`. ## The two `ln` calls `f64::MIN_POSITIVE.ln()` happens to agree bit-for-bit with `libm::log(f64::MIN_POSITIVE)` on this host — `0xc086232bdd7abcd2` both ways — so there is no measured effect here. But that is precisely the platform dependence the rule exists to remove: it is a constant, evaluated at runtime through the system libm, and nothing guarantees the same bits on a different target. Both are trivially `libm::log(f64::MIN_POSITIVE)`, or better, a `const` computed once. ## Everything else is clean `src/lib.rs` and `src/quadrature.rs` production code route through `libm` correctly; the `.exp()`/`.ln()` hits there are all inside `#[cfg(test)]`, which the rule explicitly permits. `src/predict.rs:512` (`hypot`) and `src/acquisition.rs:228` (`ln`) are also test-only. ## Worth adding A lint or a test that greps for `\.(exp|ln|log|powf|hypot|erf|erfc)\(` outside `#[cfg(test)]` would make this self-enforcing. The rule is stated clearly in CLAUDE.md and was still violated in three places, which suggests prose is not sufficient to hold it. Found by a floating-point audit, 2026-09-09. Sites confirmed by reading; divergence rates from the audit's measurement.
logaritmisk added the numericstesting labels 2026-09-09 14:54:09 +00:00
Sign in to join this conversation.