Skip to content

Commit ff85838

Browse files
committed
Add #[py_class] attribute proc macro
1 parent 6618def commit ff85838

File tree

6 files changed

+517
-278
lines changed

6 files changed

+517
-278
lines changed

derive/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,6 @@ edition = "2018"
88
proc-macro = true
99

1010
[dependencies]
11-
syn = "0.15.29"
11+
syn = { version = "0.15.29", features = ["full"] }
1212
quote = "0.6.11"
1313
proc-macro2 = "0.4.27"

derive/src/lib.rs

Lines changed: 190 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,57 @@
11
extern crate proc_macro;
22

33
use proc_macro::TokenStream;
4-
use proc_macro2::TokenStream as TokenStream2;
4+
use proc_macro2::{Span, TokenStream as TokenStream2};
55
use quote::quote;
6-
use syn::{Data, DeriveInput, Fields};
6+
use syn::{
7+
parse_macro_input, AttributeArgs, Data, DeriveInput, Fields, Ident, ImplItem, Item, Lit, Meta,
8+
NestedMeta,
9+
};
710

8-
#[proc_macro_derive(FromArgs)]
11+
fn rustpython_path(inside_vm: bool) -> syn::Path {
12+
let path = if inside_vm {
13+
quote!(crate)
14+
} else {
15+
quote!(::rustpython_vm)
16+
};
17+
syn::parse2(path).unwrap()
18+
}
19+
20+
/// Does the item have the #[__inside_vm] attribute on it, signifying that the derive target is
21+
/// being derived from inside the `rustpython_vm` crate.
22+
fn rustpython_path_derive(input: &DeriveInput) -> syn::Path {
23+
rustpython_path(
24+
input
25+
.attrs
26+
.iter()
27+
.any(|attr| attr.path.is_ident("__inside_vm")),
28+
)
29+
}
30+
31+
fn rustpython_path_attr(attr: &AttributeArgs) -> syn::Path {
32+
rustpython_path(attr.iter().any(|meta| {
33+
if let syn::NestedMeta::Meta(meta) = meta {
34+
if let syn::Meta::Word(ident) = meta {
35+
ident == "__inside_vm"
36+
} else {
37+
false
38+
}
39+
} else {
40+
false
41+
}
42+
}))
43+
}
44+
45+
#[proc_macro_derive(FromArgs, attributes(__inside_vm))]
946
pub fn derive_from_args(input: TokenStream) -> TokenStream {
1047
let ast: DeriveInput = syn::parse(input).unwrap();
1148

12-
let gen = impl_from_args(&ast);
49+
let gen = impl_from_args(ast);
1350
gen.to_string().parse().unwrap()
1451
}
1552

