Summary

Allow

許可する、可能にする
for local variables,
変数、ストレージ
function arguments,
引数
and some expressions
to have an unsized type, and implement
実装する
it by storing the temporaries
一時的な
in variably-sized allocas.

Have repeat expressions

with a length that captures local variables
変数、ストレージ
be such an expression,
returning an [T] slice.

Provide some optimization guarantees

保証する
that unnecessary temporaries
一時的な
will not create unnecessary allocas.

Motivation

There are 2 motivations for this RFC:

  1. Passing unsized values, such as trait objects, to functions by value is often desired. Currently, this must be done through a Box<T> with an unnecessary allocation.
    割当

One particularly common example is passing closures that consume their environment

環境
without using monomorphization. One would like for this code to work:

fn takes_closure(f: FnOnce()) { f(); }

But today you have to use a hack, such as taking

とる
a Box<FnBox<()>>.

  1. Allocating
    確保する
    a runtime-sized variable
    変数、ストレージ
    on the stack is important for good performance in some use-cases - see RFC #1808, which this is intended to supersede.

Detailed design
設計(する)

Unsized Rvalues - language
言語

Remove the rule that requires all locals and rvalues to have a sized type. Instead, require the following:

  1. The following expressions
    must always return a Sized type:
    1. Function calls,
      呼び出し
      method calls,
      呼び出し
      operator
      演算子
      expressions
      • implementing
        実装する
        unsized return values for function calls
        呼び出し
        would require the called
        呼び出し
        function
        to do the alloca in our stack frame.
    2. ADT expressions
      • see alternatives
    3. cast expressions
      • this seems like an implementation
        実装
        simplicity
        単純さ、簡単さ
        thing. These can only be trivial casts.
  2. The RHS of assignment
    代入
    expressions
    must always have a Sized type.
    • Assigning
      代入する
      an unsized type is impossible because we don't know how much memory is available at the destination.
      行き先、目的地
      This applies
      適用する
      to ExprAssign assignments
      代入
      and not to StmtLet let-statements.

This also allows

許可する、可能にする
passing unsized values to functions, with the ABI being as if a &move pointer was passed (a (by-move-data, extra) pair). This also means that methods taking
とる
self by value are object-safe, though vtable shims are sometimes needed to translate the ABI (as the callee-side intentionally does not pass extra to the fn in the vtable, no vtable shim is needed if the vtable function already takes
とる
its argument
引数
indirectly).

For example:

