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

allow constant evaluation of function calls #26848

Merged
merged 3 commits into from
Oct 27, 2015

Conversation

oli-obk
Copy link
Contributor

@oli-obk oli-obk commented Jul 7, 2015

this has the funky side-effect of also allowing constant evaluation of function calls to functions that are not const fn as long as check_const didn't mark that function NOT_CONST

It's still not possible to call a normal function from a const fn, but let statements' initialization value can get const evaluated (this caused the fallout in the overflowing tests)

we can now do this:

const fn add(x: usize, y: usize) -> usize { x + y }
const ARR: [i32; add(1, 2)] = [5, 6, 7];

also added a test for destructuring in const fn args

const fn i((a, b): (u32, u32)) -> u32 { a + b } //~ ERROR: E0022

This is a [breaking change], since it turns some runtime panics into compile-time errors. This statement is true for ANY improvement to the const evaluator.

@rust-highfive
Copy link
Collaborator

r? @pnkfelix

(rust_highfive has picked a reviewer for you, use r? to override)

@@ -271,6 +271,7 @@ pub enum ConstVal {
Bool(bool),
Struct(ast::NodeId),
Tuple(ast::NodeId),
Function(ast::DefId),
Copy link
Member

Choose a reason for hiding this comment

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

Why not evaluate const fn calls eagerly?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was just following the Struct/Tuple definition. And it made things easier with Expr::Call

Copy link
Member

Choose a reason for hiding this comment

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

Oh, this is a path to a function, not a call. Don't mind me!

@jdm
Copy link
Contributor

jdm commented Jul 7, 2015

🗽 This is pretty neat!

}
},
Some(def::DefFn(id, _)) => return Ok(Function(id)),
// FIXME: implement const methods?
Copy link
Member

Choose a reason for hiding this comment

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

This should at least check that the function is const fn - check_const runs long after type collection - and type collection is what triggers the evaluation of array length expressions.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

that is unfortunate... but for all practical purposes irrelevant, since the function is simply interpreted as const fn and if it hits a non-const value anywhere const_eval bails out anyway. I'd rather keep const eval for all functions, so regular statements calling non-const-fn-but-could-be-const-fn functions with const arguments can also be const evaluated

Copy link
Member

Choose a reason for hiding this comment

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

what does your PR do if it attempts to eval a call to a non const fn whose body is more complex than a single expression?

From my initial reading of the PR, it seems like it is assuming that it is always given a const fn and thus can assume that the block.expr.as_ref().unwrap() is all it needs to look at in the body, in particular it is not (I think) double-checking that the block has no statements...


Update: oh wait, let me double check, but now I'm thinking that all such functions should have already been marked as NOT_CONST from the check_const run.

Update 2: But wait, as eddyb already said, check_const runs long after type collection ... so it still seems like you could mistakenly think you are evaluating a non-const fn correctly but in fact should have flagged the fn as non-const on-the-fly...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yea, I misunderstood. I will force const-fn here. This should also un-break all the unit tests :D

@eddyb
Copy link
Member

eddyb commented Jul 7, 2015

Maybe it might be a good idea to introduce a const evaluation context that includes substitutions for type parameters and for function arguments, instead of passing these around manually.

@eefriedman
Copy link
Contributor

Is there any plan to fix rustc's usage of const_eval so it doesn't try to evaluate expressions that haven't gone through type-checking first? Fixing that would massively simplify const_eval, and make changes like this a lot easier to reason about. (See also #26683.)

For the tests, probably should just implement "id" as a store+load from a static mut variable, which is basically guaranteed to be impossible to const-evaluate.

@oli-obk
Copy link
Contributor Author

oli-obk commented Jul 7, 2015

Maybe it might be a good idea to introduce a const evaluation context that includes substitutions for type parameters and for function arguments, instead of passing these around manually.

yea, I had similar thoughts, but then got too lazy to introduce a PR before this one, but if preferred I can do that once @eefriedman gets his hint PR through

@bors
Copy link
Contributor

bors commented Jul 21, 2015

