Skip to content
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

Merged
merged 8 commits into from
May 31, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,17 @@ members = [
"crates/fj-math",
"crates/fj-operations",
"crates/fj-viewer",
"crates/fj-proc",

Comment on lines 12 to +14
Copy link
Owner

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.

"crates/fj-window",

"crates/fj-proc",


Comment on lines +17 to +19
Copy link
Owner

Choose a reason for hiding this comment

The 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",
Expand Down
22 changes: 22 additions & 0 deletions crates/fj-proc/Cargo.toml
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"
198 changes: 198 additions & 0 deletions crates/fj-proc/src/attributed_arguments.rs
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 {
Copy link
Owner

Choose a reason for hiding this comment

The 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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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());
Copy link
Owner

Choose a reason for hiding this comment

The 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 default = to specify default values. Would be more verbose in the simple case, but also easier to read and understand.

What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the behavior of this function, using min or max as the default, if no default is provided, is unexpected and potentially confusing for users. It would probably be clearer to just have no default, if none is provided, and fail with an error message, if the user doesn't provide a value for a default-less parameter.

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
Copy link
Owner

Choose a reason for hiding this comment

The 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);

// }
76 changes: 76 additions & 0 deletions crates/fj-proc/src/lib.rs
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);

// }
3 changes: 3 additions & 0 deletions models/star/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it should be possible, to re-export fj-proc from fj (pub use fj_proc;). Then we wouldn't need the extra dependency, and would also have a simpler syntax in the models (fj::model instead of fj_proc::model).

37 changes: 4 additions & 33 deletions models/star/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,38 +1,9 @@
use fj::Angle;
use std::{collections::HashMap, f64::consts::PI};
extern crate fj_proc;

#[no_mangle]
pub extern "C" fn model(args: &HashMap<String, String>) -> fj::Shape {
// Number of points of the star
//
// "Points" in the sense of "pointy ends", not in the sense of geometrical
// points, or vertices.
let num_points: u64 = args
.get("num_points")
.map(|arg| arg.parse().expect("Could not parse parameter `num_points`"))
.unwrap_or(5);

// Radius of the circle that all the vertices between the pointy ends are on
let r1: f64 = args
.get("r1")
.map(|arg| arg.parse().expect("Could not parse parameter `r1`"))
.unwrap_or(1.0);

// Radius of the circle that all the pointy ends are on
let r2: f64 = args
.get("r2")
.map(|arg| arg.parse().expect("Could not parse parameter `r2`"))
.unwrap_or(2.0);

// The height of the star
let h: f64 = args
.get("h")
.map(|arg| arg.parse().expect("Could not parse parameter `height`"))
.unwrap_or(1.0);

// We need to figure out where to generate vertices, depending on the number
// of points the star is supposed to have. Let's generate an iterator that
// gives us the angle and radius for each vertex.
#[fj_proc::model(5, 1.0, 2.0, 1.0)]
pub fn model(num_points: u64, r1: f64, r2: f64, h: f64) -> fj::Shape {
let num_vertices = num_points * 2;
let vertex_iter = (0..num_vertices).map(|i| {
let angle = Angle::from_rad(2. * PI / num_vertices as f64 * i as f64);
Expand All @@ -59,7 +30,7 @@ pub extern "C" fn model(args: &HashMap<String, String>) -> fj::Shape {

let footprint = fj::Difference2d::from_shapes([outer.into(), inner.into()]);

let star = fj::Sweep::from_path(footprint.into(), [0., 0., h]);
let star = fj::Sweep::from_path(footprint.into(), [0., 0., -h]);

star.into()
}
13 changes: 13 additions & 0 deletions models/star_attributed_arguments/Cargo.toml
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"
Loading