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

Inline const expressions and patterns #2920

Merged
merged 19 commits into from
Aug 27, 2020
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
342 changes: 342 additions & 0 deletions text/0000-inline-const.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,342 @@
- Feature Name: `inline_const`
- Start Date: 2020-04-30
- RFC PR: [rust-lang/rfcs#2920](https://github.com/rust-lang/rfcs/pull/2920)
- Rust Issue: TBD

# Summary
[summary]: #summary

Adds a new syntactical element called an "inline `const`", written as
`const { ... }`, which instructs the compiler to execute the contents of the
block at compile-time. An inline `const` can be used as an expression or
anywhere in a pattern where a named `const` would be allowed.

```rust
use std::net::Ipv6Addr;

fn mock_ip(use_localhost: bool) -> &'static Ipv6Addr {
if use_localhost {
&Ipv6Addr::LOCALHOST
} else {
const { &Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 0) }
}
}

Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
// Below is the same as
// fn main() {
// const HALF: usize = u32::MAX / 2 + 1;
// match *x {
// 0..HALF => println!("low"),
// HALF..=u32::MAX => println!("high"),
// }
// }

Add an example of how was it without inline const to let readers understand how it would look like without it. We could also make this block below the below block.

const MMIO_BIT1: u8 = 4;
const MMIO_BIT2: u8 = 5;

fn main() {
match read_mmio() {
0 => {}
const { 1 << MMIO_BIT1 } => println!("FOO"),
const { 1 << MMIO_BIT2 } => println!("BAR"),

_ => unreachable!(),
}
}
```

# Motivation
[motivation]: #motivation

Rust has `const` items, which are guaranteed to be initialized at compile-time.
Because of this, they can do things that normal variables cannot. For example,
a reference in a `const` initializer has the `'static` lifetime, and a `const`
can be used as an array initializer even if the type of the array is not
`Copy` (with [RFC 2203]).

[RFC 2203]: https://github.com/rust-lang/rfcs/pull/2203

```rust
fn foo(x: &i32) -> &i32 {
const ZERO: &'static i32 = &0;
if *x < 0 { ZERO } else { x }
}


fn foo() -> &u32 {
const RANGE: Range<i32> = 0..5; // `Range` is not `Copy`
let three_ranges = [RANGE; 3];
}
```

Writing out a `const` declaration every time we need a long-lived reference or
a non-`Copy` array initializer can be annoying. To improve the situation,
[RFC 1414] introduced rvalue static promotion to extend lifetimes, and
[RFC 2203] extended the concept of promotion to array initializers.
As a result, the previous example can be written more concisely.

[RFC 1414]: https://github.com/rust-lang/rfcs/pull/2203

```rust
fn foo(x: &i32) -> &i32 {
if *x < 0 { &0 } else { x }
}

fn foo() -> &u32 {
let three_ranges = [0..5; 3];
}
```

However, the fact that we are executing the array initializer or expression
after the `&` at compile-time is not obvious to the user. To avoid violating
their assumptions, we are very careful to promote only in cases where the user
cannot possibly tell that their code is not executing at runtime. This means a
[long list of rules][prom-rules] for determining the promotability of expressions, and it
means expressions that call a `const fn` or that result in a type with a `Drop`
impl need to use a named `const` declaration.

[prom-rules]: https://github.com/rust-lang/const-eval/blob/master/promotion.md#promotability

# Guide-level explanation
[guide-level-explanation]: #guide-level-explanation

This proposal is a middle ground, which is less verbose than named constants
but more obvious and expressive than promotion. In expression context, it
behaves much like the user had written the following, where `Ty` is the
inferred type of the code within the inline `const` expression (represented by
the ellipsis):

```rust
{ const UNIQUE_IDENT: Ty = ...; UNIQUE_IDENT }
```

With this extension to the language, users can ensure that their code executes
at compile-time without needing to declare a separate `const` item that is only
used once.

```rust
fn foo(x: &i32) -> &i32 {
if *x < 0 { const { &4i32.pow(4) } } else { x }
}

fn foo() -> &u32 {
let three_ranges = [const { (0..=5).into_inner() }; 3];
}
```

## Patterns

Patterns are another context that require a named `const` when using complex
expressions. Unlike in the expression context, where promotion is sometimes
applicable, there is no other choice here.

```rust
fn foo(x: i32) {
const CUBE: i32 = 3.pow(3);
match x {
CUBE => println!("three cubed"),
_ => {}
}
}
```

If that `const` is only used inside a single pattern, writing the code using an
inline `const` block makes it easier to scan.

```rust
fn foo(x: i32) {
match x {
const { 3.pow(3) } => println!("three cubed"),
_ => {}
}
}
```

# Reference-level explanation
[reference-level-explanation]: #reference-level-explanation

This RFC extends the [grammar for expressions] to be,

[grammar for expressions]: https://doc.rust-lang.org/stable/reference/expressions.html#expressions

> ```
> ExpressionWithBlock :
> OuterAttribute*†
> (
> BlockExpression
> | AsyncBlockExpression
> | UnsafeBlockExpression
> | ConstBlockExpression // new
> | LoopExpression
> | IfExpression
> | IfLetExpression
> | MatchExpression
> )
>
> ConstBlockExpression: `const` BlockExpression // new
> ```

This RFC extends the [grammar for patterns] to be,

[grammar for patterns]: https://doc.rust-lang.org/stable/reference/patterns.html

> ```
> Pattern :
> LiteralPattern
> | IdentifierPattern
> | WildcardPattern
> | RangePattern
> | ReferencePattern
> | StructPattern
> | TupleStructPattern
> | TuplePattern
> | GroupedPattern
> | SlicePattern
> | PathPattern
> | MacroInvocation
> | ConstBlockExpression // new
>
> RangePatternBound :
> CHAR_LITERAL
> | BYTE_LITERAL
> | -? INTEGER_LITERAL
> | -? FLOAT_LITERAL
> | PathInExpression
> | QualifiedPathInExpression
> | ConstBlockExpression // new
> ```

In both the expression and pattern context, an inline `const` behaves as if the
user had declared a uniquely named constant in the containing scope and
referenced it.

## Generic Parameters

For now, inline `const` expressions and patterns cannot refer to in-scope
generic parameters. As of this writing, the same restriction applies to array
length expressions, which seem like a good precedent for this RFC. As far as I
know, this is only a temporary restriction; the long-term goal is to allow
array length expressions to use generic parameters. When this happens, inline
`const` expressions and patterns will also be allowed to refer to in-scope
generics.

```rust
fn foo<T>() {
let x = [4i32; std::mem::size_of::<T>()]; // NOT ALLOWED (for now)
let x = const { std::mem::size_of::<T>() }; // NOT ALLOWED (for now)
}
```

## Containing `unsafe`

At present, containing `unsafe` blocks do not apply to array length expressions inside:

```rust
fn bar() {
let x = unsafe {
[4i32; std::intrinsics::unchecked_add(2i32, 3i32)] // ERROR
};
}
```

I find this somewhat strange, but consistency is important, so inline `const`
expressions should behave the same way. The following would also fail to
compile:

```rust
fn bar() {
let x = unsafe {
const { std::intrinsics::unchecked_add(2i32, 3i32) } // ERROR
};
}
```

If [#72359] is considered a bug and resolved, that change would also apply to
inline `const` expressions and patterns.

[#72359]: https://github.com/rust-lang/rust/issues/72359

# Drawbacks
[drawbacks]: #drawbacks
Copy link
Member

@RalfJung RalfJung May 5, 2020

Choose a reason for hiding this comment

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

Not sure if this is a drawback or a complication, but I presume this would also work in generic contexts -- so we would be basically introducing generic consts with this RFC. Well, we already have consts in generic contexts with associated consts, but they don't work quite as well as other consts in terms of linting etc -- we have monomorphization-time errors because only then can we actually evaluate the const. The same would likely happen here, right?

fn aptr::<T>() -> &'static *mut T {
  const { &std::ptr::NonNull::dangling().as_ptr() }
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, this is an excellent point. My initial thought was that we would indeed prevent inline constants from referring to generic parameters, just as constants currently do. If we allow this for inline constants, why not allow it for named constants as well? Are there backwards compatibility concerns?

Copy link
Contributor Author

@ecstatic-morse ecstatic-morse May 5, 2020

Choose a reason for hiding this comment

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

If we accept this RFC without allowing inline constants to refer to in-scope generic parameters, there will be a narrow class of implicitly promotable expressions (e.g. std::ptr::null::<T>(), T::CONST + T::ANOTHER_CONST) that couldn't be written in const blocks. I don't expect a ton of code is depending on the promotability of expressions such as these, but it makes it more difficult to justify the deprecation of implicit promotion for all fn calls and arithmetic expressions.

Copy link
Member

Choose a reason for hiding this comment

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

If we accept this RFC without allowing inline constants to refer to in-scope generic parameters, there will be a narrow class of implicitly promotable expressions [...]

Exactly, that's why my gut feeling is we should allow generics.
We have to figure out better how to lint wrong CTFE in potentially generic consts -- maybe we can eagerly evaluate at least those that do not actually depend on a generic parameter -- but that seems like a concern to be figured out during implementation.

If we allow this for inline constants, why not allow it for named constants as well? Are there backwards compatibility concerns?

For once, we'd need syntax, which is not the case for inline consts.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If we can backwards compatibly allow inline constants to refer to in-scope generic parameters in the future, I would like to separate that feature from the initial implementation. We would need both things before we considered deprecating the promotion of arithmetic expressions, but that doesn't mean they need to be spec'ed/implemented together.


This excludes other uses of the `const` keyword in expressions and patterns.
I'm not aware of any other proposals that would take advantage of this.

This would also be the first use of type inference for const initializers. Type
inference for named constants was proposed in [RFC 1349]. I don't believe the
blockers for this were technical, so I think this is possible.

[RFC 1349]: https://github.com/rust-lang/rfcs/issues/1349

# Rationale and alternatives
[rationale-and-alternatives]: #rationale-and-alternatives

The main alternative is the status quo. Maintaining it will likely result in
promotion being used for more contexts. The lang-team decided to [explore this
approach](https://github.com/rust-lang/rust/pull/70042#issuecomment-612221597)
instead.

It would also possible to separate out the parts of this RFC relating to patterns
so that they can be decided upon separately.

# Prior art
[prior-art]: #prior-art

Zig has the `comptime` keyword that [works similarly][zig] when it appears
before a block.

I'm not aware of equivalents in other languages.

AFAIK, this was [first proposed] by **@scottmcm**.

[zig]: https://kristoff.it/blog/what-is-zig-comptime/#compile-time-function-calls
[first proposed]: https://internals.rust-lang.org/t/quick-thought-const-blocks/7803/9

# Unresolved questions
[unresolved-questions]: #unresolved-questions

## Naming

I prefer the name inline `const`, since it signals that there is no difference
between a named `const` and an inline one.

**@scottmcm** prefers "`const` block", which is closer to the syntax and parallels
the current terminology of `async` block and `unsafe` block. It also avoids any
accidental conflation with the `#[inline]` attribute, which is unrelated.
Additionally, it doesn't extend nicely to the single-expression variant
discussed in [future possibilities].

**@RalfJung** prefers "anonymous `const`". **@scottmcm** mentioned in Zulip
that this could be confused with the `const _: () = ...;` syntax introduced in
[RFC 2526]. The reference refers to these as "unnamed" constants.

[RFC 2526]: https://github.com/rust-lang/rfcs/pull/2526

## Lints about placement of inline `const`

An inline `const` is eligible for promotion in an implicit context (just like a
named `const`), so the following are all guaranteed to work:

```rust
let x: &'static i32 = &const { 4i32.pow(4) };
let x: &'static i32 = const { &4i32.pow(4) };

// If RFC 2203 is stabilized
let v = [const { Vec::new() }; 3];
let v = const { [Vec::new(); 3] };
```

I don't have strong feelings about which version should be preferred.
**@RalfJung** points out that `&const { 4 + 2 }` is more readable than `const {
&(4 + 2) }`.

Note that it may be possible for RFC 2203 to use the explicit rules for
promotability when `T: !Copy`. In this case, the last part of the example above
could simply be written as `[Vec::new(); 3]`.

Inline `const`s are allowed within `const` and `static` initializers, just as we
currently allow nested `const` declarations. Whether to lint against inline
`const` expressions inside a `const` or `static` is also an open question.

# Future possibilities
[future possibilities]: #future-possibilities

It would be possible to allow the syntax `const expr` for an inline `const` that
consists of a single expression. This is analogous to the single expression
variant of closures: `|| 42`. This is backwards compatible with the current proposal.

At some point (an edition boundary?), we may want to narrow the scope of
expressions that are eligible for implicit promotion. Inline `const`
expressions would be the recommended replacement for expressions that were no
longer eligible.