diff --git a/Cargo.lock b/Cargo.lock index 8aac4f059..2bd606f88 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -277,6 +277,8 @@ dependencies = [ "anyhow", "clap", "conjure_core", + "conjure_rules", + "linkme", "minion_rs", "serde", "serde_json", @@ -288,6 +290,24 @@ dependencies = [ "walkdir", ] +[[package]] +name = "conjure_rules" +version = "0.1.0" +dependencies = [ + "conjure_core", + "conjure_rules_proc_macro", + "linkme", +] + +[[package]] +name = "conjure_rules_proc_macro" +version = "0.1.0" +dependencies = [ + "conjure_core", + "quote", + "syn 2.0.48", +] + [[package]] name = "core-foundation-sys" version = "0.8.4" @@ -555,6 +575,26 @@ dependencies = [ "winapi", ] +[[package]] +name = "linkme" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b53ad6a33de58864705954edb5ad5d571a010f9e296865ed43dc72a5621b430" +dependencies = [ + "linkme-impl", +] + +[[package]] +name = "linkme-impl" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04e542a18c94a9b6fcc7adb090fa3ba6b79ee220a16404f325672729f32a66ff" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + [[package]] name = "linux-raw-sys" version = "0.4.11" diff --git a/Cargo.toml b/Cargo.toml index 834320ce1..67bae205d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,8 @@ resolver = "2" members = [ "conjure_oxide", "crates/conjure_core", + "crates/conjure_rules_proc_macro", + "crates/conjure_rules", "solvers/kissat", "solvers/minion", "solvers/chuffed", @@ -11,3 +13,9 @@ members = [ [workspace.lints.clippy] unwrap_used = "warn" expect_used = "warn" + +[profile.dev] +codegen-units = 1 + +[profile.release] +codegen-units = 1 diff --git a/conjure_oxide/Cargo.toml b/conjure_oxide/Cargo.toml index 7015b3012..70e6a2107 100644 --- a/conjure_oxide/Cargo.toml +++ b/conjure_oxide/Cargo.toml @@ -10,6 +10,7 @@ walkdir = "2.4.0" [dependencies] conjure_core = {path = "../crates/conjure_core" } +conjure_rules = {path = "../crates/conjure_rules" } serde = { version = "1.0.195", features = ["derive"] } serde_json = "1.0.111" @@ -21,6 +22,7 @@ clap = { version = "4.4.16", features = ["derive"] } strum_macros = "0.25.3" strum = "0.25.0" versions = "6.1.0" +linkme = "0.3.22" [lints] workspace = true diff --git a/conjure_oxide/src/main.rs b/conjure_oxide/src/main.rs index 6452d82a9..0699ff379 100644 --- a/conjure_oxide/src/main.rs +++ b/conjure_oxide/src/main.rs @@ -19,6 +19,8 @@ struct Cli { } pub fn main() -> AnyhowResult<()> { + println!("Rules: {:?}", conjure_rules::get_rules()); + let cli = Cli::parse(); println!("Input file: {}", cli.input_file.display()); let input_file: &str = cli.input_file.to_str().ok_or(anyhow!( @@ -48,9 +50,5 @@ pub fn main() -> AnyhowResult<()> { let model = model_from_json(&astjson)?; println!("{:?}", model); - // for rule in get_rules_by_kind() { - // println!("Applying rule {:?}", rule); - // } - Ok(()) } diff --git a/conjure_oxide/src/rules/mod.rs b/conjure_oxide/src/rules/mod.rs index 3911c82ed..4d4702c64 100644 --- a/conjure_oxide/src/rules/mod.rs +++ b/conjure_oxide/src/rules/mod.rs @@ -1,11 +1,7 @@ -use conjure_core::ast::Expression; -use conjure_core::rule::{Rule, RuleApplicationError}; +use conjure_core::{ast::Expression, rule::RuleApplicationError}; +use conjure_rules::register_rule; +#[register_rule] fn identity(expr: &Expression) -> Result { Ok(expr.clone()) } - -pub static IDENTITY_RULE: Rule = Rule { - name: "identity", - application: identity, -}; diff --git a/conjure_oxide/tests/rewrite_tests.rs b/conjure_oxide/tests/rewrite_tests.rs index 13e4a6779..0061c6efe 100644 --- a/conjure_oxide/tests/rewrite_tests.rs +++ b/conjure_oxide/tests/rewrite_tests.rs @@ -4,6 +4,12 @@ use core::panic; use conjure_oxide::ast::*; +#[test] +fn rules_present() { + let rules = conjure_rules::get_rules(); + assert!(rules.len() > 0); +} + #[test] fn sum_of_constants() { let valid_sum_expression = Expression::Sum(vec![ diff --git a/crates/conjure_rules/Cargo.toml b/crates/conjure_rules/Cargo.toml new file mode 100644 index 000000000..563888c59 --- /dev/null +++ b/crates/conjure_rules/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "conjure_rules" +version = "0.1.0" +edition = "2021" + +[dependencies] +conjure_rules_proc_macro = { path = "../conjure_rules_proc_macro" } +conjure_core = { path = "../conjure_core" } +linkme = "0.3.22" + +[lints] +workspace = true diff --git a/crates/conjure_rules/src/lib.rs b/crates/conjure_rules/src/lib.rs new file mode 100644 index 000000000..7ce4b1e87 --- /dev/null +++ b/crates/conjure_rules/src/lib.rs @@ -0,0 +1,101 @@ +//! ### A decentralised rule registry for Conjure Oxide +//! +//! This crate allows registering valid functions as expression-reduction rules. +//! Functions can be decorated with the `register_rule` macro in any downstream crate and be used by Conjure Oxide's rule engine. +//! To achieve compile-time linking, we make use of the [`linkme`](https://docs.rs/linkme/latest/linkme/) crate. +//! + +// Why all the re-exports and wierdness? +// ============================ +// +// Procedural macros are unhygenic - they directly subsitute into source code, and do not have +// their own scope, imports, and so on. +// +// See [https://doc.rust-lang.org/reference/procedural-macros.html#procedural-macro-hygiene]. +// +// Therefore, we cannot assume the user has any dependencies apart from the one they imported the +// macro from. (Also, note Rust does not bring transitive dependencies into scope, so we cannot +// assume the presence of a dependency of the crate.) +// +// To solve this, the crate the macro is in must re-export everything the macro needs to run. +// +// However, proc-macro crates can only export proc-macros. Therefore, we must use a "front end +// crate" (i.e. this one) to re-export both the macro and all the things it may need. + +use conjure_core::rule::Rule; +use linkme::distributed_slice; + +#[doc(hidden)] +pub mod _dependencies { + pub use conjure_core::rule::Rule; + pub use linkme::distributed_slice; +} + +#[doc(hidden)] +#[distributed_slice] +pub static RULES_DISTRIBUTED_SLICE: [Rule<'static>]; + +/// Returns a copied `Vec` of all rules registered with the `register_rule` macro. +/// +/// Rules are not guaranteed to be in any particular order. +/// +/// # Example +/// ```rust +/// # use conjure_rules::register_rule; +/// # use conjure_core::rule::{Rule, RuleApplicationError}; +/// # use conjure_core::ast::Expression; +/// # +/// #[register_rule] +/// fn identity(expr: &Expression) -> Result { +/// Ok(expr.clone()) +/// } +/// +/// fn main() { +/// println!("Rules: {:?}", conjure_rules::get_rules()); +/// } +/// ``` +/// +/// This will print (if no other rules are registered): +/// ```text +/// Rules: [Rule { name: "identity", application: MEM }] +/// ``` +/// Where `MEM` is the memory address of the `identity` function. +pub fn get_rules() -> Vec> { + RULES_DISTRIBUTED_SLICE.to_vec() +} + +/// This procedural macro registers a decorated function with `conjure_rules`' global registry. +/// It may be used in any downstream crate. For more information on linker magic, see the [`linkme`](https://docs.rs/linkme/latest/linkme/) crate. +/// +/// **IMPORTANT**: Since the resulting rule may not be explicitly referenced, it may be removed by the compiler's dead code elimination. +/// To prevent this, you must ensure that either: +/// 1. codegen-units is set to 1, i.e. in Cargo.toml: +/// ```toml +/// [profile.release] +/// codegen-units = 1 +/// ``` +/// 2. The function is included somewhere else in the code +/// +///
+/// +/// Functions must have the signature `fn(&Expr) -> Result`. +/// The created rule will have the same name as the function. +/// +/// Intermediary static variables are created to allow for the decentralized registry, with the prefix `CONJURE_GEN_`. +/// Please ensure that other variable names in the same scope do not conflict with these. +/// +///
+/// +/// For example: +/// ```rust +/// # use conjure_core::ast::Expression; +/// # use conjure_core::rule::RuleApplicationError; +/// # use conjure_rules::register_rule; +/// # +/// #[register_rule] +/// fn identity(expr: &Expression) -> Result { +/// Ok(expr.clone()) +/// } +/// ``` +#[doc(inline)] +pub use conjure_rules_proc_macro::register_rule; diff --git a/crates/conjure_rules_proc_macro/Cargo.toml b/crates/conjure_rules_proc_macro/Cargo.toml new file mode 100644 index 000000000..a5661e5b8 --- /dev/null +++ b/crates/conjure_rules_proc_macro/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "conjure_rules_proc_macro" +version = "0.1.0" +edition = "2021" + +[lib] +proc_macro = true + +[dependencies] +quote = "1.0.34" +conjure_core = {path= "../conjure_core"} +syn = { version = "2.0.43", features = ["full"] } + +[lints] +workspace = true diff --git a/crates/conjure_rules_proc_macro/src/lib.rs b/crates/conjure_rules_proc_macro/src/lib.rs new file mode 100644 index 000000000..610cf1b4d --- /dev/null +++ b/crates/conjure_rules_proc_macro/src/lib.rs @@ -0,0 +1,25 @@ +//! This is the backend procedural macro crate for `conjure_rules`. USE THAT INSTEAD! + +use proc_macro::TokenStream; +use quote::quote; +use syn::{parse_macro_input, Ident, ItemFn}; + +#[proc_macro_attribute] +pub fn register_rule(_: TokenStream, item: TokenStream) -> TokenStream { + let func = parse_macro_input!(item as ItemFn); + let rule_ident = &func.sig.ident; + let static_name = format!("CONJURE_GEN_RULE_{}", rule_ident).to_uppercase(); + let static_ident = Ident::new(&static_name, rule_ident.span()); + + let expanded = quote! { + #func + + #[::conjure_rules::_dependencies::distributed_slice(::conjure_rules::RULES_DISTRIBUTED_SLICE)] + pub static #static_ident: ::conjure_rules::_dependencies::Rule = ::conjure_rules::_dependencies::Rule { + name: stringify!(#rule_ident), + application: #rule_ident, + }; + }; + + TokenStream::from(expanded) +}