Skip to content

Commit 790de37

Browse files
committed
feat(core): 16-bit floats gain the transcendental families (#2384 unit 2b)
39 members on each type -- the transcendental, root, power and angular families. Purely additive. Every one is a float round-trip, and that is .NET's own shape, verified member by member against Half.cs and BFloat16.cs. So unlike units 1 and 2a there is no bit-level body to get wrong here, which changes what is worth testing: the risk in a forwarding table is not arithmetic but MIS-WIRING -- Sin calling Cos, or Atan2's arguments swapped -- and every compile-only test passes against both. The cases therefore assert that each name reaches ITS OWN float function, comparing against FromSingle(expected(in)) rather than a literal so they test the FORWARDING rather than MathF's accuracy, plus cross-checks that three members disagree on the same input, plus operands chosen so a swapped argument order gives a different answer. UNIT 2a's OWN ESTIMATE WAS WRONG AND THE MEASUREMENT CORRECTS IT. It put this unit at "~45 members per type, mostly one-line forwards"; only 39 could be forwarded -- 25 via MathF, 13 via Single, and 10 via neither. Compound, ExpM1, Exp2M1, Exp10M1, LogP1, Log2P1, Log10P1, Lerp, MultiplyAddEstimate and ClampNative have no counterpart in this port's System::MathF OR System::Single, so adding them here would mean widening FLOAT'S OWN public surface first -- a different type's API and a different question from this ticket's. They are pinned absent on both types, so whichever ticket widens System::Single trips that pin and can complete the 16-bit types in the same change. Ieee754Remainder is the one member whose .NET name and this port's MathF name differ (IEEERemainder); the forward is spelled out and pinned, because a mis-wiring there would be invisible to a name-based reading. Seven mutations, all caught. Only unit 3 remains: 43 conversion operators on Half and 47 on BFloat16. Gate: 17,582 run, 17,582 passed, 0 failed, 0 skipped across 38 executables (+3). Build directory: build/ only, --parallel 2 throughout.
1 parent be56329 commit 790de37

6 files changed

Lines changed: 418 additions & 13 deletions

File tree

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

docs/Migration-SixteenBitFloatMathSurface.md

Lines changed: 57 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -112,14 +112,65 @@ Six mutations, all caught: `MaxNumber` forwarding to `Max`; dropping its signed-
112112
returning `-1` for `-0.0`; `Sign` returning `0` instead of throwing; `Round` ties away from zero;
113113
and `BFloat16::MinNumber` propagating NaN.
114114

115+
---
116+
117+
# Unit 2b — the transcendental, root, power and angular families
118+
119+
Landed 2026-08-19, same day. Purely additive. **39 members on each type.**
120+
121+
## Every one is a float round-trip, and that is .NET's own shape
122+
123+
Verified member by member against `Half.cs` and `BFloat16.cs`: they are all
124+
`(Half)MathF.Sqrt((float)x)` and friends. So unlike units 1 and 2a there is **no bit-level body to
125+
get wrong here** — which changes what is worth testing. The risk in a forwarding table is not
126+
arithmetic, it is **mis-wiring**: `Sin` calling `Cos`, or `Atan2`'s arguments swapped. Every
127+
compile-only test passes against both.
128+
129+
So the tests assert that each name reaches **its own** float function, comparing against
130+
`FromSingle(expected(in))` rather than a literal — which makes them tests of the *forwarding*
131+
rather than of `MathF`'s accuracy — plus explicit cross-checks that three members **disagree** with
132+
each other on the same input, and operands chosen so that a swapped argument order gives a
133+
different answer.
134+
135+
## The count in the previous unit's note was wrong, and the measurement corrects it
136+
137+
Unit 2a's note estimated unit 2b at "~45 members per type, mostly one-line forwards". **Only 39 of
138+
them could be forwarded**, and the reason is a real boundary rather than an oversight:
139+
140+
| forward target | members |
141+
|---|---|
142+
| `System::MathF` | 25 |
143+
| `System::Single` | 13 |
144+
| **neither — absent from this port's `float` surface entirely** | **10** |
145+
146+
**Ten members .NET declares have no counterpart in this port's `System::MathF` *or*
147+
`System::Single`**: `Compound`, `ExpM1`, `Exp2M1`, `Exp10M1`, `LogP1`, `Log2P1`, `Log10P1`, `Lerp`,
148+
`MultiplyAddEstimate` and `ClampNative`. Adding them to the 16-bit floats would mean widening
149+
**`float`'s own public surface first** — a different type's API, and a different question from
150+
*"should the 16-bit floats carry the MathF-forwarding surface"*. They are pinned absent on both
151+
types, so whichever ticket widens `System::Single` trips that pin and can complete the 16-bit types
152+
in the same change.
153+
154+
`MultiplyAddEstimate` and `ClampNative` are **doubly** absent: `float` lacks them here *and* .NET
155+
declares them on `Half` only.
156+
157+
## One naming difference worth keeping
158+
159+
`Ieee754Remainder` is the only member whose .NET name and this port's `MathF` name differ — .NET
160+
calls it `Ieee754Remainder` on the float types and `IEEERemainder` on `MathF`, and this port's
161+
`MathF` follows the latter. The forward is spelled out and pinned, because a mis-wiring there would
162+
be invisible to a name-based reading.
163+
164+
## Mutation testing
165+
166+
Seven mutations, all caught: `Sin` wired to `Cos`; `Atan2` arguments swapped; two-argument `Log`
167+
swapped; `ScaleB`'s integer argument routed through `float`; `Exp2` wired to `Exp10`;
168+
`BFloat16::Sqrt` wired to `Cbrt`; and `Ieee754Remainder` wired to `Pow`.
169+
115170
## What is still to come
116171

117-
**Unit 2b** is the transcendental families proper (`Sqrt`, `Cbrt`, `RootN`, `Exp`, `Log`, `Pow`,
118-
`Compound`, the trigonometric and `*Pi` sets, the hyperbolic set, `Hypot`, `ScaleB`, `Lerp`,
119-
`FusedMultiplyAdd`, `DegreesToRadians`/`RadiansToDegrees`, the two estimates) — measured at **~45
120-
members per type**, mostly one-line forwards. **Unit 3** is the conversion operators, measured at
121-
**43 on `Half` and 47 on `BFloat16`**. Both must move the two types in step, and unit 2b's absence
122-
is pinned on both types today via `Sqrt`.
172+
**Unit 3** — the conversion operators, measured at **43 on `Half` and 47 on `BFloat16`**. It must
173+
move the two types in step like every unit before it.
123174

124175
Out of scope permanently, and unchanged by this ticket: **generic-math conformance**
125176
(`INumber<T>`, `IFloatingPointIeee754<T>`, `IMinMaxValue<T>`). .NET's `BFloat16` implements 36

modules/core/include/System/Half.hpp

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,99 @@ namespace System {
283283
* (`Half.cs:1879`). */
284284
[[nodiscard]] static Half MinMagnitude(Half x, Half y) noexcept;
285285

286+
// -----------------------------------------------------------------------------------
287+
// #2384 unit 2b: the transcendental, root, power and angular families.
288+
//
289+
// EVERY ONE IS A FLOAT ROUND-TRIP, and that is .NET's own shape rather than a
290+
// simplification -- `(Half)MathF.Sqrt((float)x)` and friends, verified member by member
291+
// against Half.cs. So unlike units 1 and 2a, there is no bit-level body here to get wrong.
292+
//
293+
// TEN MEMBERS .NET DECLARES ARE ABSENT, AND THAT IS MEASURED RATHER THAN OVERLOOKED:
294+
// Compound, ExpM1, Exp2M1, Exp10M1, LogP1, Log2P1, Log10P1, Lerp, MultiplyAddEstimate and
295+
// ClampNative have NO counterpart in this port's System::MathF OR System::Single, so
296+
// adding them here would mean widening `float`'s surface first -- a different type's
297+
// public API and a different ticket's scope. See the migration note.
298+
// -----------------------------------------------------------------------------------
299+
300+
/** @brief C++ counterpart of .NET `Half.Acos` -- `(Half)MathF.Acos((float)...)`. */
301+
[[nodiscard]] static Half Acos(Half x) noexcept { return FromSingle(System::MathF::Acos(x.ToSingle())); }
302+
/** @brief C++ counterpart of .NET `Half.Acosh` -- `(Half)MathF.Acosh((float)...)`. */
303+
[[nodiscard]] static Half Acosh(Half x) noexcept { return FromSingle(System::MathF::Acosh(x.ToSingle())); }
304+
/** @brief C++ counterpart of .NET `Half.Asin` -- `(Half)MathF.Asin((float)...)`. */
305+
[[nodiscard]] static Half Asin(Half x) noexcept { return FromSingle(System::MathF::Asin(x.ToSingle())); }
306+
/** @brief C++ counterpart of .NET `Half.Asinh` -- `(Half)MathF.Asinh((float)...)`. */
307+
[[nodiscard]] static Half Asinh(Half x) noexcept { return FromSingle(System::MathF::Asinh(x.ToSingle())); }
308+
/** @brief C++ counterpart of .NET `Half.Atan` -- `(Half)MathF.Atan((float)...)`. */
309+
[[nodiscard]] static Half Atan(Half x) noexcept { return FromSingle(System::MathF::Atan(x.ToSingle())); }
310+
/** @brief C++ counterpart of .NET `Half.Atanh` -- `(Half)MathF.Atanh((float)...)`. */
311+
[[nodiscard]] static Half Atanh(Half x) noexcept { return FromSingle(System::MathF::Atanh(x.ToSingle())); }
312+
/** @brief C++ counterpart of .NET `Half.Atan2` -- `(Half)MathF.Atan2((float)...)`. */
313+
[[nodiscard]] static Half Atan2(Half y, Half x) noexcept { return FromSingle(System::MathF::Atan2(y.ToSingle(), x.ToSingle())); }
314+
/** @brief C++ counterpart of .NET `Half.Cbrt` -- `(Half)MathF.Cbrt((float)...)`. */
315+
[[nodiscard]] static Half Cbrt(Half x) noexcept { return FromSingle(System::MathF::Cbrt(x.ToSingle())); }
316+
/** @brief C++ counterpart of .NET `Half.Cos` -- `(Half)MathF.Cos((float)...)`. */
317+
[[nodiscard]] static Half Cos(Half x) noexcept { return FromSingle(System::MathF::Cos(x.ToSingle())); }
318+
/** @brief C++ counterpart of .NET `Half.Cosh` -- `(Half)MathF.Cosh((float)...)`. */
319+
[[nodiscard]] static Half Cosh(Half x) noexcept { return FromSingle(System::MathF::Cosh(x.ToSingle())); }
320+
/** @brief C++ counterpart of .NET `Half.Exp` -- `(Half)MathF.Exp((float)...)`. */
321+
[[nodiscard]] static Half Exp(Half x) noexcept { return FromSingle(System::MathF::Exp(x.ToSingle())); }
322+
/** @brief C++ counterpart of .NET `Half.Log` -- `(Half)MathF.Log((float)...)`. */
323+
[[nodiscard]] static Half Log(Half x) noexcept { return FromSingle(System::MathF::Log(x.ToSingle())); }
324+
/** @brief C++ counterpart of .NET `Half.Log10` -- `(Half)MathF.Log10((float)...)`. */
325+
[[nodiscard]] static Half Log10(Half x) noexcept { return FromSingle(System::MathF::Log10(x.ToSingle())); }
326+
/** @brief C++ counterpart of .NET `Half.Log2` -- `(Half)MathF.Log2((float)...)`. */
327+
[[nodiscard]] static Half Log2(Half x) noexcept { return FromSingle(System::MathF::Log2(x.ToSingle())); }
328+
/** @brief C++ counterpart of .NET `Half.Pow` -- `(Half)MathF.Pow((float)...)`. */
329+
[[nodiscard]] static Half Pow(Half x, Half y) noexcept { return FromSingle(System::MathF::Pow(x.ToSingle(), y.ToSingle())); }
330+
/** @brief C++ counterpart of .NET `Half.Sin` -- `(Half)MathF.Sin((float)...)`. */
331+
[[nodiscard]] static Half Sin(Half x) noexcept { return FromSingle(System::MathF::Sin(x.ToSingle())); }
332+
/** @brief C++ counterpart of .NET `Half.Sinh` -- `(Half)MathF.Sinh((float)...)`. */
333+
[[nodiscard]] static Half Sinh(Half x) noexcept { return FromSingle(System::MathF::Sinh(x.ToSingle())); }
334+
/** @brief C++ counterpart of .NET `Half.Sqrt` -- `(Half)MathF.Sqrt((float)...)`. */
335+
[[nodiscard]] static Half Sqrt(Half x) noexcept { return FromSingle(System::MathF::Sqrt(x.ToSingle())); }
336+
/** @brief C++ counterpart of .NET `Half.Tan` -- `(Half)MathF.Tan((float)...)`. */
337+
[[nodiscard]] static Half Tan(Half x) noexcept { return FromSingle(System::MathF::Tan(x.ToSingle())); }
338+
/** @brief C++ counterpart of .NET `Half.Tanh` -- `(Half)MathF.Tanh((float)...)`. */
339+
[[nodiscard]] static Half Tanh(Half x) noexcept { return FromSingle(System::MathF::Tanh(x.ToSingle())); }
340+
/** @brief C++ counterpart of .NET `Half.ScaleB` -- `(Half)MathF.ScaleB((float)...)`. */
341+
[[nodiscard]] static Half ScaleB(Half x, SharpRuntime::intcs n) noexcept { return FromSingle(System::MathF::ScaleB(x.ToSingle(), n)); }
342+
/** @brief C++ counterpart of .NET `Half.FusedMultiplyAdd` -- `(Half)MathF.FusedMultiplyAdd((float)...)`. */
343+
[[nodiscard]] static Half FusedMultiplyAdd(Half left, Half right, Half addend) noexcept { return FromSingle(System::MathF::FusedMultiplyAdd(left.ToSingle(), right.ToSingle(), addend.ToSingle())); }
344+
/** @brief C++ counterpart of .NET `Half.ReciprocalEstimate` -- `(Half)MathF.ReciprocalEstimate((float)...)`. */
345+
[[nodiscard]] static Half ReciprocalEstimate(Half x) noexcept { return FromSingle(System::MathF::ReciprocalEstimate(x.ToSingle())); }
346+
/** @brief C++ counterpart of .NET `Half.ReciprocalSqrtEstimate` -- `(Half)MathF.ReciprocalSqrtEstimate((float)...)`. */
347+
[[nodiscard]] static Half ReciprocalSqrtEstimate(Half x) noexcept { return FromSingle(System::MathF::ReciprocalSqrtEstimate(x.ToSingle())); }
348+
/** @brief C++ counterpart of .NET `Half.Ieee754Remainder` -- `(Half)MathF.IEEERemainder((float)...)`. */
349+
[[nodiscard]] static Half Ieee754Remainder(Half left, Half right) noexcept { return FromSingle(System::MathF::IEEERemainder(left.ToSingle(), right.ToSingle())); }
350+
/** @brief C++ counterpart of .NET `Half.Log` -- `(Half)MathF.Log((float)...)`. */
351+
[[nodiscard]] static Half Log(Half x, Half newBase) noexcept { return FromSingle(System::MathF::Log(x.ToSingle(), newBase.ToSingle())); }
352+
/** @brief C++ counterpart of .NET `Half.AcosPi` -- `(Half)Single.AcosPi((float)...)`. */
353+
[[nodiscard]] static Half AcosPi(Half x) noexcept { return FromSingle(System::Single::AcosPi(x.ToSingle())); }
354+
/** @brief C++ counterpart of .NET `Half.AsinPi` -- `(Half)Single.AsinPi((float)...)`. */
355+
[[nodiscard]] static Half AsinPi(Half x) noexcept { return FromSingle(System::Single::AsinPi(x.ToSingle())); }
356+
/** @brief C++ counterpart of .NET `Half.AtanPi` -- `(Half)Single.AtanPi((float)...)`. */
357+
[[nodiscard]] static Half AtanPi(Half x) noexcept { return FromSingle(System::Single::AtanPi(x.ToSingle())); }
358+
/** @brief C++ counterpart of .NET `Half.Atan2Pi` -- `(Half)Single.Atan2Pi((float)...)`. */
359+
[[nodiscard]] static Half Atan2Pi(Half y, Half x) noexcept { return FromSingle(System::Single::Atan2Pi(y.ToSingle(), x.ToSingle())); }
360+
/** @brief C++ counterpart of .NET `Half.CosPi` -- `(Half)Single.CosPi((float)...)`. */
361+
[[nodiscard]] static Half CosPi(Half x) noexcept { return FromSingle(System::Single::CosPi(x.ToSingle())); }
362+
/** @brief C++ counterpart of .NET `Half.SinPi` -- `(Half)Single.SinPi((float)...)`. */
363+
[[nodiscard]] static Half SinPi(Half x) noexcept { return FromSingle(System::Single::SinPi(x.ToSingle())); }
364+
/** @brief C++ counterpart of .NET `Half.TanPi` -- `(Half)Single.TanPi((float)...)`. */
365+
[[nodiscard]] static Half TanPi(Half x) noexcept { return FromSingle(System::Single::TanPi(x.ToSingle())); }
366+
/** @brief C++ counterpart of .NET `Half.DegreesToRadians` -- `(Half)Single.DegreesToRadians((float)...)`. */
367+
[[nodiscard]] static Half DegreesToRadians(Half degrees) noexcept { return FromSingle(System::Single::DegreesToRadians(degrees.ToSingle())); }
368+
/** @brief C++ counterpart of .NET `Half.RadiansToDegrees` -- `(Half)Single.RadiansToDegrees((float)...)`. */
369+
[[nodiscard]] static Half RadiansToDegrees(Half radians) noexcept { return FromSingle(System::Single::RadiansToDegrees(radians.ToSingle())); }
370+
/** @brief C++ counterpart of .NET `Half.Exp10` -- `(Half)Single.Exp10((float)...)`. */
371+
[[nodiscard]] static Half Exp10(Half x) noexcept { return FromSingle(System::Single::Exp10(x.ToSingle())); }
372+
/** @brief C++ counterpart of .NET `Half.Exp2` -- `(Half)Single.Exp2((float)...)`. */
373+
[[nodiscard]] static Half Exp2(Half x) noexcept { return FromSingle(System::Single::Exp2(x.ToSingle())); }
374+
/** @brief C++ counterpart of .NET `Half.Hypot` -- `(Half)Single.Hypot((float)...)`. */
375+
[[nodiscard]] static Half Hypot(Half x, Half y) noexcept { return FromSingle(System::Single::Hypot(x.ToSingle(), y.ToSingle())); }
376+
/** @brief C++ counterpart of .NET `Half.RootN` -- `(Half)Single.RootN((float)...)`. */
377+
[[nodiscard]] static Half RootN(Half x, SharpRuntime::intcs n) noexcept { return FromSingle(System::Single::RootN(x.ToSingle(), n)); }
378+
286379
// -----------------------------------------------------------------------------------
287380
// #2384 unit 2a: rounding, Sign, and the IEEE 754:2019 *Number family.
288381
//

0 commit comments

Comments
 (0)