Repository: denoland/deno_core
Pull request #40 (2023-07-17 17:52:02)

feat(ops): op2 async support (slow path only)
This implements:

 - #[op2(async)] for both `fn -> impl Future` and `async fn`.
 - Fastcalls are not (yet) supported
 - Result<impl Future<...>> is not yet supported


Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 1b7faa0..3c1cb88 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -93,6 +93,15 @@ version = "0.13.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
 
+[[package]]
+name = "basic-toml"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c0de75129aa8d0cceaf750b89013f0e08804d6ec61416da787b35ad0d7cddf1"
+dependencies = [
+ "serde",
+]
+
 [[package]]
 name = "bencher"
 version = "0.1.5"
@@ -322,7 +331,7 @@ dependencies = [
  "deno-proc-macro-rules",
  "lazy-regex",
  "once_cell",
- "pmutil 0.5.3",
+ "pmutil 0.6.1",
  "pretty_assertions",
  "prettyplease",
  "proc-macro-crate",
@@ -338,6 +347,22 @@ dependencies = [
  "v8",
 ]
 
+[[package]]
+name = "deno_ops_compile_test_runner"
+version = "0.1.0"
+dependencies = [
+ "bytes",
+ "deno_core",
+ "deno_ops",
+ "pretty_assertions",
+ "prettyplease",
+ "rustversion",
+ "serde",
+ "syn 2.0.22",
+ "testing_macros",
+ "trybuild",
+]
+
 [[package]]
 name = "derive_more"
 version = "0.99.17"
@@ -1782,6 +1807,15 @@ dependencies = [
  "unicode-ident",
 ]
 
+[[package]]
+name = "termcolor"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6"
+dependencies = [
+ "winapi-util",
+]
+
 [[package]]
 name = "testing_macros"
 version = "0.2.11"
@@ -1933,6 +1967,21 @@ dependencies = [
  "stable_deref_trait",
 ]
 
+[[package]]
+name = "trybuild"
+version = "1.0.80"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "501dbdbb99861e4ab6b60eb6a7493956a9defb644fd034bc4a5ef27c693c8a3a"
+dependencies = [
+ "basic-toml",
+ "glob",
+ "once_cell",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "termcolor",
+]
+
 [[package]]
 name = "typed-arena"
 version = "2.0.2"
@@ -2041,6 +2090,15 @@ version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
 
+[[package]]
+name = "winapi-util"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
+dependencies = [
+ "winapi",
+]
+
 [[package]]
 name = "winapi-x86_64-pc-windows-gnu"
 version = "0.4.0"
diff --git a/Cargo.toml b/Cargo.toml
index 6f0f359..17b7b10 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -5,6 +5,7 @@ resolver = "2"
 members = [
   "core",
   "ops",
+  "ops/compile_test_runner",
   "serde_v8",
 ]
 
diff --git a/core/ops.rs b/core/ops.rs
index 7d4bc1e..80e1c14 100644
--- a/core/ops.rs
+++ b/core/ops.rs
@@ -62,19 +62,30 @@ impl<F: Future<Output = OpResult>> Future for OpCall<F> {
   }
 }
 
+#[allow(clippy::type_complexity)]
 pub enum OpResult {
   Ok(serde_v8::SerializablePkg),
   Err(OpError),
+  /// We temporarily provide a mapping function in a box for op2. This will go away when op goes away.
+  Op2Temp(
+    Box<
+      dyn for<'a> FnOnce(
+        &mut v8::HandleScope<'a>,
+      )
+        -> Result<v8::Local<'a, v8::Value>, serde_v8::Error>,
+    >,
+  ),
 }
 
 impl OpResult {
   pub fn to_v8<'a>(
-    &mut self,
+    self,
     scope: &mut v8::HandleScope<'a>,
   ) -> Result<v8::Local<'a, v8::Value>, serde_v8::Error> {
     match self {
-      Self::Ok(x) => x.to_v8(scope),
+      Self::Ok(mut x) => x.to_v8(scope),
       Self::Err(err) => serde_v8::to_v8(scope, err),
+      Self::Op2Temp(f) => f(scope),
     }
   }
 }
diff --git a/core/runtime/jsruntime.rs b/core/runtime/jsruntime.rs
index 63dc82b..30ae136 100644
--- a/core/runtime/jsruntime.rs
+++ b/core/runtime/jsruntime.rs
@@ -2369,7 +2369,7 @@ impl JsRuntime {
           break;
         };
         // TODO(mmastrac): If this task is really errored, things could be pretty bad
-        let (promise_id, op_id, mut resp) = item.unwrap();
+        let (promise_id, op_id, resp) = item.unwrap();
         state
           .borrow()
           .op_state
