Skip to content
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

Make saturating_add and saturating_sub const functions #58030

Closed
oli-obk opened this issue Jan 31, 2019 · 7 comments
Closed

Make saturating_add and saturating_sub const functions #58030

oli-obk opened this issue Jan 31, 2019 · 7 comments
Labels
A-const-eval Area: constant evaluation (mir interpretation) E-mentor Call for participation: This issue has a mentor. Use #t-compiler/help on Zulip for discussion.

Comments

@oli-obk
Copy link
Contributor

oli-obk commented Jan 31, 2019

These used to require if to work, but are forwarded to llvm intrinsics nowadays (#58003). We can now implement them as const eval intrinsics, too.

Impl instructions:

  1. add to
    | "add_with_overflow" // ~> .overflowing_add
  2. remove unsafe for calls to the intrinsic
  3. add to
  4. make functions calling the intrinsic const fn
  5. Implement in
    | "overflowing_add"
    (implementation is basically the two read_immediate calls, a call to binary_op_imm with arguments similar to the other add/sub intrinsics, in case the overflow bool in the return value is false, write the actual value to the destination like in
    self.write_scalar(val, dest)
    . In case the overflow bool is true, compute the min or max value of the type like in
    let max = 1 << (size.bits() - 1);
    and write that value to dest.
  6. Write some tests that use the saturating methods in constants
@oli-obk oli-obk added E-mentor Call for participation: This issue has a mentor. Use #t-compiler/help on Zulip for discussion. A-const-eval Area: constant evaluation (mir interpretation) labels Jan 31, 2019
@pmccarter
Copy link
Contributor

I'd really like to do this!

@oli-obk
Copy link
Contributor Author

oli-obk commented Feb 1, 2019

It's all yours! If anything is unclear, don't hesitate to ask.

@pmccarter
Copy link
Contributor

Thanks! I have some questions, please let me know if I'm misunderstanding anything...

Is it correct that the point of this ticket is to avoid code generation (LLVM intrinsics) by evaluating during compilation (const eval intrinsics), where previously LLVM branching code was generated? Is there already compiler logic to detect non immediate arguments/terms so that the LLVM code is generated even after the functions are whitelisted as const?

I can't find any compiler code where calls to n.saturating_add(m) are wrapped in unsafe blocks, and why would they be? Also, are the implementation guidelines saying that all functions with calls to these two functions can be marked as const? Why would that be, irregardless of other statements in the function?

@nikic
Copy link
Contributor

nikic commented Feb 1, 2019

Is it correct that the point of this ticket is to avoid code generation (LLVM intrinsics) by evaluating during compilation (const eval intrinsics), where previously LLVM branching code was generated? Is there already compiler logic to detect non immediate arguments/terms so that the LLVM code is generated even after the functions are whitelisted as const?

Not quite. For normal code, the LLVM intrinsics will still be generated (at which point LLVM may evaluate them during optimization, but that's a different matter). Making the intrinsics const just means that they can be used in contexts where the result must be evaluated during compilation. It allows you to write something like const N = A.saturating_add(B);.

I can't find any compiler code where calls to n.saturating_add(m) are wrapped in unsafe blocks, and why would they be?

You need to separate two things here: The saturating_add methods on numeric types like u32, and the saturating_add intrinsic in core::intrinsics. By default all such intrinsics are unsafe, but for saturating_add in particular there is no reason for it to be. (I think this is already the case -- though maybe this has to be marked separate for const handling.)

Also, are the implementation guidelines saying that all functions with calls to these two functions can be marked as const? Why would that be, irregardless of other statements in the function?

This refers to the functions that call the intrinsics, which should just be the saturing_add/saturating_sub methods in core::num (such as https://github.com/rust-lang/rust/blob/master/src/libcore/num/mod.rs#L885). Those are the only functions that should be marked as const.

@RalfJung
Copy link
Member

RalfJung commented Feb 1, 2019

Is it correct that the point of this ticket is to avoid code generation (LLVM intrinsics) by evaluating during compilation (const eval intrinsics), where previously LLVM branching code was generated?

No. This is to make the CTFE engine in Rust support these intrinsics. When you write const FOO: T = bar; or static FOO: T = bar;, Rust has to evaluate bar at compile-time, that's called CTFE (compile-time function evaluation). And the point of this issue is to enable bar to use saturating_add, to make code like this work:

fn main() {
    const x: u8 = 13u8.saturating_sub(66);
}

CTFE is very different from const propagation, which is an optimization that opportunistically will try to evaluate code at compile-time. Const propagation is performed by LLVM, rustc does not have to do anything for it to happen.

@pmccarter
Copy link
Contributor

Thanks for the help guys.

Currently the num module saturating_add() wrapper only uses the intrinsic for stage 1+, so the function can't just be marked as const because of the non const checked_add() call in the stage 0 compiler. Looks like other intrinsic calls don't need the stage guards so not sure what the issue is. Does this need to wait for the bootstrapping compile version to advance or something?

Also not sure about the special // ~> comments...

@nikic
Copy link
Contributor

nikic commented Feb 4, 2019

@pmccarter I believe the way to handle this would be to move the cfg to cover the whole function. You'd have a stage0 variant that is non-const and also uses the old checked_add based implementation, and you'd have a not(stage0) variant that is const and uses the saturating_add intrinsic based implementation.

So basically instead of

pub fn saturating_add(self, rhs: Self) -> Self {
    #[cfg(stage0)]
    match self.checked_add(rhs) {
        Some(x) => x,
        None => Self::max_value(),
    }
    #[cfg(not(stage0))]
    {
        intrinsics::saturating_add(self, rhs)
    }
}

you'd have

#[cfg(stage0)]
pub fn saturating_add(self, rhs: Self) -> Self {
    match self.checked_add(rhs) {
        Some(x) => x,
        None => Self::max_value(),
    }
}

#[cfg(not(stage0))]
pub const fn saturating_add(self, rhs: Self) -> Self {
    intrinsics::saturating_add(self, rhs)
}

bors added a commit that referenced this issue Feb 11, 2019
Make `saturating_add` and `saturating_sub` `const` functions

Fixes #58030
bors added a commit that referenced this issue Feb 12, 2019
Make `saturating_add` and `saturating_sub` `const` functions

Fixes #58030
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-const-eval Area: constant evaluation (mir interpretation) E-mentor Call for participation: This issue has a mentor. Use #t-compiler/help on Zulip for discussion.
Projects
None yet
Development

No branches or pull requests

4 participants