Skip to main content

mz_expr_derive_impl/
sqlfunc.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
10use darling::FromMeta;
11use proc_macro2::{Ident, TokenStream};
12use quote::quote;
13use syn::spanned::Spanned;
14use syn::{Expr, Lifetime, Lit};
15
16/// Modifiers passed as key-value pairs to the `#[sqlfunc]` macro.
17#[derive(Debug, Default, darling::FromMeta)]
18pub(crate) struct Modifiers {
19    /// An optional expression that evaluates to a boolean indicating whether the function is
20    /// monotone with respect to its arguments. Defined for unary and binary functions.
21    is_monotone: Option<Expr>,
22    /// Optional expression evaluating to a boolean: whether `is_monotone`'s
23    /// endpoint-sampling guarantee still holds when an operand may be infinite.
24    /// Set `false` for multiplication and division. Applies to binary functions.
25    is_infinity_monotone: Option<Expr>,
26    /// The SQL name for the function. Applies to all functions.
27    sqlname: Option<SqlName>,
28    /// Whether the function preserves uniqueness. Applies to unary functions.
29    preserves_uniqueness: Option<Expr>,
30    /// The inverse of the function, if it exists. Applies to unary functions.
31    inverse: Option<Expr>,
32    /// The negated function, if it exists. Applies to binary functions.
33    negate: Option<Expr>,
34    /// Whether the function is an infix operator. Applies to binary functions, and needs to
35    /// be specified.
36    is_infix_op: Option<Expr>,
37    /// The output type of the function, if it cannot be inferred. Applies to all functions.
38    output_type: Option<syn::Path>,
39    /// The output type of the function as an expression. Applies to binary and variadic functions.
40    output_type_expr: Option<Expr>,
41    /// Optional expression evaluating to a boolean indicating whether the function could error.
42    /// Applies to all functions.
43    could_error: Option<Expr>,
44    /// Whether the function propagates nulls. Applies to binary and variadic functions.
45    propagates_nulls: Option<Expr>,
46    /// Whether the function introduces nulls. Applies to all functions.
47    introduces_nulls: Option<Expr>,
48    /// Whether the function is associative. Applies to variadic functions.
49    is_associative: Option<Expr>,
50    /// Whether the function is a noop cast. Applies to unary functions.
51    is_eliminable_cast: Option<Expr>,
52    /// Whether to generate a snapshot test for the function. Defaults to false.
53    test: Option<bool>,
54}
55
56/// A name for the SQL function. It can be either a literal or a macro, thus we
57/// can't use `String` or `syn::Expr` directly.
58#[derive(Debug)]
59enum SqlName {
60    /// A literal string.
61    Literal(syn::Lit),
62    /// A macro expression.
63    Macro(syn::ExprMacro),
64}
65
66impl quote::ToTokens for SqlName {
67    fn to_tokens(&self, tokens: &mut TokenStream) {
68        let name = match self {
69            SqlName::Literal(lit) => quote! { #lit },
70            SqlName::Macro(mac) => quote! { #mac },
71        };
72        tokens.extend(name);
73    }
74}
75
76impl darling::FromMeta for SqlName {
77    fn from_value(value: &Lit) -> darling::Result<Self> {
78        Ok(Self::Literal(value.clone()))
79    }
80    fn from_expr(expr: &Expr) -> darling::Result<Self> {
81        match expr {
82            Expr::Lit(lit) => Self::from_value(&lit.lit),
83            Expr::Macro(mac) => Ok(Self::Macro(mac.clone())),
84            // Syn sometimes inserts groups, see `FromMeta::from_expr` for
85            // details.
86            Expr::Group(mac) => Self::from_expr(&mac.expr),
87            _ => Err(darling::Error::unexpected_expr_type(expr)),
88        }
89    }
90}
91
92/// Implementation for the `#[sqlfunc]` macro. The first parameter is the attribute
93/// arguments, the second is the function body. The third parameter indicates
94/// whether to include the test function in the output.
95///
96/// The feature `test` must be enabled to include the test function.
97pub fn sqlfunc(
98    attr: TokenStream,
99    item: TokenStream,
100    include_test: bool,
101) -> darling::Result<TokenStream> {
102    let mut attr_args = darling::ast::NestedMeta::parse_meta_list(attr.clone())?;
103
104    // Check if the first attribute arg is a bare Path (struct name for variadic).
105    let struct_ty = match attr_args.first() {
106        Some(darling::ast::NestedMeta::Meta(syn::Meta::Path(_))) => {
107            let darling::ast::NestedMeta::Meta(syn::Meta::Path(path)) = attr_args.remove(0) else {
108                unreachable!()
109            };
110            Some(path)
111        }
112        _ => None,
113    };
114
115    let modifiers = Modifiers::from_list(&attr_args).unwrap();
116    let generate_tests = modifiers.test.unwrap_or(false);
117    let func = syn::parse2::<syn::ItemFn>(item.clone())?;
118
119    let tokens = match determine_arity(&func) {
120        Arity::Nullary => Err(darling::Error::custom("Nullary functions not supported")),
121        Arity::Unary { arena: false } => unary_func(&func, modifiers),
122        Arity::Unary { arena: true } => Err(darling::Error::custom(
123            "Unary functions do not yet support RowArena.",
124        )),
125        Arity::Binary { arena } => binary_func(&func, modifiers, arena),
126        Arity::Variadic { arena, has_self } => {
127            variadic_func(&func, modifiers, struct_ty, arena, has_self)
128        }
129    }?;
130
131    let test = (generate_tests && include_test).then(|| generate_test(attr, item, &func.sig.ident));
132
133    Ok(quote! {
134        #tokens
135        #test
136    })
137}
138
139#[cfg(any(feature = "test", test))]
140fn generate_test(attr: TokenStream, item: TokenStream, name: &Ident) -> TokenStream {
141    let attr = attr.to_string();
142    let item = item.to_string();
143    let test_name = Ident::new(&format!("test_{}", name), name.span());
144    let fn_name = name.to_string();
145
146    quote! {
147        #[cfg(test)]
148        #[cfg_attr(miri, ignore)] // unsupported operation: extern static `pidfd_spawnp` is not supported by Miri
149        #[mz_ore::test]
150        fn #test_name() {
151            let (output, input) = mz_expr_derive_impl::test_sqlfunc_str(#attr, #item);
152            insta::assert_snapshot!(#fn_name, output, &input);
153        }
154    }
155}
156
157#[cfg(not(any(feature = "test", test)))]
158fn generate_test(_attr: TokenStream, _item: TokenStream, _name: &Ident) -> TokenStream {
159    quote! {}
160}
161
162/// Checks if the last parameter of the function is a `&RowArena`.
163fn last_is_arena(func: &syn::ItemFn) -> bool {
164    func.sig.inputs.last().map_or(false, |last| {
165        if let syn::FnArg::Typed(pat) = last {
166            if let syn::Type::Reference(reference) = &*pat.ty {
167                if let syn::Type::Path(path) = &*reference.elem {
168                    return path.path.is_ident("RowArena");
169                }
170            }
171        }
172        false
173    })
174}
175
176/// Arity classification for a function annotated with `#[sqlfunc]`.
177enum Arity {
178    Nullary,
179    Unary { arena: bool },
180    Binary { arena: bool },
181    Variadic { arena: bool, has_self: bool },
182}
183
184/// Checks whether a parameter's type is `Variadic<...>` or `OptionalArg<...>`,
185/// which indicates the function should be treated as variadic regardless of
186/// parameter count.
187fn is_variadic_arg(arg: &syn::FnArg) -> bool {
188    if let syn::FnArg::Typed(pat) = arg {
189        if let syn::Type::Path(path) = &*pat.ty {
190            if let Some(segment) = path.path.segments.last() {
191                let ident = segment.ident.to_string();
192                return ident == "Variadic" || ident == "OptionalArg";
193            }
194        }
195    }
196    false
197}
198
199/// Determines the arity of a function annotated with `#[sqlfunc]`.
200///
201/// Accounts for `&self` receivers, trailing `&RowArena` parameters, and
202/// parameter types like `Variadic<T>` or `OptionalArg<T>` that indicate
203/// variadic dispatch.
204fn determine_arity(func: &syn::ItemFn) -> Arity {
205    let arena = last_is_arena(func);
206    let has_self = matches!(func.sig.inputs.first(), Some(syn::FnArg::Receiver(_)));
207
208    let mut effective_count = func.sig.inputs.len();
209    if arena {
210        effective_count -= 1;
211    }
212    if has_self {
213        effective_count -= 1;
214    }
215
216    // Check if any effective parameter uses a variadic-typed wrapper.
217    let start = if has_self { 1 } else { 0 };
218    let end = if arena {
219        func.sig.inputs.len() - 1
220    } else {
221        func.sig.inputs.len()
222    };
223    let has_variadic_param = func
224        .sig
225        .inputs
226        .iter()
227        .skip(start)
228        .take(end - start)
229        .any(is_variadic_arg);
230
231    if has_variadic_param || effective_count >= 3 {
232        Arity::Variadic { arena, has_self }
233    } else {
234        match effective_count {
235            0 => Arity::Nullary,
236            1 => Arity::Unary { arena },
237            2 => Arity::Binary { arena },
238            _ => unreachable!(),
239        }
240    }
241}
242
243/// Convert an identifier to a camel-cased identifier.
244/// Checks if a parameter type accepts NULL.
245///
246/// `Option<T>` always accepts NULL. `OptionalArg<T>` delegates to `T`.
247/// `Datum` accepts NULL (it passes through raw values including null).
248/// Everything else (references, concrete types) rejects NULL.
249fn is_nullable_type(ty: &syn::Type) -> bool {
250    if let syn::Type::Path(type_path) = ty {
251        if let Some(last_segment) = type_path.path.segments.last() {
252            let ident = &last_segment.ident;
253            if ident == "Option" || ident == "Datum" {
254                return true;
255            }
256            if ident == "OptionalArg" {
257                // OptionalArg<T> delegates nullability to T.
258                if let syn::PathArguments::AngleBracketed(args) = &last_segment.arguments {
259                    if let Some(syn::GenericArgument::Type(inner_ty)) = args.args.first() {
260                        return is_nullable_type(inner_ty);
261                    }
262                }
263                return false;
264            }
265        }
266    }
267    false
268}
269
270/// Checks if a type is `Variadic<T>`.
271fn is_variadic_type(ty: &syn::Type) -> bool {
272    if let syn::Type::Path(type_path) = ty {
273        if let Some(last_segment) = type_path.path.segments.last() {
274            return last_segment.ident == "Variadic";
275        }
276    }
277    false
278}
279
280/// For a `Variadic<T>` type, checks if `T` accepts NULL.
281fn variadic_element_is_nullable(ty: &syn::Type) -> bool {
282    if let syn::Type::Path(type_path) = ty {
283        if let Some(last_segment) = type_path.path.segments.last() {
284            if let syn::PathArguments::AngleBracketed(args) = &last_segment.arguments {
285                if let Some(syn::GenericArgument::Type(inner_ty)) = args.args.first() {
286                    return is_nullable_type(inner_ty);
287                }
288            }
289        }
290    }
291    false
292}
293
294/// Generates per-position nullability checks for non-nullable parameters.
295///
296/// For each parameter that rejects NULL (not `Option`, not `OptionalArg<Option<..>>`),
297/// generates a check that the corresponding input position is nullable. For `Variadic<T>`
298/// with non-nullable `T`, generates a check over all remaining input positions.
299fn non_nullable_position_checks(param_types: &[syn::Type]) -> Vec<TokenStream> {
300    let mut checks = Vec::new();
301    for (i, ty) in param_types.iter().enumerate() {
302        if is_variadic_type(ty) {
303            if !variadic_element_is_nullable(ty) {
304                checks.push(quote! { || input_types.iter().skip(#i).any(|t| t.nullable) });
305            }
306        } else if !is_nullable_type(ty) {
307            checks.push(quote! { || input_types.get(#i).map_or(false, |t| t.nullable) });
308        }
309    }
310    checks
311}
312
313fn camel_case(ident: &Ident) -> Ident {
314    let mut result = String::new();
315    let mut capitalize_next = true;
316    for c in ident.to_string().chars() {
317        if c == '_' {
318            capitalize_next = true;
319        } else if capitalize_next {
320            result.push(c.to_ascii_uppercase());
321            capitalize_next = false;
322        } else {
323            result.push(c);
324        }
325    }
326    Ident::new(&result, ident.span())
327}
328
329/// Extracts generic type parameters from a function signature.
330/// Returns an empty Vec if there are no type parameters.
331fn find_generic_type_params(func: &syn::ItemFn) -> Vec<Ident> {
332    func.sig
333        .generics
334        .params
335        .iter()
336        .filter_map(|p| {
337            if let syn::GenericParam::Type(tp) = p {
338                Some(tp.ident.clone())
339            } else {
340                None
341            }
342        })
343        .collect()
344}
345
346/// How a generic type parameter `T` appears in a type.
347#[derive(Debug, Clone)]
348enum GenericUsage {
349    /// `T` does not appear in this type.
350    Absent,
351    /// `T` appears bare (possibly wrapped in `Option` or `Result`).
352    Bare,
353    /// `T` appears inside a container type (e.g. `DatumList<'a, T>`, `Array<'a, T>`).
354    /// The stored `syn::TypePath` is the container with `T` erased to `Datum<'a>`.
355    InContainer(syn::TypePath),
356}
357
358impl PartialEq for GenericUsage {
359    fn eq(&self, other: &Self) -> bool {
360        match (self, other) {
361            (GenericUsage::Absent, GenericUsage::Absent) => true,
362            (GenericUsage::Bare, GenericUsage::Bare) => true,
363            (GenericUsage::InContainer(a), GenericUsage::InContainer(b)) => {
364                container_idents_match(a, b)
365            }
366            _ => false,
367        }
368    }
369}
370
371impl Eq for GenericUsage {}
372
373/// Compare two container type paths by their ident segments (ignoring lifetimes
374/// and generic arguments). Two containers are "same" if their path idents match.
375///
376/// This is safe because after erasure all container types have the same generic
377/// arity (lifetimes + `Datum<'a>`), so ident equality implies structural equality.
378fn container_idents_match(a: &syn::TypePath, b: &syn::TypePath) -> bool {
379    let a_idents: Vec<_> = a.path.segments.iter().map(|s| &s.ident).collect();
380    let b_idents: Vec<_> = b.path.segments.iter().map(|s| &s.ident).collect();
381    a_idents == b_idents
382}
383
384/// Classifies how a generic type parameter appears in a type.
385///
386/// Strips `Option<...>`, `Result<..., E>`, and `ExcludeNull<...>` wrappers before
387/// inspecting the inner type. Any generic type wrapping `T` that isn't `Option`,
388/// `Result`, or `ExcludeNull` is treated as a container. If the container doesn't
389/// implement `SqlContainerType`, the generated code won't compile (a clear error).
390fn classify_generic_usage(ty: &syn::Type, generic_name: &Ident) -> GenericUsage {
391    match ty {
392        syn::Type::Path(type_path) => {
393            if type_path.path.is_ident(generic_name) {
394                return GenericUsage::Bare;
395            }
396            if let Some(last) = type_path.path.segments.last() {
397                let ident_str = last.ident.to_string();
398                // Unwrap Option, Result, ExcludeNull wrappers
399                if ident_str == "Option" || ident_str == "Result" || ident_str == "ExcludeNull" {
400                    if let syn::PathArguments::AngleBracketed(args) = &last.arguments {
401                        if let Some(syn::GenericArgument::Type(inner)) = args.args.first() {
402                            return classify_generic_usage(inner, generic_name);
403                        }
404                    }
405                }
406                // Check if any angle-bracketed arg contains the generic param.
407                // If so, treat this type as a container.
408                if let syn::PathArguments::AngleBracketed(args) = &last.arguments {
409                    let has_generic_arg = args.args.iter().any(|arg| {
410                        if let syn::GenericArgument::Type(inner) = arg {
411                            type_contains_ident(inner, generic_name)
412                        } else {
413                            false
414                        }
415                    });
416                    if has_generic_arg {
417                        // Build the erased container type path (T → Datum<'a>).
418                        let erased = erase_generic_param(ty, generic_name);
419                        if let syn::Type::Path(erased_path) = erased {
420                            return GenericUsage::InContainer(erased_path);
421                        }
422                    }
423                    // Recurse into args for nested containers
424                    // (e.g., Option<DatumList<'a, T>> was already handled by
425                    // the Option unwrapping above, but handle other nestings)
426                    for arg in &args.args {
427                        if let syn::GenericArgument::Type(inner) = arg {
428                            let inner_usage = classify_generic_usage(inner, generic_name);
429                            if inner_usage != GenericUsage::Absent {
430                                return inner_usage;
431                            }
432                        }
433                    }
434                }
435            }
436            GenericUsage::Absent
437        }
438        syn::Type::Reference(r) => classify_generic_usage(&r.elem, generic_name),
439        syn::Type::Tuple(t) => {
440            // Prefer container usages over bare. For example, `(T, DatumList<'_, T>)`
441            // should classify as `InDatumList`, not `Bare`.
442            let mut best = GenericUsage::Absent;
443            for elem in &t.elems {
444                let usage = classify_generic_usage(elem, generic_name);
445                match (&best, &usage) {
446                    (GenericUsage::Absent, _) => best = usage,
447                    (GenericUsage::Bare, u) if *u != GenericUsage::Absent => best = usage.clone(),
448                    _ => {
449                        if usage != GenericUsage::Absent && usage != best {
450                            // Conflicting container usages — cannot resolve.
451                            return GenericUsage::Bare;
452                        }
453                    }
454                }
455            }
456            best
457        }
458        _ => GenericUsage::Absent,
459    }
460}
461
462/// Returns whether a type syntactically contains an identifier.
463fn type_contains_ident(ty: &syn::Type, ident: &Ident) -> bool {
464    match ty {
465        syn::Type::Path(type_path) => {
466            if type_path.path.is_ident(ident) {
467                return true;
468            }
469            if let Some(last) = type_path.path.segments.last() {
470                if let syn::PathArguments::AngleBracketed(args) = &last.arguments {
471                    return args.args.iter().any(|arg| {
472                        if let syn::GenericArgument::Type(inner) = arg {
473                            type_contains_ident(inner, ident)
474                        } else {
475                            false
476                        }
477                    });
478                }
479            }
480            false
481        }
482        syn::Type::Reference(r) => type_contains_ident(&r.elem, ident),
483        syn::Type::Tuple(t) => t.elems.iter().any(|e| type_contains_ident(e, ident)),
484        _ => false,
485    }
486}
487
488/// Returns whether the outermost wrapper of a type is `Option`.
489fn is_option_wrapped(ty: &syn::Type) -> bool {
490    if let syn::Type::Path(type_path) = ty {
491        if let Some(last) = type_path.path.segments.last() {
492            return last.ident == "Option";
493        }
494    }
495    false
496}
497
498/// Derives an `output_type_expr` TokenStream from the structural relationship
499/// between input types and the output type, based on where generic parameters appear.
500///
501/// Finds the first generic parameter that appears in the output type, then looks for
502/// the first input parameter containing that generic in a container type to determine
503/// the unwrap strategy.
504///
505/// `is_unary` controls whether the generated expression uses `input_type`
506/// (singular, for unary functions) or `input_types[i]` (indexed, for binary/variadic).
507///
508/// Returns `None` if no generic parameter appears in the output type.
509fn derive_output_type_for_generics(
510    input_types: &[syn::Type],
511    output_ty: &syn::Type,
512    generic_names: &[Ident],
513    is_unary: bool,
514) -> darling::Result<Option<TokenStream>> {
515    // Find the first generic param that appears in the output.
516    let generic_name = match generic_names
517        .iter()
518        .find(|gn| classify_generic_usage(output_ty, gn) != GenericUsage::Absent)
519    {
520        Some(gn) => gn,
521        None => return Ok(None),
522    };
523    derive_output_type_for_generic(input_types, output_ty, generic_name, is_unary)
524}
525
526/// Derives an `output_type_expr` for a single generic parameter.
527///
528/// Uses `SqlContainerType` trait calls instead of matching on specific container
529/// type names. The generated code calls `<Container as SqlContainerType>::unwrap_element_type()`
530/// and `wrap_element_type()`, letting Rust's type system resolve the correct behavior.
531fn derive_output_type_for_generic(
532    input_types: &[syn::Type],
533    output_ty: &syn::Type,
534    generic_name: &Ident,
535    is_unary: bool,
536) -> darling::Result<Option<TokenStream>> {
537    let output_usage = classify_generic_usage(output_ty, generic_name);
538    if output_usage == GenericUsage::Absent {
539        return Ok(None);
540    }
541
542    let nullable = is_option_wrapped(output_ty);
543
544    // Find the first input parameter that has T in a container.
545    // Prefer container inputs over bare inputs.
546    let mut container_input: Option<(usize, GenericUsage)> = None;
547    for (i, ty) in input_types.iter().enumerate() {
548        let usage = classify_generic_usage(ty, generic_name);
549        match &usage {
550            GenericUsage::InContainer(_) => {
551                container_input = Some((i, usage));
552                break;
553            }
554            GenericUsage::Bare => {
555                // Bare T in input — not a container, keep looking for a container.
556                if container_input.is_none() {
557                    container_input = Some((i, usage));
558                }
559            }
560            GenericUsage::Absent => {}
561        }
562    }
563
564    let (pos, source_usage) = container_input.ok_or_else(|| {
565        darling::Error::custom(
566            "generic parameter T appears in the output type but not in any input type",
567        )
568    })?;
569
570    // Generate the base expression to access the input type.
571    let input_access = if is_unary {
572        quote! { input_type }
573    } else {
574        let pos_lit = syn::Index::from(pos);
575        quote! { input_types[#pos_lit] }
576    };
577
578    // For multi-input functions, generate soft assertions that all inputs
579    // carrying T agree on the SQL element type. This catches bugs in the
580    // planner's overload resolution or cast insertion.
581    let consistency_checks = if !is_unary {
582        let mut checks = Vec::new();
583        for (i, ty) in input_types.iter().enumerate() {
584            if i == pos {
585                continue;
586            }
587            let usage = classify_generic_usage(ty, generic_name);
588            if usage == GenericUsage::Absent {
589                continue;
590            }
591            let primary_elem = element_type_expr(&input_access, &source_usage);
592            let i_lit = syn::Index::from(i);
593            let other_access = quote! { input_types[#i_lit] };
594            let other_elem = element_type_expr(&other_access, &usage);
595            let generic_str = generic_name.to_string();
596            checks.push(quote! {
597                mz_ore::soft_assert_or_log!(
598                    #primary_elem.base_eq(#other_elem),
599                    "auto-derived sqlfunc output type inference found inconsistent \
600                     SQL types for generic {} across inputs: {:?} vs {:?}; \
601                     this indicates a bug in polymorphic coercion, builtin \
602                     declaration, or sqlfunc inference",
603                    #generic_str,
604                    #primary_elem,
605                    #other_elem,
606                );
607            });
608        }
609        quote! { #(#checks)* }
610    } else {
611        quote! {}
612    };
613
614    // Now generate the output_type_expr based on the combination of
615    // source container and output usage.
616    let expr = match (&output_usage, &source_usage) {
617        // Output is bare T, source is a container → unwrap element type via trait.
618        (GenericUsage::Bare, GenericUsage::InContainer(in_container)) => {
619            let in_c = elide_lifetimes(in_container);
620            quote! {
621                {
622                    #consistency_checks
623                    <#in_c as mz_repr::SqlContainerType>::unwrap_element_type(
624                        &#input_access.scalar_type
625                    ).clone().nullable(#nullable)
626                }
627            }
628        }
629        // Output is bare T, source is bare T → forward input type directly.
630        (GenericUsage::Bare, GenericUsage::Bare) => {
631            quote! {
632                {
633                    #consistency_checks
634                    #input_access.scalar_type.clone().nullable(#nullable)
635                }
636            }
637        }
638        // Output is a container, source is a container (same or different) →
639        // unwrap from input container, wrap into output container via traits.
640        (GenericUsage::InContainer(out_container), GenericUsage::InContainer(in_container)) => {
641            let out_c = elide_lifetimes(out_container);
642            let in_c = elide_lifetimes(in_container);
643            quote! {
644                {
645                    #consistency_checks
646                    <#out_c as mz_repr::SqlContainerType>::wrap_element_type(
647                        <#in_c as mz_repr::SqlContainerType>::unwrap_element_type(
648                            &#input_access.scalar_type
649                        ).clone()
650                    ).nullable(#nullable)
651                }
652            }
653        }
654        // Other cases — user must provide explicit output_type_expr.
655        _ => {
656            return Err(darling::Error::custom(format!(
657                "cannot auto-derive output_type_expr: output uses T as {:?} but \
658                 the first T-containing input uses T as {:?}",
659                output_usage, source_usage
660            )));
661        }
662    };
663
664    Ok(Some(expr))
665}
666
667/// Generates a token stream that extracts the T-level SQL type from an input
668/// access expression, based on how T is used in that input.
669fn element_type_expr(input_access: &TokenStream, usage: &GenericUsage) -> TokenStream {
670    match usage {
671        GenericUsage::Bare => {
672            quote! { &#input_access.scalar_type }
673        }
674        GenericUsage::InContainer(container) => {
675            let c = elide_lifetimes(container);
676            quote! {
677                <#c as mz_repr::SqlContainerType>::unwrap_element_type(
678                    &#input_access.scalar_type
679                )
680            }
681        }
682        GenericUsage::Absent => unreachable!("element_type_expr called with Absent usage"),
683    }
684}
685
686/// Replaces all lifetime parameters in a `syn::TypePath` with `'_`.
687///
688/// Used for container type paths in turbofish position (e.g.
689/// `<DatumList<'_, Datum<'_>> as SqlContainerType>::...`).
690/// The `output_sql_type` method's `&self` provides an implicit lifetime
691/// that the compiler can infer through `'_`.
692fn elide_lifetimes(tp: &syn::TypePath) -> syn::TypePath {
693    let mut tp = tp.clone();
694    for segment in &mut tp.path.segments {
695        if let syn::PathArguments::AngleBracketed(args) = &mut segment.arguments {
696            for arg in &mut args.args {
697                match arg {
698                    syn::GenericArgument::Lifetime(lt) => {
699                        *lt = Lifetime::new("'_", lt.span());
700                    }
701                    syn::GenericArgument::Type(ty) => {
702                        elide_lifetimes_in_type(ty);
703                    }
704                    _ => {}
705                }
706            }
707        }
708    }
709    tp
710}
711
712/// Recursively replaces all lifetime parameters in a `syn::Type` with `'_`.
713fn elide_lifetimes_in_type(ty: &mut syn::Type) {
714    match ty {
715        syn::Type::Path(tp) => {
716            *tp = elide_lifetimes(tp);
717        }
718        syn::Type::Reference(r) => {
719            if let Some(lt) = &mut r.lifetime {
720                *lt = Lifetime::new("'_", lt.span());
721            }
722            elide_lifetimes_in_type(&mut r.elem);
723        }
724        syn::Type::Tuple(t) => {
725            for elem in &mut t.elems {
726                elide_lifetimes_in_type(elem);
727            }
728        }
729        _ => {}
730    }
731}
732
733/// Replaces occurrences of a generic type parameter with `Datum<'a>` in a type.
734///
735/// Used to convert types from the user's generic function signature into concrete
736/// types for the generated trait impl's associated types, where `T` is not in scope.
737fn erase_generic_param(ty: &syn::Type, generic_name: &Ident) -> syn::Type {
738    match ty {
739        syn::Type::Path(type_path) => {
740            if type_path.path.is_ident(generic_name) {
741                return syn::parse_quote!(Datum<'a>);
742            }
743            let mut type_path = type_path.clone();
744            for segment in &mut type_path.path.segments {
745                if let syn::PathArguments::AngleBracketed(args) = &mut segment.arguments {
746                    for arg in &mut args.args {
747                        if let syn::GenericArgument::Type(inner) = arg {
748                            *inner = erase_generic_param(inner, generic_name);
749                        }
750                    }
751                }
752            }
753            syn::Type::Path(type_path)
754        }
755        syn::Type::Reference(r) => {
756            let elem = Box::new(erase_generic_param(&r.elem, generic_name));
757            syn::Type::Reference(syn::TypeReference { elem, ..r.clone() })
758        }
759        syn::Type::Tuple(t) => {
760            let elems = t
761                .elems
762                .iter()
763                .map(|e| erase_generic_param(e, generic_name))
764                .collect();
765            syn::Type::Tuple(syn::TypeTuple { elems, ..t.clone() })
766        }
767        _ => ty.clone(),
768    }
769}
770
771/// Erases all generic type parameters from a type, replacing each with `Datum<'a>`.
772fn erase_all_generic_params(ty: &syn::Type, generic_names: &[Ident]) -> syn::Type {
773    let mut ty = ty.clone();
774    for gn in generic_names {
775        ty = erase_generic_param(&ty, gn);
776    }
777    ty
778}
779
780/// Determines the argument type of the nth argument of the function.
781///
782/// Adds a lifetime `'a` to the argument type if it is a reference type.
783///
784/// Panics if the function has fewer than `nth` arguments. Returns an error if
785/// the parameter is a `self` receiver.
786fn arg_type(arg: &syn::ItemFn, nth: usize) -> Result<syn::Type, syn::Error> {
787    match &arg.sig.inputs[nth] {
788        syn::FnArg::Typed(pat) => {
789            // Patch lifetimes to be 'a if reference
790            if let syn::Type::Reference(r) = &*pat.ty {
791                if r.lifetime.is_none() {
792                    let ty = syn::Type::Reference(syn::TypeReference {
793                        lifetime: Some(Lifetime::new("'a", r.span())),
794                        ..r.clone()
795                    });
796                    return Ok(ty);
797                }
798            }
799            Ok((*pat.ty).clone())
800        }
801        syn::FnArg::Receiver(_) => Err(syn::Error::new(
802            arg.sig.inputs[nth].span(),
803            "Unsupported argument type",
804        )),
805    }
806}
807
808/// Recursively patches lifetimes in a type, adding `'a` to references without a lifetime
809/// and recursing into generic arguments and tuples.
810fn patch_lifetimes(ty: &syn::Type) -> syn::Type {
811    match ty {
812        syn::Type::Reference(r) => {
813            let elem = Box::new(patch_lifetimes(&r.elem));
814            if r.lifetime.is_none() {
815                syn::Type::Reference(syn::TypeReference {
816                    lifetime: Some(Lifetime::new("'a", r.span())),
817                    elem,
818                    ..r.clone()
819                })
820            } else {
821                syn::Type::Reference(syn::TypeReference { elem, ..r.clone() })
822            }
823        }
824        syn::Type::Tuple(t) => {
825            let elems = t.elems.iter().map(patch_lifetimes).collect();
826            syn::Type::Tuple(syn::TypeTuple { elems, ..t.clone() })
827        }
828        syn::Type::Path(p) => {
829            let mut p = p.clone();
830            for segment in &mut p.path.segments {
831                if let syn::PathArguments::AngleBracketed(args) = &mut segment.arguments {
832                    for arg in &mut args.args {
833                        if let syn::GenericArgument::Type(ty) = arg {
834                            *ty = patch_lifetimes(ty);
835                        }
836                    }
837                }
838            }
839            syn::Type::Path(p)
840        }
841        _ => ty.clone(),
842    }
843}
844
845/// Determine the output type for a function. Returns an error if the function
846/// does not return a value.
847fn output_type(arg: &syn::ItemFn) -> Result<&syn::Type, syn::Error> {
848    match &arg.sig.output {
849        syn::ReturnType::Type(_, ty) => Ok(&*ty),
850        syn::ReturnType::Default => Err(syn::Error::new(
851            arg.sig.output.span(),
852            "Function needs to return a value",
853        )),
854    }
855}
856
857/// Produce a `EagerUnaryFunc` implementation.
858fn unary_func(func: &syn::ItemFn, modifiers: Modifiers) -> darling::Result<TokenStream> {
859    let fn_name = &func.sig.ident;
860    let struct_name = camel_case(&func.sig.ident);
861    let input_ty_raw = arg_type(func, 0)?;
862    let output_ty_raw = output_type(func)?;
863    let generic_params = find_generic_type_params(func);
864    // Erase generic type params → Datum<'a> for use in the trait impl's associated types.
865    let input_ty = erase_all_generic_params(&input_ty_raw, &generic_params);
866    let output_ty = erase_all_generic_params(output_ty_raw, &generic_params);
867    let Modifiers {
868        is_monotone,
869        sqlname,
870        preserves_uniqueness,
871        inverse,
872        is_infix_op,
873        output_type,
874        mut output_type_expr,
875        negate,
876        could_error,
877        propagates_nulls,
878        mut introduces_nulls,
879        is_associative,
880        is_eliminable_cast,
881        is_infinity_monotone: _,
882        test: _,
883    } = modifiers;
884
885    // If generic type parameters are present and no explicit output_type_expr,
886    // auto-derive one from the structural relationship between input and output types.
887    // Use raw (pre-erasure) types so we can see the generic parameters.
888    if !generic_params.is_empty() {
889        if output_type_expr.is_none() && output_type.is_none() {
890            if let Some(derived) = derive_output_type_for_generics(
891                &[input_ty_raw],
892                output_ty_raw,
893                &generic_params,
894                true,
895            )? {
896                output_type_expr = Some(syn::parse2(derived)?);
897                if introduces_nulls.is_none() {
898                    let nullable = is_option_wrapped(output_ty_raw);
899                    introduces_nulls = Some(syn::parse_quote!(#nullable));
900                }
901            }
902        }
903    }
904
905    if is_infix_op.is_some() {
906        return Err(darling::Error::unknown_field(
907            "is_infix_op not supported for unary functions",
908        ));
909    }
910    if output_type.is_some() && output_type_expr.is_some() {
911        return Err(darling::Error::unknown_field(
912            "output_type and output_type_expr cannot be used together",
913        ));
914    }
915    if output_type_expr.is_some() && introduces_nulls.is_none() {
916        return Err(darling::Error::unknown_field(
917            "output_type_expr requires introduces_nulls",
918        ));
919    }
920    if negate.is_some() {
921        return Err(darling::Error::unknown_field(
922            "negate not supported for unary functions",
923        ));
924    }
925    if propagates_nulls.is_some() {
926        return Err(darling::Error::unknown_field(
927            "propagates_nulls not supported for unary functions",
928        ));
929    }
930    if is_associative.is_some() {
931        return Err(darling::Error::unknown_field(
932            "is_associative not supported for unary functions",
933        ));
934    }
935
936    let preserves_uniqueness_fn = preserves_uniqueness.map(|preserves_uniqueness| {
937        quote! {
938            fn preserves_uniqueness(&self) -> bool {
939                #preserves_uniqueness
940            }
941        }
942    });
943
944    let inverse_fn = inverse.as_ref().map(|inverse| {
945        quote! {
946            fn inverse(&self) -> Option<crate::UnaryFunc> {
947                #inverse
948            }
949        }
950    });
951
952    let is_monotone_fn = is_monotone.map(|is_monotone| {
953        quote! {
954            fn is_monotone(&self) -> bool {
955                #is_monotone
956            }
957        }
958    });
959
960    let name = sqlname
961        .as_ref()
962        .map_or_else(|| quote! { stringify!(#fn_name) }, |name| quote! { #name });
963
964    let (mut output_type, mut introduces_nulls_fn) = if let Some(output_type) = output_type {
965        let introduces_nulls_fn = quote! {
966            fn introduces_nulls(&self) -> bool {
967                <#output_type as ::mz_repr::OutputDatumType<'_, ()>>::nullable()
968            }
969        };
970        let output_type = quote! { <#output_type>::as_column_type() };
971        (output_type, Some(introduces_nulls_fn))
972    } else {
973        (quote! { Self::Output::as_column_type() }, None)
974    };
975
976    if let Some(output_type_expr) = output_type_expr {
977        output_type = quote! { #output_type_expr };
978    }
979
980    if let Some(introduces_nulls) = introduces_nulls {
981        introduces_nulls_fn = Some(quote! {
982            fn introduces_nulls(&self) -> bool {
983                #introduces_nulls
984            }
985        });
986    }
987
988    let could_error_fn = could_error.map(|could_error| {
989        quote! {
990            fn could_error(&self) -> bool {
991                #could_error
992            }
993        }
994    });
995
996    let is_eliminable_cast_fn = is_eliminable_cast.map(|is_eliminable_cast| {
997        quote! {
998            fn is_eliminable_cast(&self) -> bool {
999                #is_eliminable_cast
1000            }
1001        }
1002    });
1003
1004    let result = quote! {
1005        #[derive(
1006            Ord, PartialOrd, Clone,
1007            Debug, Eq, PartialEq, serde::Serialize,
1008            serde::Deserialize, Hash, mz_lowertest::MzReflect,
1009        )]
1010        #[cfg_attr(any(test, feature = "proptest"), derive(proptest_derive::Arbitrary))]
1011        pub struct #struct_name;
1012
1013        impl crate::func::EagerUnaryFunc for #struct_name {
1014            type Input<'a> = #input_ty;
1015            type Output<'a> = #output_ty;
1016
1017            fn call<'a>(&self, a: Self::Input<'a>) -> Self::Output<'a> {
1018                #fn_name(a)
1019            }
1020
1021            fn output_sql_type(
1022                &self,
1023                input_type: mz_repr::SqlColumnType
1024            ) -> mz_repr::SqlColumnType {
1025                use mz_repr::AsColumnType;
1026                let output = #output_type;
1027                let propagates_nulls = crate::func::EagerUnaryFunc::propagates_nulls(self);
1028                let nullable = output.nullable;
1029                // The output is nullable if it is nullable by itself or the input is nullable
1030                // and this function propagates nulls
1031                output.nullable(nullable || (propagates_nulls && input_type.nullable))
1032            }
1033
1034            #could_error_fn
1035            #introduces_nulls_fn
1036            #inverse_fn
1037            #is_monotone_fn
1038            #preserves_uniqueness_fn
1039            #is_eliminable_cast_fn
1040        }
1041
1042        impl std::fmt::Display for #struct_name {
1043            fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
1044                f.write_str(#name)
1045            }
1046        }
1047
1048        #func
1049    };
1050    Ok(result)
1051}
1052
1053/// Produce a `EagerBinaryFunc` implementation.
1054fn binary_func(
1055    func: &syn::ItemFn,
1056    modifiers: Modifiers,
1057    arena: bool,
1058) -> darling::Result<TokenStream> {
1059    let fn_name = &func.sig.ident;
1060    let struct_name = camel_case(&func.sig.ident);
1061    let input1_ty_raw = arg_type(func, 0)?;
1062    let input2_ty_raw = arg_type(func, 1)?;
1063    let output_ty_raw = output_type(func)?;
1064    let generic_params = find_generic_type_params(func);
1065    // Erase generic type params → Datum<'a> for use in the trait impl's associated types.
1066    let input1_ty = erase_all_generic_params(&input1_ty_raw, &generic_params);
1067    let input2_ty = erase_all_generic_params(&input2_ty_raw, &generic_params);
1068    let output_ty = erase_all_generic_params(output_ty_raw, &generic_params);
1069
1070    let Modifiers {
1071        is_monotone,
1072        sqlname,
1073        preserves_uniqueness,
1074        inverse,
1075        is_infix_op,
1076        output_type,
1077        mut output_type_expr,
1078        negate,
1079        could_error,
1080        propagates_nulls,
1081        mut introduces_nulls,
1082        is_associative,
1083        is_eliminable_cast,
1084        is_infinity_monotone,
1085        test: _,
1086    } = modifiers;
1087
1088    // Auto-derive output_type_expr from generic parameters, if applicable.
1089    // Use raw (pre-erasure) types so we can see the generic parameters.
1090    if !generic_params.is_empty() {
1091        if output_type_expr.is_none() && output_type.is_none() {
1092            if let Some(derived) = derive_output_type_for_generics(
1093                &[input1_ty_raw, input2_ty_raw],
1094                output_ty_raw,
1095                &generic_params,
1096                false,
1097            )? {
1098                output_type_expr = Some(syn::parse2(derived)?);
1099                if introduces_nulls.is_none() {
1100                    let nullable = is_option_wrapped(output_ty_raw);
1101                    introduces_nulls = Some(syn::parse_quote!(#nullable));
1102                }
1103            }
1104        }
1105    }
1106
1107    if preserves_uniqueness.is_some() {
1108        return Err(darling::Error::unknown_field(
1109            "preserves_uniqueness not supported for binary functions",
1110        ));
1111    }
1112    if inverse.is_some() {
1113        return Err(darling::Error::unknown_field(
1114            "inverse not supported for binary functions",
1115        ));
1116    }
1117    if output_type.is_some() && output_type_expr.is_some() {
1118        return Err(darling::Error::unknown_field(
1119            "output_type and output_type_expr cannot be used together",
1120        ));
1121    }
1122    if output_type_expr.is_some() && introduces_nulls.is_none() {
1123        return Err(darling::Error::unknown_field(
1124            "output_type_expr requires introduces_nulls",
1125        ));
1126    }
1127    if is_associative.is_some() {
1128        return Err(darling::Error::unknown_field(
1129            "is_associative not supported for binary functions",
1130        ));
1131    }
1132    if is_eliminable_cast.is_some() {
1133        return Err(darling::Error::unknown_field(
1134            "is_eliminable_cast not supported for binary functions",
1135        ));
1136    }
1137
1138    let negate_fn = negate.map(|negate| {
1139        quote! {
1140            fn negate(&self) -> Option<crate::BinaryFunc> {
1141                #negate
1142            }
1143        }
1144    });
1145
1146    let is_monotone_fn = is_monotone.map(|is_monotone| {
1147        quote! {
1148            fn is_monotone(&self) -> (bool, bool) {
1149                #is_monotone
1150            }
1151        }
1152    });
1153
1154    let is_infinity_monotone_fn = is_infinity_monotone.map(|is_infinity_monotone| {
1155        quote! {
1156            fn is_infinity_monotone(&self) -> bool {
1157                #is_infinity_monotone
1158            }
1159        }
1160    });
1161
1162    let name = sqlname
1163        .as_ref()
1164        .map_or_else(|| quote! { stringify!(#fn_name) }, |name| quote! { #name });
1165
1166    let (mut output_type, mut introduces_nulls_fn) = if let Some(output_type) = output_type {
1167        let introduces_nulls_fn = quote! {
1168            fn introduces_nulls(&self) -> bool {
1169                <#output_type as ::mz_repr::OutputDatumType<'_, ()>>::nullable()
1170            }
1171        };
1172        let output_type = quote! { <#output_type>::as_column_type() };
1173        (output_type, Some(introduces_nulls_fn))
1174    } else {
1175        (quote! { Self::Output::as_column_type() }, None)
1176    };
1177
1178    if let Some(output_type_expr) = output_type_expr {
1179        output_type = quote! { #output_type_expr };
1180    }
1181
1182    if let Some(introduces_nulls) = introduces_nulls {
1183        introduces_nulls_fn = Some(quote! {
1184            fn introduces_nulls(&self) -> bool {
1185                #introduces_nulls
1186            }
1187        });
1188    }
1189
1190    let arena = if arena {
1191        quote! { , temp_storage }
1192    } else {
1193        quote! {}
1194    };
1195
1196    let could_error_fn = could_error.map(|could_error| {
1197        quote! {
1198            fn could_error(&self) -> bool {
1199                #could_error
1200            }
1201        }
1202    });
1203
1204    let is_infix_op_fn = is_infix_op.map(|is_infix_op| {
1205        quote! {
1206            fn is_infix_op(&self) -> bool {
1207                #is_infix_op
1208            }
1209        }
1210    });
1211
1212    let propagates_nulls_fn = propagates_nulls.map(|propagates_nulls| {
1213        quote! {
1214            fn propagates_nulls(&self) -> bool {
1215                #propagates_nulls
1216            }
1217        }
1218    });
1219
1220    // Per-position checks: for each non-nullable parameter, check if
1221    // the corresponding input column is nullable.
1222    let binary_non_nullable_checks =
1223        non_nullable_position_checks(&[input1_ty.clone(), input2_ty.clone()]);
1224
1225    let result = quote! {
1226        #[derive(
1227            Ord, PartialOrd, Clone,
1228            Debug, Eq, PartialEq, serde::Serialize,
1229            serde::Deserialize, Hash, mz_lowertest::MzReflect,
1230        )]
1231        #[cfg_attr(any(test, feature = "proptest"), derive(proptest_derive::Arbitrary))]
1232        pub struct #struct_name;
1233
1234        impl crate::func::binary::EagerBinaryFunc for #struct_name {
1235            type Input<'a> = (#input1_ty, #input2_ty);
1236            type Output<'a> = #output_ty;
1237
1238            fn call<'a>(
1239                &self,
1240                (a, b): Self::Input<'a>,
1241                temp_storage: &'a mz_repr::RowArena
1242            ) -> Self::Output<'a> {
1243                #fn_name(a, b #arena)
1244            }
1245
1246            fn output_sql_type(
1247                &self,
1248                input_types: &[mz_repr::SqlColumnType],
1249            ) -> mz_repr::SqlColumnType {
1250                use mz_repr::AsColumnType;
1251                let output = #output_type;
1252                let propagates_nulls =
1253                    crate::func::binary::EagerBinaryFunc::propagates_nulls(self);
1254                let nullable = output.nullable;
1255                // The output is nullable if:
1256                // 1. The function introduces nulls (output.nullable), or
1257                // 2. A non-nullable parameter's input is nullable (will reject
1258                //    NULL at runtime via try_from_iter), or
1259                // 3. propagates_nulls is true and any input is nullable
1260                //    (optimizer short-circuits all-NULL inputs)
1261                let non_nullable_input_is_nullable =
1262                    false #(#binary_non_nullable_checks)*;
1263                let inputs_nullable = input_types.iter().any(|it| it.nullable);
1264                let is_null = nullable
1265                    || non_nullable_input_is_nullable
1266                    || (propagates_nulls && inputs_nullable);
1267                output.nullable(is_null)
1268            }
1269
1270            #could_error_fn
1271            #introduces_nulls_fn
1272            #is_infix_op_fn
1273            #is_monotone_fn
1274            #is_infinity_monotone_fn
1275            #negate_fn
1276            #propagates_nulls_fn
1277        }
1278
1279        impl std::fmt::Display for #struct_name {
1280            fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
1281                f.write_str(#name)
1282            }
1283        }
1284
1285        #func
1286
1287    };
1288    Ok(result)
1289}
1290
1291/// Produce an `EagerVariadicFunc` implementation.
1292///
1293/// Two modes based on whether the function has a `&self` receiver:
1294/// * `&self` present: struct defined externally, generates method impl + trait impl + Display
1295/// * No `&self`: generates unit struct + trait impl + Display + preserves original function
1296fn variadic_func(
1297    func: &syn::ItemFn,
1298    modifiers: Modifiers,
1299    struct_ty: Option<syn::Path>,
1300    arena: bool,
1301    has_self: bool,
1302) -> darling::Result<TokenStream> {
1303    let fn_name = &func.sig.ident;
1304    let output_ty_raw = output_type(func)?;
1305    let generic_params = find_generic_type_params(func);
1306    let output_ty = erase_all_generic_params(output_ty_raw, &generic_params);
1307    let struct_name = struct_ty
1308        .as_ref()
1309        .and_then(|ty| ty.segments.last())
1310        .map_or_else(|| camel_case(fn_name), |seg| seg.ident.clone());
1311
1312    let Modifiers {
1313        is_monotone,
1314        sqlname,
1315        preserves_uniqueness,
1316        inverse,
1317        is_infix_op,
1318        output_type,
1319        mut output_type_expr,
1320        negate,
1321        could_error,
1322        propagates_nulls,
1323        mut introduces_nulls,
1324        is_associative,
1325        is_eliminable_cast,
1326        is_infinity_monotone: _,
1327        test: _,
1328    } = modifiers;
1329
1330    // Reject modifiers that don't apply to variadic functions.
1331    if preserves_uniqueness.is_some() {
1332        return Err(darling::Error::unknown_field(
1333            "preserves_uniqueness not supported for variadic functions",
1334        ));
1335    }
1336    if inverse.is_some() {
1337        return Err(darling::Error::unknown_field(
1338            "inverse not supported for variadic functions",
1339        ));
1340    }
1341    if negate.is_some() {
1342        return Err(darling::Error::unknown_field(
1343            "negate not supported for variadic functions",
1344        ));
1345    }
1346    if is_eliminable_cast.is_some() {
1347        return Err(darling::Error::unknown_field(
1348            "is_eliminable_cast not supported for variadic functions",
1349        ));
1350    }
1351    if output_type.is_some() && output_type_expr.is_some() {
1352        return Err(darling::Error::unknown_field(
1353            "output_type and output_type_expr cannot be used together",
1354        ));
1355    }
1356    if output_type_expr.is_some() && introduces_nulls.is_none() {
1357        return Err(darling::Error::unknown_field(
1358            "output_type_expr requires introduces_nulls",
1359        ));
1360    }
1361
1362    // Collect input parameters (skip &self, skip &RowArena).
1363    let start = if has_self { 1 } else { 0 };
1364    let end = if arena {
1365        func.sig.inputs.len() - 1
1366    } else {
1367        func.sig.inputs.len()
1368    };
1369    let input_params: Vec<&syn::FnArg> = func
1370        .sig
1371        .inputs
1372        .iter()
1373        .skip(start)
1374        .take(end - start)
1375        .collect();
1376
1377    if input_params.is_empty() {
1378        return Err(darling::Error::custom(
1379            "variadic function must have at least one input parameter",
1380        ));
1381    }
1382
1383    // Extract parameter names and types.
1384    let mut param_names = Vec::new();
1385    let mut param_types = Vec::new();
1386    for param in &input_params {
1387        match param {
1388            syn::FnArg::Typed(pat) => {
1389                if let syn::Pat::Ident(ident) = &*pat.pat {
1390                    param_names.push(ident.ident.clone());
1391                } else {
1392                    return Err(
1393                        darling::Error::custom("unsupported parameter pattern").with_span(&pat.pat)
1394                    );
1395                }
1396                param_types.push(patch_lifetimes(&pat.ty));
1397            }
1398            syn::FnArg::Receiver(_) => {
1399                return Err(darling::Error::custom("unexpected self parameter"));
1400            }
1401        }
1402    }
1403
1404    // Auto-derive output_type_expr from generic parameters, if applicable.
1405    // Use raw (pre-erasure) types so we can see the generic parameters.
1406    if !generic_params.is_empty() {
1407        if output_type_expr.is_none() && output_type.is_none() {
1408            if let Some(derived) = derive_output_type_for_generics(
1409                &param_types,
1410                output_ty_raw,
1411                &generic_params,
1412                false,
1413            )? {
1414                output_type_expr = Some(syn::parse2(derived)?);
1415                if introduces_nulls.is_none() {
1416                    let nullable = is_option_wrapped(output_ty_raw);
1417                    introduces_nulls = Some(syn::parse_quote!(#nullable));
1418                }
1419            }
1420        }
1421    }
1422
1423    // Erase generic type params → Datum<'a> in param types for the trait impl's associated types.
1424    for ty in &mut param_types {
1425        *ty = erase_all_generic_params(ty, &generic_params);
1426    }
1427
1428    // Build input type: single param = bare type, multiple = tuple.
1429    let input_type: syn::Type = if param_types.len() == 1 {
1430        param_types[0].clone()
1431    } else {
1432        syn::parse_quote! { (#(#param_types),*) }
1433    };
1434
1435    // Build destructure pattern for call.
1436    let destructure = if param_names.len() == 1 {
1437        let name = &param_names[0];
1438        quote! { #name }
1439    } else {
1440        quote! { (#(#param_names),*) }
1441    };
1442
1443    let arena_arg = if arena {
1444        quote! { , temp_storage }
1445    } else {
1446        quote! {}
1447    };
1448
1449    let call_expr = if has_self {
1450        quote! { self.#fn_name(#(#param_names),* #arena_arg) }
1451    } else {
1452        quote! { #fn_name(#(#param_names),* #arena_arg) }
1453    };
1454
1455    // Build modifier functions.
1456    let name = sqlname
1457        .as_ref()
1458        .map_or_else(|| quote! { stringify!(#fn_name) }, |name| quote! { #name });
1459
1460    let (mut output_type_code, mut introduces_nulls_fn) = if let Some(output_type) = output_type {
1461        let introduces_nulls_fn = quote! {
1462            fn introduces_nulls(&self) -> bool {
1463                <#output_type as ::mz_repr::OutputDatumType<'_, ()>>::nullable()
1464            }
1465        };
1466        let output_type_code = quote! { <#output_type>::as_column_type() };
1467        (output_type_code, Some(introduces_nulls_fn))
1468    } else {
1469        (quote! { Self::Output::as_column_type() }, None)
1470    };
1471
1472    if let Some(output_type_expr) = output_type_expr {
1473        output_type_code = quote! { #output_type_expr };
1474    }
1475
1476    if let Some(introduces_nulls) = introduces_nulls {
1477        introduces_nulls_fn = Some(quote! {
1478            fn introduces_nulls(&self) -> bool {
1479                #introduces_nulls
1480            }
1481        });
1482    }
1483
1484    let could_error_fn = could_error.map(|could_error| {
1485        quote! {
1486            fn could_error(&self) -> bool {
1487                #could_error
1488            }
1489        }
1490    });
1491
1492    let is_monotone_fn = is_monotone.map(|is_monotone| {
1493        quote! {
1494            fn is_monotone(&self) -> bool {
1495                #is_monotone
1496            }
1497        }
1498    });
1499
1500    let is_associative_fn = is_associative.map(|is_associative| {
1501        quote! {
1502            fn is_associative(&self) -> bool {
1503                #is_associative
1504            }
1505        }
1506    });
1507
1508    let is_infix_op_fn = is_infix_op.map(|is_infix_op| {
1509        quote! {
1510            fn is_infix_op(&self) -> bool {
1511                #is_infix_op
1512            }
1513        }
1514    });
1515
1516    let propagates_nulls_fn = propagates_nulls.map(|propagates_nulls| {
1517        quote! {
1518            fn propagates_nulls(&self) -> bool {
1519                #propagates_nulls
1520            }
1521        }
1522    });
1523
1524    // Per-position checks: for each non-nullable parameter, check if
1525    // the corresponding input column is nullable.
1526    let non_nullable_checks = non_nullable_position_checks(&param_types);
1527
1528    let trait_impl = quote! {
1529        impl crate::func::variadic::EagerVariadicFunc for #struct_name {
1530            type Input<'a> = #input_type;
1531            type Output<'a> = #output_ty;
1532
1533            fn call<'a>(
1534                &self,
1535                #destructure: Self::Input<'a>,
1536                temp_storage: &'a mz_repr::RowArena,
1537            ) -> Self::Output<'a> {
1538                #call_expr
1539            }
1540
1541            fn output_type(
1542                &self,
1543                input_types: &[mz_repr::SqlColumnType],
1544            ) -> mz_repr::SqlColumnType {
1545                use mz_repr::AsColumnType;
1546                let output = #output_type_code;
1547                let propagates_nulls =
1548                    crate::func::variadic::EagerVariadicFunc::propagates_nulls(self);
1549                let nullable = output.nullable;
1550                // The output is nullable if:
1551                // 1. The function introduces nulls (output.nullable), or
1552                // 2. A non-nullable parameter's input is nullable (will reject
1553                //    NULL at runtime via try_from_iter), or
1554                // 3. propagates_nulls is true and any input is nullable
1555                //    (optimizer short-circuits all-NULL inputs)
1556                let non_nullable_input_is_nullable =
1557                    false #(#non_nullable_checks)*;
1558                let inputs_nullable = input_types.iter().any(|it| it.nullable);
1559                output.nullable(
1560                    nullable
1561                    || non_nullable_input_is_nullable
1562                    || (propagates_nulls && inputs_nullable)
1563                )
1564            }
1565
1566            #could_error_fn
1567            #introduces_nulls_fn
1568            #is_infix_op_fn
1569            #is_monotone_fn
1570            #is_associative_fn
1571            #propagates_nulls_fn
1572        }
1573    };
1574
1575    let display_impl = quote! {
1576        impl std::fmt::Display for #struct_name {
1577            fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
1578                f.write_str(#name)
1579            }
1580        }
1581    };
1582
1583    let result = if has_self {
1584        // External struct: generate method impl + trait impl + Display.
1585        quote! {
1586            impl #struct_name {
1587                #func
1588            }
1589            #trait_impl
1590            #display_impl
1591        }
1592    } else {
1593        // Unit struct: generate struct + trait impl + Display + original function.
1594        quote! {
1595            #[derive(
1596                Ord, PartialOrd, Clone,
1597                Debug, Eq, PartialEq, serde::Serialize,
1598                serde::Deserialize, Hash, mz_lowertest::MzReflect,
1599            )]
1600            #[cfg_attr(any(test, feature = "proptest"), derive(proptest_derive::Arbitrary))]
1601            pub struct #struct_name;
1602
1603            #trait_impl
1604            #display_impl
1605
1606            #func
1607        }
1608    };
1609
1610    Ok(result)
1611}