1use crate::{normalize_ty, normalize_ty_with_access, AccessMode, BaseTy, NormalizedTy, Ty};
2
3#[derive(Debug, Clone, PartialEq, Eq)]
5pub enum CoercionDecision {
6 Denied,
8 Allowed {
10 exact: bool,
12 check: CheckPolicy,
14 materialization: MaterializationPolicy,
16 },
17}
18
19#[derive(Debug, Clone, PartialEq, Eq)]
21pub enum CheckPolicy {
22 None,
24 Runtime(RuntimeCheckKind),
26}
27
28#[derive(Debug, Clone, PartialEq, Eq)]
30pub enum RuntimeCheckKind {
31 LenEquals {
33 expected_len: usize,
35 },
36 LenAtLeast {
38 min_len: usize,
40 },
41}
42
43#[derive(Debug, Clone, PartialEq, Eq)]
45pub enum MaterializationPolicy {
46 None,
48 Required(MaterializationKind),
50}
51
52#[derive(Debug, Clone, PartialEq, Eq)]
54pub enum MaterializationKind {
55 ViewFromOwned,
57 DynamicArrayFromStaticArray,
59 StaticArrayFromDynamicArray,
61}
62
63impl CoercionDecision {
64 pub fn is_allowed(&self) -> bool {
66 matches!(self, Self::Allowed { .. })
67 }
68
69 pub fn requires_runtime_check(&self) -> bool {
71 matches!(
72 self,
73 Self::Allowed {
74 check: CheckPolicy::Runtime(_),
75 ..
76 }
77 )
78 }
79
80 pub fn requires_materialization(&self) -> bool {
82 matches!(
83 self,
84 Self::Allowed {
85 materialization: MaterializationPolicy::Required(_),
86 ..
87 }
88 )
89 }
90}
91
92pub 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
103pub 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
119pub 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
144fn allowed(check: CheckPolicy, materialization: MaterializationPolicy) -> CoercionDecision {
146 CoercionDecision::Allowed {
147 exact: false,
148 check,
149 materialization,
150 }
151}
152
153fn 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
186fn 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
229fn 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
281fn 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
298fn 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}