Skip to content

Conversation

@SciMind2460
Copy link

@SciMind2460 SciMind2460 commented Dec 2, 2025

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

@ehuss ehuss added the T-libs-api Relevant to the library API team, which will review and decide on the RFC. label Dec 2, 2025
@joshtriplett joshtriplett added the T-lang Relevant to the language team, which will review and decide on the RFC. label Dec 2, 2025
@joshtriplett
Copy link
Member

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

@joshtriplett joshtriplett added the I-libs-api-nominated Indicates that an issue has been nominated for prioritizing at the next libs-api team meeting. label Dec 2, 2025
@clarfonthey
Copy link

clarfonthey commented Dec 2, 2025

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.

@tgross35
Copy link
Contributor

tgross35 commented Dec 2, 2025

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.

@clarfonthey
Copy link

clarfonthey commented Dec 2, 2025

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.


The definition of complex numbers in the C99 standard defines the _memory layout_ of a complex number but not its _calling convention_.
This makes crates like `num-complex` untenable for calling C FFI functions containing complex numbers without at least a level of indirection (`*const Complex`) or the like.
Only in `std` is it possible to make an additional repr to match the calling convention that C uses across FFI boundaries.
Copy link

@SOF3 SOF3 Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is FFI compatibility limited to std only? It's just like how we have all the types in crate@libc. What necessitates complex becominga type in something like crate@libc?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's like va_list (core::ffi::va_list::VaListImpl) can only be implemented in libcore.

@SciMind2460
Copy link
Author

SciMind2460 commented Dec 3, 2025

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.

