use core::future::Future;
use core::pin::Pin;
use core::task::{Context, Poll};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Op {
Push(i64),
Await,
Add,
Mul,
}
pub fn await_count(ops: &[Op]) -> usize {
ops.iter().filter(|o| matches!(o, Op::Await)).count()
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct SuspendEvent {
pub await_index: usize,
pub stack_depth: usize,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Suspension {
pub await_index: usize,
pub stack: Vec<i64>,
}
enum Step {
Done(i64),
Suspended(usize),
}
trait Machine {
fn run(&mut self, ready: &[i64], awaited: &[i64]) -> Step;
fn stack(&self) -> &[i64];
}
struct Interp {
ops: Vec<Op>,
await_index_at: Vec<usize>,
pc: usize,
stack: Vec<i64>,
}
impl Interp {
fn new(ops: &[Op]) -> Self {
let mut await_index_at = vec![0usize; ops.len()];
let mut next = 0;
for (pc, op) in ops.iter().enumerate() {
if matches!(op, Op::Await) {
await_index_at[pc] = next;
next += 1;
}
}
Interp {
ops: ops.to_vec(),
await_index_at,
pc: 0,
stack: Vec::new(),
}
}
}
impl Machine for Interp {
fn run(&mut self, ready: &[i64], awaited: &[i64]) -> Step {
loop {
if self.pc >= self.ops.len() {
return Step::Done(*self.stack.last().expect("non-empty result stack"));
}
match self.ops[self.pc] {
Op::Push(n) => {
self.stack.push(n);
self.pc += 1;
}
Op::Add => {
let b = self.stack.pop().unwrap();
let a = self.stack.pop().unwrap();
self.stack.push(a + b);
self.pc += 1;
}
Op::Mul => {
let b = self.stack.pop().unwrap();
let a = self.stack.pop().unwrap();
self.stack.push(a * b);
self.pc += 1;
}
Op::Await => {
let idx = self.await_index_at[self.pc];
if ready[idx] != 0 {
self.stack.push(awaited[idx]);
self.pc += 1;
} else {
return Step::Suspended(idx);
}
}
}
}
}
fn stack(&self) -> &[i64] {
&self.stack
}
}
mod jit_lane {
use super::{Machine, Op, Step};
use crate::jit::{NativeProgram, StencilLayout, async_stencils};
#[repr(C)]
struct Ctx {
prog: *const u64,
sp: *mut i64,
ready: *const i64,
awaited: *const i64,
resume: *mut u64,
await_index: *mut u64,
suspended: *mut i64,
}
pub fn available() -> bool {
!async_stencils::PUSH.is_empty() && crate::jit::NATIVE_COPY_PATCH_AVAILABLE
}
pub struct JitMachine {
native: NativeProgram,
stack: Vec<i64>,
sp_len: usize,
prog: *const u64,
resume_offset: usize,
started: bool,
suspended: i64,
resume_scratch: u64,
await_index_scratch: u64,
}
impl JitMachine {
pub fn compile(ops: &[Op]) -> Option<JitMachine> {
if !available() {
return None;
}
let mut layout = StencilLayout::new();
let root = layout.start_chain();
let mut sites: Vec<(usize, &'static [usize])> = Vec::new();
let mut await_count = 0usize;
for op in ops {
if let Op::Push(n) = op {
layout.push_prog_word(root.prog_index, *n as u64);
}
let (bytes, cont): (&[u8], &'static [usize]) = match op {
Op::Push(_) => (async_stencils::PUSH, async_stencils::PUSH_CONT),
Op::Await => (async_stencils::AWAIT, async_stencils::AWAIT_CONT),
Op::Add => (async_stencils::ADD, async_stencils::ADD_CONT),
Op::Mul => (async_stencils::MUL, async_stencils::MUL_CONT),
};
let start = layout.emit_stencil(bytes);
if matches!(op, Op::Await) {
layout.push_prog_word(root.prog_index, start as u64);
layout.push_prog_word(root.prog_index, await_count as u64);
await_count += 1;
}
sites.push((start, cont));
}
let done = layout.emit_stencil(async_stencils::DONE);
for i in 0..sites.len() {
let (start, cont) = sites[i];
let target = sites.get(i + 1).map(|(s, _)| *s).unwrap_or(done);
for &rel in cont {
layout.patch_continuation(start + rel, target);
}
}
let native = NativeProgram::new(layout, root);
let prog = native.entry_prog();
Some(JitMachine {
native,
stack: vec![0; 256],
sp_len: 0,
prog,
resume_offset: 0,
started: false,
suspended: 0,
resume_scratch: 0,
await_index_scratch: 0,
})
}
}
impl Machine for JitMachine {
fn run(&mut self, ready: &[i64], awaited: &[i64]) -> Step {
let entry = if self.started {
self.resume_offset
} else {
self.started = true;
0
};
self.suspended = 0;
let base = self.stack.as_mut_ptr();
let mut ctx = Ctx {
prog: self.prog,
sp: unsafe { base.add(self.sp_len) },
ready: ready.as_ptr(),
awaited: awaited.as_ptr(),
resume: &mut self.resume_scratch,
await_index: &mut self.await_index_scratch,
suspended: &mut self.suspended,
};
let f = unsafe { self.native.chain_fn::<Ctx>(entry) };
unsafe { f(&mut ctx) };
self.sp_len = (ctx.sp as usize - base as usize) / size_of::<i64>();
self.prog = ctx.prog;
if self.suspended != 0 {
self.resume_offset = self.resume_scratch as usize;
Step::Suspended(self.await_index_scratch as usize)
} else {
Step::Done(self.stack[self.sp_len - 1])
}
}
fn stack(&self) -> &[i64] {
&self.stack[..self.sp_len]
}
}
}
pub fn jit_available() -> bool {
jit_lane::available()
}
fn best_machine(ops: &[Op]) -> (Box<dyn Machine>, bool) {
if let Some(m) = jit_lane::JitMachine::compile(ops) {
return (Box::new(m), true);
}
(Box::new(Interp::new(ops)), false)
}
pub struct AsyncExec {
machine: Box<dyn Machine>,
jit: bool,
inners: Vec<Pin<Box<dyn Future<Output = i64>>>>,
resolved: Vec<bool>,
ready: Vec<i64>,
awaited: Vec<i64>,
parked_on: Option<usize>,
pub trace: Vec<SuspendEvent>,
}
impl AsyncExec {
fn with_machine(
machine: Box<dyn Machine>,
jit: bool,
ops: &[Op],
inners: Vec<Pin<Box<dyn Future<Output = i64>>>>,
) -> Self {
let n = await_count(ops);
assert_eq!(inners.len(), n, "one input future per await point");
AsyncExec {
machine,
jit,
inners,
resolved: vec![false; n],
ready: vec![0; n],
awaited: vec![0; n],
parked_on: None,
trace: Vec::new(),
}
}
pub fn new(ops: &[Op], inners: Vec<Pin<Box<dyn Future<Output = i64>>>>) -> Self {
let (machine, jit) = best_machine(ops);
Self::with_machine(machine, jit, ops, inners)
}
pub fn interpret(ops: &[Op], inners: Vec<Pin<Box<dyn Future<Output = i64>>>>) -> Self {
Self::with_machine(Box::new(Interp::new(ops)), false, ops, inners)
}
pub fn is_jit(&self) -> bool {
self.jit
}
pub fn suspension(&self) -> Option<Suspension> {
self.parked_on.map(|await_index| Suspension {
await_index,
stack: self.machine.stack().to_vec(),
})
}
}
impl Future for AsyncExec {
type Output = i64;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<i64> {
let this = &mut *self;
for i in 0..this.inners.len() {
if !this.resolved[i]
&& let Poll::Ready(value) = this.inners[i].as_mut().poll(cx)
{
this.awaited[i] = value;
this.ready[i] = 1;
this.resolved[i] = true;
}
}
if let Some(i) = this.parked_on
&& this.ready[i] == 0
{
return Poll::Pending;
}
match this.machine.run(&this.ready, &this.awaited) {
Step::Done(result) => Poll::Ready(result),
Step::Suspended(await_index) => {
this.parked_on = Some(await_index);
this.trace.push(SuspendEvent {
await_index,
stack_depth: this.machine.stack().len(),
});
Poll::Pending
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
fn later(value: i64, ms: u64) -> Pin<Box<dyn Future<Output = i64>>> {
Box::pin(async move {
tokio::time::sleep(Duration::from_millis(ms)).await;
value
})
}
async fn drive(mut exec: AsyncExec) -> (i64, bool, Vec<SuspendEvent>) {
let jit = exec.is_jit();
let result = core::future::poll_fn(|cx| Pin::new(&mut exec).poll(cx)).await;
(result, jit, exec.trace.clone())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn interpreter_lane_is_always_available() {
let ops = [Op::Push(40), Op::Await, Op::Add];
let (result, jit, trace) = drive(AsyncExec::interpret(&ops, vec![later(2, 40)])).await;
assert_eq!(result, 42);
assert!(!jit, "interpret() must never use the JIT");
assert_eq!(
trace,
vec![SuspendEvent {
await_index: 0,
stack_depth: 1
}]
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn interpreter_and_jit_agree_differentially() {
let programs: &[&[Op]] = &[
&[Op::Push(40), Op::Await, Op::Add],
&[Op::Await, Op::Await, Op::Mul],
&[Op::Push(10), Op::Push(20), Op::Add, Op::Await, Op::Mul],
];
for ops in programs {
let n = await_count(ops);
let inners_i: Vec<_> = (0..n)
.map(|k| later(2 + k as i64, 20 + k as u64 * 20))
.collect();
let inners_j: Vec<_> = (0..n)
.map(|k| later(2 + k as i64, 20 + k as u64 * 20))
.collect();
let (ri, _, ti) = drive(AsyncExec::interpret(ops, inners_i)).await;
let exec_j = AsyncExec::new(ops, inners_j);
if !exec_j.is_jit() {
continue; }
let (rj, _, tj) = drive(exec_j).await;
assert_eq!(ri, rj, "result mismatch interp vs jit for {ops:?}");
assert_eq!(ti, tj, "trace mismatch interp vs jit for {ops:?}");
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn ready_input_never_suspends() {
let ops = [Op::Push(100), Op::Await, Op::Add];
let (result, _, trace) = drive(AsyncExec::interpret(
&ops,
vec![Box::pin(core::future::ready(23))],
))
.await;
assert_eq!(result, 123);
assert!(trace.is_empty());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn independent_awaits_resolve_concurrently() {
let ops = [Op::Await, Op::Await, Op::Add];
let (result, _, trace) = drive(AsyncExec::interpret(
&ops,
vec![later(40, 60), later(2, 20)],
))
.await;
assert_eq!(result, 42);
assert_eq!(trace.len(), 1, "concurrent ⇒ one park: {trace:?}");
assert_eq!(trace[0].await_index, 0);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn suspension_is_inspectable_while_parked() {
let ops = [Op::Push(10), Op::Push(20), Op::Add, Op::Await, Op::Mul];
let mut exec = AsyncExec::interpret(&ops, vec![later(4, 50)]);
let mut inspected = false;
let result = core::future::poll_fn(|cx| {
let p = Pin::new(&mut exec).poll(cx);
if p.is_pending()
&& let Some(s) = exec.suspension()
{
assert_eq!(s.await_index, 0);
assert_eq!(s.stack, vec![30]);
inspected = true;
}
p
})
.await;
assert!(inspected);
assert_eq!(result, 120);
}
}