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}