Skip to main content

reddb_server/storage/schema/
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 super::cast_catalog::can_implicit_cast;
41use super::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}