16-
fn impl_from_args(input: &DeriveInput) -> TokenStream2 {
17-
// FIXME: This references types using `crate` instead of `rustpython_vm`
18-
// so that it can be used in the latter. How can we support both?
53+
fn impl_from_args(input: DeriveInput) -> TokenStream2 {
54+
let rp_path = rustpython_path_derive(&input);
1955
let fields = match input.data {
2056
Data::Struct(ref data) => {
2157
match data.fields {
@@ -36,7 +72,7 @@ fn impl_from_args(input: &DeriveInput) -> TokenStream2 {
3672

3773
let name = &input.ident;
3874
quote! {
39-
impl crate::function::FromArgs for #name {
75+
impl #rp_path::function::FromArgs for #name {
4076
fn from_args(
4177
vm: &crate::vm::VirtualMachine,
4278
args: &mut crate::function::PyFuncArgs
@@ -46,3 +82,149 @@ fn impl_from_args(input: &DeriveInput) -> TokenStream2 {
4682
}
4783
}
4884
}
85+
86+
#[proc_macro_attribute]
87+
pub fn py_class(attr: TokenStream, item: TokenStream) -> TokenStream {
88+
let attr = parse_macro_input!(attr as AttributeArgs);
89+
let item = parse_macro_input!(item as Item);
90+
impl_py_class(attr, item).into()
91+
}
92+
93+
enum MethodKind {
94+
Method,
95+
Property,
96+
}
97+
98+
impl MethodKind {
99+
fn to_ctx_constructor_fn(&self) -> Ident {
100+
let f = match self {
101+
MethodKind::Method => "new_rustfunc",
102+
MethodKind::Property => "new_property",
103+
};
104+
Ident::new(f, Span::call_site())
105+
}
106+
}
107+
108+
struct Method {
109+
fn_name: Ident,
110+
py_name: String,
111+
kind: MethodKind,
112+
}
113+
114+
fn item_impl_to_methods<'a>(imp: &'a syn::ItemImpl) -> impl Iterator<Item = Method> + 'a {
115+
imp.items.iter().filter_map(|item| {
116+
if let ImplItem::Method(meth) = item {
117+
let mut py_name = None;
118+
let mut kind = MethodKind::Method;
119+
let metas_iter = meth
120+
.attrs
121+
.iter()
122+
.filter_map(|attr| {
123+
if attr.path.is_ident("py_class") {
124+
let meta = attr.parse_meta().expect("Invalid attribute");
125+
if let Meta::List(list) = meta {
126+
Some(list)
127+
} else {
128+
panic!(
129+
"#[py_class] attribute on a method should be a list, like \
130+
#[py_class(...)]"
131+
)
132+
}
133+
} else {
134+
None
135+
}
136+
})
137+
.flat_map(|attr| attr.nested);
138+
for meta in metas_iter {
139+
if let NestedMeta::Meta(meta) = meta {
140+
match meta {
141+
Meta::NameValue(name_value) => {
142+
if name_value.ident == "name" {
143+
if let Lit::Str(s) = &name_value.lit {
144+
py_name = Some(s.value());
145+
} else {
146+
panic!("#[py_class(name = ...)] must be a string");
147+
}
148+
}
149+
}
150+
Meta::Word(ident) => match ident.to_string().as_str() {
151+
"property" => kind = MethodKind::Property,
152+
_ => {}
153+
},
154+
_ => {}
155+
}
156+
}
157+
}
158+
let py_name = py_name.unwrap_or_else(|| meth.sig.ident.to_string());
159+
Some(Method {
160+
fn_name: meth.sig.ident.clone(),
161+
py_name,
162+
kind,
163+
})
164+
} else {
165+
None
166+
}
167+
})
168+
}
169+
170+
fn impl_py_class(attr: AttributeArgs, item: Item) -> TokenStream2 {
171+
let imp = if let Item::Impl(imp) = item {
172+
imp
173+
} else {
174+
return quote!(#item);
175+
};
176+
let rp_path = rustpython_path_attr(&attr);
177+
let mut class_name = None;
178+
let mut doc = None;
179+
for attr in attr {
180+
if let NestedMeta::Meta(meta) = attr {
181+
if let Meta::NameValue(name_value) = meta {
182+
if name_value.ident == "name" {
183+
if let Lit::Str(s) = name_value.lit {
184+
class_name = Some(s.value());
185+
} else {
186+
panic!("#[py_class(name = ...)] must be a string");
187+
}
188+
} else if name_value.ident == "doc" {
189+
if let Lit::Str(s) = name_value.lit {
190+
doc = Some(s.value());
191+
} else {
192+
panic!("#[py_class(name = ...)] must be a string");
193+
}
194+
}
195+
}
196+
}
197+
}
198+
let class_name = class_name.expect("#[py_class] must have a name");
199+
let doc = match doc {
200+
Some(doc) => quote!(Some(#doc)),
201+
None => quote!(None),
202+
};
203+
let ty = &imp.self_ty;
204+
let methods = item_impl_to_methods(&imp).map(
205+
|Method {
206+
py_name,
207+
fn_name,
208+
kind,
209+
}| {
210+
let constructor_fn = kind.to_ctx_constructor_fn();
211+
quote! {
212+
ctx.set_attr(class, #py_name, ctx.#constructor_fn(#ty::#fn_name));
213+
}
214+
},
215+
);
216+
217+
quote! {
218+
#imp
219+
impl #rp_path::pyobject::IntoPyClass for #ty {
220+
const NAME: &'static str = #class_name;
221+
const DOC: Option<&'static str> = #doc;
222+
fn _extend_class(
223+
ctx: &#rp_path::pyobject::PyContext,
224+
class: &#rp_path::obj::objtype::PyClassRef,
225+
) {
226+
#(#methods)*
227+
}
228+
}
229+
}
230+
}

vm/src/builtins.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -536,6 +536,7 @@ fn builtin_pow(vm: &VirtualMachine, args: PyFuncArgs) -> PyResult {
536536
}
537537

538538
#[derive(Debug, FromArgs)]
539+
#[__inside_vm]
539540
pub struct PrintOptions {
540541
sep: Option<PyStringRef>,
541542
end: Option<PyStringRef>,

vm/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ extern crate statrs;
3030

3131
extern crate rustpython_parser;
3232
#[macro_use]
33-
extern crate rustpython_derive;
33+
pub extern crate rustpython_derive;
3434

3535
//extern crate eval; use eval::eval::*;
3636
// use py_code_object::{Function, NativeType, PyCodeObject};

0 commit comments

Comments
 (0)