Skip to main content

nova_mir/
async_lower.rs

1//! The async state-machine transform: an `async fn` becomes a resumable poll
2//! function.
3//!
4//! A post-monomorphization pass over the finished [`Module`]. Each function
5//! [`crate::Function::is_async`] flags is split into two:
6//!
7//! - **`<mangled>$poll`** — the original body, rewritten to hold every value
8//!   in a heap *state object* rather than in a temp, with the signature
9//!   `(state, task_ctx) -> i64` the runtime's `PollFn` declares. Its
10//!   environment pointer **is** the state object, so the existing closure
11//!   calling convention (`takes_env`: temp 0 is the env) carries it with no
12//!   new ABI.
13//! - **`<mangled>`** — a wrapper keeping the original symbol and parameter
14//!   list, returning the `{ poll_code, state }` two-word future value its
15//!   callers were already compiled to expect for a `Future<T>`.
16//!
17//! Running on the finished module rather than during lowering means the pass
18//! sees no generics and lands once for both codegen backends.
19//!
20//! # How a body is split
21//!
22//! A body's suspend points arrive as [`Stmt::Await`] markers. Each block is cut
23//! at every one of them, and each cut expands into three blocks: one that polls
24//! the awaited future, one that suspends, and one that carries on. The poll
25//! function's entry becomes a [`Terminator::Switch`] on the resume tag, sending
26//! tag 0 to the body's original entry and tag *k* to await *k*'s poll block — an
27//! existing terminator, so neither backend needs a new construct.
28//!
29//! Awaiting means polling: a future value *is* the `{ poll_code, state }` fat
30//! pointer `Stmt::CallIndirect` already knows how to call, and calling it passes
31//! the inner state object as the leading environment argument, which is exactly
32//! `PollFn`'s first parameter. On `POLL_READY` the continuation copies the
33//! awaited value out of the inner state object's own output slot. On
34//! `POLL_PENDING` the suspend stores **this await's own tag**, not the next one:
35//! an inner future may report pending any number of times, and a tag one higher
36//! would resume the task past a suspend point that never finished — reading an
37//! output slot nothing has written, which completes with a wrong value rather
38//! than failing.
39//!
40//! ## Awaiting one future twice
41//!
42//! Nothing here stops a body from awaiting the same future value twice
43//! (`let fut = g()` then `fut.await + fut.await`), and the result is that the
44//! inner poll function is re-entered at whatever tag its state object holds, so
45//! it re-runs everything after its own last suspend point — its side effects
46//! included. Rust makes the shape unrepresentable instead, by move-checking the
47//! future out of the awaiting expression.
48//!
49//! This is not a miscompile of the split: each await polls a future that says it
50//! is ready and reads the slot that future wrote. It is a missing *front-end*
51//! rule, and it belongs to whatever gives `Future` an ownership story. Named here
52//! because the split is what made the shape reachable.
53//!
54//! # Why every value goes to the state object
55//!
56//! Nothing in an await-free body needs to be spilled: with no suspend point,
57//! no value can be live across one. The spill is unconditional anyway because
58//! the split puts blocks that run in *different* invocations of the poll
59//! function either side of a suspend, and a value left in a temp across such a
60//! split is read on a path where nothing defined it — which the Cranelift
61//! frontend resolves as a block parameter fed from a predecessor that never
62//! executes, i.e. garbage, with no diagnostic. Spilling everything removes the
63//! liveness question instead of answering it, for every body rather than only
64//! the ones a liveness analysis would get right.
65//!
66//! # A panic must not cross a poll function's boundary
67//!
68//! The `PollFn` ABI is `"C-unwind"`, which permits an unwind to pass through
69//! without aborting, but a Cranelift- or LLVM-emitted frame has no landing pads
70//! and no drop glue, so an unwind *through* a poll function would skip whatever
71//! the executor's bookkeeping needs. What this pass contributes to that
72//! requirement is narrow and checkable: **the only call it introduces into a
73//! poll function is the indirect poll of an awaited future.** Everything else it
74//! emits is a field load, a field store, an integer constant or a terminator,
75//! and the other calls in a poll body are the ones the `async fn` already
76//! contained, under the same discipline as any other Nova function
77//! (`nova_rt_panic_str` and `nova_rt_check_bounds` abort rather than unwind, and
78//! a `Terminator::Trap` becomes a trap instruction).
79//!
80//! That one call is sound by induction rather than by inspection, because its
81//! callee is a value and not a symbol. What the value can be is narrow. `Future`
82//! is a *nameable* type — `fn take(f: Future<Int>)` type-checks — but naming it
83//! is not constructing one: the language has no future literal and no
84//! constructor, and an `extern` signature cannot mention one either (`Future` is
85//! not FFI-safe, `require_ffi_safe` in `nova-typeck`), so no foreign function
86//! pointer can reach an await.
87//!
88//! Poll code therefore comes from exactly two *kinds* of source, and each
89//! carries the obligation itself:
90//!
91//! - The `$poll` functions this pass generates, under this same paragraph's
92//!   discipline — the induction step.
93//! - Every hand-written `PollFn` in `nova-runtime`, each stating its own
94//!   no-unwind justification at its own definition rather than here:
95//!   `nova_rt_task_yield_future`'s `poll_yield_once` (what `yield_now`
96//!   awaits, because an `async fn` body suspends only at an `.await` and
97//!   nothing in the language is a future that is not already ready) and
98//!   `nova_rt_task_sleep_future_nanos`'s `poll_sleep` (what `sleep` awaits) are two
99//!   of them, not an exhaustive count — `nova_rt_task_join_future`'s
100//!   `poll_join` (what `join` awaits) is a third, added after this paragraph
101//!   was first written, and there may be more by the time this is read.
102//!   Naming this as a *kind* rather than a roster is deliberate: this
103//!   comment went stale once already when a second hand-written `PollFn`
104//!   joined the first, and the fix is not a bigger number but a claim that
105//!   does not depend on one -- the two *kinds* are closed, by the
106//!   callee-is-a-value argument above; how many hand-written poll functions
107//!   currently exist is not, and is not this comment's business to track.
108//!
109//! The other way to break the argument is to make a runtime function that *can*
110//! unwind reachable from a poll body. `nova_rt_task_block_on` was that function:
111//! it diagnosed re-entrancy with a `panic!`, and `std/task`'s `block_on` is
112//! exactly what makes it reachable — `async fn f() { block_on(g()) }` compiles.
113//! That diagnostic aborts instead, for this reason; see
114//! `nova-runtime`'s `abort_with`.
115//!
116//! The state object's allocation is a call too. The outer async fn's own
117//! state object is allocated in its wrapper, which is an ordinary Nova
118//! function and not a poll function, and so sits outside this argument.
119//!
120//! **Corrected 2026-08-11: the inner future a body awaits is a different
121//! allocation, and is not outside this argument.** `task_yield_future`,
122//! `task_sleep_future_nanos` and `task_join_future` are called from the awaiting
123//! body itself, so that call -- and the `build_future`/`gc::alloc` inside
124//! it -- runs in the generated `$poll` frame that awaits the result, on
125//! whichever poll first reaches it. Its own no-panic argument is not
126//! restated here: it lives with `build_future`
127//! (`nova-runtime/src/task.rs`), which only ever receives a
128//! compile-time-constant size.
129//!
130//! # The layout this pass builds
131//!
132//! Declared here **and** in `nova-runtime/src/task.rs`: `nova-mir` must not
133//! depend on `nova-runtime`, and `nova-runtime` must not depend on `nova-mir`,
134//! so the two ends of this ABI are two independent declarations of one layout.
135//! `nova-codegen-cranelift` depends on both and is where they are pinned
136//! together (`the_state_layout_matches_nova_runtimes`).
137
138use crate::{Block, BlockId, Function, MirTy, Module, RtFunc, Stmt, Temp, Terminator};
139
140/// The resume tag, at byte offset 0: which of a poll function's states to
141/// enter. Written by the wrapper; a resumable poll function dispatches on it.
142pub const STATE_SLOT_TAG: u32 = 0;
143/// The output value, at byte offset 8. Written by whichever poll call reports
144/// [`POLL_READY`], and read by the executor **unconditionally** on completion.
145pub const STATE_SLOT_OUTPUT: u32 = 1;
146/// The first per-temp slot, at byte offset 16: original temp `i` lives at slot
147/// `STATE_SLOT_TEMPS + i`.
148pub const STATE_SLOT_TEMPS: u32 = 2;
149/// The smallest state object this pass may emit. The tag and output slots are
150/// read and written for every future, including one whose `async fn` returns
151/// unit and has no temps.
152pub const STATE_MIN_SIZE: i64 = STATE_SLOT_TEMPS as i64 * 8;
153/// A poll function has not produced a value: it stopped at an await whose future
154/// was not ready, and recorded a resume tag that returns to that same await.
155pub const POLL_PENDING: i64 = 0;
156/// A poll function has written its final value to [`STATE_SLOT_OUTPUT`].
157pub const POLL_READY: i64 = 1;
158/// Word 0 of a future value: the poll function's address.
159pub const FUTURE_SLOT_POLL: u32 = 0;
160/// Word 1 of a future value: the state object's address, not a record holding
161/// it.
162pub const FUTURE_SLOT_STATE: u32 = 1;
163
164/// The state object pointer inside a generated poll function: its environment
165/// parameter, which `takes_env` places at temp 0.
166const STATE: Temp = Temp(0);
167/// The task context pointer inside a generated poll function: its one real
168/// parameter, which `takes_env` places at temp 1.
169const TASK_CTX: Temp = Temp(1);
170
171/// The block a resumable poll function enters at, where it switches on the
172/// resume tag. MIR's entry is `BlockId(0)` by definition, so the dispatch has to
173/// take that id and every block the body already had has to be renumbered past
174/// it.
175const DISPATCH: BlockId = BlockId(0);
176/// The one block a resumable poll function sends every unrecognized resume tag
177/// and every unrecognized inner poll status to. Holding no statements is what
178/// lets all of them share it, and [`Terminator::Trap`] aborts rather than
179/// unwinding.
180const BAD_DISCRIMINANT: BlockId = BlockId(1);
181/// How many blocks a resumable poll function reserves ahead of the body's own:
182/// [`DISPATCH`] and [`BAD_DISCRIMINANT`].
183const RESERVED_BLOCKS: u32 = 2;
184/// How many blocks one await expands to, beyond the piece of its own block that
185/// runs before it: the poll, the suspend, and the continuation.
186const BLOCKS_PER_AWAIT: u32 = 3;
187/// The lowest tag a suspend may store. Tag 0 is the entry state — what the
188/// wrapper writes into a fresh state object — so the resume tags start above it.
189const FIRST_RESUME_TAG: i64 = 1;
190
191/// Rewrite every `async fn` in `module` into a poll function plus a wrapper.
192///
193/// Idempotent: the flag it keys on is cleared on both halves, so a second run
194/// finds nothing to do. `lower_module` asserts the postcondition — no function
195/// left flagged — because a missed one reaches codegen with its body's return
196/// class instead of a future's.
197///
198/// # Preconditions
199///
200/// Every suspend point in a flagged function's body is a [`Stmt::Await`] marker,
201/// which is what `lower::lower_expr` emits for `.await`. There is no body shape
202/// this pass rejects.
203pub(crate) fn transform(module: &mut Module) {
204    let mut wrappers: Vec<Function> = Vec::new();
205    for f in &mut module.functions {
206        if !f.is_async {
207            continue;
208        }
209        wrappers.push(split_into_poll_and_wrapper(f));
210    }
211    module.functions.append(&mut wrappers);
212}
213
214/// Rewrite `f` in place into its `$poll` function and return the wrapper that
215/// takes over its old symbol.
216fn split_into_poll_and_wrapper(f: &mut Function) -> Function {
217    // The precondition the output store depends on: a returned temp's class is
218    // the class this function's body produces. It is `lower::body_return_ty`
219    // that makes it hold for an `async fn`, whose `ret_ty` is `Future<T>` while
220    // its body has type `T` — lowering the body against the wrapped type leaves
221    // `ret` claiming `Ptr` for a `Float`-producing body, and the store below
222    // would then be describing a value it is not. Asserted rather than assumed
223    // because the two agree by coincidence at every class except `Float`, so a
224    // regression here is invisible in most fixtures.
225    debug_assert!(
226        f.blocks.iter().all(|b| match &b.term {
227            Terminator::Return(Some(t)) => f.temps[t.0 as usize] == f.ret,
228            _ => true,
229        }),
230        "`{}` returns a temp whose class is not its declared return class {:?}; \
231         see `lower::body_return_ty`",
232        f.name,
233        f.ret
234    );
235    let wrapper = build_wrapper(f);
236
237    // Every original temp becomes a state slot, so the temp list the rewritten
238    // body is *indexed against* is the old one, while the poll function's own
239    // temps start fresh: the state pointer, task_ctx, then one scratch per
240    // load and per result.
241    let orig_temps = std::mem::take(&mut f.temps);
242    let mut sp = Spiller {
243        temps: vec![MirTy::Ptr, MirTy::Ptr],
244        orig: &orig_temps,
245    };
246    let blocks = std::mem::take(&mut f.blocks);
247    f.blocks = sp.body(blocks);
248    f.temps = sp.temps;
249
250    // Every await must have been consumed by the split. A surviving marker
251    // reaches a codegen backend, which has no machine code for it — reported
252    // there as a malformed-MIR error, but the pass that was supposed to remove
253    // it is where the mistake is.
254    debug_assert!(
255        !f.blocks
256            .iter()
257            .flat_map(|b| &b.stmts)
258            .any(|s| matches!(s, Stmt::Await { .. })),
259        "`{}$poll` still holds an await marker after the split",
260        f.name
261    );
262
263    // **The two halves of the state object's size must agree.** The allocation
264    // was sized above, from the pre-transform temp count, *before* the rewrite
265    // ran; the slot indices were just chosen by `Spiller::slot`. Nothing in the
266    // types connects them, so a rewrite that addresses one slot more than the
267    // wrapper paid for writes past the allocation — and that has no diagnostic,
268    // no verifier error, and no test that can see it, because a test asserting
269    // the expected size recomputes it from the same formula the bug changed.
270    //
271    // This compares emitted code against emitted code: the highest slot any
272    // access actually names, against the exact byte count the wrapper actually
273    // allocated. Adding a slot (a per-await resume tag, an awaited future's
274    // handle) therefore has to grow `state_slot_count` in the same edit.
275    debug_assert!(
276        highest_state_slot(&f.blocks, STATE) < wrapper.state_slots,
277        "`{}$poll` addresses state slot {}, which is past the {} slots ({} \
278         bytes) its wrapper allocated: a slot added to the rewrite must be \
279         added to `state_slot_count` in the same edit",
280        f.name,
281        highest_state_slot(&f.blocks, STATE),
282        wrapper.state_slots,
283        wrapper.state_slots as i64 * 8,
284    );
285    debug_assert!(
286        highest_state_slot(&wrapper.function.blocks, wrapper.state) < wrapper.state_slots,
287        "`{}` seeds state slot {}, which is past the {} slots it allocated",
288        f.name,
289        highest_state_slot(&wrapper.function.blocks, wrapper.state),
290        wrapper.state_slots,
291    );
292
293    f.name.push_str("$poll");
294    f.takes_env = true;
295    f.params = 1;
296    f.capture_count = 0;
297    f.ret = MirTy::I64;
298    f.is_async = false;
299    wrapper.function
300}
301
302/// The highest state-object slot index any field access in `blocks` addresses
303/// through `state`.
304///
305/// Filtering on the record temp is what makes this a *state* slot count rather
306/// than a field-index count: a rewritten body's own record accesses (an
307/// `async fn` reading `self.v`, say) are `RecordField`s too, against a scratch
308/// temp, with an index that belongs to the user's record and not to this layout.
309/// In a poll function no body statement can name [`STATE`] by accident — the
310/// rewrite replaces every body operand with a scratch temp, and those start at
311/// index 2.
312///
313/// Returns [`STATE_SLOT_TAG`] when there is no access at all, which is in bounds
314/// for every state object by [`STATE_MIN_SIZE`].
315fn highest_state_slot(blocks: &[Block], state: Temp) -> u32 {
316    blocks
317        .iter()
318        .flat_map(|b| &b.stmts)
319        .filter_map(|s| match s {
320            Stmt::RecordField { record, index, .. } | Stmt::SetField { record, index, .. }
321                if *record == state =>
322            {
323                Some(*index)
324            }
325            _ => None,
326        })
327        .max()
328        .unwrap_or(STATE_SLOT_TAG)
329}
330
331/// How many 8-byte slots a state object needs for a body with `n_temps` temps.
332///
333/// The single place this is decided. A rewrite that needs another slot grows
334/// this, and the `debug_assert!`s in [`split_into_poll_and_wrapper`] fail if it
335/// does not.
336fn state_slot_count(n_temps: usize) -> u32 {
337    STATE_SLOT_TEMPS + n_temps as u32
338}
339
340/// A wrapper, plus the two facts the state-size cross-check needs from the
341/// function that built it.
342struct Wrapper {
343    function: Function,
344    /// The slot count baked into this wrapper's allocation — carried out rather
345    /// than recomputed, so the check compares against the number the emitted
346    /// code actually used.
347    state_slots: u32,
348    /// The temp holding the state pointer, so the wrapper's own seeding stores
349    /// can be told apart from its stores to the future value.
350    state: Temp,
351}
352
353/// Build the function that keeps the original symbol: allocate the state,
354/// seed it, and return the `{ poll_code, state }` future.
355///
356/// Reads `f` while it is still the pre-transform body, so it must run before
357/// [`split_into_poll_and_wrapper`] mutates it.
358fn build_wrapper(f: &Function) -> Wrapper {
359    // The temps the ABI hands this function: the environment pointer, if it
360    // has one, then the real parameters — numbered exactly as `lower_function`
361    // numbered them, which is what makes seeding them into slot `i` line up
362    // with what the rewritten body reads back out of slot `i`.
363    let abi_count = f.params as usize + usize::from(f.takes_env);
364    let mut temps: Vec<MirTy> = f.temps[..abi_count].to_vec();
365    let mut stmts: Vec<Stmt> = Vec::new();
366
367    // `state_slot_count(n_temps) * 8` bytes, through `nova_rt_alloc` — which is
368    // `gc::alloc(.., true)`, i.e. SCANNED. That is load-bearing, not incidental:
369    // a heap-valued output written to `STATE_SLOT_OUTPUT` is kept alive only by
370    // the collector tracing through this object, and an unscanned one is marked
371    // but never traced.
372    //
373    // Sized here, from the pre-transform temp count, before the rewrite that
374    // chooses the slot indices has run. `split_into_poll_and_wrapper` checks the
375    // two against each other afterwards.
376    let state_slots = state_slot_count(f.temps.len());
377    let bytes = state_slots as i64 * 8;
378    debug_assert!(
379        bytes >= STATE_MIN_SIZE,
380        "a state object must hold at least the tag and output slots, since the \
381         executor reads the output slot on completion even for a future with \
382         no temps at all"
383    );
384    let size = push_temp(&mut temps, MirTy::I64);
385    stmts.push(Stmt::ConstInt(size, bytes));
386    let state = push_temp(&mut temps, MirTy::Ptr);
387    stmts.push(Stmt::CallRuntime {
388        dst: Some(state),
389        func: RtFunc::Alloc,
390        args: vec![size],
391    });
392
393    // The resume tag. `gc::alloc` hands back zeroed memory, so this store is
394    // redundant today; it is emitted anyway so the entry tag is a property of
395    // the generated code rather than of the allocator's zeroing.
396    let zero = push_temp(&mut temps, MirTy::I64);
397    stmts.push(Stmt::ConstInt(zero, 0));
398    stmts.push(Stmt::SetField {
399        record: state,
400        index: STATE_SLOT_TAG,
401        value: zero,
402        ty: MirTy::I64,
403    });
404
405    // Seed the incoming ABI temps into their slots. An `async fn`'s arguments
406    // arrive HERE, not at poll, whose only parameters are the state and
407    // task_ctx — so without this the body reads the allocator's zeroes.
408    // Emitted for a unit-class temp too: both backends drop a `ty: Unit`
409    // field access before touching the value, so it costs one dead statement
410    // rather than a special case that could disagree with the reload side.
411    for (i, &ty) in temps[..abi_count].iter().enumerate() {
412        stmts.push(Stmt::SetField {
413            record: state,
414            index: STATE_SLOT_TEMPS + i as u32,
415            value: Temp(i as u32),
416            ty,
417        });
418    }
419
420    // The future value. `MakeClosure` is the only MIR construct that
421    // materializes a function's address, and with an empty capture list it
422    // allocates exactly the two-word block this ABI wants — `{ poll_code,
423    // null }`, with no environment record — so word 1 is then patched to the
424    // state object itself. Capturing the state instead would put a record
425    // *containing* it in word 1, one indirection too many, and the runtime
426    // would read the output slot out of that record.
427    let future = push_temp(&mut temps, MirTy::Ptr);
428    stmts.push(Stmt::MakeClosure {
429        dst: future,
430        code: format!("{}$poll", f.name),
431        captures: Vec::new(),
432    });
433    stmts.push(Stmt::SetField {
434        record: future,
435        index: FUTURE_SLOT_STATE,
436        value: state,
437        ty: MirTy::Ptr,
438    });
439
440    Wrapper {
441        function: Function {
442            name: f.name.clone(),
443            params: f.params,
444            takes_env: f.takes_env,
445            // The wrapper loads nothing from an environment record: it only
446            // forwards the pointer into a state slot, so the body's own capture
447            // loads (now inside the poll function) still find them.
448            capture_count: 0,
449            temps,
450            ret: MirTy::Ptr,
451            is_async: false,
452            blocks: vec![Block {
453                stmts,
454                term: Terminator::Return(Some(future)),
455            }],
456        },
457        state_slots,
458        state,
459    }
460}
461
462/// Append a temp of class `ty` and return its id. The temp list's length *is*
463/// the next id, in both halves of this pass.
464fn push_temp(temps: &mut Vec<MirTy>, ty: MirTy) -> Temp {
465    let t = Temp(temps.len() as u32);
466    temps.push(ty);
467    t
468}
469
470/// Rewrites a pre-transform body into the poll function's: every value it names
471/// moves into the state object, and every suspend point becomes the control flow
472/// that polls, suspends and resumes.
473///
474/// The split and the spill are one pass because each needs what the other
475/// consumes. The split reads an await's *original* temp ids to name the state
476/// slots its future and result live in — which is exactly what the spill
477/// replaces with scratch temps — and the spill has to run over the pieces the
478/// split produced, since the piece a statement lands in is where its reloads
479/// have to go.
480struct Spiller<'a> {
481    /// The poll function's temp list, grown one entry per load and per result.
482    temps: Vec<MirTy>,
483    /// The pre-transform temp classes, indexed by original temp id — which is
484    /// also the index into the state object's temp slots.
485    orig: &'a [MirTy],
486}
487
488/// One suspend point lifted out of a block's statement list.
489struct AwaitPoint {
490    /// Where the awaited value goes, or `None` when the awaited output is unit.
491    dst: Option<Temp>,
492    /// The pre-transform temp holding the awaited `{ poll_code, state }` value.
493    future: Temp,
494}
495
496/// Split one pre-transform block's statements at its suspend points.
497///
498/// Returns the straight-line runs between the awaits — always exactly one more
499/// than there are awaits, so a block whose last statement is an await still has
500/// a continuation to resume into — and the awaits themselves, in order.
501///
502/// Lifting the awaits out here, rather than substituting through them like every
503/// other statement, is what lets the sequence each one expands to address state
504/// slots by the *original* temp ids: [`visit_temps`] would already have replaced
505/// them with scratch temps that name no slot at all.
506fn split_at_awaits(stmts: Vec<Stmt>) -> (Vec<Vec<Stmt>>, Vec<AwaitPoint>) {
507    let mut segments: Vec<Vec<Stmt>> = vec![Vec::new()];
508    let mut awaits: Vec<AwaitPoint> = Vec::new();
509    for s in stmts {
510        match s {
511            Stmt::Await { dst, future } => {
512                awaits.push(AwaitPoint { dst, future });
513                segments.push(Vec::new());
514            }
515            other => segments
516                .last_mut()
517                .expect("segments starts with one entry and only ever grows")
518                .push(other),
519        }
520    }
521    (segments, awaits)
522}
523
524/// How many suspend points `b` contains.
525fn await_count(b: &Block) -> u32 {
526    b.stmts
527        .iter()
528        .filter(|s| matches!(s, Stmt::Await { .. }))
529        .count() as u32
530}
531
532/// Whether a statement's operand is consumed or produced. A load has to be
533/// emitted before the statement and a store after it, so the two cannot share
534/// one code path.
535#[derive(Clone, Copy)]
536enum Role {
537    Read,
538    Write,
539}
540
541impl Spiller<'_> {
542    fn scratch(&mut self, ty: MirTy) -> Temp {
543        push_temp(&mut self.temps, ty)
544    }
545
546    fn orig_ty(&self, t: Temp) -> MirTy {
547        self.orig[t.0 as usize]
548    }
549
550    fn slot(t: Temp) -> u32 {
551        STATE_SLOT_TEMPS + t.0
552    }
553
554    /// Load field `index` of `record` into a fresh scratch temp.
555    ///
556    /// A fresh temp per load site, never one reused across uses: a reused
557    /// scratch would be a value living outside the state object across
558    /// statements, which is exactly what this pass exists to prevent — and after
559    /// a split it could be a value living across a *suspend*, read on a resume
560    /// where nothing defined it.
561    fn load(&mut self, out: &mut Vec<Stmt>, record: Temp, index: u32, ty: MirTy) -> Temp {
562        let dst = self.scratch(ty);
563        out.push(Stmt::RecordField {
564            dst,
565            record,
566            index,
567            ty,
568        });
569        dst
570    }
571
572    /// Load original temp `t` out of its state slot into a fresh scratch temp.
573    fn reload(&mut self, out: &mut Vec<Stmt>, t: Temp) -> Temp {
574        let ty = self.orig_ty(t);
575        self.load(out, STATE, Self::slot(t), ty)
576    }
577
578    /// A fresh scratch temp holding the i64 constant `v`.
579    fn const_i64(&mut self, out: &mut Vec<Stmt>, v: i64) -> Temp {
580        let t = self.scratch(MirTy::I64);
581        out.push(Stmt::ConstInt(t, v));
582        t
583    }
584
585    /// Rewrite a whole pre-transform body: split it at its suspend points, spill
586    /// every value it names, and give a resumable body its entry dispatch.
587    fn body(&mut self, orig: Vec<Block>) -> Vec<Block> {
588        let per_block: Vec<u32> = orig.iter().map(await_count).collect();
589        let total: u32 = per_block.iter().sum();
590        // An await-free body has exactly one state, so a dispatch on its tag
591        // would be the identity and moving its entry block would renumber every
592        // terminator target for no gain. Reserving nothing leaves such a body's
593        // block list exactly as it arrived.
594        let reserved = if total == 0 { 0 } else { RESERVED_BLOCKS };
595
596        // Where each original block's leading piece lands, decided before
597        // anything is emitted: a terminator can name a block that has not been
598        // rewritten yet, and a loop's back edge always does.
599        let mut next = reserved;
600        let starts: Vec<BlockId> = per_block
601            .iter()
602            .map(|a| {
603                let id = BlockId(next);
604                next += 1 + BLOCKS_PER_AWAIT * a;
605                id
606            })
607            .collect();
608
609        let mut out: Vec<Block> = Vec::with_capacity(next as usize);
610        for _ in 0..reserved {
611            // Placeholders. `BAD_DISCRIMINANT` is already exactly what it needs
612            // to be; `DISPATCH` is overwritten below, once every resume point
613            // exists to switch to.
614            out.push(Block {
615                stmts: Vec::new(),
616                term: Terminator::Trap,
617            });
618        }
619        let mut resume: Vec<BlockId> = Vec::with_capacity(total as usize);
620        for (i, b) in orig.into_iter().enumerate() {
621            debug_assert_eq!(
622                out.len() as u32,
623                starts[i].0,
624                "block {i}'s pieces must land at the id reserved for them, or \
625                 every terminator naming it is off by however far the two drifted"
626            );
627            self.block_pieces(b, &starts, &mut out, &mut resume);
628        }
629        debug_assert_eq!(
630            out.len() as u32,
631            next,
632            "the emitted block count must match the ids reserved for it"
633        );
634
635        if total > 0 {
636            debug_assert_eq!(
637                resume.len(),
638                total as usize,
639                "one resume point per await, in tag order"
640            );
641            let mut stmts = Vec::new();
642            let tag = self.load(&mut stmts, STATE, STATE_SLOT_TAG, MirTy::I64);
643            let arms = std::iter::once((0, starts[0]))
644                .chain(
645                    resume
646                        .into_iter()
647                        .enumerate()
648                        .map(|(k, b)| (FIRST_RESUME_TAG + k as i64, b)),
649                )
650                .collect();
651            out[DISPATCH.0 as usize] = Block {
652                stmts,
653                term: Terminator::Switch {
654                    disc: tag,
655                    arms,
656                    default: BAD_DISCRIMINANT,
657                },
658            };
659        }
660        out
661    }
662
663    /// Emit one original block's pieces: the statements before its first await,
664    /// then a poll / suspend / continuation triple per await.
665    ///
666    /// Appends each await's poll block to `resume`, in tag order.
667    fn block_pieces(
668        &mut self,
669        b: Block,
670        starts: &[BlockId],
671        out: &mut Vec<Block>,
672        resume: &mut Vec<BlockId>,
673    ) {
674        let base = out.len() as u32;
675        let Block {
676            stmts: body,
677            term: orig_term,
678        } = b;
679        let (segments, awaits) = split_at_awaits(body);
680        let mut segments = segments.into_iter();
681        let n = awaits.len();
682        // Whichever piece is last takes the block's original terminator, and
683        // exactly one does.
684        let mut orig_term = Some(orig_term);
685
686        // The leading piece. A resume skips it, which is the reason for cutting
687        // the block here rather than re-entering it from the top.
688        let mut stmts = Vec::new();
689        self.spill(
690            segments.next().expect("one segment more than awaits"),
691            &mut stmts,
692        );
693        if n == 0 {
694            let term = self.finish(&mut orig_term, &mut stmts, starts);
695            out.push(Block { stmts, term });
696            return;
697        }
698        out.push(Block {
699            stmts,
700            term: Terminator::Goto(BlockId(base + 1)),
701        });
702
703        for (k, aw) in awaits.into_iter().enumerate() {
704            let poll = BlockId(base + 1 + BLOCKS_PER_AWAIT * k as u32);
705            let suspend = BlockId(poll.0 + 1);
706            let cont = BlockId(poll.0 + 2);
707            let tag = FIRST_RESUME_TAG + resume.len() as i64;
708            resume.push(poll);
709
710            // The poll. Entered both by falling out of the code before the await
711            // and by a resume at `tag`, so it holds nothing but the poll itself:
712            // anything else here would run again on every re-poll.
713            debug_assert_eq!(out.len() as u32, poll.0, "the poll block's own id");
714            let mut stmts = Vec::new();
715            let future = self.reload(&mut stmts, aw.future);
716            let status = self.scratch(MirTy::I64);
717            stmts.push(Stmt::CallIndirect {
718                dst: Some(status),
719                callee: future,
720                params: vec![MirTy::Ptr],
721                ret: MirTy::I64,
722                args: vec![TASK_CTX],
723            });
724            out.push(Block {
725                stmts,
726                // Exactly the two statuses the ABI defines, and a trap for the
727                // rest: a generated poll function returns only these, so
728                // anything else is a compiler bug, and treating an unrecognized
729                // status as ready would complete the await out of an output slot
730                // nothing wrote.
731                term: Terminator::Switch {
732                    disc: status,
733                    arms: vec![(POLL_READY, cont), (POLL_PENDING, suspend)],
734                    default: BAD_DISCRIMINANT,
735                },
736            });
737
738            // The suspend. The tag stored is THIS await's own, so the next poll
739            // re-enters the block above and polls the same future again. A tag
740            // one higher would resume past a suspend point that never finished,
741            // and the continuation would then read an inner output slot nothing
742            // has written — a wrong value on a path that still completes.
743            let mut stmts = Vec::new();
744            let t = self.const_i64(&mut stmts, tag);
745            stmts.push(Stmt::SetField {
746                record: STATE,
747                index: STATE_SLOT_TAG,
748                value: t,
749                ty: MirTy::I64,
750            });
751            let pending = self.const_i64(&mut stmts, POLL_PENDING);
752            out.push(Block {
753                stmts,
754                term: Terminator::Return(Some(pending)),
755            });
756
757            // The continuation, reached only from the READY arm above — so the
758            // awaited value exists: word `FUTURE_SLOT_STATE` of the future is the
759            // inner state object, and `STATE_SLOT_OUTPUT` of *that* is the value.
760            // Copied into the await result's own temp slot before the statements
761            // that followed the await run.
762            let mut stmts = Vec::new();
763            match aw.dst {
764                Some(dst) if self.orig_ty(dst) != MirTy::Unit => {
765                    let ty = self.orig_ty(dst);
766                    let future = self.reload(&mut stmts, aw.future);
767                    let inner = self.load(&mut stmts, future, FUTURE_SLOT_STATE, MirTy::Ptr);
768                    let value = self.load(&mut stmts, inner, STATE_SLOT_OUTPUT, ty);
769                    stmts.push(Stmt::SetField {
770                        record: STATE,
771                        index: Self::slot(dst),
772                        value,
773                        ty,
774                    });
775                }
776                // A unit-output future has nothing to copy out: it writes an
777                // explicit zero into its own output slot, and the awaiting body
778                // has no temp to put it in.
779                _ => {}
780            }
781            self.spill(
782                segments.next().expect("one segment more than awaits"),
783                &mut stmts,
784            );
785            let term = if k + 1 < n {
786                // The next await's poll block, which is the very next one
787                // emitted.
788                Terminator::Goto(BlockId(cont.0 + 1))
789            } else {
790                self.finish(&mut orig_term, &mut stmts, starts)
791            };
792            out.push(Block { stmts, term });
793        }
794    }
795
796    /// Take the original terminator and rewrite it. Panics if a second piece
797    /// asks for it, which would mean two pieces of one block both claimed to be
798    /// its last.
799    fn finish(
800        &mut self,
801        orig_term: &mut Option<Terminator>,
802        out: &mut Vec<Stmt>,
803        starts: &[BlockId],
804    ) -> Terminator {
805        let term = orig_term
806            .take()
807            .expect("exactly one piece of a block takes its original terminator");
808        self.terminator(term, out, starts)
809    }
810
811    /// Move every value one straight-line run of statements names into the state
812    /// object, appending the rewritten statements to `out`.
813    fn spill(&mut self, stmts: Vec<Stmt>, out: &mut Vec<Stmt>) {
814        for mut s in stmts {
815            let mut loads: Vec<Stmt> = Vec::new();
816            let mut stores: Vec<Stmt> = Vec::new();
817            visit_temps(&mut s, &mut |t, role| match role {
818                Role::Read => *t = self.reload(&mut loads, *t),
819                Role::Write => {
820                    let ty = self.orig_ty(*t);
821                    let dst = self.scratch(ty);
822                    stores.push(Stmt::SetField {
823                        record: STATE,
824                        index: Self::slot(*t),
825                        value: dst,
826                        ty,
827                    });
828                    *t = dst;
829                }
830            });
831            out.append(&mut loads);
832            out.push(s);
833            out.append(&mut stores);
834        }
835    }
836
837    /// Rewrite one terminator: reload whatever it reads, renumber the blocks it
838    /// names through `starts`, and turn a `Return` into a completion.
839    fn terminator(
840        &mut self,
841        term: Terminator,
842        out: &mut Vec<Stmt>,
843        starts: &[BlockId],
844    ) -> Terminator {
845        // Every original terminator names the *start* of a block, and a split
846        // block's start is its leading piece — so this is the whole renumbering.
847        // A target left un-remapped lands in whatever block now carries the old
848        // id, which for a resumable poll function is a reserved one.
849        let at = |b: BlockId| starts[b.0 as usize];
850        match term {
851            Terminator::Goto(target) => Terminator::Goto(at(target)),
852            Terminator::Branch { cond, then_, else_ } => Terminator::Branch {
853                cond: self.reload(out, cond),
854                then_: at(then_),
855                else_: at(else_),
856            },
857            Terminator::Switch {
858                disc,
859                arms,
860                default,
861            } => Terminator::Switch {
862                disc: self.reload(out, disc),
863                arms: arms.into_iter().map(|(v, b)| (v, at(b))).collect(),
864                default: at(default),
865            },
866            // Completion. The value goes to the output slot and the *return*
867            // becomes the status, rather than returning the value directly:
868            // the status is an i64, and an output that is not also an i64 —
869            // a `Float`, the one class `mir_ty` keeps disjoint from the rest —
870            // would otherwise be forced through it.
871            Terminator::Return(value) => {
872                match value {
873                    Some(t) if self.orig_ty(t) != MirTy::Unit => {
874                        let ty = self.orig_ty(t);
875                        let v = self.reload(out, t);
876                        out.push(Stmt::SetField {
877                            record: STATE,
878                            index: STATE_SLOT_OUTPUT,
879                            value: v,
880                            ty,
881                        });
882                    }
883                    // A unit output has no value to store, but the executor
884                    // reads the output slot on completion regardless, so an
885                    // explicit zero goes in rather than leaving the slot to
886                    // whatever the allocator left there.
887                    _ => {
888                        let z = self.const_i64(out, 0);
889                        out.push(Stmt::SetField {
890                            record: STATE,
891                            index: STATE_SLOT_OUTPUT,
892                            value: z,
893                            ty: MirTy::I64,
894                        });
895                    }
896                }
897                let ready = self.const_i64(out, POLL_READY);
898                Terminator::Return(Some(ready))
899            }
900            // Left as a trap, which aborts rather than unwinding: a panic must
901            // not cross a generated poll function's boundary (see the module
902            // doc comment).
903            Terminator::Trap => Terminator::Trap,
904        }
905    }
906}
907
908/// Visit every temp `stmt` mentions, telling the callback whether it is read
909/// or written, and letting it substitute a replacement.
910///
911/// Exhaustive by construction rather than by a `_` arm: a new [`Stmt`] variant
912/// must decide here which of its temps are operands and which are results.
913/// Getting that wrong is not a compile error but a miscompile — a missed read
914/// leaves a statement naming a temp that no longer exists in the poll
915/// function, and a read misfiled as a write drops the value it was supposed to
916/// consume.
917///
918/// Deliberately private to this module: the read/write classification is only
919/// as trustworthy as the pass that exercises it, and nothing else here needs
920/// it.
921fn visit_temps(stmt: &mut Stmt, f: &mut impl FnMut(&mut Temp, Role)) {
922    use Role::{Read, Write};
923    match stmt {
924        Stmt::ConstInt(dst, _)
925        | Stmt::ConstFloat(dst, _)
926        | Stmt::ConstBool(dst, _)
927        | Stmt::ConstStr(dst, _)
928        | Stmt::ConstUnit(dst) => f(dst, Write),
929        Stmt::Copy { dst, src } => {
930            f(src, Read);
931            f(dst, Write);
932        }
933        Stmt::Bin { dst, lhs, rhs, .. } => {
934            f(lhs, Read);
935            f(rhs, Read);
936            f(dst, Write);
937        }
938        Stmt::Neg { dst, src, .. } | Stmt::Not { dst, src } | Stmt::BitNot { dst, src } => {
939            f(src, Read);
940            f(dst, Write);
941        }
942        Stmt::Call { dst, args, .. } | Stmt::CallRuntime { dst, args, .. } => {
943            for a in args.iter_mut() {
944                f(a, Read);
945            }
946            if let Some(dst) = dst {
947                f(dst, Write);
948            }
949        }
950        Stmt::CallIndirect {
951            dst, callee, args, ..
952        } => {
953            f(callee, Read);
954            for a in args.iter_mut() {
955                f(a, Read);
956            }
957            if let Some(dst) = dst {
958                f(dst, Write);
959            }
960        }
961        // Not reached: [`Spiller::block_pieces`] takes every await out of the
962        // statement list before spilling what is left, because the await
963        // sequence it emits addresses state slots by the *original* temp ids
964        // this substitution would already have replaced. Classified honestly
965        // anyway rather than left to a panic, since the roles are the same ones
966        // `CallIndirect` above has — an await *is* an indirect call to a poll
967        // function — and this match is the one place that decides them.
968        Stmt::Await { dst, future } => {
969            f(future, Read);
970            if let Some(dst) = dst {
971                f(dst, Write);
972            }
973        }
974        Stmt::MakeClosure { dst, captures, .. } => {
975            for (c, _) in captures.iter_mut() {
976                f(c, Read);
977            }
978            f(dst, Write);
979        }
980        Stmt::MakeSum { dst, fields, .. } | Stmt::MakeRecord { dst, fields } => {
981            for (v, _) in fields.iter_mut() {
982                f(v, Read);
983            }
984            f(dst, Write);
985        }
986        Stmt::SumTag { dst, sum } | Stmt::SumField { dst, sum, .. } => {
987            f(sum, Read);
988            f(dst, Write);
989        }
990        Stmt::RecordField { dst, record, .. } => {
991            f(record, Read);
992            f(dst, Write);
993        }
994        Stmt::SetField { record, value, .. } => {
995            f(record, Read);
996            f(value, Read);
997        }
998        Stmt::MakeArray { dst, elems } => {
999            for (e, _) in elems.iter_mut() {
1000                f(e, Read);
1001            }
1002            f(dst, Write);
1003        }
1004        Stmt::ArrayAlloc { dst, len } => {
1005            f(len, Read);
1006            f(dst, Write);
1007        }
1008        Stmt::ArrayLen { dst, arr } => {
1009            f(arr, Read);
1010            f(dst, Write);
1011        }
1012        Stmt::ArrayGet {
1013            dst, arr, index, ..
1014        } => {
1015            f(arr, Read);
1016            f(index, Read);
1017            f(dst, Write);
1018        }
1019        Stmt::ArraySet {
1020            arr, index, value, ..
1021        } => {
1022            f(arr, Read);
1023            f(index, Read);
1024            f(value, Read);
1025        }
1026    }
1027}
1028
1029#[cfg(test)]
1030mod tests {
1031    use super::*;
1032    use crate::{Function, MirTy, Module, RtFunc, Stmt, Temp, Terminator};
1033
1034    /// A `Module` shaped exactly the way [`crate::lower::lower_function`]
1035    /// leaves an await-free `async fn f(params...) -> T { <literal> }`: one
1036    /// function, `is_async`, whose declared `ret` is the **body's** class
1037    /// (`T`) rather than `Future<T>`'s `Ptr`, with a single block that returns
1038    /// the literal.
1039    ///
1040    /// Built by hand rather than run through the real pipeline so the input to
1041    /// `transform` is fixed and visible; the pipeline-level tests below
1042    /// (`mir_for`) cover the wiring.
1043    fn module_with_async_const_fn(ret: MirTy, params: &[MirTy]) -> Module {
1044        let mut temps: Vec<MirTy> = params.to_vec();
1045        let result = Temp(temps.len() as u32);
1046        temps.push(ret);
1047        let (stmts, term) = match ret {
1048            MirTy::I64 => (
1049                vec![Stmt::ConstInt(result, 7)],
1050                Terminator::Return(Some(result)),
1051            ),
1052            MirTy::F64 => (
1053                vec![Stmt::ConstFloat(result, 1.5)],
1054                Terminator::Return(Some(result)),
1055            ),
1056            MirTy::I8 => (
1057                vec![Stmt::ConstBool(result, true)],
1058                Terminator::Return(Some(result)),
1059            ),
1060            MirTy::Ptr => (
1061                vec![Stmt::ConstStr(result, "hi".to_string())],
1062                Terminator::Return(Some(result)),
1063            ),
1064            MirTy::Unit => (vec![Stmt::ConstUnit(result)], Terminator::Return(None)),
1065        };
1066        Module {
1067            functions: vec![Function {
1068                name: "f.15".to_string(),
1069                params: params.len() as u32,
1070                takes_env: false,
1071                capture_count: 0,
1072                temps,
1073                ret,
1074                is_async: true,
1075                blocks: vec![crate::Block { stmts, term }],
1076            }],
1077            externs: Vec::new(),
1078        }
1079    }
1080
1081    /// `async fn f(a: T, b: T) -> T { a + b }`: two parameters read, one result
1082    /// written, one value returned — the smallest body with an operand of each
1083    /// kind, for asserting on the exact slots the rewrite touches.
1084    fn module_with_async_bin_fn(ret: MirTy) -> Module {
1085        let mut m = module_with_async_const_fn(ret, &[ret, ret]);
1086        let f = &mut m.functions[0];
1087        f.blocks[0].stmts = vec![Stmt::Bin {
1088            dst: Temp(2),
1089            op: nova_hir::BinOp::Add,
1090            class: crate::OperandClass::Float,
1091            lhs: Temp(0),
1092            rhs: Temp(1),
1093        }];
1094        m
1095    }
1096
1097    /// The same shape with `is_async: false` — the negative control for
1098    /// "the pass rewrites only what it was asked to".
1099    fn module_with_plain_fn() -> Module {
1100        let mut m = module_with_async_const_fn(MirTy::I64, &[]);
1101        m.functions[0].is_async = false;
1102        m
1103    }
1104
1105    fn find<'a>(m: &'a Module, name: &str) -> &'a Function {
1106        m.functions
1107            .iter()
1108            .find(|f| f.name == name)
1109            .unwrap_or_else(|| {
1110                panic!(
1111                    "no function named `{name}`; have {:?}",
1112                    m.functions.iter().map(|f| &f.name).collect::<Vec<_>>()
1113                )
1114            })
1115    }
1116
1117    fn stmts_of(f: &Function) -> impl Iterator<Item = &Stmt> {
1118        f.blocks.iter().flat_map(|b| &b.stmts)
1119    }
1120
1121    #[test]
1122    fn an_await_free_async_fn_becomes_a_poll_fn_and_a_wrapper() {
1123        let mut m = module_with_async_const_fn(MirTy::I64, &[]);
1124        transform(&mut m);
1125
1126        let poll = find(&m, "f.15$poll");
1127        assert!(poll.takes_env, "the poll fn's env IS the state object");
1128        assert_eq!(poll.params, 1, "poll takes task_ctx as its one real param");
1129        assert_eq!(poll.ret, MirTy::I64, "poll returns a status, not the value");
1130        // The ABI the runtime declares is `(state, task_ctx) -> i64`, and both
1131        // backends build the signature from `temps[..params + takes_env]`. So
1132        // the first two temps must be pointer-class, or the generated
1133        // signature is not `PollFn`'s.
1134        assert_eq!(
1135            &poll.temps[..2],
1136            &[MirTy::Ptr, MirTy::Ptr],
1137            "temp 0 is the state pointer, temp 1 is task_ctx"
1138        );
1139
1140        let wrapper = find(&m, "f.15");
1141        assert_eq!(wrapper.ret, MirTy::Ptr, "the wrapper returns a future");
1142        assert!(
1143            !wrapper.takes_env,
1144            "the wrapper keeps the original ABI its callers already compiled against"
1145        );
1146        assert!(
1147            stmts_of(wrapper).any(|s| matches!(s, Stmt::MakeClosure { .. })),
1148            "the wrapper must build the {{ poll_code, state }} fat pointer"
1149        );
1150    }
1151
1152    #[test]
1153    fn the_poll_fn_writes_its_result_to_the_output_slot_not_the_return() {
1154        // Instantiated at `Float`, not `Int`: `mir_ty` collapses `Int` and
1155        // every pointer-like type onto the same 64-bit integer class, so an
1156        // `Int` fixture cannot tell "stored the value" from "returned the
1157        // value" -- both are an i64 in a general-purpose register. `F64` is
1158        // the one class that crosses register banks, so it is the only one
1159        // where returning the value directly through the i64 status is a
1160        // visible type confusion rather than an invisible one.
1161        let mut m = module_with_async_const_fn(MirTy::F64, &[]);
1162        transform(&mut m);
1163        let poll = find(&m, "f.15$poll");
1164        assert!(
1165            stmts_of(poll).any(|s| matches!(
1166                s,
1167                Stmt::SetField { index, ty, .. }
1168                    if *index == STATE_SLOT_OUTPUT && *ty == MirTy::F64
1169            )),
1170            "the f64 result must be stored to the output slot AS an f64: {:?}",
1171            poll.blocks
1172        );
1173        assert_eq!(
1174            poll.ret,
1175            MirTy::I64,
1176            "the poll fn's own return stays the i64 status"
1177        );
1178    }
1179
1180    #[test]
1181    fn the_poll_fn_returns_the_ready_status_and_nothing_else() {
1182        // Task 4's executor panics on any status that is neither
1183        // POLL_PENDING nor POLL_READY, naming the value. An await-free body
1184        // never suspends, so every exit must be exactly POLL_READY -- and the
1185        // returned temp has to be the one a `ConstInt` set to it, not
1186        // whatever the body last computed.
1187        let mut m = module_with_async_const_fn(MirTy::F64, &[]);
1188        transform(&mut m);
1189        let poll = find(&m, "f.15$poll");
1190        let returned: Vec<Temp> = poll
1191            .blocks
1192            .iter()
1193            .filter_map(|b| match &b.term {
1194                Terminator::Return(Some(t)) => Some(*t),
1195                _ => None,
1196            })
1197            .collect();
1198        assert!(!returned.is_empty(), "the poll fn must return a status");
1199        for t in returned {
1200            assert_eq!(
1201                poll.temps[t.0 as usize],
1202                MirTy::I64,
1203                "a poll status is an i64"
1204            );
1205            assert!(
1206                stmts_of(poll)
1207                    .any(|s| matches!(s, Stmt::ConstInt(d, v) if *d == t && *v == POLL_READY)),
1208                "the returned temp %{} must be a constant POLL_READY, not a body value: {:?}",
1209                t.0,
1210                poll.blocks
1211            );
1212        }
1213    }
1214
1215    #[test]
1216    fn a_unit_returning_async_fn_still_writes_the_output_slot() {
1217        // Task 4's `poll_one` reads STATE_SLOT_OUTPUT *unconditionally* on
1218        // completion, including for a future whose `async fn` returns unit and
1219        // whose body never touches that slot. Leaving the slot untouched would
1220        // work only by accident (the allocator zeroes), so the store is
1221        // emitted explicitly.
1222        let mut m = module_with_async_const_fn(MirTy::Unit, &[]);
1223        transform(&mut m);
1224        let poll = find(&m, "f.15$poll");
1225        assert!(
1226            stmts_of(poll).any(|s| matches!(
1227                s,
1228                Stmt::SetField { index, ty, .. }
1229                    if *index == STATE_SLOT_OUTPUT && *ty != MirTy::Unit
1230            )),
1231            "a unit-returning poll fn must still write a real value to the \
1232             output slot (a `ty: Unit` store is a codegen no-op): {:?}",
1233            poll.blocks
1234        );
1235    }
1236
1237    #[test]
1238    fn the_poll_fn_reloads_every_operand_and_stores_every_result_to_its_own_slot() {
1239        // The exact slot indices, in order, for a body that is one binary
1240        // operation on two parameters -- which is the smallest shape with both
1241        // operand kinds and a result. Two classes of defect this discriminates
1242        // and nothing else here does:
1243        //
1244        // - The temp slots starting anywhere other than `STATE_SLOT_TEMPS`.
1245        //   The wrapper computes its seeding index from that constant directly
1246        //   while the body's reloads go through `Spiller::slot`, so the two can
1247        //   disagree -- and then a parameter is written to one slot and read
1248        //   from another, with `SetField`/`RecordField` staying perfectly
1249        //   consistent about the offsets they each use.
1250        // - An operand misfiled as a result in `visit_temps`. That drops the
1251        //   reload of the value the statement was supposed to consume and adds
1252        //   a store of an undefined one, which is visible here as a missing
1253        //   index rather than as the wrong answer at run time.
1254        let mut m = module_with_async_bin_fn(MirTy::F64);
1255        transform(&mut m);
1256        let poll = find(&m, "f.15$poll");
1257        let loads: Vec<u32> = stmts_of(poll)
1258            .filter_map(|s| match s {
1259                Stmt::RecordField { index, .. } => Some(*index),
1260                _ => None,
1261            })
1262            .collect();
1263        let stores: Vec<u32> = stmts_of(poll)
1264            .filter_map(|s| match s {
1265                Stmt::SetField { index, .. } => Some(*index),
1266                _ => None,
1267            })
1268            .collect();
1269        let temps = STATE_SLOT_TEMPS;
1270        assert_eq!(
1271            loads,
1272            vec![temps, temps + 1, temps + 2],
1273            "both operands, then the returned value, each reloaded from its \
1274             own slot: {:?}",
1275            poll.blocks
1276        );
1277        assert_eq!(
1278            stores,
1279            vec![temps + 2, STATE_SLOT_OUTPUT],
1280            "the result to its own slot, then the output slot on completion: \
1281             {:?}",
1282            poll.blocks
1283        );
1284    }
1285
1286    #[test]
1287    fn every_body_value_is_addressed_through_the_state_object() {
1288        // The property a resumable body depends on: no body value lives in a register
1289        // across a block boundary, because every one of them is a slot in the
1290        // state object. Asserted as "every field access in the poll fn is
1291        // against temp 0", which is the state pointer -- a body value left in
1292        // a temp would show up as a `RecordField`/`SetField` against some
1293        // other record, or as no access at all.
1294        let mut m = module_with_async_const_fn(MirTy::F64, &[MirTy::I64]);
1295        transform(&mut m);
1296        let poll = find(&m, "f.15$poll");
1297        let mut accesses = 0;
1298        for s in stmts_of(poll) {
1299            match s {
1300                Stmt::RecordField { record, .. } | Stmt::SetField { record, .. } => {
1301                    accesses += 1;
1302                    assert_eq!(
1303                        *record,
1304                        Temp(0),
1305                        "every state access must go through temp 0, the state pointer"
1306                    );
1307                }
1308                _ => {}
1309            }
1310        }
1311        assert!(accesses > 0, "the poll fn must touch the state object");
1312    }
1313
1314    #[test]
1315    fn the_wrapper_seeds_the_state_with_its_parameters() {
1316        // An async fn's arguments arrive at the WRAPPER, not at poll -- poll's
1317        // only parameters are the state and task_ctx. If the wrapper does not
1318        // copy them into their temp slots, the body reads whatever the
1319        // allocator left there (zero), and `async fn add(a, b) { a + b }`
1320        // silently returns 0.
1321        let mut m = module_with_async_const_fn(MirTy::I64, &[MirTy::F64, MirTy::I64]);
1322        transform(&mut m);
1323        let wrapper = find(&m, "f.15");
1324        for (i, ty) in [(0u32, MirTy::F64), (1, MirTy::I64)] {
1325            assert!(
1326                stmts_of(wrapper).any(|s| matches!(
1327                    s,
1328                    Stmt::SetField { index, value, ty: t, .. }
1329                        if *index == STATE_SLOT_TEMPS + i && *value == Temp(i) && *t == ty
1330                )),
1331                "parameter %{i} must be stored into state slot {} as {ty:?}: {:?}",
1332                STATE_SLOT_TEMPS + i,
1333                wrapper.blocks
1334            );
1335        }
1336    }
1337
1338    #[test]
1339    fn the_wrapper_allocates_a_scanned_state_no_smaller_than_the_minimum() {
1340        // Two Task 4 constraints in one place, because one allocation call
1341        // carries both:
1342        //
1343        // - `RtFunc::Alloc` is `nova_rt_alloc`, which is `gc::alloc(.., true)`
1344        //   -- SCANNED. An unscanned state object is marked but never traced
1345        //   (`gc.rs`'s `if !scan { continue; }`), so a heap-valued output in
1346        //   its output slot would be freed while the executor still names it.
1347        // - The size must never be below `STATE_MIN_SIZE`, because
1348        //   STATE_SLOT_OUTPUT is read on completion even for a future with no
1349        //   temps at all.
1350        let mut m = module_with_async_const_fn(MirTy::Unit, &[]);
1351        let n_temps = m.functions[0].temps.len();
1352        transform(&mut m);
1353        let wrapper = find(&m, "f.15");
1354        let expected = ((STATE_SLOT_TEMPS as usize + n_temps) * 8) as i64;
1355        assert!(expected >= STATE_MIN_SIZE);
1356        let sizes: Vec<i64> = stmts_of(wrapper)
1357            .filter_map(|s| match s {
1358                Stmt::CallRuntime {
1359                    func: RtFunc::Alloc,
1360                    args,
1361                    ..
1362                } => Some(args.clone()),
1363                _ => None,
1364            })
1365            .flatten()
1366            .filter_map(|t| {
1367                stmts_of(wrapper).find_map(|s| match s {
1368                    Stmt::ConstInt(d, v) if *d == t => Some(*v),
1369                    _ => None,
1370                })
1371            })
1372            .collect();
1373        assert_eq!(
1374            sizes,
1375            vec![expected],
1376            "the state object must be one `nova_rt_alloc` of \
1377             (STATE_SLOT_TEMPS + n_temps) * 8 bytes: {:?}",
1378            wrapper.blocks
1379        );
1380    }
1381
1382    #[test]
1383    fn every_return_becomes_its_own_completion_and_a_trap_stays_a_trap() {
1384        // Two terminator shapes an `async fn` body reaches easily -- an early
1385        // `return` gives two `Return`s, and a diverging or exhaustive-match arm
1386        // gives a `Trap` -- and neither is exercised by a single-block fixture.
1387        //
1388        // Both need pinning for opposite reasons. Every `Return` must get its
1389        // OWN output store and status: rewriting only the first would leave the
1390        // second returning whatever the body last computed, which the executor
1391        // would then reject as an out-of-range status at run time. A `Trap` must
1392        // NOT be turned into a completion: it aborts, and rewriting it into
1393        // `POLL_READY` would complete the task with an unwritten output slot.
1394        let mut m = module_with_async_const_fn(MirTy::F64, &[]);
1395        {
1396            let f = &mut m.functions[0];
1397            let second = Temp(f.temps.len() as u32);
1398            f.temps.push(MirTy::F64);
1399            f.blocks.push(Block {
1400                stmts: vec![Stmt::ConstFloat(second, 2.5)],
1401                term: Terminator::Return(Some(second)),
1402            });
1403            f.blocks.push(Block {
1404                stmts: Vec::new(),
1405                term: Terminator::Trap,
1406            });
1407        }
1408        transform(&mut m);
1409        let poll = find(&m, "f.15$poll");
1410
1411        let statuses: Vec<Temp> = poll
1412            .blocks
1413            .iter()
1414            .filter_map(|b| match &b.term {
1415                Terminator::Return(Some(t)) => Some(*t),
1416                _ => None,
1417            })
1418            .collect();
1419        assert_eq!(
1420            statuses.len(),
1421            2,
1422            "both returns must survive: {:?}",
1423            poll.blocks
1424        );
1425        for t in &statuses {
1426            assert!(
1427                stmts_of(poll)
1428                    .any(|s| matches!(s, Stmt::ConstInt(d, v) if d == t && *v == POLL_READY)),
1429                "each return must carry its own POLL_READY constant: {:?}",
1430                poll.blocks
1431            );
1432        }
1433        let output_stores = stmts_of(poll)
1434            .filter(|s| matches!(s, Stmt::SetField { index, .. } if *index == STATE_SLOT_OUTPUT))
1435            .count();
1436        assert_eq!(
1437            output_stores, 2,
1438            "each completion writes the output slot on its own path, since only \
1439             one of them runs: {:?}",
1440            poll.blocks
1441        );
1442        assert_eq!(
1443            poll.blocks
1444                .iter()
1445                .filter(|b| matches!(b.term, Terminator::Trap))
1446                .count(),
1447            1,
1448            "the trap must stay a trap, not become a completion: {:?}",
1449            poll.blocks
1450        );
1451    }
1452
1453    #[test]
1454    fn the_wrapper_writes_the_entry_resume_tag() {
1455        // The invariant is that a fresh future starts in its first state. This
1456        // fixture is await-free, so its poll function never reads the tag, and
1457        // `gc::alloc` hands back zeroed memory -- nothing about *behaviour*
1458        // depends on this store here, which is exactly why it needs an assertion
1459        // rather than a test of an observable effect. It is the only thing making
1460        // the entry tag a property of the generated code instead of the
1461        // allocator's zeroing.
1462        let mut m = module_with_async_const_fn(MirTy::I64, &[]);
1463        transform(&mut m);
1464        let wrapper = find(&m, "f.15");
1465        let tag_value = stmts_of(wrapper)
1466            .find_map(|s| match s {
1467                Stmt::SetField {
1468                    index, value, ty, ..
1469                } if *index == STATE_SLOT_TAG => {
1470                    assert_ne!(*ty, MirTy::Unit, "a `ty: Unit` store is a no-op");
1471                    Some(*value)
1472                }
1473                _ => None,
1474            })
1475            .unwrap_or_else(|| panic!("no store to the tag slot: {:?}", wrapper.blocks));
1476        assert!(
1477            stmts_of(wrapper).any(|s| matches!(s, Stmt::ConstInt(d, 0) if *d == tag_value)),
1478            "the entry tag must be the constant 0, so a fresh future starts in \
1479             its first state: {:?}",
1480            wrapper.blocks
1481        );
1482    }
1483
1484    #[test]
1485    fn the_future_carries_the_state_itself_not_a_record_holding_it() {
1486        // The ABI's word 1 IS the state object's address. `MakeClosure` with a
1487        // capture would allocate an environment RECORD containing the state
1488        // and put *that* in word 1 -- one indirection too many, which the
1489        // runtime would then transmute as a poll function's state and read the
1490        // output slot out of. So the captures list must be empty and word 1
1491        // patched to the state directly.
1492        let mut m = module_with_async_const_fn(MirTy::I64, &[]);
1493        transform(&mut m);
1494        let wrapper = find(&m, "f.15");
1495        let (fut, code) = stmts_of(wrapper)
1496            .find_map(|s| match s {
1497                Stmt::MakeClosure {
1498                    dst,
1499                    code,
1500                    captures,
1501                } => {
1502                    assert!(
1503                        captures.is_empty(),
1504                        "a captured state would sit behind an extra indirection"
1505                    );
1506                    Some((*dst, code.clone()))
1507                }
1508                _ => None,
1509            })
1510            .expect("the wrapper builds a fat pointer");
1511        assert_eq!(code, "f.15$poll", "word 0 must be the poll fn's address");
1512        let state = stmts_of(wrapper)
1513            .find_map(|s| match s {
1514                Stmt::CallRuntime {
1515                    dst: Some(d),
1516                    func: RtFunc::Alloc,
1517                    ..
1518                } => Some(*d),
1519                _ => None,
1520            })
1521            .expect("the wrapper allocates a state");
1522        assert!(
1523            stmts_of(wrapper).any(|s| matches!(
1524                s,
1525                Stmt::SetField { record, index, value, .. }
1526                    if *record == fut && *index == FUTURE_SLOT_STATE && *value == state
1527            )),
1528            "word {FUTURE_SLOT_STATE} of the future must be the state pointer \
1529             itself: {:?}",
1530            wrapper.blocks
1531        );
1532    }
1533
1534    #[test]
1535    fn a_non_async_function_is_left_byte_identical() {
1536        // The pass must be a no-op for everything else. Without this, a greedy
1537        // pass that rewrote every function would still pass the tests above.
1538        let mut m = module_with_plain_fn();
1539        let before = format!("{:?}", m.functions);
1540        transform(&mut m);
1541        assert_eq!(before, format!("{:?}", m.functions));
1542    }
1543
1544    #[test]
1545    fn the_transform_is_idempotent() {
1546        // Running the pass twice must not produce `f.15$poll$poll`. It clears
1547        // `is_async` on what it rewrites, which is also the invariant
1548        // `lower_module` asserts before handing the module to codegen: a
1549        // function still flagged async has a return class that came from its
1550        // BODY, not from `Future<T>`, and would miscompile.
1551        let mut m = module_with_async_const_fn(MirTy::F64, &[]);
1552        transform(&mut m);
1553        let once = format!("{:?}", m.functions);
1554        transform(&mut m);
1555        assert_eq!(once, format!("{:?}", m.functions));
1556        assert!(
1557            m.functions.iter().all(|f| !f.is_async),
1558            "no function may still be flagged async after the pass"
1559        );
1560    }
1561
1562    #[test]
1563    fn the_state_layout_constants_match_the_runtime() {
1564        // `nova-mir` must not depend on `nova-runtime`, so the layout is
1565        // declared twice. This pins THIS copy to its literal values; the pin
1566        // that actually compares the two copies lives in
1567        // `nova-codegen-cranelift`, the one crate that depends on both.
1568        assert_eq!(STATE_SLOT_TAG, 0);
1569        assert_eq!(STATE_SLOT_OUTPUT, 1);
1570        assert_eq!(STATE_SLOT_TEMPS, 2);
1571        assert_eq!(STATE_MIN_SIZE, 16);
1572        assert_eq!(POLL_PENDING, 0);
1573        assert_eq!(POLL_READY, 1);
1574        assert_eq!(FUTURE_SLOT_POLL, 0);
1575        assert_eq!(FUTURE_SLOT_STATE, 1);
1576    }
1577
1578    // === splitting at await points ===
1579
1580    /// A one-function `Module` holding an `async fn` with the given declared
1581    /// body class, parameter count, temps and blocks — the shape
1582    /// [`crate::lower::lower_function`] leaves before `transform` runs, for a
1583    /// body built by hand here rather than compiled from source.
1584    ///
1585    /// Hand-built for the same reason [`module_with_async_const_fn`] is: the
1586    /// input to `transform` stays fixed and visible, and a block layout with a
1587    /// back edge into a block that gets split is expressible directly.
1588    /// `tests/lower_tests.rs` covers the same shapes from real source.
1589    fn async_module(ret: MirTy, params: u32, temps: Vec<MirTy>, blocks: Vec<Block>) -> Module {
1590        Module {
1591            functions: vec![Function {
1592                name: "f.15".to_string(),
1593                params,
1594                takes_env: false,
1595                capture_count: 0,
1596                temps,
1597                ret,
1598                is_async: true,
1599                blocks,
1600            }],
1601            externs: Vec::new(),
1602        }
1603    }
1604
1605    /// `async fn f() -> T { g().await }`: one suspend point, in the entry
1606    /// block, whose result is what the body returns. Temp 0 holds the future
1607    /// `g()` produced, temp 1 the awaited value.
1608    fn module_with_async_fn_awaiting_once(out: MirTy) -> Module {
1609        async_module(
1610            out,
1611            0,
1612            vec![MirTy::Ptr, out],
1613            vec![Block {
1614                stmts: vec![
1615                    Stmt::Call {
1616                        dst: Some(Temp(0)),
1617                        callee: "g.16".to_string(),
1618                        args: Vec::new(),
1619                    },
1620                    Stmt::Await {
1621                        dst: Some(Temp(1)),
1622                        future: Temp(0),
1623                    },
1624                ],
1625                term: Terminator::Return(Some(Temp(1))),
1626            }],
1627        )
1628    }
1629
1630    /// `async fn f() -> T { g().await + h().await }`: two suspend points in one
1631    /// block, so the second await's own leading piece is a piece of a piece —
1632    /// and the two tags can be told apart.
1633    fn module_with_async_fn_awaiting_twice(out: MirTy) -> Module {
1634        async_module(
1635            out,
1636            0,
1637            vec![MirTy::Ptr, out, MirTy::Ptr, out, out],
1638            vec![Block {
1639                stmts: vec![
1640                    Stmt::Call {
1641                        dst: Some(Temp(0)),
1642                        callee: "g.16".to_string(),
1643                        args: Vec::new(),
1644                    },
1645                    Stmt::Await {
1646                        dst: Some(Temp(1)),
1647                        future: Temp(0),
1648                    },
1649                    Stmt::Call {
1650                        dst: Some(Temp(2)),
1651                        callee: "h.17".to_string(),
1652                        args: Vec::new(),
1653                    },
1654                    Stmt::Await {
1655                        dst: Some(Temp(3)),
1656                        future: Temp(2),
1657                    },
1658                    Stmt::Bin {
1659                        dst: Temp(4),
1660                        op: nova_hir::BinOp::Add,
1661                        class: crate::OperandClass::Float,
1662                        lhs: Temp(1),
1663                        rhs: Temp(3),
1664                    },
1665                ],
1666                term: Terminator::Return(Some(Temp(4))),
1667            }],
1668        )
1669    }
1670
1671    /// `async fn f() -> T { while c { v = g().await }\n v }`: the suspend point
1672    /// is inside a loop body, so one tag is resumed at repeatedly and the loop's
1673    /// back edge targets a block the split renumbered.
1674    fn module_with_async_fn_awaiting_in_a_loop(out: MirTy) -> Module {
1675        async_module(
1676            out,
1677            0,
1678            vec![MirTy::I8, MirTy::Ptr, out],
1679            vec![
1680                // 0: entry — falls into the header.
1681                Block {
1682                    stmts: Vec::new(),
1683                    term: Terminator::Goto(BlockId(1)),
1684                },
1685                // 1: header — re-tests the condition on every iteration.
1686                Block {
1687                    stmts: vec![Stmt::ConstBool(Temp(0), true)],
1688                    term: Terminator::Branch {
1689                        cond: Temp(0),
1690                        then_: BlockId(2),
1691                        else_: BlockId(3),
1692                    },
1693                },
1694                // 2: body — the await, then the back edge.
1695                Block {
1696                    stmts: vec![
1697                        Stmt::Call {
1698                            dst: Some(Temp(1)),
1699                            callee: "g.16".to_string(),
1700                            args: Vec::new(),
1701                        },
1702                        Stmt::Await {
1703                            dst: Some(Temp(2)),
1704                            future: Temp(1),
1705                        },
1706                    ],
1707                    term: Terminator::Goto(BlockId(1)),
1708                },
1709                // 3: exit.
1710                Block {
1711                    stmts: Vec::new(),
1712                    term: Terminator::Return(Some(Temp(2))),
1713                },
1714            ],
1715        )
1716    }
1717
1718    /// `async fn f() -> T { match c { A => g().await, B => v } }`: the block
1719    /// holding the await is reached through a `Switch`, which is the only
1720    /// terminator shape whose targets live in a `Vec` rather than in named
1721    /// fields — so it is the one the renumbering can miss on its own.
1722    ///
1723    /// The `B` arm leaves temp 2 undefined, which on that path would read the
1724    /// allocator's zero. Never taken: the scrutinee is the constant 0. This
1725    /// fixture is about which blocks the terminators name, not about the value.
1726    fn module_with_async_fn_awaiting_in_a_match(out: MirTy) -> Module {
1727        async_module(
1728            out,
1729            0,
1730            vec![MirTy::I64, MirTy::Ptr, out],
1731            vec![
1732                // 0: the scrutinee and the dispatch over the arms.
1733                Block {
1734                    stmts: vec![Stmt::ConstInt(Temp(0), 0)],
1735                    term: Terminator::Switch {
1736                        disc: Temp(0),
1737                        arms: vec![(0, BlockId(1)), (1, BlockId(2))],
1738                        default: BlockId(3),
1739                    },
1740                },
1741                // 1: the `A` arm — awaits, then joins.
1742                Block {
1743                    stmts: vec![
1744                        Stmt::Call {
1745                            dst: Some(Temp(1)),
1746                            callee: "g.16".to_string(),
1747                            args: Vec::new(),
1748                        },
1749                        Stmt::Await {
1750                            dst: Some(Temp(2)),
1751                            future: Temp(1),
1752                        },
1753                    ],
1754                    term: Terminator::Goto(BlockId(4)),
1755                },
1756                // 2: the `B` arm.
1757                Block {
1758                    stmts: Vec::new(),
1759                    term: Terminator::Goto(BlockId(4)),
1760                },
1761                // 3: the exhaustive-match default.
1762                Block {
1763                    stmts: Vec::new(),
1764                    term: Terminator::Trap,
1765                },
1766                // 4: the join.
1767                Block {
1768                    stmts: Vec::new(),
1769                    term: Terminator::Return(Some(Temp(2))),
1770                },
1771            ],
1772        )
1773    }
1774
1775    /// `async fn f() { g().await }` with `g` returning unit: an await with no
1776    /// value to copy out of the inner future's output slot.
1777    fn module_with_async_fn_awaiting_unit() -> Module {
1778        async_module(
1779            MirTy::Unit,
1780            0,
1781            vec![MirTy::Ptr],
1782            vec![Block {
1783                stmts: vec![
1784                    Stmt::Call {
1785                        dst: Some(Temp(0)),
1786                        callee: "g.16".to_string(),
1787                        args: Vec::new(),
1788                    },
1789                    Stmt::Await {
1790                        dst: None,
1791                        future: Temp(0),
1792                    },
1793                ],
1794                term: Terminator::Return(None),
1795            }],
1796        )
1797    }
1798
1799    /// The arms of a poll function's entry dispatch, panicking if the entry
1800    /// block is not one.
1801    fn resume_arms(poll: &Function) -> Vec<(i64, BlockId)> {
1802        match &poll.blocks[0].term {
1803            Terminator::Switch { arms, .. } => arms.clone(),
1804            other => panic!("the entry block must dispatch on the resume tag, found {other:?}"),
1805        }
1806    }
1807
1808    /// The block a resume point branches to for inner-poll status `status`.
1809    fn status_arm(poll: &Function, resume: BlockId, status: i64) -> BlockId {
1810        match &poll.blocks[resume.0 as usize].term {
1811            Terminator::Switch { arms, .. } => *arms
1812                .iter()
1813                .find_map(|(v, b)| (*v == status).then_some(b))
1814                .unwrap_or_else(|| {
1815                    panic!("resume block {resume:?} has no arm for status {status}: {arms:?}")
1816                }),
1817            other => {
1818                panic!("a resume point must branch on the inner poll's status, found {other:?}")
1819            }
1820        }
1821    }
1822
1823    /// Every constant `block` stores into the resume-tag slot, resolved through
1824    /// the `ConstInt` that produced it.
1825    fn tag_stores(poll: &Function, block: BlockId) -> Vec<i64> {
1826        let b = &poll.blocks[block.0 as usize];
1827        b.stmts
1828            .iter()
1829            .filter_map(|s| match s {
1830                Stmt::SetField {
1831                    record,
1832                    index,
1833                    value,
1834                    ..
1835                } if *record == STATE && *index == STATE_SLOT_TAG => Some(*value),
1836                _ => None,
1837            })
1838            .map(|v| {
1839                b.stmts
1840                    .iter()
1841                    .find_map(|s| match s {
1842                        Stmt::ConstInt(d, k) if *d == v => Some(*k),
1843                        _ => None,
1844                    })
1845                    .unwrap_or_else(|| {
1846                        panic!(
1847                            "the tag stored must be a constant defined in the same block: {:?}",
1848                            b.stmts
1849                        )
1850                    })
1851            })
1852            .collect()
1853    }
1854
1855    /// Every block id any terminator in `f` names.
1856    fn targets(f: &Function) -> Vec<BlockId> {
1857        f.blocks
1858            .iter()
1859            .flat_map(|b| match &b.term {
1860                Terminator::Goto(t) => vec![*t],
1861                Terminator::Branch { then_, else_, .. } => vec![*then_, *else_],
1862                Terminator::Switch { arms, default, .. } => arms
1863                    .iter()
1864                    .map(|(_, b)| *b)
1865                    .chain(std::iter::once(*default))
1866                    .collect(),
1867                Terminator::Return(_) | Terminator::Trap => Vec::new(),
1868            })
1869            .collect()
1870    }
1871
1872    #[test]
1873    fn one_await_produces_two_resume_states_dispatched_by_a_switch() {
1874        let mut m = module_with_async_fn_awaiting_once(MirTy::F64);
1875        transform(&mut m);
1876        let poll = find(&m, "f.15$poll");
1877        let arms = resume_arms(poll);
1878        assert!(
1879            arms.len() >= 2,
1880            "one await means two resume states, got {arms:?}"
1881        );
1882        // The tags must be DISTINCT. A switch whose arms all target one block is
1883        // the mutation this test exists to kill.
1884        let distinct: std::collections::HashSet<_> = arms.iter().map(|(_, b)| *b).collect();
1885        assert_eq!(
1886            distinct.len(),
1887            arms.len(),
1888            "resume arms must target distinct blocks: {arms:?}"
1889        );
1890    }
1891
1892    #[test]
1893    fn a_suspend_stores_a_resume_tag_before_returning_pending() {
1894        // Without the store, the task resumes at state 0 forever -- an infinite
1895        // loop, not a wrong value, which is why an output-only assertion misses
1896        // it.
1897        let mut m = module_with_async_fn_awaiting_once(MirTy::F64);
1898        transform(&mut m);
1899        let poll = find(&m, "f.15$poll");
1900        let stores_tag = poll.blocks.iter().any(|b| {
1901            b.stmts.iter().any(|s| {
1902                matches!(s, Stmt::SetField { record, index, .. }
1903                    if *record == STATE && *index == STATE_SLOT_TAG)
1904            })
1905        });
1906        assert!(stores_tag, "a suspend must record a resume tag");
1907    }
1908
1909    #[test]
1910    fn a_pending_inner_future_suspends_at_the_tag_that_returns_to_the_same_await() {
1911        // **The same-tag rule.** When the inner future reports PENDING, the tag
1912        // stored must be the one whose resume arm comes BACK to this await --
1913        // not the one belonging to the await after it. Storing `tag + 1` resumes
1914        // the task past a suspend point it never finished: the continuation then
1915        // reads an output slot the inner future has not written, which is a
1916        // wrong value on a path that still completes normally, so neither the
1917        // poll statuses nor the block count nor the arm count can see it.
1918        //
1919        // Two awaits, so "this await's tag" and "the next await's tag" are
1920        // different numbers. With one await, storing `tag + 1` would land on the
1921        // switch's default and trap, which is a different (and louder) failure.
1922        let mut m = module_with_async_fn_awaiting_twice(MirTy::F64);
1923        transform(&mut m);
1924        let poll = find(&m, "f.15$poll");
1925        let resumes: Vec<(i64, BlockId)> = resume_arms(poll)
1926            .into_iter()
1927            .filter(|(tag, _)| *tag != 0)
1928            .collect();
1929        assert_eq!(resumes.len(), 2, "two awaits, two resume tags: {resumes:?}");
1930        for (tag, resume) in resumes {
1931            let pending = status_arm(poll, resume, POLL_PENDING);
1932            assert_eq!(
1933                tag_stores(poll, pending),
1934                vec![tag],
1935                "the suspend reached from tag {tag}'s resume point must store \
1936                 {tag} again, so the next poll retries the same await: {:?}",
1937                poll.blocks
1938            );
1939            // And it must report PENDING. Returning READY here would complete the
1940            // task out of an output slot the awaited future has not written, and
1941            // the tag assertion above cannot see that: the store and the status
1942            // are independent. Checked against the constant rather than against
1943            // "not READY", since the executor accepts only the two.
1944            let block = &poll.blocks[pending.0 as usize];
1945            let status = match &block.term {
1946                Terminator::Return(Some(t)) => *t,
1947                other => panic!("a suspend must return a status, found {other:?}"),
1948            };
1949            assert!(
1950                block.stmts.iter().any(|s| matches!(s, Stmt::ConstInt(d, v)
1951                        if *d == status && *v == POLL_PENDING)),
1952                "the suspend must return a constant POLL_PENDING defined in its \
1953                 own block: {block:?}"
1954            );
1955        }
1956    }
1957
1958    #[test]
1959    fn the_resume_dispatch_switches_on_the_tag_slot_and_enters_the_body_at_tag_zero() {
1960        // Three things a resume dispatch can get wrong independently of each
1961        // other, all of them silent:
1962        //
1963        // - switching on something other than the tag slot (the output slot is
1964        //   the adjacent index and is also an i64);
1965        // - not reserving tag 0 for the body's own entry, which would make a
1966        //   fresh future -- whose tag the wrapper sets to 0 -- resume at an
1967        //   await whose future does not exist yet;
1968        // - collapsing the later tags onto one block.
1969        let mut m = module_with_async_fn_awaiting_twice(MirTy::F64);
1970        transform(&mut m);
1971        let poll = find(&m, "f.15$poll");
1972        let entry = &poll.blocks[0];
1973        let disc = match &entry.term {
1974            Terminator::Switch { disc, .. } => *disc,
1975            other => panic!("the entry must dispatch on the resume tag, found {other:?}"),
1976        };
1977        assert!(
1978            entry.stmts.iter().any(|s| matches!(
1979                s,
1980                Stmt::RecordField { dst, record, index, ty }
1981                    if *dst == disc && *record == STATE && *index == STATE_SLOT_TAG
1982                        && *ty == MirTy::I64
1983            )),
1984            "the discriminant must be loaded from the tag slot: {:?}",
1985            entry
1986        );
1987        let arms = resume_arms(poll);
1988        assert_eq!(
1989            arms.iter().map(|(t, _)| *t).collect::<Vec<_>>(),
1990            vec![0, 1, 2],
1991            "one arm for the entry state plus one per await, in order: {arms:?}"
1992        );
1993        let distinct: std::collections::HashSet<_> = arms.iter().map(|(_, b)| *b).collect();
1994        assert_eq!(distinct.len(), 3, "every state is its own block: {arms:?}");
1995    }
1996
1997    #[test]
1998    fn awaiting_polls_the_inner_future_through_its_own_fat_pointer() {
1999        // A future value *is* a `{ poll_code, state }` fat pointer, and
2000        // `CallIndirect` already means `code_ptr(env_ptr, args...)` -- so an
2001        // indirect call through the awaited future is a `PollFn` call, with the
2002        // inner state object arriving as the env. The signature has to be
2003        // exactly `(Ptr, Ptr) -> I64`: `params: [Ptr]` is task_ctx, the leading
2004        // env is implicit, and `ret: I64` is the status. Forwarding this
2005        // function's own task_ctx (temp 1) rather than a fresh null keeps the
2006        // parameter meaningful once anything uses it.
2007        let mut m = module_with_async_fn_awaiting_once(MirTy::F64);
2008        transform(&mut m);
2009        let poll = find(&m, "f.15$poll");
2010        let (callee, block) = poll
2011            .blocks
2012            .iter()
2013            .find_map(|b| {
2014                b.stmts.iter().find_map(|s| match s {
2015                    Stmt::CallIndirect {
2016                        dst: Some(_),
2017                        callee,
2018                        params,
2019                        ret,
2020                        args,
2021                    } => {
2022                        assert_eq!(params, &[MirTy::Ptr], "the one real param is task_ctx");
2023                        assert_eq!(*ret, MirTy::I64, "an inner poll returns a status");
2024                        assert_eq!(args, &[TASK_CTX], "task_ctx must be forwarded");
2025                        Some((*callee, b))
2026                    }
2027                    _ => None,
2028                })
2029            })
2030            .unwrap_or_else(|| panic!("the await must poll the inner future: {:?}", poll.blocks));
2031        assert!(
2032            block.stmts.iter().any(|s| matches!(
2033                s,
2034                Stmt::RecordField { dst, record, index, ty }
2035                    if *dst == callee && *record == STATE
2036                        && *index == STATE_SLOT_TEMPS && *ty == MirTy::Ptr
2037            )),
2038            "the awaited future must be reloaded from its own temp slot in the \
2039             same block as the call: {:?}",
2040            block
2041        );
2042    }
2043
2044    #[test]
2045    fn an_awaited_value_is_read_from_the_inner_futures_own_output_slot() {
2046        // The chain that has to be exactly right, at `Float` because it is the
2047        // one class that crosses register banks: word `FUTURE_SLOT_STATE` of the
2048        // awaited future is the inner state object, and `STATE_SLOT_OUTPUT` of
2049        // THAT object is the awaited value. Reading the future's word 1 *as* the
2050        // value, or reading this function's own output slot, are both mistakes
2051        // an `Int` fixture cannot distinguish, since every step is an i64-shaped
2052        // load of an 8-byte slot.
2053        let mut m = module_with_async_fn_awaiting_once(MirTy::F64);
2054        transform(&mut m);
2055        let poll = find(&m, "f.15$poll");
2056        let inner = stmts_of(poll)
2057            .find_map(|s| match s {
2058                Stmt::RecordField {
2059                    dst,
2060                    record,
2061                    index,
2062                    ty,
2063                } if *index == FUTURE_SLOT_STATE && *record != STATE => {
2064                    assert_eq!(*ty, MirTy::Ptr, "the inner state is a pointer");
2065                    Some(*dst)
2066                }
2067                _ => None,
2068            })
2069            .unwrap_or_else(|| {
2070                panic!(
2071                    "the await must load the inner state out of the future: {:?}",
2072                    poll.blocks
2073                )
2074            });
2075        let value = stmts_of(poll)
2076            .find_map(|s| match s {
2077                Stmt::RecordField {
2078                    dst,
2079                    record,
2080                    index,
2081                    ty,
2082                } if *record == inner && *index == STATE_SLOT_OUTPUT => {
2083                    assert_eq!(*ty, MirTy::F64, "the awaited value keeps its own class");
2084                    Some(*dst)
2085                }
2086                _ => None,
2087            })
2088            .unwrap_or_else(|| {
2089                panic!(
2090                    "the awaited value must come from the inner state's output \
2091                     slot: {:?}",
2092                    poll.blocks
2093                )
2094            });
2095        assert!(
2096            stmts_of(poll).any(|s| matches!(
2097                s,
2098                Stmt::SetField { record, index, value: v, ty }
2099                    if *record == STATE && *index == STATE_SLOT_TEMPS + 1
2100                        && *v == value && *ty == MirTy::F64
2101            )),
2102            "the awaited value must be stored into the await result's own temp \
2103             slot, as an f64: {:?}",
2104            poll.blocks
2105        );
2106    }
2107
2108    #[test]
2109    fn an_awaited_value_reaches_its_slot_before_the_statements_that_read_it() {
2110        // Order, which a whole-function scan of the statements cannot see. In the
2111        // two-await fixture the second await's result is temp 3 and the very next
2112        // statement reads it, so that await's continuation both stores and reads
2113        // slot `STATE_SLOT_TEMPS + 3`.
2114        //
2115        // Emitted after the statements that followed the await, the copy out of
2116        // the inner state object still exists, still names the right slot and
2117        // still reads the right class, so nothing about its presence or its shape
2118        // distinguishes it -- position is the only thing that does. What breaks
2119        // is that the expression the await feeds reads whatever the slot held
2120        // beforehand, which on a first pass is the allocator's zero.
2121        let mut m = module_with_async_fn_awaiting_twice(MirTy::F64);
2122        transform(&mut m);
2123        let poll = find(&m, "f.15$poll");
2124        let second = resume_arms(poll)
2125            .into_iter()
2126            .find_map(|(tag, b)| (tag == FIRST_RESUME_TAG + 1).then_some(b))
2127            .expect("a second resume tag");
2128        let cont = status_arm(poll, second, POLL_READY);
2129        let stmts = &poll.blocks[cont.0 as usize].stmts;
2130        let slot = STATE_SLOT_TEMPS + 3;
2131        let store = stmts
2132            .iter()
2133            .position(|s| {
2134                matches!(s, Stmt::SetField { record, index, .. }
2135                    if *record == STATE && *index == slot)
2136            })
2137            .unwrap_or_else(|| panic!("no store of the awaited value: {stmts:?}"));
2138        let read = stmts
2139            .iter()
2140            .position(|s| {
2141                matches!(s, Stmt::RecordField { record, index, .. }
2142                    if *record == STATE && *index == slot)
2143            })
2144            .unwrap_or_else(|| {
2145                panic!("the fixture's next statement must read the awaited value: {stmts:?}")
2146            });
2147        assert!(
2148            store < read,
2149            "the awaited value must be in its slot before anything reads it: {stmts:?}"
2150        );
2151    }
2152
2153    #[test]
2154    fn an_inner_poll_status_that_is_neither_pending_nor_ready_traps() {
2155        // The mirror of the executor's own strictness. A generated poll function
2156        // returns exactly PENDING or READY, so an awaited future reporting
2157        // anything else is a compiler bug -- and treating an unrecognized status
2158        // as ready would complete the await from an output slot nothing wrote.
2159        // A trap aborts, which is also what keeps a panic from having to cross
2160        // this frame's boundary.
2161        let mut m = module_with_async_fn_awaiting_once(MirTy::F64);
2162        transform(&mut m);
2163        let poll = find(&m, "f.15$poll");
2164        let resume = resume_arms(poll)
2165            .into_iter()
2166            .find_map(|(tag, b)| (tag != 0).then_some(b))
2167            .expect("a resume arm");
2168        let (arms, default) = match &poll.blocks[resume.0 as usize].term {
2169            Terminator::Switch { arms, default, .. } => (arms.clone(), *default),
2170            other => panic!("expected a status switch, found {other:?}"),
2171        };
2172        let mut values: Vec<i64> = arms.iter().map(|(v, _)| *v).collect();
2173        values.sort_unstable();
2174        assert_eq!(
2175            values,
2176            vec![POLL_PENDING, POLL_READY],
2177            "exactly the two statuses the ABI defines may be recognized: {arms:?}"
2178        );
2179        assert!(
2180            matches!(poll.blocks[default.0 as usize].term, Terminator::Trap),
2181            "an unrecognized status must reach a trap, not fall into a state: {:?}",
2182            poll.blocks
2183        );
2184    }
2185
2186    #[test]
2187    fn a_unit_awaits_result_is_not_copied_out_of_the_inner_state() {
2188        // Nothing to copy: a unit-output future writes an explicit zero to its
2189        // output slot and the awaiting body has no temp to put it in. Asserted
2190        // as "the poll function never chases a pointer other than the state
2191        // object", which is what a spurious extraction would break.
2192        let mut m = module_with_async_fn_awaiting_unit();
2193        transform(&mut m);
2194        let poll = find(&m, "f.15$poll");
2195        for s in stmts_of(poll) {
2196            if let Stmt::RecordField { record, index, .. } = s {
2197                assert_eq!(
2198                    *record, STATE,
2199                    "a unit await must not read field {index} of the inner \
2200                     future: {:?}",
2201                    poll.blocks
2202                );
2203            }
2204        }
2205        // It must still suspend and resume properly.
2206        assert_eq!(resume_arms(poll).len(), 2, "{:?}", poll.blocks);
2207    }
2208
2209    #[test]
2210    fn an_await_in_a_loop_resumes_at_the_await_and_keeps_its_back_edge() {
2211        // The same suspend point, resumed more than once. Two failures a
2212        // straight-line fixture cannot reach:
2213        //
2214        // - The loop's back edge has to follow the renumbering the split
2215        //   introduces. Left pointing at its pre-split id it lands in whatever
2216        //   block now carries that number, which for a resumable poll function
2217        //   is the shared trap block -- the loop would abort on its second
2218        //   iteration.
2219        // - The resume arm has to target the await's own poll block, not the
2220        //   loop header. Resuming at the header would re-test the condition and
2221        //   build a second future, abandoning the one already in flight.
2222        let mut m = module_with_async_fn_awaiting_in_a_loop(MirTy::F64);
2223        transform(&mut m);
2224        let poll = find(&m, "f.15$poll");
2225
2226        let header = poll
2227            .blocks
2228            .iter()
2229            .position(|b| matches!(b.term, Terminator::Branch { .. }))
2230            .expect("the loop header survives as a conditional branch");
2231        assert!(
2232            poll.blocks[header]
2233                .stmts
2234                .iter()
2235                .any(|s| matches!(s, Stmt::ConstBool(..))),
2236            "the header must be the block that re-tests the condition: {:?}",
2237            poll.blocks
2238        );
2239        assert!(
2240            poll.blocks
2241                .iter()
2242                .any(|b| matches!(b.term, Terminator::Goto(t) if t.0 as usize == header)),
2243            "the back edge must reach the renumbered header (block {header}): {:?}",
2244            poll.blocks
2245        );
2246
2247        let resume = resume_arms(poll)
2248            .into_iter()
2249            .find_map(|(tag, b)| (tag != 0).then_some(b))
2250            .expect("a resume arm");
2251        assert!(
2252            poll.blocks[resume.0 as usize]
2253                .stmts
2254                .iter()
2255                .any(|s| matches!(s, Stmt::CallIndirect { .. })),
2256            "resuming must re-poll the awaited future, not re-enter the loop \
2257             header: {:?}",
2258            poll.blocks
2259        );
2260
2261        // The header's own two arms have to be renumbered as well, and nothing
2262        // above reaches them: `Branch`'s targets are named fields, so leaving
2263        // them alone is its own defect, and un-remapped they name the entry
2264        // piece and the header itself -- both past the reserved ids, so the
2265        // reserved-block property is blind to it, and both give a loop that
2266        // never leaves. Identified by what each target *does* rather than by its
2267        // id, so this also rejects the two being swapped.
2268        let (then_, else_) = match &poll.blocks[header].term {
2269            Terminator::Branch { then_, else_, .. } => (*then_, *else_),
2270            other => panic!("expected the header's branch, found {other:?}"),
2271        };
2272        assert!(
2273            poll.blocks[then_.0 as usize]
2274                .stmts
2275                .iter()
2276                .any(|s| matches!(s, Stmt::Call { .. })),
2277            "the taken arm must reach the loop body's leading piece, the block \
2278             that builds the awaited future: {:?}",
2279            poll.blocks
2280        );
2281        assert!(
2282            matches!(poll.blocks[else_.0 as usize].term, Terminator::Return(_)),
2283            "the untaken arm must reach the exit, which completes: {:?}",
2284            poll.blocks
2285        );
2286    }
2287
2288    #[test]
2289    fn every_exit_from_a_resumable_poll_fn_returns_a_constant_pending_or_ready() {
2290        // The executor panics on any status that is neither, and that panic
2291        // cannot unwind out of a generated frame -- it kills the process. So
2292        // every `Return` must carry a constant one of the two, never the inner
2293        // poll's status forwarded (which is only PENDING by coincidence on that
2294        // path) and never a body value.
2295        for mut m in every_awaiting_module() {
2296            transform(&mut m);
2297            let poll = find(&m, "f.15$poll");
2298            let returned: Vec<Temp> = poll
2299                .blocks
2300                .iter()
2301                .filter_map(|b| match &b.term {
2302                    Terminator::Return(Some(t)) => Some(*t),
2303                    _ => None,
2304                })
2305                .collect();
2306            assert!(!returned.is_empty(), "{:?}", poll.blocks);
2307            for t in returned {
2308                assert_eq!(poll.temps[t.0 as usize], MirTy::I64, "a status is an i64");
2309                let v = stmts_of(poll)
2310                    .find_map(|s| match s {
2311                        Stmt::ConstInt(d, v) if *d == t => Some(*v),
2312                        _ => None,
2313                    })
2314                    .unwrap_or_else(|| {
2315                        panic!(
2316                            "the returned temp %{} must be a status constant: {:?}",
2317                            t.0, poll.blocks
2318                        )
2319                    });
2320                assert!(
2321                    v == POLL_PENDING || v == POLL_READY,
2322                    "a poll function may return only PENDING or READY, found {v}: {:?}",
2323                    poll.blocks
2324                );
2325            }
2326        }
2327    }
2328
2329    /// Every await fixture in this module, so a whole-function invariant is
2330    /// checked against all of them rather than whichever one it was written for.
2331    fn every_awaiting_module() -> Vec<Module> {
2332        vec![
2333            module_with_async_fn_awaiting_once(MirTy::F64),
2334            module_with_async_fn_awaiting_twice(MirTy::F64),
2335            module_with_async_fn_awaiting_in_a_loop(MirTy::F64),
2336            module_with_async_fn_awaiting_in_a_match(MirTy::F64),
2337            module_with_async_fn_awaiting_unit(),
2338        ]
2339    }
2340
2341    #[test]
2342    fn no_terminator_names_a_reserved_block_except_a_status_switchs_default() {
2343        // The renumbering, asserted as a property instead of by recomputing the
2344        // block ids the split chose -- which would only restate the arithmetic
2345        // that produced them.
2346        //
2347        // A resumable poll function keeps the two lowest ids for its dispatch and
2348        // its trap block, so any *other* terminator still naming one of them is
2349        // one the split failed to remap. Neither outcome is a crash at the seam:
2350        // reaching the trap aborts, and re-entering the dispatch re-reads the tag
2351        // and jumps somewhere plausible. `Switch` is the shape most exposed,
2352        // because its targets are a `Vec` the remap has to walk rather than named
2353        // fields the compiler would notice were unused.
2354        //
2355        // The one deliberate reference to a reserved block is `BAD_DISCRIMINANT`
2356        // as a switch `default`, so that is the only exemption.
2357        for mut m in every_awaiting_module() {
2358            transform(&mut m);
2359            let poll = find(&m, "f.15$poll");
2360            for (i, b) in poll.blocks.iter().enumerate() {
2361                let named: Vec<BlockId> = match &b.term {
2362                    Terminator::Goto(t) => vec![*t],
2363                    Terminator::Branch { then_, else_, .. } => vec![*then_, *else_],
2364                    Terminator::Switch { arms, default, .. } => arms
2365                        .iter()
2366                        .map(|(_, b)| *b)
2367                        .chain((*default != BAD_DISCRIMINANT).then_some(*default))
2368                        .collect(),
2369                    Terminator::Return(_) | Terminator::Trap => Vec::new(),
2370                };
2371                for t in named {
2372                    assert!(
2373                        t.0 >= RESERVED_BLOCKS,
2374                        "block {i} targets reserved block {t:?}, so the split did \
2375                         not renumber it: {:?}",
2376                        poll.blocks
2377                    );
2378                }
2379            }
2380        }
2381    }
2382
2383    #[test]
2384    fn a_body_switchs_default_is_renumbered_along_with_its_arms() {
2385        // `Switch::default` is a separate field from `Switch::arms`, so leaving
2386        // only the default un-remapped is its own defect -- and the reserved-block
2387        // property above cannot see it. That test exempts any default equal to
2388        // `BAD_DISCRIMINANT` and otherwise only requires `>= RESERVED_BLOCKS`, and
2389        // the match fixture's original default is `BlockId(3)`, which clears that
2390        // bar while naming a completely different block.
2391        //
2392        // In the fixture the original default is the exhaustive-match trap, and
2393        // after the split it is still the only `Trap`-terminated block the body
2394        // has -- so "the default still reaches a trap" pins it exactly. Left
2395        // un-remapped, `BlockId(3)` is the awaiting arm's leading piece, so the
2396        // defect builds a second future and polls it instead of aborting. That is
2397        // also why no runtime test can catch this one: the default is an
2398        // unreachable exhaustive-match trap, so neither a source-level `match`
2399        // test nor a driver probe ever branches through it.
2400        let mut m = module_with_async_fn_awaiting_in_a_match(MirTy::F64);
2401        transform(&mut m);
2402        let poll = find(&m, "f.15$poll");
2403        let defaults: Vec<BlockId> = poll
2404            .blocks
2405            .iter()
2406            .filter_map(|b| match &b.term {
2407                Terminator::Switch { default, .. } if *default != BAD_DISCRIMINANT => {
2408                    Some(*default)
2409                }
2410                _ => None,
2411            })
2412            .collect();
2413        assert_eq!(
2414            defaults.len(),
2415            1,
2416            "the fixture has exactly one `Switch` of its own, and its default must \
2417             not have been collapsed onto the shared trap: {:?}",
2418            poll.blocks
2419        );
2420        assert!(
2421            matches!(poll.blocks[defaults[0].0 as usize].term, Terminator::Trap),
2422            "the body `Switch`'s default must still reach the trap block it named \
2423             before the split, at its new id: {:?}",
2424            poll.blocks
2425        );
2426    }
2427
2428    #[test]
2429    fn the_split_leaves_no_await_marker_and_no_dangling_block_target() {
2430        // Two whole-function invariants, over every await shape here. A
2431        // surviving marker reaches a codegen backend, which has no machine code
2432        // for it; a block target past the end of the block list indexes out of
2433        // bounds inside the backend rather than being diagnosed.
2434        for mut m in every_awaiting_module() {
2435            transform(&mut m);
2436            for f in &m.functions {
2437                assert!(
2438                    !stmts_of(f).any(|s| matches!(s, Stmt::Await { .. })),
2439                    "`{}` still contains an await marker: {:?}",
2440                    f.name,
2441                    f.blocks
2442                );
2443                for t in targets(f) {
2444                    assert!(
2445                        (t.0 as usize) < f.blocks.len(),
2446                        "`{}` jumps to {t:?}, past its {} blocks: {:?}",
2447                        f.name,
2448                        f.blocks.len(),
2449                        f.blocks
2450                    );
2451                }
2452            }
2453        }
2454    }
2455
2456    #[test]
2457    fn an_await_free_body_gains_no_dispatch_block() {
2458        // A body with one state needs no dispatch: switching on a tag that is
2459        // always 0 is the identity, and moving the entry block would renumber
2460        // every terminator target for nothing. This is also what keeps the
2461        // exact load- and store-sequence assertions above describing the code
2462        // that runs.
2463        let mut m = module_with_async_const_fn(MirTy::F64, &[]);
2464        let before = m.functions[0].blocks.len();
2465        transform(&mut m);
2466        let poll = find(&m, "f.15$poll");
2467        assert_eq!(
2468            poll.blocks.len(),
2469            before,
2470            "the block list must be untouched: {:?}",
2471            poll.blocks
2472        );
2473        assert!(
2474            matches!(poll.blocks[0].term, Terminator::Return(_)),
2475            "no tag dispatch may be introduced: {:?}",
2476            poll.blocks
2477        );
2478    }
2479
2480    #[test]
2481    fn a_suspend_needs_no_state_slot_beyond_the_bodys_own_temps() {
2482        // The decision the state-size cross-check rests on: an await adds no
2483        // slot. The resume tag is `STATE_SLOT_TAG`, which every state object
2484        // already has, and the awaited future is an ordinary body temp with an
2485        // ordinary temp slot -- so the allocation the wrapper sizes from the
2486        // pre-transform temp count is still exactly right after the split. If a
2487        // later change does need a slot, `state_slot_count` is the one place it
2488        // goes, and the `debug_assert!`s in `split_into_poll_and_wrapper` fail
2489        // until it is added there.
2490        let mut m = module_with_async_fn_awaiting_twice(MirTy::F64);
2491        let n_temps = m.functions[0].temps.len();
2492        transform(&mut m);
2493        let wrapper = find(&m, "f.15");
2494        let expected = ((STATE_SLOT_TEMPS as usize + n_temps) * 8) as i64;
2495        let sizes: Vec<i64> = stmts_of(wrapper)
2496            .filter_map(|s| match s {
2497                Stmt::CallRuntime {
2498                    func: RtFunc::Alloc,
2499                    args,
2500                    ..
2501                } => Some(args.clone()),
2502                _ => None,
2503            })
2504            .flatten()
2505            .filter_map(|t| {
2506                stmts_of(wrapper).find_map(|s| match s {
2507                    Stmt::ConstInt(d, v) if *d == t => Some(*v),
2508                    _ => None,
2509                })
2510            })
2511            .collect();
2512        assert_eq!(
2513            sizes,
2514            vec![expected],
2515            "a resumable body's state object is still \
2516             (STATE_SLOT_TEMPS + n_temps) * 8 bytes: {:?}",
2517            wrapper.blocks
2518        );
2519    }
2520}