Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
de56873
Initial implementation of generics.
chriseth Nov 26, 2025
e847517
Finish generics.
chriseth Nov 26, 2025
da8acfd
More tests.
chriseth Nov 26, 2025
d4a3b4c
Remove unnecessary '__b lifetime.
chriseth Nov 26, 2025
00cb520
Simplify generics formatting.
chriseth Nov 26, 2025
1137eb2
simplify
chriseth Nov 26, 2025
632b4e5
simplify
chriseth Nov 26, 2025
ca688ec
cleanup
chriseth Nov 26, 2025
5a0b8ee
Profiling.
chriseth Dec 1, 2025
a9aa60f
Auto-print stats.
chriseth Dec 1, 2025
c63cb74
Adjust counter.
chriseth Dec 1, 2025
2a3508a
fix
chriseth Dec 1, 2025
ae33066
Output line number.
chriseth Dec 2, 2025
a9419a1
Do not print report by default.
chriseth Dec 2, 2025
614939a
Make Crepe public.
chriseth Dec 2, 2025
85bd74b
Some more public functions.
chriseth Dec 2, 2025
1fe4c6a
Remove redundant tests.
chriseth Dec 8, 2025
06cd6fe
Merge branch 'generics' into profiling
chriseth Dec 8, 2025
516efba
Update after review.
chriseth Dec 9, 2025
8948090
Merge pull request #1 from powdr-labs/generics
chriseth Dec 10, 2025
bfb9d4c
Merge remote-tracking branch 'origin/main' into profiling
chriseth Dec 10, 2025
bad66ed
Fix fmt.
chriseth Dec 10, 2025
7c5eb9a
Fix clippy.
chriseth Dec 10, 2025
459f939
Fix another clippy.
chriseth Dec 10, 2025
c7e5676
Update expectations.
chriseth Dec 10, 2025
23a43e4
Merge branch 'fix' into profiling_
chriseth Dec 10, 2025
12dbdbf
fmt
chriseth Dec 10, 2025
25fc908
Merge pull request #6 from powdr-labs/fix
chriseth Dec 10, 2025
c1ee2dc
Merge remote-tracking branch 'origin/main' into profiling_
chriseth Dec 10, 2025
82ecd05
Make interface functions public.
chriseth Dec 10, 2025
2bce709
Merge pull request #7 from powdr-labs/visibility
chriseth Dec 10, 2025
2797960
Merge remote-tracking branch 'origin/main' into profiling_
chriseth Dec 10, 2025
b2171d8
Update src/lib.rs
chriseth Dec 10, 2025
d326281
Initial plan
Copilot Dec 10, 2025
3a95979
Revert "Update src/lib.rs"
chriseth Dec 10, 2025
34e42ff
Remove unused pct variable from profiling report
Copilot Dec 10, 2025
a57155e
Merge pull request #8 from powdr-labs/copilot/sub-pr-5
chriseth Dec 10, 2025
d47ff4a
Initial plan
Copilot Dec 10, 2025
ae98af7
Use Itertools::sorted_by() to avoid cloning rules vector
Copilot Dec 10, 2025
70bdce4
Merge pull request #9 from powdr-labs/copilot/sub-pr-5-again
chriseth Dec 10, 2025
3600822
Merge pull request #5 from powdr-labs/profiling_
chriseth Dec 10, 2025
bf88742
Modernize CI steps
ekzhang Dec 13, 2025
6d95704
Remove bench/profiling code
ekzhang Dec 14, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 6 additions & 13 deletions .github/workflows/ci.yml → .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,9 @@ jobs:
name: Build and Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4

- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
- uses: dtolnay/rust-toolchain@stable

- run: cargo build --release --all-features

Expand All @@ -28,12 +25,10 @@ jobs:
name: Rustfmt
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4

- uses: actions-rs/toolchain@v1
- uses: dtolnay/rust-toolchain@stable
with:
profile: minimal
toolchain: stable
components: rustfmt

- run: cargo fmt -- --check
Expand All @@ -42,12 +37,10 @@ jobs:
name: Clippy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4

- uses: actions-rs/toolchain@v1
- uses: dtolnay/rust-toolchain@stable
with:
profile: minimal
toolchain: stable
components: clippy

