Skip to main content

reddb_types/
polymorphic.rs

1//! Polymorphic pseudo-types — Fase 3 extension.
2//!
3//! PG-style `anyelement` / `anyarray` / `anynonarray` /
4//! `anycompatible` family. These don't exist as concrete
5//! `DataType` variants because the analyzer instantiates them
6//! fresh at every call site — a function with signature
7//! `array_append(anyarray, anyelement) → anyarray` becomes a
8//! distinct concrete signature `array_append(int[], int) → int[]`
9//! when called with `int` / `int[]` arguments.
10//!
11//! This module owns:
12//!
13//! - The `PseudoType` enum that the function catalog uses in
14//!   its `arg_types` slice when declaring polymorphic entries.
15//! - The `PolymorphicResolver` that instantiates pseudo-types
16//!   against concrete call-site arguments, enforcing the
17//!   consistency rule: every `anyelement` at the same signature
18//!   must resolve to the same concrete type.
19//!
20//! Scope today (Fase 3 W3):
21//!
22//! - `AnyElement` — matches any single concrete type.
23//! - `AnyArray` — matches any array type. Inferred from the
24//!   `AnyElement` it shares a signature with.
25//! - `AnyNonArray` — matches any concrete type except arrays.
26//! - `AnyCompatible` — like `AnyElement` but tolerates implicit
27//!   widening (e.g. `int + float → float`).
28//!
29//! Deferred:
30//!
31//! - `AnyRange` / `AnyMultirange` — ranges aren't in Fase 3.
32//! - `AnyEnum` — enums are fine via concrete DataType::Enum
33//!   today; polymorphic enum wait.
34//!
35//! This module is **not yet wired** into the function catalog
36//! or expr_typing. Wiring adds a `PseudoType`-aware overload in
37//! `function_catalog::resolve` when the catalog starts shipping
38//! polymorphic rows.
39
40use crate::cast_catalog::can_implicit_cast;
41use crate::types::{DataType, TypeCategory};
42
43/// PG-style pseudo-type used by polymorphic function signatures.
44/// The resolver substitutes each variant with a concrete
45/// `DataType` at analyze time based on call-site arguments.
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
47pub enum PseudoType {
48    /// Matches any single concrete type. All `AnyElement`
49    /// positions in one signature must resolve to the same
50    /// concrete type.
51    AnyElement,
52    /// Matches any array type. The element type is inferred
53    /// from any `AnyElement` in the same signature — if no
54    /// `AnyElement` exists, the array's element type is the
55    /// matched type itself.
56    AnyArray,
57    /// Like `AnyElement` but rejects array types. Used by
58    /// functions that must not accept arrays to avoid
59    /// element-wise confusion.
60    AnyNonArray,
61    /// Like `AnyElement` but tolerates implicit coercion via
62    /// the cast catalog. Two `AnyCompatible` positions may
63    /// resolve to different concrete types as long as a common
64    /// implicit coercion exists.
65    AnyCompatible,
66}
67
68/// A single position in a function argument list — either a
69/// concrete type or a pseudo-type waiting for substitution.
70#[derive(Debug, Clone, Copy)]
71pub enum ArgSlot {
72    Concrete(DataType),
73    Poly(PseudoType),
74}
75
76/// The resolver's output — a substitution map that every
77/// pseudo-type in a signature has been bound to. Used by
78/// `expr_typing` to compute the concrete return type from a
79/// signature that mentions the same pseudo-type in its return
80/// position.
81#[derive(Debug, Clone, Default)]
82pub struct Substitution {
83    /// Resolved type for `AnyElement` positions.
84    pub any_element: Option<DataType>,
85    /// Resolved type for `AnyArray` positions.
86    pub any_array: Option<DataType>,
87    /// Resolved type for `AnyNonArray` positions.
88    pub any_nonarray: Option<DataType>,
89    /// Resolved type for `AnyCompatible` positions.
90    pub any_compatible: Option<DataType>,
91}
92
93impl Substitution {
94    /// Apply the substitution to a signature slot, returning the
95    /// concrete type. Returns `None` when the slot references a
96    /// pseudo-type that hasn't been resolved yet — the caller
97    /// should treat this as a typer error.
98    pub fn apply(&self, slot: ArgSlot) -> Option<DataType> {
99        match slot {
100            ArgSlot::Concrete(dt) => Some(dt),
101            ArgSlot::Poly(PseudoType::AnyElement) => self.any_element,
102            ArgSlot::Poly(PseudoType::AnyArray) => self.any_array,
103            ArgSlot::Poly(PseudoType::AnyNonArray) => self.any_nonarray,
104            ArgSlot::Poly(PseudoType::AnyCompatible) => self.any_compatible,
105        }
106    }
107}
108
109/// Errors raised during polymorphic resolution.
110#[derive(Debug, Clone)]
111pub enum ResolveError {
112    /// Two positions of the same pseudo-type resolved to
113    /// conflicting concrete types.
114    Conflict {
115        pseudo: PseudoType,
116        first: DataType,
117        other: DataType,
118    },
119    /// `AnyNonArray` matched against an array type.
120    NonArrayGotArray,
121    /// `AnyArray` matched against a non-array type.
122    ArrayGotScalar,
123    /// The signature's arity doesn't match the call site.
124    ArityMismatch { expected: usize, got: usize },
125}
126
127impl std::fmt::Display for ResolveError {
128    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
129        match self {
130            Self::Conflict {
131                pseudo,
132                first,
133                other,
134            } => {
135                write!(
136                    f,
137                    "polymorphic `{pseudo:?}` bound to `{first:?}` but later seen as `{other:?}`"
138                )
139            }
140            Self::NonArrayGotArray => write!(f, "AnyNonArray position got an array argument"),
141            Self::ArrayGotScalar => write!(f, "AnyArray position got a non-array argument"),
142            Self::ArityMismatch { expected, got } => {
143                write!(
144                    f,
145                    "polymorphic signature expects {expected} args, got {got}"
146                )
147            }
148        }
149    }
150}
151
152impl std::error::Error for ResolveError {}
153
154/// Attempt to resolve a polymorphic signature against a list of
155/// concrete call-site argument types. Returns the substitution
156/// on success so `expr_typing` can apply it to the return type.
157///
158/// Algorithm follows PG's `check_generic_type_consistency`:
159///
160/// 1. Iterate positional pairs `(signature_slot, call_arg_type)`.
161/// 2. For each `Concrete(dt)` slot, require `call_arg_type == dt`
162///    or an implicit coercion.
163/// 3. For each pseudo slot, bind the call arg to the appropriate
164///    substitution map entry. If the entry is already bound to
165///    a different type, return `Conflict`.
166/// 4. `AnyArray` + `AnyElement` consistency: if both show up in
167///    the same signature, verify that the resolved array's
168///    element type matches the resolved element.
169pub fn resolve(
170    signature: &[ArgSlot],
171    call_args: &[DataType],
172) -> Result<Substitution, ResolveError> {
173    if signature.len() != call_args.len() {
174        return Err(ResolveError::ArityMismatch {
175            expected: signature.len(),
176            got: call_args.len(),
177        });
178    }
179    let mut sub = Substitution::default();
180    for (slot, &arg_ty) in signature.iter().zip(call_args.iter()) {
181        match slot {
182            ArgSlot::Concrete(expected) => {
183                if *expected != arg_ty && !can_implicit_cast(arg_ty, *expected) {
184                    return Err(ResolveError::Conflict {
185                        pseudo: PseudoType::AnyElement, // placeholder — concrete mismatch
186                        first: *expected,
187                        other: arg_ty,
188                    });
189                }
190            }
191            ArgSlot::Poly(PseudoType::AnyElement) => {
192                bind(&mut sub.any_element, arg_ty, PseudoType::AnyElement)?;
193            }
194            ArgSlot::Poly(PseudoType::AnyArray) => {
195                if arg_ty.category() != TypeCategory::Array {
196                    return Err(ResolveError::ArrayGotScalar);
197                }
198                bind(&mut sub.any_array, arg_ty, PseudoType::AnyArray)?;
199            }
200            ArgSlot::Poly(PseudoType::AnyNonArray) => {
201                if arg_ty.category() == TypeCategory::Array {
202                    return Err(ResolveError::NonArrayGotArray);
203                }
204                bind(&mut sub.any_nonarray, arg_ty, PseudoType::AnyNonArray)?;
205            }
206            ArgSlot::Poly(PseudoType::AnyCompatible) => {
207                // AnyCompatible tolerates implicit coercion. If
208                // already bound, verify that the new arg can
209                // coerce either direction.
210                match sub.any_compatible {
211                    None => sub.any_compatible = Some(arg_ty),
212                    Some(prev) if prev == arg_ty => {}
213                    Some(prev) => {
214                        if can_implicit_cast(arg_ty, prev) {
215                            // Keep the earlier (wider) binding.
216                        } else if can_implicit_cast(prev, arg_ty) {
217                            // New arg is wider; update.
218                            sub.any_compatible = Some(arg_ty);
219                        } else {
220                            return Err(ResolveError::Conflict {
221                                pseudo: PseudoType::AnyCompatible,
222                                first: prev,
223                                other: arg_ty,
224                            });
225                        }
226                    }
227                }
228            }
229        }
230    }
231    Ok(sub)
232}
233
234/// Helper: bind a pseudo-type slot for the first time, or
235/// verify consistency with the previous binding.
236fn bind(
237    slot: &mut Option<DataType>,
238    arg: DataType,
239    pseudo: PseudoType,
240) -> Result<(), ResolveError> {
241    match *slot {
242        None => {
243            *slot = Some(arg);
244            Ok(())
245        }
246        Some(prev) if prev == arg => Ok(()),
247        Some(prev) => Err(ResolveError::Conflict {
248            pseudo,
249            first: prev,
250            other: arg,
251        }),
252    }
253}
254
255#[cfg(test)]
256mod tests {
257    use super::*;
258
259    #[test]
260    fn substitution_apply_returns_bound_or_concrete_types() {
261        let sub = Substitution {
262            any_element: Some(DataType::Integer),
263            any_array: Some(DataType::Array),
264            any_nonarray: Some(DataType::Text),
265            any_compatible: Some(DataType::Float),
266        };
267        assert_eq!(
268            sub.apply(ArgSlot::Concrete(DataType::Boolean)),
269            Some(DataType::Boolean)
270        );
271        assert_eq!(
272            sub.apply(ArgSlot::Poly(PseudoType::AnyElement)),
273            Some(DataType::Integer)
274        );
275        assert_eq!(
276            sub.apply(ArgSlot::Poly(PseudoType::AnyArray)),
277            Some(DataType::Array)
278        );
279        assert_eq!(
280            sub.apply(ArgSlot::Poly(PseudoType::AnyNonArray)),
281            Some(DataType::Text)
282        );
283        assert_eq!(
284            sub.apply(ArgSlot::Poly(PseudoType::AnyCompatible)),
285            Some(DataType::Float)
286        );
287        assert_eq!(
288            Substitution::default().apply(ArgSlot::Poly(PseudoType::AnyElement)),
289            None
290        );
291    }
292
293    #[test]
294    fn resolve_accepts_concrete_and_poly_slots() {
295        let sub = resolve(
296            &[
297                ArgSlot::Concrete(DataType::Float),
298                ArgSlot::Poly(PseudoType::AnyElement),
299                ArgSlot::Poly(PseudoType::AnyArray),
300                ArgSlot::Poly(PseudoType::AnyNonArray),
301            ],
302            &[
303                DataType::Integer,
304                DataType::Text,
305                DataType::Array,
306                DataType::Boolean,
307            ],
308        )
309        .unwrap();
310        assert_eq!(sub.any_element, Some(DataType::Text));
311        assert_eq!(sub.any_array, Some(DataType::Array));
312        assert_eq!(sub.any_nonarray, Some(DataType::Boolean));
313    }
314
315    #[test]
316    fn resolve_reports_arity_and_kind_errors() {
317        assert!(matches!(
318            resolve(&[ArgSlot::Poly(PseudoType::AnyElement)], &[]),
319            Err(ResolveError::ArityMismatch {
320                expected: 1,
321                got: 0
322            })
323        ));
324        assert!(matches!(
325            resolve(&[ArgSlot::Poly(PseudoType::AnyArray)], &[DataType::Text]),
326            Err(ResolveError::ArrayGotScalar)
327        ));
328        assert!(matches!(
329            resolve(
330                &[ArgSlot::Poly(PseudoType::AnyNonArray)],
331                &[DataType::Array]
332            ),
333            Err(ResolveError::NonArrayGotArray)
334        ));
335        assert!(matches!(
336            resolve(&[ArgSlot::Concrete(DataType::Boolean)], &[DataType::Text]),
337            Err(ResolveError::Conflict { .. })
338        ));
339    }
340
341    #[test]
342    fn repeated_pseudo_slots_must_be_consistent() {
343        let ok = resolve(
344            &[
345                ArgSlot::Poly(PseudoType::AnyElement),
346                ArgSlot::Poly(PseudoType::AnyElement),
347            ],
348            &[DataType::Integer, DataType::Integer],
349        )
350        .unwrap();
351        assert_eq!(ok.any_element, Some(DataType::Integer));
352
353        let err = resolve(
354            &[
355                ArgSlot::Poly(PseudoType::AnyElement),
356                ArgSlot::Poly(PseudoType::AnyElement),
357            ],
358            &[DataType::Integer, DataType::Text],
359        )
360        .unwrap_err();
361        assert!(matches!(
362            err,
363            ResolveError::Conflict {
364                pseudo: PseudoType::AnyElement,
365                first: DataType::Integer,
366                other: DataType::Text,
367            }
368        ));
369        assert!(err.to_string().contains("AnyElement"));
370    }
371
372    #[test]
373    fn anycompatible_uses_cast_catalog_to_resolve_binding() {
374        let int_then_float = resolve(
375            &[
376                ArgSlot::Poly(PseudoType::AnyCompatible),
377                ArgSlot::Poly(PseudoType::AnyCompatible),
378            ],
379            &[DataType::Integer, DataType::Float],
380        )
381        .unwrap();
382        assert_eq!(int_then_float.any_compatible, Some(DataType::Integer));
383
384        let float_then_int = resolve(
385            &[
386                ArgSlot::Poly(PseudoType::AnyCompatible),
387                ArgSlot::Poly(PseudoType::AnyCompatible),
388            ],
389            &[DataType::Float, DataType::Integer],
390        )
391        .unwrap();
392        assert_eq!(float_then_int.any_compatible, Some(DataType::Float));
393
394        assert!(matches!(
395            resolve(
396                &[
397                    ArgSlot::Poly(PseudoType::AnyCompatible),
398                    ArgSlot::Poly(PseudoType::AnyCompatible),
399                ],
400                &[DataType::Boolean, DataType::Json],
401            ),
402            Err(ResolveError::Conflict {
403                pseudo: PseudoType::AnyCompatible,
404                ..
405            })
406        ));
407    }
408}