mirror of
https://github.com/denoland/deno.git
synced 2025-08-04 19:08:15 +00:00
refactor(ops): Rewrite fast call optimizer and codegen (#16514)
This commit is contained in:
parent
92764c0dec
commit
bc33a4b2e0
39 changed files with 1707 additions and 653 deletions
399
ops/fast_call.rs
Normal file
399
ops/fast_call.rs
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue