Skip to main content

assura_codegen/
type_map.rs

1//! Reverse type mapping: Rust types to Assura types.
2//!
3//! The forward mapping (Assura -> Rust) lives in `map_type_token()` in `lib.rs`.
4//! This module provides the inverse: given a Rust type string, produce the
5//! canonical Assura type. Used by AI contract generation templates and the
6//! `assura infer` command.
7
8/// Map a Rust type string to its Assura equivalent.
9///
10/// Handles primitives, standard library generics, references, and nested types.
11/// Unknown types are passed through unchanged (they become user-defined types
12/// in Assura).
13///
14/// # Examples
15/// ```
16/// use assura_codegen::type_map::rust_type_to_assura;
17/// assert_eq!(rust_type_to_assura("i64"), "Int");
18/// assert_eq!(rust_type_to_assura("Vec<u8>"), "Bytes");
19/// assert_eq!(rust_type_to_assura("Vec<i64>"), "List<Int>");
20/// assert_eq!(rust_type_to_assura("Option<i64>"), "Int?");
21/// ```
22pub fn rust_type_to_assura(rust_type: &str) -> String {
23    let trimmed = rust_type.trim();
24
25    // Handle references: &str, &[u8], &T, &mut T
26    if let Some(inner) = trimmed.strip_prefix('&') {
27        let inner = inner.trim_start();
28        if let Some(inner) = inner.strip_prefix("mut ") {
29            return rust_type_to_assura(inner.trim());
30        }
31        if inner == "str" {
32            return "String".to_string();
33        }
34        if inner == "[u8]" {
35            return "Bytes".to_string();
36        }
37        // &[T] -> List<T>
38        if let Some(slice_inner) = inner.strip_prefix('[')
39            && let Some(slice_inner) = slice_inner.strip_suffix(']')
40        {
41            let mapped = rust_type_to_assura(slice_inner.trim());
42            return format!("List<{mapped}>");
43        }
44        return rust_type_to_assura(inner);
45    }
46
47    // Handle tuple types: (T, U, ...) -> (T, U, ...)
48    if trimmed.starts_with('(') && trimmed.ends_with(')') {
49        let inner = &trimmed[1..trimmed.len() - 1];
50        if inner.is_empty() {
51            return "Unit".to_string();
52        }
53        let parts = split_type_args(inner);
54        let mapped: Vec<String> = parts.iter().map(|p| rust_type_to_assura(p)).collect();
55        return format!("({})", mapped.join(", "));
56    }
57
58    // Handle simple primitives first (no generics)
59    if !trimmed.contains('<') {
60        return map_simple_rust_type(trimmed).to_string();
61    }
62
63    // Handle generic types: Name<Args>
64    if let Some((base, args)) = split_generic(trimmed) {
65        let base_trimmed = base.trim();
66        let type_args = split_type_args(args);
67
68        match base_trimmed {
69            // Vec<u8> -> Bytes, Vec<T> -> List<T>
70            "Vec" => {
71                if type_args.len() == 1 && type_args[0].trim() == "u8" {
72                    "Bytes".to_string()
73                } else if type_args.len() == 1 {
74                    let inner = rust_type_to_assura(type_args[0].trim());
75                    format!("List<{inner}>")
76                } else {
77                    format!("Vec<{}>", map_type_arg_list(&type_args))
78                }
79            }
80
81            // Option<T> -> T?
82            "Option" => {
83                if type_args.len() == 1 {
84                    let inner = rust_type_to_assura(type_args[0].trim());
85                    format!("{inner}?")
86                } else {
87                    format!("Option<{}>", map_type_arg_list(&type_args))
88                }
89            }
90
91            // Result<T, E> -> Result<T, E> (context-dependent, pass through)
92            "Result" => {
93                format!("Result<{}>", map_type_arg_list(&type_args))
94            }
95
96            // Map types
97            "HashMap" | "BTreeMap" | "std::collections::HashMap" | "std::collections::BTreeMap" => {
98                format!("Map<{}>", map_type_arg_list(&type_args))
99            }
100
101            // Set types
102            "HashSet" | "BTreeSet" | "std::collections::HashSet" | "std::collections::BTreeSet" => {
103                if type_args.len() == 1 {
104                    let inner = rust_type_to_assura(type_args[0].trim());
105                    format!("Set<{inner}>")
106                } else {
107                    format!("Set<{}>", map_type_arg_list(&type_args))
108                }
109            }
110
111            // Box<T>, Arc<T>, Rc<T>, Cow<T> -> just T (wrapper erasure)
112            "Box" | "Arc" | "Rc" | "Cow" | "std::sync::Arc" | "std::rc::Rc"
113            | "std::borrow::Cow" => {
114                if type_args.len() == 1 {
115                    rust_type_to_assura(type_args[0].trim())
116                } else {
117                    format!("{base_trimmed}<{}>", map_type_arg_list(&type_args))
118                }
119            }
120
121            // Unknown generic: pass through with mapped args
122            _ => {
123                let mapped_base = map_simple_rust_type(base_trimmed);
124                format!("{mapped_base}<{}>", map_type_arg_list(&type_args))
125            }
126        }
127    } else {
128        map_simple_rust_type(trimmed).to_string()
129    }
130}
131
132/// Map a simple (non-generic) Rust type to Assura.
133fn map_simple_rust_type(ty: &str) -> &str {
134    // Strip path prefixes so `std::num::NonZeroU8` matches.
135    let ty = ty.rsplit("::").next().unwrap_or(ty).trim();
136    match ty {
137        // Signed integers -> Int
138        "i8" | "i16" | "i32" | "i64" | "i128" | "isize" => "Int",
139        // Unsigned integers -> Nat
140        "u8" | "u16" | "u32" | "u64" | "u128" | "usize" => "Nat",
141        // NonZero* (std::num) — positive integer wrappers
142        "NonZeroU8" | "NonZeroU16" | "NonZeroU32" | "NonZeroU64" | "NonZeroU128"
143        | "NonZeroUsize" => "Nat",
144        "NonZeroI8" | "NonZeroI16" | "NonZeroI32" | "NonZeroI64" | "NonZeroI128"
145        | "NonZeroIsize" => "Int",
146        // Floats
147        "f32" | "f64" => "Float",
148        // Bool
149        "bool" => "Bool",
150        // String types
151        "String" | "str" => "String",
152        // Unit
153        "()" => "Unit",
154        // Never
155        "!" | "Infallible" | "std::convert::Infallible" => "Never",
156        // Bytes as a standalone type name
157        "Bytes" | "bytes::Bytes" => "Bytes",
158        // Pass through anything else
159        _ => ty,
160    }
161}
162
163/// Split `Name<A, B, C>` into `("Name", "A, B, C")`.
164fn split_generic(ty: &str) -> Option<(&str, &str)> {
165    let open = ty.find('<')?;
166    let close = ty.rfind('>')?;
167    if close <= open {
168        return None;
169    }
170    Some((&ty[..open], &ty[open + 1..close]))
171}
172
173/// Split a comma-separated type argument list, respecting nested `<>`.
174fn split_type_args(args: &str) -> Vec<&str> {
175    let mut result = Vec::new();
176    let mut depth = 0i32;
177    let mut paren_depth = 0i32;
178    let mut start = 0;
179
180    for (i, ch) in args.char_indices() {
181        match ch {
182            '<' => depth += 1,
183            '>' if depth > 0 => depth -= 1,
184            '(' => paren_depth += 1,
185            ')' if paren_depth > 0 => paren_depth -= 1,
186            ',' if depth == 0 && paren_depth == 0 => {
187                result.push(&args[start..i]);
188                start = i + 1;
189            }
190            _ => {}
191        }
192    }
193    if start < args.len() {
194        result.push(&args[start..]);
195    }
196    result
197}
198
199/// Map a list of type arguments recursively.
200fn map_type_arg_list(args: &[&str]) -> String {
201    args.iter()
202        .map(|a| rust_type_to_assura(a.trim()))
203        .collect::<Vec<_>>()
204        .join(", ")
205}
206
207/// Like [`rust_type_to_assura`], but maps unrecognized Rust types to `Unknown`
208/// instead of passing them through. Also handles `impl Trait`, `dyn Trait`,
209/// `Self`, and lifetime annotations that the standard mapper does not.
210///
211/// Used by `assura audit` where unknown Rust types would cause A02001
212/// resolution errors that prevent verification from running.
213pub fn rust_type_to_assura_lenient(rust_type: &str) -> String {
214    let ty = rust_type.trim();
215    // Unmappable Rust type syntax: return Unknown directly
216    if ty.starts_with("impl ") || ty.starts_with("dyn ") || ty == "Self" {
217        return "Unknown".to_string();
218    }
219    if ty.starts_with("Pin<") || ty.starts_with("PhantomData") {
220        return "Unknown".to_string();
221    }
222    // Strip lifetime from references: &'a T -> &T, &'_ T -> &T
223    let cleaned = if ty.starts_with("&'") {
224        if let Some(space_idx) = ty[1..].find(' ') {
225            format!("&{}", &ty[2 + space_idx..])
226        } else {
227            ty.to_string()
228        }
229    } else {
230        ty.to_string()
231    };
232    let mapped = rust_type_to_assura(&cleaned);
233    sanitize_mapped_type(&mapped)
234}
235
236/// Recursively replace type names that are not valid Assura types with `Unknown`.
237fn sanitize_mapped_type(ty: &str) -> String {
238    let ty = ty.trim();
239    if ty.is_empty() {
240        return "Unknown".to_string();
241    }
242    // Optional: T?
243    if let Some(inner) = ty.strip_suffix('?') {
244        let s = sanitize_mapped_type(inner);
245        return format!("{s}?");
246    }
247    // Tuple: (T, U, ...)
248    if ty.starts_with('(') && ty.ends_with(')') {
249        let inner = &ty[1..ty.len() - 1];
250        if inner.is_empty() {
251            return "Unit".to_string();
252        }
253        let parts = split_type_args(inner);
254        let sanitized: Vec<String> = parts
255            .iter()
256            .map(|p| sanitize_mapped_type(p.trim()))
257            .collect();
258        return format!("({})", sanitized.join(", "));
259    }
260    // Known generic bases: List<...>, Map<...>, Set<...>, Result<...>
261    if let Some((base, args)) = split_generic(ty) {
262        let base = base.trim();
263        match base {
264            "List" | "Map" | "Set" | "Result" => {
265                let parts = split_type_args(args);
266                let sanitized: Vec<String> = parts
267                    .iter()
268                    .map(|p| sanitize_mapped_type(p.trim()))
269                    .collect();
270                return format!("{base}<{}>", sanitized.join(", "));
271            }
272            _ => return "Unknown".to_string(),
273        }
274    }
275    // Known Assura primitive types
276    match ty {
277        "Int" | "Nat" | "Float" | "Bool" | "String" | "Bytes" | "Unit" | "Never" | "Unknown" => {
278            ty.to_string()
279        }
280        _ => "Unknown".to_string(),
281    }
282}
283
284// ---------------------------------------------------------------------------
285// Tests
286// ---------------------------------------------------------------------------
287
288#[cfg(test)]
289mod tests {
290    use super::*;
291
292    // --- Primitive mapping ---
293
294    #[test]
295    fn signed_integers_map_to_int() {
296        for ty in &["i8", "i16", "i32", "i64", "i128", "isize"] {
297            assert_eq!(rust_type_to_assura(ty), "Int", "failed for {ty}");
298        }
299    }
300
301    #[test]
302    fn unsigned_integers_map_to_nat() {
303        for ty in &["u8", "u16", "u32", "u64", "u128", "usize"] {
304            assert_eq!(rust_type_to_assura(ty), "Nat", "failed for {ty}");
305        }
306    }
307
308    #[test]
309    fn nonzero_integers_map_to_int_or_nat() {
310        for ty in &[
311            "NonZeroU8",
312            "NonZeroU16",
313            "NonZeroU32",
314            "NonZeroU64",
315            "NonZeroUsize",
316        ] {
317            assert_eq!(rust_type_to_assura(ty), "Nat", "failed for {ty}");
318        }
319        for ty in &[
320            "NonZeroI8",
321            "NonZeroI16",
322            "NonZeroI32",
323            "NonZeroI64",
324            "NonZeroIsize",
325        ] {
326            assert_eq!(rust_type_to_assura(ty), "Int", "failed for {ty}");
327        }
328        assert_eq!(rust_type_to_assura("std::num::NonZeroU8"), "Nat");
329    }
330
331    #[test]
332    fn floats_map_to_float() {
333        assert_eq!(rust_type_to_assura("f32"), "Float");
334        assert_eq!(rust_type_to_assura("f64"), "Float");
335    }
336
337    #[test]
338    fn bool_maps_to_bool() {
339        assert_eq!(rust_type_to_assura("bool"), "Bool");
340    }
341
342    #[test]
343    fn string_types() {
344        assert_eq!(rust_type_to_assura("String"), "String");
345        assert_eq!(rust_type_to_assura("&str"), "String");
346    }
347
348    #[test]
349    fn unit_and_never() {
350        assert_eq!(rust_type_to_assura("()"), "Unit");
351        assert_eq!(rust_type_to_assura("!"), "Never");
352        assert_eq!(rust_type_to_assura("Infallible"), "Never");
353    }
354
355    // --- Collection mapping ---
356
357    #[test]
358    fn vec_u8_maps_to_bytes() {
359        assert_eq!(rust_type_to_assura("Vec<u8>"), "Bytes");
360    }
361
362    #[test]
363    fn vec_maps_to_list() {
364        assert_eq!(rust_type_to_assura("Vec<i64>"), "List<Int>");
365        assert_eq!(rust_type_to_assura("Vec<String>"), "List<String>");
366    }
367
368    #[test]
369    fn map_types() {
370        assert_eq!(
371            rust_type_to_assura("HashMap<String, i64>"),
372            "Map<String, Int>"
373        );
374        assert_eq!(
375            rust_type_to_assura("BTreeMap<String, u64>"),
376            "Map<String, Nat>"
377        );
378    }
379
380    #[test]
381    fn set_types() {
382        assert_eq!(rust_type_to_assura("HashSet<i64>"), "Set<Int>");
383        assert_eq!(rust_type_to_assura("BTreeSet<String>"), "Set<String>");
384    }
385
386    // --- Option mapping ---
387
388    #[test]
389    fn option_maps_to_nullable() {
390        assert_eq!(rust_type_to_assura("Option<i64>"), "Int?");
391        assert_eq!(rust_type_to_assura("Option<String>"), "String?");
392    }
393
394    // --- Reference erasure ---
395
396    #[test]
397    fn references_are_erased() {
398        assert_eq!(rust_type_to_assura("&i64"), "Int");
399        assert_eq!(rust_type_to_assura("&mut i64"), "Int");
400        assert_eq!(rust_type_to_assura("&[u8]"), "Bytes");
401        assert_eq!(rust_type_to_assura("&[i64]"), "List<Int>");
402    }
403
404    // --- Wrapper erasure ---
405
406    #[test]
407    fn smart_pointers_are_erased() {
408        assert_eq!(rust_type_to_assura("Box<i64>"), "Int");
409        assert_eq!(rust_type_to_assura("Arc<String>"), "String");
410        assert_eq!(rust_type_to_assura("Rc<Vec<i64>>"), "List<Int>");
411    }
412
413    // --- Nested generics ---
414
415    #[test]
416    fn nested_generics() {
417        assert_eq!(rust_type_to_assura("Vec<Option<i64>>"), "List<Int?>");
418        assert_eq!(
419            rust_type_to_assura("Vec<Option<BTreeMap<String, i64>>>"),
420            "List<Map<String, Int>?>"
421        );
422    }
423
424    // --- Tuples ---
425
426    #[test]
427    fn tuple_types() {
428        assert_eq!(rust_type_to_assura("(i64, u64)"), "(Int, Nat)");
429        assert_eq!(
430            rust_type_to_assura("(String, bool, f64)"),
431            "(String, Bool, Float)"
432        );
433    }
434
435    // --- Unknown passthrough ---
436
437    #[test]
438    fn unknown_types_pass_through() {
439        assert_eq!(rust_type_to_assura("MyCustomStruct"), "MyCustomStruct");
440        assert_eq!(rust_type_to_assura("MyGeneric<i64>"), "MyGeneric<Int>");
441    }
442
443    // --- Result passthrough ---
444
445    #[test]
446    fn result_type() {
447        assert_eq!(
448            rust_type_to_assura("Result<i64, String>"),
449            "Result<Int, String>"
450        );
451    }
452
453    // --- Lenient mapping (for audit) ---
454
455    #[test]
456    fn lenient_primitives_pass_through() {
457        assert_eq!(rust_type_to_assura_lenient("i64"), "Int");
458        assert_eq!(rust_type_to_assura_lenient("u32"), "Nat");
459        assert_eq!(rust_type_to_assura_lenient("bool"), "Bool");
460        assert_eq!(rust_type_to_assura_lenient("String"), "String");
461        assert_eq!(rust_type_to_assura_lenient("f64"), "Float");
462    }
463
464    #[test]
465    fn lenient_unknown_types_become_unknown() {
466        assert_eq!(rust_type_to_assura_lenient("Config"), "Unknown");
467        assert_eq!(rust_type_to_assura_lenient("PathBuf"), "Unknown");
468        assert_eq!(rust_type_to_assura_lenient("MyStruct"), "Unknown");
469    }
470
471    #[test]
472    fn lenient_unknown_generics_become_unknown() {
473        assert_eq!(rust_type_to_assura_lenient("MyGeneric<i64>"), "Unknown");
474        assert_eq!(rust_type_to_assura_lenient("Foo<Bar, Baz>"), "Unknown");
475    }
476
477    #[test]
478    fn lenient_known_generics_preserve_structure() {
479        assert_eq!(rust_type_to_assura_lenient("Vec<i64>"), "List<Int>");
480        assert_eq!(
481            rust_type_to_assura_lenient("HashMap<String, i64>"),
482            "Map<String, Int>"
483        );
484        assert_eq!(rust_type_to_assura_lenient("Option<bool>"), "Bool?");
485    }
486
487    #[test]
488    fn lenient_nested_unknown_in_known_generic() {
489        assert_eq!(rust_type_to_assura_lenient("Vec<Config>"), "List<Unknown>");
490        assert_eq!(
491            rust_type_to_assura_lenient("Result<i64, MyError>"),
492            "Result<Int, Unknown>"
493        );
494    }
495
496    #[test]
497    fn lenient_impl_dyn_self() {
498        assert_eq!(rust_type_to_assura_lenient("impl Iterator"), "Unknown");
499        assert_eq!(rust_type_to_assura_lenient("dyn Trait"), "Unknown");
500        assert_eq!(rust_type_to_assura_lenient("Self"), "Unknown");
501    }
502
503    #[test]
504    fn lenient_lifetime_references() {
505        assert_eq!(rust_type_to_assura_lenient("&'a str"), "String");
506        assert_eq!(rust_type_to_assura_lenient("&'_ i64"), "Int");
507    }
508
509    #[test]
510    fn lenient_pin_phantomdata() {
511        assert_eq!(
512            rust_type_to_assura_lenient("Pin<Box<dyn Future>>"),
513            "Unknown"
514        );
515        assert_eq!(rust_type_to_assura_lenient("PhantomData<T>"), "Unknown");
516    }
517
518    #[test]
519    fn lenient_wrapper_erasure_then_sanitize() {
520        assert_eq!(rust_type_to_assura_lenient("Box<i64>"), "Int");
521        assert_eq!(rust_type_to_assura_lenient("Arc<String>"), "String");
522        assert_eq!(rust_type_to_assura_lenient("Arc<Config>"), "Unknown");
523    }
524}