use proc_macro::TokenStream;
use quote::{format_ident, quote};
use syn::{DeriveInput, Expr, parse_macro_input};
fn extract_verify_attr(input: &DeriveInput, key: &str) -> Option<proc_macro2::TokenStream> {
for attr in &input.attrs {
if !attr.path().is_ident("verify") {
continue;
}
let Ok(nested) = attr.parse_args_with(
syn::punctuated::Punctuated::<syn::Meta, syn::Token![,]>::parse_terminated,
) else {
continue;
};
for meta in &nested {
if let syn::Meta::NameValue(nv) = meta
&& nv.path.is_ident(key)
&& let Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(s),
..
}) = &nv.value
{
let tokens: proc_macro2::TokenStream = s
.value()
.parse()
.unwrap_or_else(|_| panic!("{key} must be a valid Rust expression"));
return Some(tokens);
}
}
}
None
}
fn extract_strategy(input: &DeriveInput) -> proc_macro2::TokenStream {
extract_verify_attr(input, "strategy")
.expect("#[verify(strategy = \"...\")] attribute is required")
}
fn extract_epsilon(input: &DeriveInput) -> Option<proc_macro2::TokenStream> {
extract_verify_attr(input, "epsilon")
}
fn make_assert(
epsilon: &Option<proc_macro2::TokenStream>,
left: proc_macro2::TokenStream,
right: proc_macro2::TokenStream,
) -> proc_macro2::TokenStream {
if let Some(eps) = epsilon {
quote! {
let __left = #left;
let __right = #right;
prop_assert!(
(__left - __right).abs() < #eps,
"expected {:?} ≈ {:?} (epsilon {})", __left, __right, #eps
);
}
} else {
quote! {
prop_assert_eq!(#left, #right);
}
}
}
#[proc_macro_derive(VerifySemigroup, attributes(verify))]
pub fn derive_verify_semigroup(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
let strategy = extract_strategy(&input);
let epsilon = extract_epsilon(&input);
let mod_name = format_ident!("verify_semigroup_{}", name.to_string().to_lowercase());
let assert_assoc = make_assert(
&epsilon,
quote! { a.clone().combine(b.clone()).combine(c.clone()) },
quote! { a.combine(b.combine(c)) },
);
let output = quote! {
#[cfg(test)]
mod #mod_name {
use super::*;
use karpal_core::Semigroup;
use proptest::prelude::*;
proptest! {
#[test]
fn associativity(a in #strategy, b in #strategy, c in #strategy) {
#assert_assoc
}
}
}
};
output.into()
}
#[proc_macro_derive(VerifyMonoid, attributes(verify))]
pub fn derive_verify_monoid(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
let strategy = extract_strategy(&input);
let epsilon = extract_epsilon(&input);
let mod_name = format_ident!("verify_monoid_{}", name.to_string().to_lowercase());
let assert_left = make_assert(
&epsilon,
quote! { <#name as Monoid>::empty().combine(a.clone()) },
quote! { a },
);
let assert_right = make_assert(
&epsilon,
quote! { a.clone().combine(<#name as Monoid>::empty()) },
quote! { a },
);
let output = quote! {
#[cfg(test)]
mod #mod_name {
use super::*;
use karpal_core::{Semigroup, Monoid};
use proptest::prelude::*;
proptest! {
#[test]
fn left_identity(a in #strategy) {
#assert_left
}
#[test]
fn right_identity(a in #strategy) {
#assert_right
}
}
}
};
output.into()
}
#[proc_macro_derive(VerifyGroup, attributes(verify))]
pub fn derive_verify_group(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
let strategy = extract_strategy(&input);
let epsilon = extract_epsilon(&input);
let mod_name = format_ident!("verify_group_{}", name.to_string().to_lowercase());
let assert_left = make_assert(
&epsilon,
quote! { a.clone().invert().combine(a.clone()) },
quote! { <#name as Monoid>::empty() },
);
let assert_right = make_assert(
&epsilon,
quote! { a.clone().combine(a.clone().invert()) },
quote! { <#name as Monoid>::empty() },
);
let output = quote! {
#[cfg(test)]
mod #mod_name {
use super::*;
use karpal_core::{Semigroup, Monoid};
use karpal_algebra::Group;
use proptest::prelude::*;
proptest! {
#[test]
fn left_inverse(a in #strategy) {
#assert_left
}
#[test]
fn right_inverse(a in #strategy) {
#assert_right
}
}
}
};
output.into()
}
#[proc_macro_derive(VerifyCommutative, attributes(verify))]
pub fn derive_verify_commutative(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
let strategy = extract_strategy(&input);
let epsilon = extract_epsilon(&input);
let mod_name = format_ident!("verify_commutative_{}", name.to_string().to_lowercase());
let assert_comm = make_assert(
&epsilon,
quote! { a.clone().combine(b.clone()) },
quote! { b.combine(a) },
);
let output = quote! {
#[cfg(test)]
mod #mod_name {
use super::*;
use karpal_core::Semigroup;
use proptest::prelude::*;
proptest! {
#[test]
fn commutativity(a in #strategy, b in #strategy) {
#assert_comm
}
}
}
};
output.into()
}
#[proc_macro_derive(VerifySemiring, attributes(verify))]
pub fn derive_verify_semiring(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
let strategy = extract_strategy(&input);
let epsilon = extract_epsilon(&input);
let mod_name = format_ident!("verify_semiring_{}", name.to_string().to_lowercase());
let assert_add_assoc = make_assert(
&epsilon,
quote! { a.clone().add(b.clone()).add(c.clone()) },
quote! { a.clone().add(b.clone().add(c.clone())) },
);
let assert_add_comm = make_assert(
&epsilon,
quote! { a.clone().add(b.clone()) },
quote! { b.clone().add(a.clone()) },
);
let assert_add_left_id = make_assert(
&epsilon,
quote! { <#name as Semiring>::zero().add(a.clone()) },
quote! { a.clone() },
);
let assert_add_right_id = make_assert(
&epsilon,
quote! { a.clone().add(<#name as Semiring>::zero()) },
quote! { a.clone() },
);
let assert_mul_assoc = make_assert(
&epsilon,
quote! { a.clone().mul(b.clone()).mul(c.clone()) },
quote! { a.clone().mul(b.clone().mul(c.clone())) },
);
let assert_mul_left_id = make_assert(
&epsilon,
quote! { <#name as Semiring>::one().mul(a.clone()) },
quote! { a.clone() },
);
let assert_mul_right_id = make_assert(
&epsilon,
quote! { a.clone().mul(<#name as Semiring>::one()) },
quote! { a.clone() },
);
let assert_left_dist = make_assert(
&epsilon,
quote! { a.clone().mul(b.clone().add(c.clone())) },
quote! { a.clone().mul(b.clone()).add(a.clone().mul(c.clone())) },
);
let assert_right_dist = make_assert(
&epsilon,
quote! { a.clone().add(b.clone()).mul(c.clone()) },
quote! { a.clone().mul(c.clone()).add(b.clone().mul(c.clone())) },
);
let assert_zero_left = make_assert(
&epsilon,
quote! { <#name as Semiring>::zero().mul(a.clone()) },
quote! { <#name as Semiring>::zero() },
);
let assert_zero_right = make_assert(
&epsilon,
quote! { a.clone().mul(<#name as Semiring>::zero()) },
quote! { <#name as Semiring>::zero() },
);
let output = quote! {
#[cfg(test)]
mod #mod_name {
use super::*;
use karpal_algebra::Semiring;
use proptest::prelude::*;
proptest! {
#[test]
fn add_associativity(a in #strategy, b in #strategy, c in #strategy) {
#assert_add_assoc
}
#[test]
fn add_commutativity(a in #strategy, b in #strategy) {
#assert_add_comm
}
#[test]
fn add_identity(a in #strategy) {
#assert_add_left_id
#assert_add_right_id
}
#[test]
fn mul_associativity(a in #strategy, b in #strategy, c in #strategy) {
#assert_mul_assoc
}
#[test]
fn mul_identity(a in #strategy) {
#assert_mul_left_id
#assert_mul_right_id
}
#[test]
fn left_distributivity(a in #strategy, b in #strategy, c in #strategy) {
#assert_left_dist
}
#[test]
fn right_distributivity(a in #strategy, b in #strategy, c in #strategy) {
#assert_right_dist
}
#[test]
fn zero_annihilation(a in #strategy) {
#assert_zero_left
#assert_zero_right
}
}
}
};
output.into()
}
#[proc_macro_derive(VerifyRing, attributes(verify))]
pub fn derive_verify_ring(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
let strategy = extract_strategy(&input);
let epsilon = extract_epsilon(&input);
let mod_name = format_ident!("verify_ring_{}", name.to_string().to_lowercase());
let assert_left = make_assert(
&epsilon,
quote! { a.clone().negate().add(a.clone()) },
quote! { <#name as Semiring>::zero() },
);
let assert_right = make_assert(
&epsilon,
quote! { a.clone().add(a.clone().negate()) },
quote! { <#name as Semiring>::zero() },
);
let output = quote! {
#[cfg(test)]
mod #mod_name {
use super::*;
use karpal_algebra::{Semiring, Ring};
use proptest::prelude::*;
proptest! {
#[test]
fn left_additive_inverse(a in #strategy) {
#assert_left
}
#[test]
fn right_additive_inverse(a in #strategy) {
#assert_right
}
}
}
};
output.into()
}
#[proc_macro_derive(VerifyLattice, attributes(verify))]
pub fn derive_verify_lattice(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
let strategy = extract_strategy(&input);
let mod_name = format_ident!("verify_lattice_{}", name.to_string().to_lowercase());
let output = quote! {
#[cfg(test)]
mod #mod_name {
use super::*;
use karpal_algebra::Lattice;
use proptest::prelude::*;
proptest! {
#[test]
fn join_associativity(a in #strategy, b in #strategy, c in #strategy) {
prop_assert_eq!(
a.clone().join(b.clone()).join(c.clone()),
a.join(b.join(c))
);
}
#[test]
fn meet_associativity(a in #strategy, b in #strategy, c in #strategy) {
prop_assert_eq!(
a.clone().meet(b.clone()).meet(c.clone()),
a.meet(b.meet(c))
);
}
#[test]
fn join_commutativity(a in #strategy, b in #strategy) {
prop_assert_eq!(a.clone().join(b.clone()), b.join(a));
}
#[test]
fn meet_commutativity(a in #strategy, b in #strategy) {
prop_assert_eq!(a.clone().meet(b.clone()), b.meet(a));
}
#[test]
fn join_idempotency(a in #strategy) {
prop_assert_eq!(a.clone().join(a.clone()), a);
}
#[test]
fn meet_idempotency(a in #strategy) {
prop_assert_eq!(a.clone().meet(a.clone()), a);
}
#[test]
fn absorption_join_meet(a in #strategy, b in #strategy) {
prop_assert_eq!(a.clone().join(a.clone().meet(b)), a);
}
#[test]
fn absorption_meet_join(a in #strategy, b in #strategy) {
prop_assert_eq!(a.clone().meet(a.clone().join(b)), a);
}
}
}
};
output.into()
}