Feature walks: generate them from field roles with #[derive(Walk)] (#1251)
The three walks over a feature (expressions, references, geometry) are generated from FeatureKind by #[derive(Walk)] in a new proc-macro crate, solidcraft-derive, from #[walk(...)] attributes written next to each field. The hand-written walks in params.rs, feature_refs.rs and feature_geometry.rs are gone; Ruled Surface and Extend have their attributes, and the three golden walk records are unchanged. Co-authored-by: jijinbei <[email protected]>
This commit is contained in:
1 parent
a026a69182
commit
464a6dfad4
13 files changed
+998
-937
No files matched your search
@@ -0,0 +1,294 @@
|
||||
//! `#[derive(Walk)]`: the walks over a feature (`crate::walk::Walk`), generated from the fields of
|
||||
//! a struct or an enum's variants. Each walk visits the fields in declaration order, and comes in
|
||||
//! `&` and `&mut`:
|
||||
//!
|
||||
//! - expressions (`walk_exprs`): each with its label, kind and role; the inputs' order names their
|
||||
//! parameters (`dN`);
|
||||
//! - references (`walk_refs`): sketches by id, features and bodies by name;
|
||||
//! - geometry (`walk_geometry`): points with their roles, planes, construction references,
|
||||
//! placement points and profile selections.
|
||||
//!
|
||||
//! What a field is, is written once, on the field, with `#[walk(...)]`:
|
||||
//!
|
||||
//! - no attribute: the field's own type walks itself (`impl Walk`): a plane, a nested struct, a
|
||||
//! list of them, or nothing for names, flags and numbers. A bare `Vec3` has no walk, so a point
|
||||
//! that does not say its role does not compile;
|
||||
//! - `length = label`, `angle = label`, `unitless = label`: an expression input (a `String`, an
|
||||
//! optional one, a list or an array of them, or `(f64, String)` pairs). The label is any
|
||||
//! expression: a `&'static str`, an array of them (one per element), or a closure
|
||||
//! `|i: usize, n: usize| -> &'static str`; it may read the other fields;
|
||||
//! - `sketch`, `feature`, `body`: a reference (an id, or a name, optional or a list);
|
||||
//! - `pick`, `pinned`, `pos`, `dir`: a point (`Vec3`, optional or a list) with that role;
|
||||
//! `pick_if = cond`: a pick when `cond` holds, else a position;
|
||||
//! - `placement`: a primitive's placement point (its X, Y and Z expressions are not inputs);
|
||||
//! - `profiles_of = sketch_field`, optionally with `when = cond`: a profile selection;
|
||||
//! - `after = field`: visit this field right after `field` instead of in declaration order (where
|
||||
//! the parameter names already given need it);
|
||||
//! - `picks_faces`, on a nested value: its positions pick faces here (a `FaceAt` that names the
|
||||
//! face it is on).
|
||||
//!
|
||||
//! Conditions and labels are worked out before the walk starts. The generated code names
|
||||
//! `crate::walk`, so the derive is for `solidcraft-doc`.
|
||||
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
||||
|
||||
use proc_macro::TokenStream;
|
||||
use proc_macro2::{Span, TokenStream as Tokens};
|
||||
use quote::{format_ident, quote};
|
||||
use syn::{Data, DeriveInput, Expr, Fields, Ident, Member, parse_macro_input};
|
||||
|
||||
#[proc_macro_derive(Walk, attributes(walk))]
|
||||
pub fn derive_walk(input: TokenStream) -> TokenStream {
|
||||
let input = parse_macro_input!(input as DeriveInput);
|
||||
match expand(&input) {
|
||||
Ok(t) => t.into(),
|
||||
Err(e) => e.to_compile_error().into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// A field's role in the geometry walk.
|
||||
enum Geo {
|
||||
Points(&'static str),
|
||||
PickIf(Expr),
|
||||
Placement,
|
||||
Profiles { sketch: Ident, when: Option<Expr> },
|
||||
}
|
||||
|
||||
/// What a field is, from its `#[walk(...)]` attributes.
|
||||
#[derive(Default)]
|
||||
struct Roles {
|
||||
geo: Option<Geo>,
|
||||
/// The kind (`Length`, `Angle`, `Unitless`) and the label.
|
||||
expr: Option<(&'static str, Expr)>,
|
||||
/// `Sketch`, `Feature` or `Body`.
|
||||
reference: Option<&'static str>,
|
||||
after: Option<Ident>,
|
||||
/// The positions inside this (nested) value pick faces here.
|
||||
picks_faces: bool,
|
||||
}
|
||||
|
||||
impl Roles {
|
||||
/// No role given: the field's type walks itself.
|
||||
fn nested(&self) -> bool {
|
||||
self.geo.is_none() && self.expr.is_none() && self.reference.is_none()
|
||||
}
|
||||
}
|
||||
|
||||
fn roles(field: &syn::Field) -> syn::Result<Roles> {
|
||||
let mut r = Roles::default();
|
||||
for attr in field.attrs.iter().filter(|a| a.path().is_ident("walk")) {
|
||||
let mut sketch = None;
|
||||
let mut when = None;
|
||||
attr.parse_nested_meta(|meta| {
|
||||
let name = meta.path.get_ident().map(Ident::to_string).unwrap_or_default();
|
||||
match name.as_str() {
|
||||
"pick" => r.geo = Some(Geo::Points("Pick")),
|
||||
"pinned" => r.geo = Some(Geo::Points("Pinned")),
|
||||
"pos" => r.geo = Some(Geo::Points("Pos")),
|
||||
"dir" => r.geo = Some(Geo::Points("Dir")),
|
||||
"placement" => r.geo = Some(Geo::Placement),
|
||||
"pick_if" => r.geo = Some(Geo::PickIf(meta.value()?.parse()?)),
|
||||
"profiles_of" => sketch = Some(meta.value()?.parse::<Ident>()?),
|
||||
"when" => when = Some(meta.value()?.parse::<Expr>()?),
|
||||
"length" => r.expr = Some(("Length", meta.value()?.parse()?)),
|
||||
"angle" => r.expr = Some(("Angle", meta.value()?.parse()?)),
|
||||
"unitless" => r.expr = Some(("Unitless", meta.value()?.parse()?)),
|
||||
"sketch" => r.reference = Some("Sketch"),
|
||||
"feature" => r.reference = Some("Feature"),
|
||||
"body" => r.reference = Some("Body"),
|
||||
"after" => r.after = Some(meta.value()?.parse()?),
|
||||
"picks_faces" => r.picks_faces = true,
|
||||
_ => return Err(meta.error("unknown walk role")),
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
if let Some(sketch) = sketch {
|
||||
r.geo = Some(Geo::Profiles { sketch, when });
|
||||
} else if when.is_some() {
|
||||
return Err(syn::Error::new_spanned(attr, "`when` goes with `profiles_of`"));
|
||||
}
|
||||
}
|
||||
Ok(r)
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
enum Which {
|
||||
Exprs,
|
||||
Refs,
|
||||
Geometry,
|
||||
}
|
||||
|
||||
/// A value worked out before the walk (a condition, a label, a sketch id) for field `b`.
|
||||
fn pre(b: &Ident, what: &str) -> Ident {
|
||||
format_ident!("__{}_{}", what, b)
|
||||
}
|
||||
|
||||
/// The statements that walk field `b` in walk `which`.
|
||||
fn walk_field(b: &Ident, r: &Roles, which: Which, mutable: bool) -> Tokens {
|
||||
let w = quote!(crate::walk);
|
||||
let each = if mutable { quote!(each_mut) } else { quote!(each) };
|
||||
if r.nested() {
|
||||
let m = match which {
|
||||
Which::Exprs => "walk_exprs",
|
||||
Which::Refs => "walk_refs",
|
||||
Which::Geometry => "walk_geometry",
|
||||
};
|
||||
let m = if mutable { format_ident!("{}_mut", m) } else { format_ident!("{}", m) };
|
||||
if r.picks_faces && which == Which::Geometry {
|
||||
return quote! {
|
||||
#w::Walk::#m(#b, &mut |g| f(match g { #w::Geo::Pos(p) => #w::Geo::Pick(p), g => g }));
|
||||
};
|
||||
}
|
||||
return quote! { #w::Walk::#m(#b, f); };
|
||||
}
|
||||
match which {
|
||||
Which::Exprs => {
|
||||
let label = pre(b, "label");
|
||||
match (&r.expr, &r.geo) {
|
||||
(Some((kind, _)), _) => {
|
||||
let kind = Ident::new(kind, Span::call_site());
|
||||
quote! {
|
||||
#w::ExprField::#each(#b, &mut |i, n, e| f((#w::Label::at(&#label, i, n), e, #w::Kind::#kind, #w::ExprRole::Input)));
|
||||
}
|
||||
}
|
||||
(None, Some(Geo::Placement)) => quote! {
|
||||
#w::ExprField::#each(#b, &mut |i, n, e| f((#w::Label::at(&["X", "Y", "Z"], i, n), e, #w::Kind::Length, #w::ExprRole::Placement)));
|
||||
},
|
||||
_ => quote!(),
|
||||
}
|
||||
}
|
||||
Which::Refs => match r.reference {
|
||||
Some(kind) => {
|
||||
let t = if kind == "Sketch" { quote!(u64) } else { quote!(String) };
|
||||
let kind = Ident::new(kind, Span::call_site());
|
||||
quote! { #w::Each::<#t>::#each(#b, &mut |x| f(#w::Ref::#kind(x))); }
|
||||
}
|
||||
None => quote!(),
|
||||
},
|
||||
Which::Geometry => match &r.geo {
|
||||
Some(Geo::Points(v)) => {
|
||||
let v = Ident::new(v, Span::call_site());
|
||||
quote! { #w::Each::<#w::Vec3>::#each(#b, &mut |p| f(#w::Geo::#v(p))); }
|
||||
}
|
||||
Some(Geo::PickIf(_)) => {
|
||||
let pick = pre(b, "pick");
|
||||
quote! {
|
||||
#w::Each::<#w::Vec3>::#each(#b, &mut |p| f(if #pick { #w::Geo::Pick(p) } else { #w::Geo::Pos(p) }));
|
||||
}
|
||||
}
|
||||
Some(Geo::Placement) => quote! { f(#w::Geo::Placement(#b)); },
|
||||
Some(Geo::Profiles { .. }) => {
|
||||
let (id, when) = (pre(b, "sketch"), pre(b, "when"));
|
||||
quote! {
|
||||
if #when && let Some(id) = #id {
|
||||
#w::Each::<#w::ProfileSel>::#each(#b, &mut |sel| f(#w::Geo::Profiles(id, sel)));
|
||||
}
|
||||
}
|
||||
}
|
||||
None => quote!(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// The values field `b`'s walk needs, worked out before the walk starts (the walk may hand out
|
||||
/// the fields they read).
|
||||
fn pre_values(b: &Ident, r: &Roles, which: Which) -> Tokens {
|
||||
let w = quote!(crate::walk);
|
||||
match (which, &r.expr, &r.geo) {
|
||||
(Which::Exprs, Some((_, label)), _) => {
|
||||
let l = pre(b, "label");
|
||||
quote! { let #l = #label; }
|
||||
}
|
||||
(Which::Geometry, _, Some(Geo::PickIf(cond))) => {
|
||||
let p = pre(b, "pick");
|
||||
quote! { let #p: bool = #cond; }
|
||||
}
|
||||
(Which::Geometry, _, Some(Geo::Profiles { sketch, when })) => {
|
||||
let (id, wh) = (pre(b, "sketch"), pre(b, "when"));
|
||||
let when = when.as_ref().map_or(quote!(true), |c| quote!(#c));
|
||||
quote! {
|
||||
let #wh: bool = #when;
|
||||
let mut #id: Option<u64> = None;
|
||||
#w::Each::<u64>::each(&*#sketch, &mut |s| #id = Some(*s));
|
||||
}
|
||||
}
|
||||
_ => quote!(),
|
||||
}
|
||||
}
|
||||
|
||||
/// A pattern that binds every field of `fields` by name (`_0`, `_1`… for tuple fields), and the
|
||||
/// body of walk `which` over them.
|
||||
fn arm(fields: &Fields, which: Which, mutable: bool) -> syn::Result<(Tokens, Tokens)> {
|
||||
let mut members = Vec::new();
|
||||
let mut walked: Vec<(Ident, Roles)> = Vec::new();
|
||||
for (i, field) in fields.iter().enumerate() {
|
||||
let b = field.ident.clone().unwrap_or_else(|| format_ident!("_{}", i));
|
||||
members.push(field.ident.clone().map_or_else(|| Member::Unnamed(i.into()), Member::Named));
|
||||
walked.push((b, roles(field)?));
|
||||
}
|
||||
let bindings: Vec<Ident> = walked.iter().map(|(b, _)| b.clone()).collect();
|
||||
// `after = x` moves a field to just after `x`.
|
||||
for b in &bindings {
|
||||
let Some(pos) = walked.iter().position(|(x, _)| x == b) else { continue };
|
||||
let Some(after) = walked.get(pos).and_then(|(_, r)| r.after.clone()) else { continue };
|
||||
let moved = walked.remove(pos);
|
||||
let Some(at) = walked.iter().position(|(x, _)| *x == after) else {
|
||||
return Err(syn::Error::new_spanned(after, "no such field"));
|
||||
};
|
||||
walked.insert(at + 1, moved);
|
||||
}
|
||||
let mut body = Tokens::new();
|
||||
for (b, r) in &walked {
|
||||
body.extend(pre_values(b, r, which));
|
||||
}
|
||||
for (b, r) in &walked {
|
||||
body.extend(walk_field(b, r, which, mutable));
|
||||
}
|
||||
let pattern = match fields {
|
||||
Fields::Named(_) => quote!({ #(#bindings),* }),
|
||||
Fields::Unnamed(_) => quote!({ #(#members: #bindings),* }),
|
||||
Fields::Unit => quote!(),
|
||||
};
|
||||
Ok((pattern, body))
|
||||
}
|
||||
|
||||
fn expand(input: &DeriveInput) -> syn::Result<Tokens> {
|
||||
let name = &input.ident;
|
||||
let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
|
||||
let w = quote!(crate::walk);
|
||||
let mut methods = Tokens::new();
|
||||
for (which, method, item) in
|
||||
[(Which::Exprs, "walk_exprs", "ExprItem"), (Which::Refs, "walk_refs", "RefItem"), (Which::Geometry, "walk_geometry", "GeoItem")]
|
||||
{
|
||||
for mutable in [false, true] {
|
||||
let body = match &input.data {
|
||||
Data::Struct(s) => {
|
||||
let (pattern, body) = arm(&s.fields, which, mutable)?;
|
||||
quote! { let Self #pattern = self; #body }
|
||||
}
|
||||
Data::Enum(e) => {
|
||||
let mut arms = Tokens::new();
|
||||
for v in &e.variants {
|
||||
let ident = &v.ident;
|
||||
let (pattern, body) = arm(&v.fields, which, mutable)?;
|
||||
arms.extend(quote! { Self::#ident #pattern => { #body } });
|
||||
}
|
||||
quote! { match self { #arms } }
|
||||
}
|
||||
Data::Union(u) => return Err(syn::Error::new_spanned(u.union_token, "unions are not walked")),
|
||||
};
|
||||
let (method, item, r) = if mutable {
|
||||
(format_ident!("{}_mut", method), format_ident!("{}Mut", item), quote!(&'a mut))
|
||||
} else {
|
||||
(format_ident!("{}", method), format_ident!("{}", item), quote!(&'a))
|
||||
};
|
||||
methods.extend(quote! {
|
||||
#[allow(unused_variables, unused_mut, clippy::needless_borrow)]
|
||||
fn #method<'a>(#r self, f: &mut dyn FnMut(#w::#item<'a>)) { #body }
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok(quote! {
|
||||
impl #impl_generics #w::Walk for #name #ty_generics #where_clause { #methods }
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user