☔ The latest upstream changes (presumably #26935) made this pull request unmergeable. Please resolve the merge conflicts.

@bors
Copy link
Contributor

bors commented Jul 22, 2015

☔ The latest upstream changes (presumably #26683) made this pull request unmergeable. Please resolve the merge conflicts.

@Gankra
Copy link
Contributor

Gankra commented Jul 27, 2015

What's the status on this? Just needs review? Blocked on @eefriedman's PR?

@oli-obk oli-obk force-pushed the const_fn_const_eval branch 3 times, most recently from e41da4c to 2e750e6 Compare July 27, 2015 07:52
unsafety,
abi,
block,
) = match try!(eval_const_expr_partial(tcx, callee, UncheckedExprNoHint, fn_args)) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@eefriedman this eval_const_expr_partial gets me the DefId of the function I'm calling. I'm not sure if anything but UncheckedExprNoHint makes sense here.

@eefriedman
Copy link
Contributor

I'll look at this more carefully tomorrow, but the way hints work inside of eval_const_expr_partial is that if the argument is ExprTypeChecked, all recursive calls should generally also be ExprTypeChecked. This limits the use of weird hinting semantics to places where it's absolutely necessary.

@glaebhoerl
Copy link
Contributor

this has the funky side-effect of also allowing constant evaluation of function calls to functions that are not const fn as long as check_const didn't mark that function NOT_CONST

Am I understanding correctly that this doesn't magically turn every fn that accidentally happens to meet the requirements of a const fn into an actual const fn, i.e. a normal fn still can't be used where a compile-time constant is required, rather this is basically for early-evaluation as an optimization in cases where evaluation would normally happen at runtime? If that's right, it's not as scary as it might've been, though it still seems preferable to bail out with a warning if doing this runs into any problems (overflow, divergence, ...)?

@eefriedman
Copy link
Contributor

Looking over this more carefully, I'm pretty sure this approach is going to exacerbate the existing problems with overflow in constant evaluation in type-checking. You simply can't correctly evaluate a const fn correctly in general without knowing the declared argument and result types. For example:

const fn bmax() -> u8 { !0 }
const fn smax() -> u16 { !0 }
let c: [u8; bmax() as usize] = [0; 255];
let c: [u8; smax() as usize] = [0; 65535];

@oli-obk
Copy link
Contributor Author

oli-obk commented Jul 28, 2015

I will add that and similar functions with function arguments as test cases. For now I will raise an error in case the type is not yet in the ast_ty_to_ty map. I wonder how this plays with generics...

@bors
Copy link
Contributor

bors commented Jul 28, 2015

☔ The latest upstream changes (presumably #27234) made this pull request unmergeable. Please resolve the merge conflicts.

@pnkfelix
Copy link
Member

(I'm looking through this now, but this seems like this may require a bit more review and/or revision compared to e.g. #25570 which seemed pretty clean to me.)

cc @jroesch

@nikomatsakis
Copy link
Contributor

@oli-obk

This is a [breaking change], since it turns some runtime panics into compile-time errors. This statement is true for ANY improvement to the const evaluator.

can you elaborate on what you mean here? Is this specifically because you were treating arbitrary fns as const fns?

@pnkfelix
Copy link
Member

@oli-obk wrote:

This is a [breaking change], since it turns some runtime panics into compile-time errors. This statement is true for ANY improvement to the const evaluator.

@nikomatsakis asked:

can you elaborate on what you mean here? Is this specifically because you were treating arbitrary fns as const fns?

Niko and I then reviewed the situation further, and determined the following:

  1. The hypothesized property (that any improvement to the const evaluator is a breaking change) is not, in general, about the fact that this PR treated arbitrary fns as const fns.

  2. In particular, while the particular test cases where this PR observed the described phenomenon were cases of arbitrary fns, those test cases could (as originally written) very well have been written using const fn instead of fn, and this PR as written would have the same effect (of turning a runtime-panic into a compile-time error).
    To be concrete, we're talking about something like this (which is a small modification of an existing test).

    // error-pattern:thread '<main>' panicked at 'shift operation overflowed'
    // compile-flags: -C debug-assertions
    
    // (Work around constant-evaluation)
    const fn id<T>(x: T) -> T { x }
    
    fn main() {
        let _x = -1_i32 >> id(32);
    }

    before this PR, the above test would panic at runtime; after this PR, it would be a compile-time failure.

  3. Nonetheless, @nikomatsakis has convinced me that that this treatment of the above case is wrong. Instead, when encountering this scenario under -C debug-assertions, we should generate the code to panic at runtime. Also emitting a compile-time warning (i.e. a lint) in that scenario would be acceptable, but not an outright compile-time error.

If you follow that policy (of emitting code to panic at runtime -- which I guess you could emulate by just not const-evaluating the overflowing cases), then the hypothesis no longer holds: This would no longer be a breaking-change, and it would re-enforce a precedent that improvements to the const-evaluator should not be breaking changes.

@oli-obk
Copy link
Contributor Author

oli-obk commented Jul 30, 2015

Is this specifically because you were treating arbitrary fns as const fns?

yes, but it applies also to my other branch about constant evaluation of indexing operations or any further operations or constant values that are added (e.g. Ranges).

we should generate the code to panic at runtime. Also emitting a compile-time warning (i.e. a lint) in that scenario would be acceptable, but not an outright compile-time error.

I'll create an RFC to make this the standard procedure for compiler-improvements that detect an error at compile-time, but insert an unconditional runtime-panic to not be a breaking change.

emitting code to panic at runtime -- which I guess you could emulate by just not const-evaluating the overflowing cases

This would require 2 things

  1. let the const evaluator know whether it is an a true const environment (Array Length or a constant's value)
  2. All new operations (const call, const index, ...) need to poison the const-evaluator to turn a compiler-error into some const-eval-error that is caught wherever normal code was fed to the const-evaluator, emits a warning "unconditional panic inserted because {}" and inserts the panic. I would not actually call regular code-gen, but simply insert the panic. But this should be discussed in the RFC.

@oli-obk
Copy link
Contributor Author

oli-obk commented Jul 30, 2015

RFC: rust-lang/rfcs#1229

@steveklabnik
Copy link
Member

This RFC was merged, any chance the PR can get rebased?

@oli-obk
Copy link
Contributor Author

oli-obk commented Oct 2, 2015

I implemented the RFC, there'll be a PR for it soon, after that, I'll rebase this PR over the RFC-PR and add the appropriate regression tests.

@nikomatsakis
Copy link
Contributor

Great!

On Fri, Oct 2, 2015 at 5:08 AM, Oliver Schneider [email protected]
wrote:

I implemented the RFC, there'll be a PR for it soon, after that, I'll
rebase this PR over the RFC-PR and add the appropriate regression tests.


Reply to this email directly or view it on GitHub
#26848 (comment).

@oli-obk oli-obk force-pushed the const_fn_const_eval branch 2 times, most recently from 0c978cb to 084c171 Compare October 14, 2015 10:34
@oli-obk
Copy link
Contributor Author

oli-obk commented Oct 14, 2015

done and rebased

depends on PR #28845 (rfc 1229)

@bors
Copy link
Contributor

bors commented Oct 18, 2015

☔ The latest upstream changes (presumably #28845) made this pull request unmergeable. Please resolve the merge conflicts.

@oli-obk
Copy link
Contributor Author

oli-obk commented Oct 19, 2015

now that #28845 (RFC 1229) has landed, we can merge this without causing breaking changes

So we cannot assume that the function call was marked NOT_CONST by check_const.
},
_ => signal!(e, NonConstPath),
};
if let ExprTypeChecked = ty_hint {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@eddyb's comment is addressed here

Copy link
Member

Choose a reason for hiding this comment

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

thank you for pointing this out.

@pnkfelix
Copy link
Member

Despite my misgivings about the change to the run-pass/shift-near-oflo.rs test, I will approve this to get it into the bors queue (and out of my review queue).

I would nonetheless be interested in hearing @oli-obk 's opinion on whether that change to the test should stand, or if a followup PR should perhaps return it to its former state (or something similar to that).

@pnkfelix
Copy link
Member

@bors r+

@bors
Copy link
Contributor

bors commented Oct 27, 2015

📌 Commit 2b000fe has been approved by pnkfelix

bors added a commit that referenced this pull request Oct 27, 2015
this has the funky side-effect of also allowing constant evaluation of function calls to functions that are not `const fn` as long as `check_const` didn't mark that function `NOT_CONST`

It's still not possible to call a normal function from a `const fn`, but let statements' initialization value can get const evaluated (this caused the fallout in the overflowing tests)

we can now do this:

```rust
const fn add(x: usize, y: usize) -> usize { x + y }
const ARR: [i32; add(1, 2)] = [5, 6, 7];
```

also added a test for destructuring in const fn args
```rust
const fn i((a, b): (u32, u32)) -> u32 { a + b } //~ ERROR: E0022
```

This is a **[breaking change]**, since it turns some runtime panics into compile-time errors. This statement is true for ANY improvement to the const evaluator.
@bors
Copy link
Contributor

bors commented Oct 27, 2015

⌛ Testing commit 2b000fe with merge 540fd3a...

@bors bors merged commit 2b000fe into rust-lang:master Oct 27, 2015
@oli-obk oli-obk deleted the const_fn_const_eval branch October 28, 2015 08:19
} else {
// we don't know much about the function, so we force it to be a const fn
// so compilation will fail later in case the const fn's body is not const
assert_eq!(constness, hir::Constness::Const)
Copy link

Choose a reason for hiding this comment

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

I'm getting a ICE here with the current nightly on playpen:

thread 'rustc' panicked at 'assertion failed: `(left == right)` (left: `NotConst`, right: `Const`)', ../src/librustc/middle/const_eval.rs:106

in this simple test program:

fn f(x: usize) -> usize {
    x
}

const Y: usize = f(2);

fn main() {
    let z = [0; X];
}

Curiously if you move the const Y... line to the top, then it at least doesn't ICE, although I'd expect to get E0015 as an error, rather than E0307 (you can then get the E0015 by removing the let z = ... line)

Copy link
Contributor

Choose a reason for hiding this comment

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

This isn't the right place to file a bug report; try https://github.com/rust-lang/rust/issues/new . (You can link to the PR from there if you think it's useful.)

Copy link

Choose a reason for hiding this comment

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

I've just been playing on my phone on playpen on the bus, and won't have a laptop for at least a week, so was just hoping to put this somewhere that someone would spot it, and they can triage it (otherwise I'll just forget)

Copy link
Contributor

Choose a reason for hiding this comment

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

Filed #29587

Copy link

Choose a reason for hiding this comment

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

Great, thanks

On Wed, 4 Nov 2015 at 20:46 eefriedman [email protected] wrote:

In src/librustc/middle/const_eval.rs
#26848 (comment):

  •                  },
    
  •                  _ => signal!(e, NonConstPath),
    
  •              }
    
  •          } else {
    
  •              signal!(e, NonConstPath)
    
  •          },
    
  •          _ => signal!(e, NonConstPath),
    
  •      };
    
  •      if let ExprTypeChecked = ty_hint {
    
  •          // no need to check for constness... either check_const
    
  •          // already forbids this or we const eval over whatever
    
  •          // we want
    
  •      } else {
    
  •          // we don't know much about the function, so we force it to be a const fn
    
  •          // so compilation will fail later in case the const fn's body is not const
    
  •          assert_eq!(constness, hir::Constness::Const)
    

Filed #29587 #29587


Reply to this email directly or view it on GitHub
https://github.com/rust-lang/rust/pull/26848/files#r43936838.

@Manishearth
Copy link
Member

Won't this introduce a backcompat hazard? Functions which are const but not marked as such may become non-const in the future, breaking libs which depend on this.

@oli-obk
Copy link
Contributor Author

oli-obk commented Nov 5, 2015

@Manishearth it's impossible to use non-const functions in a const environment

@Manishearth
Copy link
Member

I'm talking about

this has the funky side-effect of also allowing constant evaluation of function calls to functions that are not const fn as long as check_const didn't mark that function NOT_CONST

@oli-obk
Copy link
Contributor Author

oli-obk commented Nov 5, 2015

yes, normal code can be const evaluated. But in case the const evaluation fails, it falls back to normal code generation anyway. I had an RFC for that and implemented it a while back.

@oli-obk
Copy link
Contributor Author

oli-obk commented Nov 5, 2015

as you can see in #29587, there's actually an assertion preventing the use of non-const functions in a true const environment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.