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]>
295 lines
12 KiB
Rust
295 lines
12 KiB
Rust
//! `#[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 }
|
|
})
|
|
}
|