// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. //! Optimizer for #[op] use std::collections::BTreeMap; use std::fmt::Debug; use std::fmt::Formatter; use pmutil::q; use pmutil::Quote; use proc_macro2::TokenStream; use syn::parse_quote; use syn::punctuated::Punctuated; use syn::token::Colon2; use syn::AngleBracketedGenericArguments; use syn::FnArg; use syn::GenericArgument; use syn::PatType; use syn::Path; use syn::PathArguments; use syn::PathSegment; use syn::ReturnType; use syn::Signature; use syn::Type; use syn::TypePath; use syn::TypePtr; use syn::TypeReference; use syn::TypeSlice; use syn::TypeTuple; use crate::Op; #[derive(Debug)] pub(crate) enum BailoutReason { // Recoverable errors MustBeSingleSegment, FastUnsupportedParamType, } #[derive(Debug, PartialEq)] enum StringType { Cow, Ref, Owned, } #[derive(Debug, PartialEq)] enum TransformKind { // serde_v8::Value V8Value, SliceU32(bool), SliceU8(bool), SliceF64(bool), SeqOneByteString(StringType), PtrU8, PtrVoid, WasmMemory, } 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, } } fn slice_f64(index: usize, is_mut: bool) -> Self { Transform { kind: TransformKind::SliceF64(is_mut), index, } } fn seq_one_byte_string(index: usize, is_ref: StringType) -> Self { Transform { kind: TransformKind::SeqOneByteString(is_ref), index, } } fn wasm_memory(index: usize) -> Self { Transform { kind: TransformKind::WasmMemory, index, } } fn u8_ptr(index: usize) -> Self { Transform { kind: TransformKind::PtrU8, index, } } fn void_ptr(index: usize) -> Self { Transform { kind: TransformKind::PtrVoid, 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 }, { // V8 guarantees that ArrayBuffers are always 4-byte aligned // (seems to be always 8-byte aligned on 64-bit machines) // but Deno FFI makes it possible to create ArrayBuffers at any // alignment. Thus this check is needed. 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 }, { // SAFETY: U8 slice is always byte-aligned. let var = unsafe { (&*var).get_storage_if_aligned().unwrap_unchecked() }; }) } TransformKind::SliceF64(_) => { *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(); } }; }) } // &str TransformKind::SeqOneByteString(str_ty) => { *ty = parse_quote! { *const #core::v8::fast_api::FastApiOneByteString }; match str_ty { StringType::Ref => q!(Vars { var: &ident }, { let var = match ::std::str::from_utf8(unsafe { &*var }.as_bytes()) { Ok(v) => v, Err(_) => { unsafe { &mut *fast_api_callback_options }.fallback = true; return Default::default(); } }; }), StringType::Cow => q!(Vars { var: &ident }, { let var = ::std::borrow::Cow::Borrowed( match ::std::str::from_utf8(unsafe { &*var }.as_bytes()) { Ok(v) => v, Err(_) => { unsafe { &mut *fast_api_callback_options }.fallback = true; return Default::default(); } }, ); }), StringType::Owned => q!(Vars { var: &ident }, { let var = match ::std::str::from_utf8(unsafe { &*var }.as_bytes()) { Ok(v) => v.to_owned(), Err(_) => { unsafe { &mut *fast_api_callback_options }.fallback = true; return Default::default(); } }; }), } } TransformKind::WasmMemory => { // Note: `ty` is correctly set to __opts by the fast call tier. // U8 slice is always byte-aligned. q!(Vars { var: &ident, core }, { let var = unsafe { &*(__opts.wasm_memory as *const core::v8::fast_api::FastApiTypedArray) } .get_storage_if_aligned(); }) } // *const u8 TransformKind::PtrU8 => { *ty = parse_quote! { *const #core::v8::fast_api::FastApiTypedArray }; q!(Vars { var: &ident }, { // SAFETY: U8 slice is always byte-aligned. let var = unsafe { (&*var).get_storage_if_aligned().unwrap_unchecked() } .as_ptr(); }) } TransformKind::PtrVoid => { *ty = parse_quote! { *mut ::std::ffi::c_void }; q!(Vars {}, {}) } } } } fn get_fast_scalar(s: &str) -> Option { match s { "bool" => Some(FastValue::Bool), "u32" => Some(FastValue::U32), "i32" => Some(FastValue::I32), "u64" | "usize" => Some(FastValue::U64), "i64" | "isize" => Some(FastValue::I64), "f32" => Some(FastValue::F32), "f64" => Some(FastValue::F64), "* const c_void" | "* mut c_void" => Some(FastValue::Pointer), "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, Bool, U32, I32, U64, I64, F32, F64, Pointer, V8Value, Uint8Array, Uint32Array, Float64Array, SeqOneByteString, } impl FastValue { pub fn default_value(&self) -> Quote { match self { FastValue::Pointer => q!({ ::std::ptr::null_mut() }), FastValue::Void => q!({}), _ => q!({ Default::default() }), } } } 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, // Do we need an explict FastApiCallbackOptions argument? pub(crate) has_fast_callback_option: bool, // Do we depend on FastApiCallbackOptions? pub(crate) needs_fast_callback_option: bool, pub(crate) has_wasm_memory: bool, pub(crate) fast_result: Option, pub(crate) fast_parameters: Vec, pub(crate) transforms: BTreeMap, pub(crate) fast_compatible: bool, pub(crate) is_async: 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, "needs_fast_callback_option: {}", self.needs_fast_callback_option )?; writeln!(f, "fast_result: {:?}", self.fast_result)?; writeln!(f, "fast_parameters: {:?}", self.fast_parameters)?; writeln!(f, "transforms: {:?}", self.transforms)?; writeln!(f, "is_async: {}", self.is_async)?; writeln!(f, "fast_compatible: {}", self.fast_compatible)?; 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> { // Fast async ops are opt-in as they have a lazy polling behavior. if op.is_async && !op.attrs.must_be_fast { self.fast_compatible = false; return Ok(()); } if op.attrs.is_v8 { self.fast_compatible = false; return Ok(()); } self.is_async = op.is_async; self.fast_compatible = true; // Just assume for now. We will validate later. self.has_wasm_memory = op.attrs.is_wasm; 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), .. } if !self.is_async => self.analyze_return_type(ty)?, // No need to error on the return type for async ops, its OK if // it's not a fast value. Signature { output: ReturnType::Type(_, ty), .. } => { let _ = self.analyze_return_type(ty); // Recover. self.fast_result = None; self.fast_compatible = true; } }; // The reciever, which we don't actually care about. self.fast_parameters.push(FastValue::V8Value); if self.is_async { // The promise ID. self.fast_parameters.push(FastValue::I32); } // Analyze parameters for (index, param) in sig.inputs.iter().enumerate() { self.analyze_param_type(index, param)?; } // TODO(@littledivy): https://github.com/denoland/deno/issues/17159 if self.returns_result && self.fast_parameters.contains(&FastValue::SeqOneByteString) { self.fast_compatible = false; } Ok(()) } fn analyze_return_type(&mut self, ty: &Type) -> Result<(), BailoutReason> { match ty { Type::Tuple(TypeTuple { elems, .. }) if elems.is_empty() => { self.fast_result = Some(FastValue::Void); } 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); } Some(GenericArgument::Type(Type::Tuple(TypeTuple { elems, .. }))) if elems.is_empty() => { self.fast_result = Some(FastValue::Void); } Some(GenericArgument::Type(Type::Ptr(TypePtr { mutability: Some(_), elem, .. }))) => { match &**elem { Type::Path(TypePath { path: Path { segments, .. }, .. }) => { // Is `T` a c_void? let segment = single_segment(segments)?; match segment { PathSegment { ident, .. } if ident == "c_void" => { self.fast_result = Some(FastValue::Pointer); return Ok(()); } _ => { return Err(BailoutReason::FastUnsupportedParamType) } } } _ => 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); } }; } Type::Ptr(TypePtr { mutability: Some(_), elem, .. }) => { match &**elem { Type::Path(TypePath { path: Path { segments, .. }, .. }) => { // Is `T` a c_void? let segment = single_segment(segments)?; match segment { PathSegment { ident, .. } if ident == "c_void" => { self.fast_result = Some(FastValue::Pointer); return Ok(()); } _ => return Err(BailoutReason::FastUnsupportedParamType), } } _ => 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 self.has_wasm_memory { // -> Option<&mut [u8]> if let Type::Slice(TypeSlice { elem, .. }) = &**elem { if let Type::Path(TypePath { path: Path { segments, .. }, .. }) = &**elem { let segment = single_segment(segments)?; match segment { // Is `T` a u8? PathSegment { ident, .. } if ident == "u8" => { assert!(self .transforms .insert(index, Transform::wasm_memory(index)) .is_none()); } _ => { return Err(BailoutReason::FastUnsupportedParamType) } } } } } else if let Type::Path(TypePath { path: Path { segments, .. }, .. }) = &**elem { let segment = single_segment(segments)?; match segment { // Is `T` a FastApiCallbackOptions? PathSegment { ident, .. } if ident == "FastApiCallbackOptions" => { self.has_fast_callback_option = true; } _ => return Err(BailoutReason::FastUnsupportedParamType), } } else { return Err(BailoutReason::FastUnsupportedParamType); } } else { return Err(BailoutReason::FastUnsupportedParamType); } } } // -> 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, arguments, .. } 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), } } } // Cow<'_, str> PathSegment { ident, arguments, .. } if ident == "Cow" => { if let PathArguments::AngleBracketed( AngleBracketedGenericArguments { args, .. }, ) = arguments { assert_eq!(args.len(), 2); let ty = &args[1]; match ty { GenericArgument::Type(Type::Path(TypePath { path: Path { segments, .. }, .. })) => { let segment = single_segment(segments)?; match segment { PathSegment { ident, .. } if ident == "str" => { self.needs_fast_callback_option = true; self.fast_parameters.push(FastValue::SeqOneByteString); assert!(self .transforms .insert( index, Transform::seq_one_byte_string( index, StringType::Cow ) ) .is_none()); } _ => 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 if ident == "String" { self.needs_fast_callback_option = true; // Is `T` an owned String? self.fast_parameters.push(FastValue::SeqOneByteString); assert!(self .transforms .insert( index, Transform::seq_one_byte_string(index, StringType::Owned) ) .is_none()); } 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.is_async => { self.has_ref_opstate = true; } // Is `T` a str? PathSegment { ident, .. } if ident == "str" => { self.needs_fast_callback_option = true; self.fast_parameters.push(FastValue::SeqOneByteString); assert!(self .transforms .insert( index, Transform::seq_one_byte_string(index, StringType::Ref) ) .is_none()); } _ => 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.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.needs_fast_callback_option = true; self.fast_parameters.push(FastValue::Uint32Array); assert!(self .transforms .insert(index, Transform::slice_u32(index, is_mut_ref)) .is_none()); } // Is `T` a f64? PathSegment { ident, .. } if ident == "f64" => { self.needs_fast_callback_option = true; self.fast_parameters.push(FastValue::Float64Array); assert!(self .transforms .insert(index, Transform::slice_f64(index, is_mut_ref)) .is_none()); } _ => return Err(BailoutReason::FastUnsupportedParamType), } } _ => return Err(BailoutReason::FastUnsupportedParamType), }, _ => return Err(BailoutReason::FastUnsupportedParamType), }, // *const T Type::Ptr(TypePtr { elem, const_token: Some(_), .. }) => match &**elem { Type::Path(TypePath { path: Path { segments, .. }, .. }) => { let segment = single_segment(segments)?; match segment { // Is `T` a u8? PathSegment { ident, .. } if ident == "u8" => { self.fast_parameters.push(FastValue::Uint8Array); assert!(self .transforms .insert(index, Transform::u8_ptr(index)) .is_none()); } _ => return Err(BailoutReason::FastUnsupportedParamType), } } _ => return Err(BailoutReason::FastUnsupportedParamType), }, // *const T Type::Ptr(TypePtr { elem, mutability: Some(_), .. }) => match &**elem { Type::Path(TypePath { path: Path { segments, .. }, .. }) => { let segment = single_segment(segments)?; match segment { // Is `T` a c_void? PathSegment { ident, .. } if ident == "c_void" => { self.fast_parameters.push(FastValue::Pointer); assert!(self .transforms .insert(index, Transform::void_ptr(index)) .is_none()); } _ => 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::Attributes; 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 mut attrs = Attributes::default(); if source.contains("// @test-attr:wasm") { attrs.must_be_fast = true; attrs.is_wasm = true; } if source.contains("// @test-attr:fast") { attrs.must_be_fast = true; } let item = syn::parse_str(&source).expect("Failed to parse test file"); let mut op = Op::new(item, attrs); 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); } } }