rfcs icon indicating copy to clipboard operation
rfcs copied to clipboard

Complex numbers

Open SciMind2460 opened this issue 4 weeks ago • 17 comments

This RFC proposes FFI-compatible complex numbers to help scientific computing library authors use non-indirected complexes.

I apologise in advance to num-complex

Rendered

SciMind2460 avatar Dec 02 '25 10:12 SciMind2460

Labeling this T-lang because the desire to make this FFI-compatible is a lang matter.

joshtriplett avatar Dec 02 '25 18:12 joshtriplett

It's worth pointing out another big issue with this is that the canonical a+bi is not actually the best representation of complex numbers in all cases, and so deciding on this is making a decision that might make life harder for external complex-numeric libraries out there.

In particular, while a+bi (orthogonal) representation is efficient for addition, r*(iθ).exp() is more efficient for multiplication, and depending on the equation you're using, it may be advantageous to switch between the two to reduce the number of arithmetic operations needed.

I'm not super compelled by the argument that C supports this, therefore the standard library needs to support this. I think that guaranteeing a std::ffi::Complex representation would be desirable, but there's no saying that we need to make this a canonical type in, say, std::num.

clarfonthey avatar Dec 02 '25 19:12 clarfonthey

It's worth pointing out another big issue with this is that the canonical a+bi is not actually the best representation of complex numbers in all cases, and so deciding on this is making a decision that might make life harder for external complex-numeric libraries out there.

In particular, while a+bi (orthogonal) representation is efficient for addition, r*(iθ).exp() is more efficient for multiplication, and depending on the equation you're using, it may be advantageous to switch between the two to reduce the number of arithmetic operations needed.

I think that polar form almost always is the more optimal form, at least in my experience. But the ABIs do use rectangular, e.g. from x86:

Arguments of complex T where T is one of the types float or double are treated as if they are implemented as:

struct complexT {
  T real;
  T imag;
};

so it makes sense that an interchange type matches that, and users can translate to/from a polar repr at the FFI boundary if needed. But this reasoning is definitely something to have in the RFC's rationale & alternatives.

tgross35 avatar Dec 02 '25 20:12 tgross35

Right: I guess my main clarification here was that due to the polar-orthogonal discrepancy, it shouldn't be a canonical Rust type (e.g. std::num::Complex shouldn't be making a decision on which is more-canonical), but I do think that having extra FFI-compatibility types is reasonable and this shouldn't prevent us from adding std::ffi::Complex which is orthogonal.

clarfonthey avatar Dec 02 '25 20:12 clarfonthey

Thanks everyone for the feedback! I have incorporated as much as I can into the RFC. @clarfonthey I do think that the orthogonal representation is more "canonical", especially as it is the most commonly used one in crates.io and across languages. So I'm not sure if we can consider this an issue, especially as there are polar conversion methods in the RFC.

SciMind2460 avatar Dec 03 '25 09:12 SciMind2460

@clarfonthey one of the other reasons I don't agree with polar representation is that it makes it very hard to define complex integers (Gaussian integers) which I included as a future possibility and which are useful for dealing with discrete geometry.

SciMind2460 avatar Dec 04 '25 10:12 SciMind2460

Right: I guess my main clarification here was that due to the polar-orthogonal discrepancy, it shouldn't be a canonical Rust type (e.g. std::num::Complex shouldn't be making a decision on which is more-canonical), but I do think that having extra FF-compatibility types is reasonable and this shouldn't prevent us from adding std::ffi::Complex which is orthogonal.

But std::num::Complex needs to have some in-memory representation in any case, and there isn't a way to make a global toggle that says "in my app, the most natural representation is polar". So the stdlib needs to pick one or another.

What the stdlib could do is to make this representation private, and reserve the right to change it without it being considered a breaking change. But why would this change ever happen? If changing the repr of this type realistically won't happen, it's useful to make this repr a public guarantee (not only it aids FFI, it also aids unsafe code, such as people writing inline asm to manipulate such a type without the need to pass through a conversion step)

dlight avatar Dec 04 '25 16:12 dlight

The conversion between a polar and orthogonal form isn't lossless, so, it effectively can't be done "automatically" or "as an implementation detail." You need trigonometric functions to do it, and while the conversion is algebraically closed, it's certainly more complicated to do exactly and most people prefer to just use floats instead.

My point here isn't that we need to decide; the issue is that the decision itself means that we shouldn't decide, and instead avoid having a standard Complex type for the standard library.

This doesn't preclude adding std::ffi::Complex which allows ABI-compatibility with C's _Complex, however, I don't think that such a type should be made standard for the language because of the fact that there are so many different ways to go about it.

clarfonthey avatar Dec 04 '25 16:12 clarfonthey

@clarfonthey IMO if you consider the fact that complex integers are only really supported orthogonally, then it becomes clear that there is only really one choice. Though polar representations are more accurate, they simply wouldn't work with complex integers (Gaussian integers) which I proposed as a future possibility. Rust has to decide in favor of the orthogonal representation if we choose to support Gaussian integers.

On Fri, 5 Dec, 2025, 00:37 Clar Fon, @.***> wrote:

clarfonthey left a comment (rust-lang/rfcs#3892) https://github.com/rust-lang/rfcs/pull/3892#issuecomment-3613134755

The conversion between a polar and orthogonal form isn't lossless, so, it effectively can't be done "automatically" or "as an implementation detail." You need trigonometric functions to do it, and while the conversion is algebraically closed, it's certainly more complicated to do exactly and most people prefer to just use floats instead.

My point here isn't that we need to decide; the issue is that the decision itself means that we shouldn't decide, and instead avoid having a standard Complex type for the standard library.

This doesn't preclude adding std::ffi::Complex which allows ABI-compatibility with C's _Complex, however, I don't think that such a type should be made standard for the language because of the fact that there are so many different ways to go about it.

— Reply to this email directly, view it on GitHub https://github.com/rust-lang/rfcs/pull/3892#issuecomment-3613134755, or unsubscribe https://github.com/notifications/unsubscribe-auth/BLL7PYEKKLORP5MEG4D3QK34ABPK3AVCNFSM6AAAAACNY66HSWVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZTMMJTGEZTINZVGU . You are receiving this because you authored the thread.Message ID: @.***>

SciMind2460 avatar Dec 04 '25 22:12 SciMind2460

Rust doesn't have to support them, though. Gaussian integers, however useful, are not a primitive that the language needs to offer to everyone and maintain.

clarfonthey avatar Dec 05 '25 00:12 clarfonthey

I guess my main clarification here was that due to the polar-orthogonal discrepancy

[...]

My point here isn't that we need to decide; the issue is that the decision itself means that we shouldn't decide, and instead avoid having a standard Complex type for the standard library.

As binary floating point cannot represent $\pi$ exactly, you can't even accurately describe $i\;(= 1\angle\tfrac\pi2)$ in polar form. In terms of representation it can work if the angle unit uses turns (multiples of $2\pi$) rather than radians. Even so, addition and subtraction under polar form is extremely complicated ($\left(r_1\angle\theta_1\right) + \left(r_2\angle\theta_2\right) = \left(\sqrt{r_1^2+r_2^2+2r_1r_2\cos(\theta_1-\theta_2)}\right)\angle\left(\tan^{-1}\tfrac{r_1\sin\theta_1+r_2\sin\theta_2}{r_1\cos\theta_1+r_2\cos\theta_2}\right)$ ) which outweighs any slight advantages brought by multiplication ($(a_1 + b_1i)\times(a_2+b_2i) = (a_1a_2 - b_1b_2) + (a_1b_2 + a_2b_1)i$ isn't really that costly in comparison), so it does not make sense as default in terms of computation either.

So I don't see how this is a valid discrepancy in the first place, no sane library will only provide a Complex<T> type in polar form, it's going to be either always rectilinear, or having multiple convertible choices StandardComplex<T>, PolarComplex<T>, EinsensteinComplex<T> etc.

If we are going to have a std::num::Complex<T>, in additional to the above reasoning, because of easy interoperability with C, C++, etc the rectilinear form is basically the only choice. That is, the "polar form" question is a total distraction, the only decision we need to make is have it, or not.

This doesn't preclude adding std::ffi::Complex which allows ABI-compatibility with C's _Complex, however, I don't think that such a type should be made standard for the language because of the fact that there are so many different ways to go about it.

Providing only core::ffi::Complex but without any associated functions is like providing core::ffi::VaList type without the arg() method, or like providing core::arch::x86::__m128 type without all the SSE _mm_* functions.

So IMO we either:

  1. Provide a Complex<T> type which is FFI-compatible with C's _Complex T at least for T = f32, f64, and exposes all methods available in C, or
  2. Declare that [T; 2] is FFI-compatible with _Complex T and be done with it. I think it is correct for Clang and GCC, but seems not the case for MSVC.

Though polar representations are more accurate, they simply wouldn't work with complex integers (Gaussian integers) which I proposed as a future possibility.

