1use std::collections::{BTreeMap, BTreeSet};
2
3use proc_macro::TokenStream;
4use proc_macro2::{Ident, Span, TokenStream as TokenStream2};
5use quote::{format_ident, quote};
6use sha2::{Digest, Sha256};
7use syn::{
8 parse::Parser, parse_macro_input, punctuated::Punctuated, spanned::Spanned, Attribute, Data,
9 DeriveInput, Expr, ExprLit, Fields, FnArg, GenericArgument, Item, ItemFn, ItemMod, Lit, Meta,
10 PathArguments, ReturnType, Token, Type,
11};
12
13#[proc_macro_derive(RadixType, attributes(radix_type, radix_field))]
14pub fn derive_radix_type(input: TokenStream) -> TokenStream {
15 match expand_radix_type(parse_macro_input!(input as DeriveInput)) {
16 Ok(tokens) => tokens.into(),
17 Err(error) => error.into_compile_error().into(),
18 }
19}
20
21#[proc_macro_attribute]
22pub fn radixdb_plugin(attribute: TokenStream, item: TokenStream) -> TokenStream {
23 let args = match parse_options(attribute.into()) {
24 Ok(args) => args,
25 Err(error) => return error.into_compile_error().into(),
26 };
27 match expand_plugin(args, parse_macro_input!(item as ItemMod)) {
28 Ok(tokens) => tokens.into(),
29 Err(error) => error.into_compile_error().into(),
30 }
31}
32
33macro_rules! passthrough_attribute {
34 ($name:ident) => {
35 #[proc_macro_attribute]
36 pub fn $name(_attribute: TokenStream, item: TokenStream) -> TokenStream {
37 item
38 }
39 };
40}
41
42passthrough_attribute!(radixdb_scalar);
43passthrough_attribute!(radixdb_batch);
44passthrough_attribute!(radixdb_operator);
45passthrough_attribute!(radixdb_operator_class);
46passthrough_attribute!(radixdb_planner_support);
47
48macro_rules! rejected_attribute {
49 ($name:ident, $kind:literal) => {
50 #[proc_macro_attribute]
51 pub fn $name(_attribute: TokenStream, _item: TokenStream) -> TokenStream {
52 syn::Error::new(
53 Span::call_site(),
54 concat!(
55 $kind,
56 " plugin functions are outside the RadixDB 1.2.25 authoring scope"
57 ),
58 )
59 .into_compile_error()
60 .into()
61 }
62 };
63}
64
65rejected_attribute!(radixdb_aggregate, "aggregate");
66rejected_attribute!(radixdb_window, "window");
67rejected_attribute!(radixdb_tvf, "table-valued");
68
69#[derive(Default, Clone)]
70struct Options {
71 values: BTreeMap<String, Expr>,
72 flags: BTreeSet<String>,
73}
74
75impl Options {
76 fn required_string(&self, name: &str, span: Span) -> syn::Result<String> {
77 self.string(name)?
78 .ok_or_else(|| syn::Error::new(span, format!("missing required `{name}`")))
79 }
80
81 fn string(&self, name: &str) -> syn::Result<Option<String>> {
82 self.values
83 .get(name)
84 .map(|value| match value {
85 Expr::Lit(ExprLit {
86 lit: Lit::Str(value),
87 ..
88 }) => Ok(value.value()),
89 _ => Err(syn::Error::new(value.span(), "expected string literal")),
90 })
91 .transpose()
92 }
93
94 fn required_u32(&self, name: &str, span: Span) -> syn::Result<u32> {
95 self.u32(name)?
96 .ok_or_else(|| syn::Error::new(span, format!("missing required `{name}`")))
97 }
98
99 fn u32(&self, name: &str) -> syn::Result<Option<u32>> {
100 self.values
101 .get(name)
102 .map(|value| match value {
103 Expr::Lit(ExprLit {
104 lit: Lit::Int(value),
105 ..
106 }) => value.base10_parse(),
107 _ => Err(syn::Error::new(value.span(), "expected integer literal")),
108 })
109 .transpose()
110 }
111
112 fn path(&self, name: &str) -> syn::Result<Option<syn::Path>> {
113 self.values
114 .get(name)
115 .map(|value| match value {
116 Expr::Path(value) => Ok(value.path.clone()),
117 _ => Err(syn::Error::new(value.span(), "expected callback path")),
118 })
119 .transpose()
120 }
121
122 fn reject_unknown(&self, allowed_values: &[&str], allowed_flags: &[&str]) -> syn::Result<()> {
123 for name in self.values.keys() {
124 if !allowed_values.contains(&name.as_str()) {
125 return Err(syn::Error::new(
126 Span::call_site(),
127 format!("unknown RadixDB attribute option `{name}`"),
128 ));
129 }
130 }
131 for name in &self.flags {
132 if !allowed_flags.contains(&name.as_str()) {
133 return Err(syn::Error::new(
134 Span::call_site(),
135 format!("unknown RadixDB attribute flag `{name}`"),
136 ));
137 }
138 }
139 Ok(())
140 }
141}
142
143fn parse_options(tokens: TokenStream2) -> syn::Result<Options> {
144 let metas = Punctuated::<Meta, Token![,]>::parse_terminated.parse2(tokens)?;
145 let mut result = Options::default();
146 for meta in metas {
147 match meta {
148 Meta::Path(path) => {
149 let name = single_ident(&path)?;
150 if !result.flags.insert(name.clone()) {
151 return Err(syn::Error::new(path.span(), format!("duplicate `{name}`")));
152 }
153 }
154 Meta::NameValue(value) => {
155 let name = single_ident(&value.path)?;
156 if result.values.insert(name.clone(), value.value).is_some() {
157 return Err(syn::Error::new(
158 value.path.span(),
159 format!("duplicate `{name}`"),
160 ));
161 }
162 }
163 Meta::List(list) => {
164 return Err(syn::Error::new(
165 list.span(),
166 "nested RadixDB attribute options are not supported",
167 ));
168 }
169 }
170 }
171 Ok(result)
172}
173
174fn options_from_attribute(attribute: &Attribute) -> syn::Result<Options> {
175 let Meta::List(list) = &attribute.meta else {
176 return Err(syn::Error::new(
177 attribute.span(),
178 "expected attribute arguments",
179 ));
180 };
181 parse_options(list.tokens.clone())
182}
183
184fn single_ident(path: &syn::Path) -> syn::Result<String> {
185 if path.segments.len() != 1 {
186 return Err(syn::Error::new(
187 path.span(),
188 "expected a single option name",
189 ));
190 }
191 Ok(path.segments[0].ident.to_string())
192}
193
194fn attribute<'a>(attributes: &'a [Attribute], name: &str) -> Option<&'a Attribute> {
195 attributes
196 .iter()
197 .find(|attribute| attribute.path().is_ident(name))
198}
199
200fn expand_radix_type(input: DeriveInput) -> syn::Result<TokenStream2> {
201 let ident = &input.ident;
202 if !input.generics.params.is_empty() {
203 return Err(syn::Error::new(
204 input.generics.span(),
205 "RadixType does not support generic external types",
206 ));
207 }
208 let type_attribute = attribute(&input.attrs, "radix_type")
209 .ok_or_else(|| syn::Error::new(input.ident.span(), "missing #[radix_type(...)]"))?;
210 let options = options_from_attribute(type_attribute)?;
211 options.reject_unknown(
212 &[
213 "id",
214 "name",
215 "codec",
216 "semantic_revision",
217 "storage",
218 "max_bytes",
219 "equality",
220 "hash",
221 "ordering",
222 "manual",
223 ],
224 &[],
225 )?;
226 let local_id = options.required_string("id", input.ident.span())?;
227 let display_name = options.required_string("name", input.ident.span())?;
228 validate_local_id(&local_id, input.ident.span())?;
229 validate_sql_name(&display_name, input.ident.span())?;
230 let codec_version = options.required_u32("codec", input.ident.span())?;
231 let semantic_revision = options.required_u32("semantic_revision", input.ident.span())?;
232 if codec_version == 0 || semantic_revision == 0 {
233 return Err(syn::Error::new(
234 input.ident.span(),
235 "codec and semantic_revision must be non-zero",
236 ));
237 }
238 let storage = options.required_string("storage", input.ident.span())?;
239 let max_bytes = options.required_u32("max_bytes", input.ident.span())?;
240 if max_bytes == 0 || max_bytes > 16 * 1024 * 1024 {
241 return Err(syn::Error::new(
242 input.ident.span(),
243 "max_bytes must be in 1..=16777216",
244 ));
245 }
246 let storage_kind = match storage.as_str() {
247 "fixed" => quote!(::radixdb_plugin::__private::abi::RADIX_EXTERNAL_STORAGE_FIXED),
248 "variable" => quote!(::radixdb_plugin::__private::abi::RADIX_EXTERNAL_STORAGE_VARIABLE),
249 _ => {
250 return Err(syn::Error::new(
251 input.ident.span(),
252 "storage must be \"fixed\" or \"variable\"",
253 ));
254 }
255 };
256 let fixed_bytes = if storage == "fixed" { max_bytes } else { 0 };
257 let equality = options.path("equality")?;
258 let hash = options.path("hash")?;
259 let ordering = options.path("ordering")?;
260 if hash.is_some() && equality.is_none() {
261 return Err(syn::Error::new(
262 input.ident.span(),
263 "hash callback requires an explicit equality callback",
264 ));
265 }
266 let manual = options.path("manual")?;
267
268 let Data::Struct(data) = &input.data else {
269 return Err(syn::Error::new(
270 input.ident.span(),
271 "RadixType requires a struct",
272 ));
273 };
274 let Fields::Named(fields) = &data.fields else {
275 return Err(syn::Error::new(
276 data.fields.span(),
277 "RadixType requires named fields",
278 ));
279 };
280
281 let (encode_body, decode_body, corpus_body, schema, inferred_fixed_bytes) = if let Some(codec) =
282 manual
283 {
284 if fields
285 .named
286 .iter()
287 .any(|field| attribute(&field.attrs, "radix_field").is_some())
288 {
289 return Err(syn::Error::new(
290 fields.span(),
291 "manual codec types must not declare radix_field codecs",
292 ));
293 }
294 (
295 quote!(<#codec as ::radixdb_plugin::ManualCodec<Self>>::encode(self, output)),
296 quote!(<#codec as ::radixdb_plugin::ManualCodec<Self>>::decode(input)),
297 quote!(<#codec as ::radixdb_plugin::ManualCodec<Self>>::corpus()),
298 format!("manual:{}", quote!(#codec)),
299 None,
300 )
301 } else {
302 let mut encode = Vec::new();
303 let mut decode = Vec::new();
304 let mut names = Vec::new();
305 let mut edge_extensions = Vec::new();
306 let mut schema_parts = Vec::new();
307 let mut encoded_width = Some(0_u32);
308 for field in &fields.named {
309 let field_ident = field.ident.as_ref().expect("named field");
310 let field_type = &field.ty;
311 names.push(field_ident.clone());
312 let field_attribute = attribute(&field.attrs, "radix_field").ok_or_else(|| {
313 syn::Error::new(
314 field_ident.span(),
315 "every derived field requires #[radix_field(...)]",
316 )
317 })?;
318 let field_options = options_from_attribute(field_attribute)?;
319 field_options.reject_unknown(&["codec", "max_items", "max_bytes"], &[])?;
320 let codec = field_options.required_string("codec", field_ident.span())?;
321 let generated = generate_field_codec(
322 field_ident,
323 &field.ty,
324 &codec,
325 &field_options,
326 field_ident.span(),
327 )?;
328 encode.push(generated.encode);
329 decode.push(generated.decode);
330 edge_extensions.push(generated.edge);
331 encoded_width = match (encoded_width, generated.fixed_width) {
332 (Some(total), Some(width)) => Some(total.checked_add(width).ok_or_else(|| {
333 syn::Error::new(field_ident.span(), "derived fixed width exceeds u32")
334 })?),
335 _ => None,
336 };
337 schema_parts.push(format!(
338 "{}:{}:{}",
339 field_ident,
340 quote!(#field_type),
341 generated.schema
342 ));
343 }
344 let defaults = names.iter().zip(fields.named.iter()).map(|(name, field)| {
345 let ty = &field.ty;
346 quote!(#name: <#ty as ::std::default::Default>::default())
347 });
348 (
349 quote!({ #(#encode)* Ok(()) }),
350 quote!({ #(let #names = #decode;)* Ok(Self { #(#names),* }) }),
351 quote!({
352 let mut corpus = vec![Self { #(#defaults),* }];
353 #(#edge_extensions)*
354 corpus
355 }),
356 schema_parts.join(";"),
357 encoded_width,
358 )
359 };
360 if storage == "fixed" && inferred_fixed_bytes.is_some_and(|encoded| encoded != max_bytes) {
361 return Err(syn::Error::new(
362 input.ident.span(),
363 format!(
364 "fixed type max_bytes must equal derived encoded width {}",
365 inferred_fixed_bytes.unwrap()
366 ),
367 ));
368 }
369
370 let mut capabilities = 0u64;
371 if equality.is_some() {
372 capabilities |= 1 << 0;
373 }
374 if hash.is_some() {
375 capabilities |= 1 << 1;
376 }
377 if ordering.is_some() {
378 capabilities |= 1 << 2;
379 }
380 let fingerprint = digest32(format!(
385 "radixdb.type.codec.v1\0{id}\0{codec_version}\0{storage}\0{max_bytes}\0{schema}",
386 id = local_id,
387 ));
388 let fingerprint_tokens = bytes_tokens(&fingerprint);
389 let encode_wrapper =
390 format_ident!("__radixdb_type_{}_encode", ident.to_string().to_lowercase());
391 let decode_wrapper =
392 format_ident!("__radixdb_type_{}_decode", ident.to_string().to_lowercase());
393 let equal_wrapper = format_ident!("__radixdb_type_{}_equal", ident.to_string().to_lowercase());
394 let hash_wrapper = format_ident!("__radixdb_type_{}_hash", ident.to_string().to_lowercase());
395 let compare_wrapper = format_ident!(
396 "__radixdb_type_{}_compare",
397 ident.to_string().to_lowercase()
398 );
399
400 let equality_impl = if let Some(callback) = &equality {
401 quote! {
402 fn semantic_equal(left: &Self, right: &Self) -> Option<bool> {
403 let callback: fn(&Self, &Self) -> bool = #callback;
404 Some(callback(left, right))
405 }
406 }
407 } else {
408 quote!()
409 };
410 let hash_impl = if let Some(callback) = &hash {
411 quote! {
412 fn semantic_hash(
413 value: &Self,
414 sink: &mut ::radixdb_plugin::HashSink<'_>,
415 ) -> Option<::radixdb_plugin::PluginResult<()>> {
416 let callback: fn(&Self, &mut ::radixdb_plugin::HashSink<'_>)
417 -> ::radixdb_plugin::PluginResult<()> = #callback;
418 Some(callback(value, sink))
419 }
420 }
421 } else {
422 quote!()
423 };
424 let ordering_impl = if let Some(callback) = &ordering {
425 quote! {
426 fn semantic_compare(left: &Self, right: &Self) -> Option<::std::cmp::Ordering> {
427 let callback: fn(&Self, &Self) -> ::std::cmp::Ordering = #callback;
428 Some(callback(left, right))
429 }
430 }
431 } else {
432 quote!()
433 };
434 let equal_items = if equality.is_some() {
435 quote! {
436 unsafe extern "C" fn #equal_wrapper(
437 context: *const ::radixdb_plugin::__private::abi::RadixAbiCallContextV1,
438 left: *const ::radixdb_plugin::__private::abi::RadixAbiValueV1,
439 right: *const ::radixdb_plugin::__private::abi::RadixAbiValueV1,
440 output: *mut u8,
441 ) -> u32 {
442 unsafe { ::radixdb_plugin::__private::run_equal::<#ident>(context, left, right, output) }
443 }
444 }
445 } else {
446 quote!()
447 };
448 let hash_items = if hash.is_some() {
449 quote! {
450 unsafe extern "C" fn #hash_wrapper(
451 context: *const ::radixdb_plugin::__private::abi::RadixAbiCallContextV1,
452 value: *const ::radixdb_plugin::__private::abi::RadixAbiValueV1,
453 sink: *const ::radixdb_plugin::__private::abi::RadixAbiHashSinkV1,
454 ) -> u32 {
455 unsafe { ::radixdb_plugin::__private::run_hash::<#ident>(context, value, sink) }
456 }
457 }
458 } else {
459 quote!()
460 };
461 let ordering_items = if ordering.is_some() {
462 quote! {
463 unsafe extern "C" fn #compare_wrapper(
464 context: *const ::radixdb_plugin::__private::abi::RadixAbiCallContextV1,
465 left: *const ::radixdb_plugin::__private::abi::RadixAbiValueV1,
466 right: *const ::radixdb_plugin::__private::abi::RadixAbiValueV1,
467 output: *mut i8,
468 ) -> u32 {
469 unsafe { ::radixdb_plugin::__private::run_compare::<#ident>(context, left, right, output) }
470 }
471 }
472 } else {
473 quote!()
474 };
475 let equal_const = equality
476 .as_ref()
477 .map(|_| quote!(Some(#equal_wrapper)))
478 .unwrap_or(quote!(None));
479 let hash_const = hash
480 .as_ref()
481 .map(|_| quote!(Some(#hash_wrapper)))
482 .unwrap_or(quote!(None));
483 let ordering_const = ordering
484 .as_ref()
485 .map(|_| quote!(Some(#compare_wrapper)))
486 .unwrap_or(quote!(None));
487
488 Ok(quote! {
489 unsafe extern "C" fn #encode_wrapper(
490 context: *const ::radixdb_plugin::__private::abi::RadixAbiCallContextV1,
491 input: *const ::radixdb_plugin::__private::abi::RadixAbiValueV1,
492 output: *const ::radixdb_plugin::__private::abi::RadixAbiResultBuilderV1,
493 ) -> u32 {
494 unsafe { ::radixdb_plugin::__private::run_codec_encode::<#ident>(context, input, output) }
495 }
496
497 unsafe extern "C" fn #decode_wrapper(
498 context: *const ::radixdb_plugin::__private::abi::RadixAbiCallContextV1,
499 input: ::radixdb_plugin::__private::abi::RadixAbiSliceV1,
500 output: *const ::radixdb_plugin::__private::abi::RadixAbiResultBuilderV1,
501 ) -> u32 {
502 unsafe { ::radixdb_plugin::__private::run_codec_decode::<#ident>(context, input, output) }
503 }
504
505 #equal_items
506 #hash_items
507 #ordering_items
508
509 impl ::radixdb_plugin::RadixType for #ident {
510 const LOCAL_ID: &'static str = #local_id;
511 const DISPLAY_NAME: &'static str = #display_name;
512 const CODEC_VERSION: u32 = #codec_version;
513 const SEMANTIC_REVISION: u32 = #semantic_revision;
514 const STORAGE_KIND: u16 = #storage_kind;
515 const FIXED_BYTES: u32 = #fixed_bytes;
516 const MAX_BYTES: u32 = #max_bytes;
517 const CAPABILITIES: u64 = #capabilities;
518 const CODEC_FINGERPRINT: [u8; 32] = [#(#fingerprint_tokens),*];
519 const ABI_ENCODE: Option<::radixdb_plugin::__private::abi::RadixAbiCodecFnV1> =
520 Some(#encode_wrapper);
521 const ABI_DECODE: Option<::radixdb_plugin::__private::abi::RadixAbiParseFnV1> =
522 Some(#decode_wrapper);
523 const ABI_EQUALITY: Option<::radixdb_plugin::__private::abi::RadixAbiEqualFnV1> =
524 #equal_const;
525 const ABI_HASH: Option<::radixdb_plugin::__private::abi::RadixAbiHashFnV1> =
526 #hash_const;
527 const ABI_ORDERING: Option<::radixdb_plugin::__private::abi::RadixAbiCompareFnV1> =
528 #ordering_const;
529
530 fn encode(&self, output: &mut ::radixdb_plugin::CodecWriter)
531 -> ::radixdb_plugin::PluginResult<()>
532 {
533 #encode_body
534 }
535
536 fn decode(input: &mut ::radixdb_plugin::CodecReader<'_>)
537 -> ::radixdb_plugin::PluginResult<Self>
538 {
539 #decode_body
540 }
541
542 #[allow(clippy::needless_update)]
543 fn test_corpus() -> Vec<Self> {
544 #corpus_body
545 }
546
547 #equality_impl
548 #hash_impl
549 #ordering_impl
550 }
551 })
552}
553
554struct GeneratedField {
555 encode: TokenStream2,
556 decode: TokenStream2,
557 edge: TokenStream2,
558 schema: String,
559 fixed_width: Option<u32>,
560}
561
562fn generate_field_codec(
563 field: &Ident,
564 ty: &Type,
565 codec: &str,
566 options: &Options,
567 span: Span,
568) -> syn::Result<GeneratedField> {
569 if let Some(inner) = vec_inner(ty) {
570 let max_items = options
571 .u32("max_items")?
572 .ok_or_else(|| syn::Error::new(span, "bounded sequence requires max_items"))?;
573 let max_bytes = options
574 .u32("max_bytes")?
575 .ok_or_else(|| syn::Error::new(span, "bounded sequence requires max_bytes"))?;
576 validate_primitive_codec(inner, codec, span)?;
577 return Ok(GeneratedField {
578 encode: quote! {
579 ::radixdb_plugin::__private::encode_sequence(
580 &self.#field,
581 #max_items as usize,
582 #max_bytes as usize,
583 output,
584 )?;
585 },
586 decode: quote!(::radixdb_plugin::__private::decode_sequence::<#inner>(
587 input,
588 #max_items as usize,
589 #max_bytes as usize,
590 )?),
591 edge: quote! {
592 for value in <#inner as ::radixdb_plugin::__private::CanonicalField>::edge_values() {
593 corpus.push(Self {
594 #field: vec![value.clone()],
595 ..Self::default()
596 });
597 corpus.push(Self {
598 #field: vec![value; #max_items as usize],
599 ..Self::default()
600 });
601 }
602 },
603 schema: format!("{codec}[max_items={max_items},max_bytes={max_bytes}]"),
604 fixed_width: None,
605 });
606 }
607 if options.values.contains_key("max_items") || options.values.contains_key("max_bytes") {
608 return Err(syn::Error::new(
609 span,
610 "max_items/max_bytes are valid only for bounded sequences",
611 ));
612 }
613 if let Type::Array(array) = ty {
614 validate_primitive_codec(&array.elem, codec, span)?;
615 let length = match &array.len {
616 Expr::Lit(ExprLit {
617 lit: Lit::Int(length),
618 ..
619 }) => length.base10_parse::<u32>()?,
620 _ => {
621 return Err(syn::Error::new(
622 array.len.span(),
623 "fixed array length must be an integer literal",
624 ));
625 }
626 };
627 let element_width = primitive_codec_width(&array.elem).ok_or_else(|| {
628 syn::Error::new(array.elem.span(), "unsupported fixed-array element type")
629 })?;
630 return Ok(GeneratedField {
631 encode: quote!(::radixdb_plugin::__private::CanonicalField::encode_field(
632 &self.#field,
633 output,
634 )?;),
635 decode: quote!(<#ty as ::radixdb_plugin::__private::CanonicalField>::decode_field(input)?),
636 edge: quote! {
637 for value in <#ty as ::radixdb_plugin::__private::CanonicalField>::edge_values() {
638 corpus.push(Self {
639 #field: value,
640 ..Self::default()
641 });
642 }
643 },
644 schema: format!("{codec}[{}]", quote!(#array.len)),
645 fixed_width: Some(length.checked_mul(element_width).ok_or_else(|| {
646 syn::Error::new(array.len.span(), "fixed array width exceeds u32")
647 })?),
648 });
649 }
650 validate_primitive_codec(ty, codec, span)?;
651 Ok(GeneratedField {
652 encode: quote!(::radixdb_plugin::__private::CanonicalField::encode_field(
653 &self.#field,
654 output,
655 )?;),
656 decode: quote!(<#ty as ::radixdb_plugin::__private::CanonicalField>::decode_field(input)?),
657 edge: quote! {
658 for value in <#ty as ::radixdb_plugin::__private::CanonicalField>::edge_values() {
659 corpus.push(Self {
660 #field: value,
661 ..Self::default()
662 });
663 }
664 },
665 schema: codec.to_string(),
666 fixed_width: primitive_codec_width(ty),
667 })
668}
669
670fn primitive_codec_width(ty: &Type) -> Option<u32> {
671 match terminal_type_ident(ty)?.as_str() {
672 "i8" | "u8" | "bool" => Some(1),
673 "i16" | "u16" => Some(2),
674 "i32" | "u32" | "f32" => Some(4),
675 "i64" | "u64" | "f64" => Some(8),
676 _ => None,
677 }
678}
679
680fn validate_primitive_codec(ty: &Type, codec: &str, span: Span) -> syn::Result<()> {
681 let expected = match terminal_type_ident(ty).as_deref() {
682 Some("i8") => "i8-le",
683 Some("i16") => "i16-le",
684 Some("i32") => "i32-le",
685 Some("i64") => "i64-le",
686 Some("u8") => "u8-le",
687 Some("u16") => "u16-le",
688 Some("u32") => "u32-le",
689 Some("u64") => "u64-le",
690 Some("f32") => "f32-le",
691 Some("f64") => "f64-le",
692 Some("bool") => "bool-u8",
693 _ => {
694 return Err(syn::Error::new(
695 span,
696 "unsupported field type; use a primitive, fixed array, bounded Vec, or manual codec",
697 ));
698 }
699 };
700 if codec != expected {
701 return Err(syn::Error::new(
702 span,
703 format!("field type requires codec \"{expected}\""),
704 ));
705 }
706 Ok(())
707}
708
709fn expand_plugin(args: Options, mut module: ItemMod) -> syn::Result<TokenStream2> {
710 args.reject_unknown(&["id", "name", "version"], &[])?;
711 let package_id = args.required_string("id", module.ident.span())?;
712 let package_name = args.required_string("name", module.ident.span())?;
713 let package_version = args.required_string("version", module.ident.span())?;
714 let uuid = uuid::Uuid::parse_str(&package_id)
715 .map_err(|_| syn::Error::new(module.ident.span(), "plugin id must be a canonical UUID"))?;
716 if uuid.to_string() != package_id {
717 return Err(syn::Error::new(
718 module.ident.span(),
719 "plugin id must use canonical lowercase UUID spelling",
720 ));
721 }
722 validate_package_name(&package_name, module.ident.span())?;
723 let version = semver::Version::parse(&package_version)
724 .map_err(|_| syn::Error::new(module.ident.span(), "plugin version must be SemVer"))?;
725 if version.to_string() != package_version || !version.build.is_empty() {
726 return Err(syn::Error::new(
727 module.ident.span(),
728 "plugin version must be canonical SemVer without build metadata",
729 ));
730 }
731 let Some((_, items)) = &mut module.content else {
732 return Err(syn::Error::new(
733 module.ident.span(),
734 "radixdb_plugin requires an inline module",
735 ));
736 };
737
738 let mut type_specs = Vec::new();
739 let mut scalar_specs = Vec::new();
740 let mut batch_specs = BTreeMap::new();
741 let mut operator_specs = Vec::new();
742 let mut opclass_specs = Vec::new();
743 let mut planner_specs = Vec::new();
744 let mut local_ids = BTreeSet::new();
745 for item in items.iter() {
746 match item {
747 Item::Struct(item) => {
748 if let Some(attribute) = attribute(&item.attrs, "radix_type") {
749 let options = options_from_attribute(attribute)?;
750 let local_id = options.required_string("id", item.span())?;
751 admit_local_id(&mut local_ids, &local_id, item.span())?;
752 type_specs.push((item.ident.clone(), local_id, options));
753 }
754 }
755 Item::Fn(function) => {
756 if let Some(attribute) = attribute(&function.attrs, "radixdb_scalar") {
757 let options = options_from_attribute(attribute)?;
758 let local_id = options.required_string("id", function.sig.ident.span())?;
759 admit_local_id(&mut local_ids, &local_id, function.sig.ident.span())?;
760 scalar_specs.push((function.clone(), local_id, options));
761 }
762 if let Some(attribute) = attribute(&function.attrs, "radixdb_batch") {
763 let options = options_from_attribute(attribute)?;
764 let scalar =
765 options.required_string("for_scalar", function.sig.ident.span())?;
766 if batch_specs
767 .insert(scalar.clone(), (function.clone(), options))
768 .is_some()
769 {
770 return Err(syn::Error::new(
771 function.sig.ident.span(),
772 format!("duplicate batch adapter for `{scalar}`"),
773 ));
774 }
775 }
776 if let Some(attribute) = attribute(&function.attrs, "radixdb_operator") {
777 let options = options_from_attribute(attribute)?;
778 let local_id = options.required_string("id", function.sig.ident.span())?;
779 admit_local_id(&mut local_ids, &local_id, function.sig.ident.span())?;
780 operator_specs.push((function.clone(), local_id, options));
781 }
782 if let Some(attribute) = attribute(&function.attrs, "radixdb_operator_class") {
783 let options = options_from_attribute(attribute)?;
784 let local_id = options.required_string("id", function.sig.ident.span())?;
785 admit_local_id(&mut local_ids, &local_id, function.sig.ident.span())?;
786 opclass_specs.push((function.clone(), local_id, options));
787 }
788 if let Some(attribute) = attribute(&function.attrs, "radixdb_planner_support") {
789 let options = options_from_attribute(attribute)?;
790 let local_id = options.required_string("id", function.sig.ident.span())?;
791 admit_local_id(&mut local_ids, &local_id, function.sig.ident.span())?;
792 planner_specs.push((function.clone(), local_id, options));
793 }
794 }
795 _ => {}
796 }
797 }
798
799 let package_uuid = *uuid.as_bytes();
800 let type_map: BTreeMap<String, ([u8; 16], u32)> = type_specs
801 .iter()
802 .map(|(ident, local_id, options)| {
803 Ok((
804 ident.to_string(),
805 (
806 object_id(package_uuid, local_id),
807 options.required_u32("codec", ident.span())?,
808 ),
809 ))
810 })
811 .collect::<syn::Result<_>>()?;
812 let mut type_names = BTreeSet::new();
813 for (ident, _, options) in &type_specs {
814 let name = options.required_string("name", ident.span())?;
815 if !type_names.insert(name.clone()) {
816 return Err(syn::Error::new(
817 ident.span(),
818 format!("duplicate external type SQL name `{name}`"),
819 ));
820 }
821 }
822 let mut overloads = BTreeSet::new();
823 for (function, _, options) in &scalar_specs {
824 let name = options.required_string("name", function.sig.ident.span())?;
825 let arguments = function_arguments(function)?;
826 let signature = format!(
827 "{}({})",
828 name,
829 arguments
830 .iter()
831 .map(|(_, ty)| quote!(#ty).to_string())
832 .collect::<Vec<_>>()
833 .join(",")
834 );
835 if !overloads.insert(signature.clone()) {
836 return Err(syn::Error::new(
837 function.sig.ident.span(),
838 format!("duplicate scalar overload `{signature}`"),
839 ));
840 }
841 }
842 let mut operator_overloads = BTreeSet::new();
843 for (function, _, options) in &operator_specs {
844 let symbol = options.required_string("symbol", function.sig.ident.span())?;
845 let left = parse_type_option(options, "left", function.sig.ident.span())?;
846 let right = parse_type_option(options, "right", function.sig.ident.span())?;
847 let signature = format!("{}({},{})", symbol, quote!(#left), quote!(#right));
848 if !operator_overloads.insert(signature.clone()) {
849 return Err(syn::Error::new(
850 function.sig.ident.span(),
851 format!("duplicate operator overload `{signature}`"),
852 ));
853 }
854 }
855 let function_ids: BTreeMap<String, [u8; 16]> = scalar_specs
856 .iter()
857 .map(|(_, local_id, _)| (local_id.clone(), object_id(package_uuid, local_id)))
858 .chain(scalar_specs.iter().map(|(function, local_id, _)| {
859 (
860 function.sig.ident.to_string(),
861 object_id(package_uuid, local_id),
862 )
863 }))
864 .collect();
865 let operator_ids: BTreeMap<(String, String, String), [u8; 16]> = operator_specs
866 .iter()
867 .map(|(function, local_id, options)| {
868 let symbol = options.required_string("symbol", function.sig.ident.span())?;
869 let left = parse_type_option(options, "left", function.sig.ident.span())?;
870 let right = parse_type_option(options, "right", function.sig.ident.span())?;
871 Ok((
872 (
873 symbol,
874 quote!(#left).to_string(),
875 quote!(#right).to_string(),
876 ),
877 object_id(package_uuid, local_id),
878 ))
879 })
880 .collect::<syn::Result<_>>()?;
881 let opclass_ids: BTreeMap<String, [u8; 16]> = opclass_specs
882 .iter()
883 .map(|(function, local_id, _)| {
884 (
885 function.sig.ident.to_string(),
886 object_id(package_uuid, local_id),
887 )
888 })
889 .chain(
890 opclass_specs
891 .iter()
892 .map(|(_, local_id, _)| (local_id.clone(), object_id(package_uuid, local_id))),
893 )
894 .collect();
895
896 let type_descriptors = type_specs
897 .iter()
898 .map(|(ident, local_id, _)| generate_type_descriptor(ident, package_uuid, local_id));
899 let has_batch = !batch_specs.is_empty();
900 let mut generated_functions = Vec::new();
901 let mut function_descriptors = Vec::new();
902 for (function, local_id, options) in &scalar_specs {
903 let batch = batch_specs
904 .remove(local_id)
905 .or_else(|| batch_specs.remove(&function.sig.ident.to_string()));
906 let generated = generate_scalar_descriptor(
907 function,
908 local_id,
909 options,
910 batch.as_ref(),
911 package_uuid,
912 &type_map,
913 )?;
914 generated_functions.push(generated.items);
915 function_descriptors.push(generated.descriptor);
916 }
917 if let Some((name, (function, _))) = batch_specs.into_iter().next() {
918 return Err(syn::Error::new(
919 function.sig.ident.span(),
920 format!("batch adapter references unknown scalar `{name}`"),
921 ));
922 }
923 let operator_descriptors = operator_specs
924 .iter()
925 .map(|(function, local_id, options)| {
926 generate_operator_descriptor(
927 function,
928 local_id,
929 options,
930 package_uuid,
931 &type_map,
932 &function_ids,
933 )
934 })
935 .collect::<syn::Result<Vec<_>>>()?;
936 let mut opclass_items = Vec::new();
937 let mut opclass_descriptors = Vec::new();
938 for (function, local_id, options) in &opclass_specs {
939 let generated = generate_opclass_descriptor(
940 function,
941 local_id,
942 options,
943 package_uuid,
944 &type_map,
945 &operator_ids,
946 )?;
947 opclass_items.push(generated.items);
948 opclass_descriptors.push(generated.descriptor);
949 }
950 let mut planner_items = Vec::new();
951 let mut planner_descriptors = Vec::new();
952 for (function, local_id, options) in &planner_specs {
953 let generated = generate_planner_descriptor(
954 function,
955 local_id,
956 options,
957 package_uuid,
958 &function_ids,
959 &opclass_ids,
960 )?;
961 planner_items.push(generated.items);
962 planner_descriptors.push(generated.descriptor);
963 }
964
965 let package_caps: u64 = (if !type_specs.is_empty() { 1 } else { 0 })
966 | (if !scalar_specs.is_empty() { 1 << 1 } else { 0 })
967 | (if has_batch { 1 << 2 } else { 0 })
968 | (if !operator_specs.is_empty() {
969 1 << 3
970 } else {
971 0
972 })
973 | (if !opclass_specs.is_empty() { 1 << 4 } else { 0 })
974 | (if !planner_specs.is_empty() { 1 << 5 } else { 0 });
975
976 let source_contract = quote!(#(#items)*).to_string();
977 let descriptor_fingerprint = digest32(format!(
978 "radixdb.package.v1\0{}\0{}\0{}\0{}",
979 package_id,
980 package_name,
981 local_ids.iter().cloned().collect::<Vec<_>>().join("\0"),
982 source_contract,
983 ));
984 let fingerprint_tokens = bytes_tokens(&descriptor_fingerprint);
985 let package_id_tokens = bytes_tokens(&package_uuid);
986 let type_count = type_specs.len() as u32;
987 let function_count = scalar_specs.len() as u32;
988 let operator_count = operator_specs.len() as u32;
989 let opclass_count = opclass_specs.len() as u32;
990 let planner_count = planner_specs.len() as u32;
991
992 let generated = quote! {
993 #(#generated_functions)*
994 #(#opclass_items)*
995 #(#planner_items)*
996
997 static __RADIXDB_TYPES: [::radixdb_plugin::__private::abi::RadixAbiExternalTypeDescriptorV1; #type_count as usize] = [
998 #(#type_descriptors),*
999 ];
1000 static __RADIXDB_FUNCTIONS: [::radixdb_plugin::__private::abi::RadixAbiScalarFunctionDescriptorV1; #function_count as usize] = [
1001 #(#function_descriptors),*
1002 ];
1003 static __RADIXDB_OPERATORS: [::radixdb_plugin::__private::abi::RadixAbiOperatorDescriptorV1; #operator_count as usize] = [
1004 #(#operator_descriptors),*
1005 ];
1006 static __RADIXDB_OPERATOR_CLASSES: [::radixdb_plugin::__private::abi::RadixAbiOperatorClassDescriptorV1; #opclass_count as usize] = [
1007 #(#opclass_descriptors),*
1008 ];
1009 static __RADIXDB_PLANNER_SUPPORT: [::radixdb_plugin::__private::abi::RadixAbiPlannerSupportDescriptorV1; #planner_count as usize] = [
1010 #(#planner_descriptors),*
1011 ];
1012 static __RADIXDB_PACKAGE: ::radixdb_plugin::__private::abi::RadixPluginDescriptorV1 =
1013 ::radixdb_plugin::__private::abi::RadixPluginDescriptorV1 {
1014 header: ::radixdb_plugin::__private::abi::RadixAbiHeaderV1::new::<
1015 ::radixdb_plugin::__private::abi::RadixPluginDescriptorV1
1016 >(#package_caps),
1017 package_id: [#(#package_id_tokens),*],
1018 package_name: ::radixdb_plugin::__private::abi::RadixAbiSliceV1::from_static(#package_name.as_bytes()),
1019 package_version: ::radixdb_plugin::__private::abi::RadixAbiSliceV1::from_static(#package_version.as_bytes()),
1020 abi_min_minor: ::radixdb_plugin::__private::abi::RADIX_ABI_MINOR,
1021 abi_max_minor: ::radixdb_plugin::__private::abi::RADIX_ABI_MINOR,
1022 reserved: 0,
1023 descriptor_fingerprint: [#(#fingerprint_tokens),*],
1024 type_count: #type_count,
1025 reserved_types: 0,
1026 types: __RADIXDB_TYPES.as_ptr(),
1027 function_count: #function_count,
1028 reserved_functions: 0,
1029 functions: __RADIXDB_FUNCTIONS.as_ptr(),
1030 operator_count: #operator_count,
1031 reserved_operators: 0,
1032 operators: __RADIXDB_OPERATORS.as_ptr(),
1033 operator_class_count: #opclass_count,
1034 reserved_operator_classes: 0,
1035 operator_classes: __RADIXDB_OPERATOR_CLASSES.as_ptr(),
1036 planner_support_count: #planner_count,
1037 reserved_planner_support: 0,
1038 planner_support: __RADIXDB_PLANNER_SUPPORT.as_ptr(),
1039 };
1040
1041 #[doc(hidden)]
1042 pub fn __radixdb_descriptor() -> &'static ::radixdb_plugin::__private::abi::RadixPluginDescriptorV1 {
1043 &__RADIXDB_PACKAGE
1044 }
1045 };
1046 let original_items = items.clone();
1047 let attrs = &module.attrs;
1048 let visibility = &module.vis;
1049 let module_ident = &module.ident;
1050 Ok(quote! {
1051 #(#attrs)* #visibility mod #module_ident {
1052 #(#original_items)*
1053 #generated
1054 }
1055
1056 #[unsafe(no_mangle)]
1057 pub unsafe extern "C" fn radixdb_plugin_entry_v1(
1058 host: *const ::radixdb_plugin::__private::abi::RadixHostApiV1,
1059 status: *mut u32,
1060 ) -> *const ::radixdb_plugin::__private::abi::RadixPluginDescriptorV1 {
1061 let outcome = ::std::panic::catch_unwind(|| {
1062 let host = unsafe { host.as_ref() }.ok_or(())?;
1063 if host.header.abi_major != ::radixdb_plugin::__private::abi::RADIX_ABI_MAJOR
1064 || host.header.abi_minor < ::radixdb_plugin::__private::abi::RADIX_ABI_MINOR
1065 {
1066 return Err(());
1067 }
1068 Ok(#module_ident::__radixdb_descriptor() as *const _)
1069 });
1070 match outcome {
1071 Ok(Ok(descriptor)) => {
1072 if let Some(status) = unsafe { status.as_mut() } {
1073 *status = ::radixdb_plugin::__private::abi::RADIX_STATUS_OK;
1074 }
1075 descriptor
1076 }
1077 Ok(Err(())) => {
1078 if let Some(status) = unsafe { status.as_mut() } {
1079 *status = ::radixdb_plugin::__private::abi::RADIX_STATUS_UNSUPPORTED_ABI;
1080 }
1081 ::std::ptr::null()
1082 }
1083 Err(_) => {
1084 if let Some(status) = unsafe { status.as_mut() } {
1085 *status = ::radixdb_plugin::__private::abi::RADIX_STATUS_PANIC;
1086 }
1087 ::std::ptr::null()
1088 }
1089 }
1090 }
1091 })
1092}
1093
1094fn generate_type_descriptor(ident: &Ident, package_id: [u8; 16], local_id: &str) -> TokenStream2 {
1095 let object = bytes_tokens(&object_id(package_id, local_id));
1096 quote! {
1097 ::radixdb_plugin::__private::abi::RadixAbiExternalTypeDescriptorV1 {
1098 header: ::radixdb_plugin::__private::abi::RadixAbiHeaderV1::new::<
1099 ::radixdb_plugin::__private::abi::RadixAbiExternalTypeDescriptorV1
1100 >(0),
1101 object_id: [#(#object),*],
1102 local_id: ::radixdb_plugin::__private::abi::RadixAbiSliceV1::from_static(
1103 <#ident as ::radixdb_plugin::RadixType>::LOCAL_ID.as_bytes()
1104 ),
1105 display_name: ::radixdb_plugin::__private::abi::RadixAbiSliceV1::from_static(
1106 <#ident as ::radixdb_plugin::RadixType>::DISPLAY_NAME.as_bytes()
1107 ),
1108 codec_version: <#ident as ::radixdb_plugin::RadixType>::CODEC_VERSION,
1109 semantic_revision: <#ident as ::radixdb_plugin::RadixType>::SEMANTIC_REVISION,
1110 storage_kind: <#ident as ::radixdb_plugin::RadixType>::STORAGE_KIND,
1111 reserved_u16: 0,
1112 fixed_bytes: <#ident as ::radixdb_plugin::RadixType>::FIXED_BYTES,
1113 max_bytes: <#ident as ::radixdb_plugin::RadixType>::MAX_BYTES,
1114 reserved_u32: 0,
1115 capabilities: <#ident as ::radixdb_plugin::RadixType>::CAPABILITIES,
1116 codec_fingerprint: <#ident as ::radixdb_plugin::RadixType>::CODEC_FINGERPRINT,
1117 encode: <#ident as ::radixdb_plugin::RadixType>::ABI_ENCODE,
1118 decode: <#ident as ::radixdb_plugin::RadixType>::ABI_DECODE,
1119 equality: <#ident as ::radixdb_plugin::RadixType>::ABI_EQUALITY,
1120 hash: <#ident as ::radixdb_plugin::RadixType>::ABI_HASH,
1121 ordering: <#ident as ::radixdb_plugin::RadixType>::ABI_ORDERING,
1122 text_input: None,
1123 text_output: None,
1124 binary_input: None,
1125 binary_output: None,
1126 }
1127 }
1128}
1129
1130struct GeneratedDescriptor {
1131 items: TokenStream2,
1132 descriptor: TokenStream2,
1133}
1134
1135fn generate_scalar_descriptor(
1136 function: &ItemFn,
1137 local_id: &str,
1138 options: &Options,
1139 batch: Option<&(ItemFn, Options)>,
1140 package_id: [u8; 16],
1141 type_map: &BTreeMap<String, ([u8; 16], u32)>,
1142) -> syn::Result<GeneratedDescriptor> {
1143 options.reject_unknown(
1144 &[
1145 "id",
1146 "name",
1147 "semantic_revision",
1148 "cost",
1149 "cancellation",
1150 "max_output_bytes",
1151 ],
1152 &["immutable", "stable", "volatile", "strict", "parallel_safe"],
1153 )?;
1154 let name = options.required_string("name", function.sig.ident.span())?;
1155 validate_sql_name(&name, function.sig.ident.span())?;
1156 let semantic_revision = options.required_u32("semantic_revision", function.sig.ident.span())?;
1157 let cost = options.required_u32("cost", function.sig.ident.span())?;
1158 if semantic_revision == 0 || cost == 0 {
1159 return Err(syn::Error::new(
1160 function.sig.ident.span(),
1161 "semantic_revision and cost must be non-zero",
1162 ));
1163 }
1164 let volatility_flags = ["immutable", "stable", "volatile"]
1165 .into_iter()
1166 .filter(|flag| options.flags.contains(*flag))
1167 .collect::<Vec<_>>();
1168 if volatility_flags.len() != 1 {
1169 return Err(syn::Error::new(
1170 function.sig.ident.span(),
1171 "scalar requires exactly one of immutable, stable, volatile",
1172 ));
1173 }
1174 let volatility = match volatility_flags[0] {
1175 "immutable" => quote!(::radixdb_plugin::__private::abi::RADIX_VOLATILITY_IMMUTABLE),
1176 "stable" => quote!(::radixdb_plugin::__private::abi::RADIX_VOLATILITY_STABLE),
1177 _ => quote!(::radixdb_plugin::__private::abi::RADIX_VOLATILITY_VOLATILE),
1178 };
1179 let cancellation = options.required_string("cancellation", function.sig.ident.span())?;
1180 if cancellation != "bounded" {
1181 return Err(syn::Error::new(
1182 function.sig.ident.span(),
1183 "RadixDB 1.2.25 scalar cancellation must be \"bounded\"",
1184 ));
1185 }
1186 let arguments = function_arguments(function)?;
1187 let result = plugin_result_type(&function.sig.output)?;
1188 let argument_refs = arguments
1189 .iter()
1190 .map(|(_, ty)| type_ref_tokens(ty, type_map))
1191 .collect::<syn::Result<Vec<_>>>()?;
1192 let result_ref = type_ref_tokens(&result, type_map)?;
1193 let argument_count = arguments.len() as u32;
1194 let function_ident = &function.sig.ident;
1195 let wrapper = format_ident!("__radixdb_scalar_{}", function_ident);
1196 let argument_static = format_ident!(
1197 "__RADIXDB_ARGS_{}",
1198 function_ident.to_string().to_uppercase()
1199 );
1200 let decoded = arguments.iter().enumerate().map(|(index, (ident, ty))| {
1201 quote!(let #ident: #ty = <#ty as ::radixdb_plugin::ValueType>::decode_abi(&__arguments[#index])?;)
1202 });
1203 let call_arguments = arguments.iter().map(|(ident, _)| ident);
1204 let strict = options.flags.contains("strict");
1205 let parallel_safe = options.flags.contains("parallel_safe");
1206 let max_output = options
1207 .u32("max_output_bytes")?
1208 .map(|value| quote!(#value))
1209 .unwrap_or_else(|| quote!(<#result as ::radixdb_plugin::ValueType>::MAX_OUTPUT_BYTES));
1210
1211 let (batch_items, batch_pointer) = if let Some((batch, batch_options)) = batch {
1212 let generated = generate_batch_wrapper(batch, batch_options, argument_count, &result)?;
1213 let wrapper = generated.wrapper;
1214 (generated.items, quote!(Some(#wrapper)))
1215 } else {
1216 (quote!(), quote!(None))
1217 };
1218 let object = bytes_tokens(&object_id(package_id, local_id));
1219 Ok(GeneratedDescriptor {
1220 items: quote! {
1221 static #argument_static: [::radixdb_plugin::__private::abi::RadixAbiTypeRefV1; #argument_count as usize] = [
1222 #(#argument_refs),*
1223 ];
1224 unsafe extern "C" fn #wrapper(
1225 context: *const ::radixdb_plugin::__private::abi::RadixAbiCallContextV1,
1226 arguments: *const ::radixdb_plugin::__private::abi::RadixAbiValueV1,
1227 argument_count: u32,
1228 output: *const ::radixdb_plugin::__private::abi::RadixAbiResultBuilderV1,
1229 ) -> u32 {
1230 unsafe {
1231 ::radixdb_plugin::__private::run_scalar(
1232 context,
1233 arguments,
1234 argument_count,
1235 output,
1236 #argument_count,
1237 #strict,
1238 |_context, __arguments, __output| {
1239 #(#decoded)*
1240 let __result: #result = #function_ident(#(#call_arguments),*)?;
1241 __output.push(__result)
1242 },
1243 )
1244 }
1245 }
1246 #batch_items
1247 },
1248 descriptor: quote! {
1249 ::radixdb_plugin::__private::abi::RadixAbiScalarFunctionDescriptorV1 {
1250 header: ::radixdb_plugin::__private::abi::RadixAbiHeaderV1::new::<
1251 ::radixdb_plugin::__private::abi::RadixAbiScalarFunctionDescriptorV1
1252 >(0),
1253 object_id: [#(#object),*],
1254 local_id: ::radixdb_plugin::__private::abi::RadixAbiSliceV1::from_static(#local_id.as_bytes()),
1255 display_name: ::radixdb_plugin::__private::abi::RadixAbiSliceV1::from_static(#name.as_bytes()),
1256 semantic_revision: #semantic_revision,
1257 argument_count: #argument_count,
1258 arguments: #argument_static.as_ptr(),
1259 result: #result_ref,
1260 volatility: #volatility,
1261 cancellation: ::radixdb_plugin::__private::abi::RADIX_CANCELLATION_BOUNDED,
1262 strict: #strict as u8,
1263 parallel_safe: #parallel_safe as u8,
1264 reserved_u16: 0,
1265 cost: #cost,
1266 max_output_bytes: #max_output,
1267 scalar: Some(#wrapper),
1268 batch: #batch_pointer,
1269 }
1270 },
1271 })
1272}
1273
1274struct GeneratedBatch {
1275 items: TokenStream2,
1276 wrapper: Ident,
1277}
1278
1279fn generate_batch_wrapper(
1280 function: &ItemFn,
1281 options: &Options,
1282 expected_columns: u32,
1283 result: &Type,
1284) -> syn::Result<GeneratedBatch> {
1285 options.reject_unknown(&["for_scalar", "rows_per_cancel_check"], &[])?;
1286 let rows = options.required_u32("rows_per_cancel_check", function.sig.ident.span())?;
1287 if rows == 0 {
1288 return Err(syn::Error::new(
1289 function.sig.ident.span(),
1290 "rows_per_cancel_check must be non-zero",
1291 ));
1292 }
1293 let inputs = function
1294 .sig
1295 .inputs
1296 .iter()
1297 .take(expected_columns as usize)
1298 .enumerate()
1299 .map(|(index, argument)| {
1300 let FnArg::Typed(argument) = argument else {
1301 return Err(syn::Error::new(
1302 argument.span(),
1303 "methods are not supported",
1304 ));
1305 };
1306 let inner = generic_inner(&argument.ty, "ColumnView").ok_or_else(|| {
1307 syn::Error::new(argument.ty.span(), "batch inputs must be ColumnView<'_, T>")
1308 })?;
1309 let name = format_ident!("__column_{index}");
1310 Ok((name, inner))
1311 })
1312 .collect::<syn::Result<Vec<_>>>()?;
1313 if function.sig.inputs.len() != expected_columns as usize + 2 {
1314 return Err(syn::Error::new(
1315 function.sig.inputs.span(),
1316 "batch signature must contain scalar columns, output builder, and call context",
1317 ));
1318 }
1319 let function_ident = &function.sig.ident;
1320 let wrapper = format_ident!("__radixdb_batch_{}", function_ident);
1321 let columns = inputs
1322 .iter()
1323 .enumerate()
1324 .map(|(index, (name, ty))| quote!(let #name = __input.column::<#ty>(#index)?;));
1325 let names = inputs.iter().map(|(name, _)| name);
1326 Ok(GeneratedBatch {
1327 items: quote! {
1328 unsafe extern "C" fn #wrapper(
1329 context: *const ::radixdb_plugin::__private::abi::RadixAbiCallContextV1,
1330 input: *const ::radixdb_plugin::__private::abi::RadixAbiBatchViewV1,
1331 output: *const ::radixdb_plugin::__private::abi::RadixAbiResultBuilderV1,
1332 ) -> u32 {
1333 unsafe {
1334 ::radixdb_plugin::__private::run_batch(
1335 context,
1336 input,
1337 output,
1338 #expected_columns,
1339 |__context, __input, __raw_output| {
1340 #(#columns)*
1341 let mut __output = ::radixdb_plugin::__private::column_builder::<#result>(__raw_output);
1342 #function_ident(#(#names),*, &mut __output, __context)
1343 },
1344 )
1345 }
1346 }
1347 },
1348 wrapper,
1349 })
1350}
1351
1352fn generate_operator_descriptor(
1353 function: &ItemFn,
1354 local_id: &str,
1355 options: &Options,
1356 package_id: [u8; 16],
1357 type_map: &BTreeMap<String, ([u8; 16], u32)>,
1358 function_ids: &BTreeMap<String, [u8; 16]>,
1359) -> syn::Result<TokenStream2> {
1360 options.reject_unknown(
1361 &[
1362 "id",
1363 "symbol",
1364 "semantic_revision",
1365 "function",
1366 "left",
1367 "right",
1368 "result",
1369 ],
1370 &[],
1371 )?;
1372 let symbol = options.required_string("symbol", function.sig.ident.span())?;
1373 let semantic_revision = options.required_u32("semantic_revision", function.sig.ident.span())?;
1374 if semantic_revision == 0 {
1375 return Err(syn::Error::new(
1376 function.sig.ident.span(),
1377 "semantic_revision must be non-zero",
1378 ));
1379 }
1380 let target = options.required_string("function", function.sig.ident.span())?;
1381 let function_id = function_ids.get(&target).ok_or_else(|| {
1382 syn::Error::new(
1383 function.sig.ident.span(),
1384 format!("unknown scalar function `{target}`"),
1385 )
1386 })?;
1387 let left = parse_type_option(options, "left", function.sig.ident.span())?;
1388 let right = parse_type_option(options, "right", function.sig.ident.span())?;
1389 let result = parse_type_option(options, "result", function.sig.ident.span())?;
1390 let left_ref = type_ref_tokens(&left, type_map)?;
1391 let right_ref = type_ref_tokens(&right, type_map)?;
1392 let result_ref = type_ref_tokens(&result, type_map)?;
1393 let object = bytes_tokens(&object_id(package_id, local_id));
1394 let function_object = bytes_tokens(function_id);
1395 Ok(quote! {
1396 ::radixdb_plugin::__private::abi::RadixAbiOperatorDescriptorV1 {
1397 header: ::radixdb_plugin::__private::abi::RadixAbiHeaderV1::new::<
1398 ::radixdb_plugin::__private::abi::RadixAbiOperatorDescriptorV1
1399 >(0),
1400 object_id: [#(#object),*],
1401 local_id: ::radixdb_plugin::__private::abi::RadixAbiSliceV1::from_static(#local_id.as_bytes()),
1402 symbol: ::radixdb_plugin::__private::abi::RadixAbiSliceV1::from_static(#symbol.as_bytes()),
1403 semantic_revision: #semantic_revision,
1404 reserved: 0,
1405 left: #left_ref,
1406 right: #right_ref,
1407 result: #result_ref,
1408 function_id: [#(#function_object),*],
1409 }
1410 })
1411}
1412
1413fn generate_opclass_descriptor(
1414 function: &ItemFn,
1415 local_id: &str,
1416 options: &Options,
1417 package_id: [u8; 16],
1418 type_map: &BTreeMap<String, ([u8; 16], u32)>,
1419 operator_ids: &BTreeMap<(String, String, String), [u8; 16]>,
1420) -> syn::Result<GeneratedDescriptor> {
1421 options.reject_unknown(
1422 &[
1423 "id",
1424 "semantic_revision",
1425 "access_method",
1426 "input",
1427 "key",
1428 "key_codec_revision",
1429 ],
1430 &[],
1431 )?;
1432 let semantic_revision = options.required_u32("semantic_revision", function.sig.ident.span())?;
1433 let key_codec_revision =
1434 options.required_u32("key_codec_revision", function.sig.ident.span())?;
1435 if semantic_revision == 0 || key_codec_revision == 0 {
1436 return Err(syn::Error::new(
1437 function.sig.ident.span(),
1438 "semantic_revision and key_codec_revision must be non-zero",
1439 ));
1440 }
1441 let method_name = options.required_string("access_method", function.sig.ident.span())?;
1442 let (method, required_strategies): (TokenStream2, &[(u16, &str)]) = match method_name.as_str() {
1443 "btree" => (
1444 quote!(::radixdb_plugin::__private::abi::RADIX_ACCESS_METHOD_BTREE),
1445 &[(1, "<"), (2, "<="), (3, "="), (4, ">="), (5, ">")],
1446 ),
1447 "hash" => (
1448 quote!(::radixdb_plugin::__private::abi::RADIX_ACCESS_METHOD_HASH),
1449 &[(1, "=")],
1450 ),
1451 "bitmap" => (
1452 quote!(::radixdb_plugin::__private::abi::RADIX_ACCESS_METHOD_BITMAP),
1453 &[(1, "=")],
1454 ),
1455 "hnsw" => {
1456 return Err(syn::Error::new(
1457 function.sig.ident.span(),
1458 "external HNSW operator classes require planner support outside v1.2.25",
1459 ));
1460 }
1461 _ => {
1462 return Err(syn::Error::new(
1463 function.sig.ident.span(),
1464 "operator class access_method must be core-owned btree/hash/bitmap/hnsw",
1465 ));
1466 }
1467 };
1468 let input = parse_type_option(options, "input", function.sig.ident.span())?;
1469 let key = parse_type_option(options, "key", function.sig.ident.span())?;
1470 let input_ref = type_ref_tokens(&input, type_map)?;
1471 let key_ref = type_ref_tokens(&key, type_map)?;
1472 let function_ident = &function.sig.ident;
1473 let wrapper = format_ident!("__radixdb_key_{}", function_ident);
1474 let law_test = format_ident!("__radixdb_operator_class_laws_{}", function_ident);
1475 let strategy_table = format_ident!(
1476 "__RADIXDB_STRATEGIES_{}",
1477 function_ident.to_string().to_uppercase()
1478 );
1479 let input_key = quote!(#input).to_string();
1480 let strategies = required_strategies
1481 .iter()
1482 .map(|(slot, symbol)| {
1483 let key = ((*symbol).to_owned(), input_key.clone(), input_key.clone());
1484 let object_id = operator_ids.get(&key).ok_or_else(|| {
1485 syn::Error::new(
1486 function.sig.ident.span(),
1487 format!("{method_name} operator class requires `{symbol}` over `{input_key}`"),
1488 )
1489 })?;
1490 let object_id = bytes_tokens(object_id);
1491 Ok(quote! {
1492 ::radixdb_plugin::__private::abi::RadixAbiBindingEntryV1 {
1493 slot: #slot,
1494 flags: 0,
1495 object_id: [#(#object_id),*],
1496 }
1497 })
1498 })
1499 .collect::<syn::Result<Vec<_>>>()?;
1500 let strategy_count = strategies.len() as u32;
1501 let object = bytes_tokens(&object_id(package_id, local_id));
1502 let fingerprint = bytes_tokens(&digest32(format!(
1503 "radixdb.opclass.v1\0{local_id}\0{semantic_revision}\0{key_codec_revision}\0{}",
1504 quote!(#function)
1505 )));
1506 let law_check = match method_name.as_str() {
1507 "btree" => quote!(
1508 ::radixdb_plugin::testing::check_btree_operator_class::<#input, #key>(#function_ident)
1509 ),
1510 "hash" => quote!(
1511 ::radixdb_plugin::testing::check_hash_operator_class::<#input>()
1512 ),
1513 "bitmap" => quote!(
1514 ::radixdb_plugin::testing::check_bitmap_operator_class::<#input, #key>(#function_ident)
1515 ),
1516 "hnsw" => quote!(Ok(())),
1517 _ => unreachable!("access method was validated above"),
1518 };
1519 Ok(GeneratedDescriptor {
1520 items: quote! {
1521 static #strategy_table: [
1522 ::radixdb_plugin::__private::abi::RadixAbiBindingEntryV1;
1523 #strategy_count as usize
1524 ] = [#(#strategies),*];
1525
1526 unsafe extern "C" fn #wrapper(
1527 context: *const ::radixdb_plugin::__private::abi::RadixAbiCallContextV1,
1528 value: *const ::radixdb_plugin::__private::abi::RadixAbiValueV1,
1529 output: *const ::radixdb_plugin::__private::abi::RadixAbiResultBuilderV1,
1530 ) -> u32 {
1531 let callback: fn(#input) -> ::radixdb_plugin::PluginResult<#key> = #function_ident;
1532 unsafe {
1533 ::radixdb_plugin::__private::run_key_encoder::<#input, #key>(
1534 context, value, output, callback
1535 )
1536 }
1537 }
1538
1539 #[cfg(test)]
1540 #[test]
1541 fn #law_test() {
1542 #law_check.expect("operator-class law check failed");
1543 ::radixdb_plugin::testing::check_operator_class_strategies::<#input>(
1544 &__RADIXDB_PACKAGE,
1545 #local_id,
1546 )
1547 .expect("operator-class strategy law check failed");
1548 }
1549 },
1550 descriptor: quote! {
1551 ::radixdb_plugin::__private::abi::RadixAbiOperatorClassDescriptorV1 {
1552 header: ::radixdb_plugin::__private::abi::RadixAbiHeaderV1::new::<
1553 ::radixdb_plugin::__private::abi::RadixAbiOperatorClassDescriptorV1
1554 >(0),
1555 object_id: [#(#object),*],
1556 local_id: ::radixdb_plugin::__private::abi::RadixAbiSliceV1::from_static(#local_id.as_bytes()),
1557 semantic_revision: #semantic_revision,
1558 access_method: #method,
1559 reserved_u16: 0,
1560 input_type: #input_ref,
1561 key_type: #key_ref,
1562 key_codec_revision: #key_codec_revision,
1563 strategy_count: #strategy_count,
1564 strategies: #strategy_table.as_ptr(),
1565 support_count: 0,
1566 reserved_u32: 0,
1567 supports: ::std::ptr::null(),
1568 fingerprint: [#(#fingerprint),*],
1569 encode_key: Some(#wrapper),
1570 }
1571 },
1572 })
1573}
1574
1575fn generate_planner_descriptor(
1576 function: &ItemFn,
1577 local_id: &str,
1578 options: &Options,
1579 package_id: [u8; 16],
1580 function_ids: &BTreeMap<String, [u8; 16]>,
1581 opclass_ids: &BTreeMap<String, [u8; 16]>,
1582) -> syn::Result<GeneratedDescriptor> {
1583 options.reject_unknown(
1584 &[
1585 "id",
1586 "name",
1587 "semantic_revision",
1588 "for_function",
1589 "operator_class",
1590 "max_spans",
1591 "max_output_bytes",
1592 ],
1593 &["exact", "always_recheck"],
1594 )?;
1595 let _name = options.required_string("name", function.sig.ident.span())?;
1596 let semantic_revision = options.required_u32("semantic_revision", function.sig.ident.span())?;
1597 let max_spans = options.required_u32("max_spans", function.sig.ident.span())?;
1598 let max_output_bytes = options.required_u32("max_output_bytes", function.sig.ident.span())?;
1599 let policies = ["exact", "always_recheck"]
1600 .into_iter()
1601 .filter(|flag| options.flags.contains(*flag))
1602 .collect::<Vec<_>>();
1603 if policies.len() != 1 {
1604 return Err(syn::Error::new(
1605 function.sig.ident.span(),
1606 "planner support requires exactly one of exact or always_recheck",
1607 ));
1608 }
1609 if semantic_revision == 0 || max_spans == 0 || max_spans > 4096 || max_output_bytes == 0 {
1610 return Err(syn::Error::new(
1611 function.sig.ident.span(),
1612 "semantic_revision and planner bounds must be non-zero and max_spans <= 4096",
1613 ));
1614 }
1615 let target = options.required_string("for_function", function.sig.ident.span())?;
1616 let target_function = function_ids.get(&target).ok_or_else(|| {
1617 syn::Error::new(
1618 function.sig.ident.span(),
1619 format!("unknown target function `{target}`"),
1620 )
1621 })?;
1622 let opclass = options.required_string("operator_class", function.sig.ident.span())?;
1623 let target_opclass = opclass_ids.get(&opclass).ok_or_else(|| {
1624 syn::Error::new(
1625 function.sig.ident.span(),
1626 format!("unknown operator class `{opclass}`"),
1627 )
1628 })?;
1629 let recheck = if policies[0] == "exact" {
1630 quote!(::radixdb_plugin::__private::abi::RADIX_RECHECK_EXACT)
1631 } else {
1632 quote!(::radixdb_plugin::__private::abi::RADIX_RECHECK_ALWAYS)
1633 };
1634 let function_ident = &function.sig.ident;
1635 let wrapper = format_ident!("__radixdb_planner_{}", function_ident);
1636 let object = bytes_tokens(&object_id(package_id, local_id));
1637 let target_function = bytes_tokens(target_function);
1638 let target_opclass = bytes_tokens(target_opclass);
1639 let fingerprint = bytes_tokens(&digest32(format!(
1640 "radixdb.planner.v1\0{local_id}\0{semantic_revision}\0{max_spans}\0{max_output_bytes}\0{}",
1641 policies[0]
1642 )));
1643 Ok(GeneratedDescriptor {
1644 items: quote! {
1645 unsafe extern "C" fn #wrapper(
1646 context: *const ::radixdb_plugin::__private::abi::RadixAbiCallContextV1,
1647 predicate: ::radixdb_plugin::__private::abi::RadixAbiSliceV1,
1648 output: *const ::radixdb_plugin::__private::abi::RadixAbiResultBuilderV1,
1649 ) -> u32 {
1650 unsafe {
1651 ::radixdb_plugin::__private::run_planner(
1652 context,
1653 predicate,
1654 output,
1655 |predicate, output| #function_ident(predicate, output),
1656 )
1657 }
1658 }
1659 },
1660 descriptor: quote! {
1661 ::radixdb_plugin::__private::abi::RadixAbiPlannerSupportDescriptorV1 {
1662 header: ::radixdb_plugin::__private::abi::RadixAbiHeaderV1::new::<
1663 ::radixdb_plugin::__private::abi::RadixAbiPlannerSupportDescriptorV1
1664 >(0),
1665 object_id: [#(#object),*],
1666 local_id: ::radixdb_plugin::__private::abi::RadixAbiSliceV1::from_static(#local_id.as_bytes()),
1667 semantic_revision: #semantic_revision,
1668 max_spans: #max_spans,
1669 max_output_bytes: #max_output_bytes,
1670 recheck_policy: #recheck,
1671 reserved_u16: 0,
1672 target_function_id: [#(#target_function),*],
1673 target_operator_class_id: [#(#target_opclass),*],
1674 fingerprint: [#(#fingerprint),*],
1675 callback: Some(#wrapper),
1676 }
1677 },
1678 })
1679}
1680
1681fn function_arguments(function: &ItemFn) -> syn::Result<Vec<(Ident, Type)>> {
1682 function
1683 .sig
1684 .inputs
1685 .iter()
1686 .map(|argument| {
1687 let FnArg::Typed(argument) = argument else {
1688 return Err(syn::Error::new(
1689 argument.span(),
1690 "plugin methods are not supported",
1691 ));
1692 };
1693 let syn::Pat::Ident(ident) = argument.pat.as_ref() else {
1694 return Err(syn::Error::new(
1695 argument.pat.span(),
1696 "plugin arguments require simple names",
1697 ));
1698 };
1699 if matches!(argument.ty.as_ref(), Type::Reference(_)) {
1700 return Err(syn::Error::new(
1701 argument.ty.span(),
1702 "scalar arguments must be owned typed values",
1703 ));
1704 }
1705 Ok((ident.ident.clone(), (*argument.ty).clone()))
1706 })
1707 .collect()
1708}
1709
1710fn plugin_result_type(output: &ReturnType) -> syn::Result<Type> {
1711 let ReturnType::Type(_, ty) = output else {
1712 return Err(syn::Error::new(
1713 output.span(),
1714 "plugin function must return PluginResult<T>",
1715 ));
1716 };
1717 generic_inner(ty, "PluginResult")
1718 .ok_or_else(|| syn::Error::new(ty.span(), "plugin function must return PluginResult<T>"))
1719}
1720
1721fn generic_inner(ty: &Type, expected: &str) -> Option<Type> {
1722 let Type::Path(path) = ty else {
1723 return None;
1724 };
1725 let segment = path.path.segments.last()?;
1726 if segment.ident != expected {
1727 return None;
1728 }
1729 let PathArguments::AngleBracketed(arguments) = &segment.arguments else {
1730 return None;
1731 };
1732 arguments.args.iter().find_map(|argument| match argument {
1733 GenericArgument::Type(ty) => Some(ty.clone()),
1734 _ => None,
1735 })
1736}
1737
1738fn vec_inner(ty: &Type) -> Option<&Type> {
1739 let Type::Path(path) = ty else {
1740 return None;
1741 };
1742 let segment = path.path.segments.last()?;
1743 if segment.ident != "Vec" {
1744 return None;
1745 }
1746 let PathArguments::AngleBracketed(arguments) = &segment.arguments else {
1747 return None;
1748 };
1749 arguments.args.iter().find_map(|argument| match argument {
1750 GenericArgument::Type(ty) => Some(ty),
1751 _ => None,
1752 })
1753}
1754
1755fn parse_type_option(options: &Options, name: &str, span: Span) -> syn::Result<Type> {
1756 let expression = options
1757 .values
1758 .get(name)
1759 .ok_or_else(|| syn::Error::new(span, format!("missing required `{name}`")))?;
1760 match expression {
1761 Expr::Path(path) => Ok(Type::Path(syn::TypePath {
1762 qself: None,
1763 path: path.path.clone(),
1764 })),
1765 _ => Err(syn::Error::new(
1766 expression.span(),
1767 "expected Rust type path",
1768 )),
1769 }
1770}
1771
1772fn type_ref_tokens(
1773 ty: &Type,
1774 external: &BTreeMap<String, ([u8; 16], u32)>,
1775) -> syn::Result<TokenStream2> {
1776 let ty = generic_inner(ty, "Option").unwrap_or_else(|| ty.clone());
1777 let ident = terminal_type_ident(&ty)
1778 .ok_or_else(|| syn::Error::new(ty.span(), "unsupported plugin signature type"))?;
1779 let builtin = match ident.as_str() {
1780 "i8" | "i16" | "i32" | "i64" | "u8" | "u16" | "u32" | "u64" => Some(1_u16),
1781 "f32" | "f64" => Some(2_u16),
1782 "BoundedText" => Some(3_u16),
1783 "bool" => Some(4_u16),
1784 "BoundedBytes" => Some(11_u16),
1785 _ => None,
1786 };
1787 if let Some(tag) = builtin {
1788 return Ok(quote!(::radixdb_plugin::__private::abi::RadixAbiTypeRefV1::builtin(#tag)));
1789 }
1790 let (object_id, codec) = external.get(&ident).ok_or_else(|| {
1791 syn::Error::new(
1792 ty.span(),
1793 "signature type is neither a supported bounded built-in nor a local RadixType",
1794 )
1795 })?;
1796 let object = bytes_tokens(object_id);
1797 Ok(
1798 quote!(::radixdb_plugin::__private::abi::RadixAbiTypeRefV1::external(
1799 [#(#object),*], #codec
1800 )),
1801 )
1802}
1803
1804fn terminal_type_ident(ty: &Type) -> Option<String> {
1805 let Type::Path(path) = ty else {
1806 return None;
1807 };
1808 path.path
1809 .segments
1810 .last()
1811 .map(|segment| segment.ident.to_string())
1812}
1813
1814fn admit_local_id(ids: &mut BTreeSet<String>, value: &str, span: Span) -> syn::Result<()> {
1815 validate_local_id(value, span)?;
1816 if !ids.insert(value.to_string()) {
1817 return Err(syn::Error::new(
1818 span,
1819 format!("duplicate stable local id `{value}`"),
1820 ));
1821 }
1822 Ok(())
1823}
1824
1825fn validate_local_id(value: &str, span: Span) -> syn::Result<()> {
1826 if value.is_empty()
1827 || value.len() > 255
1828 || !value
1829 .bytes()
1830 .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'_')
1831 {
1832 return Err(syn::Error::new(
1833 span,
1834 "local id must match [a-z0-9_]{1,255}",
1835 ));
1836 }
1837 Ok(())
1838}
1839
1840fn validate_package_name(value: &str, span: Span) -> syn::Result<()> {
1841 if value.is_empty()
1842 || value.len() > 128
1843 || !value.bytes().all(|byte| {
1844 byte.is_ascii_lowercase() || byte.is_ascii_digit() || matches!(byte, b'_' | b'-')
1845 })
1846 {
1847 return Err(syn::Error::new(
1848 span,
1849 "package name must match [a-z0-9_-]{1,128}",
1850 ));
1851 }
1852 Ok(())
1853}
1854
1855fn validate_sql_name(value: &str, span: Span) -> syn::Result<()> {
1856 if value.is_empty() || value.len() > 255 || value.contains('\0') {
1857 return Err(syn::Error::new(span, "SQL name is empty or too large"));
1858 }
1859 Ok(())
1860}
1861
1862fn object_id(package_id: [u8; 16], local_id: &str) -> [u8; 16] {
1863 let mut digest = Sha256::new();
1864 digest.update(b"radixdb.plugin.object.v1\0");
1865 digest.update(package_id);
1866 digest.update((local_id.len() as u32).to_le_bytes());
1867 digest.update(local_id.as_bytes());
1868 digest.finalize()[..16].try_into().unwrap()
1869}
1870
1871fn digest32(value: String) -> [u8; 32] {
1872 Sha256::digest(value.as_bytes()).into()
1873}
1874
1875fn bytes_tokens<const N: usize>(bytes: &[u8; N]) -> Vec<syn::LitInt> {
1876 bytes
1877 .iter()
1878 .map(|byte| syn::LitInt::new(&byte.to_string(), Span::call_site()))
1879 .collect()
1880}
1881
1882#[cfg(test)]
1883mod tests;