diff --git a/core/runtime/ops.rs b/core/runtime/ops.rs
index 14dc5c5..9035ce4 100644
--- a/core/runtime/ops.rs
+++ b/core/runtime/ops.rs
@@ -128,7 +128,7 @@ pub fn queue_async_op<'s>(
 
   match pinned.poll_unpin(&mut Context::from_waker(noop_waker_ref())) {
     Poll::Pending => {}
-    Poll::Ready(mut res) => {
+    Poll::Ready(res) => {
       if deferred {
         ctx.context_state.borrow_mut().pending_ops.spawn(ready(res));
         return None;
@@ -143,6 +143,84 @@ pub fn queue_async_op<'s>(
   None
 }
 
+#[inline]
+pub fn map_async_op_infallible<R: 'static>(
+  ctx: &OpCtx,
+  promise_id: i32,
+  op: impl Future<Output = R> + 'static,
+  rv_map: for<'r> fn(
+    &mut v8::HandleScope<'r>,
+    R,
+  ) -> Result<v8::Local<'r, v8::Value>, serde_v8::Error>,
+) -> Option<R> {
+  let id = ctx.id;
+
+  // TODO(mmastrac): We have to poll every future here because that assumption is baked into a large number
+  // of ops. If we can figure out a way around this, we can remove this call to boxed_local and save a malloc per future.
+  let mut pinned = op.map(move |res| (promise_id, id, res)).boxed_local();
+
+  match pinned.poll_unpin(&mut Context::from_waker(noop_waker_ref())) {
+    Poll::Pending => {}
+    Poll::Ready(res) => {
+      ctx.state.borrow_mut().tracker.track_async_completed(ctx.id);
+      return Some(res.2);
+    }
+  }
+
+  ctx
+    .context_state
+    .borrow_mut()
+    .pending_ops
+    .spawn(pinned.map(move |r| {
+      (
+        r.0,
+        r.1,
+        OpResult::Op2Temp(Box::new(move |scope| rv_map(scope, r.2))),
+      )
+    }));
+  None
+}
+
+#[inline]
+pub fn map_async_op_fallible<R: 'static, E: Into<Error> + 'static>(
+  ctx: &OpCtx,
+  promise_id: i32,
+  op: impl Future<Output = Result<R, E>> + 'static,
+  rv_map: for<'r> fn(
+    &mut v8::HandleScope<'r>,
+    R,
+  ) -> Result<v8::Local<'r, v8::Value>, serde_v8::Error>,
+) -> Option<Result<R, E>> {
+  let id = ctx.id;
+
+  // TODO(mmastrac): We have to poll every future here because that assumption is baked into a large number
+  // of ops. If we can figure out a way around this, we can remove this call to boxed_local and save a malloc per future.
+  let mut pinned = op.map(move |res| (promise_id, id, res)).boxed_local();
+
+  match pinned.poll_unpin(&mut Context::from_waker(noop_waker_ref())) {
+    Poll::Pending => {}
+    Poll::Ready(res) => {
+      ctx.state.borrow_mut().tracker.track_async_completed(ctx.id);
+      return Some(res.2);
+    }
+  }
+
+  let get_class = RefCell::borrow(&ctx.state).get_error_class_fn;
+  ctx.context_state.borrow_mut().pending_ops.spawn(pinned.map(
+    move |r| match r.2 {
+      Ok(v) => (
+        r.0,
+        r.1,
+        OpResult::Op2Temp(Box::new(move |scope| rv_map(scope, v))),
+      ),
+      Err(err) => {
+        (r.0, r.1, OpResult::Err(OpError::new(get_class, err.into())))
+      }
+    },
+  ));
+  None
+}
+
 macro_rules! try_number {
   ($n:ident $type:ident $is:ident) => {
     if $n.$is() {
@@ -402,18 +480,24 @@ mod tests {
   use crate::JsRuntime;
   use crate::OpState;
   use crate::RuntimeOptions;
+  use anyhow::bail;
+  use anyhow::Error;
   use deno_ops::op2;
+  use futures::Future;
   use serde::Deserialize;
   use serde::Serialize;
   use std::borrow::Cow;
   use std::cell::Cell;
   use std::cell::RefCell;
   use std::rc::Rc;
+  use std::time::Duration;
 
   crate::extension!(
     testing,
     ops = [
       op_test_fail,
+      op_test_print_debug,
+
       op_test_add,
       op_test_add_option,
       op_test_result_void_switch,
@@ -449,6 +533,15 @@ mod tests {
       op_buffer_slice,
       op_buffer_slice_unsafe_callback,
       op_buffer_copy,
+
+      op_async_void,
+      op_async_number,
+      op_async_add,
+      op_async_sleep,
+      op_async_sleep_impl,
+      op_async_sleep_error,
+      op_async_state_rc,
+      op_async_buffer_impl,
     ],
     state = |state| {
       state.put(1234u32);
@@ -465,6 +558,11 @@ mod tests {
     FAIL.with(|b| b.set(true))
   }
 
+  #[op2(core, fast)]
+  pub fn op_test_print_debug(#[string] s: &str) {
+    println!("{s}")
+  }
+
   /// Run a test for a single op.
   fn run_test2(repeat: usize, op: &str, test: &str) -> Result<(), AnyError> {
     let mut runtime = JsRuntime::new(RuntimeOptions {
@@ -479,12 +577,15 @@ mod tests {
         FastString::Owned(
           format!(
             r"
-            const {{ op_test_fail, {op} }} = Deno.core.ensureFastOps();
+            const {{ op_test_fail, op_test_print_debug, {op} }} = Deno.core.ensureFastOps();
             function assert(b) {{
               if (!b) {{
                 op_test_fail();
               }}
             }}
+            function log(s) {{
+              op_test_print_debug(String(s))
+            }}
           "
           )
           .into(),
@@ -512,6 +613,64 @@ mod tests {
     }
   }
 
+  /// Run a test for a single op.
+  async fn run_async_test(
+    repeat: usize,
+    op: &str,
+    test: &str,
+  ) -> Result<(), AnyError> {
+    let mut runtime = JsRuntime::new(RuntimeOptions {
+      extensions: vec![testing::init_ops_and_esm()],
+      ..Default::default()
+    });
+    let err_mapper =
+      |err| generic_error(format!("{op} test failed ({test}): {err:?}"));
+    runtime
+      .execute_script(
+        "",
+        FastString::Owned(
+          format!(
+            r"
+            const {{ op_test_fail, op_test_print_debug, {op} }} = Deno.core.ensureFastOps();
+            function assert(b) {{
+              if (!b) {{
+                op_test_fail();
+              }}
+            }}
+            function log(s) {{
+              op_test_print_debug(String(s))
+            }}
+          "
+          )
+          .into(),
+        ),
+      )
+      .map_err(err_mapper)?;
+    FAIL.with(|b| b.set(false));
+    runtime.execute_script(
+      "",
+      FastString::Owned(
+        format!(
+          r"
+            (async () => {{
+              for (let __index__ = 0; __index__ < {repeat}; __index__++) {{
+                {test}
+              }}
+            }})()
+          "
+        )
+        .into(),
+      ),
+    )?;
+
+    runtime.run_event_loop(false).await?;
+    if FAIL.with(|b| b.get()) {
+      Err(generic_error(format!("{op} test failed ({test})")))
+    } else {
+      Ok(())
+    }
+  }
+
   #[tokio::test(flavor = "current_thread")]
   pub async fn test_op_fail() {
     assert!(run_test2(1, "", "assert(false)").is_err());
@@ -1165,4 +1324,115 @@ mod tests {
     )?;
     Ok(())
   }
+
+  #[op2(async, core)]
+  async fn op_async_void() {}
+
+  #[tokio::test]
+  pub async fn test_op_async_void() -> Result<(), Box<dyn std::error::Error>> {
+    run_async_test(10000, "op_async_void", "await op_async_void()").await?;
+    Ok(())
+  }
+
+  #[op2(async, core)]
+  async fn op_async_number(x: u32) -> u32 {
+    x
+  }
+
+  #[op2(async, core)]
+  async fn op_async_add(x: u32, y: u32) -> u32 {
+    x + y
+  }
+
+  #[tokio::test]
+  pub async fn test_op_async_number() -> Result<(), Box<dyn std::error::Error>>
+  {
+    run_async_test(
+      10000,
+      "op_async_number",
+      "assert(await op_async_number(__index__) == __index__)",
+    )
+    .await?;
+    run_async_test(
+      10000,
+      "op_async_add",
+      "assert(await op_async_add(__index__, 100) == __index__ + 100)",
+    )
+    .await?;
+    Ok(())
+  }
+
+  #[op2(async, core)]
+  async fn op_async_sleep() {
+    tokio::time::sleep(Duration::from_millis(500)).await
+  }
+
+  #[op2(async, core)]
+  fn op_async_sleep_impl() -> impl Future<Output = ()> {
+    tokio::time::sleep(Duration::from_millis(500))
+  }
+
+  #[tokio::test]
+  pub async fn test_op_async_sleep() -> Result<(), Box<dyn std::error::Error>> {
+    run_async_test(5, "op_async_sleep", "await op_async_sleep()").await?;
+    run_async_test(5, "op_async_sleep_impl", "await op_async_sleep_impl()")
+      .await?;
+    Ok(())
+  }
+
+  #[op2(async, core)]
+  pub async fn op_async_sleep_error() -> Result<(), Error> {
+    tokio::time::sleep(Duration::from_millis(500)).await;
+    bail!("whoops")
+  }
+
+  #[tokio::test]
+  pub async fn test_op_async_sleep_error(
+  ) -> Result<(), Box<dyn std::error::Error>> {
+    run_async_test(
+      5,
+      "op_async_sleep_error",
+      "try { await op_async_sleep_error(); assert(false) } catch (e) {}",
+    )
+    .await?;
+    Ok(())
+  }
+
+  #[op2(async, core)]
+  pub async fn op_async_state_rc(
+    state: Rc<RefCell<OpState>>,
+    value: u32,
+  ) -> u32 {
+    let old_value: u32 = state.borrow_mut().take();
+    state.borrow_mut().put(value);
+    old_value
+  }
+
+  #[tokio::test]
+  pub async fn test_op_async_state() -> Result<(), Box<dyn std::error::Error>> {
+    run_async_test(
+      5,
+      "op_async_state_rc",
+      "if (__index__ == 0) { await op_async_state_rc(__index__) } else { assert(await op_async_state_rc(__index__) == __index__ - 1) }",
+    ).await?;
+    Ok(())
+  }
+
+  #[op2(async, core)]
+  fn op_async_buffer_impl(#[buffer] input: &[u8]) -> impl Future<Output = u32> {
+    let l = input.len();
+    async move { l as _ }
+  }
+
+  #[tokio::test]
+  pub async fn test_op_async_buffer() -> Result<(), Box<dyn std::error::Error>>
+  {
+    run_async_test(
+      5,
+      "op_async_buffer_impl",
+      "assert(await op_async_buffer_impl(new Uint8Array(10)) == 10)",
+    )
+    .await?;
+    Ok(())
+  }
 }
diff --git a/ops/Cargo.toml b/ops/Cargo.toml
index f4bf41d..a5258ac 100644
--- a/ops/Cargo.toml
+++ b/ops/Cargo.toml
@@ -18,7 +18,7 @@ proc-macro = true
 deno-proc-macro-rules.workspace = true
 lazy-regex.workspace = true
 once_cell.workspace = true
-pmutil = "0.5.3"
+pmutil = "0.6.1"
 proc-macro-crate = "1.1.3"
 proc-macro2.workspace = true
 quote.workspace = true
diff --git a/ops/op2/README.md b/ops/op2/README.md
index a7982d9..e45513a 100644
--- a/ops/op2/README.md
+++ b/ops/op2/README.md
@@ -2,7 +2,7 @@
 
 `#[op2]` is the in-progress replacement for `#[op]`.
 
-# Strings
+## Strings
 
 `String`s in Rust are always UTF-8. `String`s in v8, however, are either
 two-byte UTF-16 or one-byte Latin-1. One-byte Latin-1 strings are not
@@ -15,6 +15,30 @@ string. At this time there is no way to avoid this copy, though the `op` code
 does attempt to avoid any allocations where possible by making use of a stack
 buffer.
 
+## `async` calls
+
+Asynchronous calls are supported in two forms:
+
+```rust
+async fn op_xyz(...) -> X {}
+```
+
+and
+
+```rust
+fn op_xyz(...) -> impl Future<Output = X> {}
+```
+
+These are desugared to a function that adds a hidden `promise_id` argument, and
+returns `Option<X>` instead. Deno will eagerly poll the op, and if it is
+immediately ready, the function will return `Some(X)`. If the op is not ready,
+the function will return `None` and the future will be handled by Deno's pending
+op system.
+
+```rust
+fn op_xyz(promise_id: i32, ...) -> Option<X> {}
+```
+
 # Parameters
 
 <!-- START -->
diff --git a/ops/op2/dispatch_async.rs b/ops/op2/dispatch_async.rs
new file mode 100644
index 0000000..ca05cfc
--- /dev/null
+++ b/ops/op2/dispatch_async.rs
@@ -0,0 +1,164 @@
+use proc_macro2::TokenStream;
+use quote::quote;
+
+use super::dispatch_slow::call;
+use super::dispatch_slow::extract_arg;
+use super::dispatch_slow::from_arg;
+use super::dispatch_slow::return_value_infallible;
+use super::dispatch_slow::return_value_result;
+use super::dispatch_slow::return_value_v8_value;
+use super::dispatch_slow::with_fn_args;
+use super::dispatch_slow::with_opctx;
+use super::dispatch_slow::with_opstate;
+use super::dispatch_slow::with_retval;
+use super::dispatch_slow::with_scope;
+use super::generator_state::GeneratorState;
+use super::signature::ParsedSignature;
+use super::signature::RetVal;
+use super::V8MappingError;
+
+pub(crate) fn generate_dispatch_async(
+  generator_state: &mut GeneratorState,
+  signature: &ParsedSignature,
+) -> Result<TokenStream, V8MappingError> {
+  let mut output = TokenStream::new();
+
+  // Collect virtual arguments in a deferred list that we compute at the very end. This allows us to borrow
+  // the scope/opstate in the intermediate stages.
+  let mut deferred = TokenStream::new();
+
+  // Promise ID is the first arg
+  let mut input_index = 1;
+
+  for (index, arg) in signature.args.iter().enumerate() {
+    if arg.is_virtual() {
+      deferred.extend(from_arg(generator_state, index, arg)?);
+    } else {
+      output.extend(extract_arg(generator_state, index, input_index)?);
+      output.extend(from_arg(generator_state, index, arg)?);
+      input_index += 1;
+    }
+  }
+
+  // Always need context and args
+  // TODO(mmastrac): Do we?
+  generator_state.needs_opctx = true;
+  generator_state.needs_args = true;
+  generator_state.needs_scope = true;
+  generator_state.needs_retval = true;
+
+  let return_value = match &signature.ret_val {
+    RetVal::Future(r) | RetVal::ResultFuture(r) => return_value_v8_value(
+      &generator_state.deno_core,
+      &generator_state.scope,
+      &generator_state.result,
+      r,
+    )?,
+    RetVal::FutureResult(r) | RetVal::ResultFutureResult(r) => {
+      return_value_v8_value(
+        &generator_state.deno_core,
+        &generator_state.scope,
+        &generator_state.result,
+        r,
+      )?
+    }
+    RetVal::Infallible(r) | RetVal::Result(r) => {
+      return Err(V8MappingError::NoMapping("an async return", r.clone()))
+    }
+  };
+  let (mapper, return_value_immediate) = match &signature.ret_val {
+    RetVal::Future(r) | RetVal::ResultFuture(r) => (
+      quote!(map_async_op_infallible),
+      return_value_infallible(generator_state, r)?,
+    ),
+    RetVal::FutureResult(r) | RetVal::ResultFutureResult(r) => (
+      quote!(map_async_op_fallible),
+      return_value_result(generator_state, r)?,
+    ),
+    RetVal::Infallible(r) | RetVal::Result(r) => {
+      return Err(V8MappingError::NoMapping("an async return", r.clone()))
+    }
+  };
+
+  output.extend(deferred);
+  output.extend(call(generator_state)?);
+  let result = &generator_state.result;
+  let opctx = &generator_state.opctx;
+  let scope = &generator_state.scope;
+  let fn_args = &generator_state.fn_args;
+  let promise_id = &generator_state.promise_id;
+  let deno_core = &generator_state.deno_core;
+
+  if matches!(
+    signature.ret_val,
+    RetVal::ResultFuture(_) | RetVal::ResultFutureResult(_)
+  ) {
+    output.extend(quote! {
+      todo!("result of future currently not implemented");
+      let Ok(#result) = #result else {
+        // Handle eager error -- this will leave only a Future<R> or Future<Result<R>>
+        // TODO(mmastrac): This is unimplemented
+      };
+    });
+  }
+
+  output.extend(quote! {
+    // TODO(mmastrac): We are extending the eager polling behaviour temporarily to op2, but I would like to get
+    // rid of it as soon as we can
+    let #promise_id = #deno_core::_ops::to_i32(&#fn_args.get(0));
+    if let Some(#result) = #deno_core::_ops::#mapper(#opctx, #promise_id, #result, |#scope, #result| {
+      #return_value
+    }) {
+      // Eager poll returned a value
+      #return_value_immediate
+    }
+  });
+
+  let with_scope = if generator_state.needs_scope {
+    with_scope(generator_state)
+  } else {
+    quote!()
+  };
+
+  let with_opstate = if generator_state.needs_opstate {
+    with_opstate(generator_state)
+  } else {
+    quote!()
+  };
+
+  let with_opctx = if generator_state.needs_opctx {
+    with_opctx(generator_state)
+  } else {
+    quote!()
+  };
+
+  let with_retval = if generator_state.needs_retval {
+    with_retval(generator_state)
+  } else {
+    quote!()
+  };
+
+  let with_args = if generator_state.needs_args {
+    with_fn_args(generator_state)
+  } else {
+    quote!()
+  };
+
+  let GeneratorState {
+    deno_core,
+    info,
+    slow_function,
+    ..
+  } = &generator_state;
+
+  Ok(quote! {
+    extern "C" fn #slow_function(#info: *const #deno_core::v8::FunctionCallbackInfo) {
+    #with_scope
+    #with_retval
+    #with_args
+    #with_opctx
+    #with_opstate
+
+    #output
+  }})
+}
diff --git a/ops/op2/dispatch_fast.rs b/ops/op2/dispatch_fast.rs
index 9b8e50b..2722ab2 100644
--- a/ops/op2/dispatch_fast.rs
+++ b/ops/op2/dispatch_fast.rs
@@ -129,6 +129,11 @@ pub fn generate_dispatch_fast(
     Virtual(&'a Arg),
   }
 
+  // Async not supported for fastcalls yet
+  if signature.ret_val.is_async() {
+    return Ok(None);
+  }
+
   let mut inputs = vec![];
   for arg in &signature.args {
     let Some(fv) = map_arg_to_v8_fastcall_type(arg)? else {
@@ -144,6 +149,7 @@ pub fn generate_dispatch_fast(
   let ret_val = match &signature.ret_val {
     RetVal::Infallible(arg) => arg,
     RetVal::Result(arg) => arg,
+    _ => todo!(),
   };
 
   let output = match map_retval_to_v8_fastcall_type(ret_val)? {
@@ -219,13 +225,14 @@ pub fn generate_dispatch_fast(
         };
       }
     }
+    _ => todo!(),
   };
 
   let with_opctx = if *needs_fast_opctx {
     *needs_fast_api_callback_options = true;
     quote!(
       let #opctx = unsafe {
-        &*(#deno_core::v8::Local::<v8::External>::cast(unsafe { #fast_api_callback_options.data.data }).value()
+        &*(#deno_core::v8::Local::<#deno_core::v8::External>::cast(unsafe { #fast_api_callback_options.data.data }).value()
             as *const #deno_core::_ops::OpCtx)
       };
     )
diff --git a/ops/op2/dispatch_slow.rs b/ops/op2/dispatch_slow.rs
index 467572d..24f5d32 100644
--- a/ops/op2/dispatch_slow.rs
+++ b/ops/op2/dispatch_slow.rs
@@ -100,17 +100,18 @@ pub(crate) fn generate_dispatch_slow(
 
   Ok(quote! {
     extern "C" fn #slow_function(#info: *const #deno_core::v8::FunctionCallbackInfo) {
-    #with_scope
-    #with_retval
-    #with_args
-    #with_opctx
-    #with_opstate
-
-    #output
-  }})
+      #with_scope
+      #with_retval
+      #with_args
+      #with_opctx
+      #with_opstate
+
+      #output
+    }
+  })
 }
 
-fn with_scope(generator_state: &mut GeneratorState) -> TokenStream {
+pub(crate) fn with_scope(generator_state: &mut GeneratorState) -> TokenStream {
   let GeneratorState {
     deno_core,
     scope,
@@ -121,7 +122,7 @@ fn with_scope(generator_state: &mut GeneratorState) -> TokenStream {
   quote!(let mut #scope = unsafe { #deno_core::v8::CallbackScope::new(&*#info) };)
 }
 
-fn with_retval(generator_state: &mut GeneratorState) -> TokenStream {
+pub(crate) fn with_retval(generator_state: &mut GeneratorState) -> TokenStream {
   let GeneratorState {
     deno_core,
     retval,
@@ -132,7 +133,9 @@ fn with_retval(generator_state: &mut GeneratorState) -> TokenStream {
   quote!(let mut #retval = #deno_core::v8::ReturnValue::from_function_callback_info(unsafe { &*#info });)
 }
 
-fn with_fn_args(generator_state: &mut GeneratorState) -> TokenStream {
+pub(crate) fn with_fn_args(
+  generator_state: &mut GeneratorState,
+) -> TokenStream {
   let GeneratorState {
     deno_core,
     fn_args,
@@ -143,7 +146,7 @@ fn with_fn_args(generator_state: &mut GeneratorState) -> TokenStream {
   quote!(let #fn_args = #deno_core::v8::FunctionCallbackArguments::from_function_callback_info(unsafe { &*#info });)
 }
 
-fn with_opctx(generator_state: &mut GeneratorState) -> TokenStream {
+pub(crate) fn with_opctx(generator_state: &mut GeneratorState) -> TokenStream {
   let GeneratorState {
     deno_core,
     opctx,
@@ -159,7 +162,9 @@ fn with_opctx(generator_state: &mut GeneratorState) -> TokenStream {
   };)
 }
 
-fn with_opstate(generator_state: &mut GeneratorState) -> TokenStream {
+pub(crate) fn with_opstate(
+  generator_state: &mut GeneratorState,
+) -> TokenStream {
   let GeneratorState {
     opctx,
     opstate,
@@ -457,6 +462,7 @@ pub fn return_value(
       return_value_infallible(generator_state, ret_type)
     }
     RetVal::Result(ret_type) => return_value_result(generator_state, ret_type),
+    _ => todo!(),
   }
 }
 
@@ -580,7 +586,37 @@ pub fn return_value_infallible(
   Ok(res)
 }
 
-fn return_value_result(
+/// Puts a typed result into a [`v8::Value`].
+pub fn return_value_v8_value(
+  deno_core: &TokenStream,
+  scope: &Ident,
+  result: &Ident,
+  ret_type: &Arg,
+) -> Result<TokenStream, V8MappingError> {
+  let res = match ret_type {
+    Arg::Void => {
+      quote!(Ok(#deno_core::v8::null(#scope).into()))
+    }
+    Arg::Numeric(NumericArg::bool) => {
+      quote!(Ok(#deno_core::v8::Boolean::new(#result).into()))
+    }
+    Arg::Numeric(NumericArg::i8 | NumericArg::i16 | NumericArg::i32) => {
+      quote!(Ok(#deno_core::v8::Integer::new(#scope, #result).into()))
+    }
+    Arg::Numeric(NumericArg::u8 | NumericArg::u16 | NumericArg::u32) => {
+      quote!(Ok(#deno_core::v8::Integer::new_from_unsigned(#scope, #result).into()))
+    }
+    _ => {
+      return Err(V8MappingError::NoMapping(
+        "a v8 return value",
+        ret_type.clone(),
+      ))
+    }
+  };
+  Ok(res)
+}
+
+pub fn return_value_result(
   generator_state: &mut GeneratorState,
   ret_type: &Arg,
 ) -> Result<TokenStream, V8MappingError> {
@@ -635,6 +671,7 @@ fn throw_exception(
     #maybe_scope
     #maybe_args
     #maybe_opctx
+    let err = err.into();
     let opstate = ::std::cell::RefCell::borrow(&*#opctx.state);
     let exception = #deno_core::error::to_v8_error(
       &mut #scope,
diff --git a/ops/op2/generator_state.rs b/ops/op2/generator_state.rs
index 09cdb46..ec5d6a5 100644
--- a/ops/op2/generator_state.rs
+++ b/ops/op2/generator_state.rs
@@ -31,6 +31,8 @@ pub struct GeneratorState {
   pub slow_function: Ident,
   /// The "fast" function (ie: a fastcall)
   pub fast_function: Ident,
+  /// The async function promise ID argument
+  pub promise_id: Ident,
 
   pub needs_args: bool,
   pub needs_retval: bool,
diff --git a/ops/op2/mod.rs b/ops/op2/mod.rs
index b9ba9e4..23f856a 100644
--- a/ops/op2/mod.rs
+++ b/ops/op2/mod.rs
@@ -3,6 +3,7 @@ use deno_proc_macro_rules::rules;
 use proc_macro2::Ident;
 use proc_macro2::Span;
 use proc_macro2::TokenStream;
+use proc_macro2::TokenTree;
 use quote::format_ident;
 use quote::quote;
 use quote::ToTokens;
@@ -17,6 +18,7 @@ use syn2::LifetimeParam;
 use syn2::Path;
 use thiserror::Error;
 
+use self::dispatch_async::generate_dispatch_async;
 use self::dispatch_fast::generate_dispatch_fast;
 use self::dispatch_slow::generate_dispatch_slow;
 use self::generator_state::GeneratorState;
@@ -25,11 +27,13 @@ use self::signature::parse_signature;
 use self::signature::Arg;
 use self::signature::SignatureError;
 
+pub mod dispatch_async;
 pub mod dispatch_fast;
 pub mod dispatch_shared;
 pub mod dispatch_slow;
 pub mod generator_state;
 pub mod signature;
+pub mod signature_retval;
 
 #[derive(Debug, Error)]
 pub enum Op2Error {
@@ -47,6 +51,12 @@ pub enum Op2Error {
   ShouldBeFast,
   #[error("This op is not fast-compatible and should not be marked as (fast)")]
   ShouldNotBeFast,
+  #[error("This op is async and should be marked as (async)")]
+  ShouldBeAsync,
+  #[error("This op is not async and should not be marked as (async)")]
+  ShouldNotBeAsync,
+  #[error("The flags for this attribute were not sorted alphabetically. They should be listed as '({0})'.")]
+  ImproperlySortedAttribute(String),
 }
 
 #[derive(Debug, Error)]
@@ -59,20 +69,39 @@ pub enum V8MappingError {
 pub(crate) struct MacroConfig {
   pub core: bool,
   pub fast: bool,
+  pub r#async: bool,
 }
 
 impl MacroConfig {
-  pub fn from_flags(flags: Vec<Ident>) -> Result<Self, Op2Error> {
+  pub fn from_token_trees(flags: Vec<TokenTree>) -> Result<Self, Op2Error> {
+    Self::from_flags(flags.into_iter().map(|tt| tt.to_string()))
+  }
+
+  pub fn from_flags(
+    flags: impl IntoIterator<Item = String>,
+  ) -> Result<Self, Op2Error> {
     let mut config: MacroConfig = Self::default();
+    let flags = flags.into_iter().collect::<Vec<_>>();
+
+    // Ensure that the flags are sorted in alphabetical order for consistency and searchability
+    let mut flags_sorted = flags.clone();
+    flags_sorted.sort();
+    if flags != flags_sorted {
+      return Err(Op2Error::ImproperlySortedAttribute(flags_sorted.join(", ")));
+    }
+
     for flag in flags {
       if flag == "core" {
         config.core = true;
       } else if flag == "fast" {
         config.fast = true;
+      } else if flag == "async" {
+        config.r#async = true;
       } else {
-        return Err(Op2Error::InvalidAttribute(flag.to_string()));
+        return Err(Op2Error::InvalidAttribute(flag));
       }
     }
+
     Ok(config)
   }
 
@@ -83,8 +112,8 @@ impl MacroConfig {
         () => {
           Ok(MacroConfig::default())
         }
-        ($($flags:ident),+) => {
-          Self::from_flags(flags)
+        ($($flags:tt),+) => {
+          Self::from_token_trees(flags)
         }
       })
     })
@@ -155,6 +184,7 @@ fn generate_op2(
   let info = Ident::new("info", Span::call_site());
   let opctx = Ident::new("opctx", Span::call_site());
   let opstate = Ident::new("opstate", Span::call_site());
+  let promise_id = Ident::new("promise_id", Span::call_site());
   let slow_function = Ident::new("v8_fn_ptr", Span::call_site());
   let fast_function = Ident::new("v8_fn_ptr_fast", Span::call_site());
   let fast_api_callback_options =
@@ -183,6 +213,7 @@ fn generate_op2(
     needs_args,
     slow_function,
     fast_function,
+    promise_id,
     needs_retval: false,
     needs_scope: false,
     needs_opctx: false,
@@ -193,8 +224,19 @@ fn generate_op2(
 
   let name = func.sig.ident;
 
-  let slow_fn =
-    generate_dispatch_slow(&config, &mut generator_state, &signature)?;
+  let slow_fn = if signature.ret_val.is_async() {
+    generate_dispatch_async(&mut generator_state, &signature)?
+  } else {
+    generate_dispatch_slow(&config, &mut generator_state, &signature)?
+  };
+  let is_async = signature.ret_val.is_async();
+
+  match (is_async, config.r#async) {
+    (true, false) => return Err(Op2Error::ShouldBeAsync),
+    (false, true) => return Err(Op2Error::ShouldNotBeAsync),
+    _ => {}
+  }
+
   let (fast_definition, fast_fn) =
     match generate_dispatch_fast(&mut generator_state, &signature)? {
       Some((fast_definition, fast_fn)) => {
@@ -217,7 +259,7 @@ fn generate_op2(
     ..
   } = &generator_state;
 
-  let arg_count: usize = generator_state.args.len();
+  let arg_count: usize = generator_state.args.len() + is_async as usize;
   let vis = func.vis;
   let generic = signature
     .generic_bounds
@@ -242,7 +284,7 @@ fn generate_op2(
       const NAME: &'static str = stringify!(#name);
       const DECL: #deno_core::_ops::OpDecl = #deno_core::_ops::OpDecl::new_internal(
         /*name*/ stringify!(#name),
-        /*is_async*/ false,
+        /*is_async*/ #is_async,
         /*is_unstable*/ false,
         /*is_v8*/ false,
         /*arg_count*/ #arg_count as u8,
@@ -280,12 +322,30 @@ mod tests {
   use syn2::File;
   use syn2::Item;
 
-  #[testing_macros::fixture("op2/test_cases/**/*.rs")]
-  fn test_signature_parser(input: PathBuf) {
+  #[testing_macros::fixture("op2/test_cases/sync/*.rs")]
+  fn test_proc_macro_sync(input: PathBuf) {
+    test_proc_macro_output(input)
+  }
+
+  #[testing_macros::fixture("op2/test_cases/async/*.rs")]
+  fn test_proc_macro_async(input: PathBuf) {
+    test_proc_macro_output(input)
+  }
+
+  fn test_proc_macro_output(input: PathBuf) {
     let update_expected = std::env::var("UPDATE_EXPECTED").is_ok();
 
     let source =
       std::fs::read_to_string(&input).expect("Failed to read test file");
+
+    const PRELUDE: &str = r"// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+#![deny(warnings)]
+deno_ops_compile_test_runner::prelude!();";
+
+    if !source.starts_with(PRELUDE) {
+      panic!("Source does not start with expected prelude:]n{PRELUDE}");
+    }
+
     let file = parse_str::<File>(&source).expect("Failed to parse Rust file");
     let mut expected_out = vec![];
     for item in file.items {
@@ -300,8 +360,8 @@ mod tests {
             (#[op2]) => {
               Some(MacroConfig::default())
             }
-            (#[op2( $($x:ident),* )]) => {
-              Some(MacroConfig::from_flags(x).expect("Failed to parse attribute"))
+            (#[op2( $($x:tt),* )]) => {
+              Some(MacroConfig::from_token_trees(x).expect("Failed to parse attribute"))
             }
             (#[$_attr:meta]) => {
               None
@@ -385,8 +445,14 @@ mod tests {
           let sig = parse_signature(vec![], function.sig.clone())
             .expect("Failed to parse signature");
           println!("Parsed signature: {sig:?}");
-          generate_op2(MacroConfig { core: false, fast }, function)
-            .expect("Failed to generate op");
+          generate_op2(
+            MacroConfig {
+              fast,
+              ..Default::default()
+            },
+            function,
+          )
+          .expect("Failed to generate op");
         }
         actual += &format!("<tr>\n<td>\n\n```rust\n{}\n```\n\n</td><td>\n{}\n</td><td>\n{}\n</td><td>\n{}\n</td></tr>\n", type_param, if fast { "✅" } else { "" }, v8, notes);
       }
diff --git a/ops/op2/signature.rs b/ops/op2/signature.rs
index b766763..476edb5 100644
--- a/ops/op2/signature.rs
+++ b/ops/op2/signature.rs
@@ -18,12 +18,13 @@ use syn2::GenericParam;
 use syn2::Generics;
 use syn2::Pat;
 use syn2::Path;
-use syn2::ReturnType;
 use syn2::Signature;
 use syn2::Type;
 use syn2::TypePath;
 use thiserror::Error;
 
+use super::signature_retval::parse_return;
+
 #[allow(non_camel_case_types)]
 #[derive(
   Copy, Clone, Debug, Eq, PartialEq, IntoStaticStr, EnumString, EnumIter,
@@ -283,7 +284,7 @@ impl Arg {
   }
 }
 
-enum ParsedType {
+pub enum ParsedType {
   TSpecial(Special),
   TBuffer(Buffer),
   TV8(V8Arg),
@@ -292,7 +293,7 @@ enum ParsedType {
   TNumeric(NumericArg),
 }
 
-enum ParsedTypeContainer {
+pub enum ParsedTypeContainer {
   CBare(ParsedType),
   COption(ParsedType),
   CRcRefCell(ParsedType),
@@ -303,8 +304,31 @@ enum ParsedTypeContainer {
 
 #[derive(Clone, Debug, Eq, PartialEq)]
 pub enum RetVal {
+  /// An op that can never fail.
   Infallible(Arg),
+  /// An op returning Result<Something, ...>
   Result(Arg),
+  /// An op returning a future, either `async fn() -> Something` or `fn() -> impl Future<Output = Something>`.
+  Future(Arg),
+  /// An op returning a future with a result, either `async fn() -> Result<Something, ...>`
+  /// or `fn() -> impl Future<Output = Result<Something, ...>>`.
+  FutureResult(Arg),
+  /// An op returning a result future: `fn() -> Result<impl Future<Output = Something>>`,
+  /// allowing it to exit before starting any async work.
+  ResultFuture(Arg),
+  /// An op returning a result future of a result: `fn() -> Result<impl Future<Output = Result<Something, ...>>>`,
+  /// allowing it to exit before starting any async work.
+  ResultFutureResult(Arg),
+}
+
+impl RetVal {
+  pub fn is_async(&self) -> bool {
+    use RetVal::*;
+    matches!(
+      self,
+      Future(..) | FutureResult(..) | ResultFuture(..) | ResultFutureResult(..)
+    )
+  }
 }
 
 #[derive(Clone, Debug, Eq, PartialEq)]
@@ -322,7 +346,7 @@ pub struct ParsedSignature {
 }
 
 #[derive(Copy, Clone, Debug, Eq, PartialEq)]
-enum BufferMode {
+pub enum BufferMode {
   /// Unsafely shared buffers that may possibly change on the JavaScript side upon re-entry into
   /// V8. Rust code should not treat these as traditional buffers.
   Unsafe,
@@ -336,7 +360,7 @@ enum BufferMode {
 }
 
 #[derive(Copy, Clone, Debug, Eq, PartialEq)]
-enum AttributeModifier {
+pub enum AttributeModifier {
   /// #[serde], for serde_v8 types.
   Serde,
   /// #[smi], for non-integral ID types representing small integers (-2³¹ and 2³¹-1 on 64-bit platforms,
@@ -355,7 +379,7 @@ pub enum SignatureError {
   #[error("Invalid argument: '{0}'")]
   ArgError(String, #[source] ArgError),
   #[error("Invalid return type")]
-  RetError(#[from] ArgError),
+  RetError(#[from] RetError),
   #[error("Only one lifetime is permitted")]
   TooManyLifetimes,
   #[error("Generic '{0}' must have one and only bound (either <T> and 'where T: Trait', or <T: Trait>)")]
@@ -374,6 +398,14 @@ pub enum SignatureError {
   InvalidOpStateCombination,
 }
 
+#[derive(Error, Debug)]
+pub enum AttributeError {
+  #[error("Unknown or invalid attribute '{0}'")]
+  InvalidAttribute(String),
+  #[error("Too many attributes")]
+  TooManyAttributes,
+}
+
 #[derive(Error, Debug)]
 pub enum ArgError {
   #[error("Invalid self argument")]
@@ -386,8 +418,6 @@ pub enum ArgError {
     "Invalid argument type path (should this be #[smi] or #[serde]?): {0}"
   )]
   InvalidTypePath(String),
-  #[error("Too many attributes")]
-  TooManyAttributes,
   #[error("The type {0} cannot be a reference")]
   InvalidReference(String),
   #[error("The type {0} must be a reference")]
@@ -412,12 +442,20 @@ pub enum ArgError {
   MissingBufferAttribute,
   #[error("Invalid #[state] type '{0}'")]
   InvalidStateType(String),
-  #[error("Unknown or invalid attribute '{0}'")]
-  InvalidAttribute(String),
+  #[error("Argument attribute error")]
+  AttributeError(#[from] AttributeError),
+}
+
+#[derive(Error, Debug)]
+pub enum RetError {
+  #[error("Invalid return type")]
+  InvalidType(#[from] ArgError),
+  #[error("Return value attribute error")]
+  AttributeError(#[from] AttributeError),
 }
 
 #[derive(Copy, Clone, Default)]
-struct Attributes {
+pub(crate) struct Attributes {
   primary: Option<AttributeModifier>,
 }
 
@@ -429,7 +467,7 @@ impl Attributes {
   }
 }
 
-fn stringify_token(tokens: impl ToTokens) -> String {
+pub(crate) fn stringify_token(tokens: impl ToTokens) -> String {
   tokens
     .into_token_stream()
     .into_iter()
@@ -460,8 +498,11 @@ pub fn parse_signature(
       parse_arg(input).map_err(|err| SignatureError::ArgError(name, err))?,
     );
   }
-  let ret_val =
-    parse_return(parse_attributes(&attributes)?, &signature.output)?;
+  let ret_val = parse_return(
+    signature.asyncness.is_some(),
+    parse_attributes(&attributes).map_err(RetError::AttributeError)?,
+    &signature.output,
+  )?;
   let lifetime = parse_lifetime(&signature.generics)?;
   let generic_bounds = parse_generics(&signature.generics)?;
 
@@ -589,7 +630,9 @@ fn parse_generics(
   Ok(res)
 }
 
-fn parse_attributes(attributes: &[Attribute]) -> Result<Attributes, ArgError> {
+fn parse_attributes(
+  attributes: &[Attribute],
+) -> Result<Attributes, AttributeError> {
   let mut attrs = vec![];
   for attr in attributes {
     if let Some(attr) = parse_attribute(attr)? {
@@ -601,7 +644,7 @@ fn parse_attributes(attributes: &[Attribute]) -> Result<Attributes, ArgError> {
     return Ok(Attributes::default());
   }
   if attrs.len() > 1 {
-    return Err(ArgError::TooManyAttributes);
+    return Err(AttributeError::TooManyAttributes);
   }
   Ok(Attributes {
     primary: Some(*attrs.get(0).unwrap()),
@@ -617,7 +660,7 @@ pub fn is_attribute_special(attr: &Attribute) -> bool {
 /// otherwise unknown (ie: doc comments).
 fn parse_attribute(
   attr: &Attribute,
-) -> Result<Option<AttributeModifier>, ArgError> {
+) -> Result<Option<AttributeModifier>, AttributeError> {
   let tokens = attr.into_token_stream();
   let res = std::panic::catch_unwind(|| {
     use syn2 as syn;
@@ -633,47 +676,10 @@ fn parse_attribute(
       (#[allow ($_rule:path)]) => None,
       (#[doc = $_attr:literal]) => None,
     })
-  }).map_err(|_| ArgError::InvalidAttribute(stringify_token(attr)))?;
+  }).map_err(|_| AttributeError::InvalidAttribute(stringify_token(attr)))?;
   Ok(res)
 }
 
-fn parse_return(
-  attrs: Attributes,
-  rt: &ReturnType,
-) -> Result<RetVal, ArgError> {
-  match rt {
-    ReturnType::Default => Ok(RetVal::Infallible(Arg::Void)),
-    ReturnType::Type(_, ty) => {
-      let s = stringify_token(ty);
-      let tokens = ty.into_token_stream();
-      use syn2 as syn;
-
-      std::panic::catch_unwind(|| {
-        rules!(tokens => {
-          // x::y::Result<Value>, like io::Result and other specialty result types
-          ($($_package:ident ::)* Result < $ty:ty >) => {
-            Ok(RetVal::Result(parse_type(attrs, &ty)?))
-          }
-          // x::y::Result<Value, Error>
-          ($($_package:ident ::)* Result < $ty:ty, $_error:ty >) => {
-            Ok(RetVal::Result(parse_type(attrs, &ty)?))
-          }
-          ($ty:ty) => {
-            Ok(RetVal::Infallible(parse_type(attrs, &ty)?))
-          }
-        })
-      })
-      .map_err(|e| {
-        ArgError::InternalError(format!(
-          "parse_return({}) {}",
-          s,
-          e.downcast::<&str>().unwrap_or_default()
-        ))
-      })?
-    }
-  }
-}
-
 fn parse_numeric_type(tp: &Path) -> Result<NumericArg, ArgError> {
   if tp.segments.len() == 1 {
     let segment = tp.segments.first().unwrap().ident.to_string();
@@ -847,7 +853,10 @@ fn parse_type_state(ty: &Type) -> Result<Arg, ArgError> {
   Ok(s)
 }
 
-fn parse_type(attrs: Attributes, ty: &Type) -> Result<Arg, ArgError> {
+pub(crate) fn parse_type(
+  attrs: Attributes,
+  ty: &Type,
+) -> Result<Arg, ArgError> {
   use ParsedType::*;
   use ParsedTypeContainer::*;
 
@@ -1026,7 +1035,9 @@ mod tests {
       // Function attributes
       $(# [ $fn_attr:meta ])?
       // fn name < 'scope, GENERIC1, GENERIC2, ... >
-      fn $name:ident $( < $scope:lifetime $( , $generic:ident)* >)?
+      $(async fn $name1:ident)?
+      $(fn $name2:ident)?
+      $( < $scope:lifetime $( , $generic:ident)* >)?
       (
         // Argument attribute, argument
         $( $(# [ $attr:meta ])? $ident:ident : $ty:ty ),*
@@ -1039,9 +1050,9 @@ mod tests {
       // Expected return value
       $( < $( $lifetime_res:lifetime )? $(, $generic_res:ident : $bounds_res:path )* >)? ( $( $arg_res:expr ),* ) -> $ret_res:expr ) => {
       #[test]
-      fn $name() {
+      fn $($name1)? $($name2)? () {
         test(
-          stringify!($( #[$fn_attr] )? fn op $( < $scope $( , $generic)* >)? ( $( $( #[$attr] )? $ident : $ty ),* ) $(-> $( #[$ret_attr] )? $ret)? $( where $($trait : $bounds),* )? {}),
+          stringify!($( #[$fn_attr] )? $(async fn $name1)? $(fn $name2)? $( < $scope $( , $generic)* >)? ( $( $( #[$attr] )? $ident : $ty ),* ) $(-> $( #[$ret_attr] )? $ret)? $( where $($trait : $bounds),* )? {}),
           stringify!($( < $( $lifetime_res )? $(, $generic_res : $bounds_res)* > )?),
           stringify!($($arg_res),*),
           stringify!($ret_res)
@@ -1187,7 +1198,22 @@ mod tests {
     #[buffer(copy)] fn op_buffers(#[buffer(copy)] a: Vec<u8>, #[buffer(copy)] b: Box<[u8]>, #[buffer(copy)] c: bytes::Bytes, #[buffer] d: V8Slice, #[buffer] e: JSBuffer) -> Vec<u8>;
     (Buffer(Vec(u8)), Buffer(BoxSlice(u8)), Buffer(Bytes), Buffer(V8Slice), Buffer(JSBuffer)) -> Infallible(Buffer(Vec(u8)))
   );
-
+  test!(
+    async fn op_async_void();
+    () -> Future(Void)
+  );
+  test!(
+    async fn op_async_result_void() -> Result<()>;
+    () -> FutureResult(Void)
+  );
+  test!(
+    fn op_async_impl_void() -> impl Future<Output = ()>;
+    () -> Future(Void)
+  );
+  test!(
+    fn op_async_result_impl_void() -> Result<impl Future<Output = ()>, Error>;
+    () -> ResultFuture(Void)
+  );
   // Args
 
   expect_fail!(
@@ -1217,13 +1243,13 @@ mod tests {
   );
   expect_fail!(
     op_with_bad_attr,
-    RetError(InvalidAttribute("#[badattr]")),
+    RetError(AttributeError(InvalidAttribute("#[badattr]"))),
     #[badattr]
     fn f() {}
   );
   expect_fail!(
     op_with_bad_attr2,
-    ArgError("a", InvalidAttribute("#[badattr]")),
+    ArgError("a", AttributeError(InvalidAttribute("#[badattr]"))),
     fn f(#[badattr] a: u32) {}
   );
 
@@ -1275,11 +1301,4 @@ mod tests {
     ArgError("v", InvalidSerdeType("serde_v8::Value", "use v8::Value")),
     fn f(#[serde] v: serde_v8::Value) {}
   );
-
-  #[test]
-  fn test_parse_result() {
-    let rt = parse_str::<ReturnType>("-> Result < (), Error >")
-      .expect("Failed to parse");
-    println!("{:?}", parse_return(Attributes::default(), &rt));
-  }
 }
diff --git a/ops/op2/signature_retval.rs b/ops/op2/signature_retval.rs
new file mode 100644
index 0000000..df2e00a
--- /dev/null
+++ b/ops/op2/signature_retval.rs
@@ -0,0 +1,148 @@
+// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+use crate::op2::signature::*;
+use deno_proc_macro_rules::rules;
+
+use quote::ToTokens;
+
+use syn2::ReturnType;
+
+use syn2::Type;
+use syn2::TypeParamBound;
+
+/// One level of type unwrapping for a return value. We cannot rely on `proc-macro-rules` to correctly
+/// unwrap `impl Future<...>`, so we do it by hand.
+enum UnwrappedReturn {
+  Type(Type),
+  Result(Type),
+  Future(Type),
+}
+
+fn unwrap_return(ty: &Type) -> Result<UnwrappedReturn, RetError> {
+  use syn2 as syn;
+  match ty {
+    Type::ImplTrait(imp) => {
+      if imp.bounds.len() != 1 {
+        return Err(RetError::InvalidType(ArgError::InvalidType(
+          stringify_token(ty),
+        )));
+      }
+      if let Some(TypeParamBound::Trait(t)) = imp.bounds.first() {
+        rules!(t.into_token_stream() => {
+          ($($_package:ident ::)* Future < Output = $ty:ty >) => Ok(UnwrappedReturn::Future(ty)),
+          ($ty:ty) => Err(RetError::InvalidType(ArgError::InvalidType(stringify_token(ty)))),
+        })
+      } else {
+        Err(RetError::InvalidType(ArgError::InvalidType(
+          stringify_token(ty),
+        )))
+      }
+    }
+    Type::Path(ty) => {
+      rules!(ty.to_token_stream() => {
+        // x::y::Result<Value>, like io::Result and other specialty result types
+        ($($_package:ident ::)* Result < $ty:ty >) => {
+          Ok(UnwrappedReturn::Result(ty))
+        }
+        // x::y::Result<Value, Error>
+        ($($_package:ident ::)* Result < $ty:ty, $_error:ty >) => {
+          Ok(UnwrappedReturn::Result(ty))
+        }
+        // Everything else
+        ($ty:ty) => {
+          Ok(UnwrappedReturn::Type(ty))
+        }
+      })
+    }
+    Type::Tuple(_) => Ok(UnwrappedReturn::Type(ty.clone())),
+    _ => Err(RetError::InvalidType(ArgError::InvalidType(
+      stringify_token(ty),
+    ))),
+  }
+}
+
+pub(crate) fn parse_return(
+  is_async: bool,
+  attrs: Attributes,
+  rt: &ReturnType,
+) -> Result<RetVal, RetError> {
+  use UnwrappedReturn::*;
+
+  let res = match rt {
+    ReturnType::Default => RetVal::Infallible(Arg::Void),
+    ReturnType::Type(_, rt) => match unwrap_return(rt)? {
+      Type(ty) => RetVal::Infallible(parse_type(attrs, &ty)?),
+      Result(ty) => match unwrap_return(&ty)? {
+        Type(ty) => RetVal::Result(parse_type(attrs, &ty)?),
+        Future(ty) => match unwrap_return(&ty)? {
+          Type(ty) => RetVal::ResultFuture(parse_type(attrs, &ty)?),
+          Result(ty) => RetVal::ResultFutureResult(parse_type(attrs, &ty)?),
+          _ => {
+            return Err(RetError::InvalidType(ArgError::InvalidType(
+              stringify_token(rt),
+            )))
+          }
+        },
+        _ => {
+          return Err(RetError::InvalidType(ArgError::InvalidType(
+            stringify_token(rt),
+          )))
+        }
+      },
+      Future(ty) => match unwrap_return(&ty)? {
+        Type(ty) => RetVal::Future(parse_type(attrs, &ty)?),
+        Result(ty) => RetVal::FutureResult(parse_type(attrs, &ty)?),
+        _ => {
+          return Err(RetError::InvalidType(ArgError::InvalidType(
+            stringify_token(rt),
+          )))
+        }
+      },
+    },
+  };
+
+  // If the signature was async, wrap this return value in one level of future.
+  if is_async {
+    let res = match res {
+      RetVal::Infallible(t) => RetVal::Future(t),
+      RetVal::Result(t) => RetVal::FutureResult(t),
+      _ => {
+        return Err(RetError::InvalidType(ArgError::InvalidType(
+          stringify_token(rt),
+        )))
+      }
+    };
+    Ok(res)
+  } else {
+    Ok(res)
+  }
+}
+
+#[cfg(test)]
+mod tests {
+  use super::*;
+  use syn2::parse_str;
+
+  #[test]
+  fn test_parse_result() {
+    use Arg::*;
+    use RetVal::*;
+
+    for (expected, input) in [
+      (Infallible(Void), "()"),
+      (Result(Void), "Result<()>"),
+      (Future(Void), "impl Future<Output = ()>"),
+      (FutureResult(Void), "impl Future<Output = Result<()>>"),
+      (ResultFuture(Void), "Result<impl Future<Output = ()>>"),
+      (
+        ResultFutureResult(Void),
+        "Result<impl Future<Output = Result<()>>>",
+      ),
+    ] {
+      let rt = parse_str::<ReturnType>(&format!("-> {input}"))
+        .expect("Failed to parse");
+      let actual = parse_return(false, Attributes::default(), &rt)
+        .expect("Failed to parse return");
+      assert_eq!(expected, actual);
+    }
+  }
+}


Test patch:
diff --git a/ops/compile_test_runner/Cargo.toml b/ops/compile_test_runner/Cargo.toml
new file mode 100644
index 0000000..02e6033
--- /dev/null
+++ b/ops/compile_test_runner/Cargo.toml
@@ -0,0 +1,25 @@
+
+[package]
+name = "deno_ops_compile_test_runner"
+version = "0.1.0"
+authors.workspace = true
+edition.workspace = true
+license.workspace = true
+readme = "README.md"
+repository.workspace = true
+description = "Compile-test runner for deno_ops"
+publish = false
+
+[lib]
+
+[dev-dependencies]
+trybuild = "1.0"
+pretty_assertions.workspace = true
+prettyplease = "0.2.9"
+testing_macros = "0.2.11"
+deno_ops.workspace = true
+deno_core.workspace = true
+syn2.workspace = true
+bytes.workspace = true
+serde.workspace = true
+rustversion = "1.0"
diff --git a/ops/compile_test_runner/src/lib.rs b/ops/compile_test_runner/src/lib.rs
new file mode 100644
index 0000000..22255fe
--- /dev/null
+++ b/ops/compile_test_runner/src/lib.rs
@@ -0,0 +1,39 @@
+#[macro_export]
+macro_rules! prelude {
+  () => {
+    use deno_ops::op2;
+
+    pub fn main() {}
+  };
+}
+
+#[cfg(test)]
+mod tests {
+  use std::path::PathBuf;
+
+  // TODO(mmastrac): It's faster to do things with testing_macros::fixture?
+  #[testing_macros::fixture("../op2/test_cases/compiler_pass/*.rs")]
+  fn compile_test(input: PathBuf) {
+    let t = trybuild::TestCases::new();
+    t.pass(input);
+  }
+
+  #[rustversion::nightly]
+  #[test]
+  fn compile_test_all() {
+    // Run all the tests on a nightly build (which should take advantage of cargo's --keep-going to
+    // run in parallel: https://github.com/dtolnay/trybuild/pull/168)
+    let t = trybuild::TestCases::new();
+    t.pass("../op2/test_cases/**/*.rs");
+  }
+
+  #[rustversion::not(nightly)]
+  #[test]
+  fn compile_test_all() {
+    // Run all the tests if we're in the CI
+    if let Ok(true) = std::env::var("CI").map(|s| s == "true") {
+      let t = trybuild::TestCases::new();
+      t.pass("../op2/test_cases/**/*.rs");
+    }
+  }
+}
diff --git a/ops/op2/test_cases/async/async_arg_return.out b/ops/op2/test_cases/async/async_arg_return.out
new file mode 100644
index 0000000..6689e52
--- /dev/null
+++ b/ops/op2/test_cases/async/async_arg_return.out
@@ -0,0 +1,55 @@
+#[allow(non_camel_case_types)]
+pub struct op_async {
+    _unconstructable: ::std::marker::PhantomData<()>,
+}
+impl deno_core::_ops::Op for op_async {
+    const NAME: &'static str = stringify!(op_async);
+    const DECL: deno_core::_ops::OpDecl = deno_core::_ops::OpDecl::new_internal(
+        stringify!(op_async),
+        true,
+        false,
+        false,
+        2usize as u8,
+        Self::v8_fn_ptr as _,
+        None,
+    );
+}
+impl op_async {
+    pub const fn name() -> &'static str {
+        stringify!(op_async)
+    }
+    #[deprecated(note = "Use the const op::DECL instead")]
+    pub const fn decl() -> deno_core::_ops::OpDecl {
+        <Self as deno_core::_ops::Op>::DECL
+    }
+    extern "C" fn v8_fn_ptr(info: *const deno_core::v8::FunctionCallbackInfo) {
+        let mut scope = unsafe { deno_core::v8::CallbackScope::new(&*info) };
+        let mut rv = deno_core::v8::ReturnValue::from_function_callback_info(unsafe {
+            &*info
+        });
+        let args = deno_core::v8::FunctionCallbackArguments::from_function_callback_info(unsafe {
+            &*info
+        });
+        let opctx = unsafe {
+            &*(deno_core::v8::Local::<deno_core::v8::External>::cast(args.data()).value()
+                as *const deno_core::_ops::OpCtx)
+        };
+        let arg0 = args.get(1usize as i32);
+        let arg0 = deno_core::_ops::to_i32(&arg0) as _;
+        let result = Self::call(arg0);
+        let promise_id = deno_core::_ops::to_i32(&args.get(0));
+        if let Some(result)
+            = deno_core::_ops::map_async_op_infallible(
+                opctx,
+                promise_id,
+                result,
+                |scope, result| { Ok(deno_core::v8::Integer::new(scope, result).into()) },
+            ) {
+            rv.set_int32(result as i32);
+        }
+    }
+    #[inline(always)]
+    pub async fn call(x: i32) -> i32 {
+        x
+    }
+}
diff --git a/ops/op2/test_cases/async/async_arg_return.rs b/ops/op2/test_cases/async/async_arg_return.rs
new file mode 100644
index 0000000..b7e7883
--- /dev/null
+++ b/ops/op2/test_cases/async/async_arg_return.rs
@@ -0,0 +1,8 @@
+// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+#![deny(warnings)]
+deno_ops_compile_test_runner::prelude!();
+
+#[op2(async)]
+pub async fn op_async(x: i32) -> i32 {
+    x
+}
diff --git a/ops/op2/test_cases/async/async_arg_return_result.out b/ops/op2/test_cases/async/async_arg_return_result.out
new file mode 100644
index 0000000..da83f2c
--- /dev/null
+++ b/ops/op2/test_cases/async/async_arg_return_result.out
@@ -0,0 +1,70 @@
+#[allow(non_camel_case_types)]
+pub struct op_async {
+    _unconstructable: ::std::marker::PhantomData<()>,
+}
+impl deno_core::_ops::Op for op_async {
+    const NAME: &'static str = stringify!(op_async);
+    const DECL: deno_core::_ops::OpDecl = deno_core::_ops::OpDecl::new_internal(
+        stringify!(op_async),
+        true,
+        false,
+        false,
+        2usize as u8,
+        Self::v8_fn_ptr as _,
+        None,
+    );
+}
+impl op_async {
+    pub const fn name() -> &'static str {
+        stringify!(op_async)
+    }
+    #[deprecated(note = "Use the const op::DECL instead")]
+    pub const fn decl() -> deno_core::_ops::OpDecl {
+        <Self as deno_core::_ops::Op>::DECL
+    }
+    extern "C" fn v8_fn_ptr(info: *const deno_core::v8::FunctionCallbackInfo) {
+        let mut scope = unsafe { deno_core::v8::CallbackScope::new(&*info) };
+        let mut rv = deno_core::v8::ReturnValue::from_function_callback_info(unsafe {
+            &*info
+        });
+        let args = deno_core::v8::FunctionCallbackArguments::from_function_callback_info(unsafe {
+            &*info
+        });
+        let opctx = unsafe {
+            &*(deno_core::v8::Local::<deno_core::v8::External>::cast(args.data()).value()
+                as *const deno_core::_ops::OpCtx)
+        };
+        let arg0 = args.get(1usize as i32);
+        let arg0 = deno_core::_ops::to_i32(&arg0) as _;
+        let result = Self::call(arg0);
+        let promise_id = deno_core::_ops::to_i32(&args.get(0));
+        if let Some(result)
+            = deno_core::_ops::map_async_op_fallible(
+                opctx,
+                promise_id,
+                result,
+                |scope, result| { Ok(deno_core::v8::Integer::new(scope, result).into()) },
+            ) {
+            match result {
+                Ok(result) => {
+                    rv.set_int32(result as i32);
+                }
+                Err(err) => {
+                    let err = err.into();
+                    let opstate = ::std::cell::RefCell::borrow(&*opctx.state);
+                    let exception = deno_core::error::to_v8_error(
+                        &mut scope,
+                        opstate.get_error_class_fn,
+                        &err,
+                    );
+                    scope.throw_exception(exception);
+                    return;
+                }
+            };
+        }
+    }
+    #[inline(always)]
+    pub async fn call(x: i32) -> std::io::Result<i32> {
+        Ok(x)
+    }
+}
diff --git a/ops/op2/test_cases/async/async_arg_return_result.rs b/ops/op2/test_cases/async/async_arg_return_result.rs
new file mode 100644
index 0000000..c3c6b00
--- /dev/null
+++ b/ops/op2/test_cases/async/async_arg_return_result.rs
@@ -0,0 +1,8 @@
+// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+#![deny(warnings)]
+deno_ops_compile_test_runner::prelude!();
+
+#[op2(async)]
+pub async fn op_async(x: i32) -> std::io::Result<i32> {
+    Ok(x)
+}
diff --git a/ops/op2/test_cases/async/async_opstate.out b/ops/op2/test_cases/async/async_opstate.out
new file mode 100644
index 0000000..2f12da4
--- /dev/null
+++ b/ops/op2/test_cases/async/async_opstate.out
@@ -0,0 +1,70 @@
+#[allow(non_camel_case_types)]
+pub struct op_async_opstate {
+    _unconstructable: ::std::marker::PhantomData<()>,
+}
+impl deno_core::_ops::Op for op_async_opstate {
+    const NAME: &'static str = stringify!(op_async_opstate);
+    const DECL: deno_core::_ops::OpDecl = deno_core::_ops::OpDecl::new_internal(
+        stringify!(op_async_opstate),
+        true,
+        false,
+        false,
+        2usize as u8,
+        Self::v8_fn_ptr as _,
+        None,
+    );
+}
+impl op_async_opstate {
+    pub const fn name() -> &'static str {
+        stringify!(op_async_opstate)
+    }
+    #[deprecated(note = "Use the const op::DECL instead")]
+    pub const fn decl() -> deno_core::_ops::OpDecl {
+        <Self as deno_core::_ops::Op>::DECL
+    }
+    extern "C" fn v8_fn_ptr(info: *const deno_core::v8::FunctionCallbackInfo) {
+        let mut scope = unsafe { deno_core::v8::CallbackScope::new(&*info) };
+        let mut rv = deno_core::v8::ReturnValue::from_function_callback_info(unsafe {
+            &*info
+        });
+        let args = deno_core::v8::FunctionCallbackArguments::from_function_callback_info(unsafe {
+            &*info
+        });
+        let opctx = unsafe {
+            &*(deno_core::v8::Local::<deno_core::v8::External>::cast(args.data()).value()
+                as *const deno_core::_ops::OpCtx)
+        };
+        let opstate = &opctx.state;
+        let arg0 = opstate.clone();
+        let result = Self::call(arg0);
+        let promise_id = deno_core::_ops::to_i32(&args.get(0));
+        if let Some(result)
+            = deno_core::_ops::map_async_op_fallible(
+                opctx,
+                promise_id,
+                result,
+                |scope, result| { Ok(deno_core::v8::Integer::new(scope, result).into()) },
+            ) {
+            match result {
+                Ok(result) => {
+                    rv.set_int32(result as i32);
+                }
+                Err(err) => {
+                    let err = err.into();
+                    let opstate = ::std::cell::RefCell::borrow(&*opctx.state);
+                    let exception = deno_core::error::to_v8_error(
+                        &mut scope,
+                        opstate.get_error_class_fn,
+                        &err,
+                    );
+                    scope.throw_exception(exception);
+                    return;
+                }
+            };
+        }
+    }
+    #[inline(always)]
+    pub async fn call(state: Rc<RefCell<OpState>>) -> std::io::Result<i32> {
+        Ok(*state.borrow().borrow::<i32>())
+    }
+}
diff --git a/ops/op2/test_cases/async/async_opstate.rs b/ops/op2/test_cases/async/async_opstate.rs
new file mode 100644
index 0000000..000b2d1
--- /dev/null
+++ b/ops/op2/test_cases/async/async_opstate.rs
@@ -0,0 +1,12 @@
+// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+#![deny(warnings)]
+deno_ops_compile_test_runner::prelude!();
+
+use std::rc::Rc;
+use std::cell::RefCell;
+use deno_core::OpState;
+
+#[op2(async)]
+pub async fn op_async_opstate(state: Rc<RefCell<OpState>>) -> std::io::Result<i32> {
+    Ok(*state.borrow().borrow::<i32>())
+}
diff --git a/ops/op2/test_cases/async/async_result.out b/ops/op2/test_cases/async/async_result.out
new file mode 100644
index 0000000..ba32cb6
--- /dev/null
+++ b/ops/op2/test_cases/async/async_result.out
@@ -0,0 +1,68 @@
+#[allow(non_camel_case_types)]
+pub struct op_async {
+    _unconstructable: ::std::marker::PhantomData<()>,
+}
+impl deno_core::_ops::Op for op_async {
+    const NAME: &'static str = stringify!(op_async);
+    const DECL: deno_core::_ops::OpDecl = deno_core::_ops::OpDecl::new_internal(
+        stringify!(op_async),
+        true,
+        false,
+        false,
+        1usize as u8,
+        Self::v8_fn_ptr as _,
+        None,
+    );
+}
+impl op_async {
+    pub const fn name() -> &'static str {
+        stringify!(op_async)
+    }
+    #[deprecated(note = "Use the const op::DECL instead")]
+    pub const fn decl() -> deno_core::_ops::OpDecl {
+        <Self as deno_core::_ops::Op>::DECL
+    }
+    extern "C" fn v8_fn_ptr(info: *const deno_core::v8::FunctionCallbackInfo) {
+        let mut scope = unsafe { deno_core::v8::CallbackScope::new(&*info) };
+        let mut rv = deno_core::v8::ReturnValue::from_function_callback_info(unsafe {
+            &*info
+        });
+        let args = deno_core::v8::FunctionCallbackArguments::from_function_callback_info(unsafe {
+            &*info
+        });
+        let opctx = unsafe {
+            &*(deno_core::v8::Local::<deno_core::v8::External>::cast(args.data()).value()
+                as *const deno_core::_ops::OpCtx)
+        };
+        let result = Self::call();
+        let promise_id = deno_core::_ops::to_i32(&args.get(0));
+        if let Some(result)
+            = deno_core::_ops::map_async_op_fallible(
+                opctx,
+                promise_id,
+                result,
+                |scope, result| { Ok(deno_core::v8::Integer::new(scope, result).into()) },
+            ) {
+            match result {
+                Ok(result) => {
+                    rv.set_int32(result as i32);
+                }
+                Err(err) => {
+                    let err = err.into();
+                    let opstate = ::std::cell::RefCell::borrow(&*opctx.state);
+                    let exception = deno_core::error::to_v8_error(
+                        &mut scope,
+                        opstate.get_error_class_fn,
+                        &err,
+                    );
+                    scope.throw_exception(exception);
+                    return;
+                }
+            };
+        }
+    }
+    #[inline(always)]
+    pub async fn call() -> std::io::Result<i32> {
+        Ok(0)
+    }
+}
diff --git a/ops/op2/test_cases/async/async_result.rs b/ops/op2/test_cases/async/async_result.rs
new file mode 100644
index 0000000..7fb1a32
--- /dev/null
+++ b/ops/op2/test_cases/async/async_result.rs
@@ -0,0 +1,8 @@
+// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+#![deny(warnings)]
+deno_ops_compile_test_runner::prelude!();
+
+#[op2(async)]
+pub async fn op_async() -> std::io::Result<i32> {
+    Ok(0)
+}
diff --git a/ops/op2/test_cases/async/async_void.out b/ops/op2/test_cases/async/async_void.out
new file mode 100644
index 0000000..75924af
--- /dev/null
+++ b/ops/op2/test_cases/async/async_void.out
@@ -0,0 +1,49 @@
+#[allow(non_camel_case_types)]
+pub struct op_async {
+    _unconstructable: ::std::marker::PhantomData<()>,
+}
+impl deno_core::_ops::Op for op_async {
+    const NAME: &'static str = stringify!(op_async);
+    const DECL: deno_core::_ops::OpDecl = deno_core::_ops::OpDecl::new_internal(
+        stringify!(op_async),
+        true,
+        false,
+        false,
+        1usize as u8,
+        Self::v8_fn_ptr as _,
+        None,
+    );
+}
+impl op_async {
+    pub const fn name() -> &'static str {
+        stringify!(op_async)
+    }
+    #[deprecated(note = "Use the const op::DECL instead")]
+    pub const fn decl() -> deno_core::_ops::OpDecl {
+        <Self as deno_core::_ops::Op>::DECL
+    }
+    extern "C" fn v8_fn_ptr(info: *const deno_core::v8::FunctionCallbackInfo) {
+        let mut scope = unsafe { deno_core::v8::CallbackScope::new(&*info) };
+        let mut rv = deno_core::v8::ReturnValue::from_function_callback_info(unsafe {
+            &*info
+        });
+        let args = deno_core::v8::FunctionCallbackArguments::from_function_callback_info(unsafe {
+            &*info
+        });
+        let opctx = unsafe {
+            &*(deno_core::v8::Local::<deno_core::v8::External>::cast(args.data()).value()
+                as *const deno_core::_ops::OpCtx)
+        };
+        let result = Self::call();
+        let promise_id = deno_core::_ops::to_i32(&args.get(0));
+        if let Some(result)
+            = deno_core::_ops::map_async_op_infallible(
+                opctx,
+                promise_id,
+                result,
+                |scope, result| { Ok(deno_core::v8::null(scope).into()) },
+            ) {}
+    }
+    #[inline(always)]
+    pub async fn call() {}
+}
diff --git a/ops/op2/test_cases/async/async_void.rs b/ops/op2/test_cases/async/async_void.rs
new file mode 100644
index 0000000..02a493c
--- /dev/null
+++ b/ops/op2/test_cases/async/async_void.rs
@@ -0,0 +1,6 @@
+// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+#![deny(warnings)]
+deno_ops_compile_test_runner::prelude!();
+
+#[op2(async)]
+pub async fn op_async() {}
diff --git a/ops/op2/test_cases/compiler_pass/async.rs b/ops/op2/test_cases/compiler_pass/async.rs
new file mode 100644
index 0000000..fcacd6c
--- /dev/null
+++ b/ops/op2/test_cases/compiler_pass/async.rs
@@ -0,0 +1,27 @@
+// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+#![deny(warnings)]
+deno_ops_compile_test_runner::prelude!();
+
+use std::rc::Rc;
+use std::cell::RefCell;
+use deno_core::OpState;
+
+// Collect a few examples that we'll smoke test when not running on the CI.
+
+#[op2(async)]
+pub async fn op_async1() {}
+
+#[op2(async)]
+pub async fn op_async2(x: i32) -> i32 {
+    x
+}
+
+#[op2(async)]
+pub async fn op_async3(x: i32) -> std::io::Result<i32> {
+    Ok(x)
+}
+
+#[op2(async)]
+pub async fn op_async_opstate(state: Rc<RefCell<OpState>>) -> std::io::Result<i32> {
+    Ok(*state.borrow().borrow::<i32>())
+}
diff --git a/ops/op2/test_cases/compiler_pass/sync.rs b/ops/op2/test_cases/compiler_pass/sync.rs
new file mode 100644
index 0000000..0a4ead3
--- /dev/null
+++ b/ops/op2/test_cases/compiler_pass/sync.rs
@@ -0,0 +1,19 @@
+// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+#![deny(warnings)]
+deno_ops_compile_test_runner::prelude!();
+
+// Collect a few examples that we'll smoke test when not running on the CI.
+
+#[op2(fast)]
+pub fn op_fast(x: u32) -> u32 {
+    x
+}
+
+#[op2(fast)]
+fn op_buffers(#[buffer] _a: &[u8], #[buffer(copy)] _b: Vec<u8>) {
+}
+
+struct Something {}
+
+#[op2(fast)]
+fn op_state_rc(#[state] _arg: &Something, #[state] _arg_opt: Option<&Something>) {}
diff --git a/ops/op2/test_cases/sync/add.rs b/ops/op2/test_cases/sync/add.rs
index 74dbb18..2b3db3a 100644
--- a/ops/op2/test_cases/sync/add.rs
+++ b/ops/op2/test_cases/sync/add.rs
@@ -1,4 +1,6 @@
 // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+#![deny(warnings)]
+deno_ops_compile_test_runner::prelude!();
 
 #[op2(fast)]
 fn op_add(a: u32, b: u32) -> u32 {
diff --git a/ops/op2/test_cases/sync/add_options.out b/ops/op2/test_cases/sync/add_options.out
index e6dc8d0..fdfbe7d 100644
--- a/ops/op2/test_cases/sync/add_options.out
+++ b/ops/op2/test_cases/sync/add_options.out
@@ -2,9 +2,9 @@
 pub struct op_test_add_option {
     _unconstructable: ::std::marker::PhantomData<()>,
 }
-impl crate::_ops::Op for op_test_add_option {
+impl deno_core::_ops::Op for op_test_add_option {
     const NAME: &'static str = stringify!(op_test_add_option);
-    const DECL: crate::_ops::OpDecl = crate::_ops::OpDecl::new_internal(
+    const DECL: deno_core::_ops::OpDecl = deno_core::_ops::OpDecl::new_internal(
         stringify!(op_test_add_option),
         false,
         false,
@@ -19,23 +19,23 @@ impl op_test_add_option {
         stringify!(op_test_add_option)
     }
     #[deprecated(note = "Use the const op::DECL instead")]
-    pub const fn decl() -> crate::_ops::OpDecl {
-        <Self as crate::_ops::Op>::DECL
+    pub const fn decl() -> deno_core::_ops::OpDecl {
+        <Self as deno_core::_ops::Op>::DECL
     }
-    extern "C" fn v8_fn_ptr(info: *const crate::v8::FunctionCallbackInfo) {
-        let mut rv = crate::v8::ReturnValue::from_function_callback_info(unsafe {
+    extern "C" fn v8_fn_ptr(info: *const deno_core::v8::FunctionCallbackInfo) {
+        let mut rv = deno_core::v8::ReturnValue::from_function_callback_info(unsafe {
             &*info
         });
-        let args = crate::v8::FunctionCallbackArguments::from_function_callback_info(unsafe {
+        let args = deno_core::v8::FunctionCallbackArguments::from_function_callback_info(unsafe {
             &*info
         });
         let arg0 = args.get(0usize as i32);
-        let arg0 = crate::_ops::to_u32(&arg0) as _;
+        let arg0 = deno_core::_ops::to_u32(&arg0) as _;
         let arg1 = args.get(1usize as i32);
         let arg1 = if arg1.is_null_or_undefined() {
             None
         } else {
-            let arg1 = crate::_ops::to_u32(&arg1) as _;
+            let arg1 = deno_core::_ops::to_u32(&arg1) as _;
             Some(arg1)
         };
         let result = Self::call(arg0, arg1);
diff --git a/ops/op2/test_cases/sync/add_options.rs b/ops/op2/test_cases/sync/add_options.rs
index a5f2c8f..439974a 100644
--- a/ops/op2/test_cases/sync/add_options.rs
+++ b/ops/op2/test_cases/sync/add_options.rs
@@ -1,6 +1,8 @@
 // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+#![deny(warnings)]
+deno_ops_compile_test_runner::prelude!();
 
-#[op2(core)]
+#[op2]
 pub fn op_test_add_option(a: u32, b: Option<u32>) -> u32 {
   a + b.unwrap_or(100)
 }
diff --git a/ops/op2/test_cases/sync/bool.out b/ops/op2/test_cases/sync/bool.out
index 5f22100..abf1153 100644
--- a/ops/op2/test_cases/sync/bool.out
+++ b/ops/op2/test_cases/sync/bool.out
@@ -51,5 +51,7 @@ impl op_bool {
         rv.set_bool(result as bool);
     }
     #[inline(always)]
-    pub fn call(arg: bool) -> bool {}
+    pub fn call(arg: bool) -> bool {
+        arg
+    }
 }
diff --git a/ops/op2/test_cases/sync/bool.rs b/ops/op2/test_cases/sync/bool.rs
index 8e036cc..c5ced32 100644
--- a/ops/op2/test_cases/sync/bool.rs
+++ b/ops/op2/test_cases/sync/bool.rs
@@ -1,5 +1,8 @@
 // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+#![deny(warnings)]
+deno_ops_compile_test_runner::prelude!();
 
 #[op2(fast)]
 pub fn op_bool(arg: bool) -> bool {
+    arg
 }
diff --git a/ops/op2/test_cases/sync/bool_result.out b/ops/op2/test_cases/sync/bool_result.out
index 1c4d830..d3539af 100644
--- a/ops/op2/test_cases/sync/bool_result.out
+++ b/ops/op2/test_cases/sync/bool_result.out
@@ -38,7 +38,7 @@ impl op_bool {
         let fast_api_callback_options = unsafe { &mut *fast_api_callback_options };
         let opctx = unsafe {
             &*(deno_core::v8::Local::<
-                v8::External,
+                deno_core::v8::External,
             >::cast(unsafe { fast_api_callback_options.data.data })
                 .value() as *const deno_core::_ops::OpCtx)
         };
@@ -69,6 +69,7 @@ impl op_bool {
         };
         if let Some(err) = unsafe { opctx.unsafely_take_last_error_for_ops_only() } {
             let mut scope = unsafe { deno_core::v8::CallbackScope::new(&*info) };
+            let err = err.into();
             let opstate = ::std::cell::RefCell::borrow(&*opctx.state);
             let exception = deno_core::error::to_v8_error(
                 &mut scope,
@@ -87,6 +88,7 @@ impl op_bool {
             }
             Err(err) => {
                 let mut scope = unsafe { deno_core::v8::CallbackScope::new(&*info) };
+                let err = err.into();
                 let opstate = ::std::cell::RefCell::borrow(&*opctx.state);
                 let exception = deno_core::error::to_v8_error(
                     &mut scope,
@@ -99,5 +101,7 @@ impl op_bool {
         };
     }
     #[inline(always)]
-    pub fn call(arg: bool) -> Result<bool, AnyError> {}
+    pub fn call(arg: bool) -> Result<bool, AnyError> {
+        Ok(arg)
+    }
 }
diff --git a/ops/op2/test_cases/sync/bool_result.rs b/ops/op2/test_cases/sync/bool_result.rs
index 6268b2e..2cb422f 100644
--- a/ops/op2/test_cases/sync/bool_result.rs
+++ b/ops/op2/test_cases/sync/bool_result.rs
@@ -1,5 +1,10 @@
 // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+#![deny(warnings)]
+deno_ops_compile_test_runner::prelude!();
+
+use deno_core::error::AnyError;
 
 #[op2(fast)]
 pub fn op_bool(arg: bool) -> Result<bool, AnyError> {
+    Ok(arg)
 }
diff --git a/ops/op2/test_cases/sync/buffers.out b/ops/op2/test_cases/sync/buffers.out
index 26e7029..904711c 100644
--- a/ops/op2/test_cases/sync/buffers.out
+++ b/ops/op2/test_cases/sync/buffers.out
@@ -89,5 +89,5 @@ impl op_buffers {
         let result = Self::call(arg0, arg1);
     }
     #[inline(always)]
-    fn call(a: &[u8], b: &mut [u8]) {}
+    fn call(_a: &[u8], _b: &mut [u8]) {}
 }
diff --git a/ops/op2/test_cases/sync/buffers.rs b/ops/op2/test_cases/sync/buffers.rs
index 2082edb..ee56204 100644
--- a/ops/op2/test_cases/sync/buffers.rs
+++ b/ops/op2/test_cases/sync/buffers.rs
@@ -1,5 +1,7 @@
 // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+#![deny(warnings)]
+deno_ops_compile_test_runner::prelude!();
 
 #[op2(fast)]
-fn op_buffers(#[buffer] a: &[u8], #[buffer] b: &mut [u8]) {
+fn op_buffers(#[buffer] _a: &[u8], #[buffer] _b: &mut [u8]) {
 }
diff --git a/ops/op2/test_cases/sync/buffers_copy.out b/ops/op2/test_cases/sync/buffers_copy.out
index 50b2b7f..1e1d35f 100644
--- a/ops/op2/test_cases/sync/buffers_copy.out
+++ b/ops/op2/test_cases/sync/buffers_copy.out
@@ -121,5 +121,5 @@ impl op_buffers {
         let result = Self::call(arg0, arg1, arg2);
     }
     #[inline(always)]
-    fn call(a: Vec<u8>, b: Box<[u8]>, c: bytes::Bytes) {}
+    fn call(_a: Vec<u8>, _b: Box<[u8]>, _c: bytes::Bytes) {}
 }
diff --git a/ops/op2/test_cases/sync/buffers_copy.rs b/ops/op2/test_cases/sync/buffers_copy.rs
index c349194..4a4cd63 100644
--- a/ops/op2/test_cases/sync/buffers_copy.rs
+++ b/ops/op2/test_cases/sync/buffers_copy.rs
@@ -1,5 +1,7 @@
 // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+#![deny(warnings)]
+deno_ops_compile_test_runner::prelude!();
 
 #[op2(fast)]
-fn op_buffers(#[buffer(copy)] a: Vec<u8>, #[buffer(copy)] b: Box<[u8]>, #[buffer(copy)] c: bytes::Bytes) {
+fn op_buffers(#[buffer(copy)] _a: Vec<u8>, #[buffer(copy)] _b: Box<[u8]>, #[buffer(copy)] _c: bytes::Bytes) {
 }
diff --git a/ops/op2/test_cases/sync/clippy_allow.rs b/ops/op2/test_cases/sync/clippy_allow.rs
index 009f279..6c42d4c 100644
--- a/ops/op2/test_cases/sync/clippy_allow.rs
+++ b/ops/op2/test_cases/sync/clippy_allow.rs
@@ -1,4 +1,6 @@
 // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+#![deny(warnings)]
+deno_ops_compile_test_runner::prelude!();
 
 /// This is a doc comment.
 #[op2(fast)]
diff --git a/ops/op2/test_cases/sync/doc_comment.rs b/ops/op2/test_cases/sync/doc_comment.rs
index b729a64..fa91fe4 100644
--- a/ops/op2/test_cases/sync/doc_comment.rs
+++ b/ops/op2/test_cases/sync/doc_comment.rs
@@ -1,4 +1,6 @@
 // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+#![deny(warnings)]
+deno_ops_compile_test_runner::prelude!();
 
 /// This is a doc comment.
 #[op2(fast)]
diff --git a/ops/op2/test_cases/sync/generics.rs b/ops/op2/test_cases/sync/generics.rs
index b412a7f..ee6cdc0 100644
--- a/ops/op2/test_cases/sync/generics.rs
+++ b/ops/op2/test_cases/sync/generics.rs
@@ -1,4 +1,8 @@
 // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+#![deny(warnings)]
+deno_ops_compile_test_runner::prelude!();
+
+pub trait Trait {}
 
 #[op2(fast)]
 pub fn op_generics<T: Trait>() {}
diff --git a/ops/op2/test_cases/sync/op_state_attr.out b/ops/op2/test_cases/sync/op_state_attr.out
index bff4fc6..43a3347 100644
--- a/ops/op2/test_cases/sync/op_state_attr.out
+++ b/ops/op2/test_cases/sync/op_state_attr.out
@@ -37,7 +37,7 @@ impl op_state_rc {
         let fast_api_callback_options = unsafe { &mut *fast_api_callback_options };
         let opctx = unsafe {
             &*(deno_core::v8::Local::<
-                v8::External,
+                deno_core::v8::External,
             >::cast(unsafe { fast_api_callback_options.data.data })
                 .value() as *const deno_core::_ops::OpCtx)
         };
@@ -64,5 +64,5 @@ impl op_state_rc {
         let result = Self::call(arg0, arg1);
     }
     #[inline(always)]
-    fn call(arg: &Something, arg_opt: Option<&Something>) {}
+    fn call(_arg: &Something, _arg_opt: Option<&Something>) {}
 }
diff --git a/ops/op2/test_cases/sync/op_state_attr.rs b/ops/op2/test_cases/sync/op_state_attr.rs
index 388527b..8fdde64 100644
--- a/ops/op2/test_cases/sync/op_state_attr.rs
+++ b/ops/op2/test_cases/sync/op_state_attr.rs
@@ -1,4 +1,8 @@
 // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+#![deny(warnings)]
+deno_ops_compile_test_runner::prelude!();
+
+struct Something {}
 
 #[op2(fast)]
-fn op_state_rc(#[state] arg: &Something, #[state] arg_opt: Option<&Something>) {}
+fn op_state_rc(#[state] _arg: &Something, #[state] _arg_opt: Option<&Something>) {}
diff --git a/ops/op2/test_cases/sync/op_state_rc.out b/ops/op2/test_cases/sync/op_state_rc.out
index 2ef2371..59b3f6a 100644
--- a/ops/op2/test_cases/sync/op_state_rc.out
+++ b/ops/op2/test_cases/sync/op_state_rc.out
@@ -37,7 +37,7 @@ impl op_state_rc {
         let fast_api_callback_options = unsafe { &mut *fast_api_callback_options };
         let opctx = unsafe {
             &*(deno_core::v8::Local::<
-                v8::External,
+                deno_core::v8::External,
             >::cast(unsafe { fast_api_callback_options.data.data })
                 .value() as *const deno_core::_ops::OpCtx)
         };
@@ -58,5 +58,5 @@ impl op_state_rc {
         let result = Self::call(arg0);
     }
     #[inline(always)]
-    fn call(state: Rc<RefCell<OpState>>) {}
+    fn call(_state: Rc<RefCell<OpState>>) {}
 }
diff --git a/ops/op2/test_cases/sync/op_state_rc.rs b/ops/op2/test_cases/sync/op_state_rc.rs
index e4672ad..862158d 100644
--- a/ops/op2/test_cases/sync/op_state_rc.rs
+++ b/ops/op2/test_cases/sync/op_state_rc.rs
@@ -1,4 +1,10 @@
 // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+#![deny(warnings)]
+deno_ops_compile_test_runner::prelude!();
+
+use std::rc::Rc;
+use std::cell::RefCell;
+use deno_core::OpState;
 
 #[op2(fast)]
-fn op_state_rc(state: Rc<RefCell<OpState>>) {}
+fn op_state_rc(_state: Rc<RefCell<OpState>>) {}
diff --git a/ops/op2/test_cases/sync/op_state_ref.out b/ops/op2/test_cases/sync/op_state_ref.out
index a0ad798..4db27b7 100644
--- a/ops/op2/test_cases/sync/op_state_ref.out
+++ b/ops/op2/test_cases/sync/op_state_ref.out
@@ -37,7 +37,7 @@ impl op_state_rc {
         let fast_api_callback_options = unsafe { &mut *fast_api_callback_options };
         let opctx = unsafe {
             &*(deno_core::v8::Local::<
-                v8::External,
+                deno_core::v8::External,
             >::cast(unsafe { fast_api_callback_options.data.data })
                 .value() as *const deno_core::_ops::OpCtx)
         };
@@ -58,5 +58,5 @@ impl op_state_rc {
         let result = Self::call(arg0);
     }
     #[inline(always)]
-    fn call(state: &OpState) {}
+    fn call(_state: &OpState) {}
 }
diff --git a/ops/op2/test_cases/sync/op_state_ref.rs b/ops/op2/test_cases/sync/op_state_ref.rs
index d0531d4..ad5753f 100644
--- a/ops/op2/test_cases/sync/op_state_ref.rs
+++ b/ops/op2/test_cases/sync/op_state_ref.rs
@@ -1,4 +1,8 @@
 // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+#![deny(warnings)]
+deno_ops_compile_test_runner::prelude!();
+
+use deno_core::OpState;
 
 #[op2(fast)]
-fn op_state_rc(state: &OpState) {}
+fn op_state_rc(_state: &OpState) {}
diff --git a/ops/op2/test_cases/sync/result_primitive.out b/ops/op2/test_cases/sync/result_primitive.out
index abd381f..04f8715 100644
--- a/ops/op2/test_cases/sync/result_primitive.out
+++ b/ops/op2/test_cases/sync/result_primitive.out
@@ -37,7 +37,7 @@ impl op_u32_with_result {
         let fast_api_callback_options = unsafe { &mut *fast_api_callback_options };
         let opctx = unsafe {
             &*(deno_core::v8::Local::<
-                v8::External,
+                deno_core::v8::External,
             >::cast(unsafe { fast_api_callback_options.data.data })
                 .value() as *const deno_core::_ops::OpCtx)
         };
@@ -70,6 +70,7 @@ impl op_u32_with_result {
             let args = deno_core::v8::FunctionCallbackArguments::from_function_callback_info(unsafe {
                 &*info
             });
+            let err = err.into();
             let opstate = ::std::cell::RefCell::borrow(&*opctx.state);
             let exception = deno_core::error::to_v8_error(
                 &mut scope,
@@ -89,6 +90,7 @@ impl op_u32_with_result {
                 let args = deno_core::v8::FunctionCallbackArguments::from_function_callback_info(unsafe {
                     &*info
                 });
+                let err = err.into();
                 let opstate = ::std::cell::RefCell::borrow(&*opctx.state);
                 let exception = deno_core::error::to_v8_error(
                     &mut scope,
@@ -101,5 +103,7 @@ impl op_u32_with_result {
         };
     }
     #[inline(always)]
-    pub fn call() -> Result<u32, AnyError> {}
+    pub fn call() -> Result<u32, AnyError> {
+        Ok(0)
+    }
 }
diff --git a/ops/op2/test_cases/sync/result_primitive.rs b/ops/op2/test_cases/sync/result_primitive.rs
index df89c24..e750169 100644
--- a/ops/op2/test_cases/sync/result_primitive.rs
+++ b/ops/op2/test_cases/sync/result_primitive.rs
@@ -1,4 +1,10 @@
 // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+#![deny(warnings)]
+deno_ops_compile_test_runner::prelude!();
+
+use deno_core::error::AnyError;
 
 #[op2(fast)]
-pub fn op_u32_with_result() -> Result<u32, AnyError> {}
+pub fn op_u32_with_result() -> Result<u32, AnyError> {
+    Ok(0)
+}
diff --git a/ops/op2/test_cases/sync/result_scope.out b/ops/op2/test_cases/sync/result_scope.out
index ff7c57c..4d28ad8 100644
--- a/ops/op2/test_cases/sync/result_scope.out
+++ b/ops/op2/test_cases/sync/result_scope.out
@@ -36,6 +36,7 @@ impl op_void_with_result {
                     &*(deno_core::v8::Local::<deno_core::v8::External>::cast(args.data())
                         .value() as *const deno_core::_ops::OpCtx)
                 };
+                let err = err.into();
                 let opstate = ::std::cell::RefCell::borrow(&*opctx.state);
                 let exception = deno_core::error::to_v8_error(
                     &mut scope,
@@ -48,5 +49,7 @@ impl op_void_with_result {
         };
     }
     #[inline(always)]
-    pub fn call(scope: &mut v8::HandleScope) -> Result<(), AnyError> {}
+    pub fn call(_scope: &mut v8::HandleScope) -> Result<(), AnyError> {
+        Ok(())
+    }
 }
diff --git a/ops/op2/test_cases/sync/result_scope.rs b/ops/op2/test_cases/sync/result_scope.rs
index 8d036a4..eab1e14 100644
--- a/ops/op2/test_cases/sync/result_scope.rs
+++ b/ops/op2/test_cases/sync/result_scope.rs
@@ -1,4 +1,11 @@
 // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+#![deny(warnings)]
+deno_ops_compile_test_runner::prelude!();
+
+use deno_core::error::AnyError;
+use deno_core::v8;
 
 #[op2]
-pub fn op_void_with_result(scope: &mut v8::HandleScope) -> Result<(), AnyError> {}
+pub fn op_void_with_result(_scope: &mut v8::HandleScope) -> Result<(), AnyError> {
+    Ok(())
+}
diff --git a/ops/op2/test_cases/sync/result_void.out b/ops/op2/test_cases/sync/result_void.out
index 163c56c..eac8a27 100644
--- a/ops/op2/test_cases/sync/result_void.out
+++ b/ops/op2/test_cases/sync/result_void.out
@@ -37,7 +37,7 @@ impl op_void_with_result {
         let fast_api_callback_options = unsafe { &mut *fast_api_callback_options };
         let opctx = unsafe {
             &*(deno_core::v8::Local::<
-                v8::External,
+                deno_core::v8::External,
             >::cast(unsafe { fast_api_callback_options.data.data })
                 .value() as *const deno_core::_ops::OpCtx)
         };
@@ -67,6 +67,7 @@ impl op_void_with_result {
             let args = deno_core::v8::FunctionCallbackArguments::from_function_callback_info(unsafe {
                 &*info
             });
+            let err = err.into();
             let opstate = ::std::cell::RefCell::borrow(&*opctx.state);
             let exception = deno_core::error::to_v8_error(
                 &mut scope,
@@ -84,6 +85,7 @@ impl op_void_with_result {
                 let args = deno_core::v8::FunctionCallbackArguments::from_function_callback_info(unsafe {
                     &*info
                 });
+                let err = err.into();
                 let opstate = ::std::cell::RefCell::borrow(&*opctx.state);
                 let exception = deno_core::error::to_v8_error(
                     &mut scope,
@@ -96,5 +98,7 @@ impl op_void_with_result {
         };
     }
     #[inline(always)]
-    pub fn call() -> Result<(), AnyError> {}
+    pub fn call() -> Result<(), AnyError> {
+        Ok(())
+    }
 }
diff --git a/ops/op2/test_cases/sync/result_void.rs b/ops/op2/test_cases/sync/result_void.rs
index ef3aa7b..ae0f793 100644
--- a/ops/op2/test_cases/sync/result_void.rs
+++ b/ops/op2/test_cases/sync/result_void.rs
@@ -1,4 +1,10 @@
 // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+#![deny(warnings)]
+deno_ops_compile_test_runner::prelude!();
+
+use deno_core::error::AnyError;
 
 #[op2(fast)]
-pub fn op_void_with_result() -> Result<(), AnyError> {}
+pub fn op_void_with_result() -> Result<(), AnyError> {
+    Ok(())
+}
diff --git a/ops/op2/test_cases/sync/serde_v8.out b/ops/op2/test_cases/sync/serde_v8.out
index 406dc0c..85602a1 100644
--- a/ops/op2/test_cases/sync/serde_v8.out
+++ b/ops/op2/test_cases/sync/serde_v8.out
@@ -61,5 +61,7 @@ impl op_serde_v8 {
         rv.set(result.into())
     }
     #[inline(always)]
-    pub fn call(input: Input) -> Output {}
+    pub fn call(_input: Input) -> Output {
+        Output {}
+    }
 }
diff --git a/ops/op2/test_cases/sync/serde_v8.rs b/ops/op2/test_cases/sync/serde_v8.rs
index 90d166e..c746360 100644
--- a/ops/op2/test_cases/sync/serde_v8.rs
+++ b/ops/op2/test_cases/sync/serde_v8.rs
@@ -1,5 +1,17 @@
 // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+#![deny(warnings)]
+deno_ops_compile_test_runner::prelude!();
+
+use serde::Serialize;
+use serde::Deserialize;
+
+#[derive(Serialize, Deserialize)]
+pub struct Input {}
+#[derive(Serialize, Deserialize)]
+pub struct Output {}
 
 #[op2]
 #[serde]
-pub fn op_serde_v8(#[serde] input: Input) -> Output {}
+pub fn op_serde_v8(#[serde] _input: Input) -> Output {
+    Output {}
+}
diff --git a/ops/op2/test_cases/sync/smi.out b/ops/op2/test_cases/sync/smi.out
index 422a42e..bac6fa4 100644
--- a/ops/op2/test_cases/sync/smi.out
+++ b/ops/op2/test_cases/sync/smi.out
@@ -55,5 +55,7 @@ impl op_add {
         rv.set_uint32(result as u32);
     }
     #[inline(always)]
-    fn call(id: ResourceId, extra: u16) -> u32 {}
+    fn call(id: ResourceId, extra: u16) -> u32 {
+        id as u32 + extra as u32
+    }
 }
diff --git a/ops/op2/test_cases/sync/smi.rs b/ops/op2/test_cases/sync/smi.rs
index a5a4418..22db30d 100644
--- a/ops/op2/test_cases/sync/smi.rs
+++ b/ops/op2/test_cases/sync/smi.rs
@@ -1,4 +1,10 @@
 // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+#![deny(warnings)]
+deno_ops_compile_test_runner::prelude!();
+
+pub type ResourceId = i16;
 
 #[op2(fast)]
-fn op_add(#[smi] id: ResourceId, extra: u16) -> u32 {}
+fn op_add(#[smi] id: ResourceId, extra: u16) -> u32 {
+    id as u32 + extra as u32
+}
diff --git a/ops/op2/test_cases/sync/string_cow.out b/ops/op2/test_cases/sync/string_cow.out
index e876aeb..7df29c5 100644
--- a/ops/op2/test_cases/sync/string_cow.out
+++ b/ops/op2/test_cases/sync/string_cow.out
@@ -54,5 +54,7 @@ impl op_string_cow {
         rv.set_uint32(result as u32);
     }
     #[inline(always)]
-    fn call(s: Cow<str>) -> u32 {}
+    fn call(s: Cow<str>) -> u32 {
+        s.len() as _
+    }
 }
diff --git a/ops/op2/test_cases/sync/string_cow.rs b/ops/op2/test_cases/sync/string_cow.rs
index ed4dfca..59f3772 100644
--- a/ops/op2/test_cases/sync/string_cow.rs
+++ b/ops/op2/test_cases/sync/string_cow.rs
@@ -1,4 +1,10 @@
 // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+#![deny(warnings)]
+deno_ops_compile_test_runner::prelude!();
+
+use std::borrow::Cow;
 
 #[op2(fast)]
-fn op_string_cow(#[string] s: Cow<str>) -> u32 {}
+fn op_string_cow(#[string] s: Cow<str>) -> u32 {
+    s.len() as _
+}
diff --git a/ops/op2/test_cases/sync/string_option_return.out b/ops/op2/test_cases/sync/string_option_return.out
index 9e70f8d..e36a800 100644
--- a/ops/op2/test_cases/sync/string_option_return.out
+++ b/ops/op2/test_cases/sync/string_option_return.out
@@ -40,5 +40,7 @@ impl op_string_return {
         }
     }
     #[inline(always)]
-    pub fn call() -> Option<String> {}
+    pub fn call() -> Option<String> {
+        Some("".to_owned())
+    }
 }
diff --git a/ops/op2/test_cases/sync/string_option_return.rs b/ops/op2/test_cases/sync/string_option_return.rs
index 932836d..a141cb8 100644
--- a/ops/op2/test_cases/sync/string_option_return.rs
+++ b/ops/op2/test_cases/sync/string_option_return.rs
@@ -1,5 +1,9 @@
 // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+#![deny(warnings)]
+deno_ops_compile_test_runner::prelude!();
 
 #[op2]
 #[string]
-pub fn op_string_return() -> Option<String> {}
+pub fn op_string_return() -> Option<String> {
+    Some("".to_owned())
+}
diff --git a/ops/op2/test_cases/sync/string_owned.out b/ops/op2/test_cases/sync/string_owned.out
index d0d145e..b209033 100644
--- a/ops/op2/test_cases/sync/string_owned.out
+++ b/ops/op2/test_cases/sync/string_owned.out
@@ -52,5 +52,7 @@ impl op_string_owned {
         rv.set_uint32(result as u32);
     }
     #[inline(always)]
-    fn call(s: String) -> u32 {}
+    fn call(s: String) -> u32 {
+        s.len() as _
+    }
 }
diff --git a/ops/op2/test_cases/sync/string_owned.rs b/ops/op2/test_cases/sync/string_owned.rs
index b81d7ec..e2d623a 100644
--- a/ops/op2/test_cases/sync/string_owned.rs
+++ b/ops/op2/test_cases/sync/string_owned.rs
@@ -1,4 +1,8 @@
 // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+#![deny(warnings)]
+deno_ops_compile_test_runner::prelude!();
 
 #[op2(fast)]
-fn op_string_owned(#[string] s: String) -> u32 {}
+fn op_string_owned(#[string] s: String) -> u32 {
+    s.len() as _
+}
diff --git a/ops/op2/test_cases/sync/string_ref.out b/ops/op2/test_cases/sync/string_ref.out
index 6abab29..a845fa4 100644
--- a/ops/op2/test_cases/sync/string_ref.out
+++ b/ops/op2/test_cases/sync/string_ref.out
@@ -54,5 +54,7 @@ impl op_string_owned {
         rv.set_uint32(result as u32);
     }
     #[inline(always)]
-    fn call(s: &str) -> u32 {}
+    fn call(s: &str) -> u32 {
+        s.len() as _
+    }
 }
diff --git a/ops/op2/test_cases/sync/string_ref.rs b/ops/op2/test_cases/sync/string_ref.rs
index a7efa9f..5f0f084 100644
--- a/ops/op2/test_cases/sync/string_ref.rs
+++ b/ops/op2/test_cases/sync/string_ref.rs
@@ -1,4 +1,8 @@
 // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+#![deny(warnings)]
+deno_ops_compile_test_runner::prelude!();
 
 #[op2(fast)]
-fn op_string_owned(#[string] s: &str) -> u32 {}
+fn op_string_owned(#[string] s: &str) -> u32 {
+    s.len() as _
+}
diff --git a/ops/op2/test_cases/sync/string_return.out b/ops/op2/test_cases/sync/string_return.out
index 6cd737a..ea66399 100644
--- a/ops/op2/test_cases/sync/string_return.out
+++ b/ops/op2/test_cases/sync/string_return.out
@@ -36,5 +36,7 @@ impl op_string_return {
         }
     }
     #[inline(always)]
-    pub fn call() -> String {}
+    pub fn call() -> String {
+        "".into()
+    }
 }
diff --git a/ops/op2/test_cases/sync/string_return.rs b/ops/op2/test_cases/sync/string_return.rs
index 667b68a..bd4262d 100644
--- a/ops/op2/test_cases/sync/string_return.rs
+++ b/ops/op2/test_cases/sync/string_return.rs
@@ -1,5 +1,9 @@
 // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+#![deny(warnings)]
+deno_ops_compile_test_runner::prelude!();
 
 #[op2]
 #[string]
-pub fn op_string_return() -> String {}
+pub fn op_string_return() -> String {
+    "".into()
+}
diff --git a/ops/op2/test_cases/sync/v8_handlescope.out b/ops/op2/test_cases/sync/v8_handlescope.out
index 651f57a..9cbe8f0 100644
--- a/ops/op2/test_cases/sync/v8_handlescope.out
+++ b/ops/op2/test_cases/sync/v8_handlescope.out
@@ -50,8 +50,10 @@ impl op_handlescope {
         rv.set(result.into())
     }
     #[inline(always)]
-    fn call(
-        scope: &v8::HandleScope,
-        str2: v8::Local<v8::String>,
-    ) -> v8::Local<v8::String> {}
+    fn call<'a>(
+        _scope: &v8::HandleScope<'a>,
+        _str2: v8::Local<v8::String>,
+    ) -> v8::Local<'a, v8::String> {
+        unimplemented!()
+    }
 }
diff --git a/ops/op2/test_cases/sync/v8_handlescope.rs b/ops/op2/test_cases/sync/v8_handlescope.rs
index 25657e5..30fa245 100644
--- a/ops/op2/test_cases/sync/v8_handlescope.rs
+++ b/ops/op2/test_cases/sync/v8_handlescope.rs
@@ -1,5 +1,10 @@
 // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+#![deny(warnings)]
+deno_ops_compile_test_runner::prelude!();
+
+use deno_core::v8;
 
 #[op2]
-fn op_handlescope(scope: &v8::HandleScope, str2: v8::Local<v8::String>) -> v8::Local<v8::String> {
+fn op_handlescope<'a>(_scope: &v8::HandleScope<'a>, _str2: v8::Local<v8::String>) -> v8::Local<'a, v8::String> {
+    unimplemented!()
 }
diff --git a/ops/op2/test_cases/sync/v8_lifetime.out b/ops/op2/test_cases/sync/v8_lifetime.out
index dd87902..d62c486 100644
--- a/ops/op2/test_cases/sync/v8_lifetime.out
+++ b/ops/op2/test_cases/sync/v8_lifetime.out
@@ -49,5 +49,7 @@ impl op_v8_lifetime {
         rv.set(result.into())
     }
     #[inline(always)]
-    pub fn call<'s>(s: v8::Local<'s, v8::String>) -> v8::Local<'s, v8::String> {}
+    pub fn call<'s>(_s: v8::Local<'s, v8::String>) -> v8::Local<'s, v8::String> {
+        unimplemented!()
+    }
 }
diff --git a/ops/op2/test_cases/sync/v8_lifetime.rs b/ops/op2/test_cases/sync/v8_lifetime.rs
index 3050a20..e1ca23d 100644
--- a/ops/op2/test_cases/sync/v8_lifetime.rs
+++ b/ops/op2/test_cases/sync/v8_lifetime.rs
@@ -1,4 +1,10 @@
 // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+#![deny(warnings)]
+deno_ops_compile_test_runner::prelude!();
+
+use deno_core::v8;
 
 #[op2]
-pub fn op_v8_lifetime<'s>(s: v8::Local<'s, v8::String>) -> v8::Local<'s, v8::String> {}
+pub fn op_v8_lifetime<'s>(_s: v8::Local<'s, v8::String>) -> v8::Local<'s, v8::String> {
+    unimplemented!()
+}
diff --git a/ops/op2/test_cases/sync/v8_ref_option.out b/ops/op2/test_cases/sync/v8_ref_option.out
index 968518f..a78b080 100644
--- a/ops/op2/test_cases/sync/v8_ref_option.out
+++ b/ops/op2/test_cases/sync/v8_ref_option.out
@@ -103,5 +103,5 @@ impl op_v8_lifetime {
         let result = Self::call(arg0, arg1);
     }
     #[inline(always)]
-    pub fn call<'s>(s: Option<&v8::String>, s2: Option<&v8::String>) {}
+    pub fn call<'s>(_s: Option<&v8::String>, _s2: Option<&v8::String>) {}
 }
diff --git a/ops/op2/test_cases/sync/v8_ref_option.rs b/ops/op2/test_cases/sync/v8_ref_option.rs
index eec0146..7dcfc10 100644
--- a/ops/op2/test_cases/sync/v8_ref_option.rs
+++ b/ops/op2/test_cases/sync/v8_ref_option.rs
@@ -1,5 +1,9 @@
 // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+#![deny(warnings)]
+deno_ops_compile_test_runner::prelude!();
+
+use deno_core::v8;
 
 #[op2(fast)]
-pub fn op_v8_lifetime<'s>(s: Option<&v8::String>, s2: Option<&v8::String>) {}
+pub fn op_v8_lifetime<'s>(_s: Option<&v8::String>, _s2: Option<&v8::String>) {}
 
diff --git a/ops/op2/test_cases/sync/v8_string.out b/ops/op2/test_cases/sync/v8_string.out
index 91640b6..4cf28bd 100644
--- a/ops/op2/test_cases/sync/v8_string.out
+++ b/ops/op2/test_cases/sync/v8_string.out
@@ -65,5 +65,10 @@ impl op_v8_string {
         rv.set(result.into())
     }
     #[inline(always)]
-    fn call(str1: &v8::String, str2: v8::Local<v8::String>) -> v8::Local<v8::String> {}
+    fn call<'a>(
+        _str1: &v8::String,
+        _str2: v8::Local<v8::String>,
+    ) -> v8::Local<'a, v8::String> {
+        unimplemented!()
+    }
 }
diff --git a/ops/op2/test_cases/sync/v8_string.rs b/ops/op2/test_cases/sync/v8_string.rs
index db0c350..5f6f52a 100644
--- a/ops/op2/test_cases/sync/v8_string.rs
+++ b/ops/op2/test_cases/sync/v8_string.rs
@@ -1,5 +1,10 @@
 // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+#![deny(warnings)]
+deno_ops_compile_test_runner::prelude!();
+
+use deno_core::v8;
 
 #[op2]
-fn op_v8_string(str1: &v8::String, str2: v8::Local<v8::String>) -> v8::Local<v8::String> {
+fn op_v8_string<'a>(_str1: &v8::String, _str2: v8::Local<v8::String>) -> v8::Local<'a, v8::String> {
+    unimplemented!()
 }
diff --git a/ops/tests/compile_fail/mem_slices.rs b/ops/tests/compile_fail/mem_slices.rs
deleted file mode 100644
index da74ac5..0000000
--- a/ops/tests/compile_fail/mem_slices.rs
+++ /dev/null
@@ -1,24 +0,0 @@
-// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
-
-use deno_ops::op;
-
-#[op]
-fn sync_test(slice: &mut [u32]) {
-  //
-}
-
-#[op]
-async fn async_test(slice: &[u8]) {
-  // Memory slices are not allowed in async ops.
-}
-
-#[op]
-fn async_test2(slice: &mut [u8]) -> impl Future<Output = ()> {
-  // Memory slices are not allowed in async ops, even when not implemented as an
-  // async function.
-  async {}
-}
-
-fn main() {
-  // pass
-}
diff --git a/ops/tests/compile_fail/mem_slices.stderr b/ops/tests/compile_fail/mem_slices.stderr
deleted file mode 100644
index c45acfc..0000000
--- a/ops/tests/compile_fail/mem_slices.stderr
+++ /dev/null
@@ -1,15 +0,0 @@
-error: custom attribute panicked
-  --> tests/compile_fail/mem_slices.rs:10:1
-   |
-10 | #[op]
-   | ^^^^^
-   |
-   = help: message: Memory slices are not allowed in async ops
-
-error: custom attribute panicked
-  --> tests/compile_fail/mem_slices.rs:15:1
-   |
-15 | #[op]
-   | ^^^^^
-   |
-   = help: message: Memory slices are not allowed in async ops