Add core::ffi::c_longdouble - #4003
Conversation
| **X87 F80** | ||
|
|
||
| - `f80x87`, consistent with the recent `f16b` | ||
| - `f80`, rejected because it suggests this is a first-class type like f32 and f128, it is not | ||
| - `__float80`, similar to `__m256i` and similar platform-specific types in stdarch | ||
| - `x86f80` or `x86_f80` or `X86F80` | ||
| - `x87f80` or `x87_f80` or `X87F80` |
There was a problem hiding this comment.
What should the X87 F80 type be called?
To keep this somewhat organized it would help if you could start your post with a list
- of your most preferred option
- to
- your least preferred but still acceptable option
That way we can get a sense of what names have some consensus, even if they are not everyone's favorite. You can then of course elaborate on your reasoning.
There was a problem hiding this comment.
since these are supposed to live in core::arch::x86::* not core::num::* (line 46), the name should follow whatever naming convention used in the platform intrinsics (which is why we retain the ugly names like __m128 and _mm_xor_pd).
i think msvc and icc don't give it as special name, just long double. so core::arch::x86::__float80 following GCC & clang is perhaps the only good answer.
There was a problem hiding this comment.
For me, I'd say:
x87_f80f80_x87__float80f80x87
f80x87 looks too similar to vector types like f32x16. It's nice to have x87 indicating what it is, __float80 is nice for consistency with the C compilers but doesn't do that quite as well.
x87_f80 reads slightly better than f80_x87 because it feels more like a scoping that is extensible. Not that we're likely to actually extend it, but x87_fsqrt, x87_fsin, x87_fptan strike me as better names for operations than fsqrt_x87.
There was a problem hiding this comment.
It's nice to have
x87indicating what it is,__float80is nice for consistency with the C compilers but doesn't do that quite as well.
TBF if ambiguity is an issue, using the qualified form like core::arch::{x86, x86_64}::__float80 is clear enough for distinguishing from e.g. a core::arch::m68k::__float80.
(BTW the C23 standard named the type _Float64x (extended-precision from _Float64), which I find it a very bad idea)
There was a problem hiding this comment.
For everything else in core::arch we use exactly the C name, so I think we should do the same here:
core::arch::{x86, x86_64}::__float80- (big gap)
core::arch::{x86, x86_64}::x87_f80/core::arch::{x86, x86_64}::f80_x87
| **IBM F128** | ||
|
|
||
| - `f128ppc`, consistent with the recent `f16b` | ||
| - `__ibm128`, similar to `__m256i` and similar platform-specific types in stdarch | ||
| - `ppcf128` or `ppc_f128` | ||
| - `ibmf128` or `ibm_f128` | ||
| - `doubledouble` or `DoubleDouble` | ||
| - `f64f64` or `F64F64` |
There was a problem hiding this comment.
What should the IBM F128 type be called?
To keep this somewhat organized it would help if you could start your post with a list
- of your most preferred option
- to
- your least preferred but still acceptable option
That way we can get a sense of what names have some consensus, even if they are not everyone's favorite. You can then of course elaborate on your reasoning.
There was a problem hiding this comment.
I find these names acceptable, in order of preference:
ibmf128oribm_f128__ibm128, but to be in line with the newer stdarch types, I'd prefer omitting the__, soibm128doubledoubleorDoubleDouble
Reason for 1. and 2. is that they are very much in line with the C type names and are not ambiguous or misleading. I consider 3. also okay, and it is the more descriptive name for the type if we care about not naming a type after IBM.
I would like to avoid f128ppc or ppcf128/ppc_f128 et al. because to me these seem like they would imply that this is the "f128 for PowerPC", which it really isn't. There's both the IEEE quad precision f128 on PowerPC as well as the IBM double-double type, and they're distinct things. f128 in Rust is the IEEE quad precision type, and that one exists under the same name.
For f64f64 I just think it's an all-around worse version of doubledouble naming-wise. This would be the name I would perhaps choose if this were some kind of type for more than just FFI purposes, but given the context, doubledouble makes more sense to me.
There was a problem hiding this comment.
In my opinion:
ibm128__ibm128DoubleDouble
Given the primary purpose of the types is C interop, I think matching the C name (with or without the leading underscores) is the best choice. I don't think we should use anything containing f128 as that gives the misleading impression that this is related to the actual f128 type, which it is not. If we don't want to go with the C name, I think DoubleDouble is the best choice as according to this paper the type is "called “double-double” in most of the literature". If we don't aim to match the C type name with ibm128/__ibm128, I think we should stick with the Rust standard CamelCase naming convention as this is not a Rust primitive type.
There was a problem hiding this comment.
IBM surely is a registered trademark, do we have to be concerned about using it in a type name?
There was a problem hiding this comment.
Assuming that's not an issue, I think my default choice would be that, as usual in core::arch, we should use exactly the C name:
core::arch::{powerpc, powerpc64}::__ibm128core::arch::{powerpc, powerpc64}::ibm128- (big gap)
core::arch::{powerpc, powerpc64}::F64Pairor so
None of the "new" names really cut it for me. I don't think we should start using "double" to refer to f64 when that is not a Rust term and not even C does this here. So the only alternatives that appeal to me involve indicating that it's a pair of f64.
There was a problem hiding this comment.
IBM surely is a registered trademark, do we have to be concerned about using it in a type name?
I wouldn't expect that to be an issue since we're using IBM to refer to a type they invented and they chose that name, sorta like it's just fine to use std::os::windows to refer to a trademarked OS that Microsoft invented.
There was a problem hiding this comment.
since when is registered trademark a problem? "Arm", "MIPS", "AMD", "Hexagon", "RISC-V", "Windows", "UNIX", "Linux" are all registered trademark and they all appear in core::arch::* or std::os::*.
(if using __ibm128 violates the IBM trademark rules, the existing prior art in GCC and LLVM must also be considered 🙃)
There was a problem hiding this comment.
Those are all good points regarding trademarks, sorry for the distraction.
| ### Normalization | ||
|
|
||
| Operations on `f128ppc` assume that the value is normalized: the high component is assumed to be at least as large was the low component. Hence whether a value is positive or negative can only look at the sign bit of the high component. | ||
|
|
||
| When creating a `f128ppc` value from bytes (using `transmute`, `from_le_bytes`, etc) this invariant can be broken. That can cause nonsensical results, but cannot cause UB. |
There was a problem hiding this comment.
Relevant wording from the ABI specs. The summary is that high + low (the regular f64 add operation) must equal high, and that we should always set the low to +0/-0 on infinity (the ELFv1 spec, at least the latest version I could find, doesn't seem to require this so we should make sure we can accept infinities with other values of low).
64-bit ELFv2
In this format, double-precision numbers with different magnitudes that do not overlap provide an effective precision of 106 bits or more, depending on the value. The high-order double-precision value (the one that comes first in storage) must have the larger magnitude. The high-order double- precision value must equal the sum of the two values, rounded to nearest double (the Linux convention, unlike AIX).
(In the ELFv2 spec, the following quote appears in the previous section which is about the standard IEEE f128; however this appears to be a drafting error as it refers to the low-order value being positive or negative zero, which only makes sense in the context of IBM f128, not IEEE f128. Additionally, a similar sentence appears in the correct section in the ELFv1 spec.)
This implementation does not fully support the IEEE special numbers NaN and INF. These values are encoded in the high-order double value only. The low-order value is not significant, but the low-order value of an infinity must be positive or negative zero.
64-bit ELFv1
"Extended precision" is the IBM AIX® 128-bit long double format composed of two double-precision numbers with different magnitudes that do not overlap. The high-order double-precision value (the one that comes first in storage) must have the larger magnitude. The value of the extended-precision number is the sum of the two double-precision values.
Does not fully support the IEEE special numbers NaN and INF. These values are encoded in the high-order double value only. The low-order value is not significant.
32-bit
In this format, double precision numbers with different magnitudes that do not overlap, provide an effective precision of 106-bits. The high-order double-precision value (the one that comes first in storage) must have the larger magnitude. The high-order double-precision value must equal the sum of the two values, rounded to nearest double.
Does not fully support the IEEE special numbers NaN and INF. These values are encoded in the high-order double value only. The low-order value is not significant, but the low-order value of an infinity must be positive or negative zero.
IBM AIX documentation
A 128-bit long double number consists of an ordered pair of 64-bit double-precision numbers. The first member of the ordered pair contains the high-order part of the number, and the second member contains the low-order part. The value of the long double quantity is the sum of the two 64-bit numbers.
Each of the two 64-bit numbers is itself a double-precision floating-point number with a sign, exponent, and significand. The low-order member has a magnitude that is less than 1 unit in the last place of the high part, so the values of the two 64-bit numbers do not overlap and the entire significand of the low-order number adds precision beyond the high-order number.
|
|
||
| Operations on `f128ppc` assume that the value is normalized: the high component is assumed to be at least as large was the low component. Hence whether a value is positive or negative can only look at the sign bit of the high component. | ||
|
|
||
| When creating a `f128ppc` value from bytes (using `transmute`, `from_le_bytes`, etc) this invariant can be broken. That can cause nonsensical results, but cannot cause UB. |
There was a problem hiding this comment.
| When creating a `f128ppc` value from bytes (using `transmute`, `from_le_bytes`, etc) this invariant can be broken. That can cause nonsensical results, but cannot cause UB. | |
| When creating a `f128ppc` value from bytes (using `transmute`, `from_le_bytes`, etc) this invariant can be broken. That can cause nonsensical results, but cannot cause UB in Rust: Rust code must never rely on this invariant for safety. | |
| However, C code may rely on this invariant, so care must be taken when passing Rust-produced `f128ppc` values to C code. |
This does sound like a major footgun...
There was a problem hiding this comment.
Having given this some thought, I think ideally we probably want to match C here (even if it would create safety invariants) so that callers of C APIs (one of the main use cases of this type) don't have to check a f128ppc is valid before every call, however I'm not 100% sure whether it's intended to be UB in C or not. Clang/LLVM will currently optimise __ibm128 ops assuming that the value is normalised (making it UB for it not to be), however I'm not sure if that's intentional or a bug caused by generic float optimisations that were written only thinking about standard IEEE floats - I haven't found anything in the LLVM LangRef saying one way or the other. However, the PowerPC ABI specifications use the word "must" when describing the layout of the IBM long double, which does suggest it is in fact a safety requirement, just a poorly documented one.
There was a problem hiding this comment.
So something like this?
impl f128ppc {
pub unsafe fn from_components(large: f64, small: f64) -> Self {
core::mem::transmute([large, small])
}
fn is_normal_form(large: f64, small: f64) {
if large.is_nan() {
true
} else if large.is_infinite() {
// This branch may need to be skipped on elfv1 and aix.
small == 0.0
} else {
large + small == large
}
}
pub fn checked_from_components(large: f64, small: f64) -> Option<Self> {
if Self::is_normal_form(large, small) {
Some(unsafe { Self::from_components(large, small) })
} else {
None
}
}
}cc the IBM target maintainers @daltenty @amy-kwan @lei137, can you add the safety requirements to the LLVM langref?
There was a problem hiding this comment.
Also on the LLVM side it would be good to know if this is a precondition for all float operations, or an assumption on all values of the type. If I just store such a value to memory and look at the bits, will LLVM assume this invariant?
There was a problem hiding this comment.
So something like this?
Yes. Another possibility is (instead of/as well as):
pub fn normalize_from_components(x: f64, y: f64) -> Self {
if Self::is_normal_form(x, y) {
unsafe { Self::from_components(x, y) }
} else {
let large = x + y;
if !large.is_finite() {
return unsafe { Self::from_components(large, 0.0) };
}
// Per https://doi.org/10.1145/3121432, Algorithm 2
let x1 = large - y;
let y1 = large - x1;
let x2 = x - x1;
let y2 = y - y1;
let small = x2 + y2;
debug_assert!(Self::is_normal_form(large, small));
unsafe { Self::from_components(large, small) }
}
}There was a problem hiding this comment.
I've also found https://github.com/bminor/glibc/blob/master/sysdeps/ieee754/ldbl-128ibm/s_iscanonicall.c, which is a bit different (e.g. does not distinguish elfv2 from the rest).
Overall I'm having some trouble with what (1.0, inf) should normalize to. The implementation I posted earlier says (1.0, 0.0) but that seems wrong? But then what about (inf, -inf), that would return NaN when the components are just added.
There was a problem hiding this comment.
I suspect the ELFv1 specification saying that "The low-order value is not significant." for infinity is a specification bug as it conflicts with "The value of the extended-precision number is the sum of the two double-precision values." for (inf, -inf) as you've noticed.
Generally I'd interpret normalizing as adding the two components together and giving the result as a f128ppc (which is always an exact operation, and will always result in the same value modulo NaN bits(/zero signs?) etc. for numbers which are already normalized), so (1.0, inf) would normalize to (inf, 0.0) and (inf, -inf) would normalize to (nan, 0.0).
| # Motivation | ||
| [motivation]: #motivation | ||
|
|
||
| A systems programming language should be able to express any signature that C can express. Rust already provides many ABI-compatible types in `core::ffi::*`, and the recent stabilization of c-variadic functions and the `VaList` type is another step in this direction. But, holes remain, and they should be plugged. |
There was a problem hiding this comment.
Is there a more concrete argument than this? Like, some actual API that needs these types where someone may want to use Rust?
Personally I agree that it is desirable for Rust to be able to express any signature C can express, but there's a limit for how far I'd be willing to go to achieve that property.
There was a problem hiding this comment.
one API I can think of is when implementing a libc in Rust, you need to be able to use long double in printf, strtold, and a bunch of other functions that C expects to work.
There was a problem hiding this comment.
e.g. https://github.com/sunfishcode/c-ward which uses https://github.com/lights0123/printf-compat which can't implement printf for long double since that has no corresponding rust type.
Co-authored-by: Juniper Tyree <50025784+juntyr@users.noreply.github.com>
|
While on lang we review the RFC, I'm happy to champion a lang experiment for this, @folkertdev, if you want to create the tracking issue. |
Co-authored-by: kennytm <kennytm@gmail.com>
Has stdarch started omitting the |
It is omitted for RISC-V and LoongArch intrinsics and types. See also this thread and onwards when this came up for LoongArch intrinsics. Since the PowerPC vector types and intrinsics don't have an |
View all comments
Add
core::ffi::c_longdouble, to match Clong double, and its prerequisites.This type is highly platform-specific, A
long doublecan be:f32, e.g. on AVRf64, e.g. on many 32-bit targets and some 64-bit target using MUSLf128, e.g. on aarch64 and riscvSo far so good, but on some platforms this type corresponds to a type that Rust cannot currently express:
x86andx86_64, an IEEE (ish) 80-bit floating point typepowerpcandpowerpc64, a "double double"This RFC proposes the addition of these two types, with extremely minimal APIs, so that a portable
core::ffi::c_longdoublecan be defined.Thanks to @tgross35, @beetrees and @Gelbpunkt for early review on this RFC.
Tracking issue:
c_longdoublerust#161787Important
Since RFCs involve many conversations at once that can be difficult to follow, please use review comment threads on the text changes instead of direct comments on the RFC.
If you don't have a particular section of the RFC to comment on, you can click on the "Comment on this file" button on the top-right corner of the diff, to the right of the "Viewed" checkbox. This will create a separate thread even if others have commented on the file too.
Rendered