Skip to content
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
1 change: 1 addition & 0 deletions .claude/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/settings.local.json
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
87 changes: 68 additions & 19 deletions daft-derive/src/internals/imp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<fn() -> &#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
Expand Down Expand Up @@ -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! {
Expand Down Expand Up @@ -448,16 +482,31 @@ 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
{
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
}
}
}
Expand Down
30 changes: 30 additions & 0 deletions daft-derive/tests/fixtures/valid/empty-structs.rs
Original file line number Diff line number Diff line change
@@ -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<T> {
#[daft(ignore)]
_phantom: PhantomData<T>,
}

fn main() {}
139 changes: 139 additions & 0 deletions daft-derive/tests/fixtures/valid/output/empty-structs.output.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
struct UnitStructDiff<'__daft> {
_phantom: ::core::marker::PhantomData<fn() -> &'__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<fn() -> &'__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<fn() -> &'__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<fn() -> &'__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<fn() -> &'__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<fn() -> &'__daft GenericAllIgnored<T>>,
}
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<T> ::daft::Diffable for GenericAllIgnored<T> {
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,
}
}
}
76 changes: 76 additions & 0 deletions daft-derive/tests/integration_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<u32>,
}

fn assert_send<T: Send>() {}
fn assert_sync<T: Sync>() {}

assert_send::<UnitStructDiff<'_>>();
assert_sync::<UnitStructDiff<'_>>();
assert_send::<AllIgnoredTupleDiff<'_>>();
assert_sync::<AllIgnoredTupleDiff<'_>>();
assert_send::<AllIgnoredNonSyncDiff<'_>>();
assert_sync::<AllIgnoredNonSyncDiff<'_>>();
}

#[test]
fn diff_pair_lifetimes() {
// Complex type to ensure lifetimes are correct.
Expand Down
Loading