Skip to content
Merged
9 changes: 9 additions & 0 deletions crates/bindings-macro/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
//
// (private documentation for the macro authors is totally fine here and you SHOULD write that!)

mod procedure;
mod reducer;
mod sats;
mod table;
Expand Down Expand Up @@ -105,6 +106,14 @@ mod sym {
}
}

#[proc_macro_attribute]
pub fn procedure(args: StdTokenStream, item: StdTokenStream) -> StdTokenStream {
cvt_attr::<ItemFn>(args, item, quote!(), |args, original_function| {
let args = procedure::ProcedureArgs::parse(args)?;
procedure::procedure_impl(args, original_function)
})
}

#[proc_macro_attribute]
pub fn reducer(args: StdTokenStream, item: StdTokenStream) -> StdTokenStream {
cvt_attr::<ItemFn>(args, item, quote!(), |args, original_function| {
Expand Down
130 changes: 130 additions & 0 deletions crates/bindings-macro/src/procedure.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
use crate::reducer::{assert_only_lifetime_generics, extract_typed_args};
use crate::sym;
use crate::util::{check_duplicate, ident_to_litstr, match_meta};
use proc_macro2::TokenStream;
use quote::quote;
use syn::parse::Parser as _;
use syn::{ItemFn, LitStr};

#[derive(Default)]
pub(crate) struct ProcedureArgs {
/// For consistency with reducers: allow specifying a different export name than the Rust function name.
name: Option<LitStr>,
}

impl ProcedureArgs {
pub(crate) fn parse(input: TokenStream) -> syn::Result<Self> {
let mut args = Self::default();
syn::meta::parser(|meta| {
match_meta!(match meta {
sym::name => {
check_duplicate(&args.name, &meta)?;
args.name = Some(meta.value()?.parse()?);
}
});
Ok(())
})
.parse2(input)?;
Ok(args)
}
}

pub(crate) fn procedure_impl(args: ProcedureArgs, original_function: &ItemFn) -> syn::Result<TokenStream> {
let func_name = &original_function.sig.ident;
let vis = &original_function.vis;

let procedure_name = args.name.unwrap_or_else(|| ident_to_litstr(func_name));

assert_only_lifetime_generics(original_function, "procedures")?;

let typed_args = extract_typed_args(original_function)?;

// TODO: Require that procedures be `async` functions syntactically,
// and use `futures_util::FutureExt::now_or_never` to poll them.
// if !&original_function.sig.asyncness.is_some() {
// return Err(syn::Error::new_spanned(
// original_function.sig.clone(),
// "procedures must be `async`",
// ));
// };

// Extract all function parameter names.
let opt_arg_names = typed_args.iter().map(|arg| {
if let syn::Pat::Ident(i) = &*arg.pat {
let name = i.ident.to_string();
quote!(Some(#name))
} else {
quote!(None)
}
});

let arg_tys = typed_args.iter().map(|arg| arg.ty.as_ref()).collect::<Vec<_>>();
let first_arg_ty = arg_tys.first().into_iter();
let rest_arg_tys = arg_tys.iter().skip(1);

// Extract the return type.
let ret_ty_for_assert = match &original_function.sig.output {
syn::ReturnType::Default => None,
syn::ReturnType::Type(_, t) => Some(&**t),
}
.into_iter();

let ret_ty_for_info = match &original_function.sig.output {
syn::ReturnType::Default => quote!(()),
syn::ReturnType::Type(_, t) => quote!(#t),
};

let register_describer_symbol = format!("__preinit__20_register_describer_{}", procedure_name.value());

let lifetime_params = &original_function.sig.generics;
let lifetime_where_clause = &lifetime_params.where_clause;

let generated_describe_function = quote! {
#[export_name = #register_describer_symbol]
pub extern "C" fn __register_describer() {
spacetimedb::rt::register_procedure::<_, _, #func_name>(#func_name)
}
};

Ok(quote! {
const _: () = {
#generated_describe_function
};
#[allow(non_camel_case_types)]
#vis struct #func_name { _never: ::core::convert::Infallible }
const _: () = {
fn _assert_args #lifetime_params () #lifetime_where_clause {
#(let _ = <#first_arg_ty as spacetimedb::rt::ProcedureContextArg>::_ITEM;)*
#(let _ = <#rest_arg_tys as spacetimedb::rt::ProcedureArg>::_ITEM;)*
#(let _ = <#ret_ty_for_assert as spacetimedb::rt::IntoProcedureResult>::to_result;)*
}
};
impl #func_name {
fn invoke(__ctx: spacetimedb::ProcedureContext, __args: &[u8]) -> spacetimedb::ProcedureResult {
spacetimedb::rt::invoke_procedure(#func_name, __ctx, __args)
}
}
#[automatically_derived]
impl spacetimedb::rt::FnInfo for #func_name {
/// The type of this function.
type Invoke = spacetimedb::rt::ProcedureFn;

/// The function kind, which will cause scheduled tables to accept procedures.
type FnKind = spacetimedb::rt::FnKindProcedure<#ret_ty_for_info>;

/// The name of this function
const NAME: &'static str = #procedure_name;

/// The parameter names of this function
const ARG_NAMES: &'static [Option<&'static str>] = &[#(#opt_arg_names),*];

/// The pointer for invoking this function
const INVOKE: spacetimedb::rt::ProcedureFn = #func_name::invoke;

/// The return type of this function
fn return_type(ts: &mut impl spacetimedb::sats::typespace::TypespaceBuilder) -> Option<spacetimedb::sats::AlgebraicType> {
Some(<#ret_ty_for_info as spacetimedb::SpacetimeType>::make_type(ts))
}
}
})
}
47 changes: 33 additions & 14 deletions crates/bindings-macro/src/reducer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use proc_macro2::{Span, TokenStream};
use quote::{quote, quote_spanned};
use syn::parse::Parser as _;
use syn::spanned::Spanned;
use syn::{FnArg, Ident, ItemFn, LitStr};
use syn::{FnArg, Ident, ItemFn, LitStr, PatType};

#[derive(Default)]
pub(crate) struct ReducerArgs {
Expand Down Expand Up @@ -59,33 +59,50 @@ impl ReducerArgs {
}
}

pub(crate) fn reducer_impl(args: ReducerArgs, original_function: &ItemFn) -> syn::Result<TokenStream> {
let func_name = &original_function.sig.ident;
let vis = &original_function.vis;

let reducer_name = args.name.unwrap_or_else(|| ident_to_litstr(func_name));

pub(crate) fn assert_only_lifetime_generics(original_function: &ItemFn, function_kind_plural: &str) -> syn::Result<()> {
for param in &original_function.sig.generics.params {
let err = |msg| syn::Error::new_spanned(param, msg);
match param {
syn::GenericParam::Lifetime(_) => {}
syn::GenericParam::Type(_) => return Err(err("type parameters are not allowed on reducers")),
syn::GenericParam::Const(_) => return Err(err("const parameters are not allowed on reducers")),
syn::GenericParam::Type(_) => {
return Err(err(format!(
"type parameters are not allowed on {function_kind_plural}"
)))
}
syn::GenericParam::Const(_) => {
return Err(err(format!(
"const parameters are not allowed on {function_kind_plural}"
)))
}
}
}
Ok(())
}

let lifecycle = args.lifecycle.iter().filter_map(|lc| lc.to_lifecycle_value());

// Extract all function parameters, except for `self` ones that aren't allowed.
let typed_args = original_function
/// Extract all function parameters, except for `self` ones that aren't allowed.
pub(crate) fn extract_typed_args(original_function: &ItemFn) -> syn::Result<Vec<&PatType>> {
original_function
.sig
.inputs
.iter()
.map(|arg| match arg {
FnArg::Typed(arg) => Ok(arg),
_ => Err(syn::Error::new_spanned(arg, "expected typed argument")),
})
.collect::<syn::Result<Vec<_>>>()?;
.collect()
}

pub(crate) fn reducer_impl(args: ReducerArgs, original_function: &ItemFn) -> syn::Result<TokenStream> {
let func_name = &original_function.sig.ident;
let vis = &original_function.vis;

let reducer_name = args.name.unwrap_or_else(|| ident_to_litstr(func_name));

assert_only_lifetime_generics(original_function, "reducers")?;

let lifecycle = args.lifecycle.iter().filter_map(|lc| lc.to_lifecycle_value());

let typed_args = extract_typed_args(original_function)?;

// Extract all function parameter names.
let opt_arg_names = typed_args.iter().map(|arg| {
Expand Down Expand Up @@ -141,6 +158,8 @@ pub(crate) fn reducer_impl(args: ReducerArgs, original_function: &ItemFn) -> syn
#[automatically_derived]
impl spacetimedb::rt::FnInfo for #func_name {
type Invoke = spacetimedb::rt::ReducerFn;
/// The function kind, which will cause scheduled tables to accept reducers.
type FnKind = spacetimedb::rt::FnKindReducer;
const NAME: &'static str = #reducer_name;
#(const LIFECYCLE: Option<spacetimedb::rt::LifecycleReducer> = Some(#lifecycle);)*
const ARG_NAMES: &'static [Option<&'static str>] = &[#(#opt_arg_names),*];
Expand Down
33 changes: 23 additions & 10 deletions crates/bindings-macro/src/table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ impl TableAccess {

struct ScheduledArg {
span: Span,
reducer: Path,
reducer_or_procedure: Path,
at: Option<Ident>,
}

Expand Down Expand Up @@ -113,7 +113,7 @@ impl TableArgs {
impl ScheduledArg {
fn parse_meta(meta: ParseNestedMeta) -> syn::Result<Self> {
let span = meta.path.span();
let mut reducer = None;
let mut reducer_or_procedure = None;
let mut at = None;

meta.parse_nested_meta(|meta| {
Expand All @@ -126,16 +126,26 @@ impl ScheduledArg {
}
})
} else {
check_duplicate_msg(&reducer, &meta, "can only specify one scheduled reducer")?;
reducer = Some(meta.path);
check_duplicate_msg(
&reducer_or_procedure,
&meta,
"can only specify one scheduled reducer or procedure",
)?;
reducer_or_procedure = Some(meta.path);
}
Ok(())
})?;

let reducer = reducer.ok_or_else(|| {
meta.error("must specify scheduled reducer associated with the table: scheduled(reducer_name)")
let reducer_or_procedure = reducer_or_procedure.ok_or_else(|| {
meta.error(
"must specify scheduled reducer or procedure associated with the table: scheduled(function_name)",
)
})?;
Ok(Self { span, reducer, at })
Ok(Self {
span,
reducer_or_procedure,
at,
})
}
}

Expand Down Expand Up @@ -818,17 +828,20 @@ pub(crate) fn table_impl(mut args: TableArgs, item: &syn::DeriveInput) -> syn::R
)
})?;

let reducer = &sched.reducer;
let reducer_or_procedure = &sched.reducer_or_procedure;
let scheduled_at_id = scheduled_at_column.index;
let desc = quote!(spacetimedb::table::ScheduleDesc {
reducer_name: <#reducer as spacetimedb::rt::FnInfo>::NAME,
reducer_or_procedure_name: <#reducer_or_procedure as spacetimedb::rt::FnInfo>::NAME,
scheduled_at_column: #scheduled_at_id,
});

let primary_key_ty = primary_key_column.ty;
let scheduled_at_ty = scheduled_at_column.ty;
let typecheck = quote! {
spacetimedb::rt::scheduled_reducer_typecheck::<#original_struct_ident>(#reducer);
spacetimedb::rt::scheduled_typecheck::<
#original_struct_ident,
<#reducer_or_procedure as spacetimedb::rt::FnInfo>::FnKind,
>(#reducer_or_procedure);
spacetimedb::rt::assert_scheduled_table_primary_key::<#primary_key_ty>();
let _ = |x: #scheduled_at_ty| { let _: spacetimedb::ScheduleAt = x; };
};
Expand Down
3 changes: 3 additions & 0 deletions crates/bindings-macro/src/view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,9 @@ pub(crate) fn view_impl(_args: ViewArgs, original_function: &ItemFn) -> syn::Res
/// The type of this function
type Invoke = <spacetimedb::rt::ViewKind<#ctx_ty> as spacetimedb::rt::ViewKindTrait>::InvokeFn;

/// The function kind, which will cause scheduled tables to reject views.
type FnKind = spacetimedb::rt::FnKindView;

/// The name of this function
const NAME: &'static str = #view_name;

Expand Down
8 changes: 8 additions & 0 deletions crates/bindings-sys/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1218,3 +1218,11 @@ impl Drop for RowIter {
}
}
}

pub mod procedure {
//! Side-effecting or asynchronous operations which only procedures are allowed to perform.
#[inline]
pub fn sleep_until(_wake_at_timestamp: i64) -> i64 {
todo!("Add `procedure_sleep_until` host function")
}
}
Loading
Loading