diff --git a/proposals/README.md b/proposals/README.md index c4ee46c5db449..3c6fd6583968d 100644 --- a/proposals/README.md +++ b/proposals/README.md @@ -44,6 +44,7 @@ request: - [0198 - Comments](p0198.md) - [0199 - String literals](p0199.md) - [0253 - 2021 Roadmap](p0253.md) +- [0257 - Initialization of memory and variables](p0257.md) - [0285 - if/else](p0285.md) - [0301 - Principle: Errors are values](p0301.md) - [0339 - `var` statement](p0339.md) diff --git a/proposals/p0257.md b/proposals/p0257.md new file mode 100644 index 0000000000000..0298804bdbc58 --- /dev/null +++ b/proposals/p0257.md @@ -0,0 +1,1284 @@ +# Initialization of memory and variables + + + +[Pull request](https://github.com/carbon-language/carbon-lang/pull/257) + + + +## Table of contents + +- [Problem](#problem) +- [Background](#background) +- [Proposal](#proposal) + - [Unformed objects](#unformed-objects) + - [Representation](#representation) + - [Address-of and escape](#address-of-and-escape) + - [Types with an unformed state](#types-with-an-unformed-state) + - [Raw storage and padding](#raw-storage-and-padding) + - [Necessary initialization](#necessary-initialization) + - [Optional features for unformed states](#optional-features-for-unformed-states) + - [Hardening initialization](#hardening-initialization) + - [Queryable unformed state](#queryable-unformed-state) + - [Function returns and initialization](#function-returns-and-initialization) + - [Declared `returned` variable](#declared-returned-variable) + - [Constructors and initializing aggregate objects](#constructors-and-initializing-aggregate-objects) + - [No unformed members](#no-unformed-members) +- [Rationale based on Carbon's goals](#rationale-based-on-carbons-goals) +- [Alternatives considered](#alternatives-considered) + - [Require compile-time-proven initialization](#require-compile-time-proven-initialization) + - [Trivially required initialization, with no escape hatch](#trivially-required-initialization-with-no-escape-hatch) + - [Initialize on all branches prior to use (Rust, C#, TypeScript)](#initialize-on-all-branches-prior-to-use-rust-c-typescript) + - [Definitive initialization (Swift)](#definitive-initialization-swift) + - [C/C++ uninitialized](#cc-uninitialized) + - [Allow passing unformed objects to parameters or returning them?](#allow-passing-unformed-objects-to-parameters-or-returning-them) + - [Allow assigning an unformed object to another unformed object?](#allow-assigning-an-unformed-object-to-another-unformed-object) + - [Fully destructive move (Rust)](#fully-destructive-move-rust) + - [Completely non-destructive move (C++)](#completely-non-destructive-move-c) + - [Named return variable in place of a return type](#named-return-variable-in-place-of-a-return-type) + - [Allow unformed members](#allow-unformed-members) + - [Summary of advantages and disadvantages](#summary-of-advantages-and-disadvantages) + + + +## Problem + +How should Carbon handle uninitialized memory and variables? There are a number +of goals we would like to satisfy in this space: + +- Minimize the overhead (in optimized builds) of unnecessary initialization. +- Detect and diagnose bugs in code where variables are used without being + initialized first at lower cost than a separate and unscalable build mode + (MSan for C++). +- Avoid the ergonomic, readability, and at times correctness issues when + forced to initialize all variables at the point of declaration, even if + there is no intentional value to yet use. +- Minimize the security risk posed by accidental access of uninitialized + memory. +- Avoid unnecessary overhead for dynamic tracking of whether a variable is + initialized. +- Avoid unnecessarily running destructors for variables when they haven't yet + been initialized or used. +- Allow most types similar ergonomics and behaviors as a simple machine + integer type. +- Offer similar behavior regardless of storage location (stack, heap, + thread-local-storage, static / global storage). +- Leave a path to add more strict safety rules in the future, such as + requiring explicit marking when the address of an uninitialized variable + escapes. + +Part of our goal is also to support code patterns such as: + +``` + // At block scope of some function... + var temporary: Int; + while (...) { + // ... code not using `temporary` ... + + temporary = ...; + // ... code using `temporary` ... + } +``` + +That is, we also don't want to force a _syntactic_ distinction when an +assignment occurs to an already initialized variable. That would end up with +fundamentally similar ergonomic (and potentially performance) overheads as +forcing initialization as part of declaration. Throughout this document, the +term "assignment" should instead be understood as the operation that can be used +in this construct. + +These problems are also tightly related to the problems of _moved-from_ +semantics. These semantics might range from completely non-destructive (as in +C++) to completely destructive (as in Rust). A compelling solution for the +variable initialization challenges can also provide a compelling approach for +moved-from semantics that represents a middle ground between the two extremes +seen in C++ and Rust. For example, we propose that the following should hold +equally for moved-from objects and objects without an explicit initializer: + +- The object must not be used in any way other than assignment or destruction. + - Note that this refers to the _object_. Taking the address is fine, but + the restrictions here on how the object is used apply equally to any + uses through a pointer to the object. +- It must be possible to transition it to a valid, normal object through + assignment. +- It should be allowed to destroy them. +- It should not be necessary to destroy them. + +## Background + +- [Definitive assignment or definitive initialization](https://en.wikipedia.org/wiki/Definite_assignment_analysis) + (used by Swift, Java, and so on) +- [Rust checked uninitialized data](https://doc.rust-lang.org/nomicon/checked-uninit.html) +- [Elements of Programming](http://elementsofprogramming.com/eop_bluelinks.pdf) +- [P2025 - Guaranteed copy elision for named return objects](http://wg21.link/p2025) + +## Proposal + +We propose two fundamental concepts: + +1. An _unformed state_ for objects. +2. Raw, uninitialized storage. + +The first of these is a new concept and is discussed in detail below. However, +uninitialized storage in Carbon should work in the same way as an uninitialized +array of `unsigned char` or `std::byte` in C++. This is intended primarily to +serve the needs of custom data structures such as C++'s `std::vector` or +whatever Carbon ends up with as an `Optional(T)` type. With such raw storage, +initialization and destruction of any objects within the storage must be managed +manually and externally and the language won't provide any automatic mechanisms. +The detailed syntax for working with raw storage in this manner is left to a +subsequent proposal. + +### Unformed objects + +An _unformed_ state is a state of an _object_. We both classify _objects_ based +on whether they are in an unformed state or a fully formed state, as well as +classify _types_ which support unformed states at all versus types that require +objects to always be in a fully formed state. + +An unformed state for an object is one that satisfies the following properties: + +- Assignment from a fully formed value is correct using the normal assignment + implementation for the type. +- Destruction must be correct using the type's normal destruction + implementation. +- Destruction must be optional. The behavior of the program must be equivalent + whether the destructor is run or not for an unformed object, including not + leaking resources. + +Any operation on an unformed object _other_ than destruction or assignment to a +fully formed value is an **error**, including: + +- Comparisons with other objects. +- Passing an unformed object to a function or returning it. +- Initializing or assigning _with_ an unformed object, including all forms of + self assignment or self swap: + + ``` + var unformed_x: Int; + + // Each line mentioning `unformed_x` below is an error: + var a: Int = unformed_x; + var b: Int = 42; + b = unformed_x; + var unformed_c: Int; + unformed_c = unformed_x; + unformed_x = unformed_x; + + // If we imagine a move operation similar to C++'s `std::move` spelled `~` + // and a function `Swap` similar to C++'s `std::swap` but using pointers, + // these too are errors: + unformed_x = ~unformed_x; + Swap(&unformed_x, &unformed_x); + ``` + +These errors should be diagnosed as early as possible in a debug build and at +least at runtime. In a performance build, it may lead to undefined behavior, but +in a hardened build we have a strong mitigation that restores safety by defining +the behavior using a maximally safe representation. + +When a variable is declared without initialization, such as: + +``` +var x: Int; +``` + +The variable is implicitly initialized to its type's unformed state and +represents an unformed object. + +It is invalid to declare a variable without explicit initialization unless its +type implements the interface to specify how to do that initialization. For +types that do not support initializing the variable to an unformed state, the +programmer must either explicitly initialize it to a valid and fully formed +state for the type, or must change the type to something like an `Optional(T)` +that introduces an unformed state (and the dynamic tracking necessary to +implement it) alongside raw storage for the underlying object. + +#### Representation + +The unformed state can have the same representation as valid and fully formed +states for the object (for example, an empty `Optional(T)`). While there is a +semantic difference (any operation _other_ than assignment and destruction is an +error for an unformed object), there is no problem re-using a representation +which is also used for fully formed and valid objects provided it satisfies all +three of the above constraints. The semantic restrictions remain even in this +case. Using the hypothetical `Optional(T)` API, we could imagine: + +``` +var formed: Optional(T) = .Empty; +// This is of course fine. +if (formed.empty()) { ... } + +var unformed: Optional(T); +// This would be an error even though `unformed` might have identical +// in-memory representation or bit-pattern to `formed`: +if (unformed.empty()) { ... } +``` + +#### Address-of and escape + +It is allowed to take the address of an unformed object and escape it to another +function. This function may even be responsible for assigning a fully formed +object to that location. + +``` +fn AssignIntTo(x: Int, destination: Int*) { + *destination = x; +} + +fn Example() { + var y: Int; + // `y` is unformed here. + AssignIntTo(42, &y); + // `y` is fully formed and usable. +} +``` + +This in turn is part of why it is important that the destructor is _allowed_ to +be run on an unformed object. This gives us a conservatively correct way to +implement code where it may be impossible to know whether the object has been +initialized to a fully formed object, and similarly code where it is impossible +to know whether the object has been moved-from. Consider some `HashMap` +container with allocated memory where the destructor _must_ be run to release +the memory: + +``` +fn MaybeInitializes(map: HashMap*); +fn MaybeMovesFrom(map: HashMap*); + +fn Example() { + var map: HashMap; + MaybeInitializes(&map); + if (...) { + // Don't know if we need to destroy `map`. But conservatively we can run the + // destructor unconditionally. + return; + } + + // Unconditionally assign a particular value to our map. + map = ...; + MaybeMovesFrom(&map); + + // `map` could be unformed (due to being moved from) or still fully formed. + // Run the destructor unconditionally to be safe. + return; +} +``` + +#### Types with an unformed state + +The intent is that most types have an unformed state, as it will let types "be +like the `int`s" -- we design it to closely match how machine integers actually +behave in modern optimizing compilers: when left "uninitialized" they can be +safely _assigned_ or _destroyed_. But running the destructor is _optional_. + +Types can opt into this semantic model and define an explicit operation to +create a type in the unformed state by implementing an interface. Because this +will be defined semantically by the existence of an implementation of the +relevant interface, and operationally by providing code to produce an unformed +object, there is no specific bit pattern or representation encoded for the +unformed state. The interface should provide a default implementation for types +where all members have an implementation which recurses member-wise. + +It is reasonable to wonder if types where the default implementation is valid +implicitly implement the interface, or should it require an explicit opt-in? +Initially we suggest this should be explicit. There may grow to be a set of +interfaces which should be implicitly implemented due to their prevalence, but +it seems better to be conservative at first and approach that systematically. + +Note that this state is similar but more restrictive than the _partially formed +state_ from Elements of Programming (EoP) in section 1.5. + +#### Raw storage and padding + +Raw storage and padding bytes are treated like they always consist of unformed +bytes. We propose not allowing sub-byte raw storage or padding. + +Padding bytes have specific practical considerations. It is fine for operations +that work across all bytes of raw storage, such as C-style routines like +`memset` and `memcpy`, to write to padding bytes. But doing so has no observable +effect, and even if they notionally set padding bytes to a known value, the +padding bytes continue to behave as if they are unformed bytes. + +Copying _from_ padding bytes is significantly more difficult because it risks +opening up the possibility of copying unformed objects more generally. That has +significant downsides and we are proposing it is +[not allowed](#allow-assigning-an-unformed-object-to-another-unformed-object). +Instead, there will need to be a low-level language facility that specifically +handles copying storage with padding bytes. Such a facility would have to either +exclude the padding bytes from the copy, or provide a strong guarantee that they +are only ever _directly_ copied into other padding bytes without any +intermediate steps. + +### Necessary initialization + +Some types will not provide an unformed state. Variables of such types must be +initialized when declared: + +``` +struct NecessaryInit { + var m: Mutex; + + // Potentially other members without a meaningful unformed state. +} + +// This line would be an error: +var error_no_init: NecessaryInit; +``` + +For these types, the programmer must either explicitly initialize it to a valid +and fully formed state for the type, or must change the type to something like +an `Optional(T)` that introduces an unformed state (and the dynamic tracking +necessary to implement it) alongside raw storage for the underlying object: + +``` +// This is fine, it contains the boolean necessary to track initialization. +var ok: Optional(NecessaryInit); +``` + +### Optional features for unformed states + +There are two _optional_ aspects of unformed states that types may elect to +explicitly specify (again by implementing interfaces, here by specializing the +implementation rather than relying on the default fallback). + +#### Hardening initialization + +First, types may customize how to further initialize an object that is in the +unformed state to most effectively harden it against erroneous usage. For +example, the unformed state of an `Int` is trivial -- it can simply be left +uninitialized. However, it can be hardened against bugs by setting it to zero. +User defined types have the option of specifying a specific routine to do +similar hardening initialization. The entire storage, including any padding +bytes, for all types will be at least be zeroed as part of hardening +initialization. This routine simply allows a user defined type to specifically +customize the hardening initialization of some parts or members of the object to +values other than zero. + +#### Queryable unformed state + +The second optional aspect is implementing a test for whether an object is +unformed. This test cannot be run by user code -- doing so would be a bug due to +referencing an unformed object. However, for types where the unformed state is +intrinsically distinct from any other state, they can expose the ability to +query this. That allows debug builds of Carbon to verify objects are fully +formed prior to use without external tracking state. Types which do not +implement this functionality will still get partial checking in debug builds +through external state, but may lose dynamic enforcement across ABI boundaries +where this extra state cannot be propagated. These are also the boundaries +across which the hardening will be done to ensure bugs across those boundaries +cannot become security vulnerabilities in the hardened build configuration. + +### Function returns and initialization + +This proposal also has implications for returning values from functions, +especially as it relates to how Carbon can provide similar functionality to +guaranteed return value optimization (RVO) and named return value optimization +(NRVO) in more principled ways than C++ does. While these facilities should +allow Carbon to not require tricks like NRVO and the dangers that come with +them, the primary motivation is to increase the expressivity of "normal" +function returns in order to address complex use cases in object +[construction](#constructors-and-initializing-aggregate-objects). + +Consider the following common initialization code: + +``` +fn CreateMyObject() -> MyType { + return ; +} + +var x: MyType = CreateMyObject(); +``` + +We propose that the `` in the `return` statement of +`CreateMyObject` _initializes_ the variable `x` here. There is no copy, etc. It +is precisely the same as: + +``` +var x: MyType = ; +``` + +We also propose this applies _recursively_. This matches how C++ has evolved +with "guaranteed copy elision". + +However, while most C++ implementations provide a "named return value" +optimization that applies the performance benefits of this to returning a named +variable, it has proven extremely difficult to _automatically_ provide +"guaranteed copy elision" when returning a named variable. The determining +factors for whether it can be safely applied change significantly based on +optimization levels and numerous other implementation details. The result has +been repeated requests from users and proposals to provide an explicit syntax +for marking a named variable as being returned and then both _guaranteeing_ the +copy elision takes place, and requiring that all returns are of the variable and +compatible with that elision. + +While these proposals have hit issues in C++ due to compatibility concerns and +complexity in C++ itself, those won't apply to Carbon. We suggest that Carbon +should provide a direct mechanism for this with a named return variable which +obeys the same initialization rules as any other variable. + +An important aspect of any approach to named return variables is that, much like +with normal return values, there is no copy when initializing. This means that +the address of `result` in the examples below of `CreateMyObject2` and +`CreateMyObject3` are the same as the address of `y` during the evaluation of +`y`'s initialization expression. Only one variable is created. It is first +initialized to an unformed state on entry to `CreateMyObject2` and assigned to a +fully formed value internally. + +#### Declared `returned` variable + +We propose to allow a specific variable within the function body to be declared +as `returned` from the function. Assuming that `MyType` has an unformed state, +the alternative to the above syntax would be: + +``` +fn CreateMyObject2() -> MyType { + returned var result: MyType; + // Any code we want here. + + result = CreateMyObject(); + + // Must explicitly use the variable return syntax rather than returning an expression. + return var; +} +``` + +And we can initialize the return variable to remove the unformed requirement: + +``` +fn CreateMyObject3() -> MyType { + // Any code we want here. + + returned var result: MyType = CreateMyObject(); + + // Potentially more code... + return var; +} +``` + +**Minor syntax variations (not proposed):** + +- Just use `return;` rather than `return var;` for the actual control flow + return. + - The ceremony here is intended to distinguish from the case of a function + that doesn't have a return value at all, but it could be removed. +- Require `return ;` where `` is constrained to match the declared + variable name. + - One concern with this is that the `` here is not an expression -- + it isn't evaluated or anything and must be just a name. Worse, there + might not _be_ a single name. + - However, if there were good syntax, it might help with scoping. +- Maybe better syntax than `returned var` and `return var`, open to follow-up + proposals to tweak the exact syntax of both sides of this. + +We expect subsequent proposals to generalize this facility to non-`var` +declarations where there is a reasonable and clear semantic. However, it should +be noted that for `var`-style declarations in particular where the address of +the object can be taken, the entire declared object must be marked as `returned` +-- it cannot be a component of the pattern alone. + +We also propose a set of rules to use for the types in the pattern, and to what +degree they must match the return type: whatever type would be the _allocated_ +type of the variable declaration must match the return type. For declarations +that produce storage such as `var`, the entire allocated object based on the +pattern must match the return type. If this is generalized to non-allocating +patterns, it would be fine to use any pattern that can match the return type. + +The initializer expression must initialize the declared return type, and then +any pattern match is done. + +Control flow presents some challenging questions for this design. Consider code +with the following structure: + +``` +fn ReturnEitherOr(...) -> MyType { + // ... + if (...) { + returned var result1: MyType = ...; + // ... + return var; + } else if (...) { + returned var result2: MyType = ...; + // ... + } else { + returned var result3: MyType = ...; + // ... + } + // ... + return var; +} +``` + +It seems like at least `result1` should be valid. What about `result2` and +`result3`? + +More complex control flow patterns grow the complexity this presents. For +example: + +``` +fn ReturnVarWithControlFlow() -> Point { + if ... { + // Is this allowed? + returned var p1: Point = ...; + + if ... { + // This seems fine to return `p1`. + return var; + } + } + if ... { + // But this would have to be an error... + return var; + } + + // And what happens here? + returned var p2: Point = ...; + + for ... { + // Or here? + returned var p3: Point = ...; + + // Even though we might actually return... + if ... { + return var; + } + } + // Definitely no way to know what happens here between p1, p2, and p3. + return var; +} +``` + +We propose a set of restrictions to give simple and understandable behavior +which remains reasonably expressive. + +1. Once a `returned var` is in scope, another `returned var` cannot be declared. +2. Any `return` with a `returned var` in scope must be `return var;` and returns + the declared `returned var`. +3. If control flow exits the scope of a `returned var` in any way other than a + `return var;`, it ends the lifetime of the declared `returned var` exactly + like it would end the lifetime of a `var` declaration. +4. There must be a `returned var` declaration in scope when the function does a + `return var;`. + +A consequence of these rules allows code like: + +``` +fn MaybeReturnedVar() -> Point { + { + returned var p: Point = ...; + if ... { + return var; + } + } + + return MakeMeAPoint(); +} +``` + +### Constructors and initializing aggregate objects + +Constructors of user defined aggregate types can present specific problems with +initialization because they intrinsically operate on types prior to all of their +invariants being established and when a valid value may not (yet) exist for +subobjects such as members. These problems are discussed in detail in the blog +post +["Perils of Constructors"](https://matklad.github.io/2019/07/16/perils-of-constructors.html). +A primary goal in trying to solve these problems is being able to separate the +phase of initializing members (or other kinds of subobjects) prior to the outer +object existing and establishing any necessary invariants without creating holes +in the type system soundness. An interconnected problem is correctly running +destructors (in order to release any acquired resources) in the event of errors +_during_ construction. + +The behavior of function returns provides interesting build blocks to write code +that addresses these problems. They provide a natural set of tools to build two +phases of a constructor: code that executes before a valid object exists and +code that executes afterward. It even gives a clear demarcation between these +phases. Adding `returned` variable declarations provide an important building +block for solving these problems. + +``` +struct Point { + var x: Int; + var y: Int; +} + +fn PointConstructorPreObjectPhase() -> Point { + // Code that runs prior to an object existing. + // Just assembles inputs needed to initialize the members. + return (, ); +} + +fn ComplexTwoPhasePointConstructor() -> Point { + // Code that runs prior to an object existing. + var x_init: Int = ...; + var y_init: Int = ...; + + returned var p: Point = (x_init, y_init); + // `p` is now fully formed, and its destructor *must* run. + + // More code that uses p can go here. + return var; +} +``` + +#### No unformed members + +This proposal suggests disallowing unformed members -- objects must be +completely formed or completely unformed. Allowing mixing of formed and unformed +members in an object introduces significant complexity into the language without +advantages, and the code patterns that result scale poorly to real world +examples. For details on what it would look like to allow these and how this +goes wrong, see the [alternative section](#allow-unformed-members) + +Note that inheritance opens a set of closely related but also importantly +different concerns as members here and this proposal doesn't take any position +one way or another there. If and when there is a proposal for how inheritance +should work in Carbon, it should address this question. + +## Rationale based on Carbon's goals + +This model is designed to give significant control to users of Carbon for +maximizing performance around variable initialization, which matches the primary +goal of Carbon for performance-critical software. It then tries to provide as +many ergonomic and safety tools as possible to support the goals of easy to read +and understand code and providing practical safety. Leaving space for future +evolution toward stricter checking of potentially unsafe code aligns with the +goal for language evolution. + +## Alternatives considered + +### Require compile-time-proven initialization + +A primary alternative to the approach of unformed objects for initialization is +to simply require initializing all variables at compile time. This can take a +few forms across the spectrum of compile time analysis complexity. + +#### Trivially required initialization, with no escape hatch + +The simplest approach is to require initialization at the point of declaration +or allocation for any variable or memory. An essential property to this approach +remaining simple is that it have _no_ escape hatch. Once such an escape hatch +exists, all of the semantic questions of this proposal need to be answered. + +Advantages: + +- No need to specify or support the complexity of objects that have been + declared but not fully initialized in the language. +- Simple, easy to teach, easy to implement. + +Disadvantages: + +- Will result in initialization with nonsensical values that if used remain a + bug. We won't have any realistic way to detect these bugs. +- Imposes a non-trivial performance cost in some cases and for some users + without providing any effective mechanisms to address this overhead. + +#### Initialize on all branches prior to use (Rust, C#, TypeScript) + +Use a simple rule such as requiring initialization before use along all +branches. + +Advantages: + +- Still avoids much of the language level complexity of unformed objects. +- Very simple to implement. +- Reasonably teachable. +- Avoids trivially redundant initializations. + +Disadvantages: + +- Has significant expressivity limits past which initialization is forced. + - When this happens, the disadvantages of always requiring initialization + repeat. While it is expected to happen less often, it still happens. + - Adding an unsafe escape hatch that avoids initialization even in this + case returns to the same fundamental complexity and model as is proposed + here. +- Would still need a semantic model for moved from objects which would + introduce some amount of additional complexity. + +#### Definitive initialization (Swift) + +We could do significantly more complete analysis of the dataflow of the program +to prove that initialization occurs prior to use. + +Advantages: + +- Still avoids much of the language level complexity of unformed objects. + +Disadvantages: + +- Some implementation complexity to achieve strong dataflow analysis. +- Significant complexity to teach -- it can be difficult to understand or + explain why the data flow analysis fails in edge cases. +- Can result in brittleness where changes to one part of code confuse the + dataflow analysis needed by another part of code. +- There still exist rare cases where the analysis fails, and a nonsensical + initialization may be required and the disadvantages of those return (both + bug detection and performance). + - The rate at which this occurs scale inversely proportional to the + complexity of the data flow analysis and the teachability. +- Would still need a semantic model for moved from objects which would + introduce some amount of additional complexity. + +### C/C++ uninitialized + +We could pursue the same approach as C and C++ here. However, this path has +proven to have severe problems. It has inevitably resulted in prevalent bugs and +security vulnerabilities due to programmer errors, And having uninitialized +variables in the language makes teaching the debug build to catch these bugs +extremely difficult and potentially impossible. We essentially will need +developers to heavily use an MSan-like mode (either by integrating it with the +debug build or having a separate one) in order to cope with the bugs caused by +uninitialized memory. This is the only thing that has allowed coping with C/C++ +semantics here and even it is not very effective. + +If MSan remains a separate tool that needs propagation and origin tracking +([details](https://research.google.com/pubs/archive/43308.pdf)), the cost and +the barrier to entry will remain very high: MSan is currently the least used and +the least portable of the sanitizers; it is widely used only in two Google’s +ecosystems, only on one OS, where it is maintained with a considerable effort. + +If we end up having uninitialized memory, we should aim at having MSan-like +checking be part of the normal debug build, which means no or very limited +propagation. Any form of propagation will require optional original tracking, +which is doable, but will mean extra CPU/RAM overhead for meaningful +diagnostics. + +Even without propagation, MSan-like checking requires bit-to-bit shadow that +would incur a 2x RAM overhead in the debug build. Byte-to-bit shadow, which is +< 12% RAM overhead, is prone to racy false positives and false negatives +unless a) instrumentation uses expensive bit-wise atomics or b) the language +defines data races on 8-byte granularity. Adding this restriction on data races +would diverge from C++ and further complicate porting code and interoperability. + +### Allow passing unformed objects to parameters or returning them? + +This proposal suggests **not** allowing code to pass unformed objects to other +functions as arguments or to return them. They can still be escaped to another +function by passing the address. + +Advantages of allowing this: + +- Would accept more code that can be written in C++ today and doesn't directly + exhibit a bug. For example, unused arguments or return values in C++ could + be left uninitialized, but in Carbon they would need to use an optional type + instead. + +Disadvantages of allowing this: + +- Makes checking for errors in debug builds both more expensive and less + helpful by potentially moving the error to a separate function rather than + the one most likely containing any bug. + - We have anecdotal evidence that indicates producing an error at the + function boundary would be preferable to delaying the error. This + evidence is a result of MSan delaying any such errors, while UBSan + eagerly errors at the function boundary for a few types (`bool`s and + `enum`s). Particularly for `bool` parameters the early errors from UBSan + have essentially always been correctly diagnosed and bugs. The early + errors from UBSan are also significantly easier to understand. + - Implementing interprocedural checking either requires a more expensive + ABI or using more expensive shadow-memory checks which would otherwise + be unnecessary. +- Equivalent functionality can be achieved safely using an optional type, or + unsafely by passing the address. The only cases where passing the address + would be less efficient than what C++ allows are likely to be amenable to + compiler optimization to remove the overhead. + +### Allow assigning an unformed object to another unformed object? + +This proposal suggests **not** allowing assigning an unformed object to another +unformed object. + +- We should try _not_ allowing this at first. + - This will be enforced the same way as any other use of an unformed + object: at compile time when locally detectable, at runtime when + sufficient runtime tracking is enabled. +- If we find we must allow this in some cases, we should start by allowing it + only using specially named operations. +- If either can be made to work, they are significantly easier to check for + bugs. + +Advantages of allowing this: + +- Avoids some surprise for users. + - It seems like it should work for integers. + - They may not know when the incoming object is actually unformed + resulting in an error far away from the root cause. + - Only some surprise is avoided given that we still can't assign unformed + objects to fully-formed objects -- doing so would open up the + possibility of unformed subobjects. +- Makes some idiomatic patterns in C++ either invalid or inefficient. Consider + the typical C++ implementation of a `swap` function: + + ```c++ + template + void swap(T& lhs, T& rhs) { + T tmp = std::move(lhs); + lhs = std::move(rhs); + rhs = std::move(tmp); + } + ``` + + If this function is called with `lhs` and `rhs` referring _to the same + object_, then the first move into a temporary leaves the object in an + unformed state, and the second assignment will assign that unformed object. + Whatever Carbon code ends up replicating patterns like this will require one + of two options: + + 1. Check for `lhs` and `rhs` referring to the same object, adding a branch + and its overhead to the code. + 2. Require that it is never called with `lhs` and `rhs` referring to the + same object, pushing the cost onto the caller. + + We would largely expect (2) to be the dominant choice in Carbon due to + performance concerns. However, if the ergonomic burden of (2) becomes + significant, we may revisit this rule to carve out enough of a facility for + self swap specifically. That might either be the narrow specially-named + operation mentioned above, or attempt to special case self-assignment in + some way. + + - We could consider narrowly allowing _only_ self-move-assignment or + self-swap of unformed objects to lessen the impact of precluding the + move-assignment and swap of unformed objects generally. However, that + carries surprising complexity cost for the language specification and + creates very subtle rules. + + At a minimum, types with an unformed state would need to implement move + assignment in a way that would be correct even when performing a + self-move of an unformed object. + + On top of that, the simplest solution would be to allow _any_ self move + assignment of an unformed object. However, that would require any move + assignment that _might_ be a self-move to be implemented in a checked or + sanitized build with a branch to detect that case and not error. To + lower this overhead of checked builds, we would need to add + language-level swap operation of some form and only exempt _that_ from + checking. This would reduce the overhead, but would now make it + impossible to even test the self move assignment that would still be + required to work in non-checked builds. + +- May be important to enable specific code patterns if we allow member access. + +Disadvantages of allowing this: + +- Will require the core propagation logic and origin tracking that is a source + of large complexity and cost in MSan to detect bugs. + - However, object granularity and lack of arithmetic makes this still + somewhat less severe than what MSan deals with. + - Only a concern for detection, hardening is easily accomplished + regardless. +- Can only possibly support combined debug build mode if restricted to a + special/designated operation rather than arbitrary assignment. + - Origin tracking will still be required and have significant cost. + +### Fully destructive move (Rust) + +Rust has truly destructive moves -- the object is gone after being moved from, +and no destructor ever runs past that point. This is possible because the type +system models when a variable declared in one function can be moved-from by +another function. It is incompatible with allowing move-from through a pointer +to a non-local object. + +Advantages: + +- Precise, safe, and efficient lifetime management. +- Perfect detection of use-after-move bugs. + +Disadvantages: + +- Doesn't compose with pointers and C++ idioms which involve moving out of + non-local objects through a pointer. + - Would require redesigning systems to model the lifetime in the type + system. + +### Completely non-destructive move (C++) + +C++ moves are never destructive. The result is that moves simply do not impact +the _lifetime_ of the moved-from object, only its state. + +Advantages: + +- Perfect match for C++ semantics and idioms, including interoperability and + migrated code and systems. + +Disadvantages: + +- Pervasive user expectation that destroying a moved-from object is a no-op, + but nothing in the language helps enforce this pattern. +- Allows types to provide meaningful semantics for use-after-move. + - Rarely used, resulting in strong user expectations that this isn't + allowed but special cases where it is allowed. + - Complicates generic code having to handle both patterns. + - Significantly reduces the effectiveness of use-after-move bug detection. +- Introduces difficulty in optimizing away no-op destruction post move. + - Some cases requires complex rules around ABIs and calling conventions. + - Other cases require expensive optimization techniques (inlining and + memory analysis) that can fail in some circumstances. + +### Named return variable in place of a return type + +An alternative approach to +[declaring `returned` variables](#declared-returned-variable) is to extend the +syntax for the return type such that it can declare a named variable that holds +the return value. For example, provided that `MyType` has an unformed state: + +``` +fn CreateMyObject2() -> var result: MyType { + // `result` here is an unformed object. + // so we can assign to it here: + result = CreateMyObject(); + + // Because there is a named return variable, we cannot + // have an expression in the return statement. + return; +} +``` + +We could also explicitly initialize the return variable, which eliminates the +requirement that its type has an unformed state: + +``` +fn CreateMyObject3() -> var result: MyType = CreateMyObject2() { + // any code we want + return; +} + +var y: MyType = CreateMyObject3(); +``` + +**Minor syntax variations:** + +- Possible to use different arrow symbols (such as `=>` instead of `->`) to + differentiate different kinds of returns. + - However, we don't recommend this given the subtlety of the visual + difference. +- Potentially require `return ;` where `` must exactly match the + return variable's name to make it clear what is being returned. + - One concern with this is that `` can't really be an expression in + general, it needs to _just_ be a name. This may be a bit surprising. + - Also complicated as there is no need to have a single name for the + variable: it might be `var (result: MyType, Error: error)` for example, + and it becomes unclear what to put in the `return`. + +**Advantages:** + +- The primary advantage of this approach is that it avoids any and all + challenging lifetime questions or restrictions on where the `returned` + variable can be declared or what control flow must follow. +- It is also slightly less expressive which if sufficient seems desirable. + +**Disadvantages:** + +- The syntax space of the return type is very crowded already, and this would + add more complexity there rather than fitting in cleanly with existing + declaration spaces within the body of the function. +- Likely to be an implementation detail and valid to use on a function with a + normal return _type_ in its forward declaration. This isn't obvious when + placed in the declaration position, and it might well be unnecessarily added + to forward declarations just because of its position. +- Removes the ability to _both_ specify a specific return type and a pattern + for binding parts of that object to names. Instead, the type must be + inferred from the pattern. + - This ended up feeling like a deep and fundamental problem where we would + lose important expressivity to disambiguate the return type from + patterns. For example, when the return type is some form of sum type or + variant, the pattern space is both especially attractive to use and + fundamentally fails to express enough information to capture the type. + +### Allow unformed members + +Regardless of which specific strategy, using the proposed `return` facilities +for constructing aggregate objects still presents some ergonomic difficulties +for large objects with many members, or members that are expensive/impossible to +move if we don't have the `returned var` facility. To further ease initializing +members, we could allow access to unformed members of an unformed object. +Superficially, this remains safe -- we aren't allowing access to uninitialized +storage, and the only access allowed is what is always allowed for unformed +objects: assignment (and destruction). However, making this work with +destructors adds some complexity to rules around unformed objects. + +Disallowing access to unformed members provides a simple rule for the semantics +required of the destructor: it must be valid and correct to run on an unformed +object or a _fully_ formed object. Destructors always have to be valid for fully +formed objects, so the only addition is the unformed state being a no-op. + +However, for the same reasons it might be necessary to run the destructor on an +unformed object, if we allow assigning to a _member_ of an unformed object the +destructor may also need to be run with this mixed state where some members are +unformed and others are fully formed: + +``` +struct Point { + var x: Int; + var y: Int; +} + +fn AssignToX(x: Int, p: Point*) { + p->x = x; +} + +fn Example() { + var p: Point; + if (...) { + // Allowed to call the destructor of `p`, sees a fully unformed `Point`. + return; + } + AssignToX(42, &p); + if (...) { + // If `AssignToX` is in another file, don't know if `p` is fully unformed, + // a mixture of `x` fully formed and `y` unformed, or `p` is fully formed. + // The last case can only be handled by running the destructor of `p` + // so that's what we have to do here, and it must handle all the other + // cases as well. + return; + } + p->y = 13; + // ... +} +``` + +The added requirement for destructors of types that support an unformed state is +that if they are called for an unformed object of that type, they must run the +destructors for any members that might themselves be fully formed. + +- For private members, the type itself controls which members might be + assigned fully formed values and the destructor run. The type's destructor + merely needs to agree with the constructors and/or factories of the type to + destroy any members that might have been assigned values. +- For public members, the type's destructor must destroy _all_ members as it + cannot know which ones might have been assigned a value. + +Because of these inherent restrictions around private members, types can ensure +any necessary relationship between its private data members hold in order for +the destructor to work. See the detailed example below for how this might work +in practice. + +Despite these added requirements to the destructor, it isn't exempt from the +rules governing unformed objects when accessing its members: any that are in +fact unformed when the type's destructor runs are only allowed to be assigned to +or destroyed. + +Lastly, we need some syntax other than assignment for marking when the members +have reached a state where the entire object is fully formed. This document uses +a hypothetical `?reify?` statement that is intended to be placeholder syntax +only. Just like an assignment to an unformed object, after this point it is +fully formed and follows those rules. Our example becomes: + +``` +fn EvenSimplerPointConstructor() -> var p: Point { + // p is unformed and no members are referenced. + // Destruction is completely optional. + p.x = ...; + // p.x may now be fully formed. Either p must be destroyed, + // or p.x must be destroyed. + p.y = ...; + // Now either p must be destroyed, or both p.x and p.y must be + // destroyed. + + ?reify? p; + // Now p must be destroyed, and is a fully formed object. + + // ... + + return; +} +``` + +Before the `?reify?` operation, the compiler can either track the members which +are assigned fully formed values and run their destructors, or it can elect to +run the entire object's destructor if, for example, the entire object escapes +and it becomes impossible to track which members have been assigned fully formed +values. But once the object has the `?reify?` operation performed, its +destructor _must_ be run as it is now itself a fully formed object. + +Now that we have all the components of this tool, let's consider a container +like `std::vector` using a hypothetical syntax for defining a type, its +implementation of the necessary initialization, its destructor, and a factory +function. This example specifically tries to illustrate how, for private +members, the function doing construction and the destructor can work together to +ensure at any point of destruction, the necessary state and invariants hold. + +``` +struct MyContainer(T: Type) { + private var size: Int; + private var capacity: Int; + private var data: Optional(T*); + + // ... public API ... + + impl NecessaryInit { + // Not necessarily suggesting this specific API. + // Just *some* API we pick for doing the necessary init. + // This API assumes that all members with an unformed state + // have their implementation called already, all we need to do is + // adjust to *specific* values anything that is necessary for this + // type. + fn Init(this: Self*) { + this->data = Optional(T*).Empty; + } + } + + fn Create(count: Int, T: value) -> Optional(MyContainer(T)) { + var container: MyContainer(T); + // Set the capacity first so it is available for deallocation. + container.capacity = count; + // And clear the size so we don't need a special case. + container.size = 0; + + // Next set the data to the allocated buffer. If the allocation fails, + // this routine should just return null. + container.data = AllocateRawStorage(count, T); + if (container.data == null) { + // Might destroy `container` here. + return .Empty; + } + + // Some loop syntax over [0, count). + for (i: Int = 0 ..< count) { + if (not CopyThatMightFail(value, &container.data[i])) { + // Might destroy `container` here. + return .Empty; + } + // Increment size as we go to track how many objects + // need to be destroyed if the next one fails. + ++container.size; + } + + ?reify? container; + + // Maybe we make this implicit... somehow make it move... + return .Some(container); + } + + // However we end up naming this... + fn Destructor(this: Self*) { + if (this->data.IsEmpty()) { + // If the buffer isn't allocated, nothing to do. + return; + } + // Destroy any successfully created object. The `Create` + // function ensures that `size` is valid for use here. + for (i: Int = 0 ..< this->size) { + // However we spell in-place destroy... + this->data[i].Destructor(); + } + // Deallocate the buffer. Here as well the `Create` + // function ensures that `capacity` is available for a + // non-null data pointer. + Deallocate(this->data, this->capacity); + } +} +``` + +This kind of type creates a close contract between construction and destruction +to simplify both routines by ensuring that the necessary members are valid any +time the destructor would need them to continue executing correctly. + +While this provides a very flexible and convenient set of tools for +construction, it comes at a fairly high cost. There is no way for the language +to _enforce_ any contract between the construction and destruction. I don't see +any way to provide compile time checking that one or the other doesn't make a +mistake. We can verify it dynamically, but at fairly high cost. It also adds +non-trivial complexity. Given that this is merely an ergonomic improvement as +opposed to an expressivity improvement, it isn't at all clear that we should +pursue this until we have clear evidence that the ergonomic need is sufficiently +large. + +In fact, for this kind of realistic example, the `returned` variable approach +appears to be significantly _more_ ergonomic as well as simpler, undermining the +purpose of pursing this direction: + +``` +struct MyContainer(T: Type) { + private var size: Int; + private var capacity: Int; + private var data: Optional(T*); + + // ... public API ... + + impl NecessaryInit { + // Not necessarily suggesting this specific API. + // Just *some* API we pick for doing the necessary init. + // This API assumes that all members with an unformed state + // have their implementation called already, all we need to do is + // adjust to *specific* values anything that is necessary for this + // type. + fn Init(this: Self*) { + this->data = Optional(T*).Empty; + } + } + + // Private helper factored out of the destructor. + fn DestroyElements(data: T*, count: Int) { + for (i: Int = 0 ..< count) { + data[i].Destructor(); + } + } + + fn Create(count: Int, T: value) -> Optional(MyContainer(T)) { + // Try to allocate the storage. + var data: UniquePtr(T) = AllocateRawStorage(count, T); + if (data == null) { + return .Empty; + } + + // Some loop syntax over [0, count). + for (i: Int = 0 ..< count) { + if (not CopyThatMightFail(value, &container.data[i])) { + // Here we have to destroy the copies that succeeded. + // But we can easily share the logic with the destructor. + DestroyElements(data, i); + return .Empty; + } + } + + // Now we can just initialize the object and all invariants + // hold for its destructor. + returned var Optional(MyContainer(T)): result = + .Some((count, count, data.Release())); + + return result; + } + + // However we end up naming this... + fn Destructor(this: Self*) { + if (this->data.IsEmpty()) { + // If the buffer isn't allocated, nothing to do. + return; + } + // The above check is the only thing needed, and it is only needed + // to support an unformed state. Once we get here, everything holds. + DestroyElements(this->data, this->size); + Deallocate(this->data, this->capacity); + } + +} +``` + +With this approach, the necessary steps in the `Create` function are all _local_ +constraints, and changes to the destructor can't break their assumptions. There +is no significant added complexity in practice due to needing to form the entire +object at a single point. Normal techniques for factoring common logic are +easily used to address what little added logic is needed. + +#### Summary of advantages and disadvantages + +Advantages: + +- Allows incrementally assigning meaningful members to an object. +- Simplifies some initialization patterns for objects with large numbers of + members. + +Disadvantages: + +- The simplifications don't scale up to more realistic scenarios. There, they + are eclipsed by the added complexity forced onto the destructor of any such + type. +- The design patterns that simplify the organization of the construction and + destruction logic for these kinds of types are likely to remove the need for + the feature, so it seems better to keep the language simpler.