<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" lang xml:lang>
<head>
  <meta charset="utf-8" />
  <meta name="generator" content="pandoc" />
  <!--meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes" / -->
  <title>Reconsidering the std::execution::on algorithm</title>
  <style>
    code{white-space: pre-wrap;}
    span.smallcaps{font-variant: small-caps;}
    span.underline{text-decoration: underline;}
    div.column{display: inline-block; vertical-align: top; width: 50%;}
    div.hanging-indent{margin-left: 1.5em; text-indent: -1.5em;}
    ul.task-list{list-style: none;}
    pre > code.sourceCode { white-space: pre; position: relative; }
    pre > code.sourceCode > span { display: inline-block; line-height: 1.25; }
    pre > code.sourceCode > span:empty { height: 1.2em; }
    code.sourceCode > span { color: inherit; text-decoration: inherit; }
    div.sourceCode { margin: 1em 0; }
    pre.sourceCode { margin: 0; }
    @media screen {
    div.sourceCode { overflow: auto; }
    }
    @media print {
    pre > code.sourceCode { white-space: pre-wrap; }
    pre > code.sourceCode > span { text-indent: -5em; padding-left: 5em; }
    }
    pre.numberSource code
      { counter-reset: source-line 0; }
    pre.numberSource code > span
      { position: relative; left: -4em; counter-increment: source-line; }
    pre.numberSource code > span > a:first-child::before
      { content: counter(source-line);
        position: relative; left: -1em; text-align: right; vertical-align: baseline;
        border: none; display: inline-block;
        -webkit-touch-callout: none; -webkit-user-select: none;
        -khtml-user-select: none; -moz-user-select: none;
        -ms-user-select: none; user-select: none;
        padding: 0 4px; width: 4em;
        color: #aaaaaa;
      }
    pre.numberSource { margin-left: 3em; border-left: 1px solid #aaaaaa;  padding-left: 4px; }
    div.sourceCode
      {   }
    @media screen {
    pre > code.sourceCode > span > a:first-child::before { text-decoration: underline; }
    }
    code span.al { color: #ff0000; font-weight: bold; } /* Alert */
    code span.an { color: #60a0b0; font-weight: bold; font-style: italic; } /* Annotation */
    code span.at { color: #7d9029; } /* Attribute */
    code span.bn { color: #40a070; } /* BaseN */
    code span.bu { } /* BuiltIn */
    code span.cf { color: #007020; font-weight: bold; } /* ControlFlow */
    code span.ch { color: #4070a0; } /* Char */
    code span.cn { color: #880000; } /* Constant */
    code span.co { color: #60a0b0; font-style: italic; } /* Comment */
    code span.cv { color: #60a0b0; font-weight: bold; font-style: italic; } /* CommentVar */
    code span.do { color: #ba2121; font-style: italic; } /* Documentation */
    code span.dt { color: #902000; } /* DataType */
    code span.dv { color: #40a070; } /* DecVal */
    code span.er { color: #ff0000; font-weight: bold; } /* Error */
    code span.ex { } /* Extension */
    code span.fl { color: #40a070; } /* Float */
    code span.fu { color: #06287e; } /* Function */
    code span.im { } /* Import */
    code span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Information */
    code span.kw { color: #007020; font-weight: bold; } /* Keyword */
    code span.op { color: #666666; } /* Operator */
    code span.ot { color: #007020; } /* Other */
    code span.pp { color: #bc7a00; } /* Preprocessor */
    code span.sc { color: #4070a0; } /* SpecialChar */
    code span.ss { color: #bb6688; } /* SpecialString */
    code span.st { color: #4070a0; } /* String */
    code span.va { color: #19177c; } /* Variable */
    code span.vs { color: #4070a0; } /* VerbatimString */
    code span.wa { color: #60a0b0; font-weight: bold; font-style: italic; } /* Warning */
  </style>
  <style type="text/css">body { max-width: 800px;
text-align: justify;
margin: auto
}
h1.title {text-align: center}
p {text-align:justify}
p.subtitle {
text-align: center;
font-weight: bold;
font-size: 1.4em;
}
li {text-align:justify}
blockquote.note {
background-color:#E0E0E0;
padding-left: 15px;
padding-right: 15px;
padding-top: 1px;
padding-bottom: 1px;
}
span.note:before {
content: "[Note: ";
font-style: italic;
}
span.note:after {
content: " -- end note]";
font-style: italic;
}
span.ednote:before {
content: "[Editorial note: ";
font-style: italic;
}
span.ednote:after {
content: " -- end note]";
font-style: italic;
}
span.ednote, span.ednote * {
color:blue !important;
margin-top: 0em;
margin-bottom: 0em;
}
ins, ins * {color:#00A000 !important}
del, del * {color:#A00000 !important}
div.ins, div.ins * {
color:#00A000;
text-decoration-line: none;
}
div.del, div.del * {
color:#A00000;
text-decoration-line: none;
}
dfn {
font-style: italic;
font-weight: bold;
}
code:not(sourceCode) {
white-space: normal;
font-size: 80% !important;
}
ins > code:not(sourceCode) {
white-space: normal;
font-size: 80% !important;
}
div.sourceCode {
margin-left: 20pt !important;
}
</style>
  <!--[if lt IE 9]>
    <script src="//cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv-printshiv.min.js"></script>
  <![endif]-->
</head>
<body>
<header id="title-block-header">
<h1 class="title">Reconsidering the <code>std::execution::on</code> algorithm</h1>
<p class="subtitle"><code>on</code> <em>second thought</em></p>
</header>
<table style="margin-left: auto; margin-right: 0; width: 40%; text-align: left; font-size: 90%;">
<tr>
<td>
<strong>Authors:</strong>
</td>
<td>
<a href="mailto:eric.niebler@gmail.com">Eric Niebler</a>
</td>
</tr>
<tr>
<td>
<strong>Date:</strong>
</td>
<td>
June 24, 2024
</td>
</tr>
<tr>
<td>
<strong>Source:</strong>
</td>
<td>
<a href="https://github.com/ericniebler/wg21/blob/main/P3175/P3175.md">GitHub</a>
</td>
</tr>
<tr>
<td>
<strong>Issue tracking:</strong>
</td>
<td>
<a href="https://github.com/ericniebler/wg21/issues">GitHub</a>
</td>
</tr>
<tr>
<td style=" vertical-align: top;">
<strong>Project:</strong>
</td>
<td>
ISO/IEC JTC1/SC22/WG21 14882:<br />Programming Language — C++
</td>
</tr>
<tr>
<td>
<strong>Audience:</strong>
</td>
<td>
LEWG
</td>
</tr>
</table>
<h2 id="synopsis">Synopsis</h2>
<p>Usage experience with P2300 has revealed a gap between users’ expectations and the actual behavior of the <code>std::execution::on</code> algorithm. This paper seeks to close that gap by making its behavior less surprising.</p>
<h2 id="executive-summary">Executive Summary</h2>
<p>Below are the specific changes this paper proposes:</p>
<ol type="1">
<li><p>Rename the current <code>std::execution::on</code> algorithm to <code>std::execution::starts_on</code>.</p></li>
<li><p>Rename <code>std::execution::transfer</code> to <code>std::execution::continues_on</code></p></li>
<li><p>Optional: Add a new algorithm <code>std::execution::on</code> that, like <code>starts_on</code>, starts a sender on a particular context, but that remembers where execution is transitioning <em>from</em>. After the sender completes, the <code>on</code> algorithm transitions <em>back</em> to the starting execution context, giving a scoped, there-and-back-again behavior.</p></li>
<li><p>Optional: Add a form of <code>execution::on</code> that lets you run part of a <em>continuation</em> on one scheduler, automatically transitioning back to the starting context.</p></li>
</ol>
<h2 id="revisions">Revisions</h2>
<ul>
<li><p><b>R3:</b></p>
<ul>
<li><p>Give the <code>on</code> algorithm a standard tag type again.</p></li>
<li><p>Rename <code>start_on</code> and <code>continue_on</code> to <code>starts_on</code> and <code>continues_on</code>.</p></li>
<li><p>Fix a bug in the specification of the 2-argument form of <code>on</code> (replacing the incorrect <code>get_scheduler(rcvr)</code> with the correct <code>get_scheduler(get_env(rcvr))</code>).</p></li>
<li><p>Improve the prose descriptions of the two forms of the <code>on</code> algorithm in [exec.on]/p1.</p></li>
<li><p>Rename the exposition-only <em><code>none-such</code></em> type to <em><code>not-a-scheduler</code></em>.</p></li>
<li><p>Add an editorial note that LEWG would like the semantic constraints on customizations of <code>on</code> specified differently.</p></li>
</ul></li>
<li><p><b>R2:</b></p>
<ul>
<li><p>Give the <code>on</code> algorithm an unspecified tag type to discourage its customization. See discussion in <a href="#on-the-customizability-of-the-new-executionon-algorithm">On the customizability of the new <code>execution::on</code> algorithm</a>.</p></li>
<li><p>Place strict constraints on <code>on</code> customizations so that they have the correct semantics.</p></li>
<li><p>Adds a discussion about naming for the <code>starts_on</code> and <code>continues_on</code> algorithms. See the discussion in <a href="#on-the-naming-of-starts_on-and-continues_on">On the naming of <code>starts_on</code> and <code>continues_on</code></a>.</p></li>
</ul></li>
<li><p><b>R1:</b></p>
<ul>
<li><p>Makes the <code>write_env</code> adaptor exposition-only, removes the <code>finally</code> and <code>unstoppable</code> adaptors, and reverts the changes to <code>schedule_from</code> and the <code>let_</code> algorithms.</p></li>
<li><p>A follow-on paper, <a href="https://isocpp.org/files/papers/P3284R0.html">P3284</a>, will propose to add <code>write_env</code>, <code>unstoppable</code>, and <code>finally</code> as proper members of the Standard Library.</p></li>
</ul></li>
<li><p><b>R0:</b></p>
<ul>
<li>Initial revision</li>
</ul></li>
</ul>
<h2 id="problem-description">Problem Description</h2>
<p>If, knowing little about senders and sender algorithms, someone showed you code such as the following:</p>
<div class="sourceCode" id="cb1"><pre class="sourceCode cpp"><code class="sourceCode cpp"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true"></a><span class="kw">namespace</span> ex = <span class="bu">std::</span>execution;</span>
<span id="cb1-2"><a href="#cb1-2" aria-hidden="true"></a></span>
<span id="cb1-3"><a href="#cb1-3" aria-hidden="true"></a>ex::sender <span class="kw">auto</span> work1 = ex::just()</span>
<span id="cb1-4"><a href="#cb1-4" aria-hidden="true"></a>                      | ex::transfer(scheduler_A);</span>
<span id="cb1-5"><a href="#cb1-5" aria-hidden="true"></a></span>
<span id="cb1-6"><a href="#cb1-6" aria-hidden="true"></a>ex::sender <span class="kw">auto</span> work2 = ex::on(scheduler_B, <span class="bu">std::</span>move(work1))</span>
<span id="cb1-7"><a href="#cb1-7" aria-hidden="true"></a>                      | ex::then([] { <span class="bu">std::</span>puts(<span class="st">&quot;hello world!&quot;</span>); });</span>
<span id="cb1-8"><a href="#cb1-8" aria-hidden="true"></a></span>
<span id="cb1-9"><a href="#cb1-9" aria-hidden="true"></a>ex::sender <span class="kw">auto</span> work3 = ex::on(scheduler_C, <span class="bu">std::</span>move(work2))</span>
<span id="cb1-10"><a href="#cb1-10" aria-hidden="true"></a></span>
<span id="cb1-11"><a href="#cb1-11" aria-hidden="true"></a><span class="bu">std::</span>this_thread<span class="bu">::</span>sync_wait(<span class="bu">std::</span>move(work3));</span></code></pre></div>
<p>… and asked you, which scheduler, <code>scheduler_A</code> or <code>scheduler_B</code>, is used to execute the code that prints <code>&quot;hello world!&quot;</code>? You might reasonably think the answer is <code>scheduler_C</code>. Your reasoning would go something like this:</p>
<blockquote>
<p>Well clearly the first thing we execute is <code>on(scheduler_C, work2)</code>. I’m pretty sure that is going to execute <code>work2</code> on <code>scheduler_C</code>. The <code>printf</code> is a part of <code>work2</code>, so I’m going to guess that it executes on <code>scheduler_C</code>.</p>
</blockquote>
<p>This paper exists because the <code>on</code> algorithm as specified in P2300R8 does <em>not</em> print <code>&quot;hello world!&quot;</code> from <code>scheduler_C</code>. It prints it from <code>scheduler_A</code>. Surprise!</p>
<div style="text-align: center">
<strong><em>But why?</em></strong>
</div>
<p><code>work2</code> executes <code>work1</code> on <code>scheduler_B</code>. <code>work1</code> then rather rudely transitions to <code>scheduler_A</code> and doesn’t transition back. The <code>on</code> algorithm is cool with that. It just happily runs its continuation inline, <em>still on <code>scheduler_A</code></em>, which is where <code>&quot;hello world!&quot;</code> is printed from.</p>
<p>If there was more work tacked onto the end of <code>work3</code>, it too would execute on <code>scheduler_A</code>.</p>
<h3 id="user-expectations">User expectations</h3>
<p>The authors of P2300 have witnessed this confusion in the wild. And when this author has asked his programmer friends about the code above, every single one said they expected behavior different from what is specified. This is very concerning.</p>
<p>However, if we change some of the algorithm names, people are less likely to make faulty assumptions about their behavior. Consider the above code with different names:</p>
<div class="sourceCode" id="cb2"><pre class="sourceCode cpp"><code class="sourceCode cpp"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true"></a><span class="kw">namespace</span> ex = <span class="bu">std::</span>execution;</span>
<span id="cb2-2"><a href="#cb2-2" aria-hidden="true"></a></span>
<span id="cb2-3"><a href="#cb2-3" aria-hidden="true"></a>ex::sender <span class="kw">auto</span> work1 = ex::just()</span>
<span id="cb2-4"><a href="#cb2-4" aria-hidden="true"></a>                      | ex::continues_on(scheduler_A);</span>
<span id="cb2-5"><a href="#cb2-5" aria-hidden="true"></a></span>
<span id="cb2-6"><a href="#cb2-6" aria-hidden="true"></a>ex::sender <span class="kw">auto</span> work2 = ex::starts_on(scheduler_B, <span class="bu">std::</span>move(work1))</span>
<span id="cb2-7"><a href="#cb2-7" aria-hidden="true"></a>                      | ex::then([] { <span class="bu">std::</span>puts(<span class="st">&quot;hello world!&quot;</span>); });</span>
<span id="cb2-8"><a href="#cb2-8" aria-hidden="true"></a></span>
<span id="cb2-9"><a href="#cb2-9" aria-hidden="true"></a>ex::sender <span class="kw">auto</span> work3 = ex::starts_on(scheduler_C, <span class="bu">std::</span>move(work2))</span>
<span id="cb2-10"><a href="#cb2-10" aria-hidden="true"></a></span>
<span id="cb2-11"><a href="#cb2-11" aria-hidden="true"></a><span class="bu">std::</span>this_thread<span class="bu">::</span>sync_wait(<span class="bu">std::</span>move(work3));</span></code></pre></div>
<p>Now the behavior is a little more clear. The names <code>starts_on</code> and <code>continues_on</code> both suggest a one-way execution context transition, which matches their specified behavior.</p>
<h3 id="filling-the-gap">Filling the gap</h3>
<p><code>on</code> fooled people into thinking it was a there-and-back-again algorithm. We propose to fix that by renaming it to <code>starts_on</code>. But what of the people who <em>want</em> a there-and-back-again algorithm?</p>
<p>Asynchronous work is better encapsulated when it completes on the same execution context that it started on. People are surprised, and reasonably so, if they <code>co_await</code> a task from a CPU thread pool and get resumed on, say, an OS timer thread. Yikes!</p>
<p>We have an opportunity to give the users of P2300 what they <em>thought</em> they were already getting, and now the right name is available: <strong><code>on</code></strong>.</p>
<p>We propose to add a new algorithm, called <code>on</code>, that remembers where execution came from and automatically transitions back there. Its operational semantics can be easily expressed in terms of the existing P2300 algorithms. It is approximately the following:</p>
<div class="sourceCode" id="cb3"><pre class="sourceCode cpp"><code class="sourceCode cpp"><span id="cb3-1"><a href="#cb3-1" aria-hidden="true"></a><span class="kw">template</span> &lt;ex::scheduler Sched, ex::sender Sndr&gt;</span>
<span id="cb3-2"><a href="#cb3-2" aria-hidden="true"></a>sender <span class="kw">auto</span> on(Sched sch, Sndr sndr) {</span>
<span id="cb3-3"><a href="#cb3-3" aria-hidden="true"></a>  <span class="cf">return</span> ex::read(ex::get_scheduler)</span>
<span id="cb3-4"><a href="#cb3-4" aria-hidden="true"></a>       | ex::let_value([=](<span class="kw">auto</span> orig_sch) {</span>
<span id="cb3-5"><a href="#cb3-5" aria-hidden="true"></a>           <span class="cf">return</span> ex::starts_on(sch, sndr)</span>
<span id="cb3-6"><a href="#cb3-6" aria-hidden="true"></a>                | ex::continues_on(orig_sch);</span>
<span id="cb3-7"><a href="#cb3-7" aria-hidden="true"></a>         });</span>
<span id="cb3-8"><a href="#cb3-8" aria-hidden="true"></a>}</span></code></pre></div>
<h3 id="one-step-further">One step further?</h3>
<p>Once we recast <code>on</code> as a there-and-back-again algorithm, it opens up the possibility of another there-and-back-again algorithm, one that executes a part of a <em>continuation</em> on a given scheduler. Consider the following code, where <code>async_read_file</code> and <code>async_write_file</code> are functions that return senders (description after the break):</p>
<div class="sourceCode" id="cb4"><pre class="sourceCode cpp"><code class="sourceCode cpp"><span id="cb4-1"><a href="#cb4-1" aria-hidden="true"></a>ex::sender <span class="kw">auto</span> work = async_read_file()</span>
<span id="cb4-2"><a href="#cb4-2" aria-hidden="true"></a>                     | ex::on(cpu_pool, ex::then(crunch_numbers))</span>
<span id="cb4-3"><a href="#cb4-3" aria-hidden="true"></a>                     | ex::let_value([](<span class="kw">auto</span> numbers) {</span>
<span id="cb4-4"><a href="#cb4-4" aria-hidden="true"></a>                         <span class="cf">return</span> async_write_file(numbers);</span>
<span id="cb4-5"><a href="#cb4-5" aria-hidden="true"></a>                       });</span></code></pre></div>
<p>Here, we read a file and then send it to an <code>on</code> sender. This would be a different overload of <code>on</code>, one that takes a sender, a scheduler, and a continuation. It saves the result of the sender, transitions to the given scheduler, and then forwards the results to the continuation, <code>then(crunch_numbers)</code>. After that, it returns to the previous execution context where it executes the <code>async_write_file(numbers)</code> sender.</p>
<p>The above would be roughly equivalent to:</p>
<div class="sourceCode" id="cb5"><pre class="sourceCode cpp"><code class="sourceCode cpp"><span id="cb5-1"><a href="#cb5-1" aria-hidden="true"></a>ex::sender <span class="kw">auto</span> work = async_read_file()</span>
<span id="cb5-2"><a href="#cb5-2" aria-hidden="true"></a>                     | ex::let_value([=](<span class="kw">auto</span> numbers) {</span>
<span id="cb5-3"><a href="#cb5-3" aria-hidden="true"></a>                         ex::sender <span class="kw">auto</span> work = ex::just(numbers)</span>
<span id="cb5-4"><a href="#cb5-4" aria-hidden="true"></a>                                              | ex::then(crunch_numbers);</span>
<span id="cb5-5"><a href="#cb5-5" aria-hidden="true"></a>                         <span class="cf">return</span> ex::on(cpu_pool, work)</span>
<span id="cb5-6"><a href="#cb5-6" aria-hidden="true"></a>                              | ex::let_value([=](<span class="kw">auto</span> numbers) {</span>
<span id="cb5-7"><a href="#cb5-7" aria-hidden="true"></a>                                  <span class="cf">return</span> async_write_file(numbers);</span>
<span id="cb5-8"><a href="#cb5-8" aria-hidden="true"></a>                                });</span>
<span id="cb5-9"><a href="#cb5-9" aria-hidden="true"></a>                       });</span></code></pre></div>
<p>This form of <code>on</code> would make it easy to, in the middle of a pipeline, pop over to another execution context to do a bit of work and then automatically pop back when it is done.</p>
<h2 id="implementation-experience">Implementation Experience</h2>
<p>The perennial question: has it been implemented? It has been implemented in stdexec for over a year, modulo the fact that <code>stdexec::on</code> has the behavior as specified in P2300R8, and a new algorithm <code>exec::on</code> has the there-and-back-again behavior proposed in this paper.</p>
<h2 id="design-considerations">Design Considerations</h2>
<h3 id="do-we-really-have-to-rename-the-transfer-algorithm">Do we really have to rename the <code>transfer</code> algorithm?</h3>
<p>We don’t! Within sender expressions, <code>work | transfer(over_there)</code> reads a bit nicer than <code>work | continues_on(over_there)</code>, and taken in isolation the name change is strictly for the worse.</p>
<p>However, the symmetry of the three operations:</p>
<ul>
<li><code>starts_on</code></li>
<li><code>continues_on</code></li>
<li><code>on</code></li>
</ul>
<p>… encourages developers to infer their semantics correctly. The first two are one-way transitions before and after a piece of work, respectively; the third book-ends work with transitions. In the author’s opinion, this consideration outweighs the other.</p>
<h3 id="do-we-need-the-additional-form-of-on">Do we need the additional form of <code>on</code>?</h3>
<p>We don’t! Users can build it themselves from the other pieces of P2300 that will ship in C++26. But the extra overload makes it much simpler for developers to write well-behaved asynchronous operations that complete on the same execution contexts they started on, which is why it is included here.</p>
<h3 id="what-happens-if-theres-no-scheduler-for-on-to-go-back-to">What happens if there’s no scheduler for <code>on</code> to go back to?</h3>
<p>If we recast <code>on</code> as a there-and-back-again algorithm, the implication is that the receiver that gets <code>connect</code>-ed to the <code>on</code> sender must know the current scheduler. If it doesn’t, the code will not compile because there is no scheduler to go back to.</p>
<p>Passing an <code>on</code> sender to <code>sync_wait</code> will work because <code>sync_wait</code> provides a <code>run_loop</code> scheduler as the current scheduler. But what about algorithms like <code>start_detached</code> and <code>spawn</code> from <a href="https://wg21.link/P3149">P3149</a>? Those algorithms connect the input sender with a receiver whose environment lacks a value for the <code>get_scheduler</code> query. As specified in this paper, those algorithms will reject <code>on</code> senders, which is bad from a usability point of view.</p>
<p>There are a number of possible solutions to this problem:</p>
<ol type="1">
<li><p>Any algorithm that eagerly <code>connect</code>s a sender should take an environment as an optional extra argument. That way, users have a way to tell the algorithm what the current scheduler is. They can also pass additional information like allocators and stop tokens. <b>UPDATE:</b> On 2024-05-21, straw polling indicated that LEWG would like to see a paper proposing this.</p></li>
<li><p>Those algorithms can specify a so-called “inline” scheduler as the current scheduler, essentially causing the <code>on</code> sender to perform a no-op transition when it completes. <b>UPDATE:</b> On 2024-05-21, LEWG opted to not pursue this option.</p></li>
<li><p>Those algorithms can treat top-level <code>on</code> senders specially by converting them to <code>start_on</code> senders. <b>UPDATE:</b> On 2024-05-21, LEWG opted to not pursue this option.</p></li>
<li><p>Those algorithms can set a hidden, non-forwarding “root” query in the environment. The <code>on</code> algorithm can test for this query and, if found, perform a no-op transition when it completes. This has the advantage of not setting a “current” scheduler, which could interfere with the behavior of nested senders. <b>UPDATE:</b> On 2024-05-21, LEWG opted to not pursue this option.</p></li>
</ol>
<h2 id="questions-for-lewgs-consideration">Questions for LEWG’s consideration</h2>
<p>The author would like LEWG’s feedback on the following two questions:</p>
<ol type="1">
<li><p>If <code>on</code> is renamed <code>starts_on</code>, do we also want to rename <code>transfer</code> to <code>continues_on</code>? <b>UPDATE:</b> On 2024-05-13, LEWG straw polling answered this question in the affirmative.</p></li>
<li><p>If <code>on</code> is renamed <code>starts_on</code>, do we want to add a new algorithm named <code>on</code> that book-ends a piece of work with transitions to and from a scheduler? <b>UPDATE:</b> On 2024-05-13, LEWG straw polling answered this question in the affirmative.</p></li>
<li><p>If we want the new scoped form of <code>on</code>, do we want to add the <code>on(sndr,     sch, continuation)</code> algorithm overload to permit scoped execution of continuations? <b>UPDATE:</b> On 2024-05-13, LEWG straw polling answered this question in the affirmative.</p></li>
</ol>
<h2 id="on-the-customizability-of-the-new-executionon-algorithm">On the customizability of the new <code>execution::on</code> algorithm</h2>
<p>On the 2024-05-21 telecon, LEWG requested to see a revision of this paper that removes the customizability of the proposed <code>execution::on</code> algorithm. The author agrees with this guidance in principle: the behavior of <code>on</code> should be expressed in terms of <code>starts_on</code> and <code>continues_on</code>, and users should be customizing those instead.</p>
<p>However, the author now realizes that to ban customization of <code>on</code> would make it impossible to write a recursive sender tree transformation without intrusive design changes to P2300. Consider that the author of an execution domain <code>D</code> might want a transformation to be applied to every sender in an expression tree. They would like for this expression:</p>
<div class="sourceCode" id="cb6"><pre class="sourceCode cpp"><code class="sourceCode cpp"><span id="cb6-1"><a href="#cb6-1" aria-hidden="true"></a><span class="bu">std::</span>execution<span class="bu">::</span>transform_sender(D(), <span class="bu">std::</span>execution<span class="bu">::</span>on(sch, child), env);</span></code></pre></div>
<p>to be equivalent to:</p>
<div class="sourceCode" id="cb7"><pre class="sourceCode cpp"><code class="sourceCode cpp"><span id="cb7-1"><a href="#cb7-1" aria-hidden="true"></a><span class="bu">std::</span>execution<span class="bu">::</span>on(sch, <span class="bu">std::</span>execution<span class="bu">::</span>transform_sender(D(), child, env));</span></code></pre></div>
<p>The ability to crack open a sender, transform the children, and reassemble the sender is essential for these sorts of recursive transformations, but that ability <em>also</em> permits other, more general transformations. The author strongly feels that disallowing transformations of <code>on</code> would be a step in the wrong direction.</p>
<p>However, there are a few things we can do to discourage users from customizing <code>on</code> in ways we disapprove.</p>
<ol type="1">
<li><p>Give the <code>on</code> algorithm an unspecified tag type so that it is a little awkward to single the <code>on</code> algorithm out for special treatment by a domain’s <code>transform_sender</code>.</p></li>
<li><p>Place strict requirements on customizations of <code>on</code> to ensure correct program semantics in the presence of customizations. Violations of these requirements would lead to undefined behavior.</p></li>
</ol>
<p>These changes have been applied as of revision 2 of this paper.</p>
<h2 id="on-the-naming-of-starts_on-and-continues_on">On the naming of <code>starts_on</code> and <code>continues_on</code></h2>
<p>In a previous revision of the paper, the <code>starts_on</code> algorithm was named “<code>start_on</code>”. It was pointed out in the 2024-05-14 LEWG telecon, and again on 2024-05-21, that the name <code>start_on</code> is potentially confusing given that “<code>start</code>” in P2300 means “start <em>now</em>.” The <code>start_on</code> algorithm does not mean “start now”; it means, “<em>when</em> the work is started, start it <em>there</em>.”</p>
<p>The authors of P2300 made the following suggestions:</p>
<ul>
<li>Rename <code>start_on</code> to <code>starts_on</code>.</li>
<li>Rename <code>continue_on</code> to <code>continues_on</code>.</li>
</ul>
<p>The naming of these algorithms was discussed by LEWG on 2024-06-24. LEWG decided on the names “<code>starts_on</code>” and “<code>continues_on</code>”.</p>
<h2 id="proposed-wording">Proposed Wording</h2>
<p><span class="ednote">The wording in this section is based on <a href="https://wg21.link/P2300R9">P2300R9</a> with the addition of <a href="https://wg21.link/P2855R1">P8255R1</a>.</span></p>
<p><span class="ednote">Change [exec.syn] as follows:</span></p>
<blockquote>
<pre style="white-space: pre-wrap; font-size: 85%; text-align: left;">
...
  struct <ins>starts_</ins>on_t;
  struct <del>transfer_t</del><ins>continues_on_t</ins>;
  <ins>struct on_t;</ins>
  struct schedule_from_t;
...

  inline constexpr <ins>starts_</ins>on_t <ins>starts_</ins>on{};</ins>
  inline constexpr <del>transfer_t transfer</del><ins>continues_on_t continues_on</ins>{};
  <ins>inline constexpr on_t on{};</ins>
  inline constexpr schedule_from_t schedule_from{};
</pre>
</blockquote>
<p><span class="ednote">Add a new paragraph (15) to section [exec.snd.general], paragraph 3 as follows:</span></p>
<div class="ins">
<blockquote>
<ol start="15" type="1">
<li><pre style="white-space: pre-wrap; font-size: 85%; text-align: left;">
template&lt;sender Sndr, queryable Env&gt;
constexpr auto <i>write-env</i>(Sndr&amp;&amp; sndr, Env&amp;&amp; env); // exposition only
</pre>
<ol type="1">
<li><p><i><code>write-env</code></i> is an exposition-only sender adaptor that, when connected with a receiver <code>rcvr</code>, connects the adapted sender with a receiver whose execution environment is the result of joining the <code>queryable</code> argument <code>env</code> to the result of <code>get_env(rcvr)</code>.</p></li>
<li><p>Let <i><code>write-env-t</code></i> be an exposition-only empty class type.</p></li>
<li><p><em>Returns:</em> <code><i>make-sender</i>(<i>make-env-t</i>(), std::forward&lt;Env&gt;(env), std::forward&lt;Sndr&gt;(sndr))</code>.</p></li>
<li><p><em>Remarks:</em> The exposition-only class template <i><code>impls-for</code></i> ([exec.snd.general]) is specialized for <i><code>write-env-t</code></i> as follows:</p>
<pre style="white-space: pre-wrap; font-size: 85%; text-align: left;">
 template&lt;&gt;
 struct <i>impls-for</i>&lt;<i>write-env-t</i>&gt; : <i>default-impls</i> {
   static constexpr auto <em>get-env</em> =
     [](auto, const auto&amp; state, const auto&amp; rcvr) noexcept {
       return <em>JOIN-ENV</em>(state, get_env(rcvr));
     };
 };
 </pre></li>
</ol></li>
</ol>
</blockquote>
</div>
<p><span class="ednote">Change subsection “<code>execution::on</code> [exec.on]” to “<code>execution::starts_on</code> [exec.starts.on]”, and within that subsection, replace every instance of “<code>on</code>” with “<code>starts_on</code>” and every instance of “<code>on_t</code>” with “<code>starts_on_t</code>”.</span></p>
<p><span class="ednote">Change subsection “<code>execution::transfer</code> [exec.transfer]” to “<code>execution::continues_on</code> [exec.continues.on]”, and within that subsection, replace every instance of “<code>transfer</code>” with “<code>continues_on</code>” and every instance of “<code>transfer_t</code>” with “<code>continues_on_t</code>”.</span></p>
<p><span class="ednote">Insert a new subsection “<code>execution::on</code> [exec.on]” as follows:</span></p>
<div class="ins">
<blockquote>
<h4 id="executionon-exec.on"><code>execution::on</code> <b>[exec.on]</b></h4>
<ol type="1">
<li><p>The <code>on</code> sender adaptor has two forms:</p>
<ul>
<li><p><code>on(sch, sndr)</code>, which starts a sender <code>sndr</code> on an execution agent belonging to a scheduler <code>sch</code>’s associated execution resource and that, upon <code>sndr</code>’s completion, transfers execution back to the execution resource on which the <code>on</code> sender was started.</p></li>
<li><p><code>on(sndr, sch, closure)</code>, which upon completion of a sender <code>sndr</code>, transfers execution to an execution agent belonging to a scheduler <code>sch</code>’s associated execution resource, then executes a sender adaptor closure <code>closure</code> with the async results of the sender, and that then transfers execution back to the execution resource on which <code>sndr</code> completed.</p></li>
</ul></li>
<li><p>The name <code>on</code> denotes a customization point object. For some subexpressions <code>sch</code> and <code>sndr</code>, if <code>decltype((sch))</code> does not satisfy <code>scheduler</code>, or <code>decltype((sndr))</code> does not satisfy <code>sender</code>, <code>on(sch, sndr)</code> is ill-formed.</p></li>
<li><p>Otherwise, the expression <code>on(sch, sndr)</code> is expression-equivalent to:</p>
<pre style="white-space: pre-wrap; font-size: 85%; text-align: left;">
 transform_sender(
   <em>query-or-default</em>(get_domain, sch, default_domain()),
   <em>make-sender</em>(on, sch, sndr));
 </pre></li>
<li><p>For a subexpression <code>closure</code>, if <code>decltype((closure))</code> is not a sender adaptor closure object ([exec.adapt.objects]), the expression <code>on(sndr, sch,     closure)</code> is ill-formed; otherwise, it is expression-equivalent to:</p>
<pre style="white-space: pre-wrap; font-size: 85%; text-align: left;">
 transform_sender(
   <em>get-domain-early</em>(sndr),
   <em>make-sender</em>(on, pair{sch, closure}, sndr));
 </pre></li>
<li><p>Let <code>out_sndr</code> and <code>env</code> be subexpressions, let <code>OutSndr</code> be <code>decltype((out_sndr))</code>, and let <code>Env</code> be <code>decltype((env))</code>. If <code><em>sender-for</em>&lt;OutSndr, on_t&gt;</code> is <code>false</code>, then the expressions <code>on.transform_env(out_sndr, env)</code> and <code>on.transform_sender(out_sndr, env)</code> are ill-formed; otherwise:</p>
<ol type="1">
<li><p>Let <em><code>not-a-scheduler</code></em> be an unspecified empty class type, and let <em><code>not-a-sender</code></em> be the exposition-only type:</p>
<pre style="white-space: pre-wrap; font-size: 85%; text-align: left;">
 struct <em>not-a-sender</em> {
   using sender_concept = sender_t;

   auto get_completion_signatures(auto&amp;&amp;) const {
     return <em>see below</em>;
   }
 };
 </pre>
<p>… where the member function <code>get_completion_signatures</code> returns an object of a type that is not a specialization of the <code>completion_signatures</code> class template.</p></li>
<li><p><code>on.transform_env(out_sndr, env)</code> is equivalent to:</p>
<pre style="white-space: pre-wrap; font-size: 85%; text-align: left;">
 auto&amp;&amp; [ign1, data, ign2] = out_sndr;
 if constexpr (scheduler&lt;decltype(data)&gt;) {
   return <em>JOIN-ENV</em>(<em>SCHED-ENV</em>(data), <em>FWD-ENV</em>(std::forward&lt;Env&gt;(env)));
 } else {
   return std::forward&lt;Env&gt;(env);
 }
 </pre></li>
<li><p><code>on.transform_sender(out_sndr, env)</code> is equivalent to:</p>
<pre style="white-space: pre-wrap; font-size: 85%; text-align: left;">
 auto&amp;&amp; [ign, data, sndr] = out_sndr;
 if constexpr (scheduler&lt;decltype(data)&gt;) {
   auto orig_sch =
     <em>query-with-default</em>(get_scheduler, env, <em>not-a-scheduler</em>());

   if constexpr (same_as&lt;decltype(orig_sch), <em>not-a-scheduler</em>&gt;) {
     return <em>not-a-sender</em>{};
   } else {
     return continues_on(
       starts_on(std::forward_like&lt;OutSndr&gt;(data), std::forward_like&lt;OutSndr&gt;(sndr)),
       std::move(orig_sch));
   }
 } else {
   auto&amp;&amp; [sch, closure] = std::forward_like&lt;OutSndr&gt;(data);
   auto orig_sch = <em>query-with-default</em>(
     get_completion_scheduler&lt;set_value_t&gt;,
     get_env(sndr),
     <em>query-with-default</em>(get_scheduler, env, <em>not-a-scheduler</em>()));

   if constexpr (same_as&lt;decltype(orig_sch), <em>not-a-scheduler</em>&gt;) {
     return <em>not-a-sender</em>{};
   } else {
     return <em>write-env</em>(
       continues_on(
         std::forward_like&lt;OutSndr&gt;(closure)(
           continues_on(
             <em>write-env</em>(std::forward_like&lt;OutSndr&gt;(sndr), <em>SCHED-ENV</em>(orig_sch)),
             sch)),
         orig_sch),
       <em>SCHED-ENV</em>(sch));
   }
 }
 </pre></li>
<li><p><em>Recommended practice:</em> Implementations should use the return type of <code><em>not-a-sender</em>::get_completion_signatures</code> to inform users that their usage of <code>on</code> is incorrect because there is no available scheduler onto which to restore execution.</p></li>
</ol></li>
</ol>
<p><span class="ednote">The following two paragraphs are new in R2. <b>UPDATE</b> (2024-06-24): LEWG is uncomfortable with specifying the semantic requirements of <code>on</code> customizations in terms of “semantic equivalence” to the lowered expressions. LEWG would like to clarify what effects are considered salient when determining semantic equivalence. The author thinks this is a fair request but has no recommendataions at present.</span></p>
<ol start="6" type="1">
<li><p>Let the subexpression <code>out_sndr</code> denote the result of the invocation <code>on(sch, sndr)</code> or an object copied or moved from such, let <code>OutSndr</code> be <code>decltype((out_sndr))</code>, let the subexpression <code>rcvr</code> denote a receiver such that <code>sender_to&lt;decltype((out_sndr)), decltype((rcvr))&gt;</code> is <code>true</code>, and let <code>sch_copy</code> and <code>sndr_copy</code> be lvalue subexpressions refering to objects decay-copied from <code>sch</code> and <code>sndr</code> respectively.</p>
<p>The expression <code>connect(out_sndr, rcvr)</code> has undefined behavior unless it creates an asynchronous operation as if by calling <code>connect(S, rcvr)</code>, where <code>S</code> is a sender expression semantically equivalent to:</p>
<pre style="white-space: pre-wrap; font-size: 85%; text-align: left;">
 continues_on(
   starts_on(std::forward_like&lt;OutSndr&gt;(sch_copy), std::forward_like&lt;OutSndr&gt;(sndr_copy)),
   orig_sch)
 </pre>
<p>where <code>orig_sch</code> is <code>get_scheduler(get_env(rcvr))</code>.</p></li>
<li><p>Let the subexpression <code>out_sndr2</code> denote the result of the invocation <code>on(sndr, sch, closure)</code> or an object copied or moved from such, let <code>OutSndr2</code> be <code>decltype((out_sndr2))</code>, let the subexpression <code>rcvr2</code> denote a receiver such that <code>sender_to&lt;decltype((out_sndr2)), decltype((rcvr2))&gt;</code> is <code>true</code>, and let <code>sndr_copy</code>, <code>sch_copy</code>, and <code>closure_copy</code> be lvalue subexpressions refering to objects decay-copied from <code>sndr</code>, <code>sch</code>, and <code>closure</code> respectively.</p>
<p>The expression <code>connect(out_sndr2, rcvr2)</code> has undefined behavior unless it creates an asynchronous operation as if by calling <code>connect(S2, rcvr2)</code>, where <code>S2</code> is a sender expression semantically equivalent to:</p>
<pre style="white-space: pre-wrap; font-size: 85%; text-align: left;">
 <em>write-env</em>(
   continues_on(
     std::forward_like&lt;OutSndr2&gt;(closure_copy)(
       continues_on(
         <em>write-env</em>(std::forward_like&lt;OutSndr2&gt;(sndr_copy), <em>SCHED-ENV</em>(orig_sch)),
         sch_copy)),
     orig_sch),
   <em>SCHED-ENV</em>(sch_copy))
 </pre>
<p>where <code>orig_sch</code> is an lvalue refering to an object decay-copied from <code>get_completion_scheduler&lt;set_value_t&gt;(get_env(sndr_copy))</code> if that expression is well-formed; otherwise, <code>get_scheduler(get_env(rcvr2))</code>.</p></li>
</ol>
</blockquote>
</div>
<h2 id="acknowlegments">Acknowlegments</h2>
<p>I’d like to thank my dog, Luna.</p>
</body>
</html>
