Skip to main content

mz_expr_derive_impl/
signature.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9//! Analysis of a `#[sqlfunc]`-annotated function's signature.
10//!
11//! These helpers read `syn` types and answer what a generated trait impl needs to
12//! know about the function it wraps: its associated types once generic parameters
13//! are erased, which input positions reject NULL, and the output type a polymorphic
14//! signature implies. Nothing here reads the attribute's modifiers or the arity
15//! tables, so it stays a leaf of the crate.
16
17use proc_macro2::{Ident, TokenStream};
18use quote::quote;
19use syn::Lifetime;
20use syn::spanned::Spanned;
21
22/// Checks if the last parameter of the function is a `&RowArena`.
23pub(crate) fn last_is_arena(func: &syn::ItemFn) -> bool {
24    func.sig.inputs.last().map_or(false, |last| {
25        if let syn::FnArg::Typed(pat) = last {
26            if let syn::Type::Reference(reference) = &*pat.ty {
27                if let syn::Type::Path(path) = &*reference.elem {
28                    return path.path.is_ident("RowArena");
29                }
30            }
31        }
32        false
33    })
34}
35
36/// The parameters that carry SQL arguments, which excludes a `&self` receiver and a
37/// trailing `&RowArena`.
38pub(crate) fn sql_args(func: &syn::ItemFn) -> Vec<&syn::FnArg> {
39    let end = func.sig.inputs.len() - usize::from(last_is_arena(func));
40    func.sig
41        .inputs
42        .iter()
43        .take(end)
44        .skip(usize::from(has_receiver(func)))
45        .collect()
46}
47
48/// Checks if the function takes a `self` receiver.
49pub(crate) fn has_receiver(func: &syn::ItemFn) -> bool {
50    matches!(func.sig.inputs.first(), Some(syn::FnArg::Receiver(_)))
51}
52
53/// Checks if a parameter type accepts NULL.
54///
55/// `Option<T>` always accepts NULL. `OptionalArg<T>` delegates to `T`.
56/// `Datum` accepts NULL (it passes through raw values including null).
57/// Everything else (references, concrete types) rejects NULL.
58fn is_nullable_type(ty: &syn::Type) -> bool {
59    if let syn::Type::Path(type_path) = ty {
60        if let Some(last_segment) = type_path.path.segments.last() {
61            let ident = &last_segment.ident;
62            if ident == "Option" || ident == "Datum" {
63                return true;
64            }
65            if ident == "OptionalArg" {
66                // OptionalArg<T> delegates nullability to T.
67                if let syn::PathArguments::AngleBracketed(args) = &last_segment.arguments {
68                    if let Some(syn::GenericArgument::Type(inner_ty)) = args.args.first() {
69                        return is_nullable_type(inner_ty);
70                    }
71                }
72                return false;
73            }
74        }
75    }
76    false
77}
78
79/// Checks whether a parameter's type is `Variadic<...>` or `OptionalArg<...>`, which
80/// makes the function variadic regardless of parameter count.
81pub(crate) fn is_variadic_arg(arg: &syn::FnArg) -> bool {
82    let syn::FnArg::Typed(pat) = arg else {
83        return false;
84    };
85    let syn::Type::Path(path) = &*pat.ty else {
86        return false;
87    };
88    path.path
89        .segments
90        .last()
91        .is_some_and(|segment| segment.ident == "Variadic" || segment.ident == "OptionalArg")
92}
93
94/// Checks if a type is `Variadic<T>`.
95fn is_variadic_type(ty: &syn::Type) -> bool {
96    if let syn::Type::Path(type_path) = ty {
97        if let Some(last_segment) = type_path.path.segments.last() {
98            return last_segment.ident == "Variadic";
99        }
100    }
101    false
102}
103
104/// For a `Variadic<T>` type, checks if `T` accepts NULL.
105fn variadic_element_is_nullable(ty: &syn::Type) -> bool {
106    if let syn::Type::Path(type_path) = ty {
107        if let Some(last_segment) = type_path.path.segments.last() {
108            if let syn::PathArguments::AngleBracketed(args) = &last_segment.arguments {
109                if let Some(syn::GenericArgument::Type(inner_ty)) = args.args.first() {
110                    return is_nullable_type(inner_ty);
111                }
112            }
113        }
114    }
115    false
116}
117
118/// Generates per-position nullability checks for non-nullable parameters.
119///
120/// For each parameter that rejects NULL (not `Option`, not `OptionalArg<Option<..>>`),
121/// generates a check that the corresponding input position is nullable. For `Variadic<T>`
122/// with non-nullable `T`, generates a check over all remaining input positions.
123pub(crate) fn non_nullable_position_checks(param_types: &[syn::Type]) -> Vec<TokenStream> {
124    let mut checks = Vec::new();
125    for (i, ty) in param_types.iter().enumerate() {
126        if is_variadic_type(ty) {
127            if !variadic_element_is_nullable(ty) {
128                checks.push(quote! { || input_types.iter().skip(#i).any(|t| t.nullable) });
129            }
130        } else if !is_nullable_type(ty) {
131            checks.push(quote! { || input_types.get(#i).map_or(false, |t| t.nullable) });
132        }
133    }
134    checks
135}
136
137/// Convert an identifier to a camel-cased identifier.
138pub(crate) fn camel_case(ident: &Ident) -> Ident {
139    let mut result = String::new();
140    let mut capitalize_next = true;
141    for c in ident.to_string().chars() {
142        if c == '_' {
143            capitalize_next = true;
144        } else if capitalize_next {
145            result.push(c.to_ascii_uppercase());
146            capitalize_next = false;
147        } else {
148            result.push(c);
149        }
150    }
151    Ident::new(&result, ident.span())
152}
153
154/// Extracts generic type parameters from a function signature.
155/// Returns an empty Vec if there are no type parameters.
156pub(crate) fn find_generic_type_params(func: &syn::ItemFn) -> Vec<Ident> {
157    func.sig
158        .generics
159        .params
160        .iter()
161        .filter_map(|p| {
162            if let syn::GenericParam::Type(tp) = p {
163                Some(tp.ident.clone())
164            } else {
165                None
166            }
167        })
168        .collect()
169}
170
171/// How a generic type parameter `T` appears in a type.
172#[derive(Debug, Clone)]
173enum GenericUsage {
174    /// `T` does not appear in this type.
175    Absent,
176    /// `T` appears bare (possibly wrapped in `Option` or `Result`).
177    Bare,
178    /// `T` appears inside a container type (e.g. `DatumList<'a, T>`, `Array<'a, T>`).
179    /// The stored `syn::TypePath` is the container with `T` erased to `Datum<'a>`.
180    InContainer(syn::TypePath),
181}
182
183impl PartialEq for GenericUsage {
184    fn eq(&self, other: &Self) -> bool {
185        match (self, other) {
186            (GenericUsage::Absent, GenericUsage::Absent) => true,
187            (GenericUsage::Bare, GenericUsage::Bare) => true,
188            (GenericUsage::InContainer(a), GenericUsage::InContainer(b)) => {
189                container_idents_match(a, b)
190            }
191            _ => false,
192        }
193    }
194}
195
196impl Eq for GenericUsage {}
197
198/// Compare two container type paths by their ident segments (ignoring lifetimes
199/// and generic arguments). Two containers are "same" if their path idents match.
200///
201/// This is safe because after erasure all container types have the same generic
202/// arity (lifetimes + `Datum<'a>`), so ident equality implies structural equality.
203fn container_idents_match(a: &syn::TypePath, b: &syn::TypePath) -> bool {
204    let a_idents: Vec<_> = a.path.segments.iter().map(|s| &s.ident).collect();
205    let b_idents: Vec<_> = b.path.segments.iter().map(|s| &s.ident).collect();
206    a_idents == b_idents
207}
208
209/// Classifies how a generic type parameter appears in a type.
210///
211/// Strips `Option<...>`, `Result<..., E>`, and `ExcludeNull<...>` wrappers before
212/// inspecting the inner type. Any generic type wrapping `T` that isn't `Option`,
213/// `Result`, or `ExcludeNull` is treated as a container. If the container doesn't
214/// implement `SqlContainerType`, the generated code won't compile (a clear error).
215fn classify_generic_usage(ty: &syn::Type, generic_name: &Ident) -> GenericUsage {
216    match ty {
217        syn::Type::Path(type_path) => {
218            if type_path.path.is_ident(generic_name) {
219                return GenericUsage::Bare;
220            }
221            if let Some(last) = type_path.path.segments.last() {
222                let ident_str = last.ident.to_string();
223                // Unwrap Option, Result, ExcludeNull wrappers
224                if ident_str == "Option" || ident_str == "Result" || ident_str == "ExcludeNull" {
225                    if let syn::PathArguments::AngleBracketed(args) = &last.arguments {
226                        if let Some(syn::GenericArgument::Type(inner)) = args.args.first() {
227                            return classify_generic_usage(inner, generic_name);
228                        }
229                    }
230                }
231                // Check if any angle-bracketed arg contains the generic param.
232                // If so, treat this type as a container.
233                if let syn::PathArguments::AngleBracketed(args) = &last.arguments {
234                    let has_generic_arg = args.args.iter().any(|arg| {
235                        if let syn::GenericArgument::Type(inner) = arg {
236                            type_contains_ident(inner, generic_name)
237                        } else {
238                            false
239                        }
240                    });
241                    if has_generic_arg {
242                        // Build the erased container type path, with `T` replaced by `Datum<'a>`.
243                        let erased = erase_generic_param(ty, generic_name);
244                        if let syn::Type::Path(erased_path) = erased {
245                            return GenericUsage::InContainer(erased_path);
246                        }
247                    }
248                    // Recurse into args for nested containers
249                    // (e.g., Option<DatumList<'a, T>> was already handled by
250                    // the Option unwrapping above, but handle other nestings)
251                    for arg in &args.args {
252                        if let syn::GenericArgument::Type(inner) = arg {
253                            let inner_usage = classify_generic_usage(inner, generic_name);
254                            if inner_usage != GenericUsage::Absent {
255                                return inner_usage;
256                            }
257                        }
258                    }
259                }
260            }
261            GenericUsage::Absent
262        }
263        syn::Type::Reference(r) => classify_generic_usage(&r.elem, generic_name),
264        syn::Type::Tuple(t) => {
265            // Prefer container usages over bare. For example, `(T, DatumList<'_, T>)`
266            // should classify as `InContainer`, not `Bare`.
267            let mut best = GenericUsage::Absent;
268            for elem in &t.elems {
269                let usage = classify_generic_usage(elem, generic_name);
270                match (&best, &usage) {
271                    (GenericUsage::Absent, _) => best = usage,
272                    (GenericUsage::Bare, u) if *u != GenericUsage::Absent => best = usage.clone(),
273                    _ => {
274                        if usage != GenericUsage::Absent && usage != best {
275                            // Conflicting container usages cannot be resolved.
276                            return GenericUsage::Bare;
277                        }
278                    }
279                }
280            }
281            best
282        }
283        _ => GenericUsage::Absent,
284    }
285}
286
287/// Returns whether a type syntactically contains an identifier.
288fn type_contains_ident(ty: &syn::Type, ident: &Ident) -> bool {
289    match ty {
290        syn::Type::Path(type_path) => {
291            if type_path.path.is_ident(ident) {
292                return true;
293            }
294            if let Some(last) = type_path.path.segments.last() {
295                if let syn::PathArguments::AngleBracketed(args) = &last.arguments {
296                    return args.args.iter().any(|arg| {
297                        if let syn::GenericArgument::Type(inner) = arg {
298                            type_contains_ident(inner, ident)
299                        } else {
300                            false
301                        }
302                    });
303                }
304            }
305            false
306        }
307        syn::Type::Reference(r) => type_contains_ident(&r.elem, ident),
308        syn::Type::Tuple(t) => t.elems.iter().any(|e| type_contains_ident(e, ident)),
309        _ => false,
310    }
311}
312
313/// Returns whether the outermost wrapper of a type is `Option`.
314pub(crate) fn is_option_wrapped(ty: &syn::Type) -> bool {
315    if let syn::Type::Path(type_path) = ty {
316        if let Some(last) = type_path.path.segments.last() {
317            return last.ident == "Option";
318        }
319    }
320    false
321}
322
323/// Derives an `output_type_expr` TokenStream from the structural relationship
324/// between input types and the output type, based on where generic parameters appear.
325///
326/// Finds the first generic parameter that appears in the output type, then looks for
327/// the first input parameter containing that generic in a container type to determine
328/// the unwrap strategy.
329///
330/// `is_unary` controls whether the generated expression uses `input_type`
331/// (singular, for unary functions) or `input_types[i]` (indexed, for binary/variadic).
332///
333/// Returns `None` if no generic parameter appears in the output type.
334pub(crate) fn derive_output_type_for_generics(
335    input_types: &[syn::Type],
336    output_ty: &syn::Type,
337    generic_names: &[Ident],
338    is_unary: bool,
339) -> darling::Result<Option<TokenStream>> {
340    // Find the first generic param that appears in the output.
341    let Some((generic_name, output_usage)) = generic_names.iter().find_map(|gn| {
342        let usage = classify_generic_usage(output_ty, gn);
343        (usage != GenericUsage::Absent).then_some((gn, usage))
344    }) else {
345        return Ok(None);
346    };
347    derive_output_type_for_generic(input_types, output_ty, generic_name, output_usage, is_unary)
348        .map(Some)
349}
350
351/// Derives an `output_type_expr` for a single generic parameter, which appears in the
352/// output type as `output_usage`.
353///
354/// Uses `SqlContainerType` trait calls instead of matching on specific container
355/// type names. The generated code calls `<Container as SqlContainerType>::unwrap_element_type()`
356/// and `wrap_element_type()`, letting Rust's type system resolve the correct behavior.
357fn derive_output_type_for_generic(
358    input_types: &[syn::Type],
359    output_ty: &syn::Type,
360    generic_name: &Ident,
361    output_usage: GenericUsage,
362    is_unary: bool,
363) -> darling::Result<TokenStream> {
364    let nullable = is_option_wrapped(output_ty);
365
366    // Find the first input parameter that has T in a container.
367    // Prefer container inputs over bare inputs.
368    let mut container_input: Option<(usize, GenericUsage)> = None;
369    for (i, ty) in input_types.iter().enumerate() {
370        let usage = classify_generic_usage(ty, generic_name);
371        match &usage {
372            GenericUsage::InContainer(_) => {
373                container_input = Some((i, usage));
374                break;
375            }
376            GenericUsage::Bare => {
377                // A bare `T` is a fallback. Keep looking for a container.
378                if container_input.is_none() {
379                    container_input = Some((i, usage));
380                }
381            }
382            GenericUsage::Absent => {}
383        }
384    }
385
386    let (pos, source_usage) = container_input.ok_or_else(|| {
387        darling::Error::custom(format!(
388            "generic parameter {generic_name} appears in the output type but not in any input type",
389        ))
390    })?;
391
392    // Generate the base expression to access the input type.
393    let input_access = if is_unary {
394        quote! { input_type }
395    } else {
396        let pos_lit = syn::Index::from(pos);
397        quote! { input_types[#pos_lit] }
398    };
399
400    // For multi-input functions, generate soft assertions that all inputs
401    // carrying T agree on the SQL element type. This catches bugs in the
402    // planner's overload resolution or cast insertion.
403    let consistency_checks = if !is_unary {
404        let mut checks = Vec::new();
405        for (i, ty) in input_types.iter().enumerate() {
406            if i == pos {
407                continue;
408            }
409            let usage = classify_generic_usage(ty, generic_name);
410            if usage == GenericUsage::Absent {
411                continue;
412            }
413            let primary_elem = element_type_expr(&input_access, &source_usage);
414            let i_lit = syn::Index::from(i);
415            let other_access = quote! { input_types[#i_lit] };
416            let other_elem = element_type_expr(&other_access, &usage);
417            let generic_str = generic_name.to_string();
418            checks.push(quote! {
419                mz_ore::soft_assert_or_log!(
420                    #primary_elem.base_eq(#other_elem),
421                    "auto-derived sqlfunc output type inference found inconsistent \
422                     SQL types for generic {} across inputs: {:?} vs {:?}; \
423                     this indicates a bug in polymorphic coercion, builtin \
424                     declaration, or sqlfunc inference",
425                    #generic_str,
426                    #primary_elem,
427                    #other_elem,
428                );
429            });
430        }
431        quote! { #(#checks)* }
432    } else {
433        quote! {}
434    };
435
436    // The expression depends on how `T` appears in the output and in the source
437    // input.
438    let expr = match (&output_usage, &source_usage) {
439        // Output is bare `T` and the source is a container: unwrap the element type.
440        (GenericUsage::Bare, GenericUsage::InContainer(in_container)) => {
441            let in_c = elide_lifetimes(in_container);
442            quote! {
443                {
444                    #consistency_checks
445                    <#in_c as mz_repr::SqlContainerType>::unwrap_element_type(
446                        &#input_access.scalar_type
447                    ).clone().nullable(#nullable)
448                }
449            }
450        }
451        // Output and source are both bare `T`: forward the input type.
452        (GenericUsage::Bare, GenericUsage::Bare) => {
453            quote! {
454                {
455                    #consistency_checks
456                    #input_access.scalar_type.clone().nullable(#nullable)
457                }
458            }
459        }
460        // Output and source are both containers, the same or different ones: unwrap
461        // the element from the input container and wrap it into the output container.
462        (GenericUsage::InContainer(out_container), GenericUsage::InContainer(in_container)) => {
463            let out_c = elide_lifetimes(out_container);
464            let in_c = elide_lifetimes(in_container);
465            quote! {
466                {
467                    #consistency_checks
468                    <#out_c as mz_repr::SqlContainerType>::wrap_element_type(
469                        <#in_c as mz_repr::SqlContainerType>::unwrap_element_type(
470                            &#input_access.scalar_type
471                        ).clone()
472                    ).nullable(#nullable)
473                }
474            }
475        }
476        // Any other combination needs an explicit `output_type_expr`.
477        _ => {
478            return Err(darling::Error::custom(format!(
479                "cannot auto-derive output_type_expr: output uses T as {:?} but \
480                 the first T-containing input uses T as {:?}",
481                output_usage, source_usage
482            )));
483        }
484    };
485
486    Ok(expr)
487}
488
489/// Generates a token stream that extracts the T-level SQL type from an input
490/// access expression, based on how T is used in that input.
491fn element_type_expr(input_access: &TokenStream, usage: &GenericUsage) -> TokenStream {
492    match usage {
493        GenericUsage::Bare => {
494            quote! { &#input_access.scalar_type }
495        }
496        GenericUsage::InContainer(container) => {
497            let c = elide_lifetimes(container);
498            quote! {
499                <#c as mz_repr::SqlContainerType>::unwrap_element_type(
500                    &#input_access.scalar_type
501                )
502            }
503        }
504        GenericUsage::Absent => unreachable!("element_type_expr called with Absent usage"),
505    }
506}
507
508/// Replaces all lifetime parameters in a `syn::TypePath` with `'_`.
509///
510/// Used for container type paths in turbofish position (e.g.
511/// `<DatumList<'_, Datum<'_>> as SqlContainerType>::...`).
512/// The `output_sql_type` method's `&self` provides an implicit lifetime
513/// that the compiler can infer through `'_`.
514fn elide_lifetimes(tp: &syn::TypePath) -> syn::TypePath {
515    let mut tp = tp.clone();
516    for segment in &mut tp.path.segments {
517        if let syn::PathArguments::AngleBracketed(args) = &mut segment.arguments {
518            for arg in &mut args.args {
519                match arg {
520                    syn::GenericArgument::Lifetime(lt) => {
521                        *lt = Lifetime::new("'_", lt.span());
522                    }
523                    syn::GenericArgument::Type(ty) => {
524                        elide_lifetimes_in_type(ty);
525                    }
526                    _ => {}
527                }
528            }
529        }
530    }
531    tp
532}
533
534/// Recursively replaces all lifetime parameters in a `syn::Type` with `'_`.
535fn elide_lifetimes_in_type(ty: &mut syn::Type) {
536    match ty {
537        syn::Type::Path(tp) => {
538            *tp = elide_lifetimes(tp);
539        }
540        syn::Type::Reference(r) => {
541            if let Some(lt) = &mut r.lifetime {
542                *lt = Lifetime::new("'_", lt.span());
543            }
544            elide_lifetimes_in_type(&mut r.elem);
545        }
546        syn::Type::Tuple(t) => {
547            for elem in &mut t.elems {
548                elide_lifetimes_in_type(elem);
549            }
550        }
551        _ => {}
552    }
553}
554
555/// Replaces occurrences of a generic type parameter with `Datum<'a>` in a type.
556///
557/// Used to convert types from the user's generic function signature into concrete
558/// types for the generated trait impl's associated types, where `T` is not in scope.
559fn erase_generic_param(ty: &syn::Type, generic_name: &Ident) -> syn::Type {
560    match ty {
561        syn::Type::Path(type_path) => {
562            if type_path.path.is_ident(generic_name) {
563                return syn::parse_quote!(Datum<'a>);
564            }
565            let mut type_path = type_path.clone();
566            for segment in &mut type_path.path.segments {
567                if let syn::PathArguments::AngleBracketed(args) = &mut segment.arguments {
568                    for arg in &mut args.args {
569                        if let syn::GenericArgument::Type(inner) = arg {
570                            *inner = erase_generic_param(inner, generic_name);
571                        }
572                    }
573                }
574            }
575            syn::Type::Path(type_path)
576        }
577        syn::Type::Reference(r) => {
578            let elem = Box::new(erase_generic_param(&r.elem, generic_name));
579            syn::Type::Reference(syn::TypeReference { elem, ..r.clone() })
580        }
581        syn::Type::Tuple(t) => {
582            let elems = t
583                .elems
584                .iter()
585                .map(|e| erase_generic_param(e, generic_name))
586                .collect();
587            syn::Type::Tuple(syn::TypeTuple { elems, ..t.clone() })
588        }
589        _ => ty.clone(),
590    }
591}
592
593/// Erases all generic type parameters from a type, replacing each with `Datum<'a>`.
594pub(crate) fn erase_all_generic_params(ty: &syn::Type, generic_names: &[Ident]) -> syn::Type {
595    let mut ty = ty.clone();
596    for gn in generic_names {
597        ty = erase_generic_param(&ty, gn);
598    }
599    ty
600}
601
602/// Recursively patches lifetimes in a type, adding `'a` to references without a lifetime
603/// and recursing into generic arguments and tuples.
604pub(crate) fn patch_lifetimes(ty: &syn::Type) -> syn::Type {
605    match ty {
606        syn::Type::Reference(r) => {
607            let elem = Box::new(patch_lifetimes(&r.elem));
608            if r.lifetime.is_none() {
609                syn::Type::Reference(syn::TypeReference {
610                    lifetime: Some(Lifetime::new("'a", r.span())),
611                    elem,
612                    ..r.clone()
613                })
614            } else {
615                syn::Type::Reference(syn::TypeReference { elem, ..r.clone() })
616            }
617        }
618        syn::Type::Tuple(t) => {
619            let elems = t.elems.iter().map(patch_lifetimes).collect();
620            syn::Type::Tuple(syn::TypeTuple { elems, ..t.clone() })
621        }
622        syn::Type::Path(p) => {
623            let mut p = p.clone();
624            for segment in &mut p.path.segments {
625                if let syn::PathArguments::AngleBracketed(args) = &mut segment.arguments {
626                    for arg in &mut args.args {
627                        if let syn::GenericArgument::Type(ty) = arg {
628                            *ty = patch_lifetimes(ty);
629                        }
630                    }
631                }
632            }
633            syn::Type::Path(p)
634        }
635        _ => ty.clone(),
636    }
637}
638
639/// Determine the output type for a function. Returns an error if the function
640/// does not return a value.
641pub(crate) fn output_type(arg: &syn::ItemFn) -> Result<&syn::Type, syn::Error> {
642    match &arg.sig.output {
643        syn::ReturnType::Type(_, ty) => Ok(&*ty),
644        syn::ReturnType::Default => Err(syn::Error::new(
645            arg.sig.output.span(),
646            "function needs to return a value",
647        )),
648    }
649}