From bc33a4b2e06dd5518e0d1bbf7b538d0b00df214d Mon Sep 17 00:00:00 2001 From: Divy Srivastava Date: Thu, 10 Nov 2022 03:53:31 -0800 Subject: [PATCH] refactor(ops): Rewrite fast call optimizer and codegen (#16514) --- Cargo.lock | 30 + ops/Cargo.toml | 3 + ops/attrs.rs | 37 + ops/deno.rs | 32 + ops/fast_call.rs | 399 +++++++++ ops/lib.rs | 782 ++++-------------- ops/optimizer.rs | 600 ++++++++++++++ ops/optimizer_tests/callback_options.expected | 8 + ops/optimizer_tests/callback_options.out | 25 + ops/optimizer_tests/callback_options.rs | 5 + ops/optimizer_tests/incompatible_1.expected | 1 + ops/optimizer_tests/incompatible_1.rs | 9 + ops/optimizer_tests/op_state.expected | 8 + ops/optimizer_tests/op_state.out | 34 + ops/optimizer_tests/op_state.rs | 3 + ops/optimizer_tests/op_state_basic1.expected | 8 + ops/optimizer_tests/op_state_basic1.out | 35 + ops/optimizer_tests/op_state_basic1.rs | 3 + .../op_state_generics.expected | 8 + ops/optimizer_tests/op_state_generics.out | 39 + ops/optimizer_tests/op_state_generics.rs | 5 + ops/optimizer_tests/op_state_result.expected | 8 + ops/optimizer_tests/op_state_result.out | 42 + ops/optimizer_tests/op_state_result.rs | 3 + .../op_state_with_transforms.expected | 8 + .../op_state_with_transforms.out | 47 ++ .../op_state_with_transforms.rs | 5 + .../opstate_with_arity.expected | 8 + ops/optimizer_tests/opstate_with_arity.out | 44 + ops/optimizer_tests/opstate_with_arity.rs | 3 + .../param_mut_binding_warning.expected | 1 + .../param_mut_binding_warning.rs | 11 + ops/optimizer_tests/serde_v8_value.expected | 8 + ops/optimizer_tests/serde_v8_value.out | 26 + ops/optimizer_tests/serde_v8_value.rs | 3 + ops/optimizer_tests/u64_result.expected | 1 + ops/optimizer_tests/u64_result.rs | 5 + ops/tests/compile_fail/unsupported.stderr | 53 +- tools/lint.js | 10 +- 39 files changed, 1707 insertions(+), 653 deletions(-) create mode 100644 ops/attrs.rs create mode 100644 ops/deno.rs create mode 100644 ops/fast_call.rs create mode 100644 ops/optimizer.rs create mode 100644 ops/optimizer_tests/callback_options.expected create mode 100644 ops/optimizer_tests/callback_options.out create mode 100644 ops/optimizer_tests/callback_options.rs create mode 100644 ops/optimizer_tests/incompatible_1.expected create mode 100644 ops/optimizer_tests/incompatible_1.rs create mode 100644 ops/optimizer_tests/op_state.expected create mode 100644 ops/optimizer_tests/op_state.out create mode 100644 ops/optimizer_tests/op_state.rs create mode 100644 ops/optimizer_tests/op_state_basic1.expected create mode 100644 ops/optimizer_tests/op_state_basic1.out create mode 100644 ops/optimizer_tests/op_state_basic1.rs create mode 100644 ops/optimizer_tests/op_state_generics.expected create mode 100644 ops/optimizer_tests/op_state_generics.out create mode 100644 ops/optimizer_tests/op_state_generics.rs create mode 100644 ops/optimizer_tests/op_state_result.expected create mode 100644 ops/optimizer_tests/op_state_result.out create mode 100644 ops/optimizer_tests/op_state_result.rs create mode 100644 ops/optimizer_tests/op_state_with_transforms.expected create mode 100644 ops/optimizer_tests/op_state_with_transforms.out create mode 100644 ops/optimizer_tests/op_state_with_transforms.rs create mode 100644 ops/optimizer_tests/opstate_with_arity.expected create mode 100644 ops/optimizer_tests/opstate_with_arity.out create mode 100644 ops/optimizer_tests/opstate_with_arity.rs create mode 100644 ops/optimizer_tests/param_mut_binding_warning.expected create mode 100644 ops/optimizer_tests/param_mut_binding_warning.rs create mode 100644 ops/optimizer_tests/serde_v8_value.expected create mode 100644 ops/optimizer_tests/serde_v8_value.out create mode 100644 ops/optimizer_tests/serde_v8_value.rs create mode 100644 ops/optimizer_tests/u64_result.expected create mode 100644 ops/optimizer_tests/u64_result.rs diff --git a/Cargo.lock b/Cargo.lock index 1b4a242eca..df49cb67be 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1166,11 +1166,14 @@ version = "0.36.0" dependencies = [ "deno_core", "once_cell", + "pmutil", + "prettyplease", "proc-macro-crate", "proc-macro2 1.0.43", "quote 1.0.21", "regex", "syn 1.0.99", + "testing_macros", "trybuild", ] @@ -3337,6 +3340,16 @@ dependencies = [ "yansi", ] +[[package]] +name = "prettyplease" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c142c0e46b57171fe0c528bee8c5b7569e80f0c17e377cd0e30ea57dbc11bb51" +dependencies = [ + "proc-macro2 1.0.43", + "syn 1.0.99", +] + [[package]] name = "proc-macro-crate" version = "1.2.1" @@ -4759,6 +4772,23 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "testing_macros" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e74ff09d2d4d4b7ea140ff67eb7ed8fd35a708e2c327bcde5a25707d66840099" +dependencies = [ + "anyhow", + "glob", + "once_cell", + "pmutil", + "proc-macro2 1.0.43", + "quote 1.0.21", + "regex", + "relative-path", + "syn 1.0.99", +] + [[package]] name = "text-size" version = "1.1.0" diff --git a/ops/Cargo.toml b/ops/Cargo.toml index 408d597d10..56c2f916ab 100644 --- a/ops/Cargo.toml +++ b/ops/Cargo.toml @@ -12,6 +12,7 @@ proc-macro = true [dependencies] once_cell = "1.10.0" +pmutil = "0.5.3" proc-macro-crate = "1.1.3" proc-macro2 = "1" quote = "1" @@ -20,4 +21,6 @@ syn = { version = "1", features = ["full", "extra-traits"] } [dev-dependencies] deno_core = { path = "../core" } +prettyplease = "0.1.21" +testing_macros = "0.2.7" trybuild = "1.0.61" diff --git a/ops/attrs.rs b/ops/attrs.rs new file mode 100644 index 0000000000..95374ef368 --- /dev/null +++ b/ops/attrs.rs @@ -0,0 +1,37 @@ +// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. +use syn::{ + parse::{Parse, ParseStream}, + punctuated::Punctuated, + Error, Ident, Result, Token, +}; + +#[derive(Copy, Clone, Debug, Default)] +pub struct Attributes { + pub is_unstable: bool, + pub is_v8: bool, + pub must_be_fast: bool, + pub deferred: bool, +} + +impl Parse for Attributes { + fn parse(input: ParseStream) -> Result { + let vars = Punctuated::::parse_terminated(input)?; + + let vars: Vec<_> = vars.iter().map(Ident::to_string).collect(); + let vars: Vec<_> = vars.iter().map(String::as_str).collect(); + for var in vars.iter() { + if !["unstable", "v8", "fast", "deferred"].contains(var) { + return Err(Error::new( + input.span(), + "invalid attribute, expected one of: unstable, v8, fast, deferred", + )); + } + } + Ok(Self { + is_unstable: vars.contains(&"unstable"), + is_v8: vars.contains(&"v8"), + must_be_fast: vars.contains(&"fast"), + deferred: vars.contains(&"deferred"), + }) + } +} diff --git a/ops/deno.rs b/ops/deno.rs new file mode 100644 index 0000000000..67af603e99 --- /dev/null +++ b/ops/deno.rs @@ -0,0 +1,32 @@ +// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. + +use proc_macro2::{Span, TokenStream}; +use proc_macro_crate::{crate_name, FoundCrate}; +use quote::quote; +use syn::Ident; + +/// Identifier to the `deno_core` crate. +/// +/// If macro called in deno_core, `crate` is used. +/// If macro called outside deno_core, `deno_core` OR the renamed +/// version from Cargo.toml is used. +pub(crate) fn import() -> TokenStream { + let found_crate = + crate_name("deno_core").expect("deno_core not present in `Cargo.toml`"); + + match found_crate { + FoundCrate::Itself => { + // TODO(@littledivy): This won't work for `deno_core` examples + // since `crate` does not refer to `deno_core`. + // examples must re-export deno_core to make this work + // until Span inspection APIs are stabalized. + // + // https://github.com/rust-lang/rust/issues/54725 + quote!(crate) + } + FoundCrate::Name(name) => { + let ident = Ident::new(&name, Span::call_site()); + quote!(#ident) + } + } +} diff --git a/ops/fast_call.rs b/ops/fast_call.rs new file mode 100644 index 0000000000..4b5ba6e9b5 --- /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 { ... } + let struct_generics = exclude_lifetime_params(&generics.params); + // std::marker::PhantomData + let phantom_generics: Quote = match struct_generics { + Some(ref params) => q!(Vars { params }, { params }), + None => q!({ <()> }), + }; + // op_foo_fast_fn :: + 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 { + // _phantom: ::std::marker::PhantomData, + // } + 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::>(); + + // 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::>(); + + // 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` 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::::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 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 (_: Local, 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, 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 fast_api::FastFunction for T 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 }), + 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, +) -> Option { + let params = generic_params + .iter() + .filter(|t| !matches!(t, GenericParam::Lifetime(_))) + .cloned() + .collect::>(); + 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); + } + } +} diff --git a/ops/lib.rs b/ops/lib.rs index 44f7832803..7d4e77f90d 100644 --- a/ops/lib.rs +++ b/ops/lib.rs @@ -1,189 +1,192 @@ // Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. -use core::panic; +use attrs::Attributes; use once_cell::sync::Lazy; +use optimizer::{BailoutReason, Optimizer}; use proc_macro::TokenStream; -use proc_macro2::Span; -use proc_macro2::TokenStream as TokenStream2; -use proc_macro_crate::crate_name; -use proc_macro_crate::FoundCrate; -use quote::format_ident; -use quote::quote; -use quote::ToTokens; +use proc_macro2::{Span, TokenStream as TokenStream2}; +use quote::{quote, ToTokens}; use regex::Regex; -use std::collections::HashMap; -use syn::punctuated::Punctuated; -use syn::token::Comma; -use syn::FnArg; -use syn::GenericParam; -use syn::Ident; +use syn::{ + parse, parse_macro_input, punctuated::Punctuated, token::Comma, FnArg, + GenericParam, Ident, ItemFn, Lifetime, LifetimeDef, +}; + +mod attrs; +mod deno; +mod fast_call; +mod optimizer; #[cfg(test)] mod tests; -// Identifier to the `deno_core` crate. -// -// If macro called in deno_core, `crate` is used. -// If macro called outside deno_core, `deno_core` OR the renamed -// version from Cargo.toml is used. -fn core_import() -> TokenStream2 { - let found_crate = - crate_name("deno_core").expect("deno_core not present in `Cargo.toml`"); +const SCOPE_LIFETIME: &str = "'scope"; - match found_crate { - FoundCrate::Itself => { - // TODO(@littledivy): This won't work for `deno_core` examples - // since `crate` does not refer to `deno_core`. - // examples must re-export deno_core to make this work - // until Span inspection APIs are stabalized. - // - // https://github.com/rust-lang/rust/issues/54725 - quote!(crate) - } - FoundCrate::Name(name) => { - let ident = Ident::new(&name, Span::call_site()); - quote!(#ident) - } +/// Add the 'scope lifetime to the function signature. +fn add_scope_lifetime(func: &mut ItemFn) { + let span = Span::call_site(); + let lifetime = LifetimeDef::new(Lifetime::new(SCOPE_LIFETIME, span)); + let generics = &mut func.sig.generics; + if !generics.lifetimes().any(|def| *def == lifetime) { + generics.params.push(GenericParam::Lifetime(lifetime)); } } -#[derive(Copy, Clone, Debug, Default)] -struct MacroArgs { - is_unstable: bool, - is_v8: bool, - must_be_fast: bool, - deferred: bool, +struct Op { + orig: ItemFn, + item: ItemFn, + /// Is this an async op? + /// - `async fn` + /// - returns a Future + is_async: bool, + type_params: Punctuated, + // optimizer: Optimizer, + core: TokenStream2, + attrs: Attributes, } -impl syn::parse::Parse for MacroArgs { - fn parse(input: syn::parse::ParseStream) -> syn::Result { - let vars = - syn::punctuated::Punctuated::::parse_terminated( - input, - )?; - let vars: Vec<_> = vars.iter().map(Ident::to_string).collect(); - let vars: Vec<_> = vars.iter().map(String::as_str).collect(); - for var in vars.iter() { - if !["unstable", "v8", "fast", "deferred"].contains(var) { - return Err(syn::Error::new( - input.span(), - "Ops expect #[op] or #[op(unstable)]", - )); - } +impl Op { + fn new(mut item: ItemFn, attrs: Attributes) -> Self { + add_scope_lifetime(&mut item); + + // Preserve the original function. Change the name to `call`. + // + // impl op_foo { + // fn call() {} + // ... + // } + let mut orig = item.clone(); + orig.sig.ident = Ident::new("call", Span::call_site()); + + let is_async = item.sig.asyncness.is_some() || is_future(&item.sig.output); + let type_params = exclude_lifetime_params(&item.sig.generics.params); + let core = deno::import(); + + Self { + orig, + item, + type_params, + is_async, + core, + attrs, + } + } + + fn gen(mut self) -> TokenStream2 { + let mut optimizer = Optimizer::new(); + match optimizer.analyze(&mut self) { + Ok(_) | Err(BailoutReason::MustBeSingleSegment) => {} + Err(BailoutReason::FastUnsupportedParamType) => { + optimizer.fast_compatible = false; + } + Err(err) => return quote!(compile_error!(#err);), + }; + + let Self { + core, + item, + is_async, + orig, + attrs, + type_params, + } = self; + let name = &item.sig.ident; + let generics = &item.sig.generics; + let where_clause = &item.sig.generics.where_clause; + + // First generate fast call bindings to opt-in to error handling in slow call + let fast_call::FastImplItems { + impl_and_fn, + decl, + active, + } = fast_call::generate(&core, &mut optimizer, &item); + + let has_fallible_fast_call = active && optimizer.returns_result; + + let (v8_body, argc) = if is_async { + codegen_v8_async( + &core, + &item, + attrs, + item.sig.asyncness.is_some(), + attrs.deferred, + ) + } else { + codegen_v8_sync(&core, &item, attrs, has_fallible_fast_call) + }; + + let is_v8 = attrs.is_v8; + let is_unstable = attrs.is_unstable; + + let docline = format!("Use `{name}::decl()` to get an op-declaration"); + // Generate wrapper + quote! { + #[allow(non_camel_case_types)] + #[doc="Auto-generated by `deno_ops`, i.e: `#[op]`"] + #[doc=""] + #[doc=#docline] + #[doc="you can include in a `deno_core::Extension`."] + pub struct #name; + + #[doc(hidden)] + impl #name { + pub fn name() -> &'static str { + stringify!(#name) + } + + pub fn v8_fn_ptr #generics () -> #core::v8::FunctionCallback #where_clause { + use #core::v8::MapFnTo; + Self::v8_func::<#type_params>.map_fn_to() + } + + pub fn decl #generics () -> #core::OpDecl #where_clause { + #core::OpDecl { + name: Self::name(), + v8_fn_ptr: Self::v8_fn_ptr::<#type_params>(), + enabled: true, + fast_fn: #decl, + is_async: #is_async, + is_unstable: #is_unstable, + is_v8: #is_v8, + argc: #argc, + } + } + + #[inline] + #[allow(clippy::too_many_arguments)] + #orig + + pub fn v8_func #generics ( + scope: &mut #core::v8::HandleScope<'scope>, + args: #core::v8::FunctionCallbackArguments, + mut rv: #core::v8::ReturnValue, + ) #where_clause { + #v8_body + } + } + + #impl_and_fn } - Ok(Self { - is_unstable: vars.contains(&"unstable"), - is_v8: vars.contains(&"v8"), - must_be_fast: vars.contains(&"fast"), - deferred: vars.contains(&"deferred"), - }) } } #[proc_macro_attribute] pub fn op(attr: TokenStream, item: TokenStream) -> TokenStream { - let margs = syn::parse_macro_input!(attr as MacroArgs); - let MacroArgs { - is_unstable, - is_v8, - must_be_fast, - deferred, - } = margs; - let func = syn::parse::(item).expect("expected a function"); - let name = &func.sig.ident; - let mut generics = func.sig.generics.clone(); - let scope_lifetime = - syn::LifetimeDef::new(syn::Lifetime::new("'scope", Span::call_site())); - if !generics.lifetimes().any(|def| *def == scope_lifetime) { - generics - .params - .push(syn::GenericParam::Lifetime(scope_lifetime)); - } - let type_params = exclude_lifetime_params(&func.sig.generics.params); - let where_clause = &func.sig.generics.where_clause; - - // Preserve the original func as op_foo::call() - let original_func = { - let mut func = func.clone(); - func.sig.ident = quote::format_ident!("call"); - func - }; - - let core = core_import(); - - let asyncness = func.sig.asyncness.is_some(); - let is_async = asyncness || is_future(&func.sig.output); - - // First generate fast call bindings to opt-in to error handling in slow call - let (has_fallible_fast_call, fast_impl, fast_field) = - codegen_fast_impl(&core, &func, name, is_async, must_be_fast); - - let (v8_body, argc) = if is_async { - codegen_v8_async(&core, &func, margs, asyncness, deferred) - } else { - codegen_v8_sync(&core, &func, margs, has_fallible_fast_call) - }; - - let docline = format!("Use `{name}::decl()` to get an op-declaration"); - // Generate wrapper - quote! { - #[allow(non_camel_case_types)] - #[doc="Auto-generated by `deno_ops`, i.e: `#[op]`"] - #[doc=""] - #[doc=#docline] - #[doc="you can include in a `deno_core::Extension`."] - pub struct #name; - - #[doc(hidden)] - impl #name { - pub fn name() -> &'static str { - stringify!(#name) - } - - pub fn v8_fn_ptr #generics () -> #core::v8::FunctionCallback #where_clause { - use #core::v8::MapFnTo; - Self::v8_func::<#type_params>.map_fn_to() - } - - pub fn decl #generics () -> #core::OpDecl #where_clause { - #core::OpDecl { - name: Self::name(), - v8_fn_ptr: Self::v8_fn_ptr::<#type_params>(), - enabled: true, - fast_fn: #fast_field, - is_async: #is_async, - is_unstable: #is_unstable, - is_v8: #is_v8, - argc: #argc, - } - } - - #[inline] - #[allow(clippy::too_many_arguments)] - #original_func - - pub fn v8_func #generics ( - scope: &mut #core::v8::HandleScope<'scope>, - args: #core::v8::FunctionCallbackArguments, - mut rv: #core::v8::ReturnValue, - ) #where_clause { - #v8_body - } - } - - #fast_impl - }.into() + let margs = parse_macro_input!(attr as Attributes); + let func = parse::(item).expect("expected a function"); + let op = Op::new(func, margs); + op.gen().into() } /// Generate the body of a v8 func for an async op fn codegen_v8_async( core: &TokenStream2, f: &syn::ItemFn, - margs: MacroArgs, + margs: Attributes, asyncness: bool, deferred: bool, ) -> (TokenStream2, usize) { - let MacroArgs { is_v8, .. } = margs; + let Attributes { is_v8, .. } = margs; let special_args = f .sig .inputs @@ -287,241 +290,14 @@ fn opstate_arg(arg: &FnArg) -> Option { } } -fn codegen_fast_impl( - core: &TokenStream2, - f: &syn::ItemFn, - name: &syn::Ident, - is_async: bool, - must_be_fast: bool, -) -> (bool, TokenStream2, TokenStream2) { - if is_async { - if must_be_fast { - panic!("async op cannot be a fast api. enforced by #[op(fast)]") - } - return (false, quote! {}, quote! { None }); - } - let fast_info = can_be_fast_api(core, f); - if must_be_fast && fast_info.is_none() { - panic!("op cannot be a fast api. enforced by #[op(fast)]") - } - if !is_async { - if let Some(FastApiSyn { - args, - ret, - use_op_state, - use_fast_cb_opts, - v8_values, - returns_result, - slices, - }) = fast_info - { - let offset = if use_op_state { 1 } else { 0 }; - let mut inputs = f - .sig - .inputs - .iter() - .skip(offset) - .enumerate() - .map(|(idx, arg)| { - let ident = match arg { - FnArg::Receiver(_) => unreachable!(), - FnArg::Typed(t) => match &*t.pat { - syn::Pat::Ident(i) => format_ident!("{}", i.ident), - _ => unreachable!(), - }, - }; - if let Some(ty) = slices.get(&(idx + offset)) { - return quote! { #ident: *const #core::v8::fast_api::FastApiTypedArray< #ty > }; - } - if use_fast_cb_opts && idx + offset == f.sig.inputs.len() - 1 { - return quote! { fast_api_callback_options: *mut #core::v8::fast_api::FastApiCallbackOptions }; - } - if v8_values.contains(&idx) { - return quote! { #ident: #core::v8::Local < #core::v8::Value > }; - } - quote!(#arg) - }) - .collect::>(); - if (!slices.is_empty() || use_op_state || returns_result) - && !use_fast_cb_opts - { - inputs.push(quote! { fast_api_callback_options: *mut #core::v8::fast_api::FastApiCallbackOptions }); - } - let input_idents = f - .sig - .inputs - .iter() - .enumerate() - .map(|(idx, a)| { - let ident = match a { - FnArg::Receiver(_) => unreachable!(), - FnArg::Typed(t) => match &*t.pat { - syn::Pat::Ident(i) => format_ident!("{}", i.ident), - _ => unreachable!(), - }, - }; - if slices.get(&idx).is_some() { - return quote! { - match unsafe { &* #ident }.get_storage_if_aligned() { - Some(s) => s, - None => { - unsafe { &mut * fast_api_callback_options }.fallback = true; - return Default::default(); - }, - } - }; - } - if use_fast_cb_opts && idx == f.sig.inputs.len() - 1 { - return quote! { Some(unsafe { &mut * fast_api_callback_options }) }; - } - if v8_values.contains(&idx) { - return quote! { - #core::serde_v8::Value { - v8_value: #ident, - } - }; - } - quote! { #ident } - }) - .collect::>(); - let generics = &f.sig.generics; - let (impl_generics, ty_generics, where_clause) = - generics.split_for_impl(); - let type_params = exclude_lifetime_params(&f.sig.generics.params); - let (trampoline, raw_block) = if is_async { - // TODO(@littledivy): Fast async calls. - ( - quote! { - fn func(recv: #core::v8::Local<#core::v8::Object>, __promise_id: u32, #(#inputs),*) { - // SAFETY: V8 calling convention guarantees that the callback options pointer is non-null. - let opts: &#core::v8::fast_api::FastApiCallbackOptions = unsafe { &*fast_api_callback_options }; - // SAFETY: data union is always created as the `v8::Local` version - let data = unsafe { opts.data.data }; - // SAFETY: #core guarantees data is a v8 External pointing to an OpCtx for the isolates lifetime - let ctx = unsafe { - &*(#core::v8::Local::<#core::v8::External>::cast(data).value() - as *const #core::_ops::OpCtx) - }; - let op_id = ctx.op_id; - #core::_ops::queue_async_op(scope, async move { - let result = Self::call(#args); - (__promise_id, __op_id, #core::_ops::OpResult::Ok(result)) - }); - } - func as *const _ - }, - quote! {}, - ) - } else { - let output = if returns_result { - get_fast_result_return_type(&f.sig.output) - } else { - let output = &f.sig.output; - quote! { #output } - }; - let func_name = format_ident!("func_{}", name); - let op_state_name = if use_op_state { - input_idents.first().unwrap().clone() - } else { - quote! { op_state } - }; - let recv_decl = if use_op_state || returns_result { - quote! { - // SAFETY: V8 calling convention guarantees that the callback options pointer is non-null. - let opts: &mut #core::v8::fast_api::FastApiCallbackOptions = unsafe { &mut *fast_api_callback_options }; - // SAFETY: data union is always created as the `v8::Local` version. - let data = unsafe { opts.data.data }; - // SAFETY: #core guarantees data is a v8 External pointing to an OpCtx for the isolates lifetime - let ctx = unsafe { - &*(#core::v8::Local::<#core::v8::External>::cast(data).value() - as *const #core::_ops::OpCtx) - }; - let #op_state_name = &mut std::cell::RefCell::borrow_mut(&ctx.state); - } - } else { - quote! {} - }; - - let result_handling = if returns_result { - quote! { - match result { - Ok(result) => { - result - }, - Err(err) => { - #op_state_name.last_fast_op_error.replace(err); - opts.fallback = true; - Default::default() - }, - } - } - } else { - quote! { result } - }; - - ( - quote! { - fn #func_name #generics (_recv: #core::v8::Local<#core::v8::Object>, #(#inputs),*) #output #where_clause { - #recv_decl - let result = #name::call::<#type_params>(#(#input_idents),*); - #result_handling - } - }, - quote! { - #func_name::<#type_params> as *const _ - }, - ) - }; - - let fast_struct = format_ident!("fast_{}", name); - let (type_params, ty_generics, struct_generics) = - if type_params.is_empty() { - (quote! { () }, quote! {}, quote! {}) - } else { - ( - quote! { #type_params }, - quote! { #ty_generics }, - quote! { ::<#type_params> }, - ) - }; - return ( - returns_result, - quote! { - #[allow(non_camel_case_types)] - #[doc(hidden)] - struct #fast_struct #ty_generics { - _phantom: ::std::marker::PhantomData<#type_params>, - } - #trampoline - impl #impl_generics #core::v8::fast_api::FastFunction for #fast_struct #ty_generics #where_clause { - fn function(&self) -> *const ::std::ffi::c_void { - #raw_block - } - fn args(&self) -> &'static [#core::v8::fast_api::Type] { - &[ #args ] - } - fn return_type(&self) -> #core::v8::fast_api::CType { - #ret - } - } - }, - quote! { Some(Box::new(#fast_struct #struct_generics { _phantom: ::std::marker::PhantomData })) }, - ); - } - } - - // Default impl to satisfy generic bounds for non-fast ops - (false, quote! {}, quote! { None }) -} - /// Generate the body of a v8 func for a sync op fn codegen_v8_sync( core: &TokenStream2, f: &syn::ItemFn, - margs: MacroArgs, + margs: Attributes, has_fallible_fast_call: bool, ) -> (TokenStream2, usize) { - let MacroArgs { is_v8, .. } = margs; + let Attributes { is_v8, .. } = margs; let special_args = f .sig .inputs @@ -574,242 +350,6 @@ fn codegen_v8_sync( ) } -struct FastApiSyn { - args: TokenStream2, - ret: TokenStream2, - use_op_state: bool, - use_fast_cb_opts: bool, - v8_values: Vec, - returns_result: bool, - slices: HashMap, -} - -fn can_be_fast_api(core: &TokenStream2, f: &syn::ItemFn) -> Option { - let inputs = &f.sig.inputs; - let mut returns_result = false; - let ret = match &f.sig.output { - syn::ReturnType::Default => quote!(#core::v8::fast_api::CType::Void), - syn::ReturnType::Type(_, ty) => match is_fast_return_type(core, ty) { - Some((ret, is_result)) => { - returns_result = is_result; - ret - } - None => return None, - }, - }; - - let mut use_op_state = false; - let mut use_fast_cb_opts = false; - let mut v8_values = Vec::new(); - let mut slices = HashMap::new(); - let mut args = vec![quote! { #core::v8::fast_api::Type::V8Value }]; - for (pos, input) in inputs.iter().enumerate() { - if pos == inputs.len() - 1 && is_optional_fast_callback_option(input) { - use_fast_cb_opts = true; - continue; - } - - if pos == 0 && is_mut_ref_opstate(input) { - use_op_state = true; - continue; - } - - let ty = match input { - syn::FnArg::Typed(pat) => &pat.ty, - _ => unreachable!(), - }; - - if let Some(arg) = is_fast_v8_value(core, ty) { - args.push(arg); - v8_values.push(pos); - } else { - match is_fast_scalar(core, ty, false) { - None => match is_fast_arg_sequence(core, ty) { - Some(arg) => { - args.push(arg); - } - None => match is_ref_slice(&ty) { - Some(SliceType::U32Mut) => { - args.push(quote! { #core::v8::fast_api::Type::TypedArray(#core::v8::fast_api::CType::Uint32) }); - slices.insert(pos, quote!(u32)); - } - Some(_) => { - args.push(quote! { #core::v8::fast_api::Type::TypedArray(#core::v8::fast_api::CType::Uint8) }); - slices.insert(pos, quote!(u8)); - } - // early return, this function cannot be a fast call. - None => return None, - }, - }, - Some(arg) => { - args.push(arg); - } - } - } - } - - if use_fast_cb_opts || use_op_state { - // Push CallbackOptions into args; it must be the last argument. - args.push(quote! { #core::v8::fast_api::Type::CallbackOptions }); - } - - let args = args - .iter() - .map(|arg| format!("{}", arg)) - .collect::>() - .join(", "); - Some(FastApiSyn { - args: args.parse().unwrap(), - ret, - use_op_state, - slices, - v8_values, - use_fast_cb_opts, - returns_result, - }) -} - -// A v8::Local or FastApiTypedArray -fn is_fast_arg_sequence( - core: &TokenStream2, - ty: impl ToTokens, -) -> Option { - // TODO(@littledivy): Make `v8::` parts optional. - if is_fast_typed_array(&ty) { - return Some( - quote! { #core::v8::fast_api::Type::TypedArray(#core::v8::fast_api::CType::Uint32) }, - ); - } - if is_local_array(&ty) { - return Some( - quote! { #core::v8::fast_api::Type::Sequence(#core::v8::fast_api::CType::Void) }, - ); - } - None -} - -fn is_fast_v8_value( - core: &TokenStream2, - arg: impl ToTokens, -) -> Option { - if tokens(&arg).contains("serde_v8 :: Value") { - return Some(quote! { #core::v8::fast_api::Type::V8Value }); - } - None -} - -fn is_local_array(arg: impl ToTokens) -> bool { - static RE: Lazy = - Lazy::new(|| Regex::new(r"^v8::Local$").unwrap()); - RE.is_match(&tokens(arg)) -} - -fn is_fast_typed_array(arg: impl ToTokens) -> bool { - static RE: Lazy = Lazy::new(|| { - Regex::new(r#": (?:deno_core :: )?FastApiTypedArray$"#).unwrap() - }); - RE.is_match(&tokens(arg)) -} - -fn is_fast_return_type( - core: &TokenStream2, - ty: impl ToTokens, -) -> Option<(TokenStream2, bool)> { - if is_result(&ty) { - if tokens(&ty).contains("Result < u32") || is_resource_id(&ty) { - Some((quote! { #core::v8::fast_api::CType::Uint32 }, true)) - } else if tokens(&ty).contains("Result < i32") { - Some((quote! { #core::v8::fast_api::CType::Int32 }, true)) - } else if tokens(&ty).contains("Result < f32") { - Some((quote! { #core::v8::fast_api::CType::Float32 }, true)) - } else if tokens(&ty).contains("Result < f64") { - Some((quote! { #core::v8::fast_api::CType::Float64 }, true)) - } else if tokens(&ty).contains("Result < bool") { - Some((quote! { #core::v8::fast_api::CType::Bool }, true)) - } else if tokens(&ty).contains("Result < ()") { - Some((quote! { #core::v8::fast_api::CType::Void }, true)) - } else { - None - } - } else { - is_fast_scalar(core, ty, true).map(|s| (s, false)) - } -} - -fn get_fast_result_return_type(ty: impl ToTokens) -> TokenStream2 { - if tokens(&ty).contains("Result < u32") || is_resource_id(&ty) { - quote! { -> u32 } - } else if tokens(&ty).contains("Result < i32") { - quote! { -> i32 } - } else if tokens(&ty).contains("Result < f32") { - quote! { -> f32 } - } else if tokens(&ty).contains("Result < f64") { - quote! { -> f64 } - } else if tokens(&ty).contains("Result < bool") { - quote! { -> bool } - } else if tokens(&ty).contains("Result < ()") { - quote! {} - } else { - unreachable!() - } -} - -fn is_fast_scalar( - core: &TokenStream2, - ty: impl ToTokens, - is_ret: bool, -) -> Option { - let cty = if is_ret { - quote! { CType } - } else { - quote! { Type } - }; - if is_resource_id(&ty) { - return Some(quote! { #core::v8::fast_api::#cty::Uint32 }); - } - if is_void(&ty) { - return Some(quote! { #core::v8::fast_api::#cty::Void }); - } - // TODO(@littledivy): Support u8, i8, u16, i16 by casting. - match tokens(&ty).as_str() { - "u32" => Some(quote! { #core::v8::fast_api::#cty::Uint32 }), - "i32" => Some(quote! { #core::v8::fast_api::#cty::Int32 }), - "u64" => { - if is_ret { - None - } else { - Some(quote! { #core::v8::fast_api::#cty::Uint64 }) - } - } - "i64" => { - if is_ret { - None - } else { - Some(quote! { #core::v8::fast_api::#cty::Int64 }) - } - } - // TODO(@aapoalas): Support 32 bit machines - "usize" => { - if is_ret { - None - } else { - Some(quote! { #core::v8::fast_api::#cty::Uint64 }) - } - } - "isize" => { - if is_ret { - None - } else { - Some(quote! { #core::v8::fast_api::#cty::Int64 }) - } - } - "f32" => Some(quote! { #core::v8::fast_api::#cty::Float32 }), - "f64" => Some(quote! { #core::v8::fast_api::#cty::Float64 }), - "bool" => Some(quote! { #core::v8::fast_api::#cty::Bool }), - _ => None, - } -} - /// (full declarations, idents, v8 argument count) type ArgumentDecl = (TokenStream2, TokenStream2, usize); diff --git a/ops/optimizer.rs b/ops/optimizer.rs new file mode 100644 index 0000000000..3e38875492 --- /dev/null +++ b/ops/optimizer.rs @@ -0,0 +1,600 @@ +/// Optimizer for #[op] +use crate::Op; +use pmutil::{q, Quote}; +use proc_macro2::TokenStream; +use quote::{quote, ToTokens}; +use std::collections::HashMap; +use std::fmt::Debug; +use std::fmt::Formatter; +use syn::{ + parse_quote, punctuated::Punctuated, token::Colon2, + AngleBracketedGenericArguments, FnArg, GenericArgument, PatType, Path, + PathArguments, PathSegment, ReturnType, Signature, Type, TypePath, + TypeReference, TypeSlice, +}; + +#[derive(Debug)] +pub(crate) enum BailoutReason { + // Recoverable errors + MustBeSingleSegment, + FastUnsupportedParamType, + + FastAsync, +} + +impl ToTokens for BailoutReason { + fn to_tokens(&self, tokens: &mut TokenStream) { + match self { + BailoutReason::FastAsync => { + tokens.extend(quote! { "fast async calls are not supported" }); + } + BailoutReason::MustBeSingleSegment + | BailoutReason::FastUnsupportedParamType => { + unreachable!("error not recovered"); + } + } + } +} + +#[derive(Debug, PartialEq)] +enum TransformKind { + // serde_v8::Value + V8Value, + SliceU32(bool), + SliceU8(bool), +} + +impl Transform { + fn serde_v8_value(index: usize) -> Self { + Transform { + kind: TransformKind::V8Value, + index, + } + } + + fn slice_u32(index: usize, is_mut: bool) -> Self { + Transform { + kind: TransformKind::SliceU32(is_mut), + index, + } + } + + fn slice_u8(index: usize, is_mut: bool) -> Self { + Transform { + kind: TransformKind::SliceU8(is_mut), + index, + } + } +} + +#[derive(Debug, PartialEq)] +pub(crate) struct Transform { + kind: TransformKind, + index: usize, +} + +impl Transform { + pub(crate) fn apply_for_fast_call( + &self, + core: &TokenStream, + input: &mut FnArg, + ) -> Quote { + let (ty, ident) = match input { + FnArg::Typed(PatType { + ref mut ty, + ref pat, + .. + }) => { + let ident = match &**pat { + syn::Pat::Ident(ident) => &ident.ident, + _ => unreachable!("error not recovered"), + }; + (ty, ident) + } + _ => unreachable!("error not recovered"), + }; + + match &self.kind { + // serde_v8::Value + TransformKind::V8Value => { + *ty = parse_quote! { #core::v8::Local }; + + q!(Vars { var: &ident }, { + let var = serde_v8::Value { v8_value: var }; + }) + } + // &[u32] + TransformKind::SliceU32(_) => { + *ty = + parse_quote! { *const #core::v8::fast_api::FastApiTypedArray }; + + q!(Vars { var: &ident }, { + let var = match unsafe { &*var }.get_storage_if_aligned() { + Some(v) => v, + None => { + unsafe { &mut *fast_api_callback_options }.fallback = true; + return Default::default(); + } + }; + }) + } + // &[u8] + TransformKind::SliceU8(_) => { + *ty = + parse_quote! { *const #core::v8::fast_api::FastApiTypedArray }; + + q!(Vars { var: &ident }, { + let var = match unsafe { &*var }.get_storage_if_aligned() { + Some(v) => v, + None => { + unsafe { &mut *fast_api_callback_options }.fallback = true; + return Default::default(); + } + }; + }) + } + } + } +} + +fn get_fast_scalar(s: &str) -> Option { + match s { + "u32" => Some(FastValue::U32), + "i32" => Some(FastValue::I32), + "u64" => Some(FastValue::U64), + "i64" => Some(FastValue::I64), + "f32" => Some(FastValue::F32), + "f64" => Some(FastValue::F64), + "bool" => Some(FastValue::Bool), + "ResourceId" => Some(FastValue::U32), + _ => None, + } +} + +fn can_return_fast(v: &FastValue) -> bool { + !matches!( + v, + FastValue::U64 + | FastValue::I64 + | FastValue::Uint8Array + | FastValue::Uint32Array + ) +} + +#[derive(Debug, PartialEq, Clone)] +pub(crate) enum FastValue { + Void, + U32, + I32, + U64, + I64, + F32, + F64, + Bool, + V8Value, + Uint8Array, + Uint32Array, +} + +impl Default for FastValue { + fn default() -> Self { + Self::Void + } +} + +#[derive(Default, PartialEq)] +pub(crate) struct Optimizer { + pub(crate) returns_result: bool, + + pub(crate) has_ref_opstate: bool, + + pub(crate) has_rc_opstate: bool, + + pub(crate) has_fast_callback_option: bool, + + pub(crate) fast_result: Option, + pub(crate) fast_parameters: Vec, + + pub(crate) transforms: HashMap, + pub(crate) fast_compatible: bool, +} + +impl Debug for Optimizer { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + writeln!(f, "=== Optimizer Dump ===")?; + writeln!(f, "returns_result: {}", self.returns_result)?; + writeln!(f, "has_ref_opstate: {}", self.has_ref_opstate)?; + writeln!(f, "has_rc_opstate: {}", self.has_rc_opstate)?; + writeln!( + f, + "has_fast_callback_option: {}", + self.has_fast_callback_option + )?; + writeln!(f, "fast_result: {:?}", self.fast_result)?; + writeln!(f, "fast_parameters: {:?}", self.fast_parameters)?; + writeln!(f, "transforms: {:?}", self.transforms)?; + Ok(()) + } +} + +impl Optimizer { + pub(crate) fn new() -> Self { + Default::default() + } + + pub(crate) const fn has_opstate_in_parameters(&self) -> bool { + self.has_ref_opstate || self.has_rc_opstate + } + + pub(crate) const fn needs_opstate(&self) -> bool { + self.has_ref_opstate || self.has_rc_opstate || self.returns_result + } + + pub(crate) fn analyze(&mut self, op: &mut Op) -> Result<(), BailoutReason> { + if op.is_async && op.attrs.must_be_fast { + self.fast_compatible = false; + return Err(BailoutReason::FastAsync); + } + + if op.attrs.is_v8 || op.is_async { + self.fast_compatible = false; + return Ok(()); + } + + self.fast_compatible = true; + let sig = &op.item.sig; + + // Analyze return type + match &sig { + Signature { + output: ReturnType::Default, + .. + } => self.fast_result = Some(FastValue::default()), + Signature { + output: ReturnType::Type(_, ty), + .. + } => self.analyze_return_type(ty)?, + }; + + // The reciever, which we don't actually care about. + self.fast_parameters.push(FastValue::V8Value); + + // Analyze parameters + for (index, param) in sig.inputs.iter().enumerate() { + self.analyze_param_type(index, param)?; + } + + Ok(()) + } + + fn analyze_return_type(&mut self, ty: &Type) -> Result<(), BailoutReason> { + match ty { + Type::Path(TypePath { + path: Path { segments, .. }, + .. + }) => { + let segment = single_segment(segments)?; + + match segment { + // Result + PathSegment { + ident, arguments, .. + } if ident == "Result" => { + self.returns_result = true; + + if let PathArguments::AngleBracketed( + AngleBracketedGenericArguments { args, .. }, + ) = arguments + { + match args.first() { + Some(GenericArgument::Type(Type::Path(TypePath { + path: Path { segments, .. }, + .. + }))) => { + let PathSegment { ident, .. } = single_segment(segments)?; + // Is `T` a scalar FastValue? + if let Some(val) = get_fast_scalar(ident.to_string().as_str()) + { + if can_return_fast(&val) { + self.fast_result = Some(val); + return Ok(()); + } + } + + self.fast_compatible = false; + return Err(BailoutReason::FastUnsupportedParamType); + } + _ => return Err(BailoutReason::FastUnsupportedParamType), + } + } + } + // Is `T` a scalar FastValue? + PathSegment { ident, .. } => { + if let Some(val) = get_fast_scalar(ident.to_string().as_str()) { + self.fast_result = Some(val); + return Ok(()); + } + + self.fast_compatible = false; + return Err(BailoutReason::FastUnsupportedParamType); + } + }; + } + _ => return Err(BailoutReason::FastUnsupportedParamType), + }; + + Ok(()) + } + + fn analyze_param_type( + &mut self, + index: usize, + arg: &FnArg, + ) -> Result<(), BailoutReason> { + match arg { + FnArg::Typed(typed) => match &*typed.ty { + Type::Path(TypePath { + path: Path { segments, .. }, + .. + }) if segments.len() == 2 => { + match double_segment(segments)? { + // -> serde_v8::Value + [PathSegment { ident: first, .. }, PathSegment { ident: last, .. }] + if first == "serde_v8" && last == "Value" => + { + self.fast_parameters.push(FastValue::V8Value); + assert!(self + .transforms + .insert(index, Transform::serde_v8_value(index)) + .is_none()); + } + _ => return Err(BailoutReason::FastUnsupportedParamType), + } + } + Type::Path(TypePath { + path: Path { segments, .. }, + .. + }) => { + let segment = single_segment(segments)?; + + match segment { + // -> Option + PathSegment { + ident, arguments, .. + } if ident == "Option" => { + if let PathArguments::AngleBracketed( + AngleBracketedGenericArguments { args, .. }, + ) = arguments + { + // -> Option<&mut T> + if let Some(GenericArgument::Type(Type::Reference( + TypeReference { elem, .. }, + ))) = args.last() + { + if let Type::Path(TypePath { + path: Path { segments, .. }, + .. + }) = &**elem + { + let segment = single_segment(segments)?; + match segment { + // Is `T` a FastApiCallbackOption? + PathSegment { ident, .. } + if ident == "FastApiCallbackOption" => + { + self.has_fast_callback_option = true; + } + _ => {} + } + } + } + } + } + // -> Rc + PathSegment { + ident, arguments, .. + } if ident == "Rc" => { + if let PathArguments::AngleBracketed( + AngleBracketedGenericArguments { args, .. }, + ) = arguments + { + match args.last() { + Some(GenericArgument::Type(Type::Path(TypePath { + path: Path { segments, .. }, + .. + }))) => { + let segment = single_segment(segments)?; + match segment { + // -> Rc> + PathSegment { ident, .. } if ident == "RefCell" => { + if let PathArguments::AngleBracketed( + AngleBracketedGenericArguments { args, .. }, + ) = arguments + { + match args.last() { + // -> Rc> + Some(GenericArgument::Type(Type::Path( + TypePath { + path: Path { segments, .. }, + .. + }, + ))) => { + let segment = single_segment(segments)?; + match segment { + PathSegment { ident, .. } + if ident == "OpState" => + { + self.has_rc_opstate = true; + } + _ => { + return Err( + BailoutReason::FastUnsupportedParamType, + ) + } + } + } + _ => { + return Err( + BailoutReason::FastUnsupportedParamType, + ) + } + } + } + } + _ => return Err(BailoutReason::FastUnsupportedParamType), + } + } + _ => return Err(BailoutReason::FastUnsupportedParamType), + } + } + } + // Is `T` a fast scalar? + PathSegment { ident, .. } => { + if let Some(val) = get_fast_scalar(ident.to_string().as_str()) { + self.fast_parameters.push(val); + } else { + return Err(BailoutReason::FastUnsupportedParamType); + } + } + }; + } + // &mut T + Type::Reference(TypeReference { + elem, mutability, .. + }) => match &**elem { + Type::Path(TypePath { + path: Path { segments, .. }, + .. + }) => { + let segment = single_segment(segments)?; + match segment { + // Is `T` a OpState? + PathSegment { ident, .. } if ident == "OpState" => { + self.has_ref_opstate = true; + } + _ => return Err(BailoutReason::FastUnsupportedParamType), + } + } + // &mut [T] + Type::Slice(TypeSlice { elem, .. }) => match &**elem { + Type::Path(TypePath { + path: Path { segments, .. }, + .. + }) => { + let segment = single_segment(segments)?; + let is_mut_ref = mutability.is_some(); + match segment { + // Is `T` a u8? + PathSegment { ident, .. } if ident == "u8" => { + self.has_fast_callback_option = true; + self.fast_parameters.push(FastValue::Uint8Array); + assert!(self + .transforms + .insert(index, Transform::slice_u8(index, is_mut_ref)) + .is_none()); + } + // Is `T` a u32? + PathSegment { ident, .. } if ident == "u32" => { + self.has_fast_callback_option = true; + self.fast_parameters.push(FastValue::Uint32Array); + assert!(self + .transforms + .insert(index, Transform::slice_u32(index, is_mut_ref)) + .is_none()); + } + _ => return Err(BailoutReason::FastUnsupportedParamType), + } + } + _ => return Err(BailoutReason::FastUnsupportedParamType), + }, + _ => return Err(BailoutReason::FastUnsupportedParamType), + }, + _ => return Err(BailoutReason::FastUnsupportedParamType), + }, + _ => return Err(BailoutReason::FastUnsupportedParamType), + }; + Ok(()) + } +} + +fn single_segment( + segments: &Punctuated, +) -> Result<&PathSegment, BailoutReason> { + if segments.len() != 1 { + return Err(BailoutReason::MustBeSingleSegment); + } + + match segments.last() { + Some(segment) => Ok(segment), + None => Err(BailoutReason::MustBeSingleSegment), + } +} + +fn double_segment( + segments: &Punctuated, +) -> Result<[&PathSegment; 2], BailoutReason> { + match (segments.first(), segments.last()) { + (Some(first), Some(last)) => Ok([first, last]), + // Caller ensures that there are only two segments. + _ => unreachable!(), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Op; + use std::path::PathBuf; + use syn::parse_quote; + + #[test] + fn test_single_segment() { + let segments = parse_quote!(foo); + assert!(single_segment(&segments).is_ok()); + + let segments = parse_quote!(foo::bar); + assert!(single_segment(&segments).is_err()); + } + + #[test] + fn test_double_segment() { + let segments = parse_quote!(foo::bar); + assert!(double_segment(&segments).is_ok()); + assert_eq!(double_segment(&segments).unwrap()[0].ident, "foo"); + assert_eq!(double_segment(&segments).unwrap()[1].ident, "bar"); + } + + #[testing_macros::fixture("optimizer_tests/**/*.rs")] + fn test_analyzer(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"); + let expected = std::fs::read_to_string(input.with_extension("expected")) + .expect("Failed to read expected 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 let Err(e) = optimizer.analyze(&mut op) { + let e_str = format!("{:?}", e); + if update_expected { + std::fs::write(input.with_extension("expected"), e_str) + .expect("Failed to write expected file"); + } else { + assert_eq!(e_str, expected); + } + return; + } + + if update_expected { + std::fs::write( + input.with_extension("expected"), + format!("{:#?}", optimizer), + ) + .expect("Failed to write expected file"); + } else { + assert_eq!(format!("{:#?}", optimizer), expected); + } + } +} diff --git a/ops/optimizer_tests/callback_options.expected b/ops/optimizer_tests/callback_options.expected new file mode 100644 index 0000000000..063032bb5e --- /dev/null +++ b/ops/optimizer_tests/callback_options.expected @@ -0,0 +1,8 @@ +=== Optimizer Dump === +returns_result: false +has_ref_opstate: false +has_rc_opstate: false +has_fast_callback_option: false +fast_result: Some(Void) +fast_parameters: [V8Value] +transforms: {} diff --git a/ops/optimizer_tests/callback_options.out b/ops/optimizer_tests/callback_options.out new file mode 100644 index 0000000000..426fe0c4aa --- /dev/null +++ b/ops/optimizer_tests/callback_options.out @@ -0,0 +1,25 @@ +struct op_fallback_fast { + _phantom: ::std::marker::PhantomData<()>, +} +impl<'scope> deno_core::v8::fast_api::FastFunction for op_fallback_fast { + fn function(&self) -> *const ::std::ffi::c_void { + op_fallback_fast_fn as *const ::std::ffi::c_void + } + fn args(&self) -> &'static [deno_core::v8::fast_api::Type] { + use deno_core::v8::fast_api::Type::*; + use deno_core::v8::fast_api::CType; + &[V8Value] + } + fn return_type(&self) -> deno_core::v8::fast_api::CType { + deno_core::v8::fast_api::CType::Void + } +} +fn op_fallback_fast_fn<'scope>( + _: deno_core::v8::Local, + options: Option<&mut FastApiCallbackOptions>, +) -> () { + use deno_core::v8; + use deno_core::_ops; + let result = op_fallback::call(options); + result +} diff --git a/ops/optimizer_tests/callback_options.rs b/ops/optimizer_tests/callback_options.rs new file mode 100644 index 0000000000..c210171d21 --- /dev/null +++ b/ops/optimizer_tests/callback_options.rs @@ -0,0 +1,5 @@ +fn op_fallback(options: Option<&mut FastApiCallbackOptions>) { + if let Some(options) = options { + options.fallback = true; + } +} diff --git a/ops/optimizer_tests/incompatible_1.expected b/ops/optimizer_tests/incompatible_1.expected new file mode 100644 index 0000000000..250ff1022d --- /dev/null +++ b/ops/optimizer_tests/incompatible_1.expected @@ -0,0 +1 @@ +FastUnsupportedParamType \ No newline at end of file diff --git a/ops/optimizer_tests/incompatible_1.rs b/ops/optimizer_tests/incompatible_1.rs new file mode 100644 index 0000000000..326189aa19 --- /dev/null +++ b/ops/optimizer_tests/incompatible_1.rs @@ -0,0 +1,9 @@ +fn op_sync_serialize_object_with_numbers_as_keys( + value: serde_json::Value, +) -> Result<(), Error> { + assert_eq!( + value.to_string(), + r#"{"lines":{"100":{"unit":"m"},"200":{"unit":"cm"}}}"# + ); + Ok(()) +} diff --git a/ops/optimizer_tests/op_state.expected b/ops/optimizer_tests/op_state.expected new file mode 100644 index 0000000000..f23bf764ac --- /dev/null +++ b/ops/optimizer_tests/op_state.expected @@ -0,0 +1,8 @@ +=== Optimizer Dump === +returns_result: false +has_ref_opstate: true +has_rc_opstate: false +has_fast_callback_option: false +fast_result: Some(Void) +fast_parameters: [V8Value, I32] +transforms: {} diff --git a/ops/optimizer_tests/op_state.out b/ops/optimizer_tests/op_state.out new file mode 100644 index 0000000000..a98db68d84 --- /dev/null +++ b/ops/optimizer_tests/op_state.out @@ -0,0 +1,34 @@ +struct op_set_exit_code_fast { + _phantom: ::std::marker::PhantomData<()>, +} +impl<'scope> deno_core::v8::fast_api::FastFunction for op_set_exit_code_fast { + fn function(&self) -> *const ::std::ffi::c_void { + op_set_exit_code_fast_fn as *const ::std::ffi::c_void + } + fn args(&self) -> &'static [deno_core::v8::fast_api::Type] { + use deno_core::v8::fast_api::Type::*; + use deno_core::v8::fast_api::CType; + &[V8Value, Int32, CallbackOptions] + } + fn return_type(&self) -> deno_core::v8::fast_api::CType { + deno_core::v8::fast_api::CType::Void + } +} +fn op_set_exit_code_fast_fn<'scope>( + _: deno_core::v8::Local, + code: i32, + fast_api_callback_options: *mut deno_core::v8::fast_api::FastApiCallbackOptions, +) -> () { + use deno_core::v8; + use deno_core::_ops; + let __opts: &mut v8::fast_api::FastApiCallbackOptions = unsafe { + &mut *fast_api_callback_options + }; + let __ctx = unsafe { + &*(v8::Local::::cast(unsafe { __opts.data.data }).value() + as *const _ops::OpCtx) + }; + let state = &mut ::std::cell::RefCell::borrow_mut(&__ctx.state); + let result = op_set_exit_code::call(state, code); + result +} diff --git a/ops/optimizer_tests/op_state.rs b/ops/optimizer_tests/op_state.rs new file mode 100644 index 0000000000..04e9a886df --- /dev/null +++ b/ops/optimizer_tests/op_state.rs @@ -0,0 +1,3 @@ +fn op_set_exit_code(state: &mut OpState, code: i32) { + state.borrow_mut::().set(code); +} diff --git a/ops/optimizer_tests/op_state_basic1.expected b/ops/optimizer_tests/op_state_basic1.expected new file mode 100644 index 0000000000..3639959b88 --- /dev/null +++ b/ops/optimizer_tests/op_state_basic1.expected @@ -0,0 +1,8 @@ +=== Optimizer Dump === +returns_result: false +has_ref_opstate: true +has_rc_opstate: false +has_fast_callback_option: false +fast_result: Some(U32) +fast_parameters: [V8Value, U32, U32] +transforms: {} diff --git a/ops/optimizer_tests/op_state_basic1.out b/ops/optimizer_tests/op_state_basic1.out new file mode 100644 index 0000000000..0f03f2c586 --- /dev/null +++ b/ops/optimizer_tests/op_state_basic1.out @@ -0,0 +1,35 @@ +struct foo_fast { + _phantom: ::std::marker::PhantomData<()>, +} +impl<'scope> deno_core::v8::fast_api::FastFunction for foo_fast { + fn function(&self) -> *const ::std::ffi::c_void { + foo_fast_fn as *const ::std::ffi::c_void + } + fn args(&self) -> &'static [deno_core::v8::fast_api::Type] { + use deno_core::v8::fast_api::Type::*; + use deno_core::v8::fast_api::CType; + &[V8Value, Uint32, Uint32, CallbackOptions] + } + fn return_type(&self) -> deno_core::v8::fast_api::CType { + deno_core::v8::fast_api::CType::Uint32 + } +} +fn foo_fast_fn<'scope>( + _: deno_core::v8::Local, + a: u32, + b: u32, + fast_api_callback_options: *mut deno_core::v8::fast_api::FastApiCallbackOptions, +) -> u32 { + use deno_core::v8; + use deno_core::_ops; + let __opts: &mut v8::fast_api::FastApiCallbackOptions = unsafe { + &mut *fast_api_callback_options + }; + let __ctx = unsafe { + &*(v8::Local::::cast(unsafe { __opts.data.data }).value() + as *const _ops::OpCtx) + }; + let state = &mut ::std::cell::RefCell::borrow_mut(&__ctx.state); + let result = foo::call(state, a, b); + result +} diff --git a/ops/optimizer_tests/op_state_basic1.rs b/ops/optimizer_tests/op_state_basic1.rs new file mode 100644 index 0000000000..9c89b41ceb --- /dev/null +++ b/ops/optimizer_tests/op_state_basic1.rs @@ -0,0 +1,3 @@ +fn foo(state: &mut OpState, a: u32, b: u32) -> u32 { + a + b +} diff --git a/ops/optimizer_tests/op_state_generics.expected b/ops/optimizer_tests/op_state_generics.expected new file mode 100644 index 0000000000..83e9385027 --- /dev/null +++ b/ops/optimizer_tests/op_state_generics.expected @@ -0,0 +1,8 @@ +=== Optimizer Dump === +returns_result: false +has_ref_opstate: true +has_rc_opstate: false +has_fast_callback_option: false +fast_result: Some(Void) +fast_parameters: [V8Value] +transforms: {} diff --git a/ops/optimizer_tests/op_state_generics.out b/ops/optimizer_tests/op_state_generics.out new file mode 100644 index 0000000000..d141c74457 --- /dev/null +++ b/ops/optimizer_tests/op_state_generics.out @@ -0,0 +1,39 @@ +struct op_foo_fast { + _phantom: ::std::marker::PhantomData, +} +impl<'scope, SP> deno_core::v8::fast_api::FastFunction for op_foo_fast +where + SP: SomePermission + 'static, +{ + fn function(&self) -> *const ::std::ffi::c_void { + op_foo_fast_fn:: as *const ::std::ffi::c_void + } + fn args(&self) -> &'static [deno_core::v8::fast_api::Type] { + use deno_core::v8::fast_api::Type::*; + use deno_core::v8::fast_api::CType; + &[V8Value, CallbackOptions] + } + fn return_type(&self) -> deno_core::v8::fast_api::CType { + deno_core::v8::fast_api::CType::Void + } +} +fn op_foo_fast_fn<'scope, SP>( + _: deno_core::v8::Local, + fast_api_callback_options: *mut deno_core::v8::fast_api::FastApiCallbackOptions, +) -> () +where + SP: SomePermission + 'static, +{ + use deno_core::v8; + use deno_core::_ops; + let __opts: &mut v8::fast_api::FastApiCallbackOptions = unsafe { + &mut *fast_api_callback_options + }; + let __ctx = unsafe { + &*(v8::Local::::cast(unsafe { __opts.data.data }).value() + as *const _ops::OpCtx) + }; + let state = &mut ::std::cell::RefCell::borrow_mut(&__ctx.state); + let result = op_foo::call::(state); + result +} diff --git a/ops/optimizer_tests/op_state_generics.rs b/ops/optimizer_tests/op_state_generics.rs new file mode 100644 index 0000000000..7fa498981e --- /dev/null +++ b/ops/optimizer_tests/op_state_generics.rs @@ -0,0 +1,5 @@ +pub fn op_foo(state: &mut OpState) +where + SP: SomePermission + 'static, +{ +} diff --git a/ops/optimizer_tests/op_state_result.expected b/ops/optimizer_tests/op_state_result.expected new file mode 100644 index 0000000000..16e71c38c7 --- /dev/null +++ b/ops/optimizer_tests/op_state_result.expected @@ -0,0 +1,8 @@ +=== Optimizer Dump === +returns_result: true +has_ref_opstate: true +has_rc_opstate: false +has_fast_callback_option: false +fast_result: Some(U32) +fast_parameters: [V8Value, U32, U32] +transforms: {} diff --git a/ops/optimizer_tests/op_state_result.out b/ops/optimizer_tests/op_state_result.out new file mode 100644 index 0000000000..5174dd7f2b --- /dev/null +++ b/ops/optimizer_tests/op_state_result.out @@ -0,0 +1,42 @@ +struct foo_fast { + _phantom: ::std::marker::PhantomData<()>, +} +impl<'scope> deno_core::v8::fast_api::FastFunction for foo_fast { + fn function(&self) -> *const ::std::ffi::c_void { + foo_fast_fn as *const ::std::ffi::c_void + } + fn args(&self) -> &'static [deno_core::v8::fast_api::Type] { + use deno_core::v8::fast_api::Type::*; + use deno_core::v8::fast_api::CType; + &[V8Value, Uint32, Uint32, CallbackOptions] + } + fn return_type(&self) -> deno_core::v8::fast_api::CType { + deno_core::v8::fast_api::CType::Uint32 + } +} +fn foo_fast_fn<'scope>( + _: deno_core::v8::Local, + a: u32, + b: u32, + fast_api_callback_options: *mut deno_core::v8::fast_api::FastApiCallbackOptions, +) -> u32 { + use deno_core::v8; + use deno_core::_ops; + let __opts: &mut v8::fast_api::FastApiCallbackOptions = unsafe { + &mut *fast_api_callback_options + }; + let __ctx = unsafe { + &*(v8::Local::::cast(unsafe { __opts.data.data }).value() + as *const _ops::OpCtx) + }; + let state = &mut ::std::cell::RefCell::borrow_mut(&__ctx.state); + let result = foo::call(state, a, b); + match result { + Ok(result) => result, + Err(err) => { + state.last_fast_op_error.replace(err); + __opts.fallback = true; + Default::default() + } + } +} diff --git a/ops/optimizer_tests/op_state_result.rs b/ops/optimizer_tests/op_state_result.rs new file mode 100644 index 0000000000..331005c08b --- /dev/null +++ b/ops/optimizer_tests/op_state_result.rs @@ -0,0 +1,3 @@ +fn foo(state: &mut OpState, a: u32, b: u32) -> Result { + Ok(a + b) +} diff --git a/ops/optimizer_tests/op_state_with_transforms.expected b/ops/optimizer_tests/op_state_with_transforms.expected new file mode 100644 index 0000000000..388d396f52 --- /dev/null +++ b/ops/optimizer_tests/op_state_with_transforms.expected @@ -0,0 +1,8 @@ +=== Optimizer Dump === +returns_result: false +has_ref_opstate: true +has_rc_opstate: false +has_fast_callback_option: true +fast_result: Some(Void) +fast_parameters: [V8Value, Uint8Array] +transforms: {1: Transform { kind: SliceU8(true), index: 1 }} diff --git a/ops/optimizer_tests/op_state_with_transforms.out b/ops/optimizer_tests/op_state_with_transforms.out new file mode 100644 index 0000000000..f981748be0 --- /dev/null +++ b/ops/optimizer_tests/op_state_with_transforms.out @@ -0,0 +1,47 @@ +struct op_now_fast { + _phantom: ::std::marker::PhantomData, +} +impl<'scope, TP> deno_core::v8::fast_api::FastFunction for op_now_fast +where + TP: TimersPermission + 'static, +{ + fn function(&self) -> *const ::std::ffi::c_void { + op_now_fast_fn:: as *const ::std::ffi::c_void + } + fn args(&self) -> &'static [deno_core::v8::fast_api::Type] { + use deno_core::v8::fast_api::Type::*; + use deno_core::v8::fast_api::CType; + &[V8Value, TypedArray(CType::Uint8), CallbackOptions] + } + fn return_type(&self) -> deno_core::v8::fast_api::CType { + deno_core::v8::fast_api::CType::Void + } +} +fn op_now_fast_fn<'scope, TP>( + _: deno_core::v8::Local, + buf: *const deno_core::v8::fast_api::FastApiTypedArray, + fast_api_callback_options: *mut deno_core::v8::fast_api::FastApiCallbackOptions, +) -> () +where + TP: TimersPermission + 'static, +{ + use deno_core::v8; + use deno_core::_ops; + let __opts: &mut v8::fast_api::FastApiCallbackOptions = unsafe { + &mut *fast_api_callback_options + }; + let __ctx = unsafe { + &*(v8::Local::::cast(unsafe { __opts.data.data }).value() + as *const _ops::OpCtx) + }; + let state = &mut ::std::cell::RefCell::borrow_mut(&__ctx.state); + let buf = match unsafe { &*buf }.get_storage_if_aligned() { + Some(v) => v, + None => { + unsafe { &mut *fast_api_callback_options }.fallback = true; + return Default::default(); + } + }; + let result = op_now::call::(state, buf); + result +} diff --git a/ops/optimizer_tests/op_state_with_transforms.rs b/ops/optimizer_tests/op_state_with_transforms.rs new file mode 100644 index 0000000000..4e7e616f3d --- /dev/null +++ b/ops/optimizer_tests/op_state_with_transforms.rs @@ -0,0 +1,5 @@ +pub fn op_now(state: &mut OpState, buf: &mut [u8]) +where + TP: TimersPermission + 'static, +{ +} diff --git a/ops/optimizer_tests/opstate_with_arity.expected b/ops/optimizer_tests/opstate_with_arity.expected new file mode 100644 index 0000000000..6259f3e284 --- /dev/null +++ b/ops/optimizer_tests/opstate_with_arity.expected @@ -0,0 +1,8 @@ +=== Optimizer Dump === +returns_result: true +has_ref_opstate: false +has_rc_opstate: false +has_fast_callback_option: false +fast_result: Some(U32) +fast_parameters: [V8Value, U32, U32, U32, U32] +transforms: {} diff --git a/ops/optimizer_tests/opstate_with_arity.out b/ops/optimizer_tests/opstate_with_arity.out new file mode 100644 index 0000000000..20b7769e75 --- /dev/null +++ b/ops/optimizer_tests/opstate_with_arity.out @@ -0,0 +1,44 @@ +struct op_add_4_fast { + _phantom: ::std::marker::PhantomData<()>, +} +impl<'scope> deno_core::v8::fast_api::FastFunction for op_add_4_fast { + fn function(&self) -> *const ::std::ffi::c_void { + op_add_4_fast_fn as *const ::std::ffi::c_void + } + fn args(&self) -> &'static [deno_core::v8::fast_api::Type] { + use deno_core::v8::fast_api::Type::*; + use deno_core::v8::fast_api::CType; + &[V8Value, Uint32, Uint32, Uint32, Uint32, CallbackOptions] + } + fn return_type(&self) -> deno_core::v8::fast_api::CType { + deno_core::v8::fast_api::CType::Uint32 + } +} +fn op_add_4_fast_fn<'scope>( + _: deno_core::v8::Local, + x1: u32, + x2: u32, + x3: u32, + x4: u32, + fast_api_callback_options: *mut deno_core::v8::fast_api::FastApiCallbackOptions, +) -> u32 { + use deno_core::v8; + use deno_core::_ops; + let __opts: &mut v8::fast_api::FastApiCallbackOptions = unsafe { + &mut *fast_api_callback_options + }; + let __ctx = unsafe { + &*(v8::Local::::cast(unsafe { __opts.data.data }).value() + as *const _ops::OpCtx) + }; + let op_state = &mut ::std::cell::RefCell::borrow_mut(&__ctx.state); + let result = op_add_4::call(x1, x2, x3, x4); + match result { + Ok(result) => result, + Err(err) => { + op_state.last_fast_op_error.replace(err); + __opts.fallback = true; + Default::default() + } + } +} diff --git a/ops/optimizer_tests/opstate_with_arity.rs b/ops/optimizer_tests/opstate_with_arity.rs new file mode 100644 index 0000000000..7212ca9752 --- /dev/null +++ b/ops/optimizer_tests/opstate_with_arity.rs @@ -0,0 +1,3 @@ +fn op_add_4(x1: u32, x2: u32, x3: u32, x4: u32) -> Result { + Ok(x1 + x2 + x3 + x4) +} diff --git a/ops/optimizer_tests/param_mut_binding_warning.expected b/ops/optimizer_tests/param_mut_binding_warning.expected new file mode 100644 index 0000000000..250ff1022d --- /dev/null +++ b/ops/optimizer_tests/param_mut_binding_warning.expected @@ -0,0 +1 @@ +FastUnsupportedParamType \ No newline at end of file diff --git a/ops/optimizer_tests/param_mut_binding_warning.rs b/ops/optimizer_tests/param_mut_binding_warning.rs new file mode 100644 index 0000000000..c47122728a --- /dev/null +++ b/ops/optimizer_tests/param_mut_binding_warning.rs @@ -0,0 +1,11 @@ +fn op_read_sync( + state: &mut OpState, + rid: ResourceId, + mut buf: ZeroCopyBuf, +) -> Result { + // Should not warn about unused `mut buf` binding. + // + // This was caused due to incorrect codegen by fast_call.rs + // on an incompatible op function. + Ok(23) +} diff --git a/ops/optimizer_tests/serde_v8_value.expected b/ops/optimizer_tests/serde_v8_value.expected new file mode 100644 index 0000000000..5acd38655d --- /dev/null +++ b/ops/optimizer_tests/serde_v8_value.expected @@ -0,0 +1,8 @@ +=== Optimizer Dump === +returns_result: false +has_ref_opstate: false +has_rc_opstate: false +has_fast_callback_option: false +fast_result: Some(Bool) +fast_parameters: [V8Value, V8Value] +transforms: {0: Transform { kind: V8Value, index: 0 }} diff --git a/ops/optimizer_tests/serde_v8_value.out b/ops/optimizer_tests/serde_v8_value.out new file mode 100644 index 0000000000..8c76305476 --- /dev/null +++ b/ops/optimizer_tests/serde_v8_value.out @@ -0,0 +1,26 @@ +struct op_is_proxy_fast { + _phantom: ::std::marker::PhantomData<()>, +} +impl<'scope> deno_core::v8::fast_api::FastFunction for op_is_proxy_fast { + fn function(&self) -> *const ::std::ffi::c_void { + op_is_proxy_fast_fn as *const ::std::ffi::c_void + } + fn args(&self) -> &'static [deno_core::v8::fast_api::Type] { + use deno_core::v8::fast_api::Type::*; + use deno_core::v8::fast_api::CType; + &[V8Value, V8Value] + } + fn return_type(&self) -> deno_core::v8::fast_api::CType { + deno_core::v8::fast_api::CType::Bool + } +} +fn op_is_proxy_fast_fn<'scope>( + _: deno_core::v8::Local, + value: deno_core::v8::Local, +) -> bool { + use deno_core::v8; + use deno_core::_ops; + let value = serde_v8::Value { v8_value: value }; + let result = op_is_proxy::call(value); + result +} diff --git a/ops/optimizer_tests/serde_v8_value.rs b/ops/optimizer_tests/serde_v8_value.rs new file mode 100644 index 0000000000..c986930d9a --- /dev/null +++ b/ops/optimizer_tests/serde_v8_value.rs @@ -0,0 +1,3 @@ +fn op_is_proxy(value: serde_v8::Value) -> bool { + value.v8_value.is_proxy() +} diff --git a/ops/optimizer_tests/u64_result.expected b/ops/optimizer_tests/u64_result.expected new file mode 100644 index 0000000000..250ff1022d --- /dev/null +++ b/ops/optimizer_tests/u64_result.expected @@ -0,0 +1 @@ +FastUnsupportedParamType \ No newline at end of file diff --git a/ops/optimizer_tests/u64_result.rs b/ops/optimizer_tests/u64_result.rs new file mode 100644 index 0000000000..1cc783db8b --- /dev/null +++ b/ops/optimizer_tests/u64_result.rs @@ -0,0 +1,5 @@ +fn op_bench_now(state: &mut OpState) -> Result { + let ns = state.borrow::().elapsed().as_nanos(); + let ns_u64 = u64::try_from(ns)?; + Ok(ns_u64) +} diff --git a/ops/tests/compile_fail/unsupported.stderr b/ops/tests/compile_fail/unsupported.stderr index 9a1d1462df..5dccead46c 100644 --- a/ops/tests/compile_fail/unsupported.stderr +++ b/ops/tests/compile_fail/unsupported.stderr @@ -1,39 +1,30 @@ -error: custom attribute panicked - --> tests/compile_fail/unsupported.rs:5:1 - | -5 | #[op(fast)] - | ^^^^^^^^^^^ - | - = help: message: op cannot be a fast api. enforced by #[op(fast)] - -error: custom attribute panicked - --> tests/compile_fail/unsupported.rs:10:1 - | -10 | #[op(fast)] - | ^^^^^^^^^^^ - | - = help: message: op cannot be a fast api. enforced by #[op(fast)] - -error: custom attribute panicked - --> tests/compile_fail/unsupported.rs:17:1 - | -17 | #[op(fast)] - | ^^^^^^^^^^^ - | - = help: message: op cannot be a fast api. enforced by #[op(fast)] - -error: custom attribute panicked +error: fast async calls are not supported --> tests/compile_fail/unsupported.rs:22:1 | 22 | #[op(fast)] | ^^^^^^^^^^^ | - = help: message: async op cannot be a fast api. enforced by #[op(fast)] + = note: this error originates in the attribute macro `op` (in Nightly builds, run with -Z macro-backtrace for more info) -warning: unused import: `deno_core::v8::fast_api::FastApiCallbackOptions` - --> tests/compile_fail/unsupported.rs:15:5 +error[E0277]: the trait bound `&mut FastApiCallbackOptions<'_>: Deserialize<'_>` is not satisfied + --> tests/compile_fail/unsupported.rs:17:1 | -15 | use deno_core::v8::fast_api::FastApiCallbackOptions; - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +17 | #[op(fast)] + | ^^^^^^^^^^^ the trait `Deserialize<'_>` is not implemented for `&mut FastApiCallbackOptions<'_>` | - = note: `#[warn(unused_imports)]` on by default + = help: the following other types implement trait `Deserialize<'de>`: + &'a Path + &'a [u8] + &'a str + () + (T0, T1) + (T0, T1, T2) + (T0, T1, T2, T3) + (T0, T1, T2, T3, T4) + and 143 others +note: required by a bound in `from_v8` + --> $WORKSPACE/serde_v8/de.rs + | + | T: Deserialize<'de>, + | ^^^^^^^^^^^^^^^^ required by this bound in `from_v8` + = note: this error originates in the attribute macro `op` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tools/lint.js b/tools/lint.js index 65f8888b70..11e912f49e 100755 --- a/tools/lint.js +++ b/tools/lint.js @@ -112,7 +112,15 @@ async function clippy() { } const { success } = await Deno.spawn("cargo", { - args: [...cmd, "--", "-D", "warnings"], + args: [ + ...cmd, + "--", + "-D", + "warnings", + "-A", + // https://github.com/rust-lang/rust-clippy/issues/407 + "clippy::extra_unused_lifetimes", + ], stdout: "inherit", stderr: "inherit", });