diff options
Diffstat (limited to 'ops/op2/mod.rs')
-rw-r--r-- | ops/op2/mod.rs | 287 |
1 files changed, 287 insertions, 0 deletions
diff --git a/ops/op2/mod.rs b/ops/op2/mod.rs new file mode 100644 index 000000000..73a457f25 --- /dev/null +++ b/ops/op2/mod.rs @@ -0,0 +1,287 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +use deno_proc_macro_rules::rules; +use proc_macro2::Ident; +use proc_macro2::Span; +use proc_macro2::TokenStream; +use quote::format_ident; +use quote::quote; +use quote::ToTokens; +use std::iter::zip; +use syn2::parse2; +use syn2::FnArg; +use syn2::ItemFn; +use syn2::Path; +use thiserror::Error; + +use self::dispatch_fast::generate_dispatch_fast; +use self::dispatch_slow::generate_dispatch_slow; +use self::generator_state::GeneratorState; +use self::signature::parse_signature; +use self::signature::Arg; +use self::signature::SignatureError; + +pub mod dispatch_fast; +pub mod dispatch_slow; +pub mod generator_state; +pub mod signature; + +#[derive(Debug, Error)] +pub enum Op2Error { + #[error("Failed to match a pattern for '{0}': (input was '{1}')")] + PatternMatchFailed(&'static str, String), + #[error("Invalid attribute: '{0}'")] + InvalidAttribute(String), + #[error("Failed to parse syntax tree")] + ParseError(#[from] syn2::Error), + #[error("Failed to map a parsed signature to a V8 call")] + V8MappingError(#[from] V8MappingError), + #[error("Failed to parse signature")] + SignatureError(#[from] SignatureError), + #[error("This op is fast-compatible and should be marked as (fast)")] + ShouldBeFast, + #[error("This op is not fast-compatible and should not be marked as (fast)")] + ShouldNotBeFast, +} + +#[derive(Debug, Error)] +pub enum V8MappingError { + #[error("Unable to map {1:?} to {0}")] + NoMapping(&'static str, Arg), +} + +#[derive(Default)] +struct MacroConfig { + pub core: bool, + pub fast: bool, +} + +impl MacroConfig { + pub fn from_flags(flags: Vec<Ident>) -> Result<Self, Op2Error> { + let mut config: MacroConfig = Self::default(); + for flag in flags { + if flag == "core" { + config.core = true; + } else if flag == "fast" { + config.fast = true; + } else { + return Err(Op2Error::InvalidAttribute(flag.to_string())); + } + } + Ok(config) + } + + pub fn from_tokens(tokens: TokenStream) -> Result<Self, Op2Error> { + let attr_string = tokens.to_string(); + let config = std::panic::catch_unwind(|| { + rules!(tokens => { + () => { + Ok(MacroConfig::default()) + } + ($($flags:ident),+) => { + Self::from_flags(flags) + } + }) + }) + .map_err(|_| Op2Error::PatternMatchFailed("attribute", attr_string))??; + Ok(config) + } +} + +pub fn op2( + attr: TokenStream, + item: TokenStream, +) -> Result<TokenStream, Op2Error> { + let func = parse2::<ItemFn>(item)?; + let config = MacroConfig::from_tokens(attr)?; + generate_op2(config, func) +} + +fn generate_op2( + config: MacroConfig, + func: ItemFn, +) -> Result<TokenStream, Op2Error> { + // Create a copy of the original function, named "call" + let call = Ident::new("call", Span::call_site()); + let mut op_fn = func.clone(); + op_fn.attrs.clear(); + op_fn.sig.ident = call.clone(); + + // Clear inert attributes + // TODO(mmastrac): This should limit itself to clearing ours only + for arg in op_fn.sig.inputs.iter_mut() { + match arg { + FnArg::Receiver(slf) => slf.attrs.clear(), + FnArg::Typed(ty) => ty.attrs.clear(), + } + } + + let signature = parse_signature(func.attrs, func.sig.clone())?; + let processed_args = + zip(signature.args.iter(), &func.sig.inputs).collect::<Vec<_>>(); + + let mut args = vec![]; + let mut needs_args = false; + for (index, _) in processed_args.iter().enumerate() { + let input = format_ident!("arg{index}"); + args.push(input); + needs_args = true; + } + + let retval = Ident::new("rv", Span::call_site()); + let result = Ident::new("result", Span::call_site()); + let fn_args = Ident::new("args", Span::call_site()); + let scope = Ident::new("scope", Span::call_site()); + let info = Ident::new("info", Span::call_site()); + let slow_function = Ident::new("slow_function", Span::call_site()); + let fast_function = Ident::new("fast_function", Span::call_site()); + + let deno_core = if config.core { + syn2::parse_str::<Path>("crate::deno_core") + } else { + syn2::parse_str::<Path>("deno_core") + } + .expect("Parsing crate should not fail") + .into_token_stream(); + + let mut generator_state = GeneratorState { + args, + fn_args, + call, + scope, + info, + deno_core, + result, + retval, + needs_args, + slow_function, + fast_function, + needs_retval: false, + needs_scope: false, + }; + + let name = func.sig.ident; + let slow_fn = generate_dispatch_slow(&mut generator_state, &signature)?; + let (fast_definition, fast_fn) = + match generate_dispatch_fast(&mut generator_state, &signature)? { + Some((fast_definition, fast_fn)) => { + if !config.fast { + return Err(Op2Error::ShouldBeFast); + } + (quote!(Some({#fast_definition})), fast_fn) + } + None => { + if config.fast { + return Err(Op2Error::ShouldNotBeFast); + } + (quote!(None), quote!()) + } + }; + + let GeneratorState { + deno_core, + slow_function, + .. + } = &generator_state; + + let arg_count: usize = generator_state.args.len(); + let vis = func.vis; + + Ok(quote! { + #[allow(non_camel_case_types)] + #vis struct #name { + } + + impl #name { + pub const fn name() -> &'static str { + stringify!(#name) + } + + pub const fn decl() -> #deno_core::_ops::OpDecl { + #deno_core::_ops::OpDecl { + name: stringify!(#name), + v8_fn_ptr: Self::#slow_function as _, + enabled: true, + fast_fn: #fast_definition, + is_async: false, + is_unstable: false, + is_v8: false, + arg_count: #arg_count as u8, + } + } + + #slow_fn + #fast_fn + + #[inline(always)] + #op_fn + } + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use pretty_assertions::assert_eq; + use std::path::PathBuf; + use syn2::parse_str; + use syn2::File; + use syn2::Item; + + #[testing_macros::fixture("op2/test_cases/**/*.rs")] + fn test_signature_parser(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 file = parse_str::<File>(&source).expect("Failed to parse Rust file"); + let mut expected_out = vec![]; + for item in file.items { + if let Item::Fn(mut func) = item { + let mut config = None; + func.attrs.retain(|attr| { + let tokens = attr.into_token_stream(); + let attr_string = attr.clone().into_token_stream().to_string(); + println!("{}", attr_string); + use syn2 as syn; + if let Some(new_config) = rules!(tokens => { + (#[op2]) => { + Some(MacroConfig::default()) + } + (#[op2( $($x:ident),* )]) => { + Some(MacroConfig::from_flags(x).expect("Failed to parse attribute")) + } + (#[$_attr:meta]) => { + None + } + }) { + config = Some(new_config); + false + } else { + true + } + }); + let tokens = + generate_op2(config.unwrap(), func).expect("Failed to generate op"); + println!("======== Raw tokens ========:\n{}", tokens.clone()); + let tree = syn::parse2(tokens).unwrap(); + let actual = prettyplease::unparse(&tree); + println!("======== Generated ========:\n{}", actual); + expected_out.push(actual); + } + } + + let expected_out = expected_out.join("\n"); + + if update_expected { + std::fs::write(input.with_extension("out"), expected_out) + .expect("Failed to write expectation file"); + } else { + let expected = std::fs::read_to_string(input.with_extension("out")) + .expect("Failed to read expectation file"); + assert_eq!( + expected, expected_out, + "Failed to match expectation. Use UPDATE_EXPECTED=1." + ); + } + } +} |