Skip to main content

conjure_cp_rule_macros/
lib.rs

1use proc_macro::TokenStream;
2
3use proc_macro2::Span;
4use quote::quote;
5use syn::token::Comma;
6use syn::{
7    ExprClosure, Ident, ItemFn, LitInt, LitStr, Result, bracketed, parenthesized, parse::Parse,
8    parse::ParseStream, parse_macro_input,
9};
10
11struct RegisterRuleArgs {
12    rule_sets: Vec<LitStr>,
13    priority: LitInt,
14    /// Expression variant names this rule applies to (e.g. `Add`, `Sub`).
15    /// Empty means applicable to all variants (universal rule).
16    applicable_variants: Vec<Ident>,
17}
18
19impl Parse for RegisterRuleArgs {
20    fn parse(input: ParseStream) -> Result<Self> {
21        if input.is_empty() {
22            return Ok(RegisterRuleArgs {
23                rule_sets: Vec::new(),
24                priority: LitInt::new("0", Span::call_site()),
25                applicable_variants: Vec::new(),
26            });
27        }
28
29        let rule_sets = if input.peek(syn::token::Bracket) {
30            let content;
31            bracketed!(content in input);
32
33            let mut rule_sets = Vec::new();
34            while !content.is_empty() {
35                let rule_set: LitStr = content.parse()?;
36                rule_sets.push(rule_set);
37                if content.is_empty() {
38                    break;
39                }
40                let _: Comma = content.parse()?;
41            }
42            rule_sets
43        } else {
44            vec![input.parse()?]
45        };
46
47        let _: Comma = input.parse()?;
48        let priority: LitInt = input.parse()?;
49
50        // Parse optional variant names in brackets: "Minion", 4200, [Add, Sub]
51        let mut applicable_variants = Vec::new();
52        if input.peek(Comma) {
53            let _: Comma = input.parse()?;
54            let content;
55            bracketed!(content in input);
56            while !content.is_empty() {
57                let variant: Ident = content.parse()?;
58                applicable_variants.push(variant);
59                if content.is_empty() {
60                    break;
61                }
62                let _: Comma = content.parse()?;
63            }
64        }
65
66        Ok(RegisterRuleArgs {
67            rule_sets,
68            priority,
69            applicable_variants,
70        })
71    }
72}
73
74/// Register a rule with the given rule sets and priorities.
75#[proc_macro_attribute]
76pub fn register_rule(arg_tokens: TokenStream, item: TokenStream) -> TokenStream {
77    let func = parse_macro_input!(item as ItemFn);
78    let rule_ident = &func.sig.ident;
79    let static_name = format!("CONJURE_GEN_RULE_{rule_ident}").to_uppercase();
80    let static_ident = Ident::new(&static_name, rule_ident.span());
81
82    let args = parse_macro_input!(arg_tokens as RegisterRuleArgs);
83
84    let rule_sets_token = if args.rule_sets.is_empty() {
85        quote! { &[] }
86    } else {
87        let rule_sets = &args.rule_sets;
88        let priority = &args.priority;
89        quote! { &[#((#rule_sets, #priority as u16)),*] }
90    };
91
92    let applicable_to = if args.applicable_variants.is_empty() {
93        quote! { None }
94    } else {
95        let variants = &args.applicable_variants;
96        quote! {
97            Some(&[#(::conjure_cp::discriminant_from_name!(#variants)),*])
98        }
99    };
100
101    let expanded = quote! {
102        #func
103
104        use ::conjure_cp::rule_engine::_dependencies::*; // ToDo idk if we need to explicitly do that?
105
106        #[::conjure_cp::rule_engine::_dependencies::distributed_slice(::conjure_cp::rule_engine::RULES_DISTRIBUTED_SLICE)]
107        pub static #static_ident: ::conjure_cp::rule_engine::Rule<'static> = ::conjure_cp::rule_engine::Rule {
108            name: stringify!(#rule_ident),
109            application: #rule_ident,
110            rule_sets: #rule_sets_token,
111            applicable_to: #applicable_to,
112        };
113    };
114
115    TokenStream::from(expanded)
116}
117
118fn parse_parenthesized<T: Parse>(input: ParseStream) -> Result<Vec<T>> {
119    let content;
120    parenthesized!(content in input);
121
122    let mut paths = Vec::new();
123    while !content.is_empty() {
124        let path = content.parse()?;
125        paths.push(path);
126        if content.is_empty() {
127            break;
128        }
129        content.parse::<Comma>()?;
130    }
131
132    Ok(paths)
133}
134
135struct RuleSetArgs {
136    name: LitStr,
137    dependencies: Vec<LitStr>,
138    applies_fn: Option<ExprClosure>,
139}
140
141impl Parse for RuleSetArgs {
142    fn parse(input: ParseStream) -> Result<Self> {
143        let name = input.parse()?;
144
145        if input.is_empty() {
146            return Ok(Self {
147                name,
148                dependencies: Vec::new(),
149                applies_fn: None,
150            });
151        }
152
153        input.parse::<Comma>()?;
154        let dependencies = parse_parenthesized::<LitStr>(input)?;
155
156        if input.is_empty() {
157            return Ok(Self {
158                name,
159                dependencies,
160                applies_fn: None,
161            });
162        }
163
164        input.parse::<Comma>()?;
165        let applies_fn = input.parse::<ExprClosure>()?;
166
167        Ok(Self {
168            name,
169            dependencies,
170            applies_fn: Some(applies_fn),
171        })
172    }
173}
174
175/**
176* Register a rule set with the given name, dependencies, and metadata.
177*
178* # Example
179* ```rust
180 * use conjure_cp_rule_macros::register_rule_set;
181 * register_rule_set!("MyRuleSet", ("DependencyRuleSet", "AnotherRuleSet"));
182* ```
183 */
184#[proc_macro]
185pub fn register_rule_set(args: TokenStream) -> TokenStream {
186    let RuleSetArgs {
187        name,
188        dependencies,
189        applies_fn,
190    } = parse_macro_input!(args as RuleSetArgs);
191
192    let static_name = format!("CONJURE_GEN_RULE_SET_{}", name.value()).to_uppercase();
193    let static_ident = Ident::new(&static_name, Span::call_site());
194
195    let dependencies = quote! {
196        #(#dependencies),*
197    };
198
199    let applies_to_family = match applies_fn {
200        // Does not apply by default, e.g. only used as a dependency
201        None => quote! { |_: &::conjure_cp::settings::SolverFamily| false },
202        Some(func) => quote! { #func },
203    };
204
205    let expanded = quote! {
206        use ::conjure_cp::rule_engine::_dependencies::*; // ToDo idk if we need to explicitly do that?
207        #[::conjure_cp::rule_engine::_dependencies::distributed_slice(::conjure_cp::rule_engine::RULE_SETS_DISTRIBUTED_SLICE)]
208        pub static #static_ident: ::conjure_cp::rule_engine::RuleSet<'static> =
209            ::conjure_cp::rule_engine::RuleSet::new(#name, &[#dependencies], #applies_to_family);
210    };
211
212    TokenStream::from(expanded)
213}