summaryrefslogtreecommitdiff
path: root/ops/fast_call.rs
diff options
context:
space:
mode:
Diffstat (limited to 'ops/fast_call.rs')
-rw-r--r--ops/fast_call.rs399
1 files changed, 399 insertions, 0 deletions
diff --git a/ops/fast_call.rs b/ops/fast_call.rs
new file mode 100644
index 000000000..4b5ba6e9b
--- /dev/null
+++ b/ops/fast_call.rs
@@ -0,0 +1,399 @@
+/// Code generation for V8 fast calls.
+use crate::optimizer::FastValue;
+use crate::optimizer::Optimizer;
+use pmutil::{q, Quote, ToTokensExt};
+use proc_macro2::{Span, TokenStream};
+use quote::quote;
+use syn::{
+ parse_quote, punctuated::Punctuated, token::Comma, GenericParam, Generics,
+ Ident, ItemFn, ItemImpl, Path, PathArguments, PathSegment, Type, TypePath,
+};
+
+pub(crate) struct FastImplItems {
+ pub(crate) impl_and_fn: TokenStream,
+ pub(crate) decl: TokenStream,
+ pub(crate) active: bool,
+}
+
+pub(crate) fn generate(
+ core: &TokenStream,
+ optimizer: &mut Optimizer,
+ item_fn: &ItemFn,
+) -> FastImplItems {
+ if !optimizer.fast_compatible {
+ return FastImplItems {
+ impl_and_fn: TokenStream::new(),
+ decl: quote! { None },
+ active: false,
+ };
+ }
+
+ // TODO(@littledivy): Use `let..else` on 1.65.0
+ let output_ty = match &optimizer.fast_result {
+ Some(ty) => ty,
+ None => {
+ return FastImplItems {
+ impl_and_fn: TokenStream::new(),
+ decl: quote! { None },
+ active: false,
+ }
+ }
+ };
+
+ // We've got 3 idents.
+ //
+ // - op_foo, the public op declaration contains the user function.
+ // - op_foo_fast, the fast call type.
+ // - op_foo_fast_fn, the fast call function.
+ let ident = item_fn.sig.ident.clone();
+ let fast_ident = Ident::new(&format!("{}_fast", ident), Span::call_site());
+ let fast_fn_ident =
+ Ident::new(&format!("{}_fast_fn", ident), Span::call_site());
+
+ // Deal with generics.
+ let generics = &item_fn.sig.generics;
+ let (impl_generics, _, where_clause) = generics.split_for_impl();
+
+ // struct op_foo_fast <T, U> { ... }
+ let struct_generics = exclude_lifetime_params(&generics.params);
+ // std::marker::PhantomData <A>
+ let phantom_generics: Quote = match struct_generics {
+ Some(ref params) => q!(Vars { params }, { params }),
+ None => q!({ <()> }),
+ };
+ // op_foo_fast_fn :: <T>
+ let caller_generics: Quote = match struct_generics {
+ Some(ref params) => q!(Vars { params }, { ::params }),
+ None => q!({}),
+ };
+
+ // This goes in the FastFunction impl block.
+ let mut segments = Punctuated::new();
+ {
+ let mut arguments = PathArguments::None;
+ if let Some(ref struct_generics) = struct_generics {
+ arguments = PathArguments::AngleBracketed(parse_quote! {
+ #struct_generics
+ });
+ }
+ segments.push_value(PathSegment {
+ ident: fast_ident.clone(),
+ arguments,
+ });
+ }
+
+ // struct T <A> {
+ // _phantom: ::std::marker::PhantomData<A>,
+ // }
+ let fast_ty: Quote = q!(Vars { Type: &fast_ident, generics: &struct_generics, phantom_generics }, {
+ struct Type generics {
+ _phantom: ::std::marker::PhantomData phantom_generics,
+ }
+ });
+
+ // Original inputs.
+ let mut inputs = item_fn.sig.inputs.clone();
+ let mut transforms = q!({});
+ let mut pre_transforms = q!({});
+
+ // Apply parameter transforms
+ for (index, input) in inputs.iter_mut().enumerate() {
+ if let Some(transform) = optimizer.transforms.get(&index) {
+ let quo: Quote = transform.apply_for_fast_call(core, input);
+ transforms.push_tokens(&quo);
+ }
+ }
+
+ // Collect idents to be passed into function call, we can now freely
+ // modify the inputs.
+ let idents = inputs
+ .iter()
+ .map(|input| match input {
+ syn::FnArg::Typed(pat_type) => match &*pat_type.pat {
+ syn::Pat::Ident(pat_ident) => pat_ident.ident.clone(),
+ _ => panic!("unexpected pattern"),
+ },
+ _ => panic!("unexpected argument"),
+ })
+ .collect::<Punctuated<_, Comma>>();
+
+ // Retain only *pure* parameters.
+ let mut fast_fn_inputs = if optimizer.has_opstate_in_parameters() {
+ inputs.iter().skip(1).cloned().collect()
+ } else {
+ inputs.clone()
+ };
+
+ let mut input_variants = optimizer
+ .fast_parameters
+ .iter()
+ .map(q_fast_ty_variant)
+ .collect::<Punctuated<_, Comma>>();
+
+ // Apply *hard* optimizer hints.
+ if optimizer.has_fast_callback_option || optimizer.needs_opstate() {
+ fast_fn_inputs.push(parse_quote! {
+ fast_api_callback_options: *mut #core::v8::fast_api::FastApiCallbackOptions
+ });
+
+ input_variants.push(q!({ CallbackOptions }));
+ }
+
+ let mut output_transforms = q!({});
+
+ if optimizer.needs_opstate() {
+ // Grab the op_state identifier, the first one. ¯\_(ツ)_/¯
+ let op_state = match idents.first() {
+ Some(ident) if optimizer.has_opstate_in_parameters() => ident.clone(),
+ // fn op_foo() -> Result<...>
+ _ => Ident::new("op_state", Span::call_site()),
+ };
+
+ // Dark arts 🪄 ✨
+ //
+ // - V8 calling convention guarantees that the callback options pointer is non-null.
+ // - `data` union is always initialized as the `v8::Local<v8::Value>` variant.
+ // - deno_core guarantees that `data` is a v8 External pointing to an OpCtx for the
+ // isolate's lifetime.
+ let prelude = q!(
+ Vars {
+ op_state: &op_state
+ },
+ {
+ let __opts: &mut v8::fast_api::FastApiCallbackOptions =
+ unsafe { &mut *fast_api_callback_options };
+ let __ctx = unsafe {
+ &*(v8::Local::<v8::External>::cast(unsafe { __opts.data.data })
+ .value() as *const _ops::OpCtx)
+ };
+ let op_state = &mut ::std::cell::RefCell::borrow_mut(&__ctx.state);
+ }
+ );
+
+ pre_transforms.push_tokens(&prelude);
+
+ if optimizer.returns_result {
+ // Magic fallback 🪄
+ //
+ // If Result<T, E> is Ok(T), return T as fast value.
+ //
+ // Err(E) gets put into `last_fast_op_error` slot and
+ //
+ // V8 calls the slow path so we can take the slot
+ // value and throw.
+ let result_wrap = q!(Vars { op_state }, {
+ match result {
+ Ok(result) => result,
+ Err(err) => {
+ op_state.last_fast_op_error.replace(err);
+ __opts.fallback = true;
+ Default::default()
+ }
+ }
+ });
+
+ output_transforms.push_tokens(&result_wrap);
+ }
+ }
+
+ if !optimizer.returns_result {
+ let default_output = q!({ result });
+
+ output_transforms.push_tokens(&default_output);
+ }
+
+ let output = q_fast_ty(output_ty);
+ // Generate the function body.
+ //
+ // fn f <S> (_: Local<Object>, a: T, b: U) -> R {
+ // /* Transforms */
+ // let a = a.into();
+ // let b = b.into();
+ //
+ // let r = op::call(a, b);
+ //
+ // /* Return transform */
+ // r.into()
+ // }
+ let fast_fn = q!(
+ Vars { core, pre_transforms, op_name_fast: &fast_fn_ident, op_name: &ident, fast_fn_inputs, generics, call_generics: &caller_generics, where_clause, idents, transforms, output_transforms, output: &output },
+ {
+ fn op_name_fast generics (_: core::v8::Local<core::v8::Object>, fast_fn_inputs) -> output where_clause {
+ use core::v8;
+ use core::_ops;
+ pre_transforms
+ transforms
+ let result = op_name::call call_generics (idents);
+ output_transforms
+ }
+ }
+ );
+
+ let output_variant = q_fast_ty_variant(output_ty);
+ let mut generics: Generics = parse_quote! { #impl_generics };
+ generics.where_clause = where_clause.cloned();
+
+ // impl <A> fast_api::FastFunction for T <A> where A: B {
+ // fn function(&self) -> *const ::std::ffi::c_void {
+ // f as *const ::std::ffi::c_void
+ // }
+ // fn args(&self) -> &'static [fast_api::Type] {
+ // &[ CType::T, CType::U ]
+ // }
+ // fn return_type(&self) -> fast_api::CType {
+ // CType::T
+ // }
+ // }
+ let item: ItemImpl = ItemImpl {
+ attrs: vec![],
+ defaultness: None,
+ unsafety: None,
+ impl_token: Default::default(),
+ generics,
+ trait_: Some((
+ None,
+ parse_quote!(#core::v8::fast_api::FastFunction),
+ Default::default(),
+ )),
+ self_ty: Box::new(Type::Path(TypePath {
+ qself: None,
+ path: Path {
+ leading_colon: None,
+ segments,
+ },
+ })),
+ brace_token: Default::default(),
+ items: vec![
+ parse_quote! {
+ fn function(&self) -> *const ::std::ffi::c_void {
+ #fast_fn_ident #caller_generics as *const ::std::ffi::c_void
+ }
+ },
+ parse_quote! {
+ fn args(&self) -> &'static [#core::v8::fast_api::Type] {
+ use #core::v8::fast_api::Type::*;
+ use #core::v8::fast_api::CType;
+ &[ #input_variants ]
+ }
+ },
+ parse_quote! {
+ fn return_type(&self) -> #core::v8::fast_api::CType {
+ #core::v8::fast_api::CType::#output_variant
+ }
+ },
+ ],
+ };
+
+ let mut tts = q!({});
+ tts.push_tokens(&fast_ty);
+ tts.push_tokens(&item);
+ tts.push_tokens(&fast_fn);
+
+ let impl_and_fn = tts.dump();
+ let decl = q!(
+ Vars { fast_ident, caller_generics },
+ {
+ Some(Box::new(fast_ident caller_generics { _phantom: ::std::marker::PhantomData }))
+ }
+ ).dump();
+
+ FastImplItems {
+ impl_and_fn,
+ decl,
+ active: true,
+ }
+}
+
+/// Quote fast value type.
+fn q_fast_ty(v: &FastValue) -> Quote {
+ match v {
+ FastValue::Void => q!({ () }),
+ FastValue::U32 => q!({ u32 }),
+ FastValue::I32 => q!({ i32 }),
+ FastValue::U64 => q!({ u64 }),
+ FastValue::I64 => q!({ i64 }),
+ FastValue::F32 => q!({ f32 }),
+ FastValue::F64 => q!({ f64 }),
+ FastValue::Bool => q!({ bool }),
+ FastValue::V8Value => q!({ v8::Local<v8::Value> }),
+ FastValue::Uint8Array | FastValue::Uint32Array => unreachable!(),
+ }
+}
+
+/// Quote fast value type's variant.
+fn q_fast_ty_variant(v: &FastValue) -> Quote {
+ match v {
+ FastValue::Void => q!({ Void }),
+ FastValue::U32 => q!({ Uint32 }),
+ FastValue::I32 => q!({ Int32 }),
+ FastValue::U64 => q!({ Uint64 }),
+ FastValue::I64 => q!({ Int64 }),
+ FastValue::F32 => q!({ Float32 }),
+ FastValue::F64 => q!({ Float64 }),
+ FastValue::Bool => q!({ Bool }),
+ FastValue::V8Value => q!({ V8Value }),
+ FastValue::Uint8Array => q!({ TypedArray(CType::Uint8) }),
+ FastValue::Uint32Array => q!({ TypedArray(CType::Uint32) }),
+ }
+}
+
+fn exclude_lifetime_params(
+ generic_params: &Punctuated<GenericParam, Comma>,
+) -> Option<Generics> {
+ let params = generic_params
+ .iter()
+ .filter(|t| !matches!(t, GenericParam::Lifetime(_)))
+ .cloned()
+ .collect::<Punctuated<GenericParam, Comma>>();
+ if params.is_empty() {
+ // <()>
+ return None;
+ }
+ Some(Generics {
+ lt_token: Some(Default::default()),
+ params,
+ gt_token: Some(Default::default()),
+ where_clause: None,
+ })
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::Op;
+ use std::path::PathBuf;
+
+ #[testing_macros::fixture("optimizer_tests/**/*.rs")]
+ fn test_fast_call_codegen(input: PathBuf) {
+ let update_expected = std::env::var("UPDATE_EXPECTED").is_ok();
+ let core = crate::deno::import();
+
+ let source =
+ std::fs::read_to_string(&input).expect("Failed to read test file");
+
+ let item = syn::parse_str(&source).expect("Failed to parse test file");
+ let mut op = Op::new(item, Default::default());
+ let mut optimizer = Optimizer::new();
+ if optimizer.analyze(&mut op).is_err() {
+ // Tested by optimizer::test tests.
+ return;
+ }
+
+ let expected = std::fs::read_to_string(input.with_extension("out"))
+ .expect("Failed to read expected file");
+
+ let FastImplItems {
+ impl_and_fn: actual,
+ ..
+ } = generate(&core, &mut optimizer, &op.item);
+ // Validate syntax tree.
+ let tree = syn::parse2(actual).unwrap();
+ let actual = prettyplease::unparse(&tree);
+ if update_expected {
+ std::fs::write(input.with_extension("out"), actual)
+ .expect("Failed to write expected file");
+ } else {
+ assert_eq!(actual, expected);
+ }
+ }
+}