From 686203dc18148ead11a7a559a041ff86f5b4ccf0 Mon Sep 17 00:00:00 2001 From: Rain Date: Thu, 14 May 2026 11:26:16 -0700 Subject: [PATCH] [spr] initial version Created using spr 1.3.6-beta.1 --- .claude/.gitignore | 1 + CHANGELOG.md | 6 + daft-derive/src/internals/imp.rs | 87 ++++++++--- .../tests/fixtures/valid/empty-structs.rs | 30 ++++ .../valid/output/empty-structs.output.rs | 139 ++++++++++++++++++ daft-derive/tests/integration_test.rs | 76 ++++++++++ 6 files changed, 320 insertions(+), 19 deletions(-) create mode 100644 .claude/.gitignore create mode 100644 daft-derive/tests/fixtures/valid/empty-structs.rs create mode 100644 daft-derive/tests/fixtures/valid/output/empty-structs.output.rs diff --git a/.claude/.gitignore b/.claude/.gitignore new file mode 100644 index 0000000..53ced0f --- /dev/null +++ b/.claude/.gitignore @@ -0,0 +1 @@ +/settings.local.json diff --git a/CHANGELOG.md b/CHANGELOG.md index aa51c59..989969e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Fixed + +- The `Diffable` derive macro now works properly if there are no struct fields to compare (either empty structs or all fields marked `#[daft(ignore)]`.) + ## [0.1.5] - 2025-09-29 ### Fixed diff --git a/daft-derive/src/internals/imp.rs b/daft-derive/src/internals/imp.rs index a4c4589..dddf87e 100644 --- a/daft-derive/src/internals/imp.rs +++ b/daft-derive/src/internals/imp.rs @@ -324,21 +324,50 @@ fn make_diff_struct( // --- No more errors past this point --- - let struct_def = match &s.fields { - Fields::Named(_) => quote! { - #non_exhaustive - #vis struct #name #new_generics #where_clause #diff_fields + // If the diff struct would otherwise be empty, inject a private + // `PhantomData` field that uses both `'__daft` and the original generics. + // Without it, those parameters would be declared on the diff struct but + // unused. + // + // We use `fn() -> &'__daft Self`, not `&'__daft Self` an empty diff has no + // real data, so making it Send/Sync independent of the original type's + // auto-traits is the best choice. `fn() -> &'__daft Self` is covariant in + // `'__daft` and the original generics, and always `Send + Sync`. + let phantom_ty = { + let ident = &input.ident; + let (_, orig_ty_gen, _) = input.generics.split_for_impl(); + quote! { + ::core::marker::PhantomData &#daft_lt #ident #orig_ty_gen> + } + }; - }, - Fields::Unnamed(_) => quote! { - #non_exhaustive - #vis struct #name #new_generics #diff_fields #where_clause; - }, - Fields::Unit => quote! { - // This is kinda silly - #non_exhaustive - #vis struct #name #new_generics {} #where_clause - }, + let struct_def = if diff_fields.fields.is_empty() { + match &s.fields { + Fields::Named(_) | Fields::Unit => quote! { + #non_exhaustive + #vis struct #name #new_generics #where_clause { + _phantom: #phantom_ty, + } + }, + Fields::Unnamed(_) => quote! { + #non_exhaustive + #vis struct #name #new_generics (#phantom_ty) #where_clause; + }, + } + } else { + match &s.fields { + Fields::Named(_) => quote! { + #non_exhaustive + #vis struct #name #new_generics #where_clause #diff_fields + }, + Fields::Unnamed(_) => quote! { + #non_exhaustive + #vis struct #name #new_generics #diff_fields #where_clause; + }, + Fields::Unit => unreachable!( + "Fields::Unit always produces an empty diff struct" + ), + } }; // Generate PartialEq, Eq, and Debug implementations for the diff struct. We @@ -395,8 +424,13 @@ fn make_diff_struct( ); let members = diff_fields.fields.members(); - let partial_eq_body: Expr = parse_quote! { - #(self.#members == other.#members) && * + // Return true if there aren't any fields to compare. + let partial_eq_body: Expr = if diff_fields.fields.is_empty() { + parse_quote! { true } + } else { + parse_quote! { + #(self.#members == other.#members) && * + } }; quote! { @@ -448,6 +482,23 @@ fn make_diff_impl( let (impl_gen, ty_gen, _) = &input.generics.split_for_impl(); let (_, new_ty_gen, where_clause) = &new_generics.split_for_impl(); + let constructor = if diff_fields.fields.is_empty() { + match &diff_fields.fields { + Fields::Named(_) | Fields::Unit => quote! { + Self::Diff { _phantom: ::core::marker::PhantomData } + }, + Fields::Unnamed(_) => quote! { + Self::Diff { 0: ::core::marker::PhantomData } + }, + } + } else { + quote! { + Self::Diff { + #diffs + } + } + }; + quote! { impl #impl_gen #daft_crate::Diffable for #ident #ty_gen #where_clause @@ -455,9 +506,7 @@ fn make_diff_impl( type Diff<#daft_lt> = #name #new_ty_gen where Self: #daft_lt; fn diff<#daft_lt>(&#daft_lt self, other: &#daft_lt Self) -> #name #new_ty_gen { - Self::Diff { - #diffs - } + #constructor } } } diff --git a/daft-derive/tests/fixtures/valid/empty-structs.rs b/daft-derive/tests/fixtures/valid/empty-structs.rs new file mode 100644 index 0000000..216b0e9 --- /dev/null +++ b/daft-derive/tests/fixtures/valid/empty-structs.rs @@ -0,0 +1,30 @@ +use daft::Diffable; +use std::marker::PhantomData; + +#[derive(Debug, Eq, PartialEq, Diffable)] +struct UnitStruct; + +#[derive(Debug, Eq, PartialEq, Diffable)] +struct EmptyNamed {} + +#[derive(Debug, Eq, PartialEq, Diffable)] +struct EmptyTuple(); + +#[derive(Debug, Eq, PartialEq, Diffable)] +struct AllIgnoredNamed { + #[daft(ignore)] + _a: i32, + #[daft(ignore)] + _b: String, +} + +#[derive(Debug, Eq, PartialEq, Diffable)] +struct AllIgnoredTuple(#[daft(ignore)] i32, #[daft(ignore)] String); + +#[derive(Debug, Eq, PartialEq, Diffable)] +struct GenericAllIgnored { + #[daft(ignore)] + _phantom: PhantomData, +} + +fn main() {} diff --git a/daft-derive/tests/fixtures/valid/output/empty-structs.output.rs b/daft-derive/tests/fixtures/valid/output/empty-structs.output.rs new file mode 100644 index 0000000..2d9b290 --- /dev/null +++ b/daft-derive/tests/fixtures/valid/output/empty-structs.output.rs @@ -0,0 +1,139 @@ +struct UnitStructDiff<'__daft> { + _phantom: ::core::marker::PhantomData &'__daft UnitStruct>, +} +impl<'__daft> ::core::fmt::Debug for UnitStructDiff<'__daft> { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + f.debug_struct(stringify!(UnitStructDiff)).finish() + } +} +impl<'__daft> ::core::cmp::PartialEq for UnitStructDiff<'__daft> { + fn eq(&self, other: &Self) -> bool { + true + } +} +impl<'__daft> ::core::cmp::Eq for UnitStructDiff<'__daft> {} +impl ::daft::Diffable for UnitStruct { + type Diff<'__daft> = UnitStructDiff<'__daft> where Self: '__daft; + fn diff<'__daft>(&'__daft self, other: &'__daft Self) -> UnitStructDiff<'__daft> { + Self::Diff { + _phantom: ::core::marker::PhantomData, + } + } +} +struct EmptyNamedDiff<'__daft> { + _phantom: ::core::marker::PhantomData &'__daft EmptyNamed>, +} +impl<'__daft> ::core::fmt::Debug for EmptyNamedDiff<'__daft> { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + f.debug_struct(stringify!(EmptyNamedDiff)).finish() + } +} +impl<'__daft> ::core::cmp::PartialEq for EmptyNamedDiff<'__daft> { + fn eq(&self, other: &Self) -> bool { + true + } +} +impl<'__daft> ::core::cmp::Eq for EmptyNamedDiff<'__daft> {} +impl ::daft::Diffable for EmptyNamed { + type Diff<'__daft> = EmptyNamedDiff<'__daft> where Self: '__daft; + fn diff<'__daft>(&'__daft self, other: &'__daft Self) -> EmptyNamedDiff<'__daft> { + Self::Diff { + _phantom: ::core::marker::PhantomData, + } + } +} +struct EmptyTupleDiff<'__daft>(::core::marker::PhantomData &'__daft EmptyTuple>); +impl<'__daft> ::core::fmt::Debug for EmptyTupleDiff<'__daft> { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + f.debug_tuple(stringify!(EmptyTupleDiff)).finish() + } +} +impl<'__daft> ::core::cmp::PartialEq for EmptyTupleDiff<'__daft> { + fn eq(&self, other: &Self) -> bool { + true + } +} +impl<'__daft> ::core::cmp::Eq for EmptyTupleDiff<'__daft> {} +impl ::daft::Diffable for EmptyTuple { + type Diff<'__daft> = EmptyTupleDiff<'__daft> where Self: '__daft; + fn diff<'__daft>(&'__daft self, other: &'__daft Self) -> EmptyTupleDiff<'__daft> { + Self::Diff { + 0: ::core::marker::PhantomData, + } + } +} +struct AllIgnoredNamedDiff<'__daft> { + _phantom: ::core::marker::PhantomData &'__daft AllIgnoredNamed>, +} +impl<'__daft> ::core::fmt::Debug for AllIgnoredNamedDiff<'__daft> { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + f.debug_struct(stringify!(AllIgnoredNamedDiff)).finish() + } +} +impl<'__daft> ::core::cmp::PartialEq for AllIgnoredNamedDiff<'__daft> { + fn eq(&self, other: &Self) -> bool { + true + } +} +impl<'__daft> ::core::cmp::Eq for AllIgnoredNamedDiff<'__daft> {} +impl ::daft::Diffable for AllIgnoredNamed { + type Diff<'__daft> = AllIgnoredNamedDiff<'__daft> where Self: '__daft; + fn diff<'__daft>( + &'__daft self, + other: &'__daft Self, + ) -> AllIgnoredNamedDiff<'__daft> { + Self::Diff { + _phantom: ::core::marker::PhantomData, + } + } +} +struct AllIgnoredTupleDiff<'__daft>( + ::core::marker::PhantomData &'__daft AllIgnoredTuple>, +); +impl<'__daft> ::core::fmt::Debug for AllIgnoredTupleDiff<'__daft> { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + f.debug_tuple(stringify!(AllIgnoredTupleDiff)).finish() + } +} +impl<'__daft> ::core::cmp::PartialEq for AllIgnoredTupleDiff<'__daft> { + fn eq(&self, other: &Self) -> bool { + true + } +} +impl<'__daft> ::core::cmp::Eq for AllIgnoredTupleDiff<'__daft> {} +impl ::daft::Diffable for AllIgnoredTuple { + type Diff<'__daft> = AllIgnoredTupleDiff<'__daft> where Self: '__daft; + fn diff<'__daft>( + &'__daft self, + other: &'__daft Self, + ) -> AllIgnoredTupleDiff<'__daft> { + Self::Diff { + 0: ::core::marker::PhantomData, + } + } +} +struct GenericAllIgnoredDiff<'__daft, T: '__daft> { + _phantom: ::core::marker::PhantomData &'__daft GenericAllIgnored>, +} +impl<'__daft, T: '__daft> ::core::fmt::Debug for GenericAllIgnoredDiff<'__daft, T> { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + f.debug_struct(stringify!(GenericAllIgnoredDiff)).finish() + } +} +impl<'__daft, T: '__daft> ::core::cmp::PartialEq for GenericAllIgnoredDiff<'__daft, T> { + fn eq(&self, other: &Self) -> bool { + true + } +} +impl<'__daft, T: '__daft> ::core::cmp::Eq for GenericAllIgnoredDiff<'__daft, T> {} +impl ::daft::Diffable for GenericAllIgnored { + type Diff<'__daft> = GenericAllIgnoredDiff<'__daft, T> where Self: '__daft; + fn diff<'__daft>( + &'__daft self, + other: &'__daft Self, + ) -> GenericAllIgnoredDiff<'__daft, T> { + Self::Diff { + _phantom: ::core::marker::PhantomData, + } + } +} diff --git a/daft-derive/tests/integration_test.rs b/daft-derive/tests/integration_test.rs index 56c13a8..d2f5395 100644 --- a/daft-derive/tests/integration_test.rs +++ b/daft-derive/tests/integration_test.rs @@ -159,6 +159,82 @@ fn test_struct_with_generics() { println!("{diff:#?}"); } +#[test] +fn test_empty_structs() { + // Cover every shape that yields an empty diff: unit, empty named, empty + // tuple, and any of the above where every field is `#[daft(ignore)]`. + #[derive(Debug, Eq, PartialEq, Diffable)] + struct UnitStruct; + + #[derive(Debug, Eq, PartialEq, Diffable)] + struct EmptyNamed {} + + #[derive(Debug, Eq, PartialEq, Diffable)] + struct EmptyTuple(); + + #[derive(Debug, Eq, PartialEq, Diffable)] + struct AllIgnoredNamed { + #[daft(ignore)] + a: i32, + #[daft(ignore)] + b: String, + } + + #[derive(Debug, Eq, PartialEq, Diffable)] + struct AllIgnoredTuple(#[daft(ignore)] i32, #[daft(ignore)] String); + + // Two diffs of any empty type should compare equal -- there is nothing to + // distinguish them. + assert_eq!(UnitStruct.diff(&UnitStruct), UnitStruct.diff(&UnitStruct)); + assert_eq!( + EmptyNamed {}.diff(&EmptyNamed {}), + EmptyNamed {}.diff(&EmptyNamed {}) + ); + assert_eq!( + EmptyTuple().diff(&EmptyTuple()), + EmptyTuple().diff(&EmptyTuple()) + ); + + // For all-ignored structs, even values that differ in the ignored fields + // must produce equal diffs. + let a = AllIgnoredNamed { a: 1, b: "x".into() }; + let b = AllIgnoredNamed { a: 2, b: "y".into() }; + assert_eq!(a.diff(&b), a.diff(&a)); + + let a = AllIgnoredTuple(1, "x".into()); + let b = AllIgnoredTuple(2, "y".into()); + assert_eq!(a.diff(&b), a.diff(&a)); + + // Debug output is just the type name with no field listing. + assert_eq!(format!("{:?}", UnitStruct.diff(&UnitStruct)), "UnitStructDiff"); + assert_eq!( + format!( + "{:?}", + AllIgnoredTuple(0, String::new()) + .diff(&AllIgnoredTuple(0, String::new())) + ), + "AllIgnoredTupleDiff", + ); + + // Empty diff structs should be `Send + Sync` regardless of the original + // type's auto-traits -- there is no data inside to share. + #[derive(Diffable)] + struct AllIgnoredNonSync { + #[daft(ignore)] + _c: std::cell::Cell, + } + + fn assert_send() {} + fn assert_sync() {} + + assert_send::>(); + assert_sync::>(); + assert_send::>(); + assert_sync::>(); + assert_send::>(); + assert_sync::>(); +} + #[test] fn diff_pair_lifetimes() { // Complex type to ensure lifetimes are correct.