Skip to main content

radixdb_plugin_macros/
lib.rs

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.19 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    // This fingerprint is persisted beside encoded values. It identifies only
381    // the physical codec: SQL names and semantic callbacks can evolve without
382    // forcing a codec migration. Their evolution is tracked independently by
383    // semantic revisions and the package descriptor fingerprint.
384    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.19 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.19",
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;