use crate::{Block, BlockId, Function, MirTy, Module, RtFunc, Stmt, Temp, Terminator};
pub const STATE_SLOT_TAG: u32 = 0;
pub const STATE_SLOT_OUTPUT: u32 = 1;
pub const STATE_SLOT_TEMPS: u32 = 2;
pub const STATE_MIN_SIZE: i64 = STATE_SLOT_TEMPS as i64 * 8;
pub const POLL_PENDING: i64 = 0;
pub const POLL_READY: i64 = 1;
pub const FUTURE_SLOT_POLL: u32 = 0;
pub const FUTURE_SLOT_STATE: u32 = 1;
const STATE: Temp = Temp(0);
const TASK_CTX: Temp = Temp(1);
const DISPATCH: BlockId = BlockId(0);
const BAD_DISCRIMINANT: BlockId = BlockId(1);
const RESERVED_BLOCKS: u32 = 2;
const BLOCKS_PER_AWAIT: u32 = 3;
const FIRST_RESUME_TAG: i64 = 1;
pub(crate) fn transform(module: &mut Module) {
let mut wrappers: Vec<Function> = Vec::new();
for f in &mut module.functions {
if !f.is_async {
continue;
}
wrappers.push(split_into_poll_and_wrapper(f));
}
module.functions.append(&mut wrappers);
}
fn split_into_poll_and_wrapper(f: &mut Function) -> Function {
debug_assert!(
f.blocks.iter().all(|b| match &b.term {
Terminator::Return(Some(t)) => f.temps[t.0 as usize] == f.ret,
_ => true,
}),
"`{}` returns a temp whose class is not its declared return class {:?}; \
see `lower::body_return_ty`",
f.name,
f.ret
);
let wrapper = build_wrapper(f);
let orig_temps = std::mem::take(&mut f.temps);
let mut sp = Spiller {
temps: vec![MirTy::Ptr, MirTy::Ptr],
orig: &orig_temps,
};
let blocks = std::mem::take(&mut f.blocks);
f.blocks = sp.body(blocks);
f.temps = sp.temps;
debug_assert!(
!f.blocks
.iter()
.flat_map(|b| &b.stmts)
.any(|s| matches!(s, Stmt::Await { .. })),
"`{}$poll` still holds an await marker after the split",
f.name
);
debug_assert!(
highest_state_slot(&f.blocks, STATE) < wrapper.state_slots,
"`{}$poll` addresses state slot {}, which is past the {} slots ({} \
bytes) its wrapper allocated: a slot added to the rewrite must be \
added to `state_slot_count` in the same edit",
f.name,
highest_state_slot(&f.blocks, STATE),
wrapper.state_slots,
wrapper.state_slots as i64 * 8,
);
debug_assert!(
highest_state_slot(&wrapper.function.blocks, wrapper.state) < wrapper.state_slots,
"`{}` seeds state slot {}, which is past the {} slots it allocated",
f.name,
highest_state_slot(&wrapper.function.blocks, wrapper.state),
wrapper.state_slots,
);
f.name.push_str("$poll");
f.takes_env = true;
f.params = 1;
f.capture_count = 0;
f.ret = MirTy::I64;
f.is_async = false;
wrapper.function
}
fn highest_state_slot(blocks: &[Block], state: Temp) -> u32 {
blocks
.iter()
.flat_map(|b| &b.stmts)
.filter_map(|s| match s {
Stmt::RecordField { record, index, .. } | Stmt::SetField { record, index, .. }
if *record == state =>
{
Some(*index)
}
_ => None,
})
.max()
.unwrap_or(STATE_SLOT_TAG)
}
fn state_slot_count(n_temps: usize) -> u32 {
STATE_SLOT_TEMPS + n_temps as u32
}
struct Wrapper {
function: Function,
state_slots: u32,
state: Temp,
}
fn build_wrapper(f: &Function) -> Wrapper {
let abi_count = f.params as usize + usize::from(f.takes_env);
let mut temps: Vec<MirTy> = f.temps[..abi_count].to_vec();
let mut stmts: Vec<Stmt> = Vec::new();
let state_slots = state_slot_count(f.temps.len());
let bytes = state_slots as i64 * 8;
debug_assert!(
bytes >= STATE_MIN_SIZE,
"a state object must hold at least the tag and output slots, since the \
executor reads the output slot on completion even for a future with \
no temps at all"
);
let size = push_temp(&mut temps, MirTy::I64);
stmts.push(Stmt::ConstInt(size, bytes));
let state = push_temp(&mut temps, MirTy::Ptr);
stmts.push(Stmt::CallRuntime {
dst: Some(state),
func: RtFunc::Alloc,
args: vec![size],
});
let zero = push_temp(&mut temps, MirTy::I64);
stmts.push(Stmt::ConstInt(zero, 0));
stmts.push(Stmt::SetField {
record: state,
index: STATE_SLOT_TAG,
value: zero,
ty: MirTy::I64,
});
for (i, &ty) in temps[..abi_count].iter().enumerate() {
stmts.push(Stmt::SetField {
record: state,
index: STATE_SLOT_TEMPS + i as u32,
value: Temp(i as u32),
ty,
});
}
let future = push_temp(&mut temps, MirTy::Ptr);
stmts.push(Stmt::MakeClosure {
dst: future,
code: format!("{}$poll", f.name),
captures: Vec::new(),
});
stmts.push(Stmt::SetField {
record: future,
index: FUTURE_SLOT_STATE,
value: state,
ty: MirTy::Ptr,
});
Wrapper {
function: Function {
name: f.name.clone(),
params: f.params,
takes_env: f.takes_env,
capture_count: 0,
temps,
ret: MirTy::Ptr,
is_async: false,
blocks: vec![Block {
stmts,
term: Terminator::Return(Some(future)),
}],
},
state_slots,
state,
}
}
fn push_temp(temps: &mut Vec<MirTy>, ty: MirTy) -> Temp {
let t = Temp(temps.len() as u32);
temps.push(ty);
t
}
struct Spiller<'a> {
temps: Vec<MirTy>,
orig: &'a [MirTy],
}
struct AwaitPoint {
dst: Option<Temp>,
future: Temp,
}
fn split_at_awaits(stmts: Vec<Stmt>) -> (Vec<Vec<Stmt>>, Vec<AwaitPoint>) {
let mut segments: Vec<Vec<Stmt>> = vec![Vec::new()];
let mut awaits: Vec<AwaitPoint> = Vec::new();
for s in stmts {
match s {
Stmt::Await { dst, future } => {
awaits.push(AwaitPoint { dst, future });
segments.push(Vec::new());
}
other => segments
.last_mut()
.expect("segments starts with one entry and only ever grows")
.push(other),
}
}
(segments, awaits)
}
fn await_count(b: &Block) -> u32 {
b.stmts
.iter()
.filter(|s| matches!(s, Stmt::Await { .. }))
.count() as u32
}
#[derive(Clone, Copy)]
enum Role {
Read,
Write,
}
impl Spiller<'_> {
fn scratch(&mut self, ty: MirTy) -> Temp {
push_temp(&mut self.temps, ty)
}
fn orig_ty(&self, t: Temp) -> MirTy {
self.orig[t.0 as usize]
}
fn slot(t: Temp) -> u32 {
STATE_SLOT_TEMPS + t.0
}
fn load(&mut self, out: &mut Vec<Stmt>, record: Temp, index: u32, ty: MirTy) -> Temp {
let dst = self.scratch(ty);
out.push(Stmt::RecordField {
dst,
record,
index,
ty,
});
dst
}
fn reload(&mut self, out: &mut Vec<Stmt>, t: Temp) -> Temp {
let ty = self.orig_ty(t);
self.load(out, STATE, Self::slot(t), ty)
}
fn const_i64(&mut self, out: &mut Vec<Stmt>, v: i64) -> Temp {
let t = self.scratch(MirTy::I64);
out.push(Stmt::ConstInt(t, v));
t
}
fn body(&mut self, orig: Vec<Block>) -> Vec<Block> {
let per_block: Vec<u32> = orig.iter().map(await_count).collect();
let total: u32 = per_block.iter().sum();
let reserved = if total == 0 { 0 } else { RESERVED_BLOCKS };
let mut next = reserved;
let starts: Vec<BlockId> = per_block
.iter()
.map(|a| {
let id = BlockId(next);
next += 1 + BLOCKS_PER_AWAIT * a;
id
})
.collect();
let mut out: Vec<Block> = Vec::with_capacity(next as usize);
for _ in 0..reserved {
out.push(Block {
stmts: Vec::new(),
term: Terminator::Trap,
});
}
let mut resume: Vec<BlockId> = Vec::with_capacity(total as usize);
for (i, b) in orig.into_iter().enumerate() {
debug_assert_eq!(
out.len() as u32,
starts[i].0,
"block {i}'s pieces must land at the id reserved for them, or \
every terminator naming it is off by however far the two drifted"
);
self.block_pieces(b, &starts, &mut out, &mut resume);
}
debug_assert_eq!(
out.len() as u32,
next,
"the emitted block count must match the ids reserved for it"
);
if total > 0 {
debug_assert_eq!(
resume.len(),
total as usize,
"one resume point per await, in tag order"
);
let mut stmts = Vec::new();
let tag = self.load(&mut stmts, STATE, STATE_SLOT_TAG, MirTy::I64);
let arms = std::iter::once((0, starts[0]))
.chain(
resume
.into_iter()
.enumerate()
.map(|(k, b)| (FIRST_RESUME_TAG + k as i64, b)),
)
.collect();
out[DISPATCH.0 as usize] = Block {
stmts,
term: Terminator::Switch {
disc: tag,
arms,
default: BAD_DISCRIMINANT,
},
};
}
out
}
fn block_pieces(
&mut self,
b: Block,
starts: &[BlockId],
out: &mut Vec<Block>,
resume: &mut Vec<BlockId>,
) {
let base = out.len() as u32;
let Block {
stmts: body,
term: orig_term,
} = b;
let (segments, awaits) = split_at_awaits(body);
let mut segments = segments.into_iter();
let n = awaits.len();
let mut orig_term = Some(orig_term);
let mut stmts = Vec::new();
self.spill(
segments.next().expect("one segment more than awaits"),
&mut stmts,
);
if n == 0 {
let term = self.finish(&mut orig_term, &mut stmts, starts);
out.push(Block { stmts, term });
return;
}
out.push(Block {
stmts,
term: Terminator::Goto(BlockId(base + 1)),
});
for (k, aw) in awaits.into_iter().enumerate() {
let poll = BlockId(base + 1 + BLOCKS_PER_AWAIT * k as u32);
let suspend = BlockId(poll.0 + 1);
let cont = BlockId(poll.0 + 2);
let tag = FIRST_RESUME_TAG + resume.len() as i64;
resume.push(poll);
debug_assert_eq!(out.len() as u32, poll.0, "the poll block's own id");
let mut stmts = Vec::new();
let future = self.reload(&mut stmts, aw.future);
let status = self.scratch(MirTy::I64);
stmts.push(Stmt::CallIndirect {
dst: Some(status),
callee: future,
params: vec![MirTy::Ptr],
ret: MirTy::I64,
args: vec![TASK_CTX],
});
out.push(Block {
stmts,
term: Terminator::Switch {
disc: status,
arms: vec![(POLL_READY, cont), (POLL_PENDING, suspend)],
default: BAD_DISCRIMINANT,
},
});
let mut stmts = Vec::new();
let t = self.const_i64(&mut stmts, tag);
stmts.push(Stmt::SetField {
record: STATE,
index: STATE_SLOT_TAG,
value: t,
ty: MirTy::I64,
});
let pending = self.const_i64(&mut stmts, POLL_PENDING);
out.push(Block {
stmts,
term: Terminator::Return(Some(pending)),
});
let mut stmts = Vec::new();
match aw.dst {
Some(dst) if self.orig_ty(dst) != MirTy::Unit => {
let ty = self.orig_ty(dst);
let future = self.reload(&mut stmts, aw.future);
let inner = self.load(&mut stmts, future, FUTURE_SLOT_STATE, MirTy::Ptr);
let value = self.load(&mut stmts, inner, STATE_SLOT_OUTPUT, ty);
stmts.push(Stmt::SetField {
record: STATE,
index: Self::slot(dst),
value,
ty,
});
}
_ => {}
}
self.spill(
segments.next().expect("one segment more than awaits"),
&mut stmts,
);
let term = if k + 1 < n {
Terminator::Goto(BlockId(cont.0 + 1))
} else {
self.finish(&mut orig_term, &mut stmts, starts)
};
out.push(Block { stmts, term });
}
}
fn finish(
&mut self,
orig_term: &mut Option<Terminator>,
out: &mut Vec<Stmt>,
starts: &[BlockId],
) -> Terminator {
let term = orig_term
.take()
.expect("exactly one piece of a block takes its original terminator");
self.terminator(term, out, starts)
}
fn spill(&mut self, stmts: Vec<Stmt>, out: &mut Vec<Stmt>) {
for mut s in stmts {
let mut loads: Vec<Stmt> = Vec::new();
let mut stores: Vec<Stmt> = Vec::new();
visit_temps(&mut s, &mut |t, role| match role {
Role::Read => *t = self.reload(&mut loads, *t),
Role::Write => {
let ty = self.orig_ty(*t);
let dst = self.scratch(ty);
stores.push(Stmt::SetField {
record: STATE,
index: Self::slot(*t),
value: dst,
ty,
});
*t = dst;
}
});
out.append(&mut loads);
out.push(s);
out.append(&mut stores);
}
}
fn terminator(
&mut self,
term: Terminator,
out: &mut Vec<Stmt>,
starts: &[BlockId],
) -> Terminator {
let at = |b: BlockId| starts[b.0 as usize];
match term {
Terminator::Goto(target) => Terminator::Goto(at(target)),
Terminator::Branch { cond, then_, else_ } => Terminator::Branch {
cond: self.reload(out, cond),
then_: at(then_),
else_: at(else_),
},
Terminator::Switch {
disc,
arms,
default,
} => Terminator::Switch {
disc: self.reload(out, disc),
arms: arms.into_iter().map(|(v, b)| (v, at(b))).collect(),
default: at(default),
},
Terminator::Return(value) => {
match value {
Some(t) if self.orig_ty(t) != MirTy::Unit => {
let ty = self.orig_ty(t);
let v = self.reload(out, t);
out.push(Stmt::SetField {
record: STATE,
index: STATE_SLOT_OUTPUT,
value: v,
ty,
});
}
_ => {
let z = self.const_i64(out, 0);
out.push(Stmt::SetField {
record: STATE,
index: STATE_SLOT_OUTPUT,
value: z,
ty: MirTy::I64,
});
}
}
let ready = self.const_i64(out, POLL_READY);
Terminator::Return(Some(ready))
}
Terminator::Trap => Terminator::Trap,
}
}
}
fn visit_temps(stmt: &mut Stmt, f: &mut impl FnMut(&mut Temp, Role)) {
use Role::{Read, Write};
match stmt {
Stmt::ConstInt(dst, _)
| Stmt::ConstFloat(dst, _)
| Stmt::ConstBool(dst, _)
| Stmt::ConstStr(dst, _)
| Stmt::ConstUnit(dst) => f(dst, Write),
Stmt::Copy { dst, src } => {
f(src, Read);
f(dst, Write);
}
Stmt::Bin { dst, lhs, rhs, .. } => {
f(lhs, Read);
f(rhs, Read);
f(dst, Write);
}
Stmt::Neg { dst, src, .. } | Stmt::Not { dst, src } | Stmt::BitNot { dst, src } => {
f(src, Read);
f(dst, Write);
}
Stmt::Call { dst, args, .. } | Stmt::CallRuntime { dst, args, .. } => {
for a in args.iter_mut() {
f(a, Read);
}
if let Some(dst) = dst {
f(dst, Write);
}
}
Stmt::CallIndirect {
dst, callee, args, ..
} => {
f(callee, Read);
for a in args.iter_mut() {
f(a, Read);
}
if let Some(dst) = dst {
f(dst, Write);
}
}
Stmt::Await { dst, future } => {
f(future, Read);
if let Some(dst) = dst {
f(dst, Write);
}
}
Stmt::MakeClosure { dst, captures, .. } => {
for (c, _) in captures.iter_mut() {
f(c, Read);
}
f(dst, Write);
}
Stmt::MakeSum { dst, fields, .. } | Stmt::MakeRecord { dst, fields } => {
for (v, _) in fields.iter_mut() {
f(v, Read);
}
f(dst, Write);
}
Stmt::SumTag { dst, sum } | Stmt::SumField { dst, sum, .. } => {
f(sum, Read);
f(dst, Write);
}
Stmt::RecordField { dst, record, .. } => {
f(record, Read);
f(dst, Write);
}
Stmt::SetField { record, value, .. } => {
f(record, Read);
f(value, Read);
}
Stmt::MakeArray { dst, elems } => {
for (e, _) in elems.iter_mut() {
f(e, Read);
}
f(dst, Write);
}
Stmt::ArrayAlloc { dst, len } => {
f(len, Read);
f(dst, Write);
}
Stmt::ArrayLen { dst, arr } => {
f(arr, Read);
f(dst, Write);
}
Stmt::ArrayGet {
dst, arr, index, ..
} => {
f(arr, Read);
f(index, Read);
f(dst, Write);
}
Stmt::ArraySet {
arr, index, value, ..
} => {
f(arr, Read);
f(index, Read);
f(value, Read);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Function, MirTy, Module, RtFunc, Stmt, Temp, Terminator};
fn module_with_async_const_fn(ret: MirTy, params: &[MirTy]) -> Module {
let mut temps: Vec<MirTy> = params.to_vec();
let result = Temp(temps.len() as u32);
temps.push(ret);
let (stmts, term) = match ret {
MirTy::I64 => (
vec![Stmt::ConstInt(result, 7)],
Terminator::Return(Some(result)),
),
MirTy::F64 => (
vec![Stmt::ConstFloat(result, 1.5)],
Terminator::Return(Some(result)),
),
MirTy::I8 => (
vec![Stmt::ConstBool(result, true)],
Terminator::Return(Some(result)),
),
MirTy::Ptr => (
vec![Stmt::ConstStr(result, "hi".to_string())],
Terminator::Return(Some(result)),
),
MirTy::Unit => (vec![Stmt::ConstUnit(result)], Terminator::Return(None)),
};
Module {
functions: vec![Function {
name: "f.15".to_string(),
params: params.len() as u32,
takes_env: false,
capture_count: 0,
temps,
ret,
is_async: true,
blocks: vec![crate::Block { stmts, term }],
}],
externs: Vec::new(),
}
}
fn module_with_async_bin_fn(ret: MirTy) -> Module {
let mut m = module_with_async_const_fn(ret, &[ret, ret]);
let f = &mut m.functions[0];
f.blocks[0].stmts = vec![Stmt::Bin {
dst: Temp(2),
op: nova_hir::BinOp::Add,
class: crate::OperandClass::Float,
lhs: Temp(0),
rhs: Temp(1),
}];
m
}
fn module_with_plain_fn() -> Module {
let mut m = module_with_async_const_fn(MirTy::I64, &[]);
m.functions[0].is_async = false;
m
}
fn find<'a>(m: &'a Module, name: &str) -> &'a Function {
m.functions
.iter()
.find(|f| f.name == name)
.unwrap_or_else(|| {
panic!(
"no function named `{name}`; have {:?}",
m.functions.iter().map(|f| &f.name).collect::<Vec<_>>()
)
})
}
fn stmts_of(f: &Function) -> impl Iterator<Item = &Stmt> {
f.blocks.iter().flat_map(|b| &b.stmts)
}
#[test]
fn an_await_free_async_fn_becomes_a_poll_fn_and_a_wrapper() {
let mut m = module_with_async_const_fn(MirTy::I64, &[]);
transform(&mut m);
let poll = find(&m, "f.15$poll");
assert!(poll.takes_env, "the poll fn's env IS the state object");
assert_eq!(poll.params, 1, "poll takes task_ctx as its one real param");
assert_eq!(poll.ret, MirTy::I64, "poll returns a status, not the value");
assert_eq!(
&poll.temps[..2],
&[MirTy::Ptr, MirTy::Ptr],
"temp 0 is the state pointer, temp 1 is task_ctx"
);
let wrapper = find(&m, "f.15");
assert_eq!(wrapper.ret, MirTy::Ptr, "the wrapper returns a future");
assert!(
!wrapper.takes_env,
"the wrapper keeps the original ABI its callers already compiled against"
);
assert!(
stmts_of(wrapper).any(|s| matches!(s, Stmt::MakeClosure { .. })),
"the wrapper must build the {{ poll_code, state }} fat pointer"
);
}
#[test]
fn the_poll_fn_writes_its_result_to_the_output_slot_not_the_return() {
let mut m = module_with_async_const_fn(MirTy::F64, &[]);
transform(&mut m);
let poll = find(&m, "f.15$poll");
assert!(
stmts_of(poll).any(|s| matches!(
s,
Stmt::SetField { index, ty, .. }
if *index == STATE_SLOT_OUTPUT && *ty == MirTy::F64
)),
"the f64 result must be stored to the output slot AS an f64: {:?}",
poll.blocks
);
assert_eq!(
poll.ret,
MirTy::I64,
"the poll fn's own return stays the i64 status"
);
}
#[test]
fn the_poll_fn_returns_the_ready_status_and_nothing_else() {
let mut m = module_with_async_const_fn(MirTy::F64, &[]);
transform(&mut m);
let poll = find(&m, "f.15$poll");
let returned: Vec<Temp> = poll
.blocks
.iter()
.filter_map(|b| match &b.term {
Terminator::Return(Some(t)) => Some(*t),
_ => None,
})
.collect();
assert!(!returned.is_empty(), "the poll fn must return a status");
for t in returned {
assert_eq!(
poll.temps[t.0 as usize],
MirTy::I64,
"a poll status is an i64"
);
assert!(
stmts_of(poll)
.any(|s| matches!(s, Stmt::ConstInt(d, v) if *d == t && *v == POLL_READY)),
"the returned temp %{} must be a constant POLL_READY, not a body value: {:?}",
t.0,
poll.blocks
);
}
}
#[test]
fn a_unit_returning_async_fn_still_writes_the_output_slot() {
let mut m = module_with_async_const_fn(MirTy::Unit, &[]);
transform(&mut m);
let poll = find(&m, "f.15$poll");
assert!(
stmts_of(poll).any(|s| matches!(
s,
Stmt::SetField { index, ty, .. }
if *index == STATE_SLOT_OUTPUT && *ty != MirTy::Unit
)),
"a unit-returning poll fn must still write a real value to the \
output slot (a `ty: Unit` store is a codegen no-op): {:?}",
poll.blocks
);
}
#[test]
fn the_poll_fn_reloads_every_operand_and_stores_every_result_to_its_own_slot() {
let mut m = module_with_async_bin_fn(MirTy::F64);
transform(&mut m);
let poll = find(&m, "f.15$poll");
let loads: Vec<u32> = stmts_of(poll)
.filter_map(|s| match s {
Stmt::RecordField { index, .. } => Some(*index),
_ => None,
})
.collect();
let stores: Vec<u32> = stmts_of(poll)
.filter_map(|s| match s {
Stmt::SetField { index, .. } => Some(*index),
_ => None,
})
.collect();
let temps = STATE_SLOT_TEMPS;
assert_eq!(
loads,
vec![temps, temps + 1, temps + 2],
"both operands, then the returned value, each reloaded from its \
own slot: {:?}",
poll.blocks
);
assert_eq!(
stores,
vec![temps + 2, STATE_SLOT_OUTPUT],
"the result to its own slot, then the output slot on completion: \
{:?}",
poll.blocks
);
}
#[test]
fn every_body_value_is_addressed_through_the_state_object() {
let mut m = module_with_async_const_fn(MirTy::F64, &[MirTy::I64]);
transform(&mut m);
let poll = find(&m, "f.15$poll");
let mut accesses = 0;
for s in stmts_of(poll) {
match s {
Stmt::RecordField { record, .. } | Stmt::SetField { record, .. } => {
accesses += 1;
assert_eq!(
*record,
Temp(0),
"every state access must go through temp 0, the state pointer"
);
}
_ => {}
}
}
assert!(accesses > 0, "the poll fn must touch the state object");
}
#[test]
fn the_wrapper_seeds_the_state_with_its_parameters() {
let mut m = module_with_async_const_fn(MirTy::I64, &[MirTy::F64, MirTy::I64]);
transform(&mut m);
let wrapper = find(&m, "f.15");
for (i, ty) in [(0u32, MirTy::F64), (1, MirTy::I64)] {
assert!(
stmts_of(wrapper).any(|s| matches!(
s,
Stmt::SetField { index, value, ty: t, .. }
if *index == STATE_SLOT_TEMPS + i && *value == Temp(i) && *t == ty
)),
"parameter %{i} must be stored into state slot {} as {ty:?}: {:?}",
STATE_SLOT_TEMPS + i,
wrapper.blocks
);
}
}
#[test]
fn the_wrapper_allocates_a_scanned_state_no_smaller_than_the_minimum() {
let mut m = module_with_async_const_fn(MirTy::Unit, &[]);
let n_temps = m.functions[0].temps.len();
transform(&mut m);
let wrapper = find(&m, "f.15");
let expected = ((STATE_SLOT_TEMPS as usize + n_temps) * 8) as i64;
assert!(expected >= STATE_MIN_SIZE);
let sizes: Vec<i64> = stmts_of(wrapper)
.filter_map(|s| match s {
Stmt::CallRuntime {
func: RtFunc::Alloc,
args,
..
} => Some(args.clone()),
_ => None,
})
.flatten()
.filter_map(|t| {
stmts_of(wrapper).find_map(|s| match s {
Stmt::ConstInt(d, v) if *d == t => Some(*v),
_ => None,
})
})
.collect();
assert_eq!(
sizes,
vec![expected],
"the state object must be one `nova_rt_alloc` of \
(STATE_SLOT_TEMPS + n_temps) * 8 bytes: {:?}",
wrapper.blocks
);
}
#[test]
fn every_return_becomes_its_own_completion_and_a_trap_stays_a_trap() {
let mut m = module_with_async_const_fn(MirTy::F64, &[]);
{
let f = &mut m.functions[0];
let second = Temp(f.temps.len() as u32);
f.temps.push(MirTy::F64);
f.blocks.push(Block {
stmts: vec![Stmt::ConstFloat(second, 2.5)],
term: Terminator::Return(Some(second)),
});
f.blocks.push(Block {
stmts: Vec::new(),
term: Terminator::Trap,
});
}
transform(&mut m);
let poll = find(&m, "f.15$poll");
let statuses: Vec<Temp> = poll
.blocks
.iter()
.filter_map(|b| match &b.term {
Terminator::Return(Some(t)) => Some(*t),
_ => None,
})
.collect();
assert_eq!(
statuses.len(),
2,
"both returns must survive: {:?}",
poll.blocks
);
for t in &statuses {
assert!(
stmts_of(poll)
.any(|s| matches!(s, Stmt::ConstInt(d, v) if d == t && *v == POLL_READY)),
"each return must carry its own POLL_READY constant: {:?}",
poll.blocks
);
}
let output_stores = stmts_of(poll)
.filter(|s| matches!(s, Stmt::SetField { index, .. } if *index == STATE_SLOT_OUTPUT))
.count();
assert_eq!(
output_stores, 2,
"each completion writes the output slot on its own path, since only \
one of them runs: {:?}",
poll.blocks
);
assert_eq!(
poll.blocks
.iter()
.filter(|b| matches!(b.term, Terminator::Trap))
.count(),
1,
"the trap must stay a trap, not become a completion: {:?}",
poll.blocks
);
}
#[test]
fn the_wrapper_writes_the_entry_resume_tag() {
let mut m = module_with_async_const_fn(MirTy::I64, &[]);
transform(&mut m);
let wrapper = find(&m, "f.15");
let tag_value = stmts_of(wrapper)
.find_map(|s| match s {
Stmt::SetField {
index, value, ty, ..
} if *index == STATE_SLOT_TAG => {
assert_ne!(*ty, MirTy::Unit, "a `ty: Unit` store is a no-op");
Some(*value)
}
_ => None,
})
.unwrap_or_else(|| panic!("no store to the tag slot: {:?}", wrapper.blocks));
assert!(
stmts_of(wrapper).any(|s| matches!(s, Stmt::ConstInt(d, 0) if *d == tag_value)),
"the entry tag must be the constant 0, so a fresh future starts in \
its first state: {:?}",
wrapper.blocks
);
}
#[test]
fn the_future_carries_the_state_itself_not_a_record_holding_it() {
let mut m = module_with_async_const_fn(MirTy::I64, &[]);
transform(&mut m);
let wrapper = find(&m, "f.15");
let (fut, code) = stmts_of(wrapper)
.find_map(|s| match s {
Stmt::MakeClosure {
dst,
code,
captures,
} => {
assert!(
captures.is_empty(),
"a captured state would sit behind an extra indirection"
);
Some((*dst, code.clone()))
}
_ => None,
})
.expect("the wrapper builds a fat pointer");
assert_eq!(code, "f.15$poll", "word 0 must be the poll fn's address");
let state = stmts_of(wrapper)
.find_map(|s| match s {
Stmt::CallRuntime {
dst: Some(d),
func: RtFunc::Alloc,
..
} => Some(*d),
_ => None,
})
.expect("the wrapper allocates a state");
assert!(
stmts_of(wrapper).any(|s| matches!(
s,
Stmt::SetField { record, index, value, .. }
if *record == fut && *index == FUTURE_SLOT_STATE && *value == state
)),
"word {FUTURE_SLOT_STATE} of the future must be the state pointer \
itself: {:?}",
wrapper.blocks
);
}
#[test]
fn a_non_async_function_is_left_byte_identical() {
let mut m = module_with_plain_fn();
let before = format!("{:?}", m.functions);
transform(&mut m);
assert_eq!(before, format!("{:?}", m.functions));
}
#[test]
fn the_transform_is_idempotent() {
let mut m = module_with_async_const_fn(MirTy::F64, &[]);
transform(&mut m);
let once = format!("{:?}", m.functions);
transform(&mut m);
assert_eq!(once, format!("{:?}", m.functions));
assert!(
m.functions.iter().all(|f| !f.is_async),
"no function may still be flagged async after the pass"
);
}
#[test]
fn the_state_layout_constants_match_the_runtime() {
assert_eq!(STATE_SLOT_TAG, 0);
assert_eq!(STATE_SLOT_OUTPUT, 1);
assert_eq!(STATE_SLOT_TEMPS, 2);
assert_eq!(STATE_MIN_SIZE, 16);
assert_eq!(POLL_PENDING, 0);
assert_eq!(POLL_READY, 1);
assert_eq!(FUTURE_SLOT_POLL, 0);
assert_eq!(FUTURE_SLOT_STATE, 1);
}
fn async_module(ret: MirTy, params: u32, temps: Vec<MirTy>, blocks: Vec<Block>) -> Module {
Module {
functions: vec![Function {
name: "f.15".to_string(),
params,
takes_env: false,
capture_count: 0,
temps,
ret,
is_async: true,
blocks,
}],
externs: Vec::new(),
}
}
fn module_with_async_fn_awaiting_once(out: MirTy) -> Module {
async_module(
out,
0,
vec![MirTy::Ptr, out],
vec![Block {
stmts: vec![
Stmt::Call {
dst: Some(Temp(0)),
callee: "g.16".to_string(),
args: Vec::new(),
},
Stmt::Await {
dst: Some(Temp(1)),
future: Temp(0),
},
],
term: Terminator::Return(Some(Temp(1))),
}],
)
}
fn module_with_async_fn_awaiting_twice(out: MirTy) -> Module {
async_module(
out,
0,
vec![MirTy::Ptr, out, MirTy::Ptr, out, out],
vec![Block {
stmts: vec![
Stmt::Call {
dst: Some(Temp(0)),
callee: "g.16".to_string(),
args: Vec::new(),
},
Stmt::Await {
dst: Some(Temp(1)),
future: Temp(0),
},
Stmt::Call {
dst: Some(Temp(2)),
callee: "h.17".to_string(),
args: Vec::new(),
},
Stmt::Await {
dst: Some(Temp(3)),
future: Temp(2),
},
Stmt::Bin {
dst: Temp(4),
op: nova_hir::BinOp::Add,
class: crate::OperandClass::Float,
lhs: Temp(1),
rhs: Temp(3),
},
],
term: Terminator::Return(Some(Temp(4))),
}],
)
}
fn module_with_async_fn_awaiting_in_a_loop(out: MirTy) -> Module {
async_module(
out,
0,
vec![MirTy::I8, MirTy::Ptr, out],
vec![
Block {
stmts: Vec::new(),
term: Terminator::Goto(BlockId(1)),
},
Block {
stmts: vec![Stmt::ConstBool(Temp(0), true)],
term: Terminator::Branch {
cond: Temp(0),
then_: BlockId(2),
else_: BlockId(3),
},
},
Block {
stmts: vec![
Stmt::Call {
dst: Some(Temp(1)),
callee: "g.16".to_string(),
args: Vec::new(),
},
Stmt::Await {
dst: Some(Temp(2)),
future: Temp(1),
},
],
term: Terminator::Goto(BlockId(1)),
},
Block {
stmts: Vec::new(),
term: Terminator::Return(Some(Temp(2))),
},
],
)
}
fn module_with_async_fn_awaiting_in_a_match(out: MirTy) -> Module {
async_module(
out,
0,
vec![MirTy::I64, MirTy::Ptr, out],
vec![
Block {
stmts: vec![Stmt::ConstInt(Temp(0), 0)],
term: Terminator::Switch {
disc: Temp(0),
arms: vec![(0, BlockId(1)), (1, BlockId(2))],
default: BlockId(3),
},
},
Block {
stmts: vec![
Stmt::Call {
dst: Some(Temp(1)),
callee: "g.16".to_string(),
args: Vec::new(),
},
Stmt::Await {
dst: Some(Temp(2)),
future: Temp(1),
},
],
term: Terminator::Goto(BlockId(4)),
},
Block {
stmts: Vec::new(),
term: Terminator::Goto(BlockId(4)),
},
Block {
stmts: Vec::new(),
term: Terminator::Trap,
},
Block {
stmts: Vec::new(),
term: Terminator::Return(Some(Temp(2))),
},
],
)
}
fn module_with_async_fn_awaiting_unit() -> Module {
async_module(
MirTy::Unit,
0,
vec![MirTy::Ptr],
vec![Block {
stmts: vec![
Stmt::Call {
dst: Some(Temp(0)),
callee: "g.16".to_string(),
args: Vec::new(),
},
Stmt::Await {
dst: None,
future: Temp(0),
},
],
term: Terminator::Return(None),
}],
)
}
fn resume_arms(poll: &Function) -> Vec<(i64, BlockId)> {
match &poll.blocks[0].term {
Terminator::Switch { arms, .. } => arms.clone(),
other => panic!("the entry block must dispatch on the resume tag, found {other:?}"),
}
}
fn status_arm(poll: &Function, resume: BlockId, status: i64) -> BlockId {
match &poll.blocks[resume.0 as usize].term {
Terminator::Switch { arms, .. } => *arms
.iter()
.find_map(|(v, b)| (*v == status).then_some(b))
.unwrap_or_else(|| {
panic!("resume block {resume:?} has no arm for status {status}: {arms:?}")
}),
other => {
panic!("a resume point must branch on the inner poll's status, found {other:?}")
}
}
}
fn tag_stores(poll: &Function, block: BlockId) -> Vec<i64> {
let b = &poll.blocks[block.0 as usize];
b.stmts
.iter()
.filter_map(|s| match s {
Stmt::SetField {
record,
index,
value,
..
} if *record == STATE && *index == STATE_SLOT_TAG => Some(*value),
_ => None,
})
.map(|v| {
b.stmts
.iter()
.find_map(|s| match s {
Stmt::ConstInt(d, k) if *d == v => Some(*k),
_ => None,
})
.unwrap_or_else(|| {
panic!(
"the tag stored must be a constant defined in the same block: {:?}",
b.stmts
)
})
})
.collect()
}
fn targets(f: &Function) -> Vec<BlockId> {
f.blocks
.iter()
.flat_map(|b| match &b.term {
Terminator::Goto(t) => vec![*t],
Terminator::Branch { then_, else_, .. } => vec![*then_, *else_],
Terminator::Switch { arms, default, .. } => arms
.iter()
.map(|(_, b)| *b)
.chain(std::iter::once(*default))
.collect(),
Terminator::Return(_) | Terminator::Trap => Vec::new(),
})
.collect()
}
#[test]
fn one_await_produces_two_resume_states_dispatched_by_a_switch() {
let mut m = module_with_async_fn_awaiting_once(MirTy::F64);
transform(&mut m);
let poll = find(&m, "f.15$poll");
let arms = resume_arms(poll);
assert!(
arms.len() >= 2,
"one await means two resume states, got {arms:?}"
);
let distinct: std::collections::HashSet<_> = arms.iter().map(|(_, b)| *b).collect();
assert_eq!(
distinct.len(),
arms.len(),
"resume arms must target distinct blocks: {arms:?}"
);
}
#[test]
fn a_suspend_stores_a_resume_tag_before_returning_pending() {
let mut m = module_with_async_fn_awaiting_once(MirTy::F64);
transform(&mut m);
let poll = find(&m, "f.15$poll");
let stores_tag = poll.blocks.iter().any(|b| {
b.stmts.iter().any(|s| {
matches!(s, Stmt::SetField { record, index, .. }
if *record == STATE && *index == STATE_SLOT_TAG)
})
});
assert!(stores_tag, "a suspend must record a resume tag");
}
#[test]
fn a_pending_inner_future_suspends_at_the_tag_that_returns_to_the_same_await() {
let mut m = module_with_async_fn_awaiting_twice(MirTy::F64);
transform(&mut m);
let poll = find(&m, "f.15$poll");
let resumes: Vec<(i64, BlockId)> = resume_arms(poll)
.into_iter()
.filter(|(tag, _)| *tag != 0)
.collect();
assert_eq!(resumes.len(), 2, "two awaits, two resume tags: {resumes:?}");
for (tag, resume) in resumes {
let pending = status_arm(poll, resume, POLL_PENDING);
assert_eq!(
tag_stores(poll, pending),
vec![tag],
"the suspend reached from tag {tag}'s resume point must store \
{tag} again, so the next poll retries the same await: {:?}",
poll.blocks
);
let block = &poll.blocks[pending.0 as usize];
let status = match &block.term {
Terminator::Return(Some(t)) => *t,
other => panic!("a suspend must return a status, found {other:?}"),
};
assert!(
block.stmts.iter().any(|s| matches!(s, Stmt::ConstInt(d, v)
if *d == status && *v == POLL_PENDING)),
"the suspend must return a constant POLL_PENDING defined in its \
own block: {block:?}"
);
}
}
#[test]
fn the_resume_dispatch_switches_on_the_tag_slot_and_enters_the_body_at_tag_zero() {
let mut m = module_with_async_fn_awaiting_twice(MirTy::F64);
transform(&mut m);
let poll = find(&m, "f.15$poll");
let entry = &poll.blocks[0];
let disc = match &entry.term {
Terminator::Switch { disc, .. } => *disc,
other => panic!("the entry must dispatch on the resume tag, found {other:?}"),
};
assert!(
entry.stmts.iter().any(|s| matches!(
s,
Stmt::RecordField { dst, record, index, ty }
if *dst == disc && *record == STATE && *index == STATE_SLOT_TAG
&& *ty == MirTy::I64
)),
"the discriminant must be loaded from the tag slot: {:?}",
entry
);
let arms = resume_arms(poll);
assert_eq!(
arms.iter().map(|(t, _)| *t).collect::<Vec<_>>(),
vec![0, 1, 2],
"one arm for the entry state plus one per await, in order: {arms:?}"
);
let distinct: std::collections::HashSet<_> = arms.iter().map(|(_, b)| *b).collect();
assert_eq!(distinct.len(), 3, "every state is its own block: {arms:?}");
}
#[test]
fn awaiting_polls_the_inner_future_through_its_own_fat_pointer() {
let mut m = module_with_async_fn_awaiting_once(MirTy::F64);
transform(&mut m);
let poll = find(&m, "f.15$poll");
let (callee, block) = poll
.blocks
.iter()
.find_map(|b| {
b.stmts.iter().find_map(|s| match s {
Stmt::CallIndirect {
dst: Some(_),
callee,
params,
ret,
args,
} => {
assert_eq!(params, &[MirTy::Ptr], "the one real param is task_ctx");
assert_eq!(*ret, MirTy::I64, "an inner poll returns a status");
assert_eq!(args, &[TASK_CTX], "task_ctx must be forwarded");
Some((*callee, b))
}
_ => None,
})
})
.unwrap_or_else(|| panic!("the await must poll the inner future: {:?}", poll.blocks));
assert!(
block.stmts.iter().any(|s| matches!(
s,
Stmt::RecordField { dst, record, index, ty }
if *dst == callee && *record == STATE
&& *index == STATE_SLOT_TEMPS && *ty == MirTy::Ptr
)),
"the awaited future must be reloaded from its own temp slot in the \
same block as the call: {:?}",
block
);
}
#[test]
fn an_awaited_value_is_read_from_the_inner_futures_own_output_slot() {
let mut m = module_with_async_fn_awaiting_once(MirTy::F64);
transform(&mut m);
let poll = find(&m, "f.15$poll");
let inner = stmts_of(poll)
.find_map(|s| match s {
Stmt::RecordField {
dst,
record,
index,
ty,
} if *index == FUTURE_SLOT_STATE && *record != STATE => {
assert_eq!(*ty, MirTy::Ptr, "the inner state is a pointer");
Some(*dst)
}
_ => None,
})
.unwrap_or_else(|| {
panic!(
"the await must load the inner state out of the future: {:?}",
poll.blocks
)
});
let value = stmts_of(poll)
.find_map(|s| match s {
Stmt::RecordField {
dst,
record,
index,
ty,
} if *record == inner && *index == STATE_SLOT_OUTPUT => {
assert_eq!(*ty, MirTy::F64, "the awaited value keeps its own class");
Some(*dst)
}
_ => None,
})
.unwrap_or_else(|| {
panic!(
"the awaited value must come from the inner state's output \
slot: {:?}",
poll.blocks
)
});
assert!(
stmts_of(poll).any(|s| matches!(
s,
Stmt::SetField { record, index, value: v, ty }
if *record == STATE && *index == STATE_SLOT_TEMPS + 1
&& *v == value && *ty == MirTy::F64
)),
"the awaited value must be stored into the await result's own temp \
slot, as an f64: {:?}",
poll.blocks
);
}
#[test]
fn an_awaited_value_reaches_its_slot_before_the_statements_that_read_it() {
let mut m = module_with_async_fn_awaiting_twice(MirTy::F64);
transform(&mut m);
let poll = find(&m, "f.15$poll");
let second = resume_arms(poll)
.into_iter()
.find_map(|(tag, b)| (tag == FIRST_RESUME_TAG + 1).then_some(b))
.expect("a second resume tag");
let cont = status_arm(poll, second, POLL_READY);
let stmts = &poll.blocks[cont.0 as usize].stmts;
let slot = STATE_SLOT_TEMPS + 3;
let store = stmts
.iter()
.position(|s| {
matches!(s, Stmt::SetField { record, index, .. }
if *record == STATE && *index == slot)
})
.unwrap_or_else(|| panic!("no store of the awaited value: {stmts:?}"));
let read = stmts
.iter()
.position(|s| {
matches!(s, Stmt::RecordField { record, index, .. }
if *record == STATE && *index == slot)
})
.unwrap_or_else(|| {
panic!("the fixture's next statement must read the awaited value: {stmts:?}")
});
assert!(
store < read,
"the awaited value must be in its slot before anything reads it: {stmts:?}"
);
}
#[test]
fn an_inner_poll_status_that_is_neither_pending_nor_ready_traps() {
let mut m = module_with_async_fn_awaiting_once(MirTy::F64);
transform(&mut m);
let poll = find(&m, "f.15$poll");
let resume = resume_arms(poll)
.into_iter()
.find_map(|(tag, b)| (tag != 0).then_some(b))
.expect("a resume arm");
let (arms, default) = match &poll.blocks[resume.0 as usize].term {
Terminator::Switch { arms, default, .. } => (arms.clone(), *default),
other => panic!("expected a status switch, found {other:?}"),
};
let mut values: Vec<i64> = arms.iter().map(|(v, _)| *v).collect();
values.sort_unstable();
assert_eq!(
values,
vec![POLL_PENDING, POLL_READY],
"exactly the two statuses the ABI defines may be recognized: {arms:?}"
);
assert!(
matches!(poll.blocks[default.0 as usize].term, Terminator::Trap),
"an unrecognized status must reach a trap, not fall into a state: {:?}",
poll.blocks
);
}
#[test]
fn a_unit_awaits_result_is_not_copied_out_of_the_inner_state() {
let mut m = module_with_async_fn_awaiting_unit();
transform(&mut m);
let poll = find(&m, "f.15$poll");
for s in stmts_of(poll) {
if let Stmt::RecordField { record, index, .. } = s {
assert_eq!(
*record, STATE,
"a unit await must not read field {index} of the inner \
future: {:?}",
poll.blocks
);
}
}
assert_eq!(resume_arms(poll).len(), 2, "{:?}", poll.blocks);
}
#[test]
fn an_await_in_a_loop_resumes_at_the_await_and_keeps_its_back_edge() {
let mut m = module_with_async_fn_awaiting_in_a_loop(MirTy::F64);
transform(&mut m);
let poll = find(&m, "f.15$poll");
let header = poll
.blocks
.iter()
.position(|b| matches!(b.term, Terminator::Branch { .. }))
.expect("the loop header survives as a conditional branch");
assert!(
poll.blocks[header]
.stmts
.iter()
.any(|s| matches!(s, Stmt::ConstBool(..))),
"the header must be the block that re-tests the condition: {:?}",
poll.blocks
);
assert!(
poll.blocks
.iter()
.any(|b| matches!(b.term, Terminator::Goto(t) if t.0 as usize == header)),
"the back edge must reach the renumbered header (block {header}): {:?}",
poll.blocks
);
let resume = resume_arms(poll)
.into_iter()
.find_map(|(tag, b)| (tag != 0).then_some(b))
.expect("a resume arm");
assert!(
poll.blocks[resume.0 as usize]
.stmts
.iter()
.any(|s| matches!(s, Stmt::CallIndirect { .. })),
"resuming must re-poll the awaited future, not re-enter the loop \
header: {:?}",
poll.blocks
);
let (then_, else_) = match &poll.blocks[header].term {
Terminator::Branch { then_, else_, .. } => (*then_, *else_),
other => panic!("expected the header's branch, found {other:?}"),
};
assert!(
poll.blocks[then_.0 as usize]
.stmts
.iter()
.any(|s| matches!(s, Stmt::Call { .. })),
"the taken arm must reach the loop body's leading piece, the block \
that builds the awaited future: {:?}",
poll.blocks
);
assert!(
matches!(poll.blocks[else_.0 as usize].term, Terminator::Return(_)),
"the untaken arm must reach the exit, which completes: {:?}",
poll.blocks
);
}
#[test]
fn every_exit_from_a_resumable_poll_fn_returns_a_constant_pending_or_ready() {
for mut m in every_awaiting_module() {
transform(&mut m);
let poll = find(&m, "f.15$poll");
let returned: Vec<Temp> = poll
.blocks
.iter()
.filter_map(|b| match &b.term {
Terminator::Return(Some(t)) => Some(*t),
_ => None,
})
.collect();
assert!(!returned.is_empty(), "{:?}", poll.blocks);
for t in returned {
assert_eq!(poll.temps[t.0 as usize], MirTy::I64, "a status is an i64");
let v = stmts_of(poll)
.find_map(|s| match s {
Stmt::ConstInt(d, v) if *d == t => Some(*v),
_ => None,
})
.unwrap_or_else(|| {
panic!(
"the returned temp %{} must be a status constant: {:?}",
t.0, poll.blocks
)
});
assert!(
v == POLL_PENDING || v == POLL_READY,
"a poll function may return only PENDING or READY, found {v}: {:?}",
poll.blocks
);
}
}
}
fn every_awaiting_module() -> Vec<Module> {
vec![
module_with_async_fn_awaiting_once(MirTy::F64),
module_with_async_fn_awaiting_twice(MirTy::F64),
module_with_async_fn_awaiting_in_a_loop(MirTy::F64),
module_with_async_fn_awaiting_in_a_match(MirTy::F64),
module_with_async_fn_awaiting_unit(),
]
}
#[test]
fn no_terminator_names_a_reserved_block_except_a_status_switchs_default() {
for mut m in every_awaiting_module() {
transform(&mut m);
let poll = find(&m, "f.15$poll");
for (i, b) in poll.blocks.iter().enumerate() {
let named: Vec<BlockId> = match &b.term {
Terminator::Goto(t) => vec![*t],
Terminator::Branch { then_, else_, .. } => vec![*then_, *else_],
Terminator::Switch { arms, default, .. } => arms
.iter()
.map(|(_, b)| *b)
.chain((*default != BAD_DISCRIMINANT).then_some(*default))
.collect(),
Terminator::Return(_) | Terminator::Trap => Vec::new(),
};
for t in named {
assert!(
t.0 >= RESERVED_BLOCKS,
"block {i} targets reserved block {t:?}, so the split did \
not renumber it: {:?}",
poll.blocks
);
}
}
}
}
#[test]
fn a_body_switchs_default_is_renumbered_along_with_its_arms() {
let mut m = module_with_async_fn_awaiting_in_a_match(MirTy::F64);
transform(&mut m);
let poll = find(&m, "f.15$poll");
let defaults: Vec<BlockId> = poll
.blocks
.iter()
.filter_map(|b| match &b.term {
Terminator::Switch { default, .. } if *default != BAD_DISCRIMINANT => {
Some(*default)
}
_ => None,
})
.collect();
assert_eq!(
defaults.len(),
1,
"the fixture has exactly one `Switch` of its own, and its default must \
not have been collapsed onto the shared trap: {:?}",
poll.blocks
);
assert!(
matches!(poll.blocks[defaults[0].0 as usize].term, Terminator::Trap),
"the body `Switch`'s default must still reach the trap block it named \
before the split, at its new id: {:?}",
poll.blocks
);
}
#[test]
fn the_split_leaves_no_await_marker_and_no_dangling_block_target() {
for mut m in every_awaiting_module() {
transform(&mut m);
for f in &m.functions {
assert!(
!stmts_of(f).any(|s| matches!(s, Stmt::Await { .. })),
"`{}` still contains an await marker: {:?}",
f.name,
f.blocks
);
for t in targets(f) {
assert!(
(t.0 as usize) < f.blocks.len(),
"`{}` jumps to {t:?}, past its {} blocks: {:?}",
f.name,
f.blocks.len(),
f.blocks
);
}
}
}
}
#[test]
fn an_await_free_body_gains_no_dispatch_block() {
let mut m = module_with_async_const_fn(MirTy::F64, &[]);
let before = m.functions[0].blocks.len();
transform(&mut m);
let poll = find(&m, "f.15$poll");
assert_eq!(
poll.blocks.len(),
before,
"the block list must be untouched: {:?}",
poll.blocks
);
assert!(
matches!(poll.blocks[0].term, Terminator::Return(_)),
"no tag dispatch may be introduced: {:?}",
poll.blocks
);
}
#[test]
fn a_suspend_needs_no_state_slot_beyond_the_bodys_own_temps() {
let mut m = module_with_async_fn_awaiting_twice(MirTy::F64);
let n_temps = m.functions[0].temps.len();
transform(&mut m);
let wrapper = find(&m, "f.15");
let expected = ((STATE_SLOT_TEMPS as usize + n_temps) * 8) as i64;
let sizes: Vec<i64> = stmts_of(wrapper)
.filter_map(|s| match s {
Stmt::CallRuntime {
func: RtFunc::Alloc,
args,
..
} => Some(args.clone()),
_ => None,
})
.flatten()
.filter_map(|t| {
stmts_of(wrapper).find_map(|s| match s {
Stmt::ConstInt(d, v) if *d == t => Some(*v),
_ => None,
})
})
.collect();
assert_eq!(
sizes,
vec![expected],
"a resumable body's state object is still \
(STATE_SLOT_TEMPS + n_temps) * 8 bytes: {:?}",
wrapper.blocks
);
}
}