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

Derive macro for Value trait #42

Merged
merged 6 commits into from
Dec 20, 2023
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
11 changes: 11 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
resolver = "2"
members = [
"execution-plan",
"execution-plan-macros",
"execution-plan-traits",
"modeling-cmds",
"modeling-session",
Expand Down
20 changes: 20 additions & 0 deletions execution-plan-macros/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[package]
name = "kittycad-execution-plan-macros"
version = "0.1.0"
edition = "2021"
repository = "https://github.com/KittyCAD/execution-plan"
rust-version = "1.73"
description = "Macros for working with KittyCAD execution plans"
authors = ["Adam Chalmers"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
proc-macro2 = "1.0.70"
quote = "1.0.33"
syn = "2.0.41"

[lib]
proc-macro = true

[dev-dependencies]
kittycad-execution-plan-traits = { path = "../execution-plan-traits" }
106 changes: 106 additions & 0 deletions execution-plan-macros/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
//! Proc-macros for implementing execution-plan traits.

use proc_macro::TokenStream;
use proc_macro2::Span;
use quote::{quote, quote_spanned};
use syn::{parse_macro_input, spanned::Spanned, DeriveInput, Fields, GenericParam};

/// This will derive the trait `Value` from the `kittycad-execution-plan-traits` crate.
#[proc_macro_derive(ExecutionPlanValue)]
pub fn impl_value(input: TokenStream) -> TokenStream {
// Parse the input tokens into a syntax tree
let input = parse_macro_input!(input as DeriveInput);

let span = input.span();
// Name of type that is deriving Value
let name = input.ident;

// Build the output, possibly using quasi-quotation
let expanded = match input.data {
syn::Data::Struct(data) => impl_value_on_struct(span, name, data, input.generics),
syn::Data::Enum(_) => todo!(),
syn::Data::Union(_) => quote_spanned! {span =>
compile_error!("Value cannot be implemented on a union type")
},
};

// Hand the output tokens back to the compiler
TokenStream::from(expanded)
}

fn impl_value_on_struct(
span: Span,
name: proc_macro2::Ident,
data: syn::DataStruct,
generics: syn::Generics,
) -> proc_macro2::TokenStream {
let Fields::Named(ref fields) = data.fields else {
return quote_spanned! {span =>
compile_error!("Value cannot be implemented on a struct with unnamed fields")
};
};

// We're going to construct some fragments of Rust source code, which will get used in the
// final generated code this function returns.

// For every field in the struct, this macro will:
// - In the `into_parts`, extend the Vec of parts with that field, turned into parts.
// - In the `from_parts`, instantiate a Self with a field from that part.
// Step one is to get a list of all named fields in the struct (and their spans):
let field_names: Vec<_> = fields
.named
.iter()
.filter_map(|field| field.ident.as_ref().map(|ident| (ident, field.span())))
.collect();
// Now we can construct those `into_parts` and `from_parts` fragments.
// We take some care to use the span of each `syn::Field` as
// the span of the corresponding `into_parts()` and `from_parts()`
// calls. This way if one of the field types does not
// implement `Value` then the compiler's error message
// underlines which field it is.
let extend_per_field = field_names.iter().map(|(ident, span)| {
quote_spanned! {*span=>
parts.extend(self.#ident.into_parts());
}
});
let instantiate_each_field = field_names.iter().map(|(ident, span)| {
quote_spanned! {*span=>
#ident: kittycad_execution_plan_traits::Value::from_parts(values)?,
}
});

// Handle generics in the original struct.
// Firstly, if the original struct has defaults on its generics, e.g. Point2d<T = f32>,
// don't include those defaults in this macro's output, because the compiler
// complains it's unnecessary and will soon be a compile error.
let mut generics_without_defaults = generics.clone();
for generic_param in generics_without_defaults.params.iter_mut() {
if let GenericParam::Type(type_param) = generic_param {
type_param.default = None;
}
}
let where_clause = generics.where_clause;

// Final return value: the generated Rust code to implement the trait.
// This uses the fragments above, interpolating them into the final outputted code.
quote! {
impl #generics_without_defaults kittycad_execution_plan_traits::Value for #name #generics_without_defaults
#where_clause
{
fn into_parts(self) -> Vec<kittycad_execution_plan_traits::Primitive> {
let mut parts = Vec::new();
#(#extend_per_field)*
parts
}

fn from_parts<I>(values: &mut I) -> Result<Self, kittycad_execution_plan_traits::MemoryError>
where
I: Iterator<Item = Option<kittycad_execution_plan_traits::Primitive>>,
{
Ok(Self {
#(#instantiate_each_field)*
})
}
}
}
}
19 changes: 19 additions & 0 deletions execution-plan-macros/tests/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
use kittycad_execution_plan_macros::ExecutionPlanValue;
use kittycad_execution_plan_traits::{Primitive, Value};

#[derive(ExecutionPlanValue)]
struct FooGenericWithDefault<T = f32>
where
Primitive: From<T>,
T: Value,
{
f: f64,
i: usize,
t: T,
}

#[derive(ExecutionPlanValue)]
struct FooConcrete {
f: f64,
i: usize,
}
1 change: 1 addition & 0 deletions modeling-cmds/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ enum-iterator = "1.4.1"
enum-iterator-derive = "1.2.1"
euler = "0.4.1"
http = "0.2.9"
kittycad-execution-plan-macros = { path = "../execution-plan-macros" }
kittycad-execution-plan-traits = { path = "../execution-plan-traits" }
kittycad-unit-conversion-derive = "0.1.0"
measurements = "0.11.0"
Expand Down
65 changes: 1 addition & 64 deletions modeling-cmds/src/kcep_value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,72 +3,9 @@ use kittycad_execution_plan_traits::{MemoryError, Primitive, Value};
use crate::{
ok_response::OkModelingCmdResponse,
output,
shared::{Angle, Color, ExportFile, PathSegment, Point2d, Point3d, Point4d},
shared::{Angle, PathSegment, Point2d, Point3d},
};

/// Macro to generate the methods of trait `Value` for the given fields.
/// Args:
/// `$field`: Repeated 0 or more times. Listing of each field in the struct.
/// The order in which these fields are given determines the order that fields are
/// written to and read from memory.
macro_rules! impl_value_on_struct_fields {
($($field:ident),*) => {
fn into_parts(self) -> Vec<Primitive> {
let mut parts = Vec::new();
$(
parts.extend(self.$field.into_parts());
)*
parts
}

fn from_parts<I>(values: &mut I) -> Result<Self, MemoryError>
where
I: Iterator<Item = Option<Primitive>>,
{
$(
let $field = Value::from_parts(values)?;
)*
Ok(Self {
$(
$field,
)*
})
}
};
}

impl<T> Value for Point2d<T>
where
Primitive: From<T>,
T: Value,
{
impl_value_on_struct_fields!(x, y);
}

impl<T> Value for Point3d<T>
where
Primitive: From<T>,
T: Value,
{
impl_value_on_struct_fields!(x, y, z);
}

impl<T> Value for Point4d<T>
where
Primitive: From<T>,
T: Value,
{
impl_value_on_struct_fields!(x, y, z, w);
}

impl Value for Color {
impl_value_on_struct_fields!(r, g, b, a);
}

impl Value for ExportFile {
impl_value_on_struct_fields!(name, contents);
}

pub(crate) const EMPTY: &str = "EMPTY";
pub(crate) const TAKE_SNAPSHOT: &str = "TAKE_SNAPSHOT";
pub(crate) const ARC: &str = "arc";
Expand Down
34 changes: 24 additions & 10 deletions modeling-cmds/src/shared.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ use diesel::{mysql::Mysql, serialize::ToSql, sql_types::Text};
#[cfg(feature = "diesel")]
use diesel_derives::{AsExpression, FromSqlRow};
use enum_iterator::Sequence;
use kittycad_execution_plan_macros::ExecutionPlanValue;
use kittycad_execution_plan_traits as kcep;
use parse_display_derive::{Display, FromStr};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -50,7 +52,7 @@ pub struct AnnotationOptions {
/// Color to render the annotation
pub color: Option<Color>,
/// Position to put the annotation
pub position: Option<Point3d>,
pub position: Option<Point3d<f32>>,
}

/// Options for annotation text
Expand Down Expand Up @@ -93,7 +95,7 @@ pub enum DistanceType {
}

/// An RGBA color
#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, ExecutionPlanValue)]
pub struct Color {
/// Red
pub r: f32,
Expand Down Expand Up @@ -138,10 +140,14 @@ pub enum AnnotationTextAlignmentY {
impl_string_enum_sql! {AnnotationTextAlignmentY}

/// A point in 3D space
#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Default)]
#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Default, ExecutionPlanValue)]
#[serde(rename = "Point3d")]
#[serde(rename_all = "snake_case")]
pub struct Point3d<T = f32> {
pub struct Point3d<T = f32>
where
kcep::Primitive: From<T>,
T: kcep::Value,
{
#[allow(missing_docs)]
pub x: T,
#[allow(missing_docs)]
Expand Down Expand Up @@ -256,10 +262,14 @@ pub enum PathSegment {
}

/// A point in homogeneous (4D) space
#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, ExecutionPlanValue)]
#[serde(rename = "Point4d")]
#[serde(rename_all = "snake_case")]
pub struct Point4d<T = f32> {
pub struct Point4d<T = f32>
where
kcep::Primitive: From<T>,
T: kcep::Value,
{
#[allow(missing_docs)]
pub x: T,
#[allow(missing_docs)]
Expand All @@ -270,17 +280,21 @@ pub struct Point4d<T = f32> {
pub w: T,
}

impl From<euler::Vec3> for Point3d {
impl From<euler::Vec3> for Point3d<f32> {
fn from(v: euler::Vec3) -> Self {
Self { x: v.x, y: v.y, z: v.z }
}
}

/// A point in 2D space
#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, Default)]
#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, Default, ExecutionPlanValue)]
#[serde(rename = "Point2d")]
#[serde(rename_all = "snake_case")]
pub struct Point2d<T = f32> {
pub struct Point2d<T = f32>
where
kcep::Primitive: From<T>,
T: kcep::Value,
{
#[allow(missing_docs)]
pub x: T,
#[allow(missing_docs)]
Expand Down Expand Up @@ -472,7 +486,7 @@ pub enum CurveType {
}

/// A file to be exported to the client.
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Debug, Serialize, Deserialize, JsonSchema, ExecutionPlanValue)]
pub struct ExportFile {
/// The name of the file.
pub name: String,
Expand Down
Loading