From de56873a94f9b09125062b32f557856e1198ea77 Mon Sep 17 00:00:00 2001 From: chriseth Date: Wed, 26 Nov 2025 22:05:04 +0100 Subject: [PATCH 01/30] Initial implementation of generics. --- src/lib.rs | 177 +++++++++++++++++++---- tests/test_ancestors.rs | 70 +++++++++ tests/test_arithmetic.rs | 97 +++++++++++++ tests/test_basic_generic.rs | 25 ++++ tests/test_course_enrollment.rs | 96 ++++++++++++ tests/test_generic_transitive_closure.rs | 69 +++++++++ tests/test_graph_connectivity.rs | 78 ++++++++++ tests/test_strings.rs | 89 ++++++++++++ tests/test_type_system.rs | 106 ++++++++++++++ 9 files changed, 778 insertions(+), 29 deletions(-) create mode 100644 tests/test_ancestors.rs create mode 100644 tests/test_arithmetic.rs create mode 100644 tests/test_basic_generic.rs create mode 100644 tests/test_course_enrollment.rs create mode 100644 tests/test_generic_transitive_closure.rs create mode 100644 tests/test_graph_connectivity.rs create mode 100644 tests/test_strings.rs create mode 100644 tests/test_type_system.rs diff --git a/src/lib.rs b/src/lib.rs index 04cb6b6..e664d52 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -316,11 +316,10 @@ impl Context { abort!(relation.name.span(), "Duplicate relation name: {}", name); } - if let Some(t) = relation.generics.type_params().next() { - abort!(t.span(), "Type parameters are not supported in relations"); - } + // Type parameters are now supported! + // Const parameters are not yet supported if let Some(c) = relation.generics.const_params().next() { - abort!(c.span(), "Const parameters are not supported in relations"); + abort!(c.span(), "Const parameters are not yet supported in relations"); } let num_lifetimes = relation.generics.lifetimes().count(); @@ -550,16 +549,34 @@ fn make_struct_decls(context: &Context) -> proc_macro2::TokenStream { let generics = &relation.generics; let semi_token = &relation.semi_token; let fields = &relation.fields; - quote_spanned! {name.span()=> - #[derive( - ::core::marker::Copy, - ::core::clone::Clone, - ::core::cmp::Eq, - ::core::cmp::PartialEq, - ::core::hash::Hash, - )] - #(#attrs)* - #vis #struct_token #name #generics (#fields)#semi_token + + // Check if there are generic params + if generics.params.is_empty() { + quote_spanned! {name.span()=> + #[derive( + ::core::marker::Copy, + ::core::clone::Clone, + ::core::cmp::Eq, + ::core::cmp::PartialEq, + ::core::hash::Hash, + )] + #(#attrs)* + #vis #struct_token #name (#fields) #semi_token + } + } else { + let gen_params = &generics.params; + let where_clause = &generics.where_clause; + quote_spanned! {name.span()=> + #[derive( + ::core::marker::Copy, + ::core::clone::Clone, + ::core::cmp::Eq, + ::core::cmp::PartialEq, + ::core::hash::Hash, + )] + #(#attrs)* + #vis #struct_token #name < #gen_params > (#fields) #where_clause #semi_token + } } }) .collect() @@ -570,8 +587,6 @@ fn make_runtime_decl(context: &Context) -> proc_macro2::TokenStream { .rels_input .values() .map(|relation| { - // because the generics have been validated to only contain lifetimes - // no further checking is done here. let rel_ty = relation_type(relation, LifetimeUsage::Item); let lowercase_name = to_lowercase(&relation.name); quote! { @@ -580,11 +595,11 @@ fn make_runtime_decl(context: &Context) -> proc_macro2::TokenStream { }) .collect(); - let lifetime = lifetime(context.has_input_lifetime); + let generics_decl = generic_params_decl(context); quote! { #[derive(::core::default::Default)] - struct Crepe #lifetime { + struct Crepe #generics_decl { #fields } } @@ -594,10 +609,11 @@ fn make_runtime_impl(context: &Context) -> proc_macro2::TokenStream { let builders = make_extend(context); let run = make_run(context); - let lifetime = lifetime(context.has_input_lifetime); + let generics_decl = generic_params_decl(context); + let generics_args = generic_params_args(context); quote! { - impl #lifetime Crepe #lifetime { + impl #generics_decl Crepe #generics_args { fn new() -> Self { ::core::default::Default::default() } @@ -613,21 +629,50 @@ fn make_extend(context: &Context) -> proc_macro2::TokenStream { .values() .map(|relation| { let rel_ty = relation_type(relation, LifetimeUsage::Item); - let lifetime = lifetime(context.has_input_lifetime); + let generics_decl = generic_params_decl(context); + let generics_args = generic_params_args(context); let lower = to_lowercase(&relation.name); + + // For the reference impl, we need to add the lifetime to the existing generics + let ref_impl_generics = if context.has_input_lifetime { + // Already has 'a, need to add '__b + let type_params: Vec<_> = collect_generic_params(context) + .into_iter() + .map(|tp| { + let ident = &tp.ident; + quote! { #ident: ::std::hash::Hash + ::std::cmp::Eq + ::std::clone::Clone + ::std::marker::Copy + ::std::default::Default } + }) + .collect(); + quote! { <'a, '__b, #(#type_params),*> } + } else { + // No 'a, but might have type params + let type_params: Vec<_> = collect_generic_params(context) + .into_iter() + .map(|tp| { + let ident = &tp.ident; + quote! { #ident: ::std::hash::Hash + ::std::cmp::Eq + ::std::clone::Clone + ::std::marker::Copy + ::std::default::Default } + }) + .collect(); + if type_params.is_empty() { + quote! { <'__b> } + } else { + quote! { <'__b, #(#type_params),*> } + } + }; + quote! { - impl #lifetime ::core::iter::Extend<#rel_ty> for Crepe #lifetime { - fn extend(&mut self, iter: T) + impl #generics_decl ::core::iter::Extend<#rel_ty> for Crepe #generics_args { + fn extend<__I>(&mut self, iter: __I) where - T: ::core::iter::IntoIterator, + __I: ::core::iter::IntoIterator, { self.#lower.extend(iter); } } - impl<'a> ::core::iter::Extend<&'a #rel_ty> for Crepe #lifetime { - fn extend(&mut self, iter: T) + impl #ref_impl_generics ::core::iter::Extend<&'__b #rel_ty> for Crepe #generics_args { + fn extend<__I>(&mut self, iter: __I) where - T: ::core::iter::IntoIterator, + __I: ::core::iter::IntoIterator, { self.extend(iter.into_iter().copied()); } @@ -1249,12 +1294,71 @@ fn lifetime(needs_lifetime: bool) -> proc_macro2::TokenStream { } } +/// Collect all unique type parameters from input relations +fn collect_generic_params(context: &Context) -> Vec<&syn::TypeParam> { + let mut seen = HashSet::new(); + let mut params = Vec::new(); + + for relation in context.rels_input.values() { + for param in relation.generics.type_params() { + if seen.insert(param.ident.to_string()) { + params.push(param); + } + } + } + + params +} + +/// Create a tokenstream for generic parameters (lifetimes + type params) +/// Returns both the parameter declarations and the arguments +fn generic_params_decl(context: &Context) -> proc_macro2::TokenStream { + let has_lifetime = context.has_input_lifetime; + let type_params: Vec<_> = collect_generic_params(context) + .into_iter() + .map(|tp| { + let ident = &tp.ident; + // Add required trait bounds: Hash, Eq, Clone, Copy, Default + quote! { #ident: ::std::hash::Hash + ::std::cmp::Eq + ::std::clone::Clone + ::std::marker::Copy + ::std::default::Default } + }) + .collect(); + + if !has_lifetime && type_params.is_empty() { + quote! {} + } else if has_lifetime && type_params.is_empty() { + quote! { <'a> } + } else if !has_lifetime && !type_params.is_empty() { + quote! { <#(#type_params),*> } + } else { + quote! { <'a, #(#type_params),*> } + } +} + +/// Create a tokenstream for generic arguments (just the names, no bounds) +fn generic_params_args(context: &Context) -> proc_macro2::TokenStream { + let has_lifetime = context.has_input_lifetime; + let type_param_idents: Vec<_> = collect_generic_params(context) + .into_iter() + .map(|tp| &tp.ident) + .collect(); + + if !has_lifetime && type_param_idents.is_empty() { + quote! {} + } else if has_lifetime && type_param_idents.is_empty() { + quote! { <'a> } + } else if !has_lifetime && !type_param_idents.is_empty() { + quote! { <#(#type_param_idents),*> } + } else { + quote! { <'a, #(#type_param_idents),*> } + } +} + enum LifetimeUsage { Item, Local, } -/// Returns the type of a relation, with appropriate lifetimes +/// Returns the type of a relation, with appropriate lifetimes and type parameters fn relation_type(rel: &Relation, usage: LifetimeUsage) -> proc_macro2::TokenStream { let symbol = match rel.relation_type().unwrap() { RelationType::Input | RelationType::Output => "'a", @@ -1270,5 +1374,20 @@ fn relation_type(rel: &Relation, usage: LifetimeUsage) -> proc_macro2::TokenStre .lifetimes() .map(|l| Lifetime::new(symbol, l.span())) .collect::>(); - quote! { #name<#(#lifetimes),*> } + + // Collect type parameters + let type_params = rel + .generics + .type_params() + .map(|tp| &tp.ident) + .collect::>(); + + // Combine lifetimes and type parameters + if type_params.is_empty() { + quote! { #name<#(#lifetimes),*> } + } else if lifetimes.is_empty() { + quote! { #name<#(#type_params),*> } + } else { + quote! { #name<#(#lifetimes,)* #(#type_params),*> } + } } diff --git a/tests/test_ancestors.rs b/tests/test_ancestors.rs new file mode 100644 index 0000000..4ab6116 --- /dev/null +++ b/tests/test_ancestors.rs @@ -0,0 +1,70 @@ +// This test checks ancestor relationships using recursive rules. +// It tests multiple recursive paths and demonstrates family tree queries. + +use crepe::crepe; + +crepe! { + @input + struct Parent(&'static str, &'static str); + + @output + struct Ancestor(&'static str, &'static str); + + @output + struct Sibling(&'static str, &'static str); + + // Base case: parents are ancestors + Ancestor(x, y) <- Parent(x, y); + + // Recursive case: if x is parent of z and z is ancestor of y, then x is ancestor of y + Ancestor(x, y) <- Parent(x, z), Ancestor(z, y); + + // Siblings share a parent + Sibling(a, b) <- Parent(p, a), Parent(p, b), (a != b); +} + +#[test] +fn test_ancestors() { + let mut runtime = Crepe::new(); + runtime.extend([ + Parent("alice", "bob"), + Parent("alice", "charlie"), + Parent("bob", "dave"), + Parent("bob", "eve"), + Parent("charlie", "frank"), + Parent("dave", "grace"), + ]); + + let (ancestors, siblings) = runtime.run(); + + let mut ancestor_vec: Vec<_> = ancestors + .into_iter() + .map(|Ancestor(a, b)| (a, b)) + .collect(); + ancestor_vec.sort_unstable(); + + // Check that all ancestor relationships are correct + assert!(ancestor_vec.contains(&("alice", "bob"))); + assert!(ancestor_vec.contains(&("alice", "charlie"))); + assert!(ancestor_vec.contains(&("alice", "dave"))); + assert!(ancestor_vec.contains(&("alice", "eve"))); + assert!(ancestor_vec.contains(&("alice", "frank"))); + assert!(ancestor_vec.contains(&("alice", "grace"))); + assert!(ancestor_vec.contains(&("bob", "dave"))); + assert!(ancestor_vec.contains(&("bob", "eve"))); + assert!(ancestor_vec.contains(&("bob", "grace"))); + assert!(ancestor_vec.contains(&("charlie", "frank"))); + assert!(ancestor_vec.contains(&("dave", "grace"))); + + let mut sibling_vec: Vec<_> = siblings + .into_iter() + .map(|Sibling(a, b)| (a, b)) + .collect(); + sibling_vec.sort_unstable(); + + // Check siblings (note: both directions) + assert!(sibling_vec.contains(&("bob", "charlie"))); + assert!(sibling_vec.contains(&("charlie", "bob"))); + assert!(sibling_vec.contains(&("dave", "eve"))); + assert!(sibling_vec.contains(&("eve", "dave"))); +} diff --git a/tests/test_arithmetic.rs b/tests/test_arithmetic.rs new file mode 100644 index 0000000..c83f844 --- /dev/null +++ b/tests/test_arithmetic.rs @@ -0,0 +1,97 @@ +// This test checks mathematical operations and numeric comparisons. +// It tests arithmetic expressions, multiple conditions, and range queries. + +use crepe::crepe; + +crepe! { + @input + struct Number(i32); + + @output + struct Square(i32, i32); + + @output + struct Even(i32); + + @output + struct PerfectSquare(i32); + + @output + struct InRange(i32); + + // Calculate squares + Square(x, y) <- Number(x), let y = x * x; + + // Find even numbers + Even(x) <- Number(x), (x % 2 == 0); + + // Find perfect squares within the input numbers + PerfectSquare(x) <- Number(x), Number(y), (y * y == x); + + // Numbers in a specific range [10, 30] + InRange(x) <- Number(x), (x >= 10), (x <= 30); +} + +#[test] +fn test_arithmetic() { + let mut runtime = Crepe::new(); + runtime.extend([ + Number(1), + Number(2), + Number(3), + Number(4), + Number(5), + Number(9), + Number(16), + Number(20), + Number(25), + Number(30), + Number(35), + ]); + + let (squares, evens, perfect_squares, in_range) = runtime.run(); + + let mut square_vec: Vec<_> = squares + .into_iter() + .map(|Square(x, y)| (x, y)) + .collect(); + square_vec.sort_unstable(); + + assert_eq!(square_vec, vec![ + (1, 1), + (2, 4), + (3, 9), + (4, 16), + (5, 25), + (9, 81), + (16, 256), + (20, 400), + (25, 625), + (30, 900), + (35, 1225), + ]); + + let mut even_vec: Vec<_> = evens + .into_iter() + .map(|Even(x)| x) + .collect(); + even_vec.sort_unstable(); + + assert_eq!(even_vec, vec![2, 4, 16, 20, 30]); + + let mut perfect_vec: Vec<_> = perfect_squares + .into_iter() + .map(|PerfectSquare(x)| x) + .collect(); + perfect_vec.sort_unstable(); + + assert_eq!(perfect_vec, vec![1, 4, 9, 16, 25]); + + let mut range_vec: Vec<_> = in_range + .into_iter() + .map(|InRange(x)| x) + .collect(); + range_vec.sort_unstable(); + + assert_eq!(range_vec, vec![16, 20, 25, 30]); +} diff --git a/tests/test_basic_generic.rs b/tests/test_basic_generic.rs new file mode 100644 index 0000000..b068bf1 --- /dev/null +++ b/tests/test_basic_generic.rs @@ -0,0 +1,25 @@ +// Test basic generic type parameter support + +use crepe::crepe; + +crepe! { + @input + struct Input(T); + + @output + struct Output(T); + + Output(x) <- Input(x); +} + +#[test] +fn test_basic_generic() { + let mut runtime = Crepe::new(); + runtime.extend([Input(1), Input(2), Input(3)]); + + let (output,) = runtime.run(); + let mut results: Vec<_> = output.into_iter().map(|Output(x)| x).collect(); + results.sort_unstable(); + + assert_eq!(results, vec![1, 2, 3]); +} diff --git a/tests/test_course_enrollment.rs b/tests/test_course_enrollment.rs new file mode 100644 index 0000000..d84da9a --- /dev/null +++ b/tests/test_course_enrollment.rs @@ -0,0 +1,96 @@ +// This test checks stratified negation with multiple strata. +// It demonstrates complex negation patterns and set difference operations. + +use crepe::crepe; + +crepe! { + @input + struct Student(&'static str); + + @input + struct EnrolledIn(&'static str, &'static str); + + @input + struct Prerequisite(&'static str, &'static str); + + @output + struct CanEnroll(&'static str, &'static str); + + @output + struct MissingPrereq(&'static str, &'static str); + + struct HasPrerequisite(&'static str, &'static str); + + // Student has taken the prerequisite if enrolled in it + HasPrerequisite(student, course) <- + Prerequisite(course, prereq), + EnrolledIn(student, prereq); + + // Student can enroll if not already enrolled + // and either no prerequisite exists or they have it + CanEnroll(student, course) <- + Student(student), + Prerequisite(course, _), + !EnrolledIn(student, course), + HasPrerequisite(student, course); + + // Can also enroll if no prerequisites exist + CanEnroll(student, course) <- + Student(student), + Prerequisite(course, _), + !EnrolledIn(student, course), + !Prerequisite(course, _); + + // Missing prerequisite + MissingPrereq(student, course) <- + Student(student), + Prerequisite(course, prereq), + !EnrolledIn(student, prereq); +} + +#[test] +fn test_stratified_negation() { + let mut runtime = Crepe::new(); + runtime.extend([ + Student("alice"), + Student("bob"), + Student("charlie"), + ]); + runtime.extend([ + EnrolledIn("alice", "math101"), + EnrolledIn("alice", "cs101"), + EnrolledIn("bob", "math101"), + EnrolledIn("charlie", "cs101"), + ]); + runtime.extend([ + Prerequisite("cs201", "cs101"), + Prerequisite("math201", "math101"), + Prerequisite("cs301", "cs201"), + ]); + + let (can_enroll, missing) = runtime.run(); + + let can_vec: Vec<_> = can_enroll + .into_iter() + .map(|CanEnroll(s, c)| (s, c)) + .collect(); + + // Alice has both cs101 and math101, can enroll in cs201 and math201 + assert!(can_vec.contains(&("alice", "cs201"))); + assert!(can_vec.contains(&("alice", "math201"))); + + // Bob only has math101 + assert!(can_vec.contains(&("bob", "math201"))); + assert!(!can_vec.contains(&("bob", "cs201"))); + + let missing_vec: Vec<_> = missing + .into_iter() + .map(|MissingPrereq(s, c)| (s, c)) + .collect(); + + // Bob is missing cs101 for cs201 + assert!(missing_vec.contains(&("bob", "cs201"))); + + // Charlie is missing math101 for math201 + assert!(missing_vec.contains(&("charlie", "math201"))); +} diff --git a/tests/test_generic_transitive_closure.rs b/tests/test_generic_transitive_closure.rs new file mode 100644 index 0000000..d163113 --- /dev/null +++ b/tests/test_generic_transitive_closure.rs @@ -0,0 +1,69 @@ +// Test generic transitive closure + +use crepe::crepe; + +crepe! { + @input + struct Edge(T, T); + + @output + struct Reachable(T, T); + + Reachable(x, y) <- Edge(x, y); + Reachable(x, z) <- Edge(x, y), Reachable(y, z); +} + +#[test] +fn test_generic_transitive_closure() { + let mut runtime = Crepe::new(); + runtime.extend([ + Edge(1, 2), + Edge(2, 3), + Edge(3, 4), + Edge(2, 5), + ]); + + let (reachable,) = runtime.run(); + let mut results: Vec<_> = reachable + .into_iter() + .map(|Reachable(x, y)| (x, y)) + .collect(); + results.sort_unstable(); + + let expected = vec![ + (1, 2), + (1, 3), + (1, 4), + (1, 5), + (2, 3), + (2, 4), + (2, 5), + (3, 4), + ]; + + assert_eq!(results, expected); +} + +#[test] +fn test_generic_with_strings() { + let mut runtime = Crepe::new(); + runtime.extend([ + Edge("a", "b"), + Edge("b", "c"), + Edge("c", "d"), + ]); + + let (reachable,) = runtime.run(); + let mut results: Vec<_> = reachable + .into_iter() + .map(|Reachable(x, y)| (x, y)) + .collect(); + results.sort_unstable(); + + assert!(results.contains(&("a", "b"))); + assert!(results.contains(&("a", "c"))); + assert!(results.contains(&("a", "d"))); + assert!(results.contains(&("b", "c"))); + assert!(results.contains(&("b", "d"))); + assert!(results.contains(&("c", "d"))); +} diff --git a/tests/test_graph_connectivity.rs b/tests/test_graph_connectivity.rs new file mode 100644 index 0000000..4f15a7a --- /dev/null +++ b/tests/test_graph_connectivity.rs @@ -0,0 +1,78 @@ +// This test checks path finding in a graph with multiple paths. +// It demonstrates connectivity analysis with isolated nodes. + +use crepe::crepe; + +crepe! { + @input + struct Road(&'static str, &'static str); + + @input + struct City(&'static str); + + @output + struct Connected(&'static str, &'static str); + + @output + struct Isolated(&'static str); + + // Bidirectional connectivity + Connected(x, y) <- Road(x, y); + Connected(x, y) <- Road(y, x); + Connected(x, z) <- Connected(x, y), Connected(y, z); + + // A city is isolated if it has no road connections + Isolated(x) <- City(x), !Road(x, _), !Road(_, x); +} + +#[test] +fn test_graph_connectivity() { + let mut runtime = Crepe::new(); + runtime.extend([ + Road("seattle", "portland"), + Road("portland", "san_francisco"), + Road("san_francisco", "los_angeles"), + Road("denver", "salt_lake"), + Road("salt_lake", "las_vegas"), + ]); + runtime.extend([ + City("seattle"), + City("portland"), + City("san_francisco"), + City("los_angeles"), + City("denver"), + City("salt_lake"), + City("las_vegas"), + City("miami"), // isolated city with no roads + ]); + + let (connected, isolated) = runtime.run(); + + let mut conn_vec: Vec<_> = connected + .into_iter() + .map(|Connected(a, b)| (a, b)) + .collect(); + conn_vec.sort_unstable(); + + // Check west coast connectivity + assert!(conn_vec.contains(&("seattle", "portland"))); + assert!(conn_vec.contains(&("seattle", "san_francisco"))); + assert!(conn_vec.contains(&("seattle", "los_angeles"))); + assert!(conn_vec.contains(&("portland", "los_angeles"))); + + // Check mountain region connectivity + assert!(conn_vec.contains(&("denver", "salt_lake"))); + assert!(conn_vec.contains(&("denver", "las_vegas"))); + + // Verify no cross-region connections + assert!(!conn_vec.contains(&("seattle", "denver"))); + assert!(!conn_vec.contains(&("portland", "salt_lake"))); + + let iso_vec: Vec<_> = isolated + .into_iter() + .map(|Isolated(x)| x) + .collect(); + + // Miami is isolated - it's a city but has no road connections + assert_eq!(iso_vec, vec!["miami"]); +} diff --git a/tests/test_strings.rs b/tests/test_strings.rs new file mode 100644 index 0000000..3b6ea57 --- /dev/null +++ b/tests/test_strings.rs @@ -0,0 +1,89 @@ +// This test checks string operations and pattern matching. +// It demonstrates working with string slices and comparisons. + +use crepe::crepe; + +crepe! { + @input + struct Word<'a>(&'a str); + + @output + struct ShortWord<'a>(&'a str); + + @output + struct LongWord<'a>(&'a str); + + @output + struct StartsWith<'a>(&'a str, char); + + @output + struct Palindrome<'a>(&'a str); + + // Words with length <= 4 + ShortWord(w) <- Word(w), (w.len() <= 4); + + // Words with length > 8 + LongWord(w) <- Word(w), (w.len() > 8); + + // Track first character + StartsWith(w, c) <- Word(w), let c = w.chars().next().unwrap(); + + // Check for palindromes + Palindrome(w) <- + Word(w), + let reversed = w.chars().rev().collect::(), + (w == reversed.as_str()); +} + +#[test] +fn test_string_operations() { + let mut runtime = Crepe::new(); + runtime.extend([ + Word("hello"), + Word("world"), + Word("rust"), + Word("programming"), + Word("code"), + Word("data"), + Word("a"), + Word("racecar"), + Word("level"), + Word("test"), + ]); + + let (short, long, starts, palindromes) = runtime.run(); + + let mut short_vec: Vec<_> = short + .into_iter() + .map(|ShortWord(w)| w) + .collect(); + short_vec.sort_unstable(); + + assert_eq!(short_vec, vec!["a", "code", "data", "rust", "test"]); + + let mut long_vec: Vec<_> = long + .into_iter() + .map(|LongWord(w)| w) + .collect(); + long_vec.sort_unstable(); + + assert_eq!(long_vec, vec!["programming"]); + + let starts_vec: Vec<_> = starts + .into_iter() + .map(|StartsWith(w, c)| (w, c)) + .collect(); + + assert!(starts_vec.contains(&("hello", 'h'))); + assert!(starts_vec.contains(&("world", 'w'))); + assert!(starts_vec.contains(&("rust", 'r'))); + assert!(starts_vec.contains(&("programming", 'p'))); + + let mut pal_vec: Vec<_> = palindromes + .into_iter() + .map(|Palindrome(w)| w) + .collect(); + pal_vec.sort_unstable(); + + assert_eq!(pal_vec, vec!["a", "level", "racecar"]); +} diff --git a/tests/test_type_system.rs b/tests/test_type_system.rs new file mode 100644 index 0000000..16bbf8f --- /dev/null +++ b/tests/test_type_system.rs @@ -0,0 +1,106 @@ +// This test checks multiple output relations with complex joins. +// It demonstrates a simple type system with subtyping relationships. + +use crepe::crepe; + +#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq, PartialOrd, Ord)] +enum Type { + Int, + Float, + String, + Bool, + Any, +} + +crepe! { + @input + struct Subtype(Type, Type); + + @output + struct IsSubtype(Type, Type); + + @output + struct Compatible(Type, Type); + + @output + struct NotRelated(Type, Type); + + struct AllTypes(Type); + + // Reflexive: every type is a subtype of itself + IsSubtype(t, t) <- Subtype(t, _); + IsSubtype(t, t) <- Subtype(_, t); + + // Direct subtype relationships + IsSubtype(t1, t2) <- Subtype(t1, t2); + + // Transitive: if A <: B and B <: C, then A <: C + IsSubtype(t1, t3) <- IsSubtype(t1, t2), IsSubtype(t2, t3); + + // Types are compatible if one is a subtype of the other + Compatible(t1, t2) <- IsSubtype(t1, t2); + Compatible(t1, t2) <- IsSubtype(t2, t1); + + // Collect all types + AllTypes(t) <- Subtype(t, _); + AllTypes(t) <- Subtype(_, t); + + // Types are not related if they're not compatible + NotRelated(t1, t2) <- AllTypes(t1), AllTypes(t2), !Compatible(t1, t2); +} + +#[test] +fn test_type_system() { + let mut runtime = Crepe::new(); + runtime.extend([ + Subtype(Type::Int, Type::Float), + Subtype(Type::Int, Type::Any), + Subtype(Type::Float, Type::Any), + Subtype(Type::String, Type::Any), + Subtype(Type::Bool, Type::Any), + ]); + + let (is_subtype, compatible, not_related) = runtime.run(); + + let sub_vec: Vec<_> = is_subtype + .into_iter() + .map(|IsSubtype(t1, t2)| (t1, t2)) + .collect(); + + // Check reflexivity + assert!(sub_vec.contains(&(Type::Int, Type::Int))); + assert!(sub_vec.contains(&(Type::Float, Type::Float))); + + // Check direct relationships + assert!(sub_vec.contains(&(Type::Int, Type::Float))); + assert!(sub_vec.contains(&(Type::Int, Type::Any))); + + // Check transitivity: Int <: Float <: Any implies Int <: Any (already added directly) + assert!(sub_vec.contains(&(Type::Int, Type::Any))); + + let comp_vec: Vec<_> = compatible + .into_iter() + .map(|Compatible(t1, t2)| (t1, t2)) + .collect(); + + // Int and Float are compatible + assert!(comp_vec.contains(&(Type::Int, Type::Float)) || + comp_vec.contains(&(Type::Float, Type::Int))); + + // Everything is compatible with Any + assert!(comp_vec.contains(&(Type::String, Type::Any)) || + comp_vec.contains(&(Type::Any, Type::String))); + + let not_rel_vec: Vec<_> = not_related + .into_iter() + .map(|NotRelated(t1, t2)| (t1, t2)) + .collect(); + + // String and Int are not related + assert!(not_rel_vec.contains(&(Type::String, Type::Int)) || + not_rel_vec.contains(&(Type::Int, Type::String))); + + // Bool and Float are not related + assert!(not_rel_vec.contains(&(Type::Bool, Type::Float)) || + not_rel_vec.contains(&(Type::Float, Type::Bool))); +} From e847517270a30674981d12301cad781de1fc8440 Mon Sep 17 00:00:00 2001 From: chriseth Date: Wed, 26 Nov 2025 22:48:22 +0100 Subject: [PATCH 02/30] Finish generics. --- src/lib.rs | 70 ++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 57 insertions(+), 13 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index e664d52..f939215 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -638,20 +638,14 @@ fn make_extend(context: &Context) -> proc_macro2::TokenStream { // Already has 'a, need to add '__b let type_params: Vec<_> = collect_generic_params(context) .into_iter() - .map(|tp| { - let ident = &tp.ident; - quote! { #ident: ::std::hash::Hash + ::std::cmp::Eq + ::std::clone::Clone + ::std::marker::Copy + ::std::default::Default } - }) + .map(|tp| merge_bounds_with_required(tp)) .collect(); quote! { <'a, '__b, #(#type_params),*> } } else { // No 'a, but might have type params let type_params: Vec<_> = collect_generic_params(context) .into_iter() - .map(|tp| { - let ident = &tp.ident; - quote! { #ident: ::std::hash::Hash + ::std::cmp::Eq + ::std::clone::Clone + ::std::marker::Copy + ::std::default::Default } - }) + .map(|tp| merge_bounds_with_required(tp)) .collect(); if type_params.is_empty() { quote! { <'__b> } @@ -1310,17 +1304,67 @@ fn collect_generic_params(context: &Context) -> Vec<&syn::TypeParam> { params } +/// Check if a type parameter has a specific trait bound +fn has_bound(tp: &syn::TypeParam, bound_name: &str) -> bool { + tp.bounds.iter().any(|b| { + if let syn::TypeParamBound::Trait(trait_bound) = b { + // Check if the trait path ends with bound_name + trait_bound.path.segments.last() + .map(|seg| seg.ident == bound_name) + .unwrap_or(false) + } else { + false + } + }) +} + +/// Get the TokenStream for a required bound +fn required_bound_token(name: &str) -> proc_macro2::TokenStream { + match name { + "Hash" => quote! { ::std::hash::Hash }, + "Eq" => quote! { ::std::cmp::Eq }, + "Clone" => quote! { ::std::clone::Clone }, + "Copy" => quote! { ::std::marker::Copy }, + "Default" => quote! { ::std::default::Default }, + _ => panic!("Unknown required bound: {}", name), + } +} + +/// Merge user bounds with required bounds, avoiding duplicates +fn merge_bounds_with_required(tp: &syn::TypeParam) -> proc_macro2::TokenStream { + let ident = &tp.ident; + let user_bounds = &tp.bounds; + + // Required bounds for Datalog + let required = ["Hash", "Eq", "Clone", "Copy", "Default"]; + let mut missing_bounds = Vec::new(); + + for req in &required { + if !has_bound(tp, req) { + missing_bounds.push(required_bound_token(req)); + } + } + + // Combine user bounds + missing required bounds + if missing_bounds.is_empty() { + // User already has all required bounds + quote! { #ident: #user_bounds } + } else if user_bounds.is_empty() { + // No user bounds, just add required ones + quote! { #ident: #(#missing_bounds)+* } + } else { + // Combine both + quote! { #ident: #user_bounds + #(#missing_bounds)+* } + } +} + /// Create a tokenstream for generic parameters (lifetimes + type params) /// Returns both the parameter declarations and the arguments fn generic_params_decl(context: &Context) -> proc_macro2::TokenStream { let has_lifetime = context.has_input_lifetime; let type_params: Vec<_> = collect_generic_params(context) .into_iter() - .map(|tp| { - let ident = &tp.ident; - // Add required trait bounds: Hash, Eq, Clone, Copy, Default - quote! { #ident: ::std::hash::Hash + ::std::cmp::Eq + ::std::clone::Clone + ::std::marker::Copy + ::std::default::Default } - }) + .map(|tp| merge_bounds_with_required(tp)) .collect(); if !has_lifetime && type_params.is_empty() { From da8acfdf0d566f89178e7b58b8163753accdee40 Mon Sep 17 00:00:00 2001 From: chriseth Date: Wed, 26 Nov 2025 22:55:18 +0100 Subject: [PATCH 03/30] More tests. --- tests/test_complex_multiple_bounds.rs | 83 +++++++++++ tests/test_complex_multiple_generics.rs | 80 +++++++++++ tests/test_complex_trait_methods.rs | 96 +++++++++++++ tests/test_custom_trait_bounds.rs | 86 +++++++++++ tests/test_graph_with_node_trait.rs | 70 +++++++++ tests/test_multiple_generics.rs | 59 ++++++++ tests/test_multiple_generics_with_bounds.rs | 58 ++++++++ tests/test_multiple_trait_bounds.rs | 56 ++++++++ tests/test_multiple_trait_methods.rs | 87 ++++++++++++ tests/test_no_duplicate_bounds.rs | 35 +++++ tests/test_showcase_all_features.rs | 149 ++++++++++++++++++++ tests/test_trait_method_call.rs | 53 +++++++ 12 files changed, 912 insertions(+) create mode 100644 tests/test_complex_multiple_bounds.rs create mode 100644 tests/test_complex_multiple_generics.rs create mode 100644 tests/test_complex_trait_methods.rs create mode 100644 tests/test_custom_trait_bounds.rs create mode 100644 tests/test_graph_with_node_trait.rs create mode 100644 tests/test_multiple_generics.rs create mode 100644 tests/test_multiple_generics_with_bounds.rs create mode 100644 tests/test_multiple_trait_bounds.rs create mode 100644 tests/test_multiple_trait_methods.rs create mode 100644 tests/test_no_duplicate_bounds.rs create mode 100644 tests/test_showcase_all_features.rs create mode 100644 tests/test_trait_method_call.rs diff --git a/tests/test_complex_multiple_bounds.rs b/tests/test_complex_multiple_bounds.rs new file mode 100644 index 0000000..50e44cf --- /dev/null +++ b/tests/test_complex_multiple_bounds.rs @@ -0,0 +1,83 @@ +// Test multiple type parameters each with multiple trait bounds + +use crepe::crepe; +use std::fmt::{Debug, Display}; + +trait KeyTrait { + fn key_value(&self) -> u32; +} + +trait ValueTrait { + fn value_text(&self) -> &'static str; +} + +#[derive(Hash, Eq, PartialEq, Clone, Copy, Default)] +struct MyKey(u32); + +#[derive(Hash, Eq, PartialEq, Clone, Copy, Default)] +struct MyValue(&'static str); + +impl KeyTrait for MyKey { + fn key_value(&self) -> u32 { self.0 } +} + +impl Debug for MyKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Key({})", self.0) + } +} + +impl Display for MyKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "K{}", self.0) + } +} + +impl ValueTrait for MyValue { + fn value_text(&self) -> &'static str { self.0 } +} + +impl Debug for MyValue { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Value({})", self.0) + } +} + +impl Display for MyValue { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "V:{}", self.0) + } +} + +crepe! { + @input + struct Mapping(K, V); + + @output + struct Reversed(V, K); + + Reversed(v, k) <- Mapping(k, v); +} + +#[test] +fn test_complex_multiple_bounds() { + let mut runtime = Crepe::new(); + runtime.extend([ + Mapping(MyKey(1), MyValue("one")), + Mapping(MyKey(2), MyValue("two")), + Mapping(MyKey(3), MyValue("three")), + ]); + + let (reversed,) = runtime.run(); + let mut results: Vec<_> = reversed + .into_iter() + .map(|Reversed(v, k)| (v.value_text(), k.key_value())) + .collect(); + results.sort_unstable(); + + assert_eq!(results, vec![ + ("one", 1), + ("three", 3), + ("two", 2), + ]); +} diff --git a/tests/test_complex_multiple_generics.rs b/tests/test_complex_multiple_generics.rs new file mode 100644 index 0000000..1381d10 --- /dev/null +++ b/tests/test_complex_multiple_generics.rs @@ -0,0 +1,80 @@ +// Test combining multiple generics with different relations + +use crepe::crepe; + +trait Label { + fn label(&self) -> &'static str; +} + +#[derive(Hash, Eq, PartialEq, Clone, Copy, Debug, Default)] +struct Node(u32); + +#[derive(Hash, Eq, PartialEq, Clone, Copy, Debug, Default)] +struct Tag(&'static str); + +impl Label for Tag { + fn label(&self) -> &'static str { self.0 } +} + +crepe! { + @input + struct Edge(T, T); + + @input + struct Tagged(T, L); + + @output + struct Reachable(T, T); + + @output + struct LabeledPath(T, T, L); + + // Transitive closure + Reachable(x, y) <- Edge(x, y); + Reachable(x, z) <- Edge(x, y), Reachable(y, z); + + // Labeled paths + LabeledPath(x, y, l) <- Edge(x, y), Tagged(x, l); +} + +#[test] +fn test_complex_multiple_generics() { + let mut runtime = Crepe::new(); + + // Add edges + runtime.extend([ + Edge(Node(1), Node(2)), + Edge(Node(2), Node(3)), + Edge(Node(3), Node(4)), + ]); + + // Add tags + runtime.extend([ + Tagged(Node(1), Tag("start")), + Tagged(Node(2), Tag("middle")), + ]); + + let (reachable, labeled) = runtime.run(); + + // Check reachability + let reach_vec: Vec<_> = reachable + .into_iter() + .map(|Reachable(x, y)| (x.0, y.0)) + .collect(); + + assert!(reach_vec.contains(&(1, 2))); + assert!(reach_vec.contains(&(1, 3))); + assert!(reach_vec.contains(&(1, 4))); + assert!(reach_vec.contains(&(2, 3))); + assert!(reach_vec.contains(&(2, 4))); + assert!(reach_vec.contains(&(3, 4))); + + // Check labeled paths + let labeled_vec: Vec<_> = labeled + .into_iter() + .map(|LabeledPath(x, y, l)| (x.0, y.0, l.label())) + .collect(); + + assert!(labeled_vec.contains(&(1, 2, "start"))); + assert!(labeled_vec.contains(&(2, 3, "middle"))); +} diff --git a/tests/test_complex_trait_methods.rs b/tests/test_complex_trait_methods.rs new file mode 100644 index 0000000..2b52c84 --- /dev/null +++ b/tests/test_complex_trait_methods.rs @@ -0,0 +1,96 @@ +// Test more complex trait method usage + +use crepe::crepe; + +trait Node { + fn id(&self) -> u32; + fn priority(&self) -> u32; +} + +#[derive(Hash, Eq, PartialEq, Clone, Copy, Debug, Default)] +struct Task { + task_id: u32, + prio: u32, +} + +impl Node for Task { + fn id(&self) -> u32 { + self.task_id + } + + fn priority(&self) -> u32 { + self.prio + } +} + +crepe! { + @input + struct Edge(T, T); + + @output + struct Path(T, T, u32); + + @output + struct HighPriorityPath(T, T); + + // Calculate path with combined priority + Path(x, y, p) <- + Edge(x, y), + let p = x.priority() + y.priority(); + + // Transitive paths with priority sum + Path(x, z, p) <- + Edge(x, y), + Path(y, z, p2), + let p = x.priority() + p2; + + // Filter high priority paths + HighPriorityPath(x, y) <- + Path(x, y, p), + (p > 10); +} + +#[test] +fn test_complex_trait_methods() { + let t1 = Task { task_id: 1, prio: 3 }; + let t2 = Task { task_id: 2, prio: 5 }; + let t3 = Task { task_id: 3, prio: 7 }; + + let mut runtime = Crepe::new(); + runtime.extend([ + Edge(t1, t2), + Edge(t2, t3), + ]); + + let (paths, high_prio) = runtime.run(); + + // Check path priorities + let path_vec: Vec<_> = paths + .into_iter() + .map(|Path(x, y, p)| (x.id(), y.id(), p)) + .collect(); + + // t1 -> t2: priority 3 + 5 = 8 + assert!(path_vec.contains(&(1, 2, 8))); + + // t2 -> t3: priority 5 + 7 = 12 + assert!(path_vec.contains(&(2, 3, 12))); + + // t1 -> t3: priority 3 + 12 = 15 + assert!(path_vec.contains(&(1, 3, 15))); + + // Check high priority paths (> 10) + let high_vec: Vec<_> = high_prio + .into_iter() + .map(|HighPriorityPath(x, y)| (x.id(), y.id())) + .collect(); + + // t2 -> t3 has priority 12 > 10 + assert!(high_vec.contains(&(2, 3))); + + // t1 -> t3 has priority 15 > 10 + assert!(high_vec.contains(&(1, 3))); + + // t1 -> t2 has priority 8 < 10 (should not be in high_prio) + assert!(!high_vec.contains(&(1, 2))); +} diff --git a/tests/test_custom_trait_bounds.rs b/tests/test_custom_trait_bounds.rs new file mode 100644 index 0000000..e1a4077 --- /dev/null +++ b/tests/test_custom_trait_bounds.rs @@ -0,0 +1,86 @@ +// Test generic with custom trait bounds + +use crepe::crepe; + +// Custom trait +trait Valuable { + fn value(&self) -> i32; +} + +// Type that implements the custom trait and required traits +#[derive(Hash, Eq, PartialEq, Clone, Copy, Debug)] +struct Item(i32); + +impl Valuable for Item { + fn value(&self) -> i32 { + self.0 + } +} + +impl Default for Item { + fn default() -> Self { + Item(0) + } +} + +crepe! { + @input + struct Input(T); + + @output + struct Output(T); + + Output(x) <- Input(x); +} + +#[test] +fn test_custom_trait_bound() { + let mut runtime = Crepe::new(); + runtime.extend([ + Input(Item(1)), + Input(Item(2)), + Input(Item(3)), + ]); + + let (output,) = runtime.run(); + let mut results: Vec<_> = output + .into_iter() + .map(|Output(x)| x.value()) + .collect(); + results.sort_unstable(); + + assert_eq!(results, vec![1, 2, 3]); +} + +#[test] +fn test_custom_trait_with_integers() { + // Create a simple wrapper that implements Valuable + #[derive(Hash, Eq, PartialEq, Clone, Copy, Debug)] + struct Val(i32); + + impl Valuable for Val { + fn value(&self) -> i32 { self.0 } + } + + impl Default for Val { + fn default() -> Self { Val(0) } + } + + let mut runtime = Crepe::new(); + runtime.extend([ + Input(Val(10)), + Input(Val(20)), + Input(Val(30)), + ]); + + let (output,) = runtime.run(); + let results: Vec<_> = output + .into_iter() + .map(|Output(x)| x.value()) + .collect(); + + assert_eq!(results.len(), 3); + assert!(results.contains(&10)); + assert!(results.contains(&20)); + assert!(results.contains(&30)); +} diff --git a/tests/test_graph_with_node_trait.rs b/tests/test_graph_with_node_trait.rs new file mode 100644 index 0000000..9c9e9ff --- /dev/null +++ b/tests/test_graph_with_node_trait.rs @@ -0,0 +1,70 @@ +// Test generic graph with custom node type and trait + +use crepe::crepe; +use std::fmt::Display; + +// Custom trait for graph nodes +trait Node: Display { + fn id(&self) -> u32; +} + +#[derive(Hash, Eq, PartialEq, Clone, Copy, Debug, Default)] +struct City { + id: u32, + name: &'static str, +} + +impl Node for City { + fn id(&self) -> u32 { + self.id + } +} + +impl Display for City { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}({})", self.name, self.id) + } +} + +crepe! { + @input + struct Connection(T, T); + + @output + struct Connected(T, T); + + Connected(x, y) <- Connection(x, y); + Connected(x, z) <- Connection(x, y), Connected(y, z); +} + +#[test] +fn test_graph_with_custom_node_trait() { + let seattle = City { id: 1, name: "Seattle" }; + let portland = City { id: 2, name: "Portland" }; + let sf = City { id: 3, name: "SF" }; + let la = City { id: 4, name: "LA" }; + + let mut runtime = Crepe::new(); + runtime.extend([ + Connection(seattle, portland), + Connection(portland, sf), + Connection(sf, la), + ]); + + let (connected,) = runtime.run(); + let mut results: Vec<_> = connected + .into_iter() + .map(|Connected(x, y)| (x.id(), y.id())) + .collect(); + results.sort_unstable(); + + // Check all connections + assert!(results.contains(&(1, 2))); // Seattle -> Portland + assert!(results.contains(&(1, 3))); // Seattle -> SF + assert!(results.contains(&(1, 4))); // Seattle -> LA + assert!(results.contains(&(2, 3))); // Portland -> SF + assert!(results.contains(&(2, 4))); // Portland -> LA + assert!(results.contains(&(3, 4))); // SF -> LA + + assert_eq!(results.len(), 6); +} diff --git a/tests/test_multiple_generics.rs b/tests/test_multiple_generics.rs new file mode 100644 index 0000000..5f8b852 --- /dev/null +++ b/tests/test_multiple_generics.rs @@ -0,0 +1,59 @@ +// Test multiple generic type parameters + +use crepe::crepe; + +crepe! { + @input + struct Pair(K, V); + + @output + struct Swapped(V, K); + + Swapped(v, k) <- Pair(k, v); +} + +#[test] +fn test_multiple_generics() { + let mut runtime = Crepe::new(); + runtime.extend([ + Pair(1, "one"), + Pair(2, "two"), + Pair(3, "three"), + ]); + + let (swapped,) = runtime.run(); + let mut results: Vec<_> = swapped + .into_iter() + .map(|Swapped(v, k)| (v, k)) + .collect(); + results.sort_unstable(); + + assert_eq!(results, vec![ + ("one", 1), + ("three", 3), + ("two", 2), + ]); +} + +#[test] +fn test_multiple_generics_same_type() { + let mut runtime = Crepe::new(); + runtime.extend([ + Pair(1, 10), + Pair(2, 20), + Pair(3, 30), + ]); + + let (swapped,) = runtime.run(); + let mut results: Vec<_> = swapped + .into_iter() + .map(|Swapped(v, k)| (v, k)) + .collect(); + results.sort_unstable(); + + assert_eq!(results, vec![ + (10, 1), + (20, 2), + (30, 3), + ]); +} diff --git a/tests/test_multiple_generics_with_bounds.rs b/tests/test_multiple_generics_with_bounds.rs new file mode 100644 index 0000000..49ea4ef --- /dev/null +++ b/tests/test_multiple_generics_with_bounds.rs @@ -0,0 +1,58 @@ +// Test multiple generic parameters with custom trait bounds + +use crepe::crepe; + +trait Key { + fn key_id(&self) -> u32; +} + +trait Value { + fn value_id(&self) -> u32; +} + +#[derive(Hash, Eq, PartialEq, Clone, Copy, Debug, Default)] +struct MyKey(u32); + +#[derive(Hash, Eq, PartialEq, Clone, Copy, Debug, Default)] +struct MyValue(u32); + +impl Key for MyKey { + fn key_id(&self) -> u32 { self.0 } +} + +impl Value for MyValue { + fn value_id(&self) -> u32 { self.0 } +} + +crepe! { + @input + struct Entry(K, V); + + @output + struct Flipped(V, K); + + Flipped(v, k) <- Entry(k, v); +} + +#[test] +fn test_multiple_generics_with_trait_bounds() { + let mut runtime = Crepe::new(); + runtime.extend([ + Entry(MyKey(1), MyValue(10)), + Entry(MyKey(2), MyValue(20)), + Entry(MyKey(3), MyValue(30)), + ]); + + let (flipped,) = runtime.run(); + let mut results: Vec<_> = flipped + .into_iter() + .map(|Flipped(v, k)| (v.value_id(), k.key_id())) + .collect(); + results.sort_unstable(); + + assert_eq!(results, vec![ + (10, 1), + (20, 2), + (30, 3), + ]); +} diff --git a/tests/test_multiple_trait_bounds.rs b/tests/test_multiple_trait_bounds.rs new file mode 100644 index 0000000..593ae2c --- /dev/null +++ b/tests/test_multiple_trait_bounds.rs @@ -0,0 +1,56 @@ +// Test multiple trait bounds on a single type parameter + +use crepe::crepe; +use std::fmt::{Debug, Display}; + +trait Custom { + fn custom_id(&self) -> u32; +} + +#[derive(Hash, Eq, PartialEq, Clone, Copy, Default)] +struct Item(u32); + +impl Custom for Item { + fn custom_id(&self) -> u32 { self.0 } +} + +impl Debug for Item { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Item({})", self.0) + } +} + +impl Display for Item { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +crepe! { + @input + struct Input(T); + + @output + struct Output(T); + + Output(x) <- Input(x); +} + +#[test] +fn test_multiple_trait_bounds() { + let mut runtime = Crepe::new(); + runtime.extend([ + Input(Item(1)), + Input(Item(2)), + Input(Item(3)), + ]); + + let (output,) = runtime.run(); + let mut results: Vec<_> = output + .into_iter() + .map(|Output(x)| x.custom_id()) + .collect(); + results.sort_unstable(); + + assert_eq!(results, vec![1, 2, 3]); +} diff --git a/tests/test_multiple_trait_methods.rs b/tests/test_multiple_trait_methods.rs new file mode 100644 index 0000000..dc08867 --- /dev/null +++ b/tests/test_multiple_trait_methods.rs @@ -0,0 +1,87 @@ +// Test multiple trait methods in expressions + +use crepe::crepe; + +trait Measurable { + fn weight(&self) -> u32; + fn volume(&self) -> u32; + fn density(&self) -> u32 { + if self.volume() > 0 { + self.weight() / self.volume() + } else { + 0 + } + } +} + +#[derive(Hash, Eq, PartialEq, Clone, Copy, Debug, Default)] +struct Package { + id: u32, + w: u32, + v: u32, +} + +impl Measurable for Package { + fn weight(&self) -> u32 { self.w } + fn volume(&self) -> u32 { self.v } +} + +crepe! { + @input + struct Item(T); + + @output + struct ItemStats(T, u32, u32, u32); + + @output + struct Dense(T); + + // Extract stats using multiple trait methods + ItemStats(x, w, v, d) <- + Item(x), + let w = x.weight(), + let v = x.volume(), + let d = x.density(); + + // Filter dense items (density > 5) + Dense(x) <- + Item(x), + let d = x.density(), + (d > 5); +} + +#[test] +fn test_multiple_trait_methods() { + let p1 = Package { id: 1, w: 100, v: 10 }; // density = 10 + let p2 = Package { id: 2, w: 50, v: 20 }; // density = 2 + let p3 = Package { id: 3, w: 200, v: 25 }; // density = 8 + + let mut runtime = Crepe::new(); + runtime.extend([ + Item(p1), + Item(p2), + Item(p3), + ]); + + let (stats, dense) = runtime.run(); + + // Check stats + let stats_vec: Vec<_> = stats + .into_iter() + .map(|ItemStats(pkg, w, v, d)| (pkg.id, w, v, d)) + .collect(); + + assert!(stats_vec.contains(&(1, 100, 10, 10))); + assert!(stats_vec.contains(&(2, 50, 20, 2))); + assert!(stats_vec.contains(&(3, 200, 25, 8))); + + // Check dense items (density > 5) + let dense_vec: Vec<_> = dense + .into_iter() + .map(|Dense(pkg)| pkg.id) + .collect(); + + assert!(dense_vec.contains(&1)); // density 10 > 5 + assert!(dense_vec.contains(&3)); // density 8 > 5 + assert!(!dense_vec.contains(&2)); // density 2 < 5 +} diff --git a/tests/test_no_duplicate_bounds.rs b/tests/test_no_duplicate_bounds.rs new file mode 100644 index 0000000..0eb86ed --- /dev/null +++ b/tests/test_no_duplicate_bounds.rs @@ -0,0 +1,35 @@ +// Test that duplicate bounds are not added + +use crepe::crepe; +use std::hash::Hash; + +#[derive(Hash, Eq, PartialEq, Clone, Copy, Debug, Default)] +struct Value(i32); + +crepe! { + @input + struct Input(T); + + @output + struct Output(T); + + Output(x) <- Input(x); +} + +#[test] +fn test_no_duplicate_bounds() { + let mut runtime = Crepe::new(); + runtime.extend([ + Input(Value(1)), + Input(Value(2)), + Input(Value(3)), + ]); + + let (output,) = runtime.run(); + let results: Vec<_> = output + .into_iter() + .map(|Output(x)| x.0) + .collect(); + + assert_eq!(results.len(), 3); +} diff --git a/tests/test_showcase_all_features.rs b/tests/test_showcase_all_features.rs new file mode 100644 index 0000000..8f0fc6e --- /dev/null +++ b/tests/test_showcase_all_features.rs @@ -0,0 +1,149 @@ +// Showcase: All generic features working together + +use crepe::crepe; +use std::fmt::{Debug, Display}; + +// Multiple trait definitions +trait Node: Debug + Display { + fn id(&self) -> u32; + fn name(&self) -> &'static str; +} + +trait Weighted { + fn weight(&self) -> u32; +} + +// Concrete type implementing multiple traits +#[derive(Hash, Eq, PartialEq, Clone, Copy, Default)] +struct City { + id: u32, + city_name: &'static str, + population: u32, +} + +impl Node for City { + fn id(&self) -> u32 { self.id } + fn name(&self) -> &'static str { self.city_name } +} + +impl Weighted for City { + fn weight(&self) -> u32 { self.population / 1000 } +} + +impl Debug for City { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "City{{id={}, name={}}}", self.id, self.city_name) + } +} + +impl Display for City { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.city_name) + } +} + +crepe! { + // Multiple type parameters with multiple bounds each + @input + struct Connection(N, N, W); + + @input + struct Location(N); + + @output + struct Route(N, N, u32); + + @output + struct WeightedRoute(N, N, u32); + + @output + struct HeavyNode(N); + + // Basic rule with trait method + Route(x, y, d) <- + Connection(x, y, distance), + let d = distance.weight(); + + // Transitive closure with trait methods and arithmetic + Route(x, z, total) <- + Connection(x, y, d1), + Route(y, z, d2), + let w1 = d1.weight(), + let total = w1 + d2; + + // Complex expression with multiple trait methods + WeightedRoute(x, y, score) <- + Route(x, y, d), + let wx = x.weight(), + let wy = y.weight(), + let score = d + wx + wy; + + // Filter using trait method in condition + HeavyNode(n) <- + Location(n), + let w = n.weight(), + (w > 500); +} + +#[test] +fn test_showcase_all_features() { + let seattle = City { id: 1, city_name: "Seattle", population: 750000 }; + let portland = City { id: 2, city_name: "Portland", population: 650000 }; + let sf = City { id: 3, city_name: "SF", population: 900000 }; + + // Distance is also weighted + #[derive(Hash, Eq, PartialEq, Clone, Copy, Default)] + struct Distance(u32); + + impl Weighted for Distance { + fn weight(&self) -> u32 { self.0 } + } + + let mut runtime = Crepe::new(); + + // Add connections + runtime.extend([ + Connection(seattle, portland, Distance(100)), + Connection(portland, sf, Distance(200)), + ]); + + // Add locations + runtime.extend([ + Location(seattle), + Location(portland), + Location(sf), + ]); + + let (routes, weighted_routes, heavy_nodes) = runtime.run(); + + // Test routes with trait methods + let route_vec: Vec<_> = routes + .into_iter() + .map(|Route(x, y, d)| (x.name(), y.name(), d)) + .collect(); + + assert!(route_vec.contains(&("Seattle", "Portland", 100))); + assert!(route_vec.contains(&("Portland", "SF", 200))); + assert!(route_vec.contains(&("Seattle", "SF", 300))); + + // Test weighted routes + let weighted_vec: Vec<_> = weighted_routes + .into_iter() + .map(|WeightedRoute(x, y, s)| (x.name(), y.name(), s)) + .collect(); + + // Seattle->Portland: distance(100) + seattle.weight(750) + portland.weight(650) = 1500 + assert!(weighted_vec.iter().any(|(x, y, s)| + *x == "Seattle" && *y == "Portland" && *s == 1500 + )); + + // Test heavy nodes (weight > 500) + let heavy_vec: Vec<_> = heavy_nodes + .into_iter() + .map(|HeavyNode(n)| n.name()) + .collect(); + + assert!(heavy_vec.contains(&"Seattle")); // 750 > 500 + assert!(heavy_vec.contains(&"Portland")); // 650 > 500 + assert!(heavy_vec.contains(&"SF")); // 900 > 500 +} diff --git a/tests/test_trait_method_call.rs b/tests/test_trait_method_call.rs new file mode 100644 index 0000000..98ff896 --- /dev/null +++ b/tests/test_trait_method_call.rs @@ -0,0 +1,53 @@ +// Test calling trait methods in Datalog rules + +use crepe::crepe; + +trait Cost { + fn cost(&self) -> u32; +} + +#[derive(Hash, Eq, PartialEq, Clone, Copy, Debug, Default)] +struct Item { + id: u32, + price: u32, +} + +impl Cost for Item { + fn cost(&self) -> u32 { + self.price + } +} + +crepe! { + @input + struct Product(T); + + @output + struct PriceInfo(T, u32); + + // Try calling trait method in rule + PriceInfo(x, c) <- Product(x), let c = x.cost(); +} + +#[test] +fn test_trait_method_call() { + let mut runtime = Crepe::new(); + runtime.extend([ + Product(Item { id: 1, price: 100 }), + Product(Item { id: 2, price: 200 }), + Product(Item { id: 3, price: 300 }), + ]); + + let (price_info,) = runtime.run(); + let mut results: Vec<_> = price_info + .into_iter() + .map(|PriceInfo(item, price)| (item.id, price)) + .collect(); + results.sort_unstable(); + + assert_eq!(results, vec![ + (1, 100), + (2, 200), + (3, 300), + ]); +} From d4a3b4c60bf687c7d88e63809a6a0de825f88842 Mon Sep 17 00:00:00 2001 From: chriseth Date: Wed, 26 Nov 2025 23:47:01 +0100 Subject: [PATCH 04/30] Remove unnecessary '__b lifetime. --- src/lib.rs | 52 +++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 39 insertions(+), 13 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index f939215..14d16bb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -321,6 +321,40 @@ impl Context { if let Some(c) = relation.generics.const_params().next() { abort!(c.span(), "Const parameters are not yet supported in relations"); } + + // Where clauses are not yet supported + if let Some(where_clause) = &relation.generics.where_clause { + abort!( + where_clause.where_token.span(), + "Where clauses are not yet supported in relations. \ + Please specify trait bounds directly on the type parameter instead, e.g., `T: Trait`" + ); + } + + // Check for default type parameters (not supported) + for type_param in relation.generics.type_params() { + if type_param.default.is_some() { + abort!( + type_param.ident.span(), + "Default type parameters are not supported in relations. \ + Please remove the default value from type parameter `{}`", + type_param.ident + ); + } + } + + // Check for lifetime bounds (not supported) + for lifetime_param in relation.generics.lifetimes() { + if !lifetime_param.bounds.is_empty() { + abort!( + lifetime_param.lifetime.span(), + "Lifetime bounds are not supported in relations. \ + Please remove bounds from lifetime parameter `{}`", + lifetime_param.lifetime + ); + } + } + let num_lifetimes = relation.generics.lifetimes().count(); match relation.relation_type() { @@ -634,23 +668,15 @@ fn make_extend(context: &Context) -> proc_macro2::TokenStream { let lower = to_lowercase(&relation.name); // For the reference impl, we need to add the lifetime to the existing generics - let ref_impl_generics = if context.has_input_lifetime { - // Already has 'a, need to add '__b - let type_params: Vec<_> = collect_generic_params(context) - .into_iter() - .map(|tp| merge_bounds_with_required(tp)) - .collect(); - quote! { <'a, '__b, #(#type_params),*> } - } else { - // No 'a, but might have type params + let ref_impl_generics = { let type_params: Vec<_> = collect_generic_params(context) .into_iter() .map(|tp| merge_bounds_with_required(tp)) .collect(); if type_params.is_empty() { - quote! { <'__b> } + quote! { <'a> } } else { - quote! { <'__b, #(#type_params),*> } + quote! { <'a, #(#type_params),*> } } }; @@ -663,10 +689,10 @@ fn make_extend(context: &Context) -> proc_macro2::TokenStream { self.#lower.extend(iter); } } - impl #ref_impl_generics ::core::iter::Extend<&'__b #rel_ty> for Crepe #generics_args { + impl #ref_impl_generics ::core::iter::Extend<&'a #rel_ty> for Crepe #generics_args { fn extend<__I>(&mut self, iter: __I) where - __I: ::core::iter::IntoIterator, + __I: ::core::iter::IntoIterator, { self.extend(iter.into_iter().copied()); } From 00cb5208b725522b7cb7c8a90dbc0ed96d3c9a0a Mon Sep 17 00:00:00 2001 From: chriseth Date: Wed, 26 Nov 2025 23:55:22 +0100 Subject: [PATCH 05/30] Simplify generics formatting. --- src/lib.rs | 55 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 29 insertions(+), 26 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 14d16bb..b1a8b34 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1384,43 +1384,46 @@ fn merge_bounds_with_required(tp: &syn::TypeParam) -> proc_macro2::TokenStream { } } +/// Helper to format generic parameters with angle brackets +/// Returns `` or empty if the list is empty +fn format_generics(items: Vec) -> proc_macro2::TokenStream { + if items.is_empty() { + quote! {} + } else { + quote! { <#(#items),*> } + } +} + /// Create a tokenstream for generic parameters (lifetimes + type params) /// Returns both the parameter declarations and the arguments fn generic_params_decl(context: &Context) -> proc_macro2::TokenStream { - let has_lifetime = context.has_input_lifetime; - let type_params: Vec<_> = collect_generic_params(context) - .into_iter() - .map(|tp| merge_bounds_with_required(tp)) - .collect(); + let mut items = Vec::new(); - if !has_lifetime && type_params.is_empty() { - quote! {} - } else if has_lifetime && type_params.is_empty() { - quote! { <'a> } - } else if !has_lifetime && !type_params.is_empty() { - quote! { <#(#type_params),*> } - } else { - quote! { <'a, #(#type_params),*> } + if context.has_input_lifetime { + items.push(quote! { 'a }); + } + + for tp in collect_generic_params(context) { + items.push(merge_bounds_with_required(tp)); } + + format_generics(items) } /// Create a tokenstream for generic arguments (just the names, no bounds) fn generic_params_args(context: &Context) -> proc_macro2::TokenStream { - let has_lifetime = context.has_input_lifetime; - let type_param_idents: Vec<_> = collect_generic_params(context) - .into_iter() - .map(|tp| &tp.ident) - .collect(); + let mut items = Vec::new(); - if !has_lifetime && type_param_idents.is_empty() { - quote! {} - } else if has_lifetime && type_param_idents.is_empty() { - quote! { <'a> } - } else if !has_lifetime && !type_param_idents.is_empty() { - quote! { <#(#type_param_idents),*> } - } else { - quote! { <'a, #(#type_param_idents),*> } + if context.has_input_lifetime { + items.push(quote! { 'a }); } + + for tp in collect_generic_params(context) { + let ident = &tp.ident; + items.push(quote! { #ident }); + } + + format_generics(items) } enum LifetimeUsage { From 1137eb256b485a54a4849e1c4b98088e7104ef1c Mon Sep 17 00:00:00 2001 From: chriseth Date: Wed, 26 Nov 2025 23:58:27 +0100 Subject: [PATCH 06/30] simplify --- src/lib.rs | 43 ++++++++++++++++--------------------------- 1 file changed, 16 insertions(+), 27 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index b1a8b34..4d320c3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -583,34 +583,23 @@ fn make_struct_decls(context: &Context) -> proc_macro2::TokenStream { let generics = &relation.generics; let semi_token = &relation.semi_token; let fields = &relation.fields; - - // Check if there are generic params - if generics.params.is_empty() { - quote_spanned! {name.span()=> - #[derive( - ::core::marker::Copy, - ::core::clone::Clone, - ::core::cmp::Eq, - ::core::cmp::PartialEq, - ::core::hash::Hash, - )] - #(#attrs)* - #vis #struct_token #name (#fields) #semi_token - } + let gen_params = if generics.params.is_empty() { + quote! { } } else { - let gen_params = &generics.params; - let where_clause = &generics.where_clause; - quote_spanned! {name.span()=> - #[derive( - ::core::marker::Copy, - ::core::clone::Clone, - ::core::cmp::Eq, - ::core::cmp::PartialEq, - ::core::hash::Hash, - )] - #(#attrs)* - #vis #struct_token #name < #gen_params > (#fields) #where_clause #semi_token - } + let generics_params = &generics.params; + quote! { < #generics_params > } + }; + + quote_spanned! {name.span()=> + #[derive( + ::core::marker::Copy, + ::core::clone::Clone, + ::core::cmp::Eq, + ::core::cmp::PartialEq, + ::core::hash::Hash, + )] + #(#attrs)* + #vis #struct_token #name #gen_params (#fields) #semi_token } }) .collect() From 632b4e5855cfab010509113b0e40255ca4e4201f Mon Sep 17 00:00:00 2001 From: chriseth Date: Thu, 27 Nov 2025 00:00:14 +0100 Subject: [PATCH 07/30] simplify --- src/lib.rs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 4d320c3..b209513 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -583,12 +583,6 @@ fn make_struct_decls(context: &Context) -> proc_macro2::TokenStream { let generics = &relation.generics; let semi_token = &relation.semi_token; let fields = &relation.fields; - let gen_params = if generics.params.is_empty() { - quote! { } - } else { - let generics_params = &generics.params; - quote! { < #generics_params > } - }; quote_spanned! {name.span()=> #[derive( @@ -599,7 +593,7 @@ fn make_struct_decls(context: &Context) -> proc_macro2::TokenStream { ::core::hash::Hash, )] #(#attrs)* - #vis #struct_token #name #gen_params (#fields) #semi_token + #vis #struct_token #name #generics (#fields)#semi_token } }) .collect() From ca688ece632547c663a12b6eb60aba914359b203 Mon Sep 17 00:00:00 2001 From: chriseth Date: Thu, 27 Nov 2025 00:59:48 +0100 Subject: [PATCH 08/30] cleanup --- src/lib.rs | 194 ++++++++++++++++++++++------------------------------- 1 file changed, 82 insertions(+), 112 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index b209513..49725fb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -316,44 +316,8 @@ impl Context { abort!(relation.name.span(), "Duplicate relation name: {}", name); } - // Type parameters are now supported! - // Const parameters are not yet supported - if let Some(c) = relation.generics.const_params().next() { - abort!(c.span(), "Const parameters are not yet supported in relations"); - } - - // Where clauses are not yet supported - if let Some(where_clause) = &relation.generics.where_clause { - abort!( - where_clause.where_token.span(), - "Where clauses are not yet supported in relations. \ - Please specify trait bounds directly on the type parameter instead, e.g., `T: Trait`" - ); - } - - // Check for default type parameters (not supported) - for type_param in relation.generics.type_params() { - if type_param.default.is_some() { - abort!( - type_param.ident.span(), - "Default type parameters are not supported in relations. \ - Please remove the default value from type parameter `{}`", - type_param.ident - ); - } - } - - // Check for lifetime bounds (not supported) - for lifetime_param in relation.generics.lifetimes() { - if !lifetime_param.bounds.is_empty() { - abort!( - lifetime_param.lifetime.span(), - "Lifetime bounds are not supported in relations. \ - Please remove bounds from lifetime parameter `{}`", - lifetime_param.lifetime - ); - } - } + // Validate generic parameters + validate_generic_params(&relation); let num_lifetimes = relation.generics.lifetimes().count(); @@ -652,15 +616,11 @@ fn make_extend(context: &Context) -> proc_macro2::TokenStream { // For the reference impl, we need to add the lifetime to the existing generics let ref_impl_generics = { - let type_params: Vec<_> = collect_generic_params(context) - .into_iter() - .map(|tp| merge_bounds_with_required(tp)) - .collect(); - if type_params.is_empty() { - quote! { <'a> } - } else { - quote! { <'a, #(#type_params),*> } + let mut items = vec![quote! { 'a }]; + for tp in collect_generic_params(context) { + items.push(merge_bounds_with_required(tp)); } + format_generics(items) }; quote! { @@ -1288,12 +1248,46 @@ fn to_lowercase(name: &Ident) -> Ident { Ident::new(&s, name.span()) } -/// Create a tokenstream for a lifetime bound/application if it's needed -fn lifetime(needs_lifetime: bool) -> proc_macro2::TokenStream { - if needs_lifetime { - quote! { <'a> } - } else { - quote! {} +/// Validate generic parameters on a relation +/// Checks for unsupported features and provides helpful error messages +fn validate_generic_params(relation: &Relation) { + // Type parameters are now supported! + // Const parameters are not yet supported + if let Some(c) = relation.generics.const_params().next() { + abort!(c.span(), "Const parameters are not yet supported in relations"); + } + + // Where clauses are not yet supported + if let Some(where_clause) = &relation.generics.where_clause { + abort!( + where_clause.where_token.span(), + "Where clauses are not yet supported in relations. \ + Please specify trait bounds directly on the type parameter instead, e.g., `T: Trait`" + ); + } + + // Check for default type parameters (not supported) + for type_param in relation.generics.type_params() { + if type_param.default.is_some() { + abort!( + type_param.ident.span(), + "Default type parameters are not supported in relations. \ + Please remove the default value from type parameter `{}`", + type_param.ident + ); + } + } + + // Check for lifetime bounds (not supported) + for lifetime_param in relation.generics.lifetimes() { + if !lifetime_param.bounds.is_empty() { + abort!( + lifetime_param.lifetime.span(), + "Lifetime bounds are not supported in relations. \ + Please remove bounds from lifetime parameter `{}`", + lifetime_param.lifetime + ); + } } } @@ -1315,18 +1309,18 @@ fn collect_generic_params(context: &Context) -> Vec<&syn::TypeParam> { /// Check if a type parameter has a specific trait bound fn has_bound(tp: &syn::TypeParam, bound_name: &str) -> bool { - tp.bounds.iter().any(|b| { - if let syn::TypeParamBound::Trait(trait_bound) = b { - // Check if the trait path ends with bound_name + tp.bounds.iter().any(|b| match b { + syn::TypeParamBound::Trait(trait_bound) => { trait_bound.path.segments.last() - .map(|seg| seg.ident == bound_name) - .unwrap_or(false) - } else { - false + .map_or(false, |seg| seg.ident == bound_name) } + _ => false, }) } +/// Required trait bounds for all generic types in Datalog relations +const REQUIRED_BOUNDS: &[&str] = &["Hash", "Eq", "Clone", "Copy", "Default"]; + /// Get the TokenStream for a required bound fn required_bound_token(name: &str) -> proc_macro2::TokenStream { match name { @@ -1344,68 +1338,47 @@ fn merge_bounds_with_required(tp: &syn::TypeParam) -> proc_macro2::TokenStream { let ident = &tp.ident; let user_bounds = &tp.bounds; - // Required bounds for Datalog - let required = ["Hash", "Eq", "Clone", "Copy", "Default"]; - let mut missing_bounds = Vec::new(); - - for req in &required { - if !has_bound(tp, req) { - missing_bounds.push(required_bound_token(req)); - } - } + // Collect missing required bounds + let missing_bounds: Vec<_> = REQUIRED_BOUNDS.iter() + .filter(|&req| !has_bound(tp, req)) + .map(|req| required_bound_token(req)) + .collect(); // Combine user bounds + missing required bounds - if missing_bounds.is_empty() { - // User already has all required bounds - quote! { #ident: #user_bounds } - } else if user_bounds.is_empty() { - // No user bounds, just add required ones - quote! { #ident: #(#missing_bounds)+* } - } else { - // Combine both - quote! { #ident: #user_bounds + #(#missing_bounds)+* } + match (user_bounds.is_empty(), missing_bounds.is_empty()) { + (true, true) => quote! { #ident }, // No bounds at all (shouldn't happen) + (true, false) => quote! { #ident: #(#missing_bounds)+* }, + (false, true) => quote! { #ident: #user_bounds }, + (false, false) => quote! { #ident: #user_bounds + #(#missing_bounds)+* }, } } /// Helper to format generic parameters with angle brackets /// Returns `` or empty if the list is empty fn format_generics(items: Vec) -> proc_macro2::TokenStream { - if items.is_empty() { - quote! {} - } else { - quote! { <#(#items),*> } - } + if items.is_empty() { quote! {} } else { quote! { <#(#items),*> } } } /// Create a tokenstream for generic parameters (lifetimes + type params) -/// Returns both the parameter declarations and the arguments fn generic_params_decl(context: &Context) -> proc_macro2::TokenStream { let mut items = Vec::new(); - if context.has_input_lifetime { items.push(quote! { 'a }); } - - for tp in collect_generic_params(context) { - items.push(merge_bounds_with_required(tp)); - } - + items.extend(collect_generic_params(context).into_iter().map(merge_bounds_with_required)); format_generics(items) } /// Create a tokenstream for generic arguments (just the names, no bounds) fn generic_params_args(context: &Context) -> proc_macro2::TokenStream { let mut items = Vec::new(); - if context.has_input_lifetime { items.push(quote! { 'a }); } - - for tp in collect_generic_params(context) { + items.extend(collect_generic_params(context).into_iter().map(|tp| { let ident = &tp.ident; - items.push(quote! { #ident }); - } - + quote! { #ident } + })); format_generics(items) } @@ -1425,25 +1398,22 @@ fn relation_type(rel: &Relation, usage: LifetimeUsage) -> proc_macro2::TokenStre }; let name = &rel.name; - let lifetimes = rel - .generics - .lifetimes() - .map(|l| Lifetime::new(symbol, l.span())) - .collect::>(); - // Collect type parameters - let type_params = rel - .generics - .type_params() - .map(|tp| &tp.ident) - .collect::>(); + // Build list of generic arguments + let mut items = Vec::new(); + items.extend(rel.generics.lifetimes().map(|l| { + let lifetime = Lifetime::new(symbol, l.span()); + quote! { #lifetime } + })); + items.extend(rel.generics.type_params().map(|tp| { + let ident = &tp.ident; + quote! { #ident } + })); - // Combine lifetimes and type parameters - if type_params.is_empty() { - quote! { #name<#(#lifetimes),*> } - } else if lifetimes.is_empty() { - quote! { #name<#(#type_params),*> } + // Format with angle brackets if there are any generics + if items.is_empty() { + quote! { #name } } else { - quote! { #name<#(#lifetimes,)* #(#type_params),*> } + quote! { #name<#(#items),*> } } } From 5a0b8ee9426dbfccab88f838b80fa8da2ac5657f Mon Sep 17 00:00:00 2001 From: chriseth Date: Mon, 1 Dec 2025 23:09:45 +0100 Subject: [PATCH 09/30] Profiling. --- src/lib.rs | 129 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 126 insertions(+), 3 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 49725fb..ce6d2ae 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -583,6 +583,69 @@ fn make_runtime_decl(context: &Context) -> proc_macro2::TokenStream { struct Crepe #generics_decl { #fields } + + /// Profiling statistics for individual rules + #[derive(Debug, Clone)] + pub struct RuleStats { + /// Rule identifier (goal relation and rule index) + pub rule_id: String, + /// Total time spent executing this rule across all iterations + pub total_duration: ::std::time::Duration, + /// Number of times this rule was evaluated + pub eval_count: u64, + /// Number of new facts generated by this rule + pub facts_generated: u64, + } + + impl RuleStats { + /// Get the average time per evaluation + pub fn avg_duration(&self) -> ::std::time::Duration { + if self.eval_count > 0 { + self.total_duration / self.eval_count as u32 + } else { + ::std::time::Duration::ZERO + } + } + } + + /// Profiling results from a Datalog execution + #[derive(Debug, Clone, Default)] + pub struct ProfilingStats { + /// Statistics for each rule + pub rules: ::std::vec::Vec, + /// Total execution time + pub total_duration: ::std::time::Duration, + } + + impl ProfilingStats { + /// Print a formatted report of rule timings + pub fn report(&self) { + println!("\n=== Crepe Profiling Report ==="); + println!("Total execution time: {:?}", self.total_duration); + println!("\nRule timings (sorted by total duration):"); + + let mut sorted_rules = self.rules.clone(); + sorted_rules.sort_by(|a, b| b.total_duration.cmp(&a.total_duration)); + + println!("{:<40} {:>12} {:>12} {:>15} {:>15}", + "Rule", "Total (ms)", "Calls", "Avg (μs)", "Facts Gen."); + println!("{}", "-".repeat(100)); + + for rule in &sorted_rules { + let total_ms = rule.total_duration.as_secs_f64() * 1000.0; + let avg_us = rule.avg_duration().as_secs_f64() * 1_000_000.0; + let pct = if self.total_duration.as_nanos() > 0 { + (rule.total_duration.as_nanos() as f64 / self.total_duration.as_nanos() as f64) * 100.0 + } else { + 0.0 + }; + + println!("{:<40} {:>10.2}ms {:>12} {:>13.2}μs {:>15}", + rule.rule_id, total_ms, rule.eval_count, avg_us, rule.facts_generated); + } + println!(); + } + } } } @@ -775,12 +838,30 @@ fn make_run(context: &Context) -> proc_macro2::TokenStream { #var_update.extend(self.#lower); } }); + let init_profiling = quote! { + let mut __crepe_rule_stats: ::std::collections::HashMap< + String, + (::std::time::Duration, u64, u64) + > = ::std::collections::HashMap::new(); + }; init_rels .chain(init_indices) .chain(load_inputs) + .chain(std::iter::once(init_profiling)) .collect::() }; + let finalize_profiling = quote! { + for (rule_id, (duration, eval_count, facts_generated)) in __crepe_rule_stats { + __crepe_profiling_stats.rules.push(RuleStats { + rule_id, + total_duration: duration, + eval_count, + facts_generated, + }); + } + }; + let output = { let output_fields = context.output_order.iter().map(|name| { let lower = to_lowercase(name); @@ -806,6 +887,25 @@ fn make_run(context: &Context) -> proc_macro2::TokenStream { fn run(self) -> #output_ty_default { self.run_with_hasher::<::std::collections::hash_map::RandomState>() } + + #[allow(clippy::collapsible_if)] + fn run_with_profiling_and_hasher( + self + ) -> (#output_ty_hasher, ProfilingStats) { + let __crepe_total_start = ::std::time::Instant::now(); + let mut __crepe_profiling_stats = ProfilingStats::default(); + + #initialize + #main_loops + #finalize_profiling + + __crepe_profiling_stats.total_duration = __crepe_total_start.elapsed(); + (#output, __crepe_profiling_stats) + } + + fn run_with_profiling(self) -> (#output_ty_default, ProfilingStats) { + self.run_with_profiling_and_hasher::<::std::collections::hash_map::RandomState>() + } } } @@ -852,8 +952,9 @@ fn make_stratum( let rules: proc_macro2::TokenStream = context .rules .iter() - .filter(|rule| stratum.contains(&rule.goal.relation)) - .map(|rule| make_rule(rule, &stratum, indices)) + .enumerate() + .filter(|(_, rule)| stratum.contains(&rule.goal.relation)) + .map(|(rule_idx, rule)| make_rule(rule, rule_idx, &stratum, indices)) .collect(); let set_update_to_new: proc_macro2::TokenStream = current_rels @@ -935,9 +1036,15 @@ fn make_updates( fn make_rule( rule: &Rule, + rule_idx: usize, stratum: &HashSet<&Ident>, indices: &mut HashSet, ) -> proc_macro2::TokenStream { + let goal_relation_name = rule.goal.relation.to_string(); + let rule_id = format!("{}#{}", goal_relation_name, rule_idx); + let rule_id_ident = format_ident!("__crepe_rule_{}_{}", to_lowercase(&rule.goal.relation), rule_idx); + let stats_var = format_ident!("__crepe_stats_{}_{}", to_lowercase(&rule.goal.relation), rule_idx); + let goal = { let relation = &rule.goal.relation; let fields = &rule.goal.fields; @@ -947,6 +1054,7 @@ fn make_rule( let __crepe_goal = #relation(#fields); if !#name.contains(&__crepe_goal) { #name_new.insert(__crepe_goal); + #stats_var.2 += 1; } } }; @@ -965,7 +1073,8 @@ fn make_rule( _ => None, }) .collect(); - if fact_positions.is_empty() { + + let rule_body = if fact_positions.is_empty() { // Will not change, so we only need to evaluate it once let mut datalog_vars: HashSet = HashSet::new(); #[allow(clippy::needless_collect)] @@ -1000,6 +1109,20 @@ fn make_rule( variants.push(eval_loop); } variants.into_iter().collect() + }; + + quote! { + { + let #stats_var: &mut (::std::time::Duration, u64, u64) = + __crepe_rule_stats.entry(#rule_id.to_string()) + .or_insert((::std::time::Duration::ZERO, 0, 0)); + let #rule_id_ident = ::std::time::Instant::now(); + #stats_var.1 += 1; + + #rule_body + + #stats_var.0 += #rule_id_ident.elapsed(); + } } } From a9aa60f8d464740bc98ae0c2e3d4a6b3c7cd5a5e Mon Sep 17 00:00:00 2001 From: chriseth Date: Mon, 1 Dec 2025 23:14:00 +0100 Subject: [PATCH 10/30] Auto-print stats. --- src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib.rs b/src/lib.rs index ce6d2ae..5e27e9e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -900,6 +900,7 @@ fn make_run(context: &Context) -> proc_macro2::TokenStream { #finalize_profiling __crepe_profiling_stats.total_duration = __crepe_total_start.elapsed(); + __crepe_profiling_stats.report(); (#output, __crepe_profiling_stats) } From c63cb7429708dcad53de1cf0074bd8a335b134e0 Mon Sep 17 00:00:00 2001 From: chriseth Date: Mon, 1 Dec 2025 23:36:23 +0100 Subject: [PATCH 11/30] Adjust counter. --- src/lib.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 5e27e9e..e751765 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -627,9 +627,9 @@ fn make_runtime_decl(context: &Context) -> proc_macro2::TokenStream { let mut sorted_rules = self.rules.clone(); sorted_rules.sort_by(|a, b| b.total_duration.cmp(&a.total_duration)); - println!("{:<40} {:>12} {:>12} {:>15} {:>15}", + println!("{:<50} {:>12} {:>12} {:>15} {:>15}", "Rule", "Total (ms)", "Calls", "Avg (μs)", "Facts Gen."); - println!("{}", "-".repeat(100)); + println!("{}", "-".repeat(110)); for rule in &sorted_rules { let total_ms = rule.total_duration.as_secs_f64() * 1000.0; @@ -640,7 +640,7 @@ fn make_runtime_decl(context: &Context) -> proc_macro2::TokenStream { 0.0 }; - println!("{:<40} {:>10.2}ms {:>12} {:>13.2}μs {:>15}", + println!("{:<50} {:>10.2}ms {:>12} {:>13.2}μs {:>15}", rule.rule_id, total_ms, rule.eval_count, avg_us, rule.facts_generated); } println!(); @@ -1052,6 +1052,7 @@ fn make_rule( let name = format_ident!("__{}", to_lowercase(relation)); let name_new = format_ident!("__{}_new", to_lowercase(relation)); quote! { + #stats_var.1 += 1; let __crepe_goal = #relation(#fields); if !#name.contains(&__crepe_goal) { #name_new.insert(__crepe_goal); @@ -1118,7 +1119,6 @@ fn make_rule( __crepe_rule_stats.entry(#rule_id.to_string()) .or_insert((::std::time::Duration::ZERO, 0, 0)); let #rule_id_ident = ::std::time::Instant::now(); - #stats_var.1 += 1; #rule_body From 2a3508ad00c8a5c890629d920f7fbc3c31b5bd28 Mon Sep 17 00:00:00 2001 From: chriseth Date: Mon, 1 Dec 2025 23:45:05 +0100 Subject: [PATCH 12/30] fix --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index e751765..9dbf2a1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1052,7 +1052,6 @@ fn make_rule( let name = format_ident!("__{}", to_lowercase(relation)); let name_new = format_ident!("__{}_new", to_lowercase(relation)); quote! { - #stats_var.1 += 1; let __crepe_goal = #relation(#fields); if !#name.contains(&__crepe_goal) { #name_new.insert(__crepe_goal); @@ -1118,6 +1117,7 @@ fn make_rule( let #stats_var: &mut (::std::time::Duration, u64, u64) = __crepe_rule_stats.entry(#rule_id.to_string()) .or_insert((::std::time::Duration::ZERO, 0, 0)); + #stats_var.1 += 1; let #rule_id_ident = ::std::time::Instant::now(); #rule_body From ae33066e2fe03944dda32c67702ccfbcf958a4f7 Mon Sep 17 00:00:00 2001 From: chriseth Date: Tue, 2 Dec 2025 12:32:03 +0100 Subject: [PATCH 13/30] Output line number. --- src/lib.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 9dbf2a1..0af0fdd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1042,7 +1042,9 @@ fn make_rule( indices: &mut HashSet, ) -> proc_macro2::TokenStream { let goal_relation_name = rule.goal.relation.to_string(); - let rule_id = format!("{}#{}", goal_relation_name, rule_idx); + let span = rule.goal.relation.span(); + let line_info = span.unwrap().start(); + let rule_id = format!("{}#{}:L{}", goal_relation_name, rule_idx, line_info.line()); let rule_id_ident = format_ident!("__crepe_rule_{}_{}", to_lowercase(&rule.goal.relation), rule_idx); let stats_var = format_ident!("__crepe_stats_{}_{}", to_lowercase(&rule.goal.relation), rule_idx); From a9419a185d29e9df5058ac84e52b27062fa302b7 Mon Sep 17 00:00:00 2001 From: chriseth Date: Tue, 2 Dec 2025 13:12:41 +0100 Subject: [PATCH 14/30] Do not print report by default. --- src/lib.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 0af0fdd..566b33f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -900,7 +900,6 @@ fn make_run(context: &Context) -> proc_macro2::TokenStream { #finalize_profiling __crepe_profiling_stats.total_duration = __crepe_total_start.elapsed(); - __crepe_profiling_stats.report(); (#output, __crepe_profiling_stats) } From 614939a115eb74e368bde35a21b476b9e8403a98 Mon Sep 17 00:00:00 2001 From: chriseth Date: Tue, 2 Dec 2025 20:35:24 +0100 Subject: [PATCH 15/30] Make Crepe public. --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 566b33f..17e100b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -580,7 +580,7 @@ fn make_runtime_decl(context: &Context) -> proc_macro2::TokenStream { quote! { #[derive(::core::default::Default)] - struct Crepe #generics_decl { + pub struct Crepe #generics_decl { #fields } From 85bd74b31c0df63361453ba34702c3f099bc5bc2 Mon Sep 17 00:00:00 2001 From: chriseth Date: Tue, 2 Dec 2025 20:37:16 +0100 Subject: [PATCH 16/30] Some more public functions. --- src/lib.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 17e100b..51b5f0b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -658,7 +658,7 @@ fn make_runtime_impl(context: &Context) -> proc_macro2::TokenStream { quote! { impl #generics_decl Crepe #generics_args { - fn new() -> Self { + pub fn new() -> Self { ::core::default::Default::default() } #run @@ -876,7 +876,7 @@ fn make_run(context: &Context) -> proc_macro2::TokenStream { let output_ty_default = make_output_ty(context, quote! {}); quote! { #[allow(clippy::collapsible_if)] - fn run_with_hasher( + pub fn run_with_hasher( self ) -> #output_ty_hasher { #initialize @@ -884,12 +884,12 @@ fn make_run(context: &Context) -> proc_macro2::TokenStream { #output } - fn run(self) -> #output_ty_default { + pub fn run(self) -> #output_ty_default { self.run_with_hasher::<::std::collections::hash_map::RandomState>() } #[allow(clippy::collapsible_if)] - fn run_with_profiling_and_hasher( + pub fn run_with_profiling_and_hasher( self ) -> (#output_ty_hasher, ProfilingStats) { let __crepe_total_start = ::std::time::Instant::now(); @@ -903,7 +903,7 @@ fn make_run(context: &Context) -> proc_macro2::TokenStream { (#output, __crepe_profiling_stats) } - fn run_with_profiling(self) -> (#output_ty_default, ProfilingStats) { + pub fn run_with_profiling(self) -> (#output_ty_default, ProfilingStats) { self.run_with_profiling_and_hasher::<::std::collections::hash_map::RandomState>() } } From 1fe4c6a7fd119f75560833b3e6511ddd8c1bd793 Mon Sep 17 00:00:00 2001 From: chriseth Date: Mon, 8 Dec 2025 12:29:13 +0000 Subject: [PATCH 17/30] Remove redundant tests. --- tests/test_ancestors.rs | 70 -------------------- tests/test_arithmetic.rs | 97 ---------------------------- tests/test_course_enrollment.rs | 96 ---------------------------- tests/test_graph_connectivity.rs | 78 ----------------------- tests/test_strings.rs | 89 -------------------------- tests/test_type_system.rs | 106 ------------------------------- 6 files changed, 536 deletions(-) delete mode 100644 tests/test_ancestors.rs delete mode 100644 tests/test_arithmetic.rs delete mode 100644 tests/test_course_enrollment.rs delete mode 100644 tests/test_graph_connectivity.rs delete mode 100644 tests/test_strings.rs delete mode 100644 tests/test_type_system.rs diff --git a/tests/test_ancestors.rs b/tests/test_ancestors.rs deleted file mode 100644 index 4ab6116..0000000 --- a/tests/test_ancestors.rs +++ /dev/null @@ -1,70 +0,0 @@ -// This test checks ancestor relationships using recursive rules. -// It tests multiple recursive paths and demonstrates family tree queries. - -use crepe::crepe; - -crepe! { - @input - struct Parent(&'static str, &'static str); - - @output - struct Ancestor(&'static str, &'static str); - - @output - struct Sibling(&'static str, &'static str); - - // Base case: parents are ancestors - Ancestor(x, y) <- Parent(x, y); - - // Recursive case: if x is parent of z and z is ancestor of y, then x is ancestor of y - Ancestor(x, y) <- Parent(x, z), Ancestor(z, y); - - // Siblings share a parent - Sibling(a, b) <- Parent(p, a), Parent(p, b), (a != b); -} - -#[test] -fn test_ancestors() { - let mut runtime = Crepe::new(); - runtime.extend([ - Parent("alice", "bob"), - Parent("alice", "charlie"), - Parent("bob", "dave"), - Parent("bob", "eve"), - Parent("charlie", "frank"), - Parent("dave", "grace"), - ]); - - let (ancestors, siblings) = runtime.run(); - - let mut ancestor_vec: Vec<_> = ancestors - .into_iter() - .map(|Ancestor(a, b)| (a, b)) - .collect(); - ancestor_vec.sort_unstable(); - - // Check that all ancestor relationships are correct - assert!(ancestor_vec.contains(&("alice", "bob"))); - assert!(ancestor_vec.contains(&("alice", "charlie"))); - assert!(ancestor_vec.contains(&("alice", "dave"))); - assert!(ancestor_vec.contains(&("alice", "eve"))); - assert!(ancestor_vec.contains(&("alice", "frank"))); - assert!(ancestor_vec.contains(&("alice", "grace"))); - assert!(ancestor_vec.contains(&("bob", "dave"))); - assert!(ancestor_vec.contains(&("bob", "eve"))); - assert!(ancestor_vec.contains(&("bob", "grace"))); - assert!(ancestor_vec.contains(&("charlie", "frank"))); - assert!(ancestor_vec.contains(&("dave", "grace"))); - - let mut sibling_vec: Vec<_> = siblings - .into_iter() - .map(|Sibling(a, b)| (a, b)) - .collect(); - sibling_vec.sort_unstable(); - - // Check siblings (note: both directions) - assert!(sibling_vec.contains(&("bob", "charlie"))); - assert!(sibling_vec.contains(&("charlie", "bob"))); - assert!(sibling_vec.contains(&("dave", "eve"))); - assert!(sibling_vec.contains(&("eve", "dave"))); -} diff --git a/tests/test_arithmetic.rs b/tests/test_arithmetic.rs deleted file mode 100644 index c83f844..0000000 --- a/tests/test_arithmetic.rs +++ /dev/null @@ -1,97 +0,0 @@ -// This test checks mathematical operations and numeric comparisons. -// It tests arithmetic expressions, multiple conditions, and range queries. - -use crepe::crepe; - -crepe! { - @input - struct Number(i32); - - @output - struct Square(i32, i32); - - @output - struct Even(i32); - - @output - struct PerfectSquare(i32); - - @output - struct InRange(i32); - - // Calculate squares - Square(x, y) <- Number(x), let y = x * x; - - // Find even numbers - Even(x) <- Number(x), (x % 2 == 0); - - // Find perfect squares within the input numbers - PerfectSquare(x) <- Number(x), Number(y), (y * y == x); - - // Numbers in a specific range [10, 30] - InRange(x) <- Number(x), (x >= 10), (x <= 30); -} - -#[test] -fn test_arithmetic() { - let mut runtime = Crepe::new(); - runtime.extend([ - Number(1), - Number(2), - Number(3), - Number(4), - Number(5), - Number(9), - Number(16), - Number(20), - Number(25), - Number(30), - Number(35), - ]); - - let (squares, evens, perfect_squares, in_range) = runtime.run(); - - let mut square_vec: Vec<_> = squares - .into_iter() - .map(|Square(x, y)| (x, y)) - .collect(); - square_vec.sort_unstable(); - - assert_eq!(square_vec, vec![ - (1, 1), - (2, 4), - (3, 9), - (4, 16), - (5, 25), - (9, 81), - (16, 256), - (20, 400), - (25, 625), - (30, 900), - (35, 1225), - ]); - - let mut even_vec: Vec<_> = evens - .into_iter() - .map(|Even(x)| x) - .collect(); - even_vec.sort_unstable(); - - assert_eq!(even_vec, vec![2, 4, 16, 20, 30]); - - let mut perfect_vec: Vec<_> = perfect_squares - .into_iter() - .map(|PerfectSquare(x)| x) - .collect(); - perfect_vec.sort_unstable(); - - assert_eq!(perfect_vec, vec![1, 4, 9, 16, 25]); - - let mut range_vec: Vec<_> = in_range - .into_iter() - .map(|InRange(x)| x) - .collect(); - range_vec.sort_unstable(); - - assert_eq!(range_vec, vec![16, 20, 25, 30]); -} diff --git a/tests/test_course_enrollment.rs b/tests/test_course_enrollment.rs deleted file mode 100644 index d84da9a..0000000 --- a/tests/test_course_enrollment.rs +++ /dev/null @@ -1,96 +0,0 @@ -// This test checks stratified negation with multiple strata. -// It demonstrates complex negation patterns and set difference operations. - -use crepe::crepe; - -crepe! { - @input - struct Student(&'static str); - - @input - struct EnrolledIn(&'static str, &'static str); - - @input - struct Prerequisite(&'static str, &'static str); - - @output - struct CanEnroll(&'static str, &'static str); - - @output - struct MissingPrereq(&'static str, &'static str); - - struct HasPrerequisite(&'static str, &'static str); - - // Student has taken the prerequisite if enrolled in it - HasPrerequisite(student, course) <- - Prerequisite(course, prereq), - EnrolledIn(student, prereq); - - // Student can enroll if not already enrolled - // and either no prerequisite exists or they have it - CanEnroll(student, course) <- - Student(student), - Prerequisite(course, _), - !EnrolledIn(student, course), - HasPrerequisite(student, course); - - // Can also enroll if no prerequisites exist - CanEnroll(student, course) <- - Student(student), - Prerequisite(course, _), - !EnrolledIn(student, course), - !Prerequisite(course, _); - - // Missing prerequisite - MissingPrereq(student, course) <- - Student(student), - Prerequisite(course, prereq), - !EnrolledIn(student, prereq); -} - -#[test] -fn test_stratified_negation() { - let mut runtime = Crepe::new(); - runtime.extend([ - Student("alice"), - Student("bob"), - Student("charlie"), - ]); - runtime.extend([ - EnrolledIn("alice", "math101"), - EnrolledIn("alice", "cs101"), - EnrolledIn("bob", "math101"), - EnrolledIn("charlie", "cs101"), - ]); - runtime.extend([ - Prerequisite("cs201", "cs101"), - Prerequisite("math201", "math101"), - Prerequisite("cs301", "cs201"), - ]); - - let (can_enroll, missing) = runtime.run(); - - let can_vec: Vec<_> = can_enroll - .into_iter() - .map(|CanEnroll(s, c)| (s, c)) - .collect(); - - // Alice has both cs101 and math101, can enroll in cs201 and math201 - assert!(can_vec.contains(&("alice", "cs201"))); - assert!(can_vec.contains(&("alice", "math201"))); - - // Bob only has math101 - assert!(can_vec.contains(&("bob", "math201"))); - assert!(!can_vec.contains(&("bob", "cs201"))); - - let missing_vec: Vec<_> = missing - .into_iter() - .map(|MissingPrereq(s, c)| (s, c)) - .collect(); - - // Bob is missing cs101 for cs201 - assert!(missing_vec.contains(&("bob", "cs201"))); - - // Charlie is missing math101 for math201 - assert!(missing_vec.contains(&("charlie", "math201"))); -} diff --git a/tests/test_graph_connectivity.rs b/tests/test_graph_connectivity.rs deleted file mode 100644 index 4f15a7a..0000000 --- a/tests/test_graph_connectivity.rs +++ /dev/null @@ -1,78 +0,0 @@ -// This test checks path finding in a graph with multiple paths. -// It demonstrates connectivity analysis with isolated nodes. - -use crepe::crepe; - -crepe! { - @input - struct Road(&'static str, &'static str); - - @input - struct City(&'static str); - - @output - struct Connected(&'static str, &'static str); - - @output - struct Isolated(&'static str); - - // Bidirectional connectivity - Connected(x, y) <- Road(x, y); - Connected(x, y) <- Road(y, x); - Connected(x, z) <- Connected(x, y), Connected(y, z); - - // A city is isolated if it has no road connections - Isolated(x) <- City(x), !Road(x, _), !Road(_, x); -} - -#[test] -fn test_graph_connectivity() { - let mut runtime = Crepe::new(); - runtime.extend([ - Road("seattle", "portland"), - Road("portland", "san_francisco"), - Road("san_francisco", "los_angeles"), - Road("denver", "salt_lake"), - Road("salt_lake", "las_vegas"), - ]); - runtime.extend([ - City("seattle"), - City("portland"), - City("san_francisco"), - City("los_angeles"), - City("denver"), - City("salt_lake"), - City("las_vegas"), - City("miami"), // isolated city with no roads - ]); - - let (connected, isolated) = runtime.run(); - - let mut conn_vec: Vec<_> = connected - .into_iter() - .map(|Connected(a, b)| (a, b)) - .collect(); - conn_vec.sort_unstable(); - - // Check west coast connectivity - assert!(conn_vec.contains(&("seattle", "portland"))); - assert!(conn_vec.contains(&("seattle", "san_francisco"))); - assert!(conn_vec.contains(&("seattle", "los_angeles"))); - assert!(conn_vec.contains(&("portland", "los_angeles"))); - - // Check mountain region connectivity - assert!(conn_vec.contains(&("denver", "salt_lake"))); - assert!(conn_vec.contains(&("denver", "las_vegas"))); - - // Verify no cross-region connections - assert!(!conn_vec.contains(&("seattle", "denver"))); - assert!(!conn_vec.contains(&("portland", "salt_lake"))); - - let iso_vec: Vec<_> = isolated - .into_iter() - .map(|Isolated(x)| x) - .collect(); - - // Miami is isolated - it's a city but has no road connections - assert_eq!(iso_vec, vec!["miami"]); -} diff --git a/tests/test_strings.rs b/tests/test_strings.rs deleted file mode 100644 index 3b6ea57..0000000 --- a/tests/test_strings.rs +++ /dev/null @@ -1,89 +0,0 @@ -// This test checks string operations and pattern matching. -// It demonstrates working with string slices and comparisons. - -use crepe::crepe; - -crepe! { - @input - struct Word<'a>(&'a str); - - @output - struct ShortWord<'a>(&'a str); - - @output - struct LongWord<'a>(&'a str); - - @output - struct StartsWith<'a>(&'a str, char); - - @output - struct Palindrome<'a>(&'a str); - - // Words with length <= 4 - ShortWord(w) <- Word(w), (w.len() <= 4); - - // Words with length > 8 - LongWord(w) <- Word(w), (w.len() > 8); - - // Track first character - StartsWith(w, c) <- Word(w), let c = w.chars().next().unwrap(); - - // Check for palindromes - Palindrome(w) <- - Word(w), - let reversed = w.chars().rev().collect::(), - (w == reversed.as_str()); -} - -#[test] -fn test_string_operations() { - let mut runtime = Crepe::new(); - runtime.extend([ - Word("hello"), - Word("world"), - Word("rust"), - Word("programming"), - Word("code"), - Word("data"), - Word("a"), - Word("racecar"), - Word("level"), - Word("test"), - ]); - - let (short, long, starts, palindromes) = runtime.run(); - - let mut short_vec: Vec<_> = short - .into_iter() - .map(|ShortWord(w)| w) - .collect(); - short_vec.sort_unstable(); - - assert_eq!(short_vec, vec!["a", "code", "data", "rust", "test"]); - - let mut long_vec: Vec<_> = long - .into_iter() - .map(|LongWord(w)| w) - .collect(); - long_vec.sort_unstable(); - - assert_eq!(long_vec, vec!["programming"]); - - let starts_vec: Vec<_> = starts - .into_iter() - .map(|StartsWith(w, c)| (w, c)) - .collect(); - - assert!(starts_vec.contains(&("hello", 'h'))); - assert!(starts_vec.contains(&("world", 'w'))); - assert!(starts_vec.contains(&("rust", 'r'))); - assert!(starts_vec.contains(&("programming", 'p'))); - - let mut pal_vec: Vec<_> = palindromes - .into_iter() - .map(|Palindrome(w)| w) - .collect(); - pal_vec.sort_unstable(); - - assert_eq!(pal_vec, vec!["a", "level", "racecar"]); -} diff --git a/tests/test_type_system.rs b/tests/test_type_system.rs deleted file mode 100644 index 16bbf8f..0000000 --- a/tests/test_type_system.rs +++ /dev/null @@ -1,106 +0,0 @@ -// This test checks multiple output relations with complex joins. -// It demonstrates a simple type system with subtyping relationships. - -use crepe::crepe; - -#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq, PartialOrd, Ord)] -enum Type { - Int, - Float, - String, - Bool, - Any, -} - -crepe! { - @input - struct Subtype(Type, Type); - - @output - struct IsSubtype(Type, Type); - - @output - struct Compatible(Type, Type); - - @output - struct NotRelated(Type, Type); - - struct AllTypes(Type); - - // Reflexive: every type is a subtype of itself - IsSubtype(t, t) <- Subtype(t, _); - IsSubtype(t, t) <- Subtype(_, t); - - // Direct subtype relationships - IsSubtype(t1, t2) <- Subtype(t1, t2); - - // Transitive: if A <: B and B <: C, then A <: C - IsSubtype(t1, t3) <- IsSubtype(t1, t2), IsSubtype(t2, t3); - - // Types are compatible if one is a subtype of the other - Compatible(t1, t2) <- IsSubtype(t1, t2); - Compatible(t1, t2) <- IsSubtype(t2, t1); - - // Collect all types - AllTypes(t) <- Subtype(t, _); - AllTypes(t) <- Subtype(_, t); - - // Types are not related if they're not compatible - NotRelated(t1, t2) <- AllTypes(t1), AllTypes(t2), !Compatible(t1, t2); -} - -#[test] -fn test_type_system() { - let mut runtime = Crepe::new(); - runtime.extend([ - Subtype(Type::Int, Type::Float), - Subtype(Type::Int, Type::Any), - Subtype(Type::Float, Type::Any), - Subtype(Type::String, Type::Any), - Subtype(Type::Bool, Type::Any), - ]); - - let (is_subtype, compatible, not_related) = runtime.run(); - - let sub_vec: Vec<_> = is_subtype - .into_iter() - .map(|IsSubtype(t1, t2)| (t1, t2)) - .collect(); - - // Check reflexivity - assert!(sub_vec.contains(&(Type::Int, Type::Int))); - assert!(sub_vec.contains(&(Type::Float, Type::Float))); - - // Check direct relationships - assert!(sub_vec.contains(&(Type::Int, Type::Float))); - assert!(sub_vec.contains(&(Type::Int, Type::Any))); - - // Check transitivity: Int <: Float <: Any implies Int <: Any (already added directly) - assert!(sub_vec.contains(&(Type::Int, Type::Any))); - - let comp_vec: Vec<_> = compatible - .into_iter() - .map(|Compatible(t1, t2)| (t1, t2)) - .collect(); - - // Int and Float are compatible - assert!(comp_vec.contains(&(Type::Int, Type::Float)) || - comp_vec.contains(&(Type::Float, Type::Int))); - - // Everything is compatible with Any - assert!(comp_vec.contains(&(Type::String, Type::Any)) || - comp_vec.contains(&(Type::Any, Type::String))); - - let not_rel_vec: Vec<_> = not_related - .into_iter() - .map(|NotRelated(t1, t2)| (t1, t2)) - .collect(); - - // String and Int are not related - assert!(not_rel_vec.contains(&(Type::String, Type::Int)) || - not_rel_vec.contains(&(Type::Int, Type::String))); - - // Bool and Float are not related - assert!(not_rel_vec.contains(&(Type::Bool, Type::Float)) || - not_rel_vec.contains(&(Type::Float, Type::Bool))); -} From 516efba46e7cbfc9869f1ad306a3244d35f723ed Mon Sep 17 00:00:00 2001 From: chriseth Date: Tue, 9 Dec 2025 12:51:49 +0000 Subject: [PATCH 18/30] Update after review. --- src/lib.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 49725fb..be5e741 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -547,7 +547,6 @@ fn make_struct_decls(context: &Context) -> proc_macro2::TokenStream { let generics = &relation.generics; let semi_token = &relation.semi_token; let fields = &relation.fields; - quote_spanned! {name.span()=> #[derive( ::core::marker::Copy, @@ -1249,10 +1248,8 @@ fn to_lowercase(name: &Ident) -> Ident { } /// Validate generic parameters on a relation -/// Checks for unsupported features and provides helpful error messages fn validate_generic_params(relation: &Relation) { - // Type parameters are now supported! - // Const parameters are not yet supported + // Type parameters are supported, const not yet. if let Some(c) = relation.generics.const_params().next() { abort!(c.span(), "Const parameters are not yet supported in relations"); } From bad66edaa9581fa5f1e1878f02e426bd79bd7ad8 Mon Sep 17 00:00:00 2001 From: chriseth Date: Wed, 10 Dec 2025 11:00:16 +0000 Subject: [PATCH 19/30] Fix fmt. --- src/lib.rs | 57 +++++++----- tests/test_basic_generic.rs | 4 +- tests/test_complex_multiple_bounds.rs | 18 ++-- tests/test_complex_multiple_generics.rs | 28 +++--- tests/test_complex_trait_methods.rs | 58 ++++++------ tests/test_custom_trait_bounds.rs | 52 ++++------- tests/test_generic_transitive_closure.rs | 23 ++--- tests/test_graph_with_node_trait.rs | 18 ++-- tests/test_multiple_generics.rs | 42 +++------ tests/test_multiple_generics_with_bounds.rs | 18 ++-- tests/test_multiple_trait_bounds.rs | 19 ++-- tests/test_multiple_trait_methods.rs | 61 +++++++------ tests/test_no_duplicate_bounds.rs | 15 +--- tests/test_showcase_all_features.rs | 98 ++++++++++++--------- tests/test_trait_method_call.rs | 10 +-- 15 files changed, 252 insertions(+), 269 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index be5e741..2cb6760 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -318,7 +318,7 @@ impl Context { // Validate generic parameters validate_generic_params(&relation); - + let num_lifetimes = relation.generics.lifetimes().count(); match relation.relation_type() { @@ -612,7 +612,7 @@ fn make_extend(context: &Context) -> proc_macro2::TokenStream { let generics_decl = generic_params_decl(context); let generics_args = generic_params_args(context); let lower = to_lowercase(&relation.name); - + // For the reference impl, we need to add the lifetime to the existing generics let ref_impl_generics = { let mut items = vec![quote! { 'a }]; @@ -621,7 +621,7 @@ fn make_extend(context: &Context) -> proc_macro2::TokenStream { } format_generics(items) }; - + quote! { impl #generics_decl ::core::iter::Extend<#rel_ty> for Crepe #generics_args { fn extend<__I>(&mut self, iter: __I) @@ -1251,18 +1251,21 @@ fn to_lowercase(name: &Ident) -> Ident { fn validate_generic_params(relation: &Relation) { // Type parameters are supported, const not yet. if let Some(c) = relation.generics.const_params().next() { - abort!(c.span(), "Const parameters are not yet supported in relations"); + abort!( + c.span(), + "Const parameters are not yet supported in relations" + ); } - + // Where clauses are not yet supported if let Some(where_clause) = &relation.generics.where_clause { abort!( - where_clause.where_token.span(), + where_clause.where_token.span(), "Where clauses are not yet supported in relations. \ Please specify trait bounds directly on the type parameter instead, e.g., `T: Trait`" ); } - + // Check for default type parameters (not supported) for type_param in relation.generics.type_params() { if type_param.default.is_some() { @@ -1274,7 +1277,7 @@ fn validate_generic_params(relation: &Relation) { ); } } - + // Check for lifetime bounds (not supported) for lifetime_param in relation.generics.lifetimes() { if !lifetime_param.bounds.is_empty() { @@ -1292,7 +1295,7 @@ fn validate_generic_params(relation: &Relation) { fn collect_generic_params(context: &Context) -> Vec<&syn::TypeParam> { let mut seen = HashSet::new(); let mut params = Vec::new(); - + for relation in context.rels_input.values() { for param in relation.generics.type_params() { if seen.insert(param.ident.to_string()) { @@ -1300,17 +1303,18 @@ fn collect_generic_params(context: &Context) -> Vec<&syn::TypeParam> { } } } - + params } /// Check if a type parameter has a specific trait bound fn has_bound(tp: &syn::TypeParam, bound_name: &str) -> bool { tp.bounds.iter().any(|b| match b { - syn::TypeParamBound::Trait(trait_bound) => { - trait_bound.path.segments.last() - .map_or(false, |seg| seg.ident == bound_name) - } + syn::TypeParamBound::Trait(trait_bound) => trait_bound + .path + .segments + .last() + .is_some_and(|seg| seg.ident == bound_name), _ => false, }) } @@ -1334,16 +1338,17 @@ fn required_bound_token(name: &str) -> proc_macro2::TokenStream { fn merge_bounds_with_required(tp: &syn::TypeParam) -> proc_macro2::TokenStream { let ident = &tp.ident; let user_bounds = &tp.bounds; - + // Collect missing required bounds - let missing_bounds: Vec<_> = REQUIRED_BOUNDS.iter() + let missing_bounds: Vec<_> = REQUIRED_BOUNDS + .iter() .filter(|&req| !has_bound(tp, req)) .map(|req| required_bound_token(req)) .collect(); - + // Combine user bounds + missing required bounds match (user_bounds.is_empty(), missing_bounds.is_empty()) { - (true, true) => quote! { #ident }, // No bounds at all (shouldn't happen) + (true, true) => quote! { #ident }, // No bounds at all (shouldn't happen) (true, false) => quote! { #ident: #(#missing_bounds)+* }, (false, true) => quote! { #ident: #user_bounds }, (false, false) => quote! { #ident: #user_bounds + #(#missing_bounds)+* }, @@ -1353,7 +1358,11 @@ fn merge_bounds_with_required(tp: &syn::TypeParam) -> proc_macro2::TokenStream { /// Helper to format generic parameters with angle brackets /// Returns `` or empty if the list is empty fn format_generics(items: Vec) -> proc_macro2::TokenStream { - if items.is_empty() { quote! {} } else { quote! { <#(#items),*> } } + if items.is_empty() { + quote! {} + } else { + quote! { <#(#items),*> } + } } /// Create a tokenstream for generic parameters (lifetimes + type params) @@ -1362,7 +1371,11 @@ fn generic_params_decl(context: &Context) -> proc_macro2::TokenStream { if context.has_input_lifetime { items.push(quote! { 'a }); } - items.extend(collect_generic_params(context).into_iter().map(merge_bounds_with_required)); + items.extend( + collect_generic_params(context) + .into_iter() + .map(merge_bounds_with_required), + ); format_generics(items) } @@ -1395,7 +1408,7 @@ fn relation_type(rel: &Relation, usage: LifetimeUsage) -> proc_macro2::TokenStre }; let name = &rel.name; - + // Build list of generic arguments let mut items = Vec::new(); items.extend(rel.generics.lifetimes().map(|l| { @@ -1406,7 +1419,7 @@ fn relation_type(rel: &Relation, usage: LifetimeUsage) -> proc_macro2::TokenStre let ident = &tp.ident; quote! { #ident } })); - + // Format with angle brackets if there are any generics if items.is_empty() { quote! { #name } diff --git a/tests/test_basic_generic.rs b/tests/test_basic_generic.rs index b068bf1..8872a27 100644 --- a/tests/test_basic_generic.rs +++ b/tests/test_basic_generic.rs @@ -16,10 +16,10 @@ crepe! { fn test_basic_generic() { let mut runtime = Crepe::new(); runtime.extend([Input(1), Input(2), Input(3)]); - + let (output,) = runtime.run(); let mut results: Vec<_> = output.into_iter().map(|Output(x)| x).collect(); results.sort_unstable(); - + assert_eq!(results, vec![1, 2, 3]); } diff --git a/tests/test_complex_multiple_bounds.rs b/tests/test_complex_multiple_bounds.rs index 50e44cf..a434c0d 100644 --- a/tests/test_complex_multiple_bounds.rs +++ b/tests/test_complex_multiple_bounds.rs @@ -18,7 +18,9 @@ struct MyKey(u32); struct MyValue(&'static str); impl KeyTrait for MyKey { - fn key_value(&self) -> u32 { self.0 } + fn key_value(&self) -> u32 { + self.0 + } } impl Debug for MyKey { @@ -34,7 +36,9 @@ impl Display for MyKey { } impl ValueTrait for MyValue { - fn value_text(&self) -> &'static str { self.0 } + fn value_text(&self) -> &'static str { + self.0 + } } impl Debug for MyValue { @@ -67,17 +71,13 @@ fn test_complex_multiple_bounds() { Mapping(MyKey(2), MyValue("two")), Mapping(MyKey(3), MyValue("three")), ]); - + let (reversed,) = runtime.run(); let mut results: Vec<_> = reversed .into_iter() .map(|Reversed(v, k)| (v.value_text(), k.key_value())) .collect(); results.sort_unstable(); - - assert_eq!(results, vec![ - ("one", 1), - ("three", 3), - ("two", 2), - ]); + + assert_eq!(results, vec![("one", 1), ("three", 3), ("two", 2),]); } diff --git a/tests/test_complex_multiple_generics.rs b/tests/test_complex_multiple_generics.rs index 1381d10..d3c78ea 100644 --- a/tests/test_complex_multiple_generics.rs +++ b/tests/test_complex_multiple_generics.rs @@ -13,26 +13,28 @@ struct Node(u32); struct Tag(&'static str); impl Label for Tag { - fn label(&self) -> &'static str { self.0 } + fn label(&self) -> &'static str { + self.0 + } } crepe! { @input struct Edge(T, T); - + @input struct Tagged(T, L); - + @output struct Reachable(T, T); - + @output struct LabeledPath(T, T, L); - + // Transitive closure Reachable(x, y) <- Edge(x, y); Reachable(x, z) <- Edge(x, y), Reachable(y, z); - + // Labeled paths LabeledPath(x, y, l) <- Edge(x, y), Tagged(x, l); } @@ -40,41 +42,41 @@ crepe! { #[test] fn test_complex_multiple_generics() { let mut runtime = Crepe::new(); - + // Add edges runtime.extend([ Edge(Node(1), Node(2)), Edge(Node(2), Node(3)), Edge(Node(3), Node(4)), ]); - + // Add tags runtime.extend([ Tagged(Node(1), Tag("start")), Tagged(Node(2), Tag("middle")), ]); - + let (reachable, labeled) = runtime.run(); - + // Check reachability let reach_vec: Vec<_> = reachable .into_iter() .map(|Reachable(x, y)| (x.0, y.0)) .collect(); - + assert!(reach_vec.contains(&(1, 2))); assert!(reach_vec.contains(&(1, 3))); assert!(reach_vec.contains(&(1, 4))); assert!(reach_vec.contains(&(2, 3))); assert!(reach_vec.contains(&(2, 4))); assert!(reach_vec.contains(&(3, 4))); - + // Check labeled paths let labeled_vec: Vec<_> = labeled .into_iter() .map(|LabeledPath(x, y, l)| (x.0, y.0, l.label())) .collect(); - + assert!(labeled_vec.contains(&(1, 2, "start"))); assert!(labeled_vec.contains(&(2, 3, "middle"))); } diff --git a/tests/test_complex_trait_methods.rs b/tests/test_complex_trait_methods.rs index 2b52c84..8ad901a 100644 --- a/tests/test_complex_trait_methods.rs +++ b/tests/test_complex_trait_methods.rs @@ -17,7 +17,7 @@ impl Node for Task { fn id(&self) -> u32 { self.task_id } - + fn priority(&self) -> u32 { self.prio } @@ -29,68 +29,74 @@ crepe! { @output struct Path(T, T, u32); - + @output struct HighPriorityPath(T, T); // Calculate path with combined priority - Path(x, y, p) <- - Edge(x, y), + Path(x, y, p) <- + Edge(x, y), let p = x.priority() + y.priority(); - + // Transitive paths with priority sum - Path(x, z, p) <- - Edge(x, y), + Path(x, z, p) <- + Edge(x, y), Path(y, z, p2), let p = x.priority() + p2; - + // Filter high priority paths - HighPriorityPath(x, y) <- + HighPriorityPath(x, y) <- Path(x, y, p), (p > 10); } #[test] fn test_complex_trait_methods() { - let t1 = Task { task_id: 1, prio: 3 }; - let t2 = Task { task_id: 2, prio: 5 }; - let t3 = Task { task_id: 3, prio: 7 }; - + let t1 = Task { + task_id: 1, + prio: 3, + }; + let t2 = Task { + task_id: 2, + prio: 5, + }; + let t3 = Task { + task_id: 3, + prio: 7, + }; + let mut runtime = Crepe::new(); - runtime.extend([ - Edge(t1, t2), - Edge(t2, t3), - ]); - + runtime.extend([Edge(t1, t2), Edge(t2, t3)]); + let (paths, high_prio) = runtime.run(); - + // Check path priorities let path_vec: Vec<_> = paths .into_iter() .map(|Path(x, y, p)| (x.id(), y.id(), p)) .collect(); - + // t1 -> t2: priority 3 + 5 = 8 assert!(path_vec.contains(&(1, 2, 8))); - + // t2 -> t3: priority 5 + 7 = 12 assert!(path_vec.contains(&(2, 3, 12))); - + // t1 -> t3: priority 3 + 12 = 15 assert!(path_vec.contains(&(1, 3, 15))); - + // Check high priority paths (> 10) let high_vec: Vec<_> = high_prio .into_iter() .map(|HighPriorityPath(x, y)| (x.id(), y.id())) .collect(); - + // t2 -> t3 has priority 12 > 10 assert!(high_vec.contains(&(2, 3))); - + // t1 -> t3 has priority 15 > 10 assert!(high_vec.contains(&(1, 3))); - + // t1 -> t2 has priority 8 < 10 (should not be in high_prio) assert!(!high_vec.contains(&(1, 2))); } diff --git a/tests/test_custom_trait_bounds.rs b/tests/test_custom_trait_bounds.rs index e1a4077..7ad6a1e 100644 --- a/tests/test_custom_trait_bounds.rs +++ b/tests/test_custom_trait_bounds.rs @@ -8,7 +8,7 @@ trait Valuable { } // Type that implements the custom trait and required traits -#[derive(Hash, Eq, PartialEq, Clone, Copy, Debug)] +#[derive(Hash, Eq, PartialEq, Clone, Copy, Debug, Default)] struct Item(i32); impl Valuable for Item { @@ -17,12 +17,6 @@ impl Valuable for Item { } } -impl Default for Item { - fn default() -> Self { - Item(0) - } -} - crepe! { @input struct Input(T); @@ -36,49 +30,33 @@ crepe! { #[test] fn test_custom_trait_bound() { let mut runtime = Crepe::new(); - runtime.extend([ - Input(Item(1)), - Input(Item(2)), - Input(Item(3)), - ]); - + runtime.extend([Input(Item(1)), Input(Item(2)), Input(Item(3))]); + let (output,) = runtime.run(); - let mut results: Vec<_> = output - .into_iter() - .map(|Output(x)| x.value()) - .collect(); + let mut results: Vec<_> = output.into_iter().map(|Output(x)| x.value()).collect(); results.sort_unstable(); - + assert_eq!(results, vec![1, 2, 3]); } #[test] fn test_custom_trait_with_integers() { // Create a simple wrapper that implements Valuable - #[derive(Hash, Eq, PartialEq, Clone, Copy, Debug)] + #[derive(Hash, Eq, PartialEq, Clone, Copy, Debug, Default)] struct Val(i32); - + impl Valuable for Val { - fn value(&self) -> i32 { self.0 } - } - - impl Default for Val { - fn default() -> Self { Val(0) } + fn value(&self) -> i32 { + self.0 + } } - + let mut runtime = Crepe::new(); - runtime.extend([ - Input(Val(10)), - Input(Val(20)), - Input(Val(30)), - ]); - + runtime.extend([Input(Val(10)), Input(Val(20)), Input(Val(30))]); + let (output,) = runtime.run(); - let results: Vec<_> = output - .into_iter() - .map(|Output(x)| x.value()) - .collect(); - + let results: Vec<_> = output.into_iter().map(|Output(x)| x.value()).collect(); + assert_eq!(results.len(), 3); assert!(results.contains(&10)); assert!(results.contains(&20)); diff --git a/tests/test_generic_transitive_closure.rs b/tests/test_generic_transitive_closure.rs index d163113..76e496e 100644 --- a/tests/test_generic_transitive_closure.rs +++ b/tests/test_generic_transitive_closure.rs @@ -16,20 +16,15 @@ crepe! { #[test] fn test_generic_transitive_closure() { let mut runtime = Crepe::new(); - runtime.extend([ - Edge(1, 2), - Edge(2, 3), - Edge(3, 4), - Edge(2, 5), - ]); - + runtime.extend([Edge(1, 2), Edge(2, 3), Edge(3, 4), Edge(2, 5)]); + let (reachable,) = runtime.run(); let mut results: Vec<_> = reachable .into_iter() .map(|Reachable(x, y)| (x, y)) .collect(); results.sort_unstable(); - + let expected = vec![ (1, 2), (1, 3), @@ -40,26 +35,22 @@ fn test_generic_transitive_closure() { (2, 5), (3, 4), ]; - + assert_eq!(results, expected); } #[test] fn test_generic_with_strings() { let mut runtime = Crepe::new(); - runtime.extend([ - Edge("a", "b"), - Edge("b", "c"), - Edge("c", "d"), - ]); - + runtime.extend([Edge("a", "b"), Edge("b", "c"), Edge("c", "d")]); + let (reachable,) = runtime.run(); let mut results: Vec<_> = reachable .into_iter() .map(|Reachable(x, y)| (x, y)) .collect(); results.sort_unstable(); - + assert!(results.contains(&("a", "b"))); assert!(results.contains(&("a", "c"))); assert!(results.contains(&("a", "d"))); diff --git a/tests/test_graph_with_node_trait.rs b/tests/test_graph_with_node_trait.rs index 9c9e9ff..bded911 100644 --- a/tests/test_graph_with_node_trait.rs +++ b/tests/test_graph_with_node_trait.rs @@ -39,25 +39,31 @@ crepe! { #[test] fn test_graph_with_custom_node_trait() { - let seattle = City { id: 1, name: "Seattle" }; - let portland = City { id: 2, name: "Portland" }; + let seattle = City { + id: 1, + name: "Seattle", + }; + let portland = City { + id: 2, + name: "Portland", + }; let sf = City { id: 3, name: "SF" }; let la = City { id: 4, name: "LA" }; - + let mut runtime = Crepe::new(); runtime.extend([ Connection(seattle, portland), Connection(portland, sf), Connection(sf, la), ]); - + let (connected,) = runtime.run(); let mut results: Vec<_> = connected .into_iter() .map(|Connected(x, y)| (x.id(), y.id())) .collect(); results.sort_unstable(); - + // Check all connections assert!(results.contains(&(1, 2))); // Seattle -> Portland assert!(results.contains(&(1, 3))); // Seattle -> SF @@ -65,6 +71,6 @@ fn test_graph_with_custom_node_trait() { assert!(results.contains(&(2, 3))); // Portland -> SF assert!(results.contains(&(2, 4))); // Portland -> LA assert!(results.contains(&(3, 4))); // SF -> LA - + assert_eq!(results.len(), 6); } diff --git a/tests/test_multiple_generics.rs b/tests/test_multiple_generics.rs index 5f8b852..9b8749b 100644 --- a/tests/test_multiple_generics.rs +++ b/tests/test_multiple_generics.rs @@ -15,45 +15,23 @@ crepe! { #[test] fn test_multiple_generics() { let mut runtime = Crepe::new(); - runtime.extend([ - Pair(1, "one"), - Pair(2, "two"), - Pair(3, "three"), - ]); - + runtime.extend([Pair(1, "one"), Pair(2, "two"), Pair(3, "three")]); + let (swapped,) = runtime.run(); - let mut results: Vec<_> = swapped - .into_iter() - .map(|Swapped(v, k)| (v, k)) - .collect(); + let mut results: Vec<_> = swapped.into_iter().map(|Swapped(v, k)| (v, k)).collect(); results.sort_unstable(); - - assert_eq!(results, vec![ - ("one", 1), - ("three", 3), - ("two", 2), - ]); + + assert_eq!(results, vec![("one", 1), ("three", 3), ("two", 2),]); } #[test] fn test_multiple_generics_same_type() { let mut runtime = Crepe::new(); - runtime.extend([ - Pair(1, 10), - Pair(2, 20), - Pair(3, 30), - ]); - + runtime.extend([Pair(1, 10), Pair(2, 20), Pair(3, 30)]); + let (swapped,) = runtime.run(); - let mut results: Vec<_> = swapped - .into_iter() - .map(|Swapped(v, k)| (v, k)) - .collect(); + let mut results: Vec<_> = swapped.into_iter().map(|Swapped(v, k)| (v, k)).collect(); results.sort_unstable(); - - assert_eq!(results, vec![ - (10, 1), - (20, 2), - (30, 3), - ]); + + assert_eq!(results, vec![(10, 1), (20, 2), (30, 3),]); } diff --git a/tests/test_multiple_generics_with_bounds.rs b/tests/test_multiple_generics_with_bounds.rs index 49ea4ef..0230c06 100644 --- a/tests/test_multiple_generics_with_bounds.rs +++ b/tests/test_multiple_generics_with_bounds.rs @@ -17,11 +17,15 @@ struct MyKey(u32); struct MyValue(u32); impl Key for MyKey { - fn key_id(&self) -> u32 { self.0 } + fn key_id(&self) -> u32 { + self.0 + } } impl Value for MyValue { - fn value_id(&self) -> u32 { self.0 } + fn value_id(&self) -> u32 { + self.0 + } } crepe! { @@ -42,17 +46,13 @@ fn test_multiple_generics_with_trait_bounds() { Entry(MyKey(2), MyValue(20)), Entry(MyKey(3), MyValue(30)), ]); - + let (flipped,) = runtime.run(); let mut results: Vec<_> = flipped .into_iter() .map(|Flipped(v, k)| (v.value_id(), k.key_id())) .collect(); results.sort_unstable(); - - assert_eq!(results, vec![ - (10, 1), - (20, 2), - (30, 3), - ]); + + assert_eq!(results, vec![(10, 1), (20, 2), (30, 3),]); } diff --git a/tests/test_multiple_trait_bounds.rs b/tests/test_multiple_trait_bounds.rs index 593ae2c..21b425a 100644 --- a/tests/test_multiple_trait_bounds.rs +++ b/tests/test_multiple_trait_bounds.rs @@ -11,7 +11,9 @@ trait Custom { struct Item(u32); impl Custom for Item { - fn custom_id(&self) -> u32 { self.0 } + fn custom_id(&self) -> u32 { + self.0 + } } impl Debug for Item { @@ -39,18 +41,11 @@ crepe! { #[test] fn test_multiple_trait_bounds() { let mut runtime = Crepe::new(); - runtime.extend([ - Input(Item(1)), - Input(Item(2)), - Input(Item(3)), - ]); - + runtime.extend([Input(Item(1)), Input(Item(2)), Input(Item(3))]); + let (output,) = runtime.run(); - let mut results: Vec<_> = output - .into_iter() - .map(|Output(x)| x.custom_id()) - .collect(); + let mut results: Vec<_> = output.into_iter().map(|Output(x)| x.custom_id()).collect(); results.sort_unstable(); - + assert_eq!(results, vec![1, 2, 3]); } diff --git a/tests/test_multiple_trait_methods.rs b/tests/test_multiple_trait_methods.rs index dc08867..298aff3 100644 --- a/tests/test_multiple_trait_methods.rs +++ b/tests/test_multiple_trait_methods.rs @@ -22,8 +22,12 @@ struct Package { } impl Measurable for Package { - fn weight(&self) -> u32 { self.w } - fn volume(&self) -> u32 { self.v } + fn weight(&self) -> u32 { + self.w + } + fn volume(&self) -> u32 { + self.v + } } crepe! { @@ -32,19 +36,19 @@ crepe! { @output struct ItemStats(T, u32, u32, u32); - + @output struct Dense(T); // Extract stats using multiple trait methods - ItemStats(x, w, v, d) <- + ItemStats(x, w, v, d) <- Item(x), let w = x.weight(), let v = x.volume(), let d = x.density(); - + // Filter dense items (density > 5) - Dense(x) <- + Dense(x) <- Item(x), let d = x.density(), (d > 5); @@ -52,36 +56,41 @@ crepe! { #[test] fn test_multiple_trait_methods() { - let p1 = Package { id: 1, w: 100, v: 10 }; // density = 10 - let p2 = Package { id: 2, w: 50, v: 20 }; // density = 2 - let p3 = Package { id: 3, w: 200, v: 25 }; // density = 8 - + let p1 = Package { + id: 1, + w: 100, + v: 10, + }; // density = 10 + let p2 = Package { + id: 2, + w: 50, + v: 20, + }; // density = 2 + let p3 = Package { + id: 3, + w: 200, + v: 25, + }; // density = 8 + let mut runtime = Crepe::new(); - runtime.extend([ - Item(p1), - Item(p2), - Item(p3), - ]); - + runtime.extend([Item(p1), Item(p2), Item(p3)]); + let (stats, dense) = runtime.run(); - + // Check stats let stats_vec: Vec<_> = stats .into_iter() .map(|ItemStats(pkg, w, v, d)| (pkg.id, w, v, d)) .collect(); - + assert!(stats_vec.contains(&(1, 100, 10, 10))); assert!(stats_vec.contains(&(2, 50, 20, 2))); assert!(stats_vec.contains(&(3, 200, 25, 8))); - + // Check dense items (density > 5) - let dense_vec: Vec<_> = dense - .into_iter() - .map(|Dense(pkg)| pkg.id) - .collect(); - - assert!(dense_vec.contains(&1)); // density 10 > 5 - assert!(dense_vec.contains(&3)); // density 8 > 5 + let dense_vec: Vec<_> = dense.into_iter().map(|Dense(pkg)| pkg.id).collect(); + + assert!(dense_vec.contains(&1)); // density 10 > 5 + assert!(dense_vec.contains(&3)); // density 8 > 5 assert!(!dense_vec.contains(&2)); // density 2 < 5 } diff --git a/tests/test_no_duplicate_bounds.rs b/tests/test_no_duplicate_bounds.rs index 0eb86ed..50db3a9 100644 --- a/tests/test_no_duplicate_bounds.rs +++ b/tests/test_no_duplicate_bounds.rs @@ -19,17 +19,10 @@ crepe! { #[test] fn test_no_duplicate_bounds() { let mut runtime = Crepe::new(); - runtime.extend([ - Input(Value(1)), - Input(Value(2)), - Input(Value(3)), - ]); - + runtime.extend([Input(Value(1)), Input(Value(2)), Input(Value(3))]); + let (output,) = runtime.run(); - let results: Vec<_> = output - .into_iter() - .map(|Output(x)| x.0) - .collect(); - + let results: Vec<_> = output.into_iter().map(|Output(x)| x.0).collect(); + assert_eq!(results.len(), 3); } diff --git a/tests/test_showcase_all_features.rs b/tests/test_showcase_all_features.rs index 8f0fc6e..ceb416c 100644 --- a/tests/test_showcase_all_features.rs +++ b/tests/test_showcase_all_features.rs @@ -22,12 +22,18 @@ struct City { } impl Node for City { - fn id(&self) -> u32 { self.id } - fn name(&self) -> &'static str { self.city_name } + fn id(&self) -> u32 { + self.id + } + fn name(&self) -> &'static str { + self.city_name + } } impl Weighted for City { - fn weight(&self) -> u32 { self.population / 1000 } + fn weight(&self) -> u32 { + self.population / 1000 + } } impl Debug for City { @@ -46,40 +52,40 @@ crepe! { // Multiple type parameters with multiple bounds each @input struct Connection(N, N, W); - + @input struct Location(N); - + @output struct Route(N, N, u32); - + @output struct WeightedRoute(N, N, u32); - + @output struct HeavyNode(N); - + // Basic rule with trait method - Route(x, y, d) <- + Route(x, y, d) <- Connection(x, y, distance), let d = distance.weight(); - + // Transitive closure with trait methods and arithmetic - Route(x, z, total) <- + Route(x, z, total) <- Connection(x, y, d1), Route(y, z, d2), let w1 = d1.weight(), let total = w1 + d2; - + // Complex expression with multiple trait methods - WeightedRoute(x, y, score) <- + WeightedRoute(x, y, score) <- Route(x, y, d), let wx = x.weight(), let wy = y.weight(), let score = d + wx + wy; - + // Filter using trait method in condition - HeavyNode(n) <- + HeavyNode(n) <- Location(n), let w = n.weight(), (w > 500); @@ -87,63 +93,73 @@ crepe! { #[test] fn test_showcase_all_features() { - let seattle = City { id: 1, city_name: "Seattle", population: 750000 }; - let portland = City { id: 2, city_name: "Portland", population: 650000 }; - let sf = City { id: 3, city_name: "SF", population: 900000 }; - + let seattle = City { + id: 1, + city_name: "Seattle", + population: 750000, + }; + let portland = City { + id: 2, + city_name: "Portland", + population: 650000, + }; + let sf = City { + id: 3, + city_name: "SF", + population: 900000, + }; + // Distance is also weighted #[derive(Hash, Eq, PartialEq, Clone, Copy, Default)] struct Distance(u32); - + impl Weighted for Distance { - fn weight(&self) -> u32 { self.0 } + fn weight(&self) -> u32 { + self.0 + } } - + let mut runtime = Crepe::new(); - + // Add connections runtime.extend([ Connection(seattle, portland, Distance(100)), Connection(portland, sf, Distance(200)), ]); - + // Add locations - runtime.extend([ - Location(seattle), - Location(portland), - Location(sf), - ]); - + runtime.extend([Location(seattle), Location(portland), Location(sf)]); + let (routes, weighted_routes, heavy_nodes) = runtime.run(); - + // Test routes with trait methods let route_vec: Vec<_> = routes .into_iter() .map(|Route(x, y, d)| (x.name(), y.name(), d)) .collect(); - + assert!(route_vec.contains(&("Seattle", "Portland", 100))); assert!(route_vec.contains(&("Portland", "SF", 200))); assert!(route_vec.contains(&("Seattle", "SF", 300))); - + // Test weighted routes let weighted_vec: Vec<_> = weighted_routes .into_iter() .map(|WeightedRoute(x, y, s)| (x.name(), y.name(), s)) .collect(); - + // Seattle->Portland: distance(100) + seattle.weight(750) + portland.weight(650) = 1500 - assert!(weighted_vec.iter().any(|(x, y, s)| - *x == "Seattle" && *y == "Portland" && *s == 1500 - )); - + assert!(weighted_vec + .iter() + .any(|(x, y, s)| *x == "Seattle" && *y == "Portland" && *s == 1500)); + // Test heavy nodes (weight > 500) let heavy_vec: Vec<_> = heavy_nodes .into_iter() .map(|HeavyNode(n)| n.name()) .collect(); - - assert!(heavy_vec.contains(&"Seattle")); // 750 > 500 + + assert!(heavy_vec.contains(&"Seattle")); // 750 > 500 assert!(heavy_vec.contains(&"Portland")); // 650 > 500 - assert!(heavy_vec.contains(&"SF")); // 900 > 500 + assert!(heavy_vec.contains(&"SF")); // 900 > 500 } diff --git a/tests/test_trait_method_call.rs b/tests/test_trait_method_call.rs index 98ff896..2c52480 100644 --- a/tests/test_trait_method_call.rs +++ b/tests/test_trait_method_call.rs @@ -37,17 +37,13 @@ fn test_trait_method_call() { Product(Item { id: 2, price: 200 }), Product(Item { id: 3, price: 300 }), ]); - + let (price_info,) = runtime.run(); let mut results: Vec<_> = price_info .into_iter() .map(|PriceInfo(item, price)| (item.id, price)) .collect(); results.sort_unstable(); - - assert_eq!(results, vec![ - (1, 100), - (2, 200), - (3, 300), - ]); + + assert_eq!(results, vec![(1, 100), (2, 200), (3, 300),]); } From 7c5eb9aa388c4cf4b768803928cc4dcca5577abe Mon Sep 17 00:00:00 2001 From: chriseth Date: Wed, 10 Dec 2025 11:04:07 +0000 Subject: [PATCH 20/30] Fix clippy. --- src/lib.rs | 2 +- src/parse.rs | 32 ++++++++++++++--------------- tests/test_parse.rs | 1 + tests/test_showcase_all_features.rs | 4 ---- 4 files changed, 18 insertions(+), 21 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 2cb6760..27232b2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -396,7 +396,7 @@ impl Context { }; program.rules.iter().for_each(|rule| { check(&rule.goal); - if rels_input.get(&rule.goal.relation.to_string()).is_some() { + if rels_input.contains_key(&rule.goal.relation.to_string()) { abort!( rule.goal.relation.span(), "Relations marked as @input cannot be derived from a rule." diff --git a/src/parse.rs b/src/parse.rs index fa58cf2..0c80dd1 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -49,7 +49,7 @@ pub struct Relation { pub struct_token: Token![struct], pub name: Ident, pub generics: Generics, - pub paren_token: token::Paren, + pub _paren_token: token::Paren, pub fields: Punctuated, pub semi_token: Token![;], } @@ -87,7 +87,7 @@ impl Parse for Relation { struct_token: input.parse()?, name: input.parse()?, generics: input.parse()?, - paren_token: parenthesized!(content in input), + _paren_token: parenthesized!(content in input), fields: content.parse_terminated(Field::parse_unnamed, Token![,])?, semi_token: input.parse()?, }) @@ -97,9 +97,9 @@ impl Parse for Relation { #[derive(Clone)] pub struct Rule { pub goal: Fact, - pub arrow_token: Token![<-], + pub _arrow_token: Token![<-], pub clauses: Punctuated, - pub semi_token: Token![;], + pub _semi_token: Token![;], } impl Parse for Rule { @@ -110,23 +110,23 @@ impl Parse for Rule { // A fact followed by a semicolon is the same as a rule with a single // clause of `(true)` if lookahead.peek(Token![;]) { - let semi_token = input.parse()?; + let _semi_token = input.parse()?; - let arrow_token = parse_quote!(<-); + let _arrow_token = parse_quote!(<-); let clauses = parse_quote!((true)); Ok(Self { goal, - arrow_token, + _arrow_token, clauses, - semi_token, + _semi_token, }) } else { Ok(Self { goal, - arrow_token: input.parse()?, + _arrow_token: input.parse()?, clauses: Punctuated::parse_separated_nonempty(input)?, - semi_token: input.parse()?, + _semi_token: input.parse()?, }) } } @@ -161,7 +161,7 @@ impl Parse for Clause { pub struct Fact { pub negate: Option, pub relation: Ident, - pub paren_token: token::Paren, + pub _paren_token: token::Paren, pub fields: Punctuated, } @@ -172,7 +172,7 @@ impl Parse for Fact { Ok(Self { negate: input.parse()?, relation: input.parse()?, - paren_token: parenthesized!(content in input), + _paren_token: parenthesized!(content in input), fields: content.parse_terminated( |input| { if input.peek(Token![_]) { @@ -193,9 +193,9 @@ impl Parse for Fact { #[derive(Clone)] pub struct For { - pub for_token: Token![for], + pub _for_token: Token![for], pub pat: Pat, - pub in_token: Token![in], + pub _in_token: Token![in], pub expr: Expr, } @@ -203,9 +203,9 @@ impl Parse for For { fn parse(input: ParseStream) -> Result { #[allow(clippy::mixed_read_write_in_expression)] Ok(Self { - for_token: input.parse()?, + _for_token: input.parse()?, pat: Pat::parse_single(input)?, - in_token: input.parse()?, + _in_token: input.parse()?, expr: input.parse()?, }) } diff --git a/tests/test_parse.rs b/tests/test_parse.rs index 9ad0bb7..656bd37 100644 --- a/tests/test_parse.rs +++ b/tests/test_parse.rs @@ -3,6 +3,7 @@ // Not much is done besides checking that crepe::crepe! is defined, // as well as not self-destructing with a compilation error. +#[allow(dead_code)] mod datalog { use crepe::crepe; diff --git a/tests/test_showcase_all_features.rs b/tests/test_showcase_all_features.rs index ceb416c..a2ea868 100644 --- a/tests/test_showcase_all_features.rs +++ b/tests/test_showcase_all_features.rs @@ -5,7 +5,6 @@ use std::fmt::{Debug, Display}; // Multiple trait definitions trait Node: Debug + Display { - fn id(&self) -> u32; fn name(&self) -> &'static str; } @@ -22,9 +21,6 @@ struct City { } impl Node for City { - fn id(&self) -> u32 { - self.id - } fn name(&self) -> &'static str { self.city_name } From 459f939cc872c406ae820650c3baffa1b18ef3f5 Mon Sep 17 00:00:00 2001 From: chriseth Date: Wed, 10 Dec 2025 11:05:36 +0000 Subject: [PATCH 21/30] Fix another clippy. --- src/strata.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/strata.rs b/src/strata.rs index 2ac07e2..5ff318a 100644 --- a/src/strata.rs +++ b/src/strata.rs @@ -40,7 +40,7 @@ impl Strata { Self { list, index } } - pub fn iter(&self) -> Iter> { + pub fn iter(&self) -> Iter<'_, Vec> { self.list.iter() } From c7e56763e2e8a3fc5fc8ef99423d9080cf704575 Mon Sep 17 00:00:00 2001 From: chriseth Date: Wed, 10 Dec 2025 13:04:06 +0000 Subject: [PATCH 22/30] Update expectations. --- tests/ui/bad_visibility_of_relation.stderr | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/ui/bad_visibility_of_relation.stderr b/tests/ui/bad_visibility_of_relation.stderr index d283f8b..0e3ee9e 100644 --- a/tests/ui/bad_visibility_of_relation.stderr +++ b/tests/ui/bad_visibility_of_relation.stderr @@ -1,7 +1,7 @@ error[E0603]: tuple struct constructor `Test` is private --> tests/ui/bad_visibility_of_relation.rs:21:22 | -9 | struct Test(u32); + 9 | struct Test(u32); | --- a constructor is private if any of the fields is private ... 21 | let _ = datalog::Test(1); @@ -10,11 +10,11 @@ error[E0603]: tuple struct constructor `Test` is private note: the tuple struct constructor `Test` is defined here --> tests/ui/bad_visibility_of_relation.rs:9:9 | -9 | struct Test(u32); + 9 | struct Test(u32); | ^^^^^^^^^^^^^^^^^ help: consider making the field publicly accessible | -9 | struct Test(pub u32); + 9 | struct Test(pub u32); | +++ error[E0603]: tuple struct constructor `MoreTest` is private From 12dbdbf39737ac05b0505b8d1d6a6c5635b3ad84 Mon Sep 17 00:00:00 2001 From: chriseth Date: Wed, 10 Dec 2025 13:22:02 +0000 Subject: [PATCH 23/30] fmt --- src/lib.rs | 42 +++++++++++++++++++++++++----------------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index b4440b1..d4eb43a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -622,14 +622,14 @@ fn make_runtime_decl(context: &Context) -> proc_macro2::TokenStream { println!("\n=== Crepe Profiling Report ==="); println!("Total execution time: {:?}", self.total_duration); println!("\nRule timings (sorted by total duration):"); - + let mut sorted_rules = self.rules.clone(); sorted_rules.sort_by(|a, b| b.total_duration.cmp(&a.total_duration)); - - println!("{:<50} {:>12} {:>12} {:>15} {:>15}", + + println!("{:<50} {:>12} {:>12} {:>15} {:>15}", "Rule", "Total (ms)", "Calls", "Avg (μs)", "Facts Gen."); println!("{}", "-".repeat(110)); - + for rule in &sorted_rules { let total_ms = rule.total_duration.as_secs_f64() * 1000.0; let avg_us = rule.avg_duration().as_secs_f64() * 1_000_000.0; @@ -638,8 +638,8 @@ fn make_runtime_decl(context: &Context) -> proc_macro2::TokenStream { } else { 0.0 }; - - println!("{:<50} {:>10.2}ms {:>12} {:>13.2}μs {:>15}", + + println!("{:<50} {:>10.2}ms {:>12} {:>13.2}μs {:>15}", rule.rule_id, total_ms, rule.eval_count, avg_us, rule.facts_generated); } println!(); @@ -839,7 +839,7 @@ fn make_run(context: &Context) -> proc_macro2::TokenStream { }); let init_profiling = quote! { let mut __crepe_rule_stats: ::std::collections::HashMap< - String, + String, (::std::time::Duration, u64, u64) > = ::std::collections::HashMap::new(); }; @@ -893,11 +893,11 @@ fn make_run(context: &Context) -> proc_macro2::TokenStream { ) -> (#output_ty_hasher, ProfilingStats) { let __crepe_total_start = ::std::time::Instant::now(); let mut __crepe_profiling_stats = ProfilingStats::default(); - + #initialize #main_loops #finalize_profiling - + __crepe_profiling_stats.total_duration = __crepe_total_start.elapsed(); (#output, __crepe_profiling_stats) } @@ -1043,9 +1043,17 @@ fn make_rule( let span = rule.goal.relation.span(); let line_info = span.unwrap().start(); let rule_id = format!("{}#{}:L{}", goal_relation_name, rule_idx, line_info.line()); - let rule_id_ident = format_ident!("__crepe_rule_{}_{}", to_lowercase(&rule.goal.relation), rule_idx); - let stats_var = format_ident!("__crepe_stats_{}_{}", to_lowercase(&rule.goal.relation), rule_idx); - + let rule_id_ident = format_ident!( + "__crepe_rule_{}_{}", + to_lowercase(&rule.goal.relation), + rule_idx + ); + let stats_var = format_ident!( + "__crepe_stats_{}_{}", + to_lowercase(&rule.goal.relation), + rule_idx + ); + let goal = { let relation = &rule.goal.relation; let fields = &rule.goal.fields; @@ -1074,7 +1082,7 @@ fn make_rule( _ => None, }) .collect(); - + let rule_body = if fact_positions.is_empty() { // Will not change, so we only need to evaluate it once let mut datalog_vars: HashSet = HashSet::new(); @@ -1111,17 +1119,17 @@ fn make_rule( } variants.into_iter().collect() }; - + quote! { { - let #stats_var: &mut (::std::time::Duration, u64, u64) = + let #stats_var: &mut (::std::time::Duration, u64, u64) = __crepe_rule_stats.entry(#rule_id.to_string()) .or_insert((::std::time::Duration::ZERO, 0, 0)); #stats_var.1 += 1; let #rule_id_ident = ::std::time::Instant::now(); - + #rule_body - + #stats_var.0 += #rule_id_ident.elapsed(); } } From 82ecd0514c1d8ece8c8940546b7b7d7e5be64c3b Mon Sep 17 00:00:00 2001 From: chriseth Date: Wed, 10 Dec 2025 14:10:38 +0000 Subject: [PATCH 24/30] Make interface functions public. --- src/lib.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 27232b2..b6f5ea1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -299,7 +299,7 @@ struct Context { } impl Context { - fn new(program: Program) -> Self { + pub fn new(program: Program) -> Self { // Read in relations, ensure no duplicates let mut rels_input = HashMap::new(); let mut rels_output = HashMap::new(); @@ -579,7 +579,7 @@ fn make_runtime_decl(context: &Context) -> proc_macro2::TokenStream { quote! { #[derive(::core::default::Default)] - struct Crepe #generics_decl { + pub struct Crepe #generics_decl { #fields } } @@ -794,7 +794,7 @@ fn make_run(context: &Context) -> proc_macro2::TokenStream { let output_ty_default = make_output_ty(context, quote! {}); quote! { #[allow(clippy::collapsible_if)] - fn run_with_hasher( + pub fn run_with_hasher( self ) -> #output_ty_hasher { #initialize @@ -802,7 +802,7 @@ fn make_run(context: &Context) -> proc_macro2::TokenStream { #output } - fn run(self) -> #output_ty_default { + pub fn run(self) -> #output_ty_default { self.run_with_hasher::<::std::collections::hash_map::RandomState>() } } From b2171d8ccc686734322a38a32fd1a65129730235 Mon Sep 17 00:00:00 2001 From: chriseth Date: Wed, 10 Dec 2025 18:18:21 +0100 Subject: [PATCH 25/30] Update src/lib.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 40d9308..eacc401 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1123,7 +1123,7 @@ fn make_rule( quote! { { let #stats_var: &mut (::std::time::Duration, u64, u64) = - __crepe_rule_stats.entry(#rule_id.to_string()) + __crepe_rule_stats.entry(#rule_id) .or_insert((::std::time::Duration::ZERO, 0, 0)); #stats_var.1 += 1; let #rule_id_ident = ::std::time::Instant::now(); From d32628139346cc35c0f8bd727bb72c643b9d003a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Dec 2025 17:20:06 +0000 Subject: [PATCH 26/30] Initial plan From 3a959799fda26b426513e1c0621f394af57544d6 Mon Sep 17 00:00:00 2001 From: chriseth Date: Wed, 10 Dec 2025 17:21:17 +0000 Subject: [PATCH 27/30] Revert "Update src/lib.rs" This reverts commit b2171d8ccc686734322a38a32fd1a65129730235. --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index eacc401..40d9308 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1123,7 +1123,7 @@ fn make_rule( quote! { { let #stats_var: &mut (::std::time::Duration, u64, u64) = - __crepe_rule_stats.entry(#rule_id) + __crepe_rule_stats.entry(#rule_id.to_string()) .or_insert((::std::time::Duration::ZERO, 0, 0)); #stats_var.1 += 1; let #rule_id_ident = ::std::time::Instant::now(); From 34e42ffb9df768265134c649ef0ae394c3357573 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Dec 2025 17:22:49 +0000 Subject: [PATCH 28/30] Remove unused pct variable from profiling report Co-authored-by: chriseth <9073706+chriseth@users.noreply.github.com> --- src/lib.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index eacc401..926d79d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -633,11 +633,6 @@ fn make_runtime_decl(context: &Context) -> proc_macro2::TokenStream { for rule in &sorted_rules { let total_ms = rule.total_duration.as_secs_f64() * 1000.0; let avg_us = rule.avg_duration().as_secs_f64() * 1_000_000.0; - let pct = if self.total_duration.as_nanos() > 0 { - (rule.total_duration.as_nanos() as f64 / self.total_duration.as_nanos() as f64) * 100.0 - } else { - 0.0 - }; println!("{:<50} {:>10.2}ms {:>12} {:>13.2}μs {:>15}", rule.rule_id, total_ms, rule.eval_count, avg_us, rule.facts_generated); From d47ff4afd13f2ccaa5e41af712401e829839b9ef Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Dec 2025 17:25:00 +0000 Subject: [PATCH 29/30] Initial plan From ae98af70ed5f32fd32710b0e2479d65edb5f2604 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Dec 2025 17:28:51 +0000 Subject: [PATCH 30/30] Use Itertools::sorted_by() to avoid cloning rules vector Co-authored-by: chriseth <9073706+chriseth@users.noreply.github.com> --- Cargo.toml | 1 + src/lib.rs | 7 +++---- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 777cb6f..36f1626 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,3 +37,4 @@ quote = "1.0" proc-macro2 = "1.0" proc-macro-error = "1.0" petgraph = "0.6" +itertools = "0.10" diff --git a/src/lib.rs b/src/lib.rs index 41fa912..df9fad5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -619,18 +619,17 @@ fn make_runtime_decl(context: &Context) -> proc_macro2::TokenStream { impl ProfilingStats { /// Print a formatted report of rule timings pub fn report(&self) { + use ::itertools::Itertools; + println!("\n=== Crepe Profiling Report ==="); println!("Total execution time: {:?}", self.total_duration); println!("\nRule timings (sorted by total duration):"); - let mut sorted_rules = self.rules.clone(); - sorted_rules.sort_by(|a, b| b.total_duration.cmp(&a.total_duration)); - println!("{:<50} {:>12} {:>12} {:>15} {:>15}", "Rule", "Total (ms)", "Calls", "Avg (μs)", "Facts Gen."); println!("{}", "-".repeat(110)); - for rule in &sorted_rules { + for rule in self.rules.iter().sorted_by(|a, b| b.total_duration.cmp(&a.total_duration)) { let total_ms = rule.total_duration.as_secs_f64() * 1000.0; let avg_us = rule.avg_duration().as_secs_f64() * 1_000_000.0;