diff --git a/Cargo.lock b/Cargo.lock index 5ebfc1cc8..bf86092fa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -797,12 +797,24 @@ dependencies = [ "colored", "derive_more", "gosub_shared", + "gosub_webinterop", "lazy_static", "serde_json", "thiserror", "v8", ] +[[package]] +name = "gosub_webinterop" +version = "0.1.0" +dependencies = [ + "lazy_static", + "proc-macro2", + "quote", + "serde", + "syn 2.0.50", +] + [[package]] name = "half" version = "2.3.1" diff --git a/crates/gosub_webexecutor/Cargo.toml b/crates/gosub_webexecutor/Cargo.toml index 69d2f313d..0dccebb98 100644 --- a/crates/gosub_webexecutor/Cargo.toml +++ b/crates/gosub_webexecutor/Cargo.toml @@ -15,4 +15,5 @@ v8 = "0.84.0" anyhow = "1.0.80" [dev-dependencies] -serde_json = "1.0.114" \ No newline at end of file +serde_json = "1.0.114" +gosub_webinterop = { path = "../gosub_webinterop" } \ No newline at end of file diff --git a/crates/gosub_webexecutor/src/test.rs b/crates/gosub_webexecutor/tests/interop.rs similarity index 50% rename from crates/gosub_webexecutor/src/test.rs rename to crates/gosub_webexecutor/tests/interop.rs index 53fbf6afd..bbaaaa10e 100644 --- a/crates/gosub_webexecutor/src/test.rs +++ b/crates/gosub_webexecutor/tests/interop.rs @@ -1,79 +1,167 @@ -use crate::js::{ - Args, IntoJSValue, JSContext, JSFunction, JSFunctionCallBack, JSFunctionCallBackVariadic, - JSFunctionVariadic, JSGetterCallback, JSInterop, JSObject, JSRuntime, JSSetterCallback, - JSValue, VariadicArgs, VariadicArgsInternal, -}; -//use webinterop::{web_fns, web_interop}; -use crate::js::v8::V8Engine; -use gosub_shared::types::Result; use std::cell::RefCell; use std::rc::Rc; -//#[web_interop] -// struct TestStruct { -// //#[property] -// field: i32, -// -// //#[property] -// field2: HashMap, //should crash it -// } -// -// //#[web_fns] -// impl TestStruct { -// fn add(&self, other: i32) -> i32 { -// self.field + other -// } -// -// fn add2(&mut self, other: i32) { -// self.field += other -// } -// -// fn add3(a: i32, b: i32) -> i32 { -// a + b -// } -// fn variadic(_nums: T) {} -// -// fn v8_variadic(_nums: V8Value) {} -// } - -//test, how we need to implement slices and vectors with refs (deref things) -// fn array_test() { -// let mut test_vec = vec![1, 2, 3]; -// -// vec(test_vec.clone()); //clone only needed for the test -// -// ref_vec(&test_vec); -// -// mut_vec(&mut test_vec); -// -// ref_slice(&test_vec); -// -// mut_slice(&mut test_vec); -// -// size_slice(<[i32; 3]>::try_from(test_vec.clone()).unwrap()); //clone only needed for the test -// -// ref_size_slice(&<[i32; 3]>::try_from(test_vec.clone()).unwrap()); //clone only needed for the test -// -// mut_size_slice(&mut <[i32; 3]>::try_from(test_vec.clone()).unwrap()); //clone only needed for the test -// } -// -// fn vec(_vec: Vec) {} -// -// #[allow(clippy::ptr_arg)] -// fn ref_vec(_vec: &Vec) {} -// -// #[allow(clippy::ptr_arg)] -// fn mut_vec(_vec: &mut Vec) {} -// -// fn ref_slice(_slice: &[i32]) {} -// -// fn mut_slice(_slice: &mut [i32]) {} -// -// fn size_slice(_array: [i32; 3]) {} -// -// fn ref_size_slice(_slice: &[i32; 3]) {} -// -// fn mut_size_slice(_slice: &mut [i32; 3]) {} +use gosub_shared::types::Result; +use gosub_v8::V8Engine; +use gosub_webexecutor::js::{ + Args, IntoJSValue, IntoRustValue, JSContext, JSFunction, JSFunctionCallBack, + JSFunctionCallBackVariadic, JSFunctionVariadic, JSGetterCallback, JSInterop, JSObject, + JSRuntime, JSSetterCallback, JSValue, VariadicArgs, VariadicArgsInternal, +}; +use gosub_webinterop::{web_fns, web_interop}; + +#[web_interop] +struct TestStruct { + #[property] + field: i32, + #[property] + field2: u32, +} + +#[web_fns(1)] +impl TestStruct { + fn add(&self, other: i32) -> i32 { + self.field + other + } + + fn add2(&mut self, other: i32) { + self.field += other + } + + fn add3(a: i32, b: i32) -> i32 { + a + b + } + + fn vec_test(&self, vec: Vec) -> Vec { + vec + } + + fn slice_test<'a>(&self, slice: &'a [i32]) -> &'a [i32] { + slice + } + + fn tuple(&self, tuple: (i32, String)) -> (i32, String) { + tuple + } + + fn array_test(&self, array: &[[i32; 3]; 3]) { + println!("{:?}", array); + } + + fn array_test2(&self, array: &[[i32; 3]; 3]) -> Vec { + array.iter().flatten().copied().collect() + } + + fn variadic2(args: &impl VariadicArgs) { + for a in args.as_vec() { + println!("got an arg...: {}", a.as_string().unwrap()); + } + } + + fn variadic3(num: i32, args: &impl VariadicArgs) { + println!("got num arg...: {}", num); + for a in args.as_vec() { + println!("got an arg...: {}", a.as_string().unwrap()); + } + } + + fn variadic3_2(num: i32, args: &impl VariadicArgs) -> Vec { + let mut vec = Vec::new(); + vec.push(num); + for a in args.as_vec() { + vec.push(a.as_number().unwrap() as i32); + } + + vec + } + + fn variadic4(_args: &RT::VariadicArgs) {} + + #[generic(I, i32, String)] + fn variadic5(_i: I, _args: &RT::VariadicArgs, _ctx: &RT::Context) {} + + fn test222() {} + + fn uses_ctx(_ctx: &impl JSContext) {} + + #[generic(T, i32, String)] + fn generic(_num: i32, _val: T) {} + + #[generic(T1, i32)] + fn generic2(_num: u64, _val: impl T1) {} +} + +trait T1 {} + +impl T1 for i32 {} + +impl T1 for String {} + +#[test] +fn macro_interop() { + let test_struct = TestStruct { + field: 14, + field2: 14, + }; + + let mut engine = V8Engine::new(); + let mut context = engine.new_context().unwrap(); + + TestStruct::implement::(Rc::new(RefCell::new(test_struct)), context.clone()).unwrap(); + + let out = context + .run( + r#" + let calls = [] + calls.push([TestStruct.add(3), TestStruct.field, TestStruct.field2]) + calls.push([TestStruct.add2(3), TestStruct.field, TestStruct.field2]) + calls.push([TestStruct.add3(3, 4), TestStruct.field, TestStruct.field2]) + calls.push([TestStruct.vec_test([1, 2, 3]), TestStruct.field, TestStruct.field2]) + calls.push([TestStruct.slice_test([1, 2, 3]), TestStruct.field, TestStruct.field2]) + calls.push([TestStruct.tuple([1, "2"]), TestStruct.field, TestStruct.field2]) + calls.push([TestStruct.array_test([[1, 2, 3], [4, 5, 6], [7, 8, 9]]), TestStruct.field, TestStruct.field2]) + calls.push([TestStruct.array_test2([[1, 2, 3], [4, 5, 6], [7, 8, 9]]), TestStruct.field, TestStruct.field2]) + calls.push([TestStruct.variadic2(1, 2, 3, 4, 5, 6, 7, 8, 9, 10), TestStruct.field, TestStruct.field2]) + calls.push([TestStruct.variadic3(1, 2, 3, 4, 5, 6, 7, 8, 9, 10), TestStruct.field, TestStruct.field2]) + calls.push([TestStruct.variadic3_2(1, 2, 3, 4, 5, 6, 7, 8, 9, 10), TestStruct.field, TestStruct.field2]) + calls.push([TestStruct.variadic4(1, 2, 3, 4, 5, 6, 7, 8, 9, 10), TestStruct.field, TestStruct.field2]) + calls.push([TestStruct.variadic5(1, 2, 3, 4, 5, 6, 7, 8, 9, 10), TestStruct.field, TestStruct.field2]) + calls.push([TestStruct.test222(), TestStruct.field, TestStruct.field2]) + calls.push([TestStruct.uses_ctx(), TestStruct.field, TestStruct.field2]) + calls.push([TestStruct.generic(1, "hello"), TestStruct.field, TestStruct.field2]) + calls.push([TestStruct.generic2(1, 2), TestStruct.field, TestStruct.field2]) + + calls + "#).expect("failed to run"); + + let mut expected = vec![ + "17,14,14", + ",17,14", + "7,17,14", + "1,2,3,17,14", + "1,2,3,17,14", + "1,2,17,14", + ",17,14", + "1,2,3,4,5,6,7,8,9,17,14", + ",17,14", + ",17,14", + "1,2,3,4,5,6,7,8,9,10,17,14", + ",17,14", + ",17,14", + ",17,14", + ",17,14", + ",17,14", + ",17,14", + ]; + + let arr = out + .as_array() + .expect("failed to get array from run ret value"); + + for v in arr { + assert_eq!(v.as_string().unwrap(), expected.remove(0)); + } +} #[derive(Debug)] struct Test2 { @@ -103,8 +191,26 @@ impl Test2 { println!("got an arg...: {}", a.as_string().unwrap()); } } + + fn generic(&self, val: A, _val2: B) -> A { + val + } } +trait I32 {} + +impl I32 for i32 {} + +impl I32 for String {} + +impl I32 for bool {} + +trait U64 {} + +impl U64 for u64 {} + +impl U64 for String {} + impl JSInterop for Test2 { //this function will be generated by a macro fn implement(s: Rc>, mut ctx: RT::Context) -> Result<()> { @@ -116,7 +222,7 @@ impl JSInterop for Test2 { let s = Rc::clone(&s); Box::new(move |cb: &mut RT::GetterCB| { let ctx = cb.context(); - let value: i32 = s.borrow().field; + let value = s.borrow().field; println!("got a call to getter: {}", value); let value = match value.to_js_value(ctx.clone()) { Ok(value) => value, @@ -264,7 +370,7 @@ impl JSInterop for Test2 { return; }; - let Ok(arg0) = arg0.as_string() else { + let Ok(arg0) = arg0.to_rust_value() else { cb.error("failed to convert argument"); return; }; @@ -292,7 +398,7 @@ impl JSInterop for Test2 { return; }; - let Ok(arg0) = arg0.as_string() else { + let Ok(arg0) = arg0.to_rust_value() else { cb.error("failed to convert argument"); return; }; @@ -325,12 +431,98 @@ impl JSInterop for Test2 { obj.set_method_variadic("variadic", &variadic)?; + let generic = { + let s = Rc::clone(&s); + RT::Function::new(ctx.clone(), move |cb| { + let num_args = 1; //function.arguments.len(); + if num_args != cb.len() { + cb.error("wrong number of arguments"); + return; + } + + let ctx = cb.context(); + + let Some(arg0) = cb.args().get(0, ctx.clone()) else { + cb.error("failed to get argument"); + return; + }; + + let Some(arg1) = cb.args().get(1, ctx.clone()) else { + cb.error("failed to get argument"); + return; + }; + + if arg0.is_number() { + let Ok(arg0): Result = arg0.to_rust_value() else { + cb.error("failed to convert argument"); + return; + }; + + if arg1.is_number() { + let Ok(arg1): Result = arg1.to_rust_value() else { + cb.error("failed to convert argument"); + return; + }; + + let ret = s + .borrow() + .generic(arg0, arg1) + .to_js_value(ctx.clone()) + .unwrap(); + + cb.ret(ret); + } + } + + if arg0.is_string() { + let Ok(arg0): Result = arg0.to_rust_value() else { + cb.error("failed to convert argument"); + return; + }; + + if arg1.is_number() { + let Ok(arg1): Result = arg1.to_rust_value() else { + cb.error("failed to convert argument"); + return; + }; + + let ret = s + .borrow() + .generic(arg0, arg1) + .to_js_value(ctx.clone()) + .unwrap(); + + cb.ret(ret); + + return; + } + + if arg1.is_string() { + let Ok(arg1): Result = arg1.to_rust_value() else { + cb.error("failed to convert argument"); + return; + }; + + let ret = s + .borrow() + .generic(arg0, arg1) + .to_js_value(ctx.clone()) + .unwrap(); + + cb.ret(ret); + } + } + })? + }; + + obj.set_method("generic", &generic)?; + Ok(()) } } #[test] -fn manual_js_inop() { +fn manual_js_interop() { let mut engine = V8Engine::new(); let mut context = engine.new_context().unwrap(); diff --git a/crates/gosub_webinterop/Cargo.toml b/crates/gosub_webinterop/Cargo.toml new file mode 100644 index 000000000..b73bee3ca --- /dev/null +++ b/crates/gosub_webinterop/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "gosub_webinterop" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +syn = { version = "2.0.43", features = ["full", "extra-traits"] } +quote = { version = "1.0.33", features = [] } +proc-macro2 = "1.0.69" +serde = { version = "1.0.193", features = ["derive"] } +lazy_static = "1.4.0" + + +[lib] +proc-macro = true diff --git a/crates/gosub_webinterop/src/function.rs b/crates/gosub_webinterop/src/function.rs new file mode 100644 index 000000000..aad0a20e7 --- /dev/null +++ b/crates/gosub_webinterop/src/function.rs @@ -0,0 +1,279 @@ +use proc_macro2::{Ident, TokenStream}; +use quote::{format_ident, quote}; +use syn::{GenericParam, Path}; + +use crate::types::executor::Executor; +use crate::types::{Arg, ArgVariant, Generics, Primitive, ReturnType, SelfType}; + +#[derive(Clone, Debug)] +pub(crate) struct Function { + pub(crate) name: String, + pub(crate) ident: Ident, + pub(crate) arguments: Vec, + pub(crate) self_type: SelfType, + #[allow(unused)] + pub(crate) return_type: ReturnType, + //needed later + pub(crate) executor: Executor, + pub(crate) generics: Vec, + pub(crate) func_generics: syn::Generics, + pub(crate) variadic: bool, + pub(crate) needs_ctx: bool, +} + +impl Function { + pub(crate) fn implement(&self, name: &Ident) -> TokenStream { + assert!(self.executor.is_js()); + + let ident = &self.ident; + let func_name = &self.name; + let mut num_args = self.arguments.len(); + + let var_func = if self.variadic { + quote! { FunctionVariadic } + } else { + quote! { Function } + }; + let set_method = if self.variadic { + quote! { set_method_variadic } + } else { + quote! { set_method } + }; + + let args_call = self.args_and_call(name); + + if self.needs_ctx { + num_args -= 1; + } + + let len_check = if self.variadic { + num_args -= 1; + + quote! { + if cb.len() <= #num_args { + cb.error("wrong number of arguments"); + return; + } + } + } else { + quote! { + if cb.len() != #num_args { + cb.error("wrong number of arguments"); + return; + } + } + }; + + let clone = if self.self_type == SelfType::NoSelf { + TokenStream::new() + } else { + quote! { let s = Rc::clone(&s); } + }; + + let args = if self.arguments.is_empty() { + TokenStream::new() + } else { + quote! { let args = cb.args(); } + }; + + quote! { + let #ident = { + #clone + RT::#var_func::new(ctx.clone(), move |cb| { + #len_check + + let ctx = cb.context(); + + #args + + #args_call + })? + }; + + obj.#set_method(#func_name, &#ident)?; + } + } + + fn args_and_call(&self, name: &Ident) -> TokenStream { + if !self.generics.is_empty() { + return self.generic_call(name); + } + + let prepare_args = self.prepare_args(); + + let call = self.call(name); + quote! { + #prepare_args + + #call + } + } + + fn prepare_args(&self) -> TokenStream { + if self.arguments.is_empty() { + return TokenStream::new(); + } + + let mut prepared_args = Vec::with_capacity(self.arguments.len()); + + for arg in &self.arguments { + prepared_args.push(arg.prepare()); + } + + quote! { + #(#prepared_args)* + } + } + + fn generic_call(&self, name: &Ident) -> TokenStream { + let non_generic = self.prepare_args(); + let call = self.call(name); + + let mut generic_args = Vec::new(); + + for arg in &self.arguments { + if arg.variant == ArgVariant::Generic { + generic_args.push(arg); + } + } + + let generic = self.generic(&mut generic_args, call); + + quote! { + #non_generic + + #generic + } + } + + fn match_generics(&self, arg: &Arg) -> Vec<(Path, Primitive)> { + let matches: Vec<_> = self + .generics + .iter() + .filter_map(|matcher| { + if matcher + .matcher + .is_match(&arg.ty.ty.generics().unwrap(), arg.index) + { + Some(matcher.types.clone()) + } else { + None + } + }) + .collect(); + + assert_eq!( + matches.len(), + 1, + "Multiple or no matches found for generic type: {} matches, expected 1", + matches.len() + ); + + matches.first().unwrap().clone() + } + + fn generic(&self, args: &mut Vec<&Arg>, prev: TokenStream) -> TokenStream { + let Some(arg) = args.pop() else { + return prev; + }; + let arg_name = format_ident!("arg{}", arg.index); + + let mut out = TokenStream::new(); + + let js_types = self.match_generics(arg); + + for js_ty in js_types { + let check = js_ty.1.get_check(&arg_name); + let ty = js_ty.0; + + out.extend(quote! { + if #check { + let Ok(#arg_name): Result<#ty> = #arg_name.to_rust_value() else { + cb.error("failed to convert argument"); + return; + }; + + #prev + } else + }); + } + + out.extend(quote! { + { + cb.error("failed to convert argument"); + return; + } + }); + + self.generic(args, out) + } + + pub(crate) fn call_args(&self) -> TokenStream { + if self.arguments.is_empty() { + return TokenStream::new(); + } + + let mut call_args = Vec::with_capacity(self.arguments.len()); + for (index, arg) in self.arguments.iter().enumerate() { + call_args.push(arg.call(index)); + } + + quote! { + #(#call_args),* + } + } + fn call(&self, name: &Ident) -> TokenStream { + let ident = &self.ident; + let call_args = self.call_args(); + + let func = { + match self.self_type { + SelfType::NoSelf => quote! { #name::#ident }, + SelfType::SelfRef => quote! { s.borrow().#ident }, + SelfType::SelfMutRef => quote! { s.borrow_mut().#ident }, + } + }; + let func_generics = self.get_generics(); + + quote! { + let ret = match #func #func_generics(#call_args).to_js_value(ctx.clone()) { + Ok(ret) => ret, + Err(e) => { + cb.error(e); + return; + } + }; + cb.ret(ret); + } + } + + fn get_generics(&self) -> TokenStream { + if !self + .func_generics + .params + .iter() + .any(|gen| matches!(gen, GenericParam::Type(_))) + { + return TokenStream::new(); + } + + let mut generics = Vec::new(); + + for gen in &self.func_generics.params { + if let GenericParam::Type(p) = &gen { + if p.ident == "VariadicArgs" { + generics.push(quote! { RT::VariadicArgs }); + continue; + } + if p.ident == "RT" || p.ident == "Runtime" { + generics.push(quote! { RT }); + continue; + } + + generics.push(quote! { _ }); + } + } + + quote! { ::<#(#generics),*> } + } +} diff --git a/crates/gosub_webinterop/src/impl_function.rs b/crates/gosub_webinterop/src/impl_function.rs new file mode 100644 index 000000000..904ce74e8 --- /dev/null +++ b/crates/gosub_webinterop/src/impl_function.rs @@ -0,0 +1,61 @@ +use proc_macro2::Ident; +use proc_macro2::TokenStream; +use quote::{format_ident, quote}; + +use crate::function::Function; +use crate::utils::crate_name; +use crate::{Options, STATE}; + +pub fn impl_js_functions(functions: &[Function], name: &Ident, options: &Options) -> TokenStream { + let mut impls = Vec::new(); + for function in functions { + if !function.executor.is_js() { + continue; + } + + impls.push(function.implement(name)); + } + + let marker_struct = if let Some(marker_struct) = options.marker_struct.as_ref() { + marker_struct.clone() + } else { + format_ident!("{}JSMethodsMarker", name) + }; + + let marker_trait = if let Some(marker_trait) = options.marker_trait.as_ref() { + marker_trait.clone() + } else { + format_ident!("{}JSMethods", name) + }; + + let refs = get_refs(name.to_string(), options.refs); + + quote! { + impl #marker_trait for #refs #marker_struct { + #[inline(always)] + fn implement(&self, obj: &mut RT::Object, s: Rc>, ctx: RT::Context) -> Result<()> { + #(#impls)* + + (*self).implement::(obj, s, ctx) + } + } + } +} + +fn get_refs(name: String, num_refs: Option) -> TokenStream { + let num_refs = num_refs.unwrap_or_else(|| { + let mut state = STATE.write().unwrap(); + let num_refs = state + .get_mut(&(crate_name(), name)) + .expect("Struct does not have the #[web_interop] attribute"); + *num_refs += 1; + *num_refs + }); + let mut refs = TokenStream::new(); + + for _ in 0..num_refs { + refs.extend(quote! { & }) + } + + refs +} diff --git a/crates/gosub_webinterop/src/impl_interop_struct.rs b/crates/gosub_webinterop/src/impl_interop_struct.rs new file mode 100644 index 000000000..cc428db27 --- /dev/null +++ b/crates/gosub_webinterop/src/impl_interop_struct.rs @@ -0,0 +1,38 @@ +use proc_macro2::{Ident, TokenStream}; +use quote::{format_ident, quote}; + +use crate::types::Field; + +pub fn impl_interop_struct(name: Ident, fields: &[Field]) -> TokenStream { + let marker_struct = format_ident!("{}JSMethodsMarker", name); + let marker_trait = format_ident!("{}JSMethods", name); + + let getters_setters = Field::getters_setters(fields); + + quote! { + impl JSInterop for #name { + fn implement(s: Rc>, mut ctx: RT::Context) -> Result<()> { + let mut obj = ctx.new_global_object(stringify!(#name))?; + + #getters_setters + + (&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&#marker_struct) + .implement::(&mut obj, s, ctx)?; + + Ok(()) + } + } + + struct #marker_struct; + trait #marker_trait { + fn implement(&self, obj: &mut RT::Object, s: Rc>, ctx: RT::Context) -> Result<()>; + } + + impl #marker_trait for #marker_struct { + #[inline(always)] + fn implement(&self, _: &mut RT::Object, _: Rc>, _: RT::Context) -> Result<()> { + Ok(()) + } + } + } +} diff --git a/crates/gosub_webinterop/src/lib.rs b/crates/gosub_webinterop/src/lib.rs new file mode 100644 index 000000000..3d5909e26 --- /dev/null +++ b/crates/gosub_webinterop/src/lib.rs @@ -0,0 +1,202 @@ +extern crate proc_macro; + +use proc_macro::TokenStream; +use std::collections::HashMap; +use std::sync::RwLock; + +use lazy_static::lazy_static; +use proc_macro2::{Ident, TokenTree}; +use quote::ToTokens; +use syn::spanned::Spanned; +use syn::{FnArg, ItemImpl, ItemStruct}; + +use crate::function::Function; +use crate::impl_function::impl_js_functions; +use crate::impl_interop_struct::impl_interop_struct; +use crate::property::{FieldProperty, FunctionProperty}; +use crate::types::{Arg, ArgVariant, Field, GenericsMatcher, ReturnType, SelfType}; +use crate::utils::crate_name; + +mod function; +mod impl_function; +mod impl_interop_struct; +mod property; +mod types; +mod utils; + +lazy_static! { + static ref STATE: RwLock> = RwLock::new(HashMap::new()); +} + +#[proc_macro_attribute] +pub fn web_interop(_: TokenStream, item: TokenStream) -> TokenStream { + let mut fields: Vec = Vec::new(); + + let mut input: ItemStruct = syn::parse_macro_input!(item); + + for field in &mut input.fields { + if let Some(property) = FieldProperty::parse(&mut field.attrs) { + let f = Field { + name: property + .rename + .unwrap_or(field.ident.as_ref().unwrap().to_string()), + executor: property.executor, + ty: field.ty.clone(), + ident: field.ident.as_ref().unwrap().clone(), + }; + + fields.push(f); + } + } + + let extend = impl_interop_struct(input.ident.clone(), &fields); + + let name = input.ident.clone().into_token_stream().to_string(); + STATE.write().unwrap().insert((crate_name(), name), 0); + + let mut out = input.into_token_stream(); + out.extend(extend); + + out.into() +} + +#[proc_macro_attribute] +pub fn web_fns(attr: TokenStream, item: TokenStream) -> TokenStream { + // let item = preprocess_variadic(item); // custom `...` syntax for variadic functions, but it breaks code editors + let mut input: ItemImpl = { + let item = item.clone(); + syn::parse_macro_input!(item) + }; + + let mut functions: Vec = Vec::new(); + + for func in &mut input.items { + if let syn::ImplItem::Fn(method) = func { + let args = &method.sig.inputs; + + let property = FunctionProperty::parse(&mut method.attrs).unwrap_or_default(); + + let name = property.rename.unwrap_or(method.sig.ident.to_string()); + let mut func = Function { + ident: Ident::new(&name, method.sig.ident.span()), + name, + arguments: Vec::with_capacity(args.len()), // we don't know if the first is self, so no args.len() - 1 + self_type: SelfType::NoSelf, + return_type: ReturnType::parse(&method.sig.output) + .expect("failed to parse return type"), + executor: property.executor, + generics: GenericsMatcher::get_matchers(property.generics, method), + func_generics: method.sig.generics.clone(), + variadic: false, + needs_ctx: false, + }; + + if let Some(FnArg::Receiver(self_arg)) = args.first() { + if self_arg.reference.is_none() { + panic!("Self must be a reference"); + } + + match self_arg.mutability { + Some(_) => func.self_type = SelfType::SelfMutRef, + None => func.self_type = SelfType::SelfRef, + }; + } + + let mut index = 0; + for arg in args { + if let FnArg::Typed(arg) = arg { + let arg = + Arg::parse(&arg.ty, index, &func.generics).expect("failed to parse arg"); + if arg.variant == ArgVariant::Variadic { + func.variadic = true; + } else if arg.variant == ArgVariant::Context { + func.needs_ctx = true; + } + func.arguments.push(arg); + index += 1; + } + } + + if func.variadic { + if let Some(arg) = func.arguments.last() { + if arg.variant != ArgVariant::Variadic { + if arg.variant != ArgVariant::Context { + panic!("Variadic argument must be the last argument111"); + } + //get second last + if func.arguments.len() <= 1 { + panic!("Variadic argument must be the last argument222"); + } + if let Some(arg) = func.arguments.get(func.arguments.len() - 2) { + if arg.variant != ArgVariant::Variadic { + panic!("Variadic argument must be the last argument333"); + } + } + } + } + } + + if func.needs_ctx { + if let Some(arg) = func.arguments.last() { + if arg.variant != ArgVariant::Context { + panic!("Context argument must be the last argument"); + } + } + } + + functions.push(func); + } + } + + let name = Ident::new( + &input.self_ty.to_token_stream().to_string(), + input.self_ty.span(), + ); + + let options = parse_attrs(attr); + + let extend = impl_js_functions(&functions, &name, &options); + + let mut out = input.into_token_stream(); + out.extend(extend); + + out.into() +} + +struct Options { + refs: Option, + marker_struct: Option, + marker_trait: Option, +} + +fn parse_attrs(attrs: TokenStream) -> Options { + let attrs: proc_macro2::TokenStream = attrs.into(); + let mut options = Options { + refs: None, + marker_struct: None, + marker_trait: None, + }; + + for item in attrs { + match item { + TokenTree::Ident(i) => { + if options.marker_struct.is_none() { + options.marker_struct = Some(i); + } else if options.marker_trait.is_none() { + options.marker_trait = Some(i); + } + } + + TokenTree::Literal(l) => { + let num = l.to_string(); + let Ok(num) = num.parse::() else { continue }; + + options.refs = Some(num); + } + + _ => {} + } + } + + options +} diff --git a/crates/gosub_webinterop/src/property.rs b/crates/gosub_webinterop/src/property.rs new file mode 100644 index 000000000..e865ffd51 --- /dev/null +++ b/crates/gosub_webinterop/src/property.rs @@ -0,0 +1,215 @@ +use quote::{format_ident, ToTokens}; +use syn::{Attribute, LitStr, Meta}; + +use crate::types::executor::Executor; +use crate::types::{GenericProperty, Primitive}; + +pub(crate) struct FieldProperty { + pub(crate) rename: Option, + pub(crate) executor: Executor, +} + +pub(crate) struct FunctionProperty { + pub(crate) rename: Option, + pub(crate) executor: Executor, + pub(crate) generics: Vec, +} + +impl Default for FieldProperty { + fn default() -> Self { + Self { + rename: None, + executor: Executor::Both, + } + } +} + +impl Default for FunctionProperty { + fn default() -> Self { + Self { + rename: None, + executor: Executor::Both, + generics: Vec::new(), + } + } +} + +impl FieldProperty { + pub(crate) fn parse(attrs: &mut Vec) -> Option { + let mut remove_attrs = None; + let mut property = None; + + for (index, attr) in attrs.iter().enumerate() { + if attr.path().is_ident("property") { + property = Some(FieldProperty { + rename: None, + executor: Executor::Both, + }); + + //rename = "____", js => rename to name, and it is a js only property + //rename = "____", wasm => rename to name, and it is a wasm only property + //rename = "____" => rename to name, and it is a property for both, js and wasm + //js => name is the same, and it is a js only property + //wasm => name is the same, and it is a wasm only property + // => name is the same, and it is a property for both, js and wasm + + match &attr.meta { + Meta::Path(_) => {} + Meta::List(_) => { + attr.parse_nested_meta(|meta| { + match &meta.path { + path if path.is_ident("rename") => { + let lit: LitStr = meta.value()?.parse()?; + + property.as_mut().unwrap().rename = Some(lit.value()); + } + path if path.is_ident("js") => { + if property.as_mut().unwrap().executor != Executor::Both { + panic!("Executor cannot be specified twice!") + } + property.as_mut().unwrap().executor = Executor::JS; + } + path if path.is_ident("wasm") => { + if property.as_mut().unwrap().executor != Executor::Both { + panic!("Executor cannot be specified twice!") + } + property.as_mut().unwrap().executor = Executor::WASM; + } + path if path.is_ident("none") => { + if property.as_mut().unwrap().executor != Executor::Both { + panic!("Executor cannot be specified twice!") + } + property.as_mut().unwrap().executor = Executor::None; + } + _ => Err(syn::Error::new_spanned( + attr, + "Unknown attribute in property attribute", + ))?, + } + + Ok(()) + }) + .unwrap(); + } + Meta::NameValue(_) => { + panic!("Unexpected NameValue in property attribute"); + } + } + + remove_attrs = Some(index); + } + } + + if let Some(index) = remove_attrs { + attrs.remove(index); + } + + property + } +} + +impl FunctionProperty { + pub(crate) fn parse(attrs: &mut Vec) -> Option { + let mut remove_attrs = Vec::new(); + let mut property = None; + + for (index, attr) in attrs.iter().enumerate() { + if attr.path().is_ident("property") { + property = Some(FunctionProperty::default()); + + match &attr.meta { + Meta::Path(_) => {} + Meta::List(_) => { + attr.parse_nested_meta(|meta| { + match &meta.path { + path if path.is_ident("rename") => { + let lit: LitStr = meta.value()?.parse()?; + + property.as_mut().unwrap().rename = Some(lit.value()); + } + path if path.is_ident("js") => { + if property.as_mut().unwrap().executor != Executor::Both { + panic!("Executor cannot be specified twice!") + } + property.as_mut().unwrap().executor = Executor::JS; + } + path if path.is_ident("wasm") => { + if property.as_mut().unwrap().executor != Executor::Both { + panic!("Executor cannot be specified twice!") + } + property.as_mut().unwrap().executor = Executor::WASM; + } + path if path.is_ident("none") => { + if property.as_mut().unwrap().executor != Executor::Both { + panic!("Executor cannot be specified twice!") + } + property.as_mut().unwrap().executor = Executor::None; + } + + path => Err(syn::Error::new_spanned( + attr, + format_ident!( + "Unknown attribute in property attribute {}", + path.to_token_stream().to_string() + ), + ))?, + } + + Ok(()) + }) + .unwrap(); + } + Meta::NameValue(_) => { + panic!("Unexpected NameValue in property attribute"); + } + } + + remove_attrs.push(index); + } + + if attr.path().is_ident("generic") { + if property.is_none() { + property = Some(FunctionProperty::default()); + } + + if matches!(attr.meta, Meta::List(_)) { + let mut name_found = false; + let mut param = None; + let mut types = Vec::new(); + attr.parse_nested_meta(|meta| { + if name_found { + let prim = Primitive::get(&meta.path.to_token_stream().to_string()); + if types.iter().any(|(_, p)| p == &prim) { + panic!("Cannot have multiple {:?}s in generic attribute", prim); + } + types.push((meta.path, prim)); + } else { + param = Some(meta.path); + name_found = true; + } + Ok(()) + }) + .unwrap(); + + let param = param.expect("Expected param in generic attribute"); + + property + .as_mut() + .unwrap() + .generics + .push(GenericProperty { param, types }); + } else { + panic!("Unexpected NameValue in generic attribute"); + } + + remove_attrs.push(index); + } + } + + for index in remove_attrs { + attrs.remove(index); + } + + property + } +} diff --git a/crates/gosub_webinterop/src/types.rs b/crates/gosub_webinterop/src/types.rs new file mode 100644 index 000000000..c409ba008 --- /dev/null +++ b/crates/gosub_webinterop/src/types.rs @@ -0,0 +1,22 @@ +use std::fmt::Debug; + +pub(crate) use args::*; +use executor::Executor; +pub(crate) use field::*; +pub(crate) use generics::*; +pub(crate) use primitive::*; +pub(crate) use slice::*; +pub(crate) use ty::*; +mod args; +pub mod executor; +mod field; +mod generics; +mod primitive; +mod slice; +mod ty; + +#[derive(Clone, PartialEq, Debug)] +pub(crate) struct PropertyOptions { + pub(crate) executor: Executor, + pub(crate) rename: Option, +} diff --git a/crates/gosub_webinterop/src/types/args.rs b/crates/gosub_webinterop/src/types/args.rs new file mode 100644 index 000000000..8ffe829e6 --- /dev/null +++ b/crates/gosub_webinterop/src/types/args.rs @@ -0,0 +1,179 @@ +use proc_macro2::TokenStream; +use quote::{format_ident, quote, ToTokens}; +use syn::punctuated::Punctuated; +use syn::{Path, Token, TypeParamBound}; + +use crate::types::{handle_slice_conv, Generics, Reference, Type, TypeT}; + +#[derive(Clone, PartialEq, Debug)] +pub(crate) struct Arg { + pub(crate) index: usize, + pub(crate) ty: Type, + pub(crate) variant: ArgVariant, +} + +impl Arg { + pub(crate) fn prepare(&self) -> TokenStream { + let index = self.index; + let arg_name = format_ident!("arg{}", index); + + let rust_type = &self.ty.ty(); + + let conv = handle_slice_conv(&self.ty, &arg_name); + + let mutability = &self + .ty + .mutability(conv.to_string() == TokenStream::new().to_string()); + + let get_args = if index == 0 { + quote! { + args.variadic(ctx.clone()) + } + } else { + quote! { + args.variadic_start(#index, ctx.clone()) + } + }; + + match self.variant { + ArgVariant::Variadic => { + quote! { + let #mutability #arg_name = #get_args; + } + } + ArgVariant::Generic => { + quote! { + let Some(#arg_name) = args.get(#index, ctx.clone()) else { + cb.error("failed to get argument"); + return; + }; + } + } + ArgVariant::Normal => { + quote! { + let Some(#arg_name) = args.get(#index, ctx.clone()) else { + cb.error("failed to get argument"); + return; + }; + + let Ok(#mutability #arg_name): Result<#rust_type> = #arg_name.to_rust_value() else { + cb.error("failed to convert argument"); + return; + }; + + #conv + } + } + + _ => TokenStream::new(), + } + } + + pub(crate) fn call(&self, index: usize) -> TokenStream { + let arg_name = format_ident!("arg{}", index); + if self.variant == ArgVariant::Context { + return match self.ty.reference { + Reference::Ref => quote! { &ctx }, + Reference::MutRef => panic!("Context argument cannot be referenced mutable"), + Reference::None => quote! { ctx.clone() }, + }; + } + + let reference = &self.ty.get_reference(); + + quote! { #reference #arg_name } + } +} + +#[derive(Clone, PartialEq, Debug)] +pub(crate) enum ArgVariant { + Normal, + Variadic, + Context, + Generic, +} + +impl Arg { + pub(crate) fn parse( + arg: &syn::Type, + index: usize, + generics: &[Generics], + ) -> Result { + let ty = Type::parse(arg, true)?; + let mut variant = ArgVariant::Normal; + + match &ty.ty { + TypeT::Type(p) => { + if let Some(s) = p.segments.last() { + if s.ident == "Context" { + variant = ArgVariant::Context; + } else if s.ident == "VariadicArgs" { + variant = ArgVariant::Variadic; + } + if generics.iter().any(|gen| { + gen.matcher + .is_match(&p.to_token_stream().to_string(), index) + }) { + variant = ArgVariant::Generic; + } + } + } + TypeT::Generic(p) => { + p.iter().for_each(|p| { + if let Some(s) = p.segments.last() { + if s.ident == "JSContext" { + variant = ArgVariant::Context; + } else if s.ident == "VariadicArgs" { + variant = ArgVariant::Variadic; + } else { + variant = ArgVariant::Generic; + } + } + }); + } + _ => {} + } + + Ok(Arg { index, ty, variant }) + } +} + +#[derive(Clone, PartialEq, Debug)] +pub(crate) enum ReturnType { + Undefined, + Type(TypeT), +} + +#[derive(Clone, PartialEq, Debug)] +pub(crate) enum SelfType { + NoSelf, + SelfRef, + SelfMutRef, +} + +impl ReturnType { + pub(crate) fn parse(ret: &syn::ReturnType) -> Result { + Ok(match ret { + syn::ReturnType::Default => ReturnType::Undefined, + syn::ReturnType::Type(_, ty) => ReturnType::Type(Type::parse(ty, true)?.ty), + }) + } +} + +pub(crate) fn parse_impl( + bounds: &Punctuated, +) -> Result, &'static str> { + let mut out = Vec::with_capacity(bounds.len()); + + for bound in bounds { + match bound { + TypeParamBound::Trait(t) => { + out.push(t.path.clone()); + } + TypeParamBound::Verbatim(_) => panic!("Verbatim not supported"), + _ => {} //ignore, they will just be lifetimes + } + } + + Ok(out) +} diff --git a/crates/gosub_webinterop/src/types/executor.rs b/crates/gosub_webinterop/src/types/executor.rs new file mode 100644 index 000000000..73f4e5d2c --- /dev/null +++ b/crates/gosub_webinterop/src/types/executor.rs @@ -0,0 +1,19 @@ +#[derive(PartialEq, Debug, Clone)] +#[allow(clippy::upper_case_acronyms)] +pub(crate) enum Executor { + JS, + WASM, + Both, + None, +} + +impl Executor { + pub(crate) fn is_js(&self) -> bool { + *self == Executor::JS || *self == Executor::Both + } + + #[allow(dead_code)] // needed when we implement WASM + pub(crate) fn is_wasm(&self) -> bool { + *self == Executor::WASM || *self == Executor::Both + } +} diff --git a/crates/gosub_webinterop/src/types/field.rs b/crates/gosub_webinterop/src/types/field.rs new file mode 100644 index 000000000..4eb1297a1 --- /dev/null +++ b/crates/gosub_webinterop/src/types/field.rs @@ -0,0 +1,84 @@ +use proc_macro2::TokenStream; +use quote::quote; + +use crate::types::executor::Executor; + +#[derive(Clone, Debug)] +pub(crate) struct Field { + pub(crate) name: String, + pub(crate) ident: syn::Ident, + #[allow(unused)] + pub(crate) executor: Executor, + //needed later + pub(crate) ty: syn::Type, +} + +impl Field { + pub(crate) fn getters_setters(fields: &[Field]) -> TokenStream { + let mut gs = TokenStream::new(); + + for field in fields { + gs.extend(field.getter_setter()); + } + + gs + } + + fn getter_setter(&self) -> TokenStream { + let getters = self.getter(); + let setters = self.setter(); + + let field_name = &self.name; + + quote! { + { + #getters + #setters + + obj.set_property_accessor(#field_name, getter, setter)?; + } + } + } + + fn getter(&self) -> TokenStream { + let ident = &self.ident; + quote! { + let getter = { + let s = Rc::clone(&s); + Box::new(move |cb: &mut RT::GetterCB| { + let ctx = cb.context(); + let value = s.borrow().#ident; + let value = match value.to_js_value(ctx.clone()) { + Ok(value) => value, + Err(e) => { + cb.error(e); + return; + } + }; + cb.ret(value); + }) + }; + } + } + + fn setter(&self) -> TokenStream { + let ident = &self.ident; + let field_type = &self.ty; + quote! { + let setter = { + let s = Rc::clone(&s); + Box::new(move |cb: &mut RT::SetterCB| { + let value = cb.value(); + let value: #field_type = match value.to_rust_value() { + Ok(value) => value, + Err(e) => { + cb.error(e); + return; + } + }; + s.borrow_mut().#ident = value; + }) + }; + } + } +} diff --git a/crates/gosub_webinterop/src/types/generics.rs b/crates/gosub_webinterop/src/types/generics.rs new file mode 100644 index 000000000..ad1a5fcb1 --- /dev/null +++ b/crates/gosub_webinterop/src/types/generics.rs @@ -0,0 +1,76 @@ +use crate::types::Primitive; +use quote::ToTokens; +use syn::Path; + +#[derive(Clone, PartialEq, Debug)] +pub(crate) struct GenericProperty { + pub(crate) param: Path, + pub(crate) types: Vec<(Path, Primitive)>, +} + +#[derive(Clone, PartialEq, Debug)] +pub(crate) struct Generics { + pub(crate) matcher: GenericsMatcher, + pub(crate) types: Vec<(Path, Primitive)>, +} + +#[derive(Clone, PartialEq, Debug)] +pub(crate) enum GenericsMatcher { + Param(Path), + Trait(Path), + Index(usize), +} + +impl GenericsMatcher { + pub(crate) fn is_match(&self, ty: &str, index: usize) -> bool { + let ty = ty.replace(' ', ""); + match self { + GenericsMatcher::Param(p) => p.to_token_stream().to_string().replace(' ', "") == ty, + GenericsMatcher::Trait(p) => p.to_token_stream().to_string().replace(' ', "") == ty, + GenericsMatcher::Index(i) => i == &index, + } + } + + pub(crate) fn new(generic: Path, func: &syn::ImplItemFn) -> GenericsMatcher { + let mut generic_params = Vec::new(); + + for generic in &func.sig.generics.params { + if let syn::GenericParam::Type(t) = generic { + generic_params.push(t.ident.clone()); + } + } + + // check if it is a number + if let Ok(a) = generic.to_token_stream().to_string().parse::() { + return GenericsMatcher::Index(a); + } + + if generic_params.contains(generic.get_ident().unwrap()) { + return GenericsMatcher::Param(generic); + } + + GenericsMatcher::Trait(generic) + } + + pub(crate) fn get_matchers( + generics: Vec, + func: &syn::ImplItemFn, + ) -> Vec { + let mut gen = Vec::new(); + + for generic in generics { + let matcher = GenericsMatcher::new(generic.param, func); + + if gen.iter().any(|g: &Generics| g.matcher == matcher) { + panic!("Duplicate generic matcher"); + } + + gen.push(Generics { + matcher, + types: generic.types, + }); + } + + gen + } +} diff --git a/crates/gosub_webinterop/src/types/primitive.rs b/crates/gosub_webinterop/src/types/primitive.rs new file mode 100644 index 000000000..1b928b2d5 --- /dev/null +++ b/crates/gosub_webinterop/src/types/primitive.rs @@ -0,0 +1,35 @@ +use proc_macro2::{Ident, TokenStream}; +use quote::quote; + +#[derive(Clone, PartialEq, Debug)] +pub(crate) enum Primitive { + Number, + String, + Boolean, + UndefinedNull, + Object, +} + +impl Primitive { + pub(crate) fn get(ty: &str) -> Self { + let ty = ty.replace('&', ""); + match &*ty { + "i8" | "u8" | "i16" | "u16" | "i32" | "u32" | "i64" | "u64" | "i128" | "u128" + | "isize" | "usize" | "f32" | "f64" => Primitive::Number, + "String" | "&str" => Primitive::String, + "bool" => Primitive::Boolean, + "()" => Primitive::UndefinedNull, + _ => Primitive::Object, + } + } + + pub(crate) fn get_check(&self, arg_name: &Ident) -> TokenStream { + match self { + Primitive::Number => quote! { #arg_name.is_number() }, + Primitive::String => quote! { #arg_name.is_string() }, + Primitive::Boolean => quote! { #arg_name.is_boolean() }, + Primitive::UndefinedNull => quote! { #arg_name.is_undefined() || #arg_name.is_null() }, + Primitive::Object => quote! { #arg_name.is_object() }, //TODO we need better checks here, (e.g strict check, so fields are matched too) + } + } +} diff --git a/crates/gosub_webinterop/src/types/slice.rs b/crates/gosub_webinterop/src/types/slice.rs new file mode 100644 index 000000000..7400d331b --- /dev/null +++ b/crates/gosub_webinterop/src/types/slice.rs @@ -0,0 +1,81 @@ +use crate::types::{Type, TypeT}; +use proc_macro2::{Ident, TokenStream}; +use quote::quote; + +pub(crate) fn handle_slice_conv(root_arg: &Type, arg_name: &Ident) -> TokenStream { + if let TypeT::Array(arg, len) = &root_arg.ty { + if len.is_none() { + return TokenStream::new(); + } + + let ty = array_type(arg); + + let conv = handle_slice_conv_recursive(arg, arg_name, true); + + let mutability = root_arg.mutability(true); + + quote! { + + #conv + + let Ok(#mutability #arg_name) = <#ty>::try_from(#arg_name) else { + cb.error("failed to convert argument"); + return; + }; + } + } else { + TokenStream::new() + } +} + +fn handle_slice_conv_recursive(arg: &Type, arg_name: &Ident, first: bool) -> TokenStream { + if let TypeT::Array(arg, _len) = &arg.ty { + let ty = array_type(arg); + + let conv = handle_slice_conv_recursive(arg, arg_name, false); + + if first { + return quote! { + let Some(#arg_name): Option> = #arg_name.into_iter().map(|#arg_name| { + #conv + <#ty>::try_from(#arg_name).ok() + }).collect::>() else { + cb.error("failed to convert argument"); + return; + }; + }; + } + + quote! { + let #arg_name: Vec<#ty> = #arg_name.into_iter().map(|#arg_name| { + #conv + <#ty>::try_from(#arg_name).ok() + }).collect::>()?; + } + } else { + TokenStream::new() + } +} + +fn array_type(arg: &Type) -> TokenStream { + match &arg.ty { + TypeT::Type(_) | TypeT::Generic(_) => quote! { _ }, + TypeT::Array(t, len) => { + let ty = array_type(t); + let len = if let Some(len) = len { + quote! {; #len } + } else { + TokenStream::new() + }; + quote! { [#ty #len] } + } + TypeT::Tuple(t) => { + let mut types = Vec::new(); + for t in t { + types.push(t.ty()); + } + + quote! { (#(#types),*) } + } + } +} diff --git a/crates/gosub_webinterop/src/types/ty.rs b/crates/gosub_webinterop/src/types/ty.rs new file mode 100644 index 000000000..f6a6d6672 --- /dev/null +++ b/crates/gosub_webinterop/src/types/ty.rs @@ -0,0 +1,138 @@ +use crate::types::parse_impl; +use proc_macro2::TokenStream; +use quote::{quote, ToTokens}; +use syn::{Expr, Path}; + +#[derive(Clone, PartialEq, Debug)] +pub(crate) enum Reference { + None, + Ref, + MutRef, +} + +#[derive(Clone, PartialEq, Debug)] +pub(crate) struct Type { + pub(crate) reference: Reference, + pub(crate) ty: TypeT, +} + +#[derive(Clone, PartialEq, Debug)] +pub(crate) enum TypeT { + Type(Path), + Array(Box, Option), + Tuple(Vec), //Array on the JS side + Generic(Vec), +} + +impl Type { + pub(crate) fn parse(ty: &syn::Type, allow_ref: bool) -> Result { + match ty { + syn::Type::Reference(r) => { + if !allow_ref { + return Err("type can't be a reference here"); + } + + Ok(Type { + reference: if r.mutability.is_none() { + Reference::Ref + } else { + Reference::MutRef + }, + ty: Self::parse(&r.elem, false) + .map_err(|_| "double references not supported!")? + .ty, + }) + } + syn::Type::Array(a) => Ok(Type { + reference: Reference::None, + ty: TypeT::Array( + Box::new(Self::parse(&a.elem, allow_ref)?), + Some(a.len.clone()), + ), + }), + syn::Type::Slice(s) => Ok(Type { + reference: Reference::None, + ty: TypeT::Array(Box::new(Self::parse(&s.elem, allow_ref)?), None), + }), + syn::Type::Tuple(t) => { + let mut elements = Vec::with_capacity(t.elems.len()); + + for elem in &t.elems { + elements.push(Self::parse(elem, allow_ref)?) + } + + Ok(Type { + reference: Reference::None, + ty: TypeT::Tuple(elements), + }) + } + + syn::Type::Path(p) => Ok(Type { + reference: Reference::None, + ty: TypeT::Type(p.path.clone()), + }), + + syn::Type::ImplTrait(p) => Ok(Type { + reference: Reference::None, + ty: TypeT::Generic(parse_impl(&p.bounds)?), + }), + + t => { + panic!("Invalid argument type: {}", t.into_token_stream()); + } + } + } + + pub(crate) fn mutability(&self, slices: bool) -> TokenStream { + if self.reference == Reference::MutRef && (!matches!(self.ty, TypeT::Array(..)) || slices) { + quote! { mut } + } else { + TokenStream::new() + } + } + + pub(crate) fn get_reference(&self) -> TokenStream { + match self.reference { + Reference::Ref => quote! { & }, + Reference::MutRef => quote! { &mut }, + Reference::None => TokenStream::new(), + } + } + + pub(crate) fn ty(&self) -> TokenStream { + match &self.ty { + TypeT::Type(_) | TypeT::Generic(_) => quote! { _ }, + TypeT::Array(t, _) => { + let ty = t.ty(); + quote! { Vec<#ty> } + } + TypeT::Tuple(t) => { + let mut types = Vec::new(); + for t in t { + types.push(t.ty()); + } + + quote! { (#(#types),*) } + } + } + } +} + +impl TypeT { + pub(crate) fn generics(&self) -> Option { + Some(match self { + TypeT::Type(p) => p.get_ident()?.to_token_stream().to_string(), + TypeT::Generic(t) => { + let mut out = String::new(); + for (i, p) in t.iter().enumerate() { + if i != 0 { + out.push('+'); + } + out.push_str(&p.get_ident()?.to_token_stream().to_string()); + } + out + } + _ => None?, + }) + } +} diff --git a/crates/gosub_webinterop/src/utils.rs b/crates/gosub_webinterop/src/utils.rs new file mode 100644 index 000000000..6fe9b8c7d --- /dev/null +++ b/crates/gosub_webinterop/src/utils.rs @@ -0,0 +1,19 @@ +use std::env; + +use syn::Path; + +#[allow(dead_code)] +pub fn crate_ident() -> Path { + let mut name = env::var("CARGO_PKG_NAME").unwrap(); + if name == "gosub_webexecutor" { + name = "crate".to_string(); + } + + let name = name.replace('-', "_"); + + syn::parse_str::(&name).unwrap() +} + +pub fn crate_name() -> String { + env::var("CARGO_PKG_NAME").unwrap() +} diff --git a/crates/gosub_webinterop/src/variadic.rs b/crates/gosub_webinterop/src/variadic.rs new file mode 100644 index 000000000..7e7e31f33 --- /dev/null +++ b/crates/gosub_webinterop/src/variadic.rs @@ -0,0 +1,125 @@ +//! This file is currently unused. +//! It generates a custom syntax for variadic functions, but it breaks code editors. + + +use proc_macro::TokenStream; +use proc_macro2::{Delimiter, Spacing, TokenStream as TokenStream2, TokenTree}; +use proc_macro2::Punct; +use quote::quote; + +#[allow(unused)] +pub(crate) fn preprocess_variadic(item: TokenStream) -> TokenStream { + let mut item: TokenStream2 = item.into(); + + let mut iter = item.clone().into_iter(); + + let mut out = TokenStream2::new(); + + while let Some(tt) = iter.next() { + match &tt { + TokenTree::Ident(i) => { + if i == "impl" { + out.extend(std::iter::once(tt)); + 'inner: for tt in iter.by_ref() { + if let TokenTree::Group(g) = tt { + + let mut group = TokenStream2::new(); + expand_impl(g.stream(), &mut group); + + push_group(&mut out, group, g.delimiter()); + break 'inner; + } else { + out.extend(std::iter::once(tt)); + } + } + + + } else { + out.extend(std::iter::once(tt)); + } + + + } + _ => { + out.extend(std::iter::once(tt)); + } + } + } + + out.into() +} + +fn push_group(out: &mut TokenStream2, stream: TokenStream2, delimiter: Delimiter) { + out.extend(std::iter::once(TokenTree::Group(proc_macro2::Group::new(delimiter, stream)))); + +} + +fn expand_impl(stream: TokenStream2, out: &mut TokenStream2) { + let mut iter = stream.into_iter(); + + while let Some(tt) = iter.next() { + match &tt { + TokenTree::Ident(i) => { + if i == "fn" { + out.extend(std::iter::once(tt)); + for tt in iter.by_ref() { + if let TokenTree::Group(g) = tt { + let mut group = TokenStream2::new(); + expand_args(g.stream(), &mut group); + + push_group(out, group, g.delimiter()); + break; + } else { + out.extend(std::iter::once(tt)); + } + } + } + } + _ => { + out.extend(std::iter::once(tt)); + } + } + } +} + +fn expand_args(stream: TokenStream2, out: &mut TokenStream2) { + let mut found = false; + + let mut dot_count = 0; + + let mut iter = stream.into_iter(); + + let mut dots = TokenStream2::new(); + + while let Some(tt) = iter.next() { + match &tt { + TokenTree::Punct(p) => { + if p.as_char() == '.' { + dot_count += 1; + dots.extend(std::iter::once(tt)); + } else { + dot_count = 0; + out.extend(dots.clone().into_iter()); + out.extend(std::iter::once(tt)); + } + + if dot_count == 3 { + let name = iter.next().expect("expected name after ..."); + + let arg = quote!{ + #name: &impl VariadicArgs + }; + + out.extend(arg.into_iter()); + + assert!(iter.next().is_none(), "variadic args must be the last argument"); + } + } + _ => { + dot_count = 0; + out.extend(dots.clone().into_iter()); + out.extend(std::iter::once(tt)); + } + } + } +} \ No newline at end of file