Skip to main content

aver/types/
mod.rs

1/// Aver static type representation and built-in type namespaces.
2///
3/// Type annotations in the AST are plain strings; this module converts them
4/// to a structured enum and provides the compatibility relation used by
5/// the type checker.
6///
7/// Sub-modules:
8/// - `checker` — static type checker
9/// - `bool`, `int`, `float`, `string`, `list`, `map`, `char`, `byte` — pure namespace helpers (no effects)
10pub mod bool;
11pub mod branch_path;
12pub mod byte;
13pub mod char;
14pub mod checker;
15pub mod effect_event;
16pub mod float;
17pub mod int;
18#[cfg(feature = "runtime")]
19pub mod list;
20#[cfg(feature = "runtime")]
21pub mod map;
22pub mod option;
23pub mod result;
24#[cfg(feature = "runtime")]
25pub mod string;
26pub mod trace;
27#[cfg(feature = "runtime")]
28pub mod vector;
29
30// `Type` lives in `crate::ast::types` so that `Spanned<T>` can carry an
31// optional `Type` annotation without a cycle through `crate::types`. This
32// re-export preserves the historical `crate::types::Type` path.
33pub use crate::ast::Type;
34
35/// Parse a type annotation string strictly.
36/// Returns `Err(unknown_name)` if the string is a non-empty identifier
37/// that does not map to a known type (i.e. a likely typo).
38/// Generic forms (`Result<...>`, `Option<...>`, `List<...>`) with valid inner types are accepted.
39pub fn parse_type_str_strict(s: &str) -> Result<Type, String> {
40    let s = s.trim();
41    if s.is_empty() || s == "Any" {
42        return Err(s.to_string());
43    }
44    if let Some(fn_ty) = parse_fn_type_strict(s)? {
45        return Ok(fn_ty);
46    }
47
48    // Paren-tuple types (`(A, B)`) were removed — `Tuple<A, B>` is
49    // the only spelling for tuple types now. Paren `(a, b)` stays as
50    // the tuple value literal in expression position.
51    if s.starts_with('(') && s.ends_with(')') {
52        return Err(s.to_string());
53    }
54
55    match s {
56        "Int" => Ok(Type::Int),
57        "Float" => Ok(Type::Float),
58        "String" | "Str" => Ok(Type::Str),
59        "Bool" => Ok(Type::Bool),
60        "Unit" => Ok(Type::Unit),
61        _ => {
62            if let Some(inner) = strip_wrapper(s, "Result<", ">") {
63                if let Some((ok_s, err_s)) = split_top_level_comma(inner) {
64                    let ok_ty = parse_type_str_strict(ok_s)?;
65                    let err_ty = parse_type_str_strict(err_s)?;
66                    return Ok(Type::Result(Box::new(ok_ty), Box::new(err_ty)));
67                }
68                return Err(s.to_string());
69            }
70            if let Some(inner) = strip_wrapper(s, "Option<", ">") {
71                let inner_ty = parse_type_str_strict(inner)?;
72                return Ok(Type::Option(Box::new(inner_ty)));
73            }
74            if let Some(inner) = strip_wrapper(s, "List<", ">") {
75                let inner_ty = parse_type_str_strict(inner)?;
76                return Ok(Type::List(Box::new(inner_ty)));
77            }
78            if let Some(inner) = strip_wrapper(s, "Map<", ">") {
79                if let Some((key_s, value_s)) = split_top_level_comma(inner) {
80                    let key_ty = parse_type_str_strict(key_s)?;
81                    if matches!(key_ty, Type::Fn(..) | Type::Unit) {
82                        return Err(s.to_string());
83                    }
84                    let value_ty = parse_type_str_strict(value_s)?;
85                    return Ok(Type::Map(Box::new(key_ty), Box::new(value_ty)));
86                }
87                return Err(s.to_string());
88            }
89            if let Some(inner) = strip_wrapper(s, "Vector<", ">") {
90                let inner_ty = parse_type_str_strict(inner)?;
91                return Ok(Type::Vector(Box::new(inner_ty)));
92            }
93            if let Some(inner) = strip_wrapper(s, "Tuple<", ">") {
94                let parts = split_top_level(inner, ',')?;
95                if parts.len() < 2 {
96                    return Err(s.to_string());
97                }
98                let elems = parts
99                    .into_iter()
100                    .map(parse_type_str_strict)
101                    .collect::<Result<Vec<_>, _>>()?;
102                return Ok(Type::Tuple(elems));
103            }
104
105            // Capitalized identifier with only alphanumeric/_ and dot chars = user-defined type name
106            // Supports dotted names like "Tcp.Connection"
107            if s.chars().next().is_some_and(|c| c.is_uppercase())
108                && s.chars()
109                    .all(|c| c.is_alphanumeric() || c == '_' || c == '.')
110            {
111                return Ok(Type::Named(s.to_string()));
112            }
113
114            Err(s.to_string())
115        }
116    }
117}
118
119/// Parse an Aver type annotation string into a `Type`.
120/// Returns `Type::Invalid` for malformed or unknown type strings (internal recovery).
121/// Prefer `parse_type_str_strict` for user-facing type annotations.
122pub fn parse_type_str(s: &str) -> Type {
123    let s = s.trim();
124    if s.starts_with("Fn(") {
125        if let Ok(Some(fn_ty)) = parse_fn_type_strict(s) {
126            return fn_ty;
127        }
128        return Type::Invalid;
129    }
130    // Paren-tuple types removed; `(...)` in type position is invalid.
131    if s.starts_with('(') && s.ends_with(')') {
132        return Type::Invalid;
133    }
134    match s {
135        "Int" => Type::Int,
136        "Float" => Type::Float,
137        "String" | "Str" => Type::Str,
138        "Bool" => Type::Bool,
139        "Unit" => Type::Unit,
140        "" => Type::Invalid,
141        _ => {
142            // Try generic forms: Result<A, B>, Option<A>, List<A>
143            if let Some(inner) = strip_wrapper(s, "Result<", ">") {
144                // Split on the first top-level comma
145                if let Some((ok_str, err_str)) = split_top_level_comma(inner) {
146                    return Type::Result(
147                        Box::new(parse_type_str(ok_str)),
148                        Box::new(parse_type_str(err_str)),
149                    );
150                }
151            }
152            if let Some(inner) = strip_wrapper(s, "Option<", ">") {
153                return Type::Option(Box::new(parse_type_str(inner)));
154            }
155            if let Some(inner) = strip_wrapper(s, "List<", ">") {
156                return Type::List(Box::new(parse_type_str(inner)));
157            }
158            if let Some(inner) = strip_wrapper(s, "Map<", ">")
159                && let Some((key_str, value_str)) = split_top_level_comma(inner)
160            {
161                return Type::Map(
162                    Box::new(parse_type_str(key_str)),
163                    Box::new(parse_type_str(value_str)),
164                );
165            }
166            if let Some(inner) = strip_wrapper(s, "Vector<", ">") {
167                return Type::Vector(Box::new(parse_type_str(inner)));
168            }
169            if let Some(inner) = strip_wrapper(s, "Tuple<", ">")
170                && let Ok(parts) = split_top_level(inner, ',')
171                && parts.len() >= 2
172            {
173                return Type::Tuple(parts.into_iter().map(parse_type_str).collect());
174            }
175            // Capitalized identifier with only alphanumeric/_ and dot chars = user-defined type
176            // Supports dotted names like "Tcp.Connection"
177            if s.chars().next().is_some_and(|c| c.is_uppercase())
178                && s.chars()
179                    .all(|c| c.is_alphanumeric() || c == '_' || c == '.')
180                && s != "Any"
181            {
182                return Type::Named(s.to_string());
183            }
184            // Invalid — internal recovery fallback
185            Type::Invalid
186        }
187    }
188}
189
190fn parse_fn_type_strict(s: &str) -> Result<Option<Type>, String> {
191    if !s.starts_with("Fn(") {
192        return Ok(None);
193    }
194
195    let close_idx = find_matching_paren(s, 2).ok_or_else(|| s.to_string())?;
196    let params_src = &s[3..close_idx];
197
198    let after_params = s[close_idx + 1..].trim_start();
199    if !after_params.starts_with("->") {
200        return Err(s.to_string());
201    }
202    let ret_and_effects = after_params[2..].trim();
203    if ret_and_effects.is_empty() {
204        return Err(s.to_string());
205    }
206
207    let (ret_src, effects) = split_fn_effects_suffix(ret_and_effects)?;
208    let ret_ty = parse_type_str_strict(ret_src)?;
209    let params = parse_type_list_strict(params_src)?;
210    Ok(Some(Type::Fn(params, Box::new(ret_ty), effects)))
211}
212
213fn parse_type_list_strict(src: &str) -> Result<Vec<Type>, String> {
214    if src.trim().is_empty() {
215        return Ok(vec![]);
216    }
217    split_top_level(src, ',')?
218        .into_iter()
219        .map(|part| {
220            let part = part.trim();
221            if part.is_empty() {
222                Err(src.to_string())
223            } else {
224                parse_type_str_strict(part)
225            }
226        })
227        .collect()
228}
229
230fn split_fn_effects_suffix(src: &str) -> Result<(&str, Vec<String>), String> {
231    if let Some(bang_idx) = find_top_level_bang(src) {
232        let ret_src = src[..bang_idx].trim();
233        if ret_src.is_empty() {
234            return Err(src.to_string());
235        }
236        let effects_src = src[bang_idx + 1..].trim();
237        if !(effects_src.starts_with('[') && effects_src.ends_with(']')) {
238            return Err(src.to_string());
239        }
240        let inner = &effects_src[1..effects_src.len() - 1];
241        let effects = if inner.trim().is_empty() {
242            vec![]
243        } else {
244            split_top_level(inner, ',')?
245                .into_iter()
246                .map(|part| {
247                    let name = part.trim();
248                    if name.is_empty() {
249                        Err(src.to_string())
250                    } else {
251                        Ok(name.to_string())
252                    }
253                })
254                .collect::<Result<Vec<_>, _>>()?
255        };
256        Ok((ret_src, effects))
257    } else {
258        Ok((src.trim(), vec![]))
259    }
260}
261
262fn find_matching_paren(s: &str, open_idx: usize) -> Option<usize> {
263    if s.as_bytes().get(open_idx).copied() != Some(b'(') {
264        return None;
265    }
266    let mut depth = 1usize;
267    for (i, ch) in s.char_indices().skip(open_idx + 1) {
268        match ch {
269            '(' => depth += 1,
270            ')' => {
271                depth -= 1;
272                if depth == 0 {
273                    return Some(i);
274                }
275            }
276            _ => {}
277        }
278    }
279    None
280}
281
282fn find_top_level_bang(s: &str) -> Option<usize> {
283    let mut angle = 0usize;
284    let mut paren = 0usize;
285    let mut bracket = 0usize;
286
287    for (i, ch) in s.char_indices() {
288        match ch {
289            '<' => angle += 1,
290            '>' => angle = angle.saturating_sub(1),
291            '(' => paren += 1,
292            ')' => paren = paren.saturating_sub(1),
293            '[' => bracket += 1,
294            ']' => bracket = bracket.saturating_sub(1),
295            '!' if angle == 0 && paren == 0 && bracket == 0 => return Some(i),
296            _ => {}
297        }
298    }
299
300    None
301}
302
303fn split_top_level(s: &str, delimiter: char) -> Result<Vec<&str>, String> {
304    let mut out = Vec::new();
305    let mut start = 0usize;
306    let mut angle = 0usize;
307    let mut paren = 0usize;
308    let mut bracket = 0usize;
309
310    for (i, ch) in s.char_indices() {
311        match ch {
312            '<' => angle += 1,
313            '>' => {
314                if angle == 0 {
315                    return Err(s.to_string());
316                }
317                angle -= 1;
318            }
319            '(' => paren += 1,
320            ')' => {
321                if paren == 0 {
322                    return Err(s.to_string());
323                }
324                paren -= 1;
325            }
326            '[' => bracket += 1,
327            ']' => {
328                if bracket == 0 {
329                    return Err(s.to_string());
330                }
331                bracket -= 1;
332            }
333            _ if ch == delimiter && angle == 0 && paren == 0 && bracket == 0 => {
334                out.push(&s[start..i]);
335                start = i + ch.len_utf8();
336            }
337            _ => {}
338        }
339    }
340
341    if angle != 0 || paren != 0 || bracket != 0 {
342        return Err(s.to_string());
343    }
344    out.push(&s[start..]);
345    Ok(out)
346}
347
348/// If `s` starts with `prefix` and ends with `suffix`, return the middle part.
349fn strip_wrapper<'a>(s: &'a str, prefix: &str, suffix: &str) -> Option<&'a str> {
350    if s.starts_with(prefix) && s.ends_with(suffix) {
351        let inner = &s[prefix.len()..s.len() - suffix.len()];
352        Some(inner)
353    } else {
354        None
355    }
356}
357
358/// Split a string on the first top-level comma (depth=0), returning the two sides.
359fn split_top_level_comma(s: &str) -> Option<(&str, &str)> {
360    let mut angle = 0usize;
361    let mut paren = 0usize;
362    let mut bracket = 0usize;
363    for (i, ch) in s.char_indices() {
364        match ch {
365            '<' => angle += 1,
366            '>' => angle = angle.saturating_sub(1),
367            '(' => paren += 1,
368            ')' => paren = paren.saturating_sub(1),
369            '[' => bracket += 1,
370            ']' => bracket = bracket.saturating_sub(1),
371            ',' if angle == 0 && paren == 0 && bracket == 0 => {
372                return Some((&s[..i], &s[i + 1..]));
373            }
374            _ => {}
375        }
376    }
377    None
378}
379
380#[cfg(test)]
381mod tests {
382    use super::*;
383
384    #[test]
385    fn test_primitives() {
386        assert_eq!(parse_type_str("Int"), Type::Int);
387        assert_eq!(parse_type_str("Float"), Type::Float);
388        assert_eq!(parse_type_str("String"), Type::Str);
389        assert_eq!(parse_type_str("Bool"), Type::Bool);
390        assert_eq!(parse_type_str("Unit"), Type::Unit);
391    }
392
393    #[test]
394    fn test_generics() {
395        assert_eq!(
396            parse_type_str("Result<Int, String>"),
397            Type::Result(Box::new(Type::Int), Box::new(Type::Str))
398        );
399        assert_eq!(
400            parse_type_str("Option<Bool>"),
401            Type::Option(Box::new(Type::Bool))
402        );
403        assert_eq!(parse_type_str("List<Int>"), Type::List(Box::new(Type::Int)));
404        assert_eq!(
405            parse_type_str("Map<String, Int>"),
406            Type::Map(Box::new(Type::Str), Box::new(Type::Int))
407        );
408        assert_eq!(
409            parse_type_str("Tuple<Int, String>"),
410            Type::Tuple(vec![Type::Int, Type::Str])
411        );
412        // Paren-tuple types are no longer valid — `Type::Invalid` is
413        // the lenient parser's recovery shape for unknown forms.
414        assert_eq!(parse_type_str("(Int, String)"), Type::Invalid);
415    }
416
417    #[test]
418    fn test_nested() {
419        assert_eq!(
420            parse_type_str("Result<Float, String>"),
421            Type::Result(Box::new(Type::Float), Box::new(Type::Str))
422        );
423    }
424
425    #[test]
426    fn test_unknown() {
427        // Capitalized identifiers are now parsed as user-defined Named types
428        assert_eq!(
429            parse_type_str("SomeUnknownType"),
430            Type::Named("SomeUnknownType".to_string())
431        );
432        // Lowercase non-keyword identifiers and empty strings become invalid recovery.
433        assert_eq!(parse_type_str(""), Type::Invalid);
434    }
435
436    #[test]
437    fn test_compatible() {
438        assert!(Type::Int.compatible(&Type::Int));
439        assert!(!Type::Int.compatible(&Type::Str));
440        assert!(!Type::Invalid.compatible(&Type::Int));
441        assert!(!Type::Int.compatible(&Type::Invalid));
442        assert!(!Type::Int.compatible(&Type::Float)); // no implicit widening
443        assert!(
444            Type::Result(Box::new(Type::Int), Box::new(Type::Str))
445                .compatible(&Type::Result(Box::new(Type::Int), Box::new(Type::Str)))
446        );
447        assert!(
448            Type::Map(Box::new(Type::Str), Box::new(Type::Int))
449                .compatible(&Type::Map(Box::new(Type::Str), Box::new(Type::Int)))
450        );
451        assert!(
452            !Type::Map(Box::new(Type::Str), Box::new(Type::Int))
453                .compatible(&Type::Map(Box::new(Type::Int), Box::new(Type::Int)))
454        );
455    }
456
457    #[test]
458    fn test_function_type_parsing() {
459        assert_eq!(
460            parse_type_str_strict("Fn(Int, String) -> Bool").unwrap(),
461            Type::Fn(vec![Type::Int, Type::Str], Box::new(Type::Bool), vec![])
462        );
463        assert_eq!(
464            parse_type_str_strict("Fn(Int) -> Int ! [Console]").unwrap(),
465            Type::Fn(
466                vec![Type::Int],
467                Box::new(Type::Int),
468                vec!["Console".to_string()]
469            )
470        );
471    }
472
473    #[test]
474    fn test_function_effect_compatibility_subset() {
475        let pure = Type::Fn(vec![Type::Int], Box::new(Type::Int), vec![]);
476        let console = Type::Fn(
477            vec![Type::Int],
478            Box::new(Type::Int),
479            vec!["Console".to_string()],
480        );
481
482        assert!(pure.compatible(&console));
483        assert!(!console.compatible(&pure));
484
485        let child = Type::Fn(
486            vec![Type::Int],
487            Box::new(Type::Int),
488            vec!["Http.get".to_string()],
489        );
490        let parent = Type::Fn(
491            vec![Type::Int],
492            Box::new(Type::Int),
493            vec!["Http".to_string()],
494        );
495        // Http.get fits where Http is expected (subset of namespace)
496        assert!(child.compatible(&parent));
497        // Http does NOT fit where Http.get is expected (might use Http.post)
498        assert!(!parent.compatible(&child));
499    }
500
501    #[test]
502    fn test_strict_parser_accepts_valid_generics() {
503        assert_eq!(
504            parse_type_str_strict("Result<Int, String>").unwrap(),
505            Type::Result(Box::new(Type::Int), Box::new(Type::Str))
506        );
507        assert_eq!(
508            parse_type_str_strict("List<Option<Float>>").unwrap(),
509            Type::List(Box::new(Type::Option(Box::new(Type::Float))))
510        );
511        assert_eq!(
512            parse_type_str_strict("Map<String, Int>").unwrap(),
513            Type::Map(Box::new(Type::Str), Box::new(Type::Int))
514        );
515        assert_eq!(
516            parse_type_str_strict("Tuple<Int, String>").unwrap(),
517            Type::Tuple(vec![Type::Int, Type::Str])
518        );
519        // Paren-tuple types are an error in the strict parser.
520        assert!(parse_type_str_strict("(Int, String)").is_err());
521    }
522
523    #[test]
524    fn test_strict_parser_accepts_user_defined_types() {
525        // Capitalized identifiers are accepted as user-defined Named types
526        assert_eq!(
527            parse_type_str_strict("Result<MyError, String>").unwrap(),
528            Type::Result(
529                Box::new(Type::Named("MyError".to_string())),
530                Box::new(Type::Str)
531            )
532        );
533        assert_eq!(
534            parse_type_str_strict("Option<Shape>").unwrap(),
535            Type::Option(Box::new(Type::Named("Shape".to_string())))
536        );
537        assert_eq!(
538            parse_type_str_strict("List<User>").unwrap(),
539            Type::List(Box::new(Type::Named("User".to_string())))
540        );
541        // Lowercase unknown types still fail
542        assert!(parse_type_str_strict("integ").is_err());
543    }
544
545    #[test]
546    fn test_dotted_named_type() {
547        assert_eq!(
548            parse_type_str("Tcp.Connection"),
549            Type::Named("Tcp.Connection".to_string())
550        );
551        assert_eq!(
552            parse_type_str_strict("Tcp.Connection").unwrap(),
553            Type::Named("Tcp.Connection".to_string())
554        );
555        assert_eq!(
556            parse_type_str_strict("Result<Tcp.Connection, String>").unwrap(),
557            Type::Result(
558                Box::new(Type::Named("Tcp.Connection".to_string())),
559                Box::new(Type::Str)
560            )
561        );
562    }
563
564    #[test]
565    fn test_strict_parser_rejects_malformed_generics() {
566        assert!(parse_type_str_strict("Result<Int>").is_err());
567        assert!(parse_type_str_strict("Option<Int, String>").is_err());
568        assert!(parse_type_str_strict("Map<Int>").is_err());
569        // List/User-type keys are now valid — runtime hashes deeply.
570        assert!(parse_type_str_strict("Map<List<Int>, String>").is_ok());
571        // Functions still cannot be hashed.
572        assert!(parse_type_str_strict("Map<Fn(Int) -> Int, String>").is_err());
573        assert!(parse_type_str_strict("(Int)").is_err());
574        assert!(parse_type_str_strict("Fn(Int) Int").is_err());
575        assert!(parse_type_str_strict("Fn(Int) -> ! [Console]").is_err());
576    }
577}