ascent_macro/
lib.rs

1#![allow(clippy::useless_format, clippy::redundant_static_lifetimes, clippy::get_first)]
2#![cfg_attr(not(test), deny(unused_crate_dependencies))]
3mod tests;
4mod ascent_mir;
5mod utils;
6mod ascent_hir;
7mod scratchpad;
8mod ascent_codegen;
9mod ascent_syntax;
10mod test_errors;
11mod syn_utils;
12
13#[macro_use]
14extern crate quote;
15
16extern crate proc_macro;
17use ascent_syntax::{AscentProgram, desugar_ascent_program, parse_ascent_program};
18use derive_syn_parse::Parse;
19use itertools::Either;
20use proc_macro::TokenStream;
21use proc_macro2::Span;
22use syn::parse::{ParseStream, Parser};
23use syn::spanned::Spanned;
24use syn::{Attribute, Ident, Result, Token, parse_quote, parse_quote_spanned};
25use syn_utils::ResTokenStream2Ext;
26
27use crate::ascent_codegen::compile_mir;
28use crate::ascent_hir::compile_ascent_program_to_hir;
29use crate::ascent_mir::compile_hir_to_mir;
30
31/// The main macro of the ascent library. Allows writing logical inference rules similar to Datalog.
32///
33/// Example:
34/// ```
35/// # #[macro_use] extern crate ascent_macro;
36/// # use ascent::ascent;
37/// ascent!{
38///   relation edge(i32, i32);
39///   relation path(i32, i32);
40///   
41///   path(x, y) <-- edge(x,y);
42///   path(x, z) <-- edge(x,y), path(y, z);
43/// }
44///
45/// fn main() {
46///   let mut tc_comp = AscentProgram::default();
47///   tc_comp.edge = vec![(1,2), (2,3)];
48///   tc_comp.run();
49///   println!("{:?}", tc_comp.path);
50/// }
51/// ```
52/// this macro creates a type named `AscentProgram` that can be instantiated using `AscentProgram::default()`.
53/// The type has a `run()` method, which runs the computation to a fixed point.
54#[proc_macro]
55pub fn ascent(input: TokenStream) -> TokenStream {
56   ascent_impl(input.into(), AscentMacroKind { is_ascent_run: false, is_parallel: false }).into_token_stream()
57}
58
59/// Similar to `ascent`, allows writing logic programs in Rust.
60///
61/// The difference is that `ascent_par` generates parallelized code.
62#[proc_macro]
63pub fn ascent_par(input: TokenStream) -> TokenStream {
64   ascent_impl(input.into(), AscentMacroKind { is_ascent_run: false, is_parallel: true }).into_token_stream()
65}
66
67/// Like `ascent`, except that the result of an `ascent_run` invocation is a value containing all the relations
68/// defined inside the macro body, and computed to a fixed point.
69///
70/// The advantage of `ascent_run` compared to `ascent` is the fact that `ascent_run` has access to local variables
71/// in scope:
72/// ```
73/// # #[macro_use] extern crate ascent;
74/// # use ascent::ascent_run;
75/// let r = vec![(1,2), (2,3)];
76/// let r_tc = ascent_run!{
77///    relation tc(i32, i32);
78///    tc(x, y) <-- for (x, y) in r.iter();
79///    tc(x, z) <-- for (x, y) in r.iter(), tc(y, z);
80/// }.tc;
81///
82/// ```
83#[proc_macro]
84pub fn ascent_run(input: TokenStream) -> TokenStream {
85   ascent_impl(input.into(), AscentMacroKind { is_ascent_run: true, is_parallel: false }).into_token_stream()
86}
87
88/// The parallelized version of `ascent_run`
89#[proc_macro]
90pub fn ascent_run_par(input: TokenStream) -> TokenStream {
91   ascent_impl(input.into(), AscentMacroKind { is_ascent_run: true, is_parallel: true }).into_token_stream()
92}
93
94/// This macro allows writing Ascent code that can later be included in an actual Ascent program.
95///
96/// In an Ascent program, you can include the contents of an `ascent_source` using the `include_source!(path);`
97/// syntax. It will look like the following:
98/// ```
99/// # #[macro_use] extern crate ascent;
100/// mod my_ascent_sources {
101///   ascent_source! { secret_sauce:
102///     // secret Ascent code ...
103///   }
104/// }
105///
106/// // somewhere else, we define an actual Ascent program:
107/// ascent! {
108///   include_source!(my_ascent_sources::secret_sauce);
109///   // More Ascent code ...
110/// }
111/// ```
112///
113/// # Be warned!
114/// This feature comes with all the caveats of C's `#include`, or Rust's `include!()` macro:
115/// when `include_source!` is used, it is as if the contents of the included `ascent_source` was copy-pasted
116/// into the containing Ascent program. This means every type, function, etc. will be resolved relative to
117/// the containing Ascent program, and **not** relative to where the included `ascent_source` is defined.
118///
119/// # Example
120/// ```
121/// # #[macro_use] extern crate ascent;
122/// mod base {
123///    ascent::ascent_source! {
124///       /// Defines `edge` and `path`, the transitive closure of `edge`
125///       tc:
126///       relation edge(usize, usize);
127///       relation path(usize, usize);
128///       path(x, y) <-- edge(x, y);
129///       path(x, z) <-- edge(x, y), path(y, z);
130///    }
131/// }
132///
133/// ascent! {
134///    struct Tc;
135///    include_source!(base::tc);
136/// }
137///
138/// ascent! {
139///    struct Rtc;
140///    include_source!(base::tc);
141///    path(x, x), path(y, y) <-- edge(x, y);  
142/// }
143///
144/// ascent_par! {
145///    struct ParallelTc;
146///    include_source!(base::tc);
147/// }
148/// ```
149#[proc_macro]
150pub fn ascent_source(input: TokenStream) -> TokenStream { ascent_source_impl(input.into()).into_token_stream() }
151
152fn ascent_source_impl(input: proc_macro2::TokenStream) -> Result<proc_macro2::TokenStream> {
153   #[derive(Parse)]
154   struct AscentSourceInput {
155      #[call(Attribute::parse_outer)]
156      attrs: Vec<Attribute>,
157      name: Ident,
158      colon: Token![:],
159      ascent_code: proc_macro2::TokenStream,
160   }
161
162   let AscentSourceInput { attrs, name, colon, ascent_code } = syn::parse2(input.into())?;
163
164   match Parser::parse2(
165      |input: ParseStream| parse_ascent_program(input, parse_quote!(::ascent::ascent_source)),
166      ascent_code.clone(),
167   )? {
168      itertools::Either::Left(_prog) => (),
169      itertools::Either::Right(mut include_source_call) => {
170         let before_tokens = include_source_call.before_tokens;
171         include_source_call.before_tokens = quote! {
172            #(#attrs)*
173            #name #colon
174            #before_tokens
175         };
176         // This allows transitive `include_source`s. Disabled for now ...
177         // return Ok(include_source_call.macro_call_output())
178         return Err(syn::Error::new(
179            include_source_call.include_node.include_source_kw.span,
180            "`ascent_source`s cannot contain `include_source!`",
181         ))
182      },
183   };
184
185   if let Some(bad_attr) = attrs.iter().find(|attr| attr.path().get_ident().map_or(true, |ident| ident != "doc")) {
186      return Err(syn::Error::new(bad_attr.span(), "unexpected attribute. Only `doc` attribute is allowed"))
187   }
188
189   let macro_name = Ident::new(&format!("ascent_source_{name}"), name.span());
190
191   Ok(quote! {
192      #(#attrs)*
193      #[macro_export]
194      macro_rules! #macro_name {
195         ({$($cb: tt)*}, {$($before: tt)*}, {$($after: tt)*}) => {
196            $($cb)*! {
197               $($before)*
198               #ascent_code
199               $($after)*
200            }
201         }
202      }
203      pub use #macro_name as #name;
204   })
205}
206
207#[derive(Clone, Copy, Default)]
208pub(crate) struct AscentMacroKind {
209   pub is_ascent_run: bool,
210   pub is_parallel: bool,
211}
212
213impl AscentMacroKind {
214   pub fn name(&self) -> &'static str {
215      match (self.is_ascent_run, self.is_parallel) {
216         (false, false) => "ascent",
217         (false, true) => "ascent_par",
218         (true, false) => "ascent_run",
219         (true, true) => "ascent_run_par",
220      }
221   }
222
223   pub fn macro_path(&self, span: Span) -> syn::Path {
224      let name_ident = Ident::new(self.name(), span);
225      parse_quote_spanned! {span=>
226         ::ascent::#name_ident
227      }
228   }
229}
230
231pub(crate) fn ascent_impl(input: proc_macro2::TokenStream, kind: AscentMacroKind) -> Result<proc_macro2::TokenStream> {
232   let AscentMacroKind { is_ascent_run, is_parallel } = kind;
233   let prog = match Parser::parse2(
234      |input: ParseStream| parse_ascent_program(input, kind.macro_path(Span::call_site())),
235      input,
236   )? {
237      Either::Left(prog) => prog,
238      Either::Right(include_source_call) => return Ok(include_source_call.macro_call_output()),
239   };
240
241   let prog = desugar_ascent_program(prog)?;
242
243   let hir = compile_ascent_program_to_hir(&prog, is_parallel)?;
244
245   let mir = compile_hir_to_mir(&hir)?;
246
247   let code = compile_mir(&mir, is_ascent_run);
248
249   Ok(code)
250}