-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Complex numbers #3892
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Complex numbers #3892
Changes from all commits
ba634bb
0f4d922
863794c
60465d0
66b4a8e
e6a51ea
f63d8e6
8033da5
d86a918
4add4e1
5aceb3e
94dece5
0cf423a
512a3be
ca8f9ba
23f1e67
c055e1a
e438f35
d2c671a
3911071
73f823f
e69a67f
d1e41cb
a1774ac
6851400
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,227 @@ | ||
| - Feature Name: complex-numbers | ||
| - Start Date: 2025-12-02 | ||
| - RFC PR: [rust-lang/rfcs#3892](https://github.com/rust-lang/rfcs/pull/3892) | ||
| - Rust Issue: [rust-lang/rust#0000](https://github.com/rust-lang/rust/issues/0000) | ||
|
|
||
| ## Summary | ||
| [summary]: #summary | ||
|
|
||
| FFI-compatible and calling-convention-compatible complex types are to be introduced into `core` to ensure synchronity with C primitives. | ||
|
|
||
| ## Motivation | ||
| [motivation]: #motivation | ||
|
|
||
| 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. | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it's like |
||
| In essence, this RFC makes code like this: | ||
SciMind2460 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| ```C | ||
| extern double _Complex computes_function(double _Complex x); | ||
| ``` | ||
| callable in Rust without indirection: | ||
| ```rust | ||
| extern "C" { | ||
| fn computes_function(x: Complex<f64>) -> Complex<f64>; | ||
| } | ||
| fn main() { | ||
| let returned_value = computes_function(Complex<f64>::new(3, 4)) | ||
SciMind2460 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
| ``` | ||
| using the standard library's FFI-compatible complex numbers. | ||
|
|
||
| ## Guide-level explanation | ||
| [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 ( includes all numbers). | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. removing "this" here seems like an accidental change |
||
| ```rust | ||
| let x = Complex::new(3.0, 4.0); | ||
| ``` | ||
| They can even be passed as an array: | ||
| ```rust | ||
| let y = Complex::from([3.0, 4.0]); | ||
| ``` | ||
| or as a tuple: | ||
| ```rust | ||
| let z = Complex::from((3.0, 4.0)); | ||
| ``` | ||
| They can even be passed in polar form (but only as a float): | ||
| ```rust | ||
| let polar = Complex::from_polar(3.0, f32::PI/2.0); | ||
| ``` | ||
| They are added and multiplied as complexes are: | ||
| ```rust | ||
| let first = Complex::new(1.0, 2.0); | ||
| let second = Complex::new(3.0, 4.0); | ||
| let added = first + second; // 4 + 6i | ||
| let multiplied = first * second; // -4 + 10i | ||
| ``` | ||
|
|
||
| They can be divided using normal floating-point division: | ||
| ```rust | ||
| let float_first = Complex::new(1.0, 2.0); | ||
| let float_second = Complex::new(3.0, 4.0); | ||
| let divided = float_second / float_first; // 2.4 - 0.2i | ||
| ``` | ||
|
|
||
| You can even calculate the complex sine, cosine and more: | ||
| ```rust | ||
| let val = Complex::new(3.0, 4.0); | ||
| let sine_cmplx = csin(val); // 3.8537380379 - 27.016813258i | ||
| ``` | ||
| If you want to call certain C libraries with complex numbers, you use this type: | ||
| ```C | ||
| // in the C library | ||
| extern double _Complex computes_function(double _Complex x); | ||
| ``` | ||
| ```rust | ||
| // in YOUR Rust code | ||
| extern "C" { | ||
| fn computes_function(x: Complex<f64>) -> Complex<f64>; | ||
| } | ||
| fn main() { | ||
| let returned_value = computes_function(Complex::<f64>::new(3.0, 4.0)) | ||
| } | ||
| ``` | ||
|
|
||
| ## Reference-level explanation | ||
| [reference-level-explanation]: #reference-level-explanation | ||
|
|
||
| The `core` crate will provide implementations for operator traits for possible component types. For now, complex operations like `Mul` and `Div` are defined specifically for each floating-point type, while `Add` and `Sub` are defined for any components that themselves implement `Add` and `Sub`. | ||
| Calls to some `libgcc` functions may also be needed, and will be emitted by the backend via compiler-builtins, specifically `__mulsc3`, `__muldc3`, `__divsc3` and `__divdc3` for the proper and complete implementation of these types. | ||
| They will have an internal representation similar to this (with public fields for real and imaginary parts): | ||
| ```rust | ||
| // in core::complex | ||
| #[lang = "complex"] | ||
| #[repr(C)] | ||
| #[derive(Copy, Clone, PartialEq, Debug)] | ||
| pub struct Complex<T> {pub re: T, pub im: T}; | ||
| ``` | ||
| have construction methods and `From` impls: | ||
| ```rust | ||
| impl Complex<T> { | ||
| fn new(re: T, im: T) -> Self; | ||
| } | ||
|
|
||
| impl<T> From<(T, T)> for Complex<T> { | ||
| fn from(value: (T, T)) -> Self; | ||
| } | ||
| impl<T> From<[T; 2]> for Complex<T> { | ||
| fn from(value: [T; 2]) -> Self; | ||
| } | ||
| ``` | ||
| and have arithmetic implementations similar to this: | ||
| ```rust | ||
| impl<T: Add> Add for Complex<T> { // and for corresponding real types | ||
| type Output = Self; | ||
|
|
||
| fn add(self, other: Self) -> Self::Output; | ||
| } | ||
|
|
||
| impl<T: Sub> Sub for Complex<T> { // and for corresponding real types | ||
| type Output = Self; | ||
|
|
||
| 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`: | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This will require By comparison,
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why would we have these in std but not core in the first place? |
||
| ```rust | ||
| impl Mul for Complex<f32> { // calls to __mulsc3 will be required here for implementation details and corresponding real types will also be implemented | ||
| type Output = Self; | ||
|
|
||
| fn mul(self, other: Self) -> Self::Output; | ||
| } | ||
| impl Mul for Complex<f64> { // calls to __muldc3 will be required here for implementation details and corresponding real types will also be implemented | ||
| type Output = Self; | ||
|
|
||
| fn mul(self, other: Self) -> Self::Output; | ||
| } | ||
| impl Div for Complex<f32> { // 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<f64> { // 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. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These will all forward from
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) |
||
| ## Drawbacks | ||
| [drawbacks]: #drawbacks | ||
SciMind2460 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| The implementation surface of the complex types means more items for the libs and lang teams to maintain. | ||
| Also, the multiple emitted calls to `libgcc.so` (`__mulsc3` and the like) may cause a bit of overhead and may not be what the Rust lang team and compiler team want. | ||
|
|
||
| ## Rationale and alternatives | ||
| [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. | ||
SciMind2460 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You're mistaken about https://docs.rs/nalgebra/latest/src/nalgebra/lib.rs.html#169 https://github.com/dimforge/nalgebra/blob/96c5d876f49afbcb0c22748b4f548b654cfef4fe/src/lib.rs#L169
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
|
|
||
| You could theoretically do something like this: | ||
| ```c | ||
| double _Complex function(double _Complex value); | ||
| void wrapper_function(double _Complex* value, double _Complex* out) { | ||
| *out = function(*value); | ||
| } | ||
| ``` | ||
| for all functions you wish for. But this still needs to happen in C. | ||
| ### Alternatives: | ||
| - Don't do this: There are, obviously, millions of alternatives on crates.io, the foremost being `num-complex`. However, I believe that if we wish to support proper FFI with C, then a standard type that matches calling conventions with C complex numbers is an important feature of the language. Hence, I do not recommend this idea. | ||
| - Use a polar layout: Polar complex numbers are undoubtedly a more optimal solution for multiplying complexes. However, I believe that if we wish to have proper FFI with C, then complex number layout should be chosen in accordance with the layout that is used in the C standard, and that is the orthogonal layout. This is also the layout used by most other languages and other crates on crates.io. | ||
| - Non-generic primitive types: These are, obviously, the most obvious and practical solution. However, if we implemented lots of such types, then we would not be able to expand for `f16` and `f128` support without repeating the code already implemented. It would be extremely repetitive and tedious to document new types and their behavior, even if we used macros to generate implementations | ||
| ## Prior art | ||
| [prior-art]: #prior-art | ||
| FORTRAN, C, C++, Go, Perl and Python all have complex types implemented in the standard library or as a primitive. This clearly appears to be an important feature many languages have. | ||
SciMind2460 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| For example, in Python: | ||
| ```py | ||
| complex_num = 1 + 2j | ||
| complex_second = 3 + 4j | ||
| print(complex_num * complex_second) | ||
| ``` | ||
| or in C: | ||
| ```c | ||
| float _Complex cmplx = 1 + 2*I; | ||
| float _Complex two_cmplx = 3 + 4*I; | ||
| printf("%.1f%+.1fi\n", creal(cmplx * two_cmplx), cimag(cmplx * two_cmplx)); | ||
| ``` | ||
| Even in Rust, it has been discussed two times in IRLO: | ||
| - [First discussion](https://internals.rust-lang.org/t/c-compatible-complex-types-using-traits/13757) | ||
| - [Second discussion](https://internals.rust-lang.org/t/standard-complex-number-in-std-library/23748) | ||
| Many crates, like `num-complex` also provide this feature, though it is not FFI-safe. | ||
| ## Unresolved questions | ||
| [unresolved-questions]: #unresolved-questions | ||
| Should this type be in `core::ffi`? This type's purpose is mostly FFI, but it might be useful in library contexts as well, so I am not sure if we should place it in `core::ffi`. | ||
| ## Future possibilities | ||
| [future-possibilities]: #future-possibilities | ||
| - Maybe later on, we can think of adding a special custom suffix for complex numbers (`1+2j` for example), and using that as a simpler way of writing complex numbers if this RFC is accepted? This is very similar to how most languages implement complex numbers? Or perhaps we could consider a constant: | ||
| ```rust | ||
| impl<T: Float> Complex<T: Float> { | ||
| const I: T = Complex::new(T::zero(), T::one()); | ||
| } | ||
| ``` | ||
| where `zero` and `one` is implemented on the `Float` trait similar to `num_traits`? | ||
| Or maybe we could have a method on normal numbers: | ||
| ```rust | ||
| // for example | ||
| impl f32 { | ||
| fn i(self) -> Complex<f32> { | ||
| Complex::new(0, self) | ||
| } | ||
| } | ||
| ``` | ||
| 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. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. FWIW, a recent issue raised that Although as I commented there, it can be seen similarly to the
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
| - We can also support f16 and f128 once methods for them are stabilised. | ||
| - We should also think about a `Display` implementation. Should we support something like `1 + 2i` or something else? Should we not make a `Display` impl at all, and just use re() and im() for the implementation? | ||
| - We should also consider adding aliases (like c32 and c64) for floating points once they are established, to allow for a shorthand syntax. | ||
| - Eventually, we should also consider adding polar conversions (e.g, `modulus` and `angle`) | ||
Uh oh!
There was an error while loading. Please reload this page.