- run: cargo clippy --all-targets -- -D warnings
231 changes: 192 additions & 39 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -262,9 +262,8 @@ use strata::Strata;
///
/// # Hygiene
/// In addition to the relation structs, this macro generates implementations
/// of a private struct named `Crepe` for the runtime. Therefore, it is
/// recommended to place each Datalog program within its own module, to prevent
/// name collisions.
/// of a public struct named `Crepe` for the runtime. It is recommended to place
/// each Datalog program within its own module to prevent name collisions.
#[proc_macro]
#[proc_macro_error]
pub fn crepe(input: TokenStream) -> TokenStream {
Expand Down Expand Up @@ -299,7 +298,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();
Expand All @@ -316,12 +315,7 @@ 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");
}
if let Some(c) = relation.generics.const_params().next() {
abort!(c.span(), "Const parameters are not supported in relations");
}
validate_generic_params(&relation);
let num_lifetimes = relation.generics.lifetimes().count();

match relation.relation_type() {
Expand Down Expand Up @@ -399,7 +393,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."
Expand Down Expand Up @@ -570,8 +564,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! {
Expand All @@ -580,11 +572,12 @@ 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! {
/// The Crepe runtime generated from a Datalog program.
#[derive(::core::default::Default)]
struct Crepe #lifetime {
pub struct Crepe #generics_decl {
#fields
}
}
Expand All @@ -594,11 +587,12 @@ 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 {
fn new() -> Self {
impl #generics_decl Crepe #generics_args {
pub fn new() -> Self {
::core::default::Default::default()
}
#run
Expand All @@ -613,21 +607,32 @@ 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 = {
let mut items = vec![quote! { 'a }];
for tp in collect_generic_params(context) {
items.push(merge_bounds_with_required(tp));
}
format_generics(items)
};

quote! {
impl #lifetime ::core::iter::Extend<#rel_ty> for Crepe #lifetime {
fn extend<T>(&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<Item = #rel_ty>,
__I: ::core::iter::IntoIterator<Item = #rel_ty>,
{
self.#lower.extend(iter);
}
}
impl<'a> ::core::iter::Extend<&'a #rel_ty> for Crepe #lifetime {
fn extend<T>(&mut self, iter: T)
impl #ref_impl_generics ::core::iter::Extend<&'a #rel_ty> for Crepe #generics_args {
fn extend<__I>(&mut self, iter: __I)
where
T: ::core::iter::IntoIterator<Item = &'a #rel_ty>,
__I: ::core::iter::IntoIterator<Item = &'a #rel_ty>,
{
self.extend(iter.into_iter().copied());
}
Expand Down Expand Up @@ -787,15 +792,15 @@ 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<CrepeHasher: ::std::hash::BuildHasher + ::core::default::Default>(
pub fn run_with_hasher<CrepeHasher: ::std::hash::BuildHasher + ::core::default::Default>(
self
) -> #output_ty_hasher {
#initialize
#main_loops
#output
}

fn run(self) -> #output_ty_default {
pub fn run(self) -> #output_ty_default {
self.run_with_hasher::<::std::collections::hash_map::RandomState>()
}
}
Expand Down Expand Up @@ -957,6 +962,7 @@ fn make_rule(
_ => None,
})
.collect();

if fact_positions.is_empty() {
// Will not change, so we only need to evaluate it once
let mut datalog_vars: HashSet<String> = HashSet::new();
Expand Down Expand Up @@ -1240,21 +1246,156 @@ 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 {
/// Validate generic paraeters on a relation.
fn validate_generic_params(relation: &Relation) {
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
);
}
}
}

/// 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
}

/// 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()
.is_some_and(|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 {
"Hash" => quote! { ::core::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;

// 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
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 `<item1, item2, ...>` or empty if the list is empty.
fn format_generics(items: Vec<proc_macro2::TokenStream>) -> proc_macro2::TokenStream {
if items.is_empty() {
quote! {}
} else {
quote! { <#(#items),*> }
}
}

/// Create a TokenStream for generic parameters (lifetimes + type params).
fn generic_params_decl(context: &Context) -> proc_macro2::TokenStream {
let mut items = Vec::new();
if context.has_input_lifetime {
items.push(quote! { 'a });
}
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 });
}
items.extend(collect_generic_params(context).into_iter().map(|tp| {
let ident = &tp.ident;
quote! { #ident }
}));
format_generics(items)
}

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",
Expand All @@ -1265,10 +1406,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::<Vec<_>>();
quote! { #name<#(#lifetimes),*> }

// 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 }
}));

// Format with angle brackets if there are any generics
if items.is_empty() {
quote! { #name }
} else {
quote! { #name<#(#items),*> }
}
}
Loading