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}