Skip to content

Commit

Permalink
Editing changes to P3444
Browse files Browse the repository at this point in the history
  • Loading branch information
seanbaxter committed Oct 14, 2024
1 parent c4e41dc commit 7446ab4
Show file tree
Hide file tree
Showing 2 changed files with 14 additions and 12 deletions.
16 changes: 9 additions & 7 deletions docs/P3444R0.html
Original file line number Diff line number Diff line change
Expand Up @@ -898,7 +898,9 @@ <h1 data-number="2" id="second-class-references"><span class="header-section-num
compiler <em>solves the constraint equation</em> to find the liveness of
each <em>loan</em>. All instructions in the MIR are scanned for
<em>conflicting actions</em> with any of the loans in scope at that
point. Conflicting actions raise borrow checker errors.</p>
point. Examples of conflicting actions are stores to places with live
shared borrows or loads from places with live mutable borrows.
Conflicting actions raise borrow checker errors.</p>
<p>The Hylo<span class="citation" data-cites="hylo">[<a href="https://2023.splashcon.org/details/iwaco-2023-papers/5/Borrow-checking-Hylo" role="doc-biblioref">hylo</a>]</span> model is largely equivalent to
this model and it requires borrow checking technology.
<code class="sourceCode cpp">let</code> and
Expand Down Expand Up @@ -928,7 +930,7 @@ <h1 data-number="2" id="second-class-references"><span class="header-section-num
in a <em>coroutine frame</em> so that it’s available to the continuation
function. These <code class="sourceCode cpp">Array</code> subscripts
don’t have instructions after the yield, so the continuation function is
empty and hopefully elided by the optimizer.</p>
empty and hopefully optimized away.</p>
<div class="sourceCode" id="cb13"><pre class="sourceCode cpp"><code class="sourceCode cpp"><span id="cb13-1"><a href="#cb13-1" aria-hidden="true" tabindex="-1"></a><span class="kw">template</span><span class="op">&lt;</span><span class="kw">typename</span> T<span class="op">&gt;</span></span>
<span id="cb13-2"><a href="#cb13-2" aria-hidden="true" tabindex="-1"></a><span class="kw">struct</span> Vec <span class="op">{</span></span>
<span id="cb13-3"><a href="#cb13-3" aria-hidden="true" tabindex="-1"></a> <span class="kw">const</span> T<span class="op">%</span> <span class="kw">operator</span><span class="op">[](</span><span class="dt">size_t</span> idx<span class="op">)</span> <span class="kw">const</span> <span class="op">%</span> safe;</span>
Expand Down Expand Up @@ -973,8 +975,8 @@ <h1 data-number="3" id="other-aspects-of-safety"><span class="header-section-num
<li><strong>Runtime checks</strong> - The compiler automatically emits
runtime bounds checks on array and slice subscripts. It emits checks for
integer divide-by-zero and INT_MIN / -1, which are undefined behavior.
Conforming safe library functions must also implement panics to prevent
out-of-bounds access to heap allocations.</li>
Conforming safe library functions must panic to prevent out-of-bounds
access to heap allocations.</li>
</ol>
<p>Most critically, the <em>safe-specifier</em> is added to a function’s
type. Inside a safe function, only safe operations may be used, unless
Expand Down Expand Up @@ -1019,8 +1021,8 @@ <h1 data-number="4" id="achieving-first-class-references"><span class="header-se
unlocks the mutex. The reference to the mutex is kept in the coroutine
frame. But this still reduces to supporting structs with reference data
members. In this case it’s not a user-defined type, but a
compiler-defined coroutine frame. I feel that the coroutine solution is
an unidiomatic fit for C++ for several reasons: static allocation of the
compiler-defined coroutine frame. The coroutine solution is an
unidiomatic fit for C++ for several reasons: static allocation of the
coroutine frame requires exposing the definition of the coroutine to the
caller, which breaks C++’s approach to modularity; the continuation is
called immediately after the last use of the yielded reference, which
Expand All @@ -1045,7 +1047,7 @@ <h1 data-number="4" id="achieving-first-class-references"><span class="header-se
reference semantics</em>. <code class="sourceCode cpp"><span class="kw">class</span> name <span class="op">%</span>;</code>
is a possible syntax. Inside these classes, you can declare data members
and base classes with safe reference semantics: that includes both safe
reference and other classes with safe reference semantics.</p>
references and other classes with safe reference semantics.</p>
<div class="sourceCode" id="cb14"><pre class="sourceCode cpp"><code class="sourceCode cpp"><span id="cb14-1"><a href="#cb14-1" aria-hidden="true" tabindex="-1"></a><span class="kw">class</span> lock_guard <span class="op">%</span> <span class="op">{</span></span>
<span id="cb14-2"><a href="#cb14-2" aria-hidden="true" tabindex="-1"></a> <span class="co">// Permitted because the containing class has safe reference semantics.</span></span>
<span id="cb14-3"><a href="#cb14-3" aria-hidden="true" tabindex="-1"></a> std2<span class="op">::</span>mutex<span class="op">%</span> mutex;</span>
Expand Down
10 changes: 5 additions & 5 deletions lifetimes/P3444R0.md
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,7 @@ In this fragment, the reference parameters `vec` and `x` serve as _second-class

The safe references presented here are more powerful than second-class references. While they don't support all the capabilities of borrows, they can be returned from functions and made into objects. The compiler must implement borrow checking to support this additional capability.

Borrow checking operates on a function lowering called mid-level IR (MIR). A fresh region variable is provisioned for each local variable with a safe reference type. Dataflow analysis populates each region variable with the liveness of its reference. Assignments and function calls involving references generate _lifetime constraints_. The compiler _solves the constraint equation_ to find the liveness of each _loan_. All instructions in the MIR are scanned for _conflicting actions_ with any of the loans in scope at that point. Conflicting actions raise borrow checker errors.
Borrow checking operates on a function lowering called mid-level IR (MIR). A fresh region variable is provisioned for each local variable with a safe reference type. Dataflow analysis populates each region variable with the liveness of its reference. Assignments and function calls involving references generate _lifetime constraints_. The compiler _solves the constraint equation_ to find the liveness of each _loan_. All instructions in the MIR are scanned for _conflicting actions_ with any of the loans in scope at that point. Examples of conflicting actions are stores to places with live shared borrows or loads from places with live mutable borrows. Conflicting actions raise borrow checker errors.

The Hylo[@hylo] model is largely equivalent to this model and it requires borrow checking technology. `let` and `inout` parameter directives use mutable value semantics to ensure memory safety for objects passed by reference into functions. But Hylo also supports returning references in the form of subscripts:

Expand All @@ -349,7 +349,7 @@ public conformance Array: Collection {
}
```

Subscripts are reference-returning _coroutines_. Coroutines with a single yield point are split into two normal functions: a ramp function that starts at the top and returns the expression of the yield statement, and a continuation function which resumes after the yield and runs to the end. Local state that's live over the yield point must live in a _coroutine frame_ so that it's available to the continuation function. These `Array` subscripts don't have instructions after the yield, so the continuation function is empty and hopefully elided by the optimizer.
Subscripts are reference-returning _coroutines_. Coroutines with a single yield point are split into two normal functions: a ramp function that starts at the top and returns the expression of the yield statement, and a continuation function which resumes after the yield and runs to the end. Local state that's live over the yield point must live in a _coroutine frame_ so that it's available to the continuation function. These `Array` subscripts don't have instructions after the yield, so the continuation function is empty and hopefully optimized away.

```cpp
template<typename T>
Expand All @@ -372,7 +372,7 @@ As detailed in the Safe C++[@safecpp] proposal, there are four categories of mem
1. **Lifetime safety** - This proposal advances a simpler form of safe references that provides safety against use-after-free defects. The feature is complementary with borrow types `T^` that take lifetime arguments. Both types can be used in the same translation unit, and even the same function, without conflict.
2. **Type safety** - Relocation must replace move semantics to eliminate unsafe null pointer exposure. Choice types and pattern matching must be included for safe modeling of optional types.
3. **Thread safety** - The `send` and `sync` interfaces account for which types can be copied and shared between threads.
4. **Runtime checks** - The compiler automatically emits runtime bounds checks on array and slice subscripts. It emits checks for integer divide-by-zero and INT_MIN / -1, which are undefined behavior. Conforming safe library functions must also implement panics to prevent out-of-bounds access to heap allocations.
4. **Runtime checks** - The compiler automatically emits runtime bounds checks on array and slice subscripts. It emits checks for integer divide-by-zero and INT_MIN / -1, which are undefined behavior. Conforming safe library functions must panic to prevent out-of-bounds access to heap allocations.
Most critically, the _safe-specifier_ is added to a function's type. Inside a safe function, only safe operations may be used, unless escaped by an _unsafe-block_.
Expand All @@ -392,11 +392,11 @@ Robust support for user-defined types with reference data members isn't just a c
What are some options for RAII reference semantics?
* Coroutines. This is the Hylo strategy. The ramp function locks a mutex and returns a safe reference to the data within. The continuation unlocks the mutex. The reference to the mutex is kept in the coroutine frame. But this still reduces to supporting structs with reference data members. In this case it's not a user-defined type, but a compiler-defined coroutine frame. I feel that the coroutine solution is an unidiomatic fit for C++ for several reasons: static allocation of the coroutine frame requires exposing the definition of the coroutine to the caller, which breaks C++'s approach to modularity; the continuation is called immediately after the last use of the yielded reference, which runs counter to expectation that cleanup runs at the end of the enclosing scope; and since the continuation is called implicitly, there's nothing textual on the caller side to indicate an unlock.
* Coroutines. This is the Hylo strategy. The ramp function locks a mutex and returns a safe reference to the data within. The continuation unlocks the mutex. The reference to the mutex is kept in the coroutine frame. But this still reduces to supporting structs with reference data members. In this case it's not a user-defined type, but a compiler-defined coroutine frame. The coroutine solution is an unidiomatic fit for C++ for several reasons: static allocation of the coroutine frame requires exposing the definition of the coroutine to the caller, which breaks C++'s approach to modularity; the continuation is called immediately after the last use of the yielded reference, which runs counter to expectation that cleanup runs at the end of the enclosing scope; and since the continuation is called implicitly, there's nothing textual on the caller side to indicate an unlock.
* Defer expressions. Some garbage-collected languages include _defer_ expressions, which run after some condition is met. We could defer a call to the mutex unlock until the end of the enclosing lexical scope. This has the benefit of being explicit to the caller and not requiring computation of a coroutine frame. But it introduces a fundamental new control flow mechanism to the language with applicability that almost perfectly overlaps with destructors.
* Destructors. This is the idiomatic C++ choice. A local object is destroyed when it goes out of scope (or is dropped, with the Safe C++ `drop` keyword). The destructor calls the mutex unlock.
It makes sense to strengthen safe references to support current RAII practice. How do we support safe references as data members? A reasonable starting point is to declare a class as having _safe reference semantics_. `class name %;` is a possible syntax. Inside these classes, you can declare data members and base classes with safe reference semantics: that includes both safe reference and other classes with safe reference semantics.
It makes sense to strengthen safe references to support current RAII practice. How do we support safe references as data members? A reasonable starting point is to declare a class as having _safe reference semantics_. `class name %;` is a possible syntax. Inside these classes, you can declare data members and base classes with safe reference semantics: that includes both safe references and other classes with safe reference semantics.
```cpp
class lock_guard % {
Expand Down

0 comments on commit 7446ab4

Please sign in to comment.