Note that Gaussian integers are not the only type of complex integers.

kennytm avatar Dec 05 '25 06:12 kennytm

  1. Provide a Complex<T> type which is FFI-compatible with C's _Complex T at least for T = f32, f64, and exposes all methods available in C, or
  2. Declare that [T; 2] is FFI-compatible with _Complex T and be done with it. I think it is correct for Clang and GCC, but seems not the case for MSVC.

Deciding the in-memory representation is the easy part, the hard part is how exactly complex numbers are passed by value in function arguments and return values -- I recall reading ABI specs that treat complex numbers specially such that they don't really match the ABI of any other single type (so it has to be handled specially by rustc and can't just be an existing type), though I can't currently recall which ABI specs.

programmerjake avatar Dec 05 '25 07:12 programmerjake

@programmerjake you're right - the calling convention of complex numbers is really not defined very well, which is why in my opinion No. 2 seems untenable. Sometimes complex numbers are passed as a struct, sometimes SSE registers - it really is untenable for anything other than STD to match the calling convention. (I am not an AI - I use dashes to denote pauses very frequently.)

SciMind2460 avatar Dec 05 '25 08:12 SciMind2460

No, what I was intending to say was that the fact that complex numbers would be introduced would make some libraries use the std complexes immediately whereas others would still use num-complex. I'll add implementation complexity though.

On Sun, 7 Dec, 2025, 06:54 nora, @.***> wrote:

@.**** commented on this pull request.

In text/3892-complex-numbers.md https://github.com/rust-lang/rfcs/pull/3892#discussion_r2595642450:

+impl Div for Complex { // calls to __divsc3 will be required here for implementation details and corresponding real types will also be implemented

  • type Output = Self;
  • fn div(self, other: Self) -> Self::Output; +} +impl Div for Complex { // calls to __divdc3 will be required here for implementation details and corresponding real types will also be implemented
  • type Output = Self;
  • fn div(self, other: Self) -> Self::Output; +} +``` +The floating point numbers shall have sine and cosine and tangent functions, their inverses, their hyperbolic variants, and their inverses defined as per the C standard and with Infinity and Nan values defined as per the C standard. +## Drawbacks +[drawbacks]: #drawbacks

+If there is suddenly a standard-library Complex type, people may rush to include it in their current implementations, which would leave people behind if they didn't know about it. I really don't think this is a drawback though, since similar things have happened in Rust before: the inclusion of OnceCell in Rust, for example.

I don't really understand what exactly this means. Who would want to rush to include complex numbers in their code? And who would be be left behind? I think the biggest downside here is the increased complexity and API surface and implementation maintenance.

— Reply to this email directly, view it on GitHub https://github.com/rust-lang/rfcs/pull/3892#pullrequestreview-3548517940, or unsubscribe https://github.com/notifications/unsubscribe-auth/BLL7PYAVAPV6XHQUYQJ6OO34ANND5AVCNFSM6AAAAACNY66HSWVHI2DSMVQWIX3LMV43YUDVNRWFEZLROVSXG5CSMV3GSZLXHMZTKNBYGUYTOOJUGA . You are receiving this because you authored the thread.Message ID: @.***>

SciMind2460 avatar Dec 06 '25 23:12 SciMind2460

If the motivation for putting it in std is just for FFI, could the std one just be a struct with pub fields and no other API, and then have the nice complex type be provided by a crate which is a #[repr(transparent)] wrapper around that?

ComputerDruid avatar Dec 11 '25 03:12 ComputerDruid

If the motivation for putting it in std is just for FFI, could the std one just be a struct with pub fields and no otger API, and then have the nice complex type be provided by a crate which is a #[repr(transparent)] wrapper around that?

that's technically possible, but I think we should support more than that -- it's not like it's some super weird type like PowerPC's 128-bit float that they used to use for long double that's really 2x f64 in disguise with some weird arithmetic.

programmerjake avatar Dec 11 '25 05:12 programmerjake

If the motivation for putting it in std is just for FFI, could the std one just be a struct with pub fields and no otger API, and then have the nice complex type be provided by a crate which is a #[repr(transparent)] wrapper around that?

Another problem that this RFC aims to resolve, which @miikkas pointed out, is to unify the API of complex numbers. Providing the std type as just a complex number with no methods would go against that.

SciMind2460 avatar Dec 11 '25 08:12 SciMind2460

I would like @cuviper (as the author of num-complex) to review the proposed API here.

Amanieu avatar Dec 16 '25 18:12 Amanieu