Comment on lines 167 to 174
impl Complex<f64> {
fn angle(self) {
f32::atan2(self.re(), self.im())
}
fn from_polar(modulus: f32, angle: f32) -> Complex<f32> {
Complex::new(modulus * f32::cos(angle), modulus * f32::sin(angle))
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably want to return something here? And not use the f32 types for f64

That being said: I think polar conversions should be put into "future possibilities" since they aren't needed for basic support.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was trying to write something to help with @clarfonthey's polar proposal, but if they are fine with this becoming a future possibility, then OK!

Copy link
Member

@joshtriplett joshtriplett Dec 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed that they should be moved to "future possibilities" for now.

Clarified language regarding division and method descriptions for complex numbers. Expanded rationale for the complex number type and its implications for FFI.
```
that could help simplify the life of people who otherwise would have to keep writing `Complex::new()`?
- Should we support Imaginary eventually? This RFC doesn't cover it, but I think we can do this later in another RFC.
- Eventually we may support Gaussian integers (an extension of the real integers) which have a Euclidean division procedure with remainder. We could theoretically eventually support these integers?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll note GCC has integer complex numbers as a C extension, which I think Rust should eventually support FFI with.

@ComputerDruid
Copy link

ComputerDruid commented Dec 11, 2025

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?

@programmerjake
Copy link
Member

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.

@SciMind2460
Copy link
Author

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.

Clarify future support for Gaussian integers and floating point types in the RFC.
SciMind2460 and others added 4 commits December 16, 2025 08:20
Clarified the rationale for a unified API for complex numbers and addressed compatibility issues with C libraries. Updated examples and alternatives to improve understanding of complex number implementations.
Co-authored-by: Kevin Reid <[email protected]>
[guide-level-explanation]: #guide-level-explanation

`Complex<T>` numbers can be instantiated with any component type using `Complex::new(re, im)` where `re` and `im` are of the same type (this includes all numbers).
`Complex<T>` numbers can be instantiated with any component type using `Complex::new(re, im)` where `re` and `im` are of the same type ( includes all numbers).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removing "this" here seems like an accidental change

@Amanieu
Copy link
Member

Amanieu commented Dec 16, 2025

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

SciMind2460 and others added 2 commits December 17, 2025 09:20
Refactor Complex struct to have public fields and update methods for real and imaginary parts. Added notes on future considerations for polar conversions and arithmetic implementations.
[rationale-and-alternatives]: #rationale-and-alternatives

The rationale for this type is mostly FFI: C libraries that may be linked from Rust code currently cannot provide functions with direct struct implementations of Complex - they must be hidden under at least a layer of indirection. This is because of the undefined calling convention of complex numbers in C. For example: on powerpc64-linux-gnu, [returning double _Complex doesn't do the same thing as returning a struct with a field of type double[2].](https://gcc.godbolt.org/z/hh7zYcnK6) However, it is not always possible to write a C complex-valued function that wraps the first function in a pointer. Thus, FFI becomes a problem if such complex-valued functions are passed by value and not by reference.
Additionally, another issue this solves is to have a unified API for complex numbers. Right now, many crates are using their own implementation (`num-complex` could serve as a unifying factor, but other crates do not implement the same complex numbers, such as `nalgebra`, due to less care over incompatibility concerns), which makes it difficult to implement unifying interfaces without complicating the code with too many conversion functions. This serves a problem for crates that use different implementations of complex numbers for different tasks (`sprs` relying on `num-complex` and `nalgebra` implementing its own variation)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're mistaken about nalgebra, at least -- it re-exports num_complex::Complex in its own API so you don't have to depend on num-complex yourself, but it is still the same unified type.

https://docs.rs/nalgebra/latest/src/nalgebra/lib.rs.html#169

https://github.com/dimforge/nalgebra/blob/96c5d876f49afbcb0c22748b4f548b654cfef4fe/src/lib.rs#L169

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It appears I made a mistake, because these docs link back to nalgebra::Complex as its own struct, but it is actually there in the definition of num-complex. I'll rework the example.

Copy link
Member

@cuviper cuviper left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I apologise in advance to num-complex

No worries! There are definitely things that a compiler-integrated implementation can do better, especially with ABI as you've highlighted. A lot of the num-complex API is also limited by trying to be so generic, whereas the standard library can choose to keep all the implementation flexibility for itself.

fn sub(self, other: Self) -> Self::Output;
}
```
and then implementations of those functions which require external calls to C (`__mulsc3` etc.), which will be in `std` and not `core`:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will require #[rustc_allow_incoherent_impl] or something like it, as we do for std floating-point methods. However, I think it would be the first time we put that on a trait impl, let alone on lang-item traits such as Mul and Div. We'd better be sure the compiler is ready and willing to support that!

By comparison, num-complex implements them generically (Mul, Div) using straightforward mathematical formulas, but this doesn't take any special care for precision or overflow issues.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could do them genetically but IMO if we do it via LLVM we can guarantee that the implementation is the exact same. I do like the idea of doing it genetically though.

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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These will all forward from std to external libraries as well?
(the incoherent bit is ok for inherent methods though, just like we do for regular floats)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, they will be forwarded from Std and not core (if that's what you mean)

```
that could help simplify the life of people who otherwise would have to keep writing `Complex::new()`?
- Should we support Imaginary eventually? This RFC doesn't cover it, but I think we can do this later in another RFC.
- Eventually we may support Gaussian integers (an extension of the real integers) which have a Euclidean division procedure with remainder. GCC has these, and we could theoretically eventually support these integers alongside GCC FFI.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW, a recent issue raised that num-complex's remainder is not ideal for their purpose: rust-num/num#445

Although as I commented there, it can be seen similarly to the Rem vs. rem_euclid methods for integers in the standard library.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could do both a biggest check and a nearest check using different methods? Like you pointed out. It will be an issue to determine the canonical remainder though.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

I-libs-api-nominated Indicates that an issue has been nominated for prioritizing at the next libs-api team meeting. T-lang Relevant to the language team, which will review and decide on the RFC. T-libs-api Relevant to the library API team, which will review and decide on the RFC.

Projects

None yet

Development

Successfully merging this pull request may close these issues.