Repository navigation
Conversation
Avoid promoting float and double arguments to real by adding type-preserving overloads backed by the corresponding Cephes single- and double-precision kernels. Retain their coefficients, approximation intervals, evaluation order, and underflow cutoffs. Leave the existing real implementation unchanged. Preserve function attributes and real results for integral arguments and integer enums. Document changes to inferred return types, precision, and range, including the explicit casts needed for real results and floating-point enum arguments. Retain the Cephes accuracy and early-underflow limitations for this initial port. In particular, erfc(10.0f) and erfc(27.0) return zero despite representable subnormal results. Defer numerical refinements to follow-up work. Cover return types, integral and enum calls, special values, signed zero, reference values, approximation boundaries, and inherited underflow behavior. Add a compatibility release note. Partially addresses dlang#11144. AI-Assisted-By: Codex (GPT-6 Astra Medium)
| // Preserve the real result of existing integral calls. | ||
| static foreach (I; AliasSeq!(bool, byte, ubyte, short, ushort, int, uint, long, ulong, char, wchar, dchar)) | ||
| { | ||
| /// ditto | ||
| real erf(I x) | ||
| { | ||
| return erf(cast(real) x); | ||
| } | ||
| } |
There was a problem hiding this comment.
Do we really need this?
And if so, can't this be done using constraints (optionally shorthand syntax)
real erf(I)(I x) if (isIntegral!I) => erf(cast(real) x);There was a problem hiding this comment.
I checked this with Codex (GPT-6 Astra Medium) on DMD 2.113 and LDC 1.43. The compatibility overloads are needed to keep existing calls such as erf(1) compiling. Unfortunately, the constrained template still leaves that call ambiguous alongside the non-template floating-point overloads:
import std.traits : isIntegral;
real f(real x) { return x; }
double f(double x) { return x; }
float f(float x) { return x; }
real f(I)(I x) if (isIntegral!I) { return f(cast(real) x); }
void main()
{
auto x = f(1); // Error: matches multiple overloads after implicit conversions
}Explicitly calling f!int(1) works, but would require callers to change their code. Broadening the constraint to include bool and character types does not resolve the ambiguity either; those types are excluded by isIntegral but were accepted by the previous real-only API.
I would therefore retain the non-template compatibility overloads generated by static foreach. The existing tests cover the built-in types and their integer enums.
There was a problem hiding this comment.
Accepting bool and character types in a math function is a bit weird though
There was a problem hiding this comment.
I agree. Those were mainly kept to avoid breaking changes. Below compiles currently (or changing the bools to char). After making this change, it would be easier to deprecate that behavior in a future PR if that's the approach phobos wants to take.
import std.mathspecial;
import std.stdio: writeln;
void main() {
bool x0 = 0;
auto y0 = erf(x0);
auto z0 = erfc(x0);
writeln(y0); //prints 0
writeln(z0); //prints 1
bool x1 = 1;
auto y1 = erf(x1);
auto z1 = erfc(x1);
writeln(y1); //prints ~0.842701
writeln(z1); //prints ~0.157299
}
There was a problem hiding this comment.
Hmm, I guess if you had an int overload (or whatever), then it should also take a bool input.
Use overloaded erf and erfc names for the internal float and double kernels. Move their coefficient tables into the existing private block and document the approximation formulas and intervals. Inline the single-use exponential-of-square helper into erfc(double), preserving its arithmetic. Keep the existing real-specific unit tests on explicitly real arguments after adding the internal overloads. AI-Assisted-By: Codex (GPT-6 Astra Medium)
Partially addresses #11144. The broader discussion originated in #10318.
Add type-preserving float and double overloads for
std.mathspecial.erfanderfc, using the corresponding Cephes kernels. Float calls use the native single-precision approximation. The existing real implementation is unchanged.Implementation
Port the coefficients, approximation intervals, evaluation order, and underflow cutoffs from Cephes
ndtr.candndtrf.c, with the double exponential-of-square calculation fromexpx2.c. Coefficients are reversed for Phobos' polynomial evaluator; implicit leading denominator coefficients are made explicit. Replace Cephes error reporting with Phobos return-value behavior and preserve@safe pure nothrow @nogc.Source archives: https://netlib.org/cephes/cprob.tgz and https://netlib.org/cephes/single.tgz.
Compatibility and numerical limitations
Float/double arguments now return their own type, changing
autoinference and available precision/range. Cast arguments to real to retain extended-precision behavior. Explicit integral overloads preserve real results for integral arguments and integer enums. Floating-point enums require an explicit cast to select an overload. A release note documents these changes.The port deliberately retains the original Cephes limitations. In particular,
erfc(10.0f)anderfc(27.0)return zero although their correctly rounded target-type results are nonzero subnormals. Improving tail handling is deferred to a follow-up.The single-precision kernel also trades accuracy for speed compared with computing in real and narrowing. On the sampled positive normal-result grid with Windows LDC, maximum relative errors were approximately
1.23e-7for float erf,3.80e-6for float erfc,2.61e-16for double erf, and9.15e-16for double erfc. These are measured sample maxima, not exhaustive bounds; zero results caused by the inherited cutoff are excluded from those relative-error figures.Performance
Local microbenchmarks compare the new kernels against the original real implementation narrowed to the same target type. The table shows the range of median paired reductions in execution time for both functions, excluding early-underflow tail cases.
The additional sampled tail inputs (float 9.5, 10, 11; double 27) were faster too, but those gains partly reflect returning zero early rather than computing subnormal erfc results. No sampled case was slower than the baseline.
Each case used seven rounds of 300,000 indirect calls, discarded the first round, rotated old/new function order, and consumed results. Flags were DMD
-O -inlineand LDC-O2, without fast-math. Host load varied; these results are not portable speed guarantees.Validation
Focused optimized tests passed on Windows DMD x64 and x86, Windows LDC for the affected modules, and Linux LDC. Coverage includes return types, integral and enum calls, signed zero, infinities, NaNs, reference values, float approximation boundaries, and inherited underflow behavior.
The broad numerical harness samples 6,456 float/double outputs per target and separately checks that real results remain identical to the original implementation. Independent high-precision checks characterize numerical error and the inherited cutoff limitations.
The full repository suite and binary128 runtime have not been tested locally. The previously observed Windows LDC assertion in the gamma module also occurs on the unchanged baseline; the affected-module tests pass.
This PR was written entirely by an LLM/AI.
AI-Assisted-By: Codex (GPT-6 Astra Medium)