From d01203cb5df7acd6066067e5483fa1dfda009be2 Mon Sep 17 00:00:00 2001 From: J Henry Waugh Date: Sat, 1 Aug 2020 11:52:26 -0500 Subject: [PATCH] New Procedural Macros Crate v0.1 --- .github/workflows/build.yml | 23 +- Cargo.toml | 5 +- README.md | 1 + codegen/Cargo.toml | 21 + codegen/src/function.rs | 746 ++++++++++++++++++++ codegen/src/lib.rs | 143 ++++ codegen/src/module.rs | 661 +++++++++++++++++ codegen/src/rhai_module.rs | 90 +++ codegen/tests/test_functions.rs | 141 ++++ codegen/tests/test_modules.rs | 165 +++++ codegen/ui_tests/first_shared_ref.rs | 22 + codegen/ui_tests/first_shared_ref.stderr | 11 + codegen/ui_tests/non_clonable.rs | 22 + codegen/ui_tests/non_clonable.stderr | 5 + codegen/ui_tests/non_clonable_second.rs | 22 + codegen/ui_tests/non_clonable_second.stderr | 5 + codegen/ui_tests/return_mut_ref.rs | 23 + codegen/ui_tests/return_mut_ref.stderr | 11 + codegen/ui_tests/return_pointer.rs | 19 + codegen/ui_tests/return_pointer.stderr | 11 + codegen/ui_tests/return_shared_ref.rs | 19 + codegen/ui_tests/return_shared_ref.stderr | 11 + codegen/ui_tests/second_shared_ref.rs | 23 + codegen/ui_tests/second_shared_ref.stderr | 11 + src/any.rs | 7 + src/fn_register.rs | 9 +- src/lib.rs | 2 + src/plugin.rs | 6 +- 28 files changed, 2230 insertions(+), 5 deletions(-) create mode 100644 codegen/Cargo.toml create mode 100644 codegen/src/function.rs create mode 100644 codegen/src/lib.rs create mode 100644 codegen/src/module.rs create mode 100644 codegen/src/rhai_module.rs create mode 100644 codegen/tests/test_functions.rs create mode 100644 codegen/tests/test_modules.rs create mode 100644 codegen/ui_tests/first_shared_ref.rs create mode 100644 codegen/ui_tests/first_shared_ref.stderr create mode 100644 codegen/ui_tests/non_clonable.rs create mode 100644 codegen/ui_tests/non_clonable.stderr create mode 100644 codegen/ui_tests/non_clonable_second.rs create mode 100644 codegen/ui_tests/non_clonable_second.stderr create mode 100644 codegen/ui_tests/return_mut_ref.rs create mode 100644 codegen/ui_tests/return_mut_ref.stderr create mode 100644 codegen/ui_tests/return_pointer.rs create mode 100644 codegen/ui_tests/return_pointer.stderr create mode 100644 codegen/ui_tests/return_shared_ref.rs create mode 100644 codegen/ui_tests/return_shared_ref.stderr create mode 100644 codegen/ui_tests/second_shared_ref.rs create mode 100644 codegen/ui_tests/second_shared_ref.stderr diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1370e03e..259d8c51 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,7 +18,6 @@ jobs: flags: - "" - "--features serde" - - "--features plugins" - "--features unchecked" - "--features sync" - "--features no_optimize" @@ -75,3 +74,25 @@ jobs: with: command: build args: --manifest-path=no_std/no_std_test/Cargo.toml ${{matrix.flags}} + codegen_build: + name: Codegen Build + runs-on: ${{matrix.os}} + continue-on-error: ${{matrix.experimental}} + strategy: + matrix: + include: + - {toolchain: nightly, os: ubuntu-latest, experimental: false, flags: ""} + - {toolchain: nightly, os: windows-latest, experimental: false, flags: ""} + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Setup Toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: ${{matrix.toolchain}} + override: true + - name: Build Project + uses: actions-rs/cargo@v1 + with: + command: test + args: --manifest-path=codegen/Cargo.toml ${{matrix.flags}} diff --git a/Cargo.toml b/Cargo.toml index ec00a10c..9a282a45 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,6 @@ smallvec = { version = "1.4.1", default-features = false } [features] #default = ["unchecked", "sync", "no_optimize", "no_float", "only_i32", "no_index", "no_object", "no_function", "no_module"] default = [] -plugins = [] unchecked = [] # unchecked arithmetic sync = [] # restrict to only types that implement Send + Sync no_optimize = [] # no script optimizer @@ -81,6 +80,10 @@ version = "0.2.1" default_features = false optional = true +[dependencies.rhai_codegen] +version = "0.1" +path = "codegen" + [target.'cfg(target_arch = "wasm32")'.dependencies] instant= { version = "0.1.4", features = ["wasm-bindgen"] } # WASM implementation of std::time::Instant diff --git a/README.md b/README.md index 7aef538c..c755d4b2 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ Supported targets and builds * All common CPU targets for Windows, Linux and MacOS. * WebAssembly (WASM) * `no-std` +* Minimum Rust version 1.45 Standard features ----------------- diff --git a/codegen/Cargo.toml b/codegen/Cargo.toml new file mode 100644 index 00000000..53aa7d31 --- /dev/null +++ b/codegen/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "rhai_codegen" +version = "0.1.0" +edition = "2018" +authors = ["Stephen Chung", "jhwgh1968"] +description = "Proceducral macro support package for Rhai, a scripting language for Rust" +homepage = "https://github.com/jonathandturner/rhai" +repository = "https://github.com/jonathandturner/rhai" +license = "MIT OR Apache-2.0" + +[lib] +proc-macro = true + +[dev-dependencies] +rhai = { version = "*", path = ".." } +trybuild = "1" + +[dependencies] +proc-macro2 = "1" +syn = { version = "1", features = ["full", "parsing", "printing", "proc-macro", "extra-traits"] } +quote = "1" diff --git a/codegen/src/function.rs b/codegen/src/function.rs new file mode 100644 index 00000000..46015a7c --- /dev/null +++ b/codegen/src/function.rs @@ -0,0 +1,746 @@ +#![allow(unused)] +use quote::{quote, quote_spanned}; +use syn::{parse::Parse, parse::ParseStream, spanned::Spanned}; + +#[derive(Debug)] +pub(crate) struct ExportedFn { + entire_span: proc_macro2::Span, + signature: syn::Signature, + is_public: bool, + mut_receiver: bool, +} + +impl Parse for ExportedFn { + fn parse(input: ParseStream) -> syn::Result { + let fn_all: syn::ItemFn = input.parse()?; + let entire_span = fn_all.span(); + let str_type_path = syn::parse2::(quote! { str }).unwrap(); + + // Determine if the function is public. + let is_public = match fn_all.vis { + syn::Visibility::Public(_) => true, + _ => false + }; + // Determine whether function generates a special calling convention for a mutable + // reciever. + let mut_receiver = { + if let Some(first_arg) = fn_all.sig.inputs.first() { + match first_arg { + syn::FnArg::Receiver(syn::Receiver { reference: Some(_), ..}) => true, + syn::FnArg::Typed(syn::PatType { ref ty, .. }) => { + match ty.as_ref() { + &syn::Type::Reference(syn::TypeReference { + mutability: Some(_), .. }) => true, + &syn::Type::Reference(syn::TypeReference { mutability: None, + ref elem, .. }) => { + match elem.as_ref() { + &syn::Type::Path(ref p) if p.path == str_type_path => false, + _ => return Err(syn::Error::new(ty.span(), + "references from Rhai in this position \ + must be mutable")), + } + }, + _ => false, + } + }, + _ => false, + } + } else { + false + } + }; + + // All arguments after the first must be moved except for &str. + for arg in fn_all.sig.inputs.iter().skip(1) { + let ty = match arg { + syn::FnArg::Typed(syn::PatType { ref ty, .. }) => ty, + _ => panic!("internal error: receiver argument outside of first position!?"), + }; + let is_ok = match ty.as_ref() { + &syn::Type::Reference(syn::TypeReference { mutability: Some(_), .. }) => false, + &syn::Type::Reference(syn::TypeReference { mutability: None, + ref elem, .. }) => { + match elem.as_ref() { + &syn::Type::Path(ref p) if p.path == str_type_path => true, + _ => false, + } + }, + &syn::Type::Verbatim(_) => false, + _ => true, + }; + if !is_ok { + return Err(syn::Error::new(ty.span(), "this type in this position passes from \ + Rhai by value")); + } + } + + // No returning references or pointers. + if let syn::ReturnType::Type(_, ref rtype) = fn_all.sig.output { + match rtype.as_ref() { + &syn::Type::Ptr(_) => return Err(syn::Error::new(fn_all.sig.output.span(), + "cannot return a pointer to Rhai")), + &syn::Type::Reference(_) => return Err(syn::Error::new(fn_all.sig.output.span(), + "cannot return a reference to Rhai")), + _ => {}, + } + } + Ok(ExportedFn { + entire_span, + signature: fn_all.sig, + is_public, + mut_receiver, + }) + } +} + +impl ExportedFn { + pub(crate) fn mutable_receiver(&self) -> bool { + self.mut_receiver + } + + pub(crate) fn is_public(&self) -> bool { + self.is_public + } + + pub(crate) fn span(&self) -> &proc_macro2::Span { + &self.entire_span + } + + pub(crate) fn name(&self) -> &syn::Ident { + &self.signature.ident + } + + pub(crate) fn arg_list(&self) -> impl Iterator { + self.signature.inputs.iter() + } + + pub(crate) fn arg_count(&self) -> usize { + self.signature.inputs.len() + } + + pub(crate) fn return_type(&self) -> Option<&syn::Type> { + if let syn::ReturnType::Type(_, ref rtype) = self.signature.output { + Some(rtype) + } else { + None + } + } + + pub fn generate(self) -> proc_macro2::TokenStream { + let name: syn::Ident = syn::Ident::new(&format!("rhai_fn__{}", self.name().to_string()), + self.name().span()); + let impl_block = self.generate_impl("Token"); + quote! { + #[allow(unused)] + pub mod #name { + use super::*; + pub struct Token(); + #impl_block + } + } + } + + pub fn generate_impl(&self, on_type_name: &str) -> proc_macro2::TokenStream { + let name: syn::Ident = self.name().clone(); + let arg_count = self.arg_count(); + let is_method_call = self.mutable_receiver(); + + let mut unpack_stmts: Vec = Vec::new(); + let mut unpack_exprs: Vec = Vec::new(); + let mut input_type_exprs: Vec = Vec::new(); + let skip_first_arg; + + // Handle the first argument separately if the function has a "method like" receiver + if is_method_call { + skip_first_arg = true; + let first_arg = self.arg_list().next().unwrap(); + let var = syn::Ident::new("arg0", proc_macro2::Span::call_site()); + match first_arg { + syn::FnArg::Typed(pattern) => { + let arg_type: &syn::Type = { + match pattern.ty.as_ref() { + &syn::Type::Reference( + syn::TypeReference { ref elem, .. }) => elem.as_ref(), + ref p => p, + } + }; + let downcast_span = quote_spanned!( + arg_type.span()=> args[0usize].downcast_mut::<#arg_type>().unwrap()); + unpack_stmts.push(syn::parse2::(quote! { + let #var: &mut _ = #downcast_span; + }).unwrap()); + input_type_exprs.push(syn::parse2::(quote_spanned!( + arg_type.span()=> std::any::TypeId::of::<#arg_type>() + )).unwrap()); + }, + syn::FnArg::Receiver(_) => todo!("true self parameters not implemented yet"), + } + unpack_exprs.push(syn::parse2::(quote! { #var }).unwrap()); + } else { + skip_first_arg = false; + } + + // Handle the rest of the arguments, which all are passed by value. + // + // The only exception is strings, which need to be downcast to ImmutableString to enable a + // zero-copy conversion to &str by reference. + let str_type_path = syn::parse2::(quote! { str }).unwrap(); + for (i, arg) in self.arg_list().enumerate().skip(skip_first_arg as usize) { + let var = syn::Ident::new(&format!("arg{}", i), proc_macro2::Span::call_site()); + let is_str_ref; + match arg { + syn::FnArg::Typed(pattern) => { + let arg_type: &syn::Type = pattern.ty.as_ref(); + let downcast_span = match pattern.ty.as_ref() { + &syn::Type::Reference(syn::TypeReference { mutability: None, + ref elem, .. }) => { + match elem.as_ref() { + &syn::Type::Path(ref p) if p.path == str_type_path => { + is_str_ref = true; + quote_spanned!(arg_type.span()=> + args[#i] + .downcast_clone::() + .unwrap()) + }, + _ => panic!("internal error: why wasn't this found earlier!?"), + } + }, + _ => { + is_str_ref = false; + quote_spanned!(arg_type.span()=> + args[#i].downcast_clone::<#arg_type>().unwrap()) + }, + }; + + unpack_stmts.push(syn::parse2::(quote! { + let #var = #downcast_span; + }).unwrap()); + if !is_str_ref { + input_type_exprs.push(syn::parse2::(quote_spanned!( + arg_type.span()=> std::any::TypeId::of::<#arg_type>() + )).unwrap()); + } else { + input_type_exprs.push(syn::parse2::(quote_spanned!( + arg_type.span()=> std::any::TypeId::of::() + )).unwrap()); + } + }, + syn::FnArg::Receiver(_) => panic!("internal error: how did this happen!?"), + } + if !is_str_ref { + unpack_exprs.push(syn::parse2::(quote! { #var }).unwrap()); + } else { + unpack_exprs.push(syn::parse2::(quote! { &#var }).unwrap()); + } + } + + // In method calls, the first argument will need to be mutably borrowed. Because Rust marks + // that as needing to borrow the entire array, all of the previous argument unpacking via + // clone needs to happen first. + if is_method_call { + let arg0 = unpack_stmts.remove(0); + unpack_stmts.push(arg0); + } + + let type_name = syn::Ident::new(on_type_name, proc_macro2::Span::call_site()); + quote! { + impl rhai::plugin::PluginFunction for #type_name { + fn call(&self, + args: &mut [&mut rhai::Dynamic], pos: rhai::Position + ) -> Result> { + if args.len() != #arg_count { + return Err(Box::new(rhai::EvalAltResult::ErrorRuntime( + format!("wrong arg count: {} != {}", + args.len(), #arg_count), rhai::Position::none()))); + } + #(#unpack_stmts)* + Ok(rhai::Dynamic::from(#name(#(#unpack_exprs),*))) + } + + fn is_method_call(&self) -> bool { #is_method_call } + fn is_varadic(&self) -> bool { false } + fn clone_boxed(&self) -> Box { Box::new(#type_name()) } + fn input_types(&self) -> Box<[std::any::TypeId]> { + vec![#(#input_type_exprs),*].into_boxed_slice() + } + } + } + } +} + +#[cfg(test)] +mod function_tests { + use super::ExportedFn; + + use proc_macro2::TokenStream; + use quote::quote; + + #[test] + fn minimal_fn() { + let input_tokens: TokenStream = quote! { + pub fn do_nothing() { } + }; + + let item_fn = syn::parse2::(input_tokens).unwrap(); + assert_eq!(&item_fn.name().to_string(), "do_nothing"); + assert!(!item_fn.mutable_receiver()); + assert!(item_fn.is_public()); + assert!(item_fn.return_type().is_none()); + assert_eq!(item_fn.arg_list().count(), 0); + } + + #[test] + fn one_arg_fn() { + let input_tokens: TokenStream = quote! { + pub fn do_something(x: usize) { } + }; + + let item_fn = syn::parse2::(input_tokens).unwrap(); + assert_eq!(&item_fn.name().to_string(), "do_something"); + assert_eq!(item_fn.arg_list().count(), 1); + assert!(!item_fn.mutable_receiver()); + assert!(item_fn.is_public()); + assert!(item_fn.return_type().is_none()); + + assert_eq!(item_fn.arg_list().next().unwrap(), + &syn::parse2::(quote! { x: usize }).unwrap()); + } + + #[test] + fn two_arg_fn() { + let input_tokens: TokenStream = quote! { + pub fn do_something(x: usize, y: f32) { } + }; + + let item_fn = syn::parse2::(input_tokens).unwrap(); + assert_eq!(&item_fn.name().to_string(), "do_something"); + assert_eq!(item_fn.arg_list().count(), 2); + assert!(!item_fn.mutable_receiver()); + assert!(item_fn.is_public()); + assert!(item_fn.return_type().is_none()); + + assert_eq!(item_fn.arg_list().next().unwrap(), + &syn::parse2::(quote! { x: usize }).unwrap()); + assert_eq!(item_fn.arg_list().skip(1).next().unwrap(), + &syn::parse2::(quote! { y: f32 }).unwrap()); + } + + #[test] + fn usize_returning_fn() { + let input_tokens: TokenStream = quote! { + pub fn get_magic_number() -> usize { 42 } + }; + + let item_fn = syn::parse2::(input_tokens).unwrap(); + assert_eq!(&item_fn.name().to_string(), "get_magic_number"); + assert!(!item_fn.mutable_receiver()); + assert!(item_fn.is_public()); + assert_eq!(item_fn.arg_list().count(), 0); + assert_eq!(item_fn.return_type().unwrap(), + &syn::Type::Path(syn::TypePath { + qself: None, + path: syn::parse2::(quote! { usize }).unwrap() + }) + ); + } + + #[test] + fn ref_returning_fn() { + let input_tokens: TokenStream = quote! { + pub fn get_magic_phrase() -> &'static str { "open sesame" } + }; + + let err = syn::parse2::(input_tokens).unwrap_err(); + assert_eq!(format!("{}", err), + "cannot return a reference to Rhai"); + } + + #[test] + fn ptr_returning_fn() { + let input_tokens: TokenStream = quote! { + pub fn get_magic_phrase() -> *const str { "open sesame" } + }; + + let err = syn::parse2::(input_tokens).unwrap_err(); + assert_eq!(format!("{}", err), + "cannot return a pointer to Rhai"); + } + + #[test] + fn ref_arg_fn() { + let input_tokens: TokenStream = quote! { + pub fn greet(who: &Person) { } + }; + + let err = syn::parse2::(input_tokens).unwrap_err(); + assert_eq!(format!("{}", err), + "references from Rhai in this position must be mutable"); + } + + #[test] + fn ref_second_arg_fn() { + let input_tokens: TokenStream = quote! { + pub fn greet(count: usize, who: &Person) { } + }; + + let err = syn::parse2::(input_tokens).unwrap_err(); + assert_eq!(format!("{}", err), + "this type in this position passes from Rhai by value"); + } + + #[test] + fn mut_ref_second_arg_fn() { + let input_tokens: TokenStream = quote! { + pub fn give(item_name: &str, who: &mut Person) { } + }; + + let err = syn::parse2::(input_tokens).unwrap_err(); + assert_eq!(format!("{}", err), + "this type in this position passes from Rhai by value"); + } + + #[test] + fn str_arg_fn() { + let input_tokens: TokenStream = quote! { + pub fn log(message: &str) { } + }; + + let item_fn = syn::parse2::(input_tokens).unwrap(); + assert_eq!(&item_fn.name().to_string(), "log"); + assert_eq!(item_fn.arg_list().count(), 1); + assert!(!item_fn.mutable_receiver()); + assert!(item_fn.is_public()); + assert!(item_fn.return_type().is_none()); + + assert_eq!(item_fn.arg_list().next().unwrap(), + &syn::parse2::(quote! { message: &str }).unwrap()); + } + + #[test] + fn str_second_arg_fn() { + let input_tokens: TokenStream = quote! { + pub fn log(level: usize, message: &str) { } + }; + + let item_fn = syn::parse2::(input_tokens).unwrap(); + assert_eq!(&item_fn.name().to_string(), "log"); + assert_eq!(item_fn.arg_list().count(), 2); + assert!(!item_fn.mutable_receiver()); + assert!(item_fn.is_public()); + assert!(item_fn.return_type().is_none()); + + assert_eq!(item_fn.arg_list().next().unwrap(), + &syn::parse2::(quote! { level: usize }).unwrap()); + assert_eq!(item_fn.arg_list().skip(1).next().unwrap(), + &syn::parse2::(quote! { message: &str }).unwrap()); + } + + #[test] + fn private_fn() { + let input_tokens: TokenStream = quote! { + fn do_nothing() { } + }; + + let item_fn = syn::parse2::(input_tokens).unwrap(); + assert_eq!(&item_fn.name().to_string(), "do_nothing"); + assert!(!item_fn.mutable_receiver()); + assert!(!item_fn.is_public()); + assert!(item_fn.return_type().is_none()); + assert_eq!(item_fn.arg_list().count(), 0); + } + + #[test] + fn receiver_fn() { + let input_tokens: TokenStream = quote! { + pub fn act_upon(&mut self) { } + }; + + let item_fn = syn::parse2::(input_tokens).unwrap(); + assert_eq!(&item_fn.name().to_string(), "act_upon"); + assert!(item_fn.mutable_receiver()); + assert!(item_fn.is_public()); + assert!(item_fn.return_type().is_none()); + assert_eq!(item_fn.arg_list().count(), 1); + } + + #[test] + fn immutable_receiver_fn() { + let input_tokens: TokenStream = quote! { + pub fn act_upon(&self) { } + }; + + let item_fn = syn::parse2::(input_tokens).unwrap(); + assert_eq!(&item_fn.name().to_string(), "act_upon"); + assert!(item_fn.mutable_receiver()); + assert!(item_fn.is_public()); + assert!(item_fn.return_type().is_none()); + assert_eq!(item_fn.arg_list().count(), 1); + } +} + +#[cfg(test)] +mod generate_tests { + use super::ExportedFn; + + use proc_macro2::TokenStream; + use quote::quote; + + fn assert_streams_eq(actual: TokenStream, expected: TokenStream) { + let actual = actual.to_string(); + let expected = expected.to_string(); + if &actual != &expected { + let mut counter = 0; + let iter = actual.chars().zip(expected.chars()) + .inspect(|_| counter += 1) + .skip_while(|(a, e)| *a == *e); + let (actual_diff, expected_diff) = { + let mut actual_diff = String::new(); + let mut expected_diff = String::new(); + for (a, e) in iter.take(50) { + actual_diff.push(a); + expected_diff.push(e); + } + (actual_diff, expected_diff) + }; + eprintln!("actual != expected, diverge at char {}", counter); + } + assert_eq!(actual, expected); + } + + #[test] + fn minimal_fn () { + let input_tokens: TokenStream = quote! { + pub fn do_nothing() { } + }; + + let expected_tokens = quote! { + #[allow(unused)] + pub mod rhai_fn__do_nothing { + use super::*; + pub struct Token(); + impl rhai::plugin::PluginFunction for Token { + fn call(&self, + args: &mut [&mut rhai::Dynamic], pos: rhai::Position + ) -> Result> { + if args.len() != 0usize { + return Err(Box::new(rhai::EvalAltResult::ErrorRuntime( + format!("wrong arg count: {} != {}", + args.len(), 0usize), rhai::Position::none()))); + } + Ok(rhai::Dynamic::from(do_nothing())) + } + + fn is_method_call(&self) -> bool { false } + fn is_varadic(&self) -> bool { false } + fn clone_boxed(&self) -> Box { Box::new(Token()) } + fn input_types(&self) -> Box<[std::any::TypeId]> { + vec![].into_boxed_slice() + } + } + } + }; + + let item_fn = syn::parse2::(input_tokens).unwrap(); + assert_streams_eq(item_fn.generate(), expected_tokens); + } + + #[test] + fn one_arg_usize_fn() { + let input_tokens: TokenStream = quote! { + pub fn do_something(x: usize) { } + }; + + let expected_tokens = quote! { + #[allow(unused)] + pub mod rhai_fn__do_something { + use super::*; + pub struct Token(); + impl rhai::plugin::PluginFunction for Token { + fn call(&self, + args: &mut [&mut rhai::Dynamic], pos: rhai::Position + ) -> Result> { + if args.len() != 1usize { + return Err(Box::new(rhai::EvalAltResult::ErrorRuntime( + format!("wrong arg count: {} != {}", + args.len(), 1usize), rhai::Position::none()))); + } + let arg0 = args[0usize].downcast_clone::().unwrap(); + Ok(rhai::Dynamic::from(do_something(arg0))) + } + + fn is_method_call(&self) -> bool { false } + fn is_varadic(&self) -> bool { false } + fn clone_boxed(&self) -> Box { Box::new(Token()) } + fn input_types(&self) -> Box<[std::any::TypeId]> { + vec![std::any::TypeId::of::()].into_boxed_slice() + } + } + } + }; + + let item_fn = syn::parse2::(input_tokens).unwrap(); + assert_streams_eq(item_fn.generate(), expected_tokens); + } + + #[test] + fn one_arg_usize_fn_impl() { + let input_tokens: TokenStream = quote! { + pub fn do_something(x: usize) { } + }; + + let expected_tokens = quote! { + impl rhai::plugin::PluginFunction for MyType { + fn call(&self, + args: &mut [&mut rhai::Dynamic], pos: rhai::Position + ) -> Result> { + if args.len() != 1usize { + return Err(Box::new(rhai::EvalAltResult::ErrorRuntime( + format!("wrong arg count: {} != {}", + args.len(), 1usize), rhai::Position::none()))); + } + let arg0 = args[0usize].downcast_clone::().unwrap(); + Ok(rhai::Dynamic::from(do_something(arg0))) + } + + fn is_method_call(&self) -> bool { false } + fn is_varadic(&self) -> bool { false } + fn clone_boxed(&self) -> Box { Box::new(MyType()) } + fn input_types(&self) -> Box<[std::any::TypeId]> { + vec![std::any::TypeId::of::()].into_boxed_slice() + } + } + }; + + let item_fn = syn::parse2::(input_tokens).unwrap(); + assert_streams_eq(item_fn.generate_impl("MyType"), expected_tokens); + } + + #[test] + fn two_arg_returning_usize_fn() { + let input_tokens: TokenStream = quote! { + pub fn add_together(x: usize, y: usize) -> usize { x + y } + }; + + let expected_tokens = quote! { + #[allow(unused)] + pub mod rhai_fn__add_together { + use super::*; + pub struct Token(); + impl rhai::plugin::PluginFunction for Token { + fn call(&self, + args: &mut [&mut rhai::Dynamic], pos: rhai::Position + ) -> Result> { + if args.len() != 2usize { + return Err(Box::new(rhai::EvalAltResult::ErrorRuntime( + format!("wrong arg count: {} != {}", + args.len(), 2usize), rhai::Position::none()))); + } + let arg0 = args[0usize].downcast_clone::().unwrap(); + let arg1 = args[1usize].downcast_clone::().unwrap(); + Ok(rhai::Dynamic::from(add_together(arg0, arg1))) + } + + fn is_method_call(&self) -> bool { false } + fn is_varadic(&self) -> bool { false } + fn clone_boxed(&self) -> Box { Box::new(Token()) } + fn input_types(&self) -> Box<[std::any::TypeId]> { + vec![std::any::TypeId::of::(), + std::any::TypeId::of::()].into_boxed_slice() + } + } + } + }; + + let item_fn = syn::parse2::(input_tokens).unwrap(); + assert_streams_eq(item_fn.generate(), expected_tokens); + } + + #[test] + fn mut_arg_usize_fn() { + let input_tokens: TokenStream = quote! { + pub fn increment(x: &mut usize, y: usize) { *x += y; } + }; + + let expected_tokens = quote! { + #[allow(unused)] + pub mod rhai_fn__increment { + use super::*; + pub struct Token(); + impl rhai::plugin::PluginFunction for Token { + fn call(&self, + args: &mut [&mut rhai::Dynamic], pos: rhai::Position + ) -> Result> { + if args.len() != 2usize { + return Err(Box::new(rhai::EvalAltResult::ErrorRuntime( + format!("wrong arg count: {} != {}", + args.len(), 2usize), rhai::Position::none()))); + } + let arg1 = args[1usize].downcast_clone::().unwrap(); + let arg0: &mut _ = args[0usize].downcast_mut::().unwrap(); + Ok(rhai::Dynamic::from(increment(arg0, arg1))) + } + + fn is_method_call(&self) -> bool { true } + fn is_varadic(&self) -> bool { false } + fn clone_boxed(&self) -> Box { Box::new(Token()) } + fn input_types(&self) -> Box<[std::any::TypeId]> { + vec![std::any::TypeId::of::(), + std::any::TypeId::of::()].into_boxed_slice() + } + } + } + }; + + let item_fn = syn::parse2::(input_tokens).unwrap(); + assert!(item_fn.mutable_receiver()); + assert_streams_eq(item_fn.generate(), expected_tokens); + } + + #[test] + fn str_arg_fn() { + let input_tokens: TokenStream = quote! { + pub fn special_print(message: &str) { eprintln!("----{}----", message); } + }; + + let expected_tokens = quote! { + #[allow(unused)] + pub mod rhai_fn__special_print { + use super::*; + pub struct Token(); + impl rhai::plugin::PluginFunction for Token { + fn call(&self, + args: &mut [&mut rhai::Dynamic], pos: rhai::Position + ) -> Result> { + if args.len() != 1usize { + return Err(Box::new(rhai::EvalAltResult::ErrorRuntime( + format!("wrong arg count: {} != {}", + args.len(), 1usize), rhai::Position::none()))); + } + let arg0 = args[0usize].downcast_clone::().unwrap(); + Ok(rhai::Dynamic::from(special_print(&arg0))) + } + + fn is_method_call(&self) -> bool { false } + fn is_varadic(&self) -> bool { false } + fn clone_boxed(&self) -> Box { Box::new(Token()) } + fn input_types(&self) -> Box<[std::any::TypeId]> { + vec![std::any::TypeId::of::()].into_boxed_slice() + } + } + } + }; + + let item_fn = syn::parse2::(input_tokens).unwrap(); + assert!(!item_fn.mutable_receiver()); + assert_streams_eq(item_fn.generate(), expected_tokens); + } +} + +#[cfg(test)] +mod ui_tests { + #[test] + fn all() { + let t = trybuild::TestCases::new(); + t.compile_fail("ui_tests/*.rs"); + } +} diff --git a/codegen/src/lib.rs b/codegen/src/lib.rs new file mode 100644 index 00000000..f22b9da9 --- /dev/null +++ b/codegen/src/lib.rs @@ -0,0 +1,143 @@ +//! +//! This crate contains procedural macros to make creating Rhai modules much easier. +//! +//! # Exporting a Macro to Rhai +//! +//! ``` +//! use rhai::{EvalAltResult, FLOAT, RegisterFn}; +//! use rhai::plugin::*; +//! use rhai::module_resolvers::*; +//! +//! #[rhai::export_module] +//! pub mod advanced_math { +//! use rhai::FLOAT; +//! +//! pub const MYSTIC_NUMBER: FLOAT = 42.0 as FLOAT; +//! +//! pub fn euclidean_distance(x1: FLOAT, y1: FLOAT, x2: FLOAT, y2: FLOAT) -> FLOAT { +//! ((y2 - y1).abs().powf(2.0) + (x2 -x1).abs().powf(2.0)).sqrt() +//! } +//! } +//! +//! fn main() -> Result<(), Box> { +//! let mut engine = Engine::new(); +//! let m = rhai::exported_module!(advanced_math); +//! let mut r = StaticModuleResolver::new(); +//! r.insert("Math::Advanced".to_string(), m); +//! engine.set_module_resolver(Some(r)); +//! +//! assert_eq!(engine.eval::( +//! r#"import "Math::Advanced" as math; +//! let m = math::MYSTIC_NUMBER; +//! let x = math::euclidean_distance(0.0, 1.0, 0.0, m); +//! x"#)?, 41.0); +//! Ok(()) +//! } +//! ``` +//! +//! # Exporting a Function to Rhai +//! +//! ``` +//! use rhai::{EvalAltResult, FLOAT, Module, RegisterFn}; +//! use rhai::plugin::*; +//! use rhai::module_resolvers::*; +//! +//! #[rhai::export_fn] +//! pub fn distance_function(x1: FLOAT, y1: FLOAT, x2: FLOAT, y2: FLOAT) -> FLOAT { +//! ((y2 - y1).abs().powf(2.0) + (x2 -x1).abs().powf(2.0)).sqrt() +//! } +//! +//! fn main() -> Result<(), Box> { +//! +//! let mut engine = Engine::new(); +//! engine.register_fn("get_mystic_number", || { 42 as FLOAT }); +//! let mut m = Module::new(); +//! rhai::register_exported_fn!(m, "euclidean_distance", distance_function); +//! let mut r = StaticModuleResolver::new(); +//! r.insert("Math::Advanced".to_string(), m); +//! engine.set_module_resolver(Some(r)); +//! +//! assert_eq!(engine.eval::( +//! r#"import "Math::Advanced" as math; +//! let m = get_mystic_number(); +//! let x = math::euclidean_distance(0.0, 1.0, 0.0, m); +//! x"#)?, 41.0); +//! Ok(()) +//! } +//! ``` +//! + +use quote::{quote, quote_spanned}; +use syn::{parse::Parser, parse_macro_input, spanned::Spanned}; + +mod function; +mod module; +mod rhai_module; + +#[proc_macro_attribute] +pub fn export_fn(_args: proc_macro::TokenStream, + input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let mut output = proc_macro2::TokenStream::from(input.clone()); + let function_def = parse_macro_input!(input as function::ExportedFn); + output.extend(function_def.generate()); + proc_macro::TokenStream::from(output) +} + +#[proc_macro_attribute] +pub fn export_module(_args: proc_macro::TokenStream, + input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let module_def = parse_macro_input!(input as module::Module); + let tokens = module_def.generate(); + proc_macro::TokenStream::from(tokens) +} + +#[proc_macro] +pub fn exported_module(module_path: proc_macro::TokenStream) -> proc_macro::TokenStream { + let module_path = parse_macro_input!(module_path as syn::Path); + let tokens = quote::quote! { + #module_path::rhai_module__generate() + }; + proc_macro::TokenStream::from(tokens) +} + +#[proc_macro] +pub fn register_exported_fn(args: proc_macro::TokenStream) -> proc_macro::TokenStream { + let parser = syn::punctuated::Punctuated::::parse_separated_nonempty; + let args = parser.parse(args).unwrap(); + let arg_span = args.span(); + let items: Vec = args.into_iter().collect(); + if items.len() != 3 { + return proc_macro::TokenStream::from( + syn::Error::new(arg_span, "this macro requires three arguments") + .to_compile_error()); + } + let rhai_module = &items[0]; + let export_name = match &items[1] { + syn::Expr::Lit(litstr) => quote_spanned!(items[1].span()=> + #litstr.to_string()), + expr => quote! { #expr }, + }; + let rust_modpath = if let syn::Expr::Path(ref path) = &items[2] { + &path.path + } else { + return proc_macro::TokenStream::from( + syn::Error::new(items[2].span(), "third argument must be a function name") + .to_compile_error()); + }; + let gen_mod_path: syn::punctuated::Punctuated = { + let mut g = rust_modpath.clone().segments; + g.pop(); + let ident = syn::Ident::new(&format!("rhai_fn__{}", + rust_modpath.segments.last().unwrap().ident), + items[2].span()); + g.push_value(syn::PathSegment { ident, arguments: syn::PathArguments::None }); + g + }; + let tokens = quote! { + #rhai_module.set_fn(#export_name, rhai::FnAccess::Public, + #gen_mod_path::Token().input_types().as_ref(), + CallableFunction::from_plugin(#gen_mod_path::Token())); + + }; + proc_macro::TokenStream::from(tokens) +} diff --git a/codegen/src/module.rs b/codegen/src/module.rs new file mode 100644 index 00000000..e22b6ec8 --- /dev/null +++ b/codegen/src/module.rs @@ -0,0 +1,661 @@ +use quote::{quote, ToTokens}; +use syn::{parse::Parse, parse::ParseStream}; + +use crate::function::ExportedFn; +use crate::rhai_module::ExportedConst; + +#[derive(Debug)] +pub(crate) struct Module { + mod_all: Option, + fns: Vec, + consts: Vec, +} + +impl Parse for Module { + fn parse(input: ParseStream) -> syn::Result { + let mod_all: syn::ItemMod = input.parse()?; + let fns: Vec<_>; + let consts: Vec<_>; + if let Some((_, ref content)) = mod_all.content { + fns = content.iter() + .filter_map(|item| { + match item { + syn::Item::Fn(f) => { + if let syn::Visibility::Public(_) = f.vis { + Some(f) + } else { + None + } + }, + _ => None, + } + }) + .try_fold(Vec::new(), |mut vec, itemfn| { + syn::parse2::(itemfn.to_token_stream()) + .map(|f| vec.push(f)) + .map(|_| vec) + })?; + consts = content.iter() + .filter_map(|item| { + match item { + syn::Item::Const(syn::ItemConst {vis, ref expr, ident, ..}) => { + if let syn::Visibility::Public(_) = vis { + Some((ident.to_string(), expr.as_ref().clone())) + } else { + None + } + }, + _ => None, + } + }) + .collect(); + } else { + consts = vec![]; + fns = vec![]; + } + Ok(Module { + mod_all: Some(mod_all), + fns, + consts, + }) + } +} + +impl Module { + pub fn generate(self) -> proc_macro2::TokenStream { + let mod_gen = crate::rhai_module::generate_body(&self.fns, &self.consts); + let Module { mod_all, .. } = self; + let mut mod_all = mod_all.unwrap(); + let mod_name = mod_all.ident.clone(); + let (_, orig_content) = mod_all.content.take().unwrap(); + + quote! { + pub mod #mod_name { + #(#orig_content)* + #mod_gen + } + } + } +} + +#[cfg(test)] +mod module_tests { + use super::Module; + + use proc_macro2::TokenStream; + use quote::quote; + + #[test] + fn empty_module() { + + let input_tokens: TokenStream = quote! { + pub mod empty { } + }; + + let item_mod = syn::parse2::(input_tokens).unwrap(); + assert!(item_mod.fns.is_empty()); + assert!(item_mod.consts.is_empty()); + } + + #[test] + fn one_factory_fn_module() { + + let input_tokens: TokenStream = quote! { + pub mod one_fn { + pub fn get_mystic_number() -> INT { + 42 + } + } + }; + + let item_mod = syn::parse2::(input_tokens).unwrap(); + assert!(item_mod.consts.is_empty()); + assert_eq!(item_mod.fns.len(), 1); + assert_eq!(item_mod.fns[0].name().to_string(), "get_mystic_number"); + assert_eq!(item_mod.fns[0].arg_count(), 0); + assert_eq!(item_mod.fns[0].return_type().unwrap(), + &syn::parse2::(quote! { INT }).unwrap()); + } + + #[test] + fn one_single_arg_fn_module() { + + let input_tokens: TokenStream = quote! { + pub mod one_fn { + pub fn add_one_to(x: INT) -> INT { + x + 1 + } + } + }; + + let item_mod = syn::parse2::(input_tokens).unwrap(); + assert!(item_mod.consts.is_empty()); + assert_eq!(item_mod.fns.len(), 1); + assert_eq!(item_mod.fns[0].name().to_string(), "add_one_to"); + assert_eq!(item_mod.fns[0].arg_count(), 1); + assert_eq!(item_mod.fns[0].arg_list().next().unwrap(), + &syn::parse2::(quote! { x: INT }).unwrap()); + assert_eq!(item_mod.fns[0].return_type().unwrap(), + &syn::parse2::(quote! { INT }).unwrap()); + } + + #[test] + fn one_double_arg_fn_module() { + + let input_tokens: TokenStream = quote! { + pub mod one_fn { + pub fn add_together(x: INT, y: INT) -> INT { + x + y + } + } + }; + + let item_mod = syn::parse2::(input_tokens).unwrap(); + let mut args = item_mod.fns[0].arg_list(); + assert!(item_mod.consts.is_empty()); + assert_eq!(item_mod.fns.len(), 1); + assert_eq!(item_mod.fns[0].name().to_string(), "add_together"); + assert_eq!(item_mod.fns[0].arg_count(), 2); + assert_eq!(args.next().unwrap(), + &syn::parse2::(quote! { x: INT }).unwrap()); + assert_eq!(args.next().unwrap(), + &syn::parse2::(quote! { y: INT }).unwrap()); + assert!(args.next().is_none()); + assert_eq!(item_mod.fns[0].return_type().unwrap(), + &syn::parse2::(quote! { INT }).unwrap()); + } + + #[test] + fn one_constant_module() { + + let input_tokens: TokenStream = quote! { + pub mod one_constant { + pub const MYSTIC_NUMBER: INT = 42; + } + }; + + let item_mod = syn::parse2::(input_tokens).unwrap(); + assert!(item_mod.fns.is_empty()); + assert_eq!(item_mod.consts.len(), 1); + assert_eq!(&item_mod.consts[0].0, "MYSTIC_NUMBER"); + assert_eq!(item_mod.consts[0].1, syn::parse2::(quote! { 42 }).unwrap()); + } + + #[test] + fn one_private_fn_module() { + + let input_tokens: TokenStream = quote! { + pub mod one_fn { + fn get_mystic_number() -> INT { + 42 + } + } + }; + + let item_mod = syn::parse2::(input_tokens).unwrap(); + assert!(item_mod.fns.is_empty()); + assert!(item_mod.consts.is_empty()); + } + + #[test] + fn one_private_constant_module() { + + let input_tokens: TokenStream = quote! { + pub mod one_constant { + const MYSTIC_NUMBER: INT = 42; + } + }; + + let item_mod = syn::parse2::(input_tokens).unwrap(); + assert!(item_mod.fns.is_empty()); + assert!(item_mod.consts.is_empty()); + } +} + +#[cfg(test)] +mod generate_tests { + use super::Module; + + use proc_macro2::TokenStream; + use quote::quote; + + fn assert_streams_eq(actual: TokenStream, expected: TokenStream) { + let actual = actual.to_string(); + let expected = expected.to_string(); + if &actual != &expected { + let mut counter = 0; + let iter = actual.chars().zip(expected.chars()) + .inspect(|_| counter += 1) + .skip_while(|(a, e)| *a == *e); + let (actual_diff, expected_diff) = { + let mut actual_diff = String::new(); + let mut expected_diff = String::new(); + for (a, e) in iter.take(50) { + actual_diff.push(a); + expected_diff.push(e); + } + (actual_diff, expected_diff) + }; + eprintln!("actual != expected, diverge at char {}", counter); + } + assert_eq!(actual, expected); + } + + #[test] + fn empty_module() { + let input_tokens: TokenStream = quote! { + pub mod empty { } + }; + + let expected_tokens = quote! { + pub mod empty { + #[allow(unused_imports)] + use rhai::{Module, FnAccess}; + #[allow(unused_mut)] + pub fn rhai_module__generate() -> Module { + let mut m = Module::new(); + m + } + } + }; + + let item_mod = syn::parse2::(input_tokens).unwrap(); + assert_streams_eq(item_mod.generate(), expected_tokens); + } + + #[test] + fn one_factory_fn_module() { + + let input_tokens: TokenStream = quote! { + pub mod one_fn { + pub fn get_mystic_number() -> INT { + 42 + } + } + }; + + let expected_tokens = quote! { + pub mod one_fn { + pub fn get_mystic_number() -> INT { + 42 + } + #[allow(unused_imports)] + use rhai::{Module, FnAccess}; + #[allow(unused_mut)] + pub fn rhai_module__generate() -> Module { + let mut m = Module::new(); + m.set_fn("get_mystic_number", FnAccess::Public, &[], + rhai::plugin::CallableFunction::from_plugin(get_mystic_number__Token())); + m + } + #[allow(non_camel_case_types)] + pub struct get_mystic_number__Token(); + impl rhai::plugin::PluginFunction for get_mystic_number__Token { + fn call(&self, + args: &mut [&mut rhai::Dynamic], pos: rhai::Position + ) -> Result> { + if args.len() != 0usize { + return Err(Box::new(rhai::EvalAltResult::ErrorRuntime( + format!("wrong arg count: {} != {}", + args.len(), 0usize), rhai::Position::none()))); + } + Ok(rhai::Dynamic::from(get_mystic_number())) + } + + fn is_method_call(&self) -> bool { false } + fn is_varadic(&self) -> bool { false } + fn clone_boxed(&self) -> Box { + Box::new(get_mystic_number__Token()) + } + fn input_types(&self) -> Box<[std::any::TypeId]> { + vec![].into_boxed_slice() + } + } + } + }; + + let item_mod = syn::parse2::(input_tokens).unwrap(); + assert_streams_eq(item_mod.generate(), expected_tokens); + } + + #[test] + fn one_single_arg_fn_module() { + + let input_tokens: TokenStream = quote! { + pub mod one_fn { + pub fn add_one_to(x: INT) -> INT { + x + 1 + } + } + }; + + let expected_tokens = quote! { + pub mod one_fn { + pub fn add_one_to(x: INT) -> INT { + x + 1 + } + #[allow(unused_imports)] + use rhai::{Module, FnAccess}; + #[allow(unused_mut)] + pub fn rhai_module__generate() -> Module { + let mut m = Module::new(); + m.set_fn("add_one_to", FnAccess::Public, &[core::any::TypeId::of::()], + rhai::plugin::CallableFunction::from_plugin(add_one_to__Token())); + m + } + #[allow(non_camel_case_types)] + pub struct add_one_to__Token(); + impl rhai::plugin::PluginFunction for add_one_to__Token { + fn call(&self, + args: &mut [&mut rhai::Dynamic], pos: rhai::Position + ) -> Result> { + if args.len() != 1usize { + return Err(Box::new(rhai::EvalAltResult::ErrorRuntime( + format!("wrong arg count: {} != {}", + args.len(), 1usize), rhai::Position::none()))); + } + let arg0 = args[0usize].downcast_clone::().unwrap(); + Ok(rhai::Dynamic::from(add_one_to(arg0))) + } + + fn is_method_call(&self) -> bool { false } + fn is_varadic(&self) -> bool { false } + fn clone_boxed(&self) -> Box { + Box::new(add_one_to__Token()) + } + fn input_types(&self) -> Box<[std::any::TypeId]> { + vec![std::any::TypeId::of::()].into_boxed_slice() + } + } + } + }; + + let item_mod = syn::parse2::(input_tokens).unwrap(); + assert_streams_eq(item_mod.generate(), expected_tokens); + } + + #[test] + fn one_double_arg_fn_module() { + + let input_tokens: TokenStream = quote! { + pub mod one_fn { + pub fn add_together(x: INT, y: INT) -> INT { + x + y + } + } + }; + + let expected_tokens = quote! { + pub mod one_fn { + pub fn add_together(x: INT, y: INT) -> INT { + x + y + } + #[allow(unused_imports)] + use rhai::{Module, FnAccess}; + #[allow(unused_mut)] + pub fn rhai_module__generate() -> Module { + let mut m = Module::new(); + m.set_fn("add_together", FnAccess::Public, &[core::any::TypeId::of::(), + core::any::TypeId::of::()], + rhai::plugin::CallableFunction::from_plugin(add_together__Token())); + m + } + #[allow(non_camel_case_types)] + pub struct add_together__Token(); + impl rhai::plugin::PluginFunction for add_together__Token { + fn call(&self, + args: &mut [&mut rhai::Dynamic], pos: rhai::Position + ) -> Result> { + if args.len() != 2usize { + return Err(Box::new(rhai::EvalAltResult::ErrorRuntime( + format!("wrong arg count: {} != {}", + args.len(), 2usize), rhai::Position::none()))); + } + let arg0 = args[0usize].downcast_clone::().unwrap(); + let arg1 = args[1usize].downcast_clone::().unwrap(); + Ok(rhai::Dynamic::from(add_together(arg0, arg1))) + } + + fn is_method_call(&self) -> bool { false } + fn is_varadic(&self) -> bool { false } + fn clone_boxed(&self) -> Box { + Box::new(add_together__Token()) + } + fn input_types(&self) -> Box<[std::any::TypeId]> { + vec![std::any::TypeId::of::(), + std::any::TypeId::of::()].into_boxed_slice() + } + } + } + }; + + let item_mod = syn::parse2::(input_tokens).unwrap(); + assert_streams_eq(item_mod.generate(), expected_tokens); + } + + #[test] + fn one_constant_module() { + + let input_tokens: TokenStream = quote! { + pub mod one_constant { + pub const MYSTIC_NUMBER: INT = 42; + } + }; + + let expected_tokens = quote! { + pub mod one_constant { + pub const MYSTIC_NUMBER: INT = 42; + #[allow(unused_imports)] + use rhai::{Module, FnAccess}; + #[allow(unused_mut)] + pub fn rhai_module__generate() -> Module { + let mut m = Module::new(); + m.set_var("MYSTIC_NUMBER", 42); + m + } + } + }; + + let item_mod = syn::parse2::(input_tokens).unwrap(); + assert_streams_eq(item_mod.generate(), expected_tokens); + } + + #[test] + fn one_constant_module_imports_preserved() { + + let input_tokens: TokenStream = quote! { + pub mod one_constant { + pub use rhai::INT; + pub const MYSTIC_NUMBER: INT = 42; + } + }; + + let expected_tokens = quote! { + pub mod one_constant { + pub use rhai::INT; + pub const MYSTIC_NUMBER: INT = 42; + #[allow(unused_imports)] + use rhai::{Module, FnAccess}; + #[allow(unused_mut)] + pub fn rhai_module__generate() -> Module { + let mut m = Module::new(); + m.set_var("MYSTIC_NUMBER", 42); + m + } + } + }; + + let item_mod = syn::parse2::(input_tokens).unwrap(); + assert_streams_eq(item_mod.generate(), expected_tokens); + } + + #[test] + fn one_private_fn_module() { + + let input_tokens: TokenStream = quote! { + pub mod one_fn { + fn get_mystic_number() -> INT { + 42 + } + } + }; + + let expected_tokens = quote! { + pub mod one_fn { + fn get_mystic_number() -> INT { + 42 + } + #[allow(unused_imports)] + use rhai::{Module, FnAccess}; + #[allow(unused_mut)] + pub fn rhai_module__generate() -> Module { + let mut m = Module::new(); + m + } + } + }; + + let item_mod = syn::parse2::(input_tokens).unwrap(); + assert_streams_eq(item_mod.generate(), expected_tokens); + } + + #[test] + fn one_private_constant_module() { + + let input_tokens: TokenStream = quote! { + pub mod one_constant { + const MYSTIC_NUMBER: INT = 42; + } + }; + + let expected_tokens = quote! { + pub mod one_constant { + const MYSTIC_NUMBER: INT = 42; + #[allow(unused_imports)] + use rhai::{Module, FnAccess}; + #[allow(unused_mut)] + pub fn rhai_module__generate() -> Module { + let mut m = Module::new(); + m + } + } + }; + + let item_mod = syn::parse2::(input_tokens).unwrap(); + assert_streams_eq(item_mod.generate(), expected_tokens); + } + + #[test] + fn one_str_arg_fn_module() { + + let input_tokens: TokenStream = quote! { + pub mod str_fn { + pub fn print_out_to(x: &str) { + x + 1 + } + } + }; + + let expected_tokens = quote! { + pub mod str_fn { + pub fn print_out_to(x: &str) { + x + 1 + } + #[allow(unused_imports)] + use rhai::{Module, FnAccess}; + #[allow(unused_mut)] + pub fn rhai_module__generate() -> Module { + let mut m = Module::new(); + m.set_fn("print_out_to", FnAccess::Public, + &[core::any::TypeId::of::()], + rhai::plugin::CallableFunction::from_plugin(print_out_to__Token())); + m + } + #[allow(non_camel_case_types)] + pub struct print_out_to__Token(); + impl rhai::plugin::PluginFunction for print_out_to__Token { + fn call(&self, + args: &mut [&mut rhai::Dynamic], pos: rhai::Position + ) -> Result> { + if args.len() != 1usize { + return Err(Box::new(rhai::EvalAltResult::ErrorRuntime( + format!("wrong arg count: {} != {}", + args.len(), 1usize), rhai::Position::none()))); + } + let arg0 = args[0usize].downcast_clone::().unwrap(); + Ok(rhai::Dynamic::from(print_out_to(&arg0))) + } + + fn is_method_call(&self) -> bool { false } + fn is_varadic(&self) -> bool { false } + fn clone_boxed(&self) -> Box { + Box::new(print_out_to__Token()) + } + fn input_types(&self) -> Box<[std::any::TypeId]> { + vec![std::any::TypeId::of::()].into_boxed_slice() + } + } + } + }; + + let item_mod = syn::parse2::(input_tokens).unwrap(); + assert_streams_eq(item_mod.generate(), expected_tokens); + } + + #[test] + fn one_mut_ref_fn_module() { + + let input_tokens: TokenStream = quote! { + pub mod ref_fn { + pub fn increment(x: &mut FLOAT) { + *x += 1.0 as FLOAT; + } + } + }; + + let expected_tokens = quote! { + pub mod ref_fn { + pub fn increment(x: &mut FLOAT) { + *x += 1.0 as FLOAT; + } + #[allow(unused_imports)] + use rhai::{Module, FnAccess}; + #[allow(unused_mut)] + pub fn rhai_module__generate() -> Module { + let mut m = Module::new(); + m.set_fn("increment", FnAccess::Public, + &[core::any::TypeId::of::()], + rhai::plugin::CallableFunction::from_plugin(increment__Token())); + m + } + #[allow(non_camel_case_types)] + pub struct increment__Token(); + impl rhai::plugin::PluginFunction for increment__Token { + fn call(&self, + args: &mut [&mut rhai::Dynamic], pos: rhai::Position + ) -> Result> { + if args.len() != 1usize { + return Err(Box::new(rhai::EvalAltResult::ErrorRuntime( + format!("wrong arg count: {} != {}", + args.len(), 1usize), rhai::Position::none()))); + } + let arg0: &mut _ = args[0usize].downcast_mut::().unwrap(); + Ok(rhai::Dynamic::from(increment(arg0))) + } + + fn is_method_call(&self) -> bool { true } + fn is_varadic(&self) -> bool { false } + fn clone_boxed(&self) -> Box { + Box::new(increment__Token()) + } + fn input_types(&self) -> Box<[std::any::TypeId]> { + vec![std::any::TypeId::of::()].into_boxed_slice() + } + } + } + }; + + let item_mod = syn::parse2::(input_tokens).unwrap(); + assert_streams_eq(item_mod.generate(), expected_tokens); + } +} diff --git a/codegen/src/rhai_module.rs b/codegen/src/rhai_module.rs new file mode 100644 index 00000000..ba04b50c --- /dev/null +++ b/codegen/src/rhai_module.rs @@ -0,0 +1,90 @@ +use quote::quote; + +use crate::function::ExportedFn; + +pub(crate) type ExportedConst = (String, syn::Expr); + +pub(crate) fn generate_body( + fns: &Vec, + consts: &Vec +) -> proc_macro2::TokenStream { + let mut set_fn_stmts: Vec = Vec::new(); + let mut set_const_stmts: Vec = Vec::new(); + let str_type_path = syn::parse2::(quote! { str }).unwrap(); + + for (const_name, const_expr) in consts { + let const_literal = syn::LitStr::new(&const_name, proc_macro2::Span::call_site()); + set_const_stmts.push(syn::parse2::(quote! { + m.set_var(#const_literal, #const_expr); + }).unwrap()); + } + + // NB: these are token streams, because reparsing messes up "> >" vs ">>" + let mut gen_fn_tokens: Vec = Vec::new(); + for function in fns { + let fn_token_name = syn::Ident::new(&format!("{}__Token", function.name().to_string()), + function.name().span()); + let fn_literal = syn::LitStr::new(&function.name().to_string(), + proc_macro2::Span::call_site()); + let fn_input_types: Vec = function.arg_list() + .map(|fnarg| match fnarg { + syn::FnArg::Receiver(_) => panic!("internal error: receiver fn outside impl!?"), + syn::FnArg::Typed(syn::PatType { ref ty, .. }) => { + let arg_type = match ty.as_ref() { + &syn::Type::Reference(syn::TypeReference { mutability: None, + ref elem, .. }) => { + match elem.as_ref() { + &syn::Type::Path(ref p) if p.path == str_type_path => + syn::parse2::(quote! { + rhai::ImmutableString }).unwrap(), + _ => panic!("internal error: non-string shared reference!?"), + } + }, + &syn::Type::Reference(syn::TypeReference { mutability: Some(_), + ref elem, .. }) => { + match elem.as_ref() { + &syn::Type::Path(ref p) => syn::parse2::(quote! { + #p }).unwrap(), + _ => panic!("internal error: non-string shared reference!?"), + } + }, + t => t.clone(), + }; + syn::parse2::(quote! { + core::any::TypeId::of::<#arg_type>()}).unwrap() + }, + }).collect(); + + set_fn_stmts.push(syn::parse2::(quote! { + m.set_fn(#fn_literal, FnAccess::Public, &[#(#fn_input_types),*], + rhai::plugin::CallableFunction::from_plugin(#fn_token_name())); + }).unwrap()); + + gen_fn_tokens.push(quote! { + #[allow(non_camel_case_types)] + pub struct #fn_token_name(); + }); + gen_fn_tokens.push(function.generate_impl(&fn_token_name.to_string())); + } + + let mut generate_fncall = syn::parse2::(quote! { + pub mod generate_info { + #[allow(unused_imports)] + use rhai::{Module, FnAccess}; + #[allow(unused_mut)] + pub fn rhai_module__generate() -> Module { + let mut m = Module::new(); + #(#set_fn_stmts)* + #(#set_const_stmts)* + m + } + } + }).unwrap(); + + let (_, generate_call_content) = generate_fncall.content.take().unwrap(); + + quote! { + #(#generate_call_content)* + #(#gen_fn_tokens)* + } +} diff --git a/codegen/tests/test_functions.rs b/codegen/tests/test_functions.rs new file mode 100644 index 00000000..a0ebb6c0 --- /dev/null +++ b/codegen/tests/test_functions.rs @@ -0,0 +1,141 @@ +use rhai::{EvalAltResult, FLOAT, INT, Module, RegisterFn}; +use rhai::plugin::*; +use rhai::module_resolvers::*; + +pub mod raw_fn { + use rhai::FLOAT; + use rhai::export_fn; + + #[export_fn] + pub fn distance_function(x1: FLOAT, y1: FLOAT, x2: FLOAT, y2: FLOAT) -> FLOAT { + ((y2 - y1).abs().powf(2.0) + (x2 -x1).abs().powf(2.0)).sqrt() + } +} + +#[test] +fn raw_fn_test() -> Result<(), Box> { + let mut engine = Engine::new(); + engine.register_fn("get_mystic_number", || { 42 as FLOAT }); + let mut m = Module::new(); + rhai::register_exported_fn!(m, "euclidean_distance".to_string(), raw_fn::distance_function); + let mut r = StaticModuleResolver::new(); + r.insert("Math::Advanced".to_string(), m); + engine.set_module_resolver(Some(r)); + + assert_eq!(engine.eval::( + r#"import "Math::Advanced" as math; + let x = math::euclidean_distance(0.0, 1.0, 0.0, get_mystic_number()); x"#)?, 41.0); + Ok(()) +} + +mod raw_fn_mut { + use rhai::FLOAT; + use rhai::export_fn; + + #[export_fn] + pub fn add_in_place(f1: &mut FLOAT, f2: FLOAT) { + *f1 += f2; + } +} + +#[test] +fn raw_fn_mut_test() -> Result<(), Box> { + let mut engine = Engine::new(); + engine.register_fn("get_mystic_number", || { 42 as FLOAT }); + let mut m = Module::new(); + rhai::register_exported_fn!(m, "add_in_place", raw_fn_mut::add_in_place); + let mut r = StaticModuleResolver::new(); + r.insert("Math::Advanced".to_string(), m); + engine.set_module_resolver(Some(r)); + + assert_eq!(engine.eval::( + r#"import "Math::Advanced" as math; + let x = get_mystic_number(); + math::add_in_place(x, 1.0); + x"#)?, 43.0); + Ok(()) +} + +mod raw_fn_str { + use rhai::export_fn; + + #[export_fn] + pub fn write_out_str(message: &str) -> bool { + eprintln!("{}", message); + true + } +} + +#[test] +fn raw_fn_str_test() -> Result<(), Box> { + let mut engine = Engine::new(); + engine.register_fn("get_mystic_number", || { 42 as FLOAT }); + let mut m = Module::new(); + rhai::register_exported_fn!(m, "write_out_str", raw_fn_str::write_out_str); + let mut r = StaticModuleResolver::new(); + r.insert("Host::IO".to_string(), m); + engine.set_module_resolver(Some(r)); + + assert_eq!(engine.eval::( + r#"import "Host::IO" as io; + let x = io::write_out_str("hello world!"); + x"#)?, true); + Ok(()) +} + +mod mut_opaque_ref { + use rhai::INT; + use rhai::export_fn; + + #[derive(Clone)] + pub struct StatusMessage { + os_code: Option, + message: String, + is_ok: bool + } + + #[export_fn] + pub fn new_message(is_ok: bool, message: &str) -> StatusMessage { + StatusMessage { + is_ok, + os_code: None, + message: message.to_string(), + } + } + + #[export_fn] + pub fn new_os_message(is_ok: bool, os_code: INT) -> StatusMessage { + StatusMessage { + is_ok, + os_code: Some(os_code), + message: format!("OS Code {}", os_code), + } + } + + #[export_fn] + pub fn write_out_message(message: &mut StatusMessage) -> bool { + eprintln!("{}", message.message); + true + } +} + +#[test] +fn mut_opaque_ref_test() -> Result<(), Box> { + let mut engine = Engine::new(); + let mut m = Module::new(); + rhai::register_exported_fn!(m, "new_message", mut_opaque_ref::new_message); + rhai::register_exported_fn!(m, "new_os_message", mut_opaque_ref::new_os_message); + rhai::register_exported_fn!(m, "write_out_message", mut_opaque_ref::write_out_message); + let mut r = StaticModuleResolver::new(); + r.insert("Host::Msg".to_string(), m); + engine.set_module_resolver(Some(r)); + + assert_eq!(engine.eval::( + r#"import "Host::Msg" as msg; + let message1 = msg::new_message(true, "it worked"); + let ok1 = msg::write_out_message(message1); + let message2 = msg::new_os_message(true, 0); + let ok2 = msg::write_out_message(message2); + ok1 && ok2"#)?, true); + Ok(()) +} diff --git a/codegen/tests/test_modules.rs b/codegen/tests/test_modules.rs new file mode 100644 index 00000000..ebc661a9 --- /dev/null +++ b/codegen/tests/test_modules.rs @@ -0,0 +1,165 @@ +use rhai::{EvalAltResult, FLOAT, INT, RegisterFn}; +use rhai::plugin::*; +use rhai::module_resolvers::*; + +pub mod empty_module { + use rhai::export_module; + + #[export_module] + pub mod EmptyModule { } +} + +#[test] +fn empty_module_test() -> Result<(), Box> { + let mut engine = Engine::new(); + let m = rhai::exported_module!(crate::empty_module::EmptyModule); + let mut r = StaticModuleResolver::new(); + r.insert("Module::Empty".to_string(), m); + engine.set_module_resolver(Some(r)); + + assert_eq!(engine.eval::( + r#"import "Module::Empty" as m; 42"#)?, 42); + Ok(()) +} + +pub mod one_fn_module { + use rhai::export_module; + + #[export_module] + pub mod advanced_math { + use rhai::FLOAT; + pub fn get_mystic_number() -> FLOAT { + 42.0 as FLOAT + } + } +} + +#[test] +fn one_fn_module_test() -> Result<(), Box> { + let mut engine = Engine::new(); + let m = rhai::exported_module!(crate::one_fn_module::advanced_math); + let mut r = StaticModuleResolver::new(); + r.insert("Math::Advanced".to_string(), m); + engine.set_module_resolver(Some(r)); + + assert_eq!(engine.eval::( + r#"import "Math::Advanced" as math; + let m = math::get_mystic_number(); + m"#)?, 42.0); + Ok(()) +} + +pub mod one_fn_and_const_module { + use rhai::export_module; + + #[export_module] + pub mod advanced_math { + use rhai::FLOAT; + + pub const MYSTIC_NUMBER: FLOAT = 42.0 as FLOAT; + + pub fn euclidean_distance(x1: FLOAT, y1: FLOAT, x2: FLOAT, y2: FLOAT) -> FLOAT { + ((y2 - y1).abs().powf(2.0) + (x2 -x1).abs().powf(2.0)).sqrt() + } + } +} + +#[test] +fn one_fn_and_const_module_test() -> Result<(), Box> { + let mut engine = Engine::new(); + let m = rhai::exported_module!(crate::one_fn_and_const_module::advanced_math); + let mut r = StaticModuleResolver::new(); + r.insert("Math::Advanced".to_string(), m); + engine.set_module_resolver(Some(r)); + + assert_eq!(engine.eval::( + r#"import "Math::Advanced" as math; + let m = math::MYSTIC_NUMBER; + let x = math::euclidean_distance(0.0, 1.0, 0.0, m); + x"#)?, 41.0); + Ok(()) +} + +pub mod raw_fn_str_module { + use rhai::export_module; + + #[export_module] + pub mod host_io { + pub fn write_out_str(message: &str) -> bool { + eprintln!("{}", message); + true + } + } +} + +#[test] +fn raw_fn_str_module_test() -> Result<(), Box> { + let mut engine = Engine::new(); + let m = rhai::exported_module!(crate::raw_fn_str_module::host_io); + let mut r = StaticModuleResolver::new(); + r.insert("Host::IO".to_string(), m); + engine.set_module_resolver(Some(r)); + + assert_eq!(engine.eval::( + r#"import "Host::IO" as io; + let x = io::write_out_str("hello world!"); + x"#)?, true); + Ok(()) +} + +pub mod mut_opaque_ref_module { + use rhai::INT; + use rhai::export_module; + + #[derive(Clone)] + pub struct StatusMessage { + os_code: Option, + message: String, + is_ok: bool + } + + #[export_module] + pub mod host_msg { + use super::{INT, StatusMessage}; + + pub fn new_message(is_ok: bool, message: &str) -> StatusMessage { + StatusMessage { + is_ok, + os_code: None, + message: message.to_string(), + } + } + + pub fn new_os_message(is_ok: bool, os_code: INT) -> StatusMessage { + StatusMessage { + is_ok, + os_code: Some(os_code), + message: format!("OS Code {}", os_code), + } + } + + pub fn write_out_message(message: &mut StatusMessage) -> bool { + eprintln!("{}", message.message); + true + } + } +} + +#[test] +fn mut_opaque_ref_test() -> Result<(), Box> { + let mut engine = Engine::new(); + let m = rhai::exported_module!(crate::mut_opaque_ref_module::host_msg); + let mut r = StaticModuleResolver::new(); + r.insert("Host::Msg".to_string(), m); + engine.set_module_resolver(Some(r)); + + assert_eq!(engine.eval::( + r#"import "Host::Msg" as msg; + let success = "it worked"; + let message1 = msg::new_message(true, success); + let ok1 = msg::write_out_message(message1); + let message2 = msg::new_os_message(true, 0); + let ok2 = msg::write_out_message(message2); + ok1 && ok2"#)?, true); + Ok(()) +} diff --git a/codegen/ui_tests/first_shared_ref.rs b/codegen/ui_tests/first_shared_ref.rs new file mode 100644 index 00000000..c8d6c9f4 --- /dev/null +++ b/codegen/ui_tests/first_shared_ref.rs @@ -0,0 +1,22 @@ +use rhai::export_fn; + +struct NonClonable { + a: f32, + b: u32, + c: char, + d: bool, +} + +#[export_fn] +pub fn test_fn(input: &NonClonable) -> bool { + input.d +} + +fn main() { + let n = NonClonable { a: 0.0, b: 10, c: 'a', d: true }; + if test_fn(n) { + println!("yes"); + } else { + println!("no"); + } +} diff --git a/codegen/ui_tests/first_shared_ref.stderr b/codegen/ui_tests/first_shared_ref.stderr new file mode 100644 index 00000000..a92ef117 --- /dev/null +++ b/codegen/ui_tests/first_shared_ref.stderr @@ -0,0 +1,11 @@ +error: references from Rhai in this position must be mutable + --> $DIR/first_shared_ref.rs:11:23 + | +11 | pub fn test_fn(input: &NonClonable) -> bool { + | ^^^^^^^^^^^^ + +error[E0425]: cannot find function `test_fn` in this scope + --> $DIR/first_shared_ref.rs:17:8 + | +17 | if test_fn(n) { + | ^^^^^^^ not found in this scope diff --git a/codegen/ui_tests/non_clonable.rs b/codegen/ui_tests/non_clonable.rs new file mode 100644 index 00000000..c5c6d5a2 --- /dev/null +++ b/codegen/ui_tests/non_clonable.rs @@ -0,0 +1,22 @@ +use rhai::export_fn; + +struct NonClonable { + a: f32, + b: u32, + c: char, + d: bool, +} + +#[export_fn] +pub fn test_fn(input: NonClonable) -> bool { + input.d +} + +fn main() { + let n = NonClonable { a: 0.0, b: 10, c: 'a', d: true }; + if test_fn(n) { + println!("yes"); + } else { + println!("no"); + } +} diff --git a/codegen/ui_tests/non_clonable.stderr b/codegen/ui_tests/non_clonable.stderr new file mode 100644 index 00000000..dbae1d47 --- /dev/null +++ b/codegen/ui_tests/non_clonable.stderr @@ -0,0 +1,5 @@ +error[E0277]: the trait bound `NonClonable: std::clone::Clone` is not satisfied + --> $DIR/non_clonable.rs:11:23 + | +11 | pub fn test_fn(input: NonClonable) -> bool { + | ^^^^^^^^^^^ the trait `std::clone::Clone` is not implemented for `NonClonable` diff --git a/codegen/ui_tests/non_clonable_second.rs b/codegen/ui_tests/non_clonable_second.rs new file mode 100644 index 00000000..d31e3436 --- /dev/null +++ b/codegen/ui_tests/non_clonable_second.rs @@ -0,0 +1,22 @@ +use rhai::export_fn; + +struct NonClonable { + a: f32, + b: u32, + c: char, + d: bool, +} + +#[export_fn] +pub fn test_fn(a: u32, b: NonClonable) -> bool { + a == 0 && b.d +} + +fn main() { + let n = NonClonable { a: 0.0, b: 10, c: 'a', d: true }; + if test_fn(10, n) { + println!("yes"); + } else { + println!("no"); + } +} diff --git a/codegen/ui_tests/non_clonable_second.stderr b/codegen/ui_tests/non_clonable_second.stderr new file mode 100644 index 00000000..26202d83 --- /dev/null +++ b/codegen/ui_tests/non_clonable_second.stderr @@ -0,0 +1,5 @@ +error[E0277]: the trait bound `NonClonable: std::clone::Clone` is not satisfied + --> $DIR/non_clonable_second.rs:11:27 + | +11 | pub fn test_fn(a: u32, b: NonClonable) -> bool { + | ^^^^^^^^^^^ the trait `std::clone::Clone` is not implemented for `NonClonable` diff --git a/codegen/ui_tests/return_mut_ref.rs b/codegen/ui_tests/return_mut_ref.rs new file mode 100644 index 00000000..5568beee --- /dev/null +++ b/codegen/ui_tests/return_mut_ref.rs @@ -0,0 +1,23 @@ +use rhai::export_fn; + +#[derive(Clone)] +struct Clonable { + a: f32, + b: u32, + c: char, + d: bool, +} + +#[export_fn] +pub fn test_fn(input: &mut Clonable) -> &mut bool { + &mut input.d +} + +fn main() { + let n = Clonable { a: 0.0, b: 10, c: 'a', d: true }; + if test_fn(n) { + println!("yes"); + } else { + println!("no"); + } +} diff --git a/codegen/ui_tests/return_mut_ref.stderr b/codegen/ui_tests/return_mut_ref.stderr new file mode 100644 index 00000000..04853c8b --- /dev/null +++ b/codegen/ui_tests/return_mut_ref.stderr @@ -0,0 +1,11 @@ +error: cannot return a reference to Rhai + --> $DIR/return_mut_ref.rs:12:38 + | +12 | pub fn test_fn(input: &mut Clonable) -> &mut bool { + | ^^^^^^^^^^^^ + +error[E0425]: cannot find function `test_fn` in this scope + --> $DIR/return_mut_ref.rs:18:8 + | +18 | if test_fn(n) { + | ^^^^^^^ not found in this scope diff --git a/codegen/ui_tests/return_pointer.rs b/codegen/ui_tests/return_pointer.rs new file mode 100644 index 00000000..fa20d73c --- /dev/null +++ b/codegen/ui_tests/return_pointer.rs @@ -0,0 +1,19 @@ +use rhai::export_fn; + +#[derive(Clone)] +struct Clonable { + a: f32, + b: u32, + c: char, + d: bool, +} + +#[export_fn] +pub fn test_fn(input: Clonable) -> *const str { + "yes" +} + +fn main() { + let n = Clonable { a: 0.0, b: 10, c: 'a', d: true }; + println!("{}", unsafe { let ptr = test_fn(n); *ptr }); +} diff --git a/codegen/ui_tests/return_pointer.stderr b/codegen/ui_tests/return_pointer.stderr new file mode 100644 index 00000000..bee7f1d3 --- /dev/null +++ b/codegen/ui_tests/return_pointer.stderr @@ -0,0 +1,11 @@ +error: cannot return a pointer to Rhai + --> $DIR/return_pointer.rs:12:33 + | +12 | pub fn test_fn(input: Clonable) -> *const str { + | ^^^^^^^^^^^^^ + +error[E0425]: cannot find function `test_fn` in this scope + --> $DIR/return_pointer.rs:18:39 + | +18 | println!("{}", unsafe { let ptr = test_fn(n); *ptr }); + | ^^^^^^^ not found in this scope diff --git a/codegen/ui_tests/return_shared_ref.rs b/codegen/ui_tests/return_shared_ref.rs new file mode 100644 index 00000000..384c3882 --- /dev/null +++ b/codegen/ui_tests/return_shared_ref.rs @@ -0,0 +1,19 @@ +use rhai::export_fn; + +#[derive(Clone)] +struct Clonable { + a: f32, + b: u32, + c: char, + d: bool, +} + +#[export_fn] +pub fn test_fn(input: Clonable) -> & 'static str { + "yes" +} + +fn main() { + let n = Clonable { a: 0.0, b: 10, c: 'a', d: true }; + println!("{}", test_fn(n)); +} diff --git a/codegen/ui_tests/return_shared_ref.stderr b/codegen/ui_tests/return_shared_ref.stderr new file mode 100644 index 00000000..6be110ae --- /dev/null +++ b/codegen/ui_tests/return_shared_ref.stderr @@ -0,0 +1,11 @@ +error: cannot return a reference to Rhai + --> $DIR/return_shared_ref.rs:12:33 + | +12 | pub fn test_fn(input: Clonable) -> & 'static str { + | ^^^^^^^^^^^^^^^^ + +error[E0425]: cannot find function `test_fn` in this scope + --> $DIR/return_shared_ref.rs:18:20 + | +18 | println!("{}", test_fn(n)); + | ^^^^^^^ not found in this scope diff --git a/codegen/ui_tests/second_shared_ref.rs b/codegen/ui_tests/second_shared_ref.rs new file mode 100644 index 00000000..cf09608d --- /dev/null +++ b/codegen/ui_tests/second_shared_ref.rs @@ -0,0 +1,23 @@ +use rhai::export_fn; + +#[derive(Clone)] +pub struct Clonable { + a: f32, + b: u32, + c: char, + d: bool, +} + +#[export_fn] +pub fn test_fn(input: Clonable, factor: &bool) -> bool { + input.d & factor +} + +fn main() { + let n = Clonable { a: 0.0, b: 10, c: 'a', d: true }; + if test_fn(n, &true) { + println!("yes"); + } else { + println!("no"); + } +} diff --git a/codegen/ui_tests/second_shared_ref.stderr b/codegen/ui_tests/second_shared_ref.stderr new file mode 100644 index 00000000..51908e82 --- /dev/null +++ b/codegen/ui_tests/second_shared_ref.stderr @@ -0,0 +1,11 @@ +error: this type in this position passes from Rhai by value + --> $DIR/second_shared_ref.rs:12:41 + | +12 | pub fn test_fn(input: Clonable, factor: &bool) -> bool { + | ^^^^^ + +error[E0425]: cannot find function `test_fn` in this scope + --> $DIR/second_shared_ref.rs:18:8 + | +18 | if test_fn(n, &true) { + | ^^^^^^^ not found in this scope diff --git a/src/any.rs b/src/any.rs index 9d28c768..e310787a 100644 --- a/src/any.rs +++ b/src/any.rs @@ -681,6 +681,13 @@ impl Dynamic { } } + /// Copy and return a `Dynamic` if it contains a type that can be trivially copied. + /// Returns `None` if the cast fails. + #[inline(always)] + pub fn downcast_clone(&self) -> Option { + self.downcast_ref::().map(|t| t.clone()) + } + /// Cast the `Dynamic` as the system integer type `INT` and return it. /// Returns the name of the actual type if the cast fails. pub fn as_int(&self) -> Result { diff --git a/src/fn_register.rs b/src/fn_register.rs index afc76295..55fb2218 100644 --- a/src/fn_register.rs +++ b/src/fn_register.rs @@ -43,7 +43,7 @@ pub trait RegisterPlugin { /// fn is_method_call(&self) -> bool { false } /// fn is_varadic(&self) -> bool { false } /// - /// fn call(&self, args: &[&mut Dynamic], pos: Position) -> Result> { + /// fn call(&self, args: &mut[&mut Dynamic], pos: Position) -> Result> { /// let x1: &FLOAT = args[0].downcast_ref::().unwrap(); /// let y1: &FLOAT = args[1].downcast_ref::().unwrap(); /// let x2: &FLOAT = args[2].downcast_ref::().unwrap(); @@ -55,6 +55,13 @@ pub trait RegisterPlugin { /// fn clone_boxed(&self) -> Box { /// Box::new(DistanceFunction()) /// } + /// + /// fn input_types(&self) -> Box<[std::any::TypeId]> { + /// vec![std::any::TypeId::of::(), + /// std::any::TypeId::of::(), + /// std::any::TypeId::of::(), + /// std::any::TypeId::of::()].into_boxed_slice() + /// } /// } /// /// // A simple custom plugin. This should not usually be done with hand-written code. diff --git a/src/lib.rs b/src/lib.rs index 1ba4cda1..b6177699 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -98,6 +98,8 @@ pub use syntax::{EvalContext, Expression}; pub use token::Position; pub use utils::calc_fn_spec as calc_fn_hash; +pub use rhai_codegen::*; + #[cfg(not(feature = "no_function"))] pub use parser::FnAccess; diff --git a/src/plugin.rs b/src/plugin.rs index 56e6af5d..01a652e3 100644 --- a/src/plugin.rs +++ b/src/plugin.rs @@ -1,6 +1,6 @@ //! Module defining plugins in Rhai. Is exported for use by plugin authors. -use crate::stdlib::boxed::Box; +use crate::stdlib::{any::TypeId, boxed::Box}; pub use crate::any::{Dynamic, Variant}; pub use crate::fn_native::{CallableFunction, FnCallArgs, IteratorFn}; @@ -38,7 +38,9 @@ pub trait PluginFunction { fn is_method_call(&self) -> bool; fn is_varadic(&self) -> bool; - fn call(&self, args: &[&mut Dynamic], pos: Position) -> Result>; + fn call(&self, args: &mut[&mut Dynamic], pos: Position) -> Result>; fn clone_boxed(&self) -> Box; + + fn input_types(&self) -> Box<[TypeId]>; }