ruka_types/
coercion.rs

1use crate::{normalize_ty, normalize_ty_with_access, AccessMode, BaseTy, NormalizedTy, Ty};
2
3/// Coercion decision returned by the normalized compatibility table.
4#[derive(Debug, Clone, PartialEq, Eq)]
5pub enum CoercionDecision {
6    /// Source and destination are incompatible.
7    Denied,
8    /// Source and destination are compatible.
9    Allowed {
10        /// Whether both normalized types are identical.
11        exact: bool,
12        /// Runtime check requirement for this coercion.
13        check: CheckPolicy,
14        /// Materialization requirement for this coercion.
15        materialization: MaterializationPolicy,
16    },
17}
18
19/// Runtime check requirement for one coercion.
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub enum CheckPolicy {
22    /// No runtime check is required.
23    None,
24    /// Runtime check is required, identified by kind.
25    Runtime(RuntimeCheckKind),
26}
27
28/// Runtime check category used by coercion planning.
29#[derive(Debug, Clone, PartialEq, Eq)]
30pub enum RuntimeCheckKind {
31    /// Requires `len == expected_len`.
32    LenEquals {
33        /// Required exact sequence length.
34        expected_len: usize,
35    },
36    /// Requires `len >= min_len`.
37    LenAtLeast {
38        /// Required minimum sequence length.
39        min_len: usize,
40    },
41}
42
43/// Materialization requirement for one coercion.
44#[derive(Debug, Clone, PartialEq, Eq)]
45pub enum MaterializationPolicy {
46    /// No materialization is required.
47    None,
48    /// Materialization is required, identified by kind.
49    Required(MaterializationKind),
50}
51
52/// Materialization category used by lowering.
53#[derive(Debug, Clone, PartialEq, Eq)]
54pub enum MaterializationKind {
55    /// Coercion materializes a read-only view from an owned source value.
56    ViewFromOwned,
57    /// Coercion materializes a dynamic owned array from a static owned array.
58    DynamicArrayFromStaticArray,
59    /// Coercion materializes a static owned array from a dynamic owned array.
60    StaticArrayFromDynamicArray,
61}
62
63impl CoercionDecision {
64    /// Return whether this coercion is accepted.
65    pub fn is_allowed(&self) -> bool {
66        matches!(self, Self::Allowed { .. })
67    }
68
69    /// Return whether this coercion requires a runtime check.
70    pub fn requires_runtime_check(&self) -> bool {
71        matches!(
72            self,
73            Self::Allowed {
74                check: CheckPolicy::Runtime(_),
75                ..
76            }
77        )
78    }
79
80    /// Return whether this coercion requires representation materialization.
81    pub fn requires_materialization(&self) -> bool {
82        matches!(
83            self,
84            Self::Allowed {
85                materialization: MaterializationPolicy::Required(_),
86                ..
87            }
88        )
89    }
90}
91
92/// Compute normalized coercion compatibility for two semantic types.
93pub fn ty_coercion_decision(expected: &Ty, actual: &Ty) -> CoercionDecision {
94    let Ok(expected_norm) = normalize_ty(expected) else {
95        return CoercionDecision::Denied;
96    };
97    let Ok(actual_norm) = normalize_ty(actual) else {
98        return CoercionDecision::Denied;
99    };
100    normalized_coercion_decision(&expected_norm, &actual_norm)
101}
102
103/// Compute coercion compatibility for one boundary where access mode is tracked separately.
104pub fn boundary_coercion_decision(
105    expected_access: AccessMode,
106    expected_ty: &Ty,
107    actual_access: AccessMode,
108    actual_ty: &Ty,
109) -> CoercionDecision {
110    let Ok(expected_norm) = normalize_ty_with_access(expected_ty, expected_access) else {
111        return CoercionDecision::Denied;
112    };
113    let Ok(actual_norm) = normalize_ty_with_access(actual_ty, actual_access) else {
114        return CoercionDecision::Denied;
115    };
116    normalized_coercion_decision(&expected_norm, &actual_norm)
117}
118
119/// Compute coercion compatibility for two normalized types.
120pub fn normalized_coercion_decision(
121    expected: &NormalizedTy,
122    actual: &NormalizedTy,
123) -> CoercionDecision {
124    if expected == actual {
125        return CoercionDecision::Allowed {
126            exact: true,
127            check: CheckPolicy::None,
128            materialization: MaterializationPolicy::None,
129        };
130    }
131
132    match (expected.access, actual.access) {
133        (AccessMode::Owned, AccessMode::Owned) => owned_coercion(&expected.base, &actual.base),
134        (AccessMode::View, AccessMode::Owned) => {
135            view_from_owned_coercion(&expected.base, &actual.base)
136        }
137        (AccessMode::View, AccessMode::View) | (AccessMode::MutBorrow, AccessMode::MutBorrow) => {
138            borrow_like_coercion(&expected.base, &actual.base)
139        }
140        _ => CoercionDecision::Denied,
141    }
142}
143
144/// Return one non-exact allowed coercion with provided policies.
145fn allowed(check: CheckPolicy, materialization: MaterializationPolicy) -> CoercionDecision {
146    CoercionDecision::Allowed {
147        exact: false,
148        check,
149        materialization,
150    }
151}
152
153/// Compute one owned-to-owned base coercion decision.
154fn owned_coercion(expected: &BaseTy, actual: &BaseTy) -> CoercionDecision {
155    match (expected, actual) {
156        (
157            BaseTy::DynamicArray(expected_item),
158            BaseTy::StaticArray {
159                item: actual_item, ..
160            },
161        ) if expected_item == actual_item => allowed(
162            CheckPolicy::None,
163            MaterializationPolicy::Required(MaterializationKind::DynamicArrayFromStaticArray),
164        ),
165        (
166            BaseTy::StaticArray {
167                item: expected_item,
168                len,
169            },
170            BaseTy::DynamicArray(actual_item),
171        ) if expected_item == actual_item => allowed(
172            CheckPolicy::Runtime(RuntimeCheckKind::LenEquals { expected_len: *len }),
173            MaterializationPolicy::Required(MaterializationKind::StaticArrayFromDynamicArray),
174        ),
175        (BaseTy::Option(expected_item), BaseTy::Option(actual_item)) => {
176            nested_owned_compatible(expected_item, actual_item)
177        }
178        (BaseTy::Tuple(expected_items), BaseTy::Tuple(actual_items)) => {
179            nested_tuple_owned_compatible(expected_items, actual_items)
180        }
181        _ if expected == actual => allowed(CheckPolicy::None, MaterializationPolicy::None),
182        _ => CoercionDecision::Denied,
183    }
184}
185
186/// Compute one view-from-owned coercion decision.
187fn view_from_owned_coercion(expected: &BaseTy, actual: &BaseTy) -> CoercionDecision {
188    match (expected, actual) {
189        (BaseTy::Slice(expected_item), BaseTy::StaticArray { item, .. })
190            if expected_item == item =>
191        {
192            allowed(CheckPolicy::None, MaterializationPolicy::None)
193        }
194        (BaseTy::Slice(expected_item), BaseTy::DynamicArray(actual_item))
195            if expected_item == actual_item =>
196        {
197            allowed(CheckPolicy::None, MaterializationPolicy::None)
198        }
199        (
200            BaseTy::StaticSlice {
201                item: expected_item,
202                len: expected_len,
203            },
204            BaseTy::StaticArray {
205                item: actual_item,
206                len: actual_len,
207            },
208        ) if expected_item == actual_item && actual_len >= expected_len => {
209            allowed(CheckPolicy::None, MaterializationPolicy::None)
210        }
211        (
212            BaseTy::StaticSlice {
213                item: expected_item,
214                len,
215            },
216            BaseTy::DynamicArray(actual_item),
217        ) if expected_item == actual_item => allowed(
218            CheckPolicy::Runtime(RuntimeCheckKind::LenAtLeast { min_len: *len }),
219            MaterializationPolicy::None,
220        ),
221        _ if expected == actual => allowed(
222            CheckPolicy::None,
223            MaterializationPolicy::Required(MaterializationKind::ViewFromOwned),
224        ),
225        _ => CoercionDecision::Denied,
226    }
227}
228
229/// Compute one borrow/view coercion decision where destination expects a view-like access mode.
230fn borrow_like_coercion(expected: &BaseTy, actual: &BaseTy) -> CoercionDecision {
231    match (expected, actual) {
232        (BaseTy::Slice(expected_item), BaseTy::StaticArray { item, .. })
233            if expected_item == item =>
234        {
235            allowed(CheckPolicy::None, MaterializationPolicy::None)
236        }
237        (BaseTy::Slice(expected_item), BaseTy::StaticSlice { item, .. })
238            if expected_item == item =>
239        {
240            allowed(CheckPolicy::None, MaterializationPolicy::None)
241        }
242        (
243            BaseTy::StaticSlice {
244                item: expected_item,
245                len: expected_len,
246            },
247            BaseTy::StaticArray {
248                item: actual_item,
249                len: actual_len,
250            },
251        ) if expected_item == actual_item && actual_len >= expected_len => {
252            allowed(CheckPolicy::None, MaterializationPolicy::None)
253        }
254        (
255            BaseTy::StaticSlice {
256                item: expected_item,
257                len: expected_len,
258            },
259            BaseTy::StaticSlice {
260                item: actual_item,
261                len: actual_len,
262            },
263        ) if expected_item == actual_item && actual_len >= expected_len => {
264            allowed(CheckPolicy::None, MaterializationPolicy::None)
265        }
266        (
267            BaseTy::StaticSlice {
268                item: expected_item,
269                len,
270            },
271            BaseTy::Slice(actual_item),
272        ) if expected_item == actual_item => allowed(
273            CheckPolicy::Runtime(RuntimeCheckKind::LenAtLeast { min_len: *len }),
274            MaterializationPolicy::None,
275        ),
276        _ if expected == actual => allowed(CheckPolicy::None, MaterializationPolicy::None),
277        _ => CoercionDecision::Denied,
278    }
279}
280
281/// Return one nested owned compatibility decision for one option payload.
282fn nested_owned_compatible(expected: &BaseTy, actual: &BaseTy) -> CoercionDecision {
283    let decision = owned_coercion(expected, actual);
284    if matches!(
285        decision,
286        CoercionDecision::Allowed {
287            check: CheckPolicy::None,
288            materialization: MaterializationPolicy::None,
289            ..
290        }
291    ) {
292        decision
293    } else {
294        CoercionDecision::Denied
295    }
296}
297
298/// Return one nested owned compatibility decision for one tuple payload.
299fn nested_tuple_owned_compatible(
300    expected_items: &[BaseTy],
301    actual_items: &[BaseTy],
302) -> CoercionDecision {
303    if expected_items.len() != actual_items.len() {
304        return CoercionDecision::Denied;
305    }
306    for (expected_item, actual_item) in expected_items.iter().zip(actual_items.iter()) {
307        if !matches!(
308            nested_owned_compatible(expected_item, actual_item),
309            CoercionDecision::Allowed { .. }
310        ) {
311            return CoercionDecision::Denied;
312        }
313    }
314    allowed(CheckPolicy::None, MaterializationPolicy::None)
315}
316
317#[cfg(test)]
318mod tests {
319    use super::*;
320
321    #[test]
322    fn accepts_exact_types() {
323        let decision = ty_coercion_decision(&Ty::I64, &Ty::I64);
324        assert_eq!(
325            decision,
326            CoercionDecision::Allowed {
327                exact: true,
328                check: CheckPolicy::None,
329                materialization: MaterializationPolicy::None,
330            }
331        );
332    }
333
334    #[test]
335    fn accepts_slice_from_array() {
336        let decision = ty_coercion_decision(
337            &Ty::Slice(Box::new(Ty::I64)),
338            &Ty::Array {
339                item: Box::new(Ty::I64),
340                len: 4,
341            },
342        );
343        assert_eq!(
344            decision,
345            CoercionDecision::Allowed {
346                exact: false,
347                check: CheckPolicy::None,
348                materialization: MaterializationPolicy::Required(
349                    MaterializationKind::DynamicArrayFromStaticArray
350                ),
351            }
352        );
353    }
354
355    #[test]
356    fn rejects_access_mismatch() {
357        let decision = normalized_coercion_decision(
358            &NormalizedTy {
359                base: BaseTy::I64,
360                access: AccessMode::MutBorrow,
361            },
362            &NormalizedTy {
363                base: BaseTy::I64,
364                access: AccessMode::Owned,
365            },
366        );
367        assert_eq!(decision, CoercionDecision::Denied);
368    }
369
370    #[test]
371    fn marks_view_from_owned_as_materialization_required() {
372        let decision = normalized_coercion_decision(
373            &NormalizedTy {
374                base: BaseTy::I64,
375                access: AccessMode::View,
376            },
377            &NormalizedTy {
378                base: BaseTy::I64,
379                access: AccessMode::Owned,
380            },
381        );
382        assert_eq!(
383            decision,
384            CoercionDecision::Allowed {
385                exact: false,
386                check: CheckPolicy::None,
387                materialization: MaterializationPolicy::Required(
388                    MaterializationKind::ViewFromOwned
389                ),
390            }
391        );
392    }
393
394    #[test]
395    fn boundary_decision_tracks_view_from_owned_policy() {
396        let decision =
397            boundary_coercion_decision(AccessMode::View, &Ty::I64, AccessMode::Owned, &Ty::I64);
398        assert_eq!(
399            decision,
400            CoercionDecision::Allowed {
401                exact: false,
402                check: CheckPolicy::None,
403                materialization: MaterializationPolicy::Required(
404                    MaterializationKind::ViewFromOwned
405                ),
406            }
407        );
408    }
409
410    #[test]
411    fn accepts_nested_option_element_compatibility() {
412        let decision = ty_coercion_decision(
413            &Ty::Option(Box::new(Ty::Slice(Box::new(Ty::I32)))),
414            &Ty::Option(Box::new(Ty::Array {
415                item: Box::new(Ty::I32),
416                len: 8,
417            })),
418        );
419        assert_eq!(decision, CoercionDecision::Denied);
420    }
421
422    #[test]
423    fn rejects_tuple_arity_mismatch() {
424        let decision = ty_coercion_decision(
425            &Ty::Tuple(vec![Ty::I32, Ty::Bool]),
426            &Ty::Tuple(vec![Ty::I32]),
427        );
428        assert_eq!(decision, CoercionDecision::Denied);
429    }
430
431    #[test]
432    fn rejects_slice_item_type_mismatch() {
433        let decision =
434            ty_coercion_decision(&Ty::Slice(Box::new(Ty::I64)), &Ty::Slice(Box::new(Ty::U64)));
435        assert_eq!(decision, CoercionDecision::Denied);
436    }
437
438    #[test]
439    fn requires_runtime_check_for_dynamic_to_static_array() {
440        let decision = ty_coercion_decision(
441            &Ty::Array {
442                item: Box::new(Ty::I64),
443                len: 4,
444            },
445            &Ty::Slice(Box::new(Ty::I64)),
446        );
447        assert!(decision.is_allowed());
448        assert!(decision.requires_runtime_check());
449        assert!(decision.requires_materialization());
450    }
451
452    #[test]
453    fn accepts_static_slice_from_slice_with_runtime_len_check() {
454        let decision = boundary_coercion_decision(
455            AccessMode::View,
456            &Ty::Array {
457                item: Box::new(Ty::I64),
458                len: 4,
459            },
460            AccessMode::Owned,
461            &Ty::Slice(Box::new(Ty::I64)),
462        );
463        assert_eq!(
464            decision,
465            CoercionDecision::Allowed {
466                exact: false,
467                check: CheckPolicy::Runtime(RuntimeCheckKind::LenAtLeast { min_len: 4 }),
468                materialization: MaterializationPolicy::None,
469            }
470        );
471    }
472}