-
-
Notifications
You must be signed in to change notification settings - Fork 115
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Provides two macros to make model creation more convenient #643
Changes from all commits
ed7abf7
1b623b3
29dbeb5
79ae023
495dcfe
934fca2
efed269
78b4756
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,11 +10,17 @@ members = [ | |
"crates/fj-math", | ||
"crates/fj-operations", | ||
"crates/fj-viewer", | ||
"crates/fj-proc", | ||
|
||
"crates/fj-window", | ||
|
||
"crates/fj-proc", | ||
|
||
|
||
Comment on lines
+17
to
+19
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's the same crate a second time. I'm surprised that doesn't result in some kind of error! 😄 |
||
"models/cuboid", | ||
"models/spacer", | ||
"models/star", | ||
"models/star_attributed_arguments", | ||
"models/test", | ||
|
||
"tools/export-validator", | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
[package] | ||
name = "fj-proc" | ||
version = "0.6.0" | ||
edition = "2021" | ||
|
||
description = "The world needs another CAD program." | ||
readme = "../../README.md" | ||
homepage = "https://www.fornjot.app/" | ||
repository = "https://github.com/hannobraun/fornjot" | ||
license = "0BSD" | ||
keywords = ["cad", "programmatic", "code-cad"] | ||
categories = ["encoding", "mathematics", "rendering"] | ||
|
||
|
||
[lib] | ||
proc-macro = true | ||
|
||
[dependencies] | ||
serde = { version = "1.0.7", optional = true } | ||
syn={version="1.0",features=["full", "extra-traits"]} | ||
quote = "1.0" | ||
proc-macro2 = "1.0" |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,198 @@ | ||
use proc_macro::TokenStream; | ||
use quote::quote; | ||
use syn::{ | ||
bracketed, parenthesized, parse::Parse, parse_macro_input, parse_quote, | ||
}; | ||
|
||
pub fn attributed_arguments(_: TokenStream, input: TokenStream) -> TokenStream { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think having two different attributes (and two different models for testing/demonstrating) them is overkill. I like the approach implemented in this one much better. It would be better, if this just replaced the other one. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The first one was more or less just left in because it was the first one I came up with. So more for completeness than anything else. I'll get rid of it :) |
||
let item = parse_macro_input!(input as syn::ItemFn); | ||
let inputs = item.clone().sig.inputs; | ||
|
||
let args: Vec<Argument> = | ||
inputs.iter().map(|inp| parse_quote!(#inp)).collect(); | ||
|
||
let mut defaults = Vec::new(); | ||
let mut mins = Vec::new(); | ||
let mut maxs = Vec::new(); | ||
let mut names = Vec::new(); | ||
let mut types = Vec::new(); | ||
for arg in args { | ||
let mut default = None; | ||
let mut min = None; | ||
let mut max = None; | ||
names.push(arg.ident); | ||
types.push(arg.ty); | ||
for value in arg.attr.values.clone() { | ||
if let Some(ident) = value.ident.clone() { | ||
match ident.to_string().as_str() { | ||
"default" => default = Some(value.val.clone()), | ||
"min" => min = Some(value.val.clone()), | ||
"max" => max = Some(value.val.clone()), | ||
_ => {} | ||
} | ||
} else { | ||
default = Some(value.val.clone()); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe it would be better not to have this fallback, and always require an explicit What do you think? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think I'll simplify the macro to not allow for this implicit fallback |
||
} | ||
} | ||
let [default, min, max] = determine_default(default, min, max); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the behavior of this function, using |
||
defaults.push(default); | ||
mins.push(min); | ||
maxs.push(max); | ||
} | ||
let block = item.block; | ||
|
||
quote! { | ||
#[no_mangle] | ||
pub extern "C" fn model(args: &HashMap<String, String>) -> fj::Shape { | ||
#( | ||
let #names: #types = args.get(stringify!(#names)).map(|arg| arg.parse().unwrap()).unwrap_or(#defaults); | ||
)* | ||
#block | ||
} | ||
}.into() | ||
} | ||
|
||
/// Represents one parameter given to the `model` | ||
/// `#[value(default=3, min=4)] num_points: u64` | ||
/// `^^^^^^^^^^^^^^^^^^^^^^^^^^ ~~~~~~~~~~ ^^^-- ty` | ||
/// ` | |` | ||
/// ` attr ident` | ||
Comment on lines
+55
to
+59
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice style of documentation 👍 |
||
#[derive(Debug, Clone)] | ||
struct Argument { | ||
pub attr: HelperAttribute, | ||
pub ident: proc_macro2::Ident, | ||
pub ty: proc_macro2::Ident, | ||
} | ||
|
||
impl Parse for Argument { | ||
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> { | ||
let attr: HelperAttribute = input.parse()?; | ||
let ident: proc_macro2::Ident = input.parse()?; | ||
|
||
let _: syn::token::Colon = input.parse()?; | ||
|
||
let ty: proc_macro2::Ident = input.parse()?; | ||
Ok(Self { attr, ident, ty }) | ||
} | ||
} | ||
|
||
/// Represents all arguments given to the `#[value]` attribute eg: | ||
/// `#[value(default=3, min=4)]` | ||
/// ` ^^^^^^^^^^^^^^^^` | ||
#[derive(Debug, Clone)] | ||
struct HelperAttribute { | ||
pub values: syn::punctuated::Punctuated<DefaultParam, syn::Token![,]>, | ||
} | ||
|
||
impl Parse for HelperAttribute { | ||
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> { | ||
let attr_content; | ||
let value_content; | ||
let _: syn::token::Pound = input.parse()?; | ||
bracketed!(attr_content in input); | ||
let ident: proc_macro2::Ident = attr_content.parse()?; | ||
if ident.to_string() != *"value" { | ||
return Err(syn::Error::new_spanned( | ||
ident.clone(), | ||
format!( | ||
"Unknown attribute \"{}\" found, expected \"value\"", | ||
ident | ||
), | ||
)); | ||
} | ||
parenthesized!(value_content in attr_content); | ||
|
||
Ok(Self { | ||
values: syn::punctuated::Punctuated::parse_separated_nonempty_with( | ||
&value_content, | ||
DefaultParam::parse, | ||
)?, | ||
}) | ||
} | ||
} | ||
|
||
/// Represents one argument given to the `#[value]` attribute eg: | ||
/// `#[value(default=3)]` | ||
/// ` ^^^^^^^^^----- is parsed as DefaultParam{ ident: Some(default), val: 3 }` | ||
#[derive(Debug, Clone)] | ||
struct DefaultParam { | ||
pub ident: Option<proc_macro2::Ident>, | ||
pub val: syn::Expr, | ||
} | ||
|
||
impl Parse for DefaultParam { | ||
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> { | ||
if input.peek(syn::Ident) { | ||
let ident: Option<proc_macro2::Ident> = Some(input.parse()?); | ||
let _: syn::token::Eq = input.parse()?; | ||
Ok(Self { | ||
ident, | ||
val: input.parse()?, | ||
}) | ||
} else { | ||
Ok(Self { | ||
ident: None, | ||
val: input.parse()?, | ||
}) | ||
} | ||
} | ||
} | ||
|
||
/// Checks if a default value is supplied, otherwise applies either the min or max (if specified) as default. | ||
fn determine_default( | ||
default: Option<syn::Expr>, | ||
min: Option<syn::Expr>, | ||
max: Option<syn::Expr>, | ||
) -> [Option<syn::Expr>; 3] { | ||
if let Some(default) = default { | ||
let min = if min.is_some() { min } else { None }; | ||
let max = if max.is_some() { max } else { None }; | ||
[Some(default), min, max] | ||
} else { | ||
let mut default = None; | ||
let max = if max.is_some() { | ||
default = max.clone(); | ||
max | ||
} else { | ||
None | ||
}; | ||
|
||
let min = if min.is_some() { | ||
default = min.clone(); | ||
min | ||
} else { | ||
None | ||
}; | ||
|
||
[default, min, max] | ||
} | ||
} | ||
|
||
// #[fj::model] | ||
// pub fn model( | ||
// #[default(5)] num_points: u64, | ||
// #[default(1.0)] r1: f64, | ||
// #[default(2.0)] r2: f64, | ||
// #[default(1.0)] h: f64, | ||
// ) -> fj::Shape | ||
|
||
// #[no_mangle] | ||
// pub extern "C" fn model(args: &HashMap<String, String>) -> fj::Shape { | ||
// let num_points: u64 = args | ||
// .get("num_points") | ||
// .map(|arg| arg.parse().unwrap()) | ||
// .unwrap_or(5); | ||
|
||
// let r1: f64 = args | ||
// .get("r1") | ||
// .map(|arg| arg.parse().unwrap()) | ||
// .unwrap_or(1.0); | ||
|
||
// let r2: f64 = args | ||
// .get("r2") | ||
// .map(|arg| arg.parse().unwrap()) | ||
// .unwrap_or(2.0); | ||
|
||
// let h: f64 = args.get("h").map(|arg| arg.parse().unwrap()).unwrap_or(1.0); | ||
|
||
// } |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
use proc_macro::TokenStream; | ||
use quote::quote; | ||
use syn::parse_macro_input; | ||
|
||
mod attributed_arguments; | ||
|
||
#[proc_macro_attribute] | ||
pub fn model(default_values: TokenStream, input: TokenStream) -> TokenStream { | ||
let vals: Vec<String> = default_values | ||
.into_iter() | ||
.filter_map(|tree| { | ||
if let proc_macro::TokenTree::Literal(lit) = tree { | ||
Some(lit.to_string()) | ||
} else { | ||
None | ||
} | ||
}) | ||
.collect(); | ||
let item = parse_macro_input!(input as syn::ItemFn); | ||
|
||
let inputs = item.sig.inputs; | ||
let mut names = Vec::new(); | ||
let mut types = Vec::new(); | ||
for f in inputs.iter() { | ||
if let syn::FnArg::Typed(meep) = f { | ||
if let syn::Pat::Ident(ident) = *meep.clone().pat { | ||
names.push(ident.ident); | ||
} | ||
if let syn::Type::Path(path) = *meep.clone().ty { | ||
types.push(path.path.get_ident().unwrap().clone()); | ||
} | ||
} | ||
} | ||
let block = item.block; | ||
|
||
quote! { | ||
#[no_mangle] | ||
pub extern "C" fn model(args: &HashMap<String, String>) -> fj::Shape { | ||
#(let #names: #types = args.get(stringify!(#names)).map(|arg| arg.parse().unwrap()).unwrap_or(#vals.parse::<#types>().unwrap());)* | ||
#block | ||
} | ||
}.into() | ||
} | ||
|
||
#[proc_macro_attribute] | ||
pub fn attributed_arguments( | ||
default_values: TokenStream, | ||
input: TokenStream, | ||
) -> TokenStream { | ||
attributed_arguments::attributed_arguments(default_values, input) | ||
} | ||
|
||
// #[fj_proc::model(5, 1.0, 2.0, 1.0)] | ||
// pub fn model(num_points: u64, r1: f64, r2: f64, h: f64) -> fj::Shape { | ||
// } | ||
|
||
// #[no_mangle] | ||
// pub extern "C" fn model(args: &HashMap<String, String>) -> fj::Shape { | ||
// let num_points: u64 = args | ||
// .get("num_points") | ||
// .map(|arg| arg.parse().unwrap()) | ||
// .unwrap_or(5); | ||
|
||
// let r1: f64 = args | ||
// .get("r1") | ||
// .map(|arg| arg.parse().unwrap()) | ||
// .unwrap_or(1.0); | ||
|
||
// let r2: f64 = args | ||
// .get("r2") | ||
// .map(|arg| arg.parse().unwrap()) | ||
// .unwrap_or(2.0); | ||
|
||
// let h: f64 = args.get("h").map(|arg| arg.parse().unwrap()).unwrap_or(1.0); | ||
|
||
// } |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,3 +8,6 @@ crate-type = ["cdylib"] | |
|
||
[dependencies.fj] | ||
path = "../../crates/fj" | ||
|
||
[dependencies.fj-proc] | ||
path = "../../crates/fj-proc" | ||
Comment on lines
+11
to
+13
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it should be possible, to re-export |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
[package] | ||
name = "star_attributed_arguments" | ||
version = "0.1.0" | ||
edition = "2021" | ||
|
||
[lib] | ||
crate-type = ["cdylib"] | ||
|
||
[dependencies.fj] | ||
path = "../../crates/fj" | ||
|
||
[dependencies.fj-proc] | ||
path = "../../crates/fj-proc" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The rest of that list is alphabetically sorted. Would be nice to keep that. Also, no need for an empty line.