struct StringData { len: usize, data: [u8], } fn foo(s1: Box<StringData>, s2: Box<StringData>, cond: bool) { // this creates a VLA copy of either `s1.1` or `s2.1` on // the stack. let mut s = if cond { s1.data } else { s2.data }; drop(s1); drop(s2); foo(s); } fn example(f: for<'a> FnOnce(&'a X<'a>)) { let x = X::new(); f(x); // aka FnOnce::call_once(f, (x,)); }

VLA expressions

Allow

許可する、可能にする
repeat expressions
to capture variables
変数、ストレージ
from their surrounding environment.
環境
If a repeat expression
captures such a variable,
変数、ストレージ
it has type [T] with the length being evaluated
評価する(される)
at run-time.
実行時の(に)
If the repeat expression
does not capture any variable,
変数、ストレージ
the length is evaluated
評価する(される)
at compile-time. For example:

extern "C" { fn random() -> usize; } fn foo(n: usize) { let x = [0u8; n]; // x: [u8] let x = [0u8; n + (random() % 100)]; // x: [u8] let x = [0u8; 42]; // x: [u8; 42], like today let x = [0u8; random() % 100]; //~ ERROR constant evaluation error }

"captures a variable"

変数、ストレージ
- as in RFC #1558 - is used as the condition
条件
for making the return be [T] because it is simple, easy to understand, and introduces no type-checking complications.

The last error message could have a user-helpful note, for example "extract

抽出する
the length to a local variable
変数、ストレージ
if you want a variable-length array".
配列

Unsized Rvalues - MIR

The way this is implemented

実装する
in MIR is that operands,
被演算子
rvalues, and temporaries
一時的な
are allowed
許可する、可能にする
to be unsized. An unsized operand
被演算子
is always "by-ref". Unsized rvalues are either a Use or a Repeat and both can be translated easily.

Unsized locals can never be reassigned within a scope. When first assigning

代入する
to an unsized local, a stack allocation
割当
is made with the correct size.

MIR construction

構築
remains unchanged.

Guaranteed
保証する
Temporary
一時的な
Elision

MIR likes to create lots of temporaries

一時的な
for OOE reason. We should optimize
最適化する
them out in a guaranteed
保証する
way in these cases (FIXME: extend
拡張する
these guarantees
保証する
to locals aka NRVO?).

TODO: add description of problem & solution.

How We Teach This

Passing arguments

引数
to functions by value should not be too complicated to teach. I would like VLAs to be mentioned in the book.

The "guaranteed

保証する
temporary
一時的な
elimination" rules require more work to teach. It might be better to come up with new rules entirely.

Drawbacks

In Unsafe code, it is very easy to create unintended temporaries,

一時的な
such as in:

unsafe fn poke(ptr: *mut [u8]) { /* .. */ } unsafe fn foo(mut a: [u8]) { let ptr: *mut [u8] = &mut a; // here, `a` must be copied to a temporary, because // `poke(ptr)` might access the original. bar(a, poke(ptr)); }

If we make [u8] be Copy, that would be even easier, because even uses of poke(ptr); after the function call

呼び出し
could potentially access the supposedly-valid data behind a.

And even if it is not as easy, it is possible to accidentally create temporaries

一時的な
in safe code.

Unsized temporaries

一時的な
are dangerous - they can easily cause
起こす
aborts through stack overflow.

Alternatives

The bikeshed

There are several alternative

代わりのもの、選択肢
options for the VLA syntax.
文法

  1. The RFC choice, [t; φ] has type [T; φ] if φ captures no variables
    変数、ストレージ
    and type [T] if φ captures a variable.
    変数、ストレージ
    • pro: can be understood using "HIR"/resolution only.
    • pro: requires no additional
      追加の
      syntax.
      文法
    • con: might be confusing at first glance.
    • con: [t; foo()] requires the length to be extracted
      抽出する
      to a local.
  2. The "permissive" choice: [t; φ] has type [T; φ] if φ is a constexpr, otherwise
    さもなければ
    [T]
    • pro: allows
      許可する、可能にする
      the most code
    • pro: requires no additional
      追加の
      syntax.
      文法
    • con: depends on what is exactly
      正確に
      a const expression.
      This is a big issue because that is both non-local and might change between rustc versions.
  3. Use the expected type - [t; φ] has type [T] if it is evaluated
    評価する(される)
    in a context
    文脈、背景
    that expects that type (for example [t; foo()]: [T]) and [T; _] otherwise.
    さもなければ
    • pro: in most cases, very human-visible.
    • pro: requires no additional
      追加の
      syntax.
      文法
    • con: relies on the notion of "expected type". While I think we do have to rely on that in the unsafe code semantics of &foo borrow expressions
      (as in, whether a borrow is treated
      取り扱う
      as a "safe" or "unsafe" borrow - I'll write more details sometime), it might be better to not rely on expected types too much.
  4. use an explicit
    明示的な
    syntax,
    文法
    for example [t; virtual φ].
    • bikeshed: exact syntax.
      文法
    • pro: very explicit
      明示的な
      and visible.
    • con: more syntax.
      文法
  5. use an intrinsic, std::intrinsics::repeat(t, n) or something.
    • pro: theoretically minimizes changes to the language.
      言語
    • con: requires returning unsized values from intrinsics.
    • con: unergonomic to use.

Unsized ADT Expressions

Allowing unsized ADT expressions

would make unsized structs
構造、構造体
constructible without using unsafe code, as in:

let len_ = s.len(); let p = Box::new(PascalString { length: len_, data: *s });

However, without some way to guarantee

保証する
that this can be done without allocas, that might be a large footgun.

Copy Slices

One somewhat-orthogonal proposal that came up was to make Clone (and therefore Copy) not depend on Sized, and to make [u8] be Copy, by moving the Self: Sized bound

制限する、結び付けられて
from the trait to the methods, i.e. using the following declaration:
宣言

pub trait Clone { fn clone(&self) -> Self where Self: Sized; fn clone_from(&mut self, source: &Self) where Self: Sized { // ... } }

That would be a backwards-compatability-breaking change, because today T: Clone + ?Sized (or of course Self: Clone in a trait context,

文脈、背景
with no implied
暗黙の
Self: Sized) implies that T: Sized, but it might be that its impact is small enough to allow
許可する、可能にする
(and even if not, it might be worth it for Rust 2.0).

Unresolved questions

How can we mitigate the risk of unintended unsized or large allocas? Note that the problem already exists today with large structs/arrays. A MIR lint against large/variable stack sizes would probably help users avoid

避ける、回避する
these stack overflows. Do we want it in Clippy? rustc?

How do we handle truely-unsized DSTs when we get them? They can theoretically be passed to functions, but they can never be put in temporaries.

一時的な

Accumulative allocas (aka 'fn borrows) are beyond the scope of this RFC.